editor/AdvancedMappingTools/Architecture/WindTurbineEditor.cs

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;
}