Editor tool that places a procedural wind turbine prop. A settings class and generator build the tower sections, foundation, door, nacelle, spinner and lofted airfoil blades with MeshKit, plus the primitive tool that drives them.
namespace AdvancedMappingTools;
public sealed class WindTurbineSettings : PropSettings
{
[Property, Group( "Wind Turbine" ), Title( "Tower Height" ), Range( 32, 4096, slider: false ), WideMode, Description( "Height of the hub above the ground." )]
public float TowerHeight { get; set; } = 720.0f;
[Property, Group( "Wind Turbine" ), Title( "Rotor Diameter" ), Range( 16, 4096, slider: false ), WideMode]
public float RotorDiameter { get; set; } = 560.0f;
[Property, Group( "Wind Turbine" ), Title( "Blade Angle" ), Range( 0, 360, slider: false ), WideMode, Description( "Rotation of the blades around the hub." )]
public float BladeAngle { get; set; } = 0.0f;
[Property, Group( "Wind Turbine" ), WideMode, Description( "An octagonal concrete pad with a plinth under the tower." )]
public bool Foundation { get; set; } = true;
[Property, Group( "Wind Turbine" ), Title( "Blades" ), Range( 2, 5, slider: false ), WideMode]
public int BladeCount { get; set; } = 3;
[Property, Group( "Wind Turbine" ), Title( "Tower Sections" ), Range( 1, 6, slider: false ), WideMode, Description( "Steel sections of the tower, with a flange ring between two." )]
public int TowerSections { get; set; } = 3;
[Property, Group( "Wind Turbine" ), Title( "Door" ), WideMode, Description( "A door with a step at the foot of the tower." )]
public bool Door { get; set; } = true;
[Property, Group( "Wind Turbine" ), Title( "Instruments" ), WideMode, Description( "Cooler, wind vane and anemometer on the nacelle." )]
public bool Instruments { get; set; } = true;
public override string PropName => "Wind Turbine";
}
/// <summary>
/// A modern wind turbine. A tapered steel tower in sections with flange rings, a door and a step
/// at its foot on a concrete pad, a yaw bearing, a streamlined nacelle with cooler and instruments,
/// the main shaft, a spinner with a pointed nose and blades with a real airfoil that taper and
/// twist from a round root to a thin tip. The rotor faces -X. No part reaches into another.
/// </summary>
public sealed class WindTurbineGenerator : PropGenerator
{
public override string Id => "windturbine";
public override string Title => "Wind Turbine";
public override Type SettingsType => typeof( WindTurbineSettings );
const int TowerSides = 32;
const int BladeSections = 14;
const int AirfoilPoints = 16;
protected override void Build( PropSettings settings, PropBuilder b )
{
var s = (WindTurbineSettings)settings;
var scale = MathF.Max( s.Scale, 0.01f );
var height = MathF.Max( s.TowerHeight, 32.0f );
var radius = MathF.Max( s.RotorDiameter, 16.0f ) * 0.5f;
// Tower: radius from the height, narrower at the top.
var baseR = MathF.Max( height * 0.032f, 6.0f );
var topR = baseR * 0.58f;
float TowerR( float z, float z0 ) => baseR + (topR - baseR) * Math.Clamp( (z - z0) / (height - z0), 0.0f, 1.0f );
var ground = 0.0f;
if ( s.Foundation )
{
b.Round( "foundation", "Foundation", b.Main, 0, 0, baseR * 2.6f, baseR * 2.6f, 0.0f, baseR * 0.4f, 8 );
b.Round( "plinth", "Plinth", b.Main, 0, 0, baseR + 3.0f, baseR + 3.0f, baseR * 0.4f, baseR * 0.4f + 6.0f, TowerSides );
ground = baseR * 0.4f + 6.0f;
}
// Nacelle size from the rotor, so a big rotor gets a big housing.
var nl = MathF.Max( radius * 0.36f, baseR * 3.0f );
var nh = nl * 0.36f;
var nw = nh * 0.5f;
var bearing = 4.0f;
var towerTop = height - nh * 0.55f - bearing;
var z0 = towerTop + bearing;
// Sections with a flange ring between two.
var sections = Math.Clamp( s.TowerSections, 1, 6 );
const float flange = 1.5f;
for ( var i = 0; i < sections; i++ )
{
var from = ground + (towerTop - ground) * i / sections + (i > 0 ? flange : 0.0f);
var to = ground + (towerTop - ground) * (i + 1) / sections;
b.Round( $"tower{i}", $"Tower Section {i + 1}", b.Main, 0, 0, TowerR( from, ground ), TowerR( to, ground ), from, to, TowerSides );
if ( i + 1 < sections )
b.Round( $"flange{i}", "Flange Ring", b.Metal, 0, 0, TowerR( to, ground ) + 1.0f, TowerR( to, ground ) + 1.0f, to, to + flange, TowerSides );
}
b.Round( "yaw_bearing", "Yaw Bearing", b.Metal, 0, 0, topR + 2.0f, topR + 2.0f, towerTop, z0, TowerSides );
if ( s.Door && baseR >= 12.0f )
{
var sill = ground + 2.0f;
var doorHalf = MathF.Min( baseR * 0.35f, 18.0f );
var face = -(baseR + 0.3f);
b.Box( "door", "Door", b.Metal, new Vector3( -doorHalf, face - 1.0f, sill ), new Vector3( doorHalf, face, sill + MathF.Min( 80.0f, height * 0.2f ) ) );
if ( s.Foundation )
b.Box( "door_step", "Door Step", b.Main, new Vector3( -doorHalf - 4.0f, -(baseR + 3.0f) - 14.0f, baseR * 0.4f ), new Vector3( doorHalf + 4.0f, -(baseR + 3.0f), ground ) );
}
// The nacelle seen from the side, front at -X, extruded across its width.
var front = -nl * 0.30f;
b.Side( "nacelle", "Nacelle", b.Main,
[
new( front + nl * 0.04f, z0 ), new( nl * 0.64f, z0 ), new( nl * 0.70f, z0 + nh * 0.22f ), new( nl * 0.68f, z0 + nh * 0.88f ),
new( nl * 0.62f, z0 + nh ), new( front + nl * 0.08f, z0 + nh ), new( front, z0 + nh * 0.78f ), new( front, z0 + nh * 0.18f ),
], -nw, nw );
if ( s.Instruments )
{
var roof = z0 + nh;
b.Box( "cooler", "Cooler", b.Metal, new Vector3( nl * 0.38f, -nw * 0.6f, roof ), new Vector3( nl * 0.58f, nw * 0.6f, roof + nh * 0.18f ) );
b.Round( "mast", "Instrument Mast", b.Metal, nl * 0.30f, 0, 0.8f, 0.8f, roof, roof + nh * 0.45f, 8 );
b.Box( "wind_vane", "Wind Vane", b.Metal, new Vector3( nl * 0.30f + 0.8f, -0.25f, roof + nh * 0.40f ), new Vector3( nl * 0.30f + 8.0f, 0.25f, roof + nh * 0.44f ) );
b.Box( "anemometer", "Anemometer", b.Metal, new Vector3( nl * 0.30f - 4.0f, -4.0f, roof + nh * 0.45f ), new Vector3( nl * 0.30f + 4.0f, 4.0f, roof + nh * 0.45f + 0.8f ) );
}
// Rotor axis through the hub, shaft from the nacelle front to the spinner.
var hubZ = height;
var spinnerR = MathF.Max( nh * 0.42f, 3.0f );
var rootR = spinnerR * 0.42f;
var spinnerLength = rootR * 3.4f;
var shaftLength = nl * 0.08f;
var spinnerBack = front - shaftLength;
var hubX = spinnerBack - spinnerLength * 0.5f;
var spinnerFront = spinnerBack - spinnerLength;
// Round bodies along X: a vertical turned shape laid on its side.
void AlongX( string id, string label, Material m, Action<MeshKit> build ) => b.Part( id, label, m, kit =>
{
kit.Frame = new Transform( Vector3.Zero, Rotation.Identity, scale ).ToWorld( new Transform( new Vector3( 0, 0, hubZ ), Rotation.FromPitch( -90.0f ) ) );
build( kit );
} );
// In that frame local z runs along -X from the hub height: z = -x.
AlongX( "shaft", "Main Shaft", b.Metal, kit => kit.Cylinder( Vector3.Zero, spinnerR * 0.38f, spinnerR * 0.38f, -front, -spinnerBack, 16 ) );
AlongX( "spinner", "Spinner", b.Detail, kit => kit.Cylinder( Vector3.Zero, spinnerR, spinnerR, -spinnerBack, -spinnerFront, 24 ) );
AlongX( "nose", "Nose Cone", b.Detail, kit => kit.Lathe( Vector3.Zero,
[new( 0.0f, -spinnerFront ), new( spinnerR, -spinnerFront ), new( spinnerR * 0.92f, -spinnerFront + spinnerR * 0.35f ), new( spinnerR * 0.72f, -spinnerFront + spinnerR * 0.75f ),
new( spinnerR * 0.42f, -spinnerFront + spinnerR * 1.05f ), new( 0.0f, -spinnerFront + spinnerR * 1.25f )], 24 ) );
// Blades start just outside the spinner and run to the rotor radius.
var count = Math.Clamp( s.BladeCount, 2, 5 );
var length = radius - spinnerR - 0.3f;
if ( length < 4.0f ) return;
for ( var i = 0; i < count; i++ )
{
var angle = (s.BladeAngle + i * 360.0f / count).DegreeToRadian();
b.Part( $"blade{i}", $"Blade {i + 1}", b.Detail, kit => Blade( kit, new Vector3( hubX, 0, hubZ ), angle, spinnerR + 0.3f, length, rootR, radius ) );
}
}
/// <summary>
/// One blade lofted through sections: a circle at the root, an airfoil from a fifth of the length
/// on, the chord widest there and narrowing to the tip, twisted flatter towards the tip and
/// leaning a little forward.
/// </summary>
static void Blade( MeshKit kit, Vector3 hub, float angle, float start, float length, float rootR, float radius )
{
var axis = new Vector3( -1, 0, 0 );
var radial = (new Vector3( 0, MathF.Cos( angle ), MathF.Sin( angle ) ) + axis * 0.035f).Normal;
var tangent = Vector3.Cross( axis, radial ).Normal;
var maxChord = MathF.Max( radius * 0.075f, rootR * 2.2f );
var tipChord = maxChord * 0.25f;
var rings = new Vector3[AirfoilPoints][];
for ( var p = 0; p < AirfoilPoints; p++ ) rings[p] = new Vector3[BladeSections];
for ( var k = 0; k < BladeSections; k++ )
{
var t = k / (float)(BladeSections - 1);
var round = Math.Clamp( 1.0f - t / 0.2f, 0.0f, 1.0f );
var chord = t < 0.2f ? rootR * 2.0f + (maxChord - rootR * 2.0f) * (t / 0.2f) : maxChord + (tipChord - maxChord) * ((t - 0.2f) / 0.8f);
var thick = t < 0.2f ? 0.5f : 0.21f + (0.12f - 0.21f) * ((t - 0.2f) / 0.8f);
var twist = (14.0f * (1.0f - Math.Clamp( (t - 0.2f) / 0.8f, 0.0f, 1.0f ))).DegreeToRadian();
var along = tangent * MathF.Cos( twist ) + axis * MathF.Sin( twist );
// The profile has to run the same way round as the sweep expects, or the blade turns inside out.
var normal = Vector3.Cross( along, radial ).Normal;
var center = hub + radial * (start + length * t);
for ( var p = 0; p < AirfoilPoints; p++ )
{
var theta = MathF.PI * 2.0f * p / AirfoilPoints;
// Airfoil: cosine spaced along the chord, NACA thickness, leading edge first.
var x = (1.0f - MathF.Cos( theta )) * 0.5f;
var sign = theta <= MathF.PI ? 1.0f : -1.0f;
var yt = 5.0f * thick * (0.2969f * MathF.Sqrt( x ) - 0.1260f * x - 0.3516f * x * x + 0.2843f * x * x * x - 0.1036f * x * x * x * x);
var foil = new Vector2( (x - 0.3f) * chord, sign * yt * chord );
var circle = new Vector2( -MathF.Cos( theta ) * rootR, MathF.Sin( theta ) * rootR );
var q = round >= 1.0f ? circle : Vector2.Lerp( foil, circle, round );
rings[p][k] = center + along * q.x + normal * q.y;
}
}
kit.Sweep( rings, false, true );
}
}
/// <summary>
/// Places a wind turbine. One click, the tower stands with its rotor facing -X.
/// </summary>
[ToolCategory( "Industrial" )]
[Title( "Wind Turbine" ), Icon( "wind_power" )]
public sealed class WindTurbineEditor( PrimitiveTool tool ) : PropEditor( tool )
{
readonly WindTurbineGenerator _generator = new();
public WindTurbineSettings Settings { get; } = new();
protected override IPartGenerator Generator => _generator;
public override PropSettings Props => Settings;
}