editor/AdvancedMappingTools/Procedural/PowerLineGenerator.cs

I'll check the mesh/kit APIs this generator uses before deciding on bugs.

Really Bad Code
namespace AdvancedMappingTools;

public enum PowerPoleStyle
{
	Wood,
	Concrete,
	[Title( "Lattice Pylon" )] Pylon
}

public sealed class PowerLineSettings : GeneratorSettings
{
	/// <summary>
	/// The line the poles follow, set by the tool.
	/// </summary>
	public PathCurve Path { get; set; } = PathCurve.FromPoints( [Vector3.Zero, new Vector3( 1024.0f, 0.0f, 0.0f )] );

	[Property, Group( "Power Line" ), Title( "Pole Spacing" ), Range( 32, 4096, slider: false ), WideMode, Description( "Distance between poles. A corner always gets its own pole." )]
	public float PoleSpacing { get; set; } = 384.0f;

	[Property, Group( "Power Line" ), Title( "Pole Style" ), WideMode]
	public PowerPoleStyle Style { get; set; } = PowerPoleStyle.Wood;

	[Property, Group( "Power Line" ), Range( 32, 2048, slider: false ), WideMode, Description( "Height of a pole above its point on the line." )]
	public float Height { get; set; } = 360.0f;

	[Property, Group( "Power Line" ), Range( 1, 6, slider: false ), WideMode, Description( "How many wires the line carries." )]
	public int Wires { get; set; } = 3;

	[Property, Group( "Power Line" ), Title( "Wire Thickness" ), Range( 0.25f, 8, slider: false ), WideMode]
	public float WireThickness { get; set; } = 0.75f;

	[Property, Group( "Power Line" ), Range( 0, 256, slider: false ), WideMode, Description( "How far a wire hangs down in the middle between two poles." )]
	public float Sag { get; set; } = 16.0f;

	[Property, Group( "Power Line" ), Title( "Crossarm Width" ), Range( 8, 512, slider: false ), WideMode]
	public float ArmWidth { get; set; } = 72.0f;

	[Property, Group( "Power Line" ), Title( "Guy Wire" ), WideMode, Description( "Anchor cables at both ends and on the outside of every corner." )]
	public bool GuyWire { get; set; } = true;

	[Property, Group( "Materials" ), Title( "Poles" ), ResourceType( "vmat" ), WideMode]
	public string PoleMaterial { get; set; }

	[Property, Group( "Materials" ), Title( "Wires" ), ResourceType( "vmat" ), WideMode]
	public string WireMaterial { get; set; }

	[Property, Group( "Power Line" ), Title( "Transformer Every" ), Range( 0, 32, slider: false ), WideMode, Description( "A transformer can on every this many poles. Zero leaves them off. Not on lattice pylons." )]
	public int TransformerEvery { get; set; } = 0;
}

/// <summary>
/// A run of power poles along a line. Every corner gets a pole, its crossarm square to the bisector.
/// Wood and concrete poles carry a crossarm on their side with braces and ribbed pin insulators, an
/// odd middle wire rides on the pole top; lattice pylons spread four legs with girts and cross
/// bracing and carry the wires on hanging insulator strings. The wires are round cables that sag
/// between the poles and rest on the insulators. No part reaches into another.
/// </summary>
public sealed class PowerLineGenerator : IPartGenerator
{
	public string Id => "powerline";
	public string Title => "Power Line";
	public Type SettingsType => typeof( PowerLineSettings );

	const string LineCategory = "powerline";
	const int WireSegments = 24;
	const int WireSides = 6;
	const float Air = 0.1f;

	public PartPlan Generate( GeneratorSettings settings, GenContext context )
	{
		var s = (PowerLineSettings)settings;
		var root = context.Group( "root", "Power Line", "Building", LineCategory );

		if ( s.Path is null || s.Path.Nodes.Count < 2 ) return root;

		var points = s.Path.Nodes.Select( x => x.Position ).ToList();
		var stations = Stations( points, MathF.Max( s.PoleSpacing, 32.0f ) );
		if ( stations.Count < 2 ) return root;

		var poleMat = context.Material( s.PoleMaterial );
		var wireMat = context.Material( s.WireMaterial );
		var height = MathF.Max( s.Height, 32.0f );
		var half = MathF.Max( s.ArmWidth, 8.0f ) * 0.5f;
		var wires = Math.Clamp( s.Wires, 1, 6 );
		var r = MathF.Max( s.WireThickness, 0.25f ) * 0.5f;
		var pylon = s.Style == PowerPoleStyle.Pylon;

		// Where each wire rests on each pole.
		var rests = new Vector3[stations.Count, wires];

		for ( var i = 0; i < stations.Count; i++ )
		{
			var at = stations[i];
			var forward = Along( stations, i );
			var across = new Vector3( -forward.y, forward.x, 0.0f );

			if ( pylon ) Pylon( context, root, i, at, forward, across, height, half, wires, r, poleMat, wireMat, rests );
			else Pole( s, context, root, i, at, forward, across, height, half, wires, r, poleMat, wireMat, rests, s.GuyWire && (i == 0 || i + 1 == stations.Count || (i > 0 && i + 1 < stations.Count && MathF.Abs( Turn( stations, i ) ) > 0.2f)) );

			if ( s.GuyWire )
			{
				var turn = i > 0 && i + 1 < stations.Count ? Turn( stations, i ) : 0.0f;
				if ( i == 0 ) Guy( context, root, $"guy{i:00}", at, -forward, height, Radius( s, height, height * 0.85f ), wireMat, pylon );
				else if ( i + 1 == stations.Count ) Guy( context, root, $"guy{i:00}", at, forward, height, Radius( s, height, height * 0.85f ), wireMat, pylon );
				else if ( MathF.Abs( turn ) > 0.2f )
				{
					// The pull of a corner points inside the turn; the guy holds against it from outside.
					var inside = (Along( stations, i, after: true ) - Along( stations, i, before: true )).WithZ( 0 ).Normal;
					Guy( context, root, $"guy{i:00}", at, -inside, height, Radius( s, height, height * 0.85f ), wireMat, pylon );
				}
			}
		}

		for ( var w = 0; w < wires; w++ )
		{
			var ends = Enumerable.Range( 0, stations.Count ).Select( i => rests[i, w] ).ToList();
			root.Add( context.Solid( $"wire{w}", $"Wire {w + 1}", "PowerWire", LineCategory, wireMat, kit => Cable( kit, ends, MathF.Max( s.Sag, 0.0f ), r ) ) );
		}

		return root;
	}

	static Vector3 Along( List<Vector3> stations, int i, bool before = false, bool after = false )
	{
		var a = stations[Math.Max( i - 1, 0 )];
		var b = stations[Math.Min( i + 1, stations.Count - 1 )];
		if ( before ) b = stations[i];
		if ( after ) a = stations[i];
		var d = (b - a).WithZ( 0.0f );
		return d.LengthSquared < 1e-6f ? Vector3.Forward : d.Normal;
	}

	static float Turn( List<Vector3> stations, int i )
	{
		var a = Along( stations, i, before: true );
		var b = Along( stations, i, after: true );
		return MathF.Acos( Math.Clamp( Vector3.Dot( a, b ), -1.0f, 1.0f ) );
	}

	/// <summary>
	/// Poles along the line: every drawn point, and the spacing in between.
	/// </summary>
	static List<Vector3> Stations( IReadOnlyList<Vector3> points, float spacing )
	{
		var stations = new List<Vector3>();

		void Add( Vector3 point )
		{
			if ( stations.Count == 0 || stations[^1].WithZ( 0.0f ).Distance( point.WithZ( 0.0f ) ) > 0.5f )
				stations.Add( point );
		}

		Add( points[0] );

		for ( var i = 0; i + 1 < points.Count; i++ )
		{
			var a = points[i];
			var b = points[i + 1];
			var length = a.WithZ( 0.0f ).Distance( b.WithZ( 0.0f ) );
			if ( length < 0.01f ) continue;

			var n = Math.Max( 1, (int)MathF.Round( length / spacing ) );
			for ( var k = 1; k < n; k++ ) Add( Vector3.Lerp( a, b, k / (float)n ) );
			Add( b );
		}

		return stations;
	}

	/// <summary>
	/// Wire positions across the arm; with an odd count on a pole the middle one goes on the pole top.
	/// </summary>
	static float Offset( int index, int wires, float half ) => wires <= 1 ? 0.0f : (index / (float)(wires - 1) - 0.5f) * half * 2.0f;

	static float Radius( PowerLineSettings s, float height, float z )
	{
		var bottom = s.Style == PowerPoleStyle.Concrete ? 7.0f : 6.0f;
		var top = s.Style == PowerPoleStyle.Concrete ? 4.0f : 4.5f;
		return bottom + (top - bottom) * Math.Clamp( z / height, 0.0f, 1.0f );
	}

	static void Pole( PowerLineSettings s, GenContext context, PartPlan root, int i, Vector3 at, Vector3 forward, Vector3 across, float height, float half, int wires, float r,
		Material poleMat, Material wireMat, Vector3[,] rests, bool guyed )
	{
		var id = $"pole{i:00}";
		var top = at.z + height;
		// The arm sits at the very top, so the wires run above the pole and never through it at a corner.
		var armZ = height - 2.0f;
		var armRadius = Radius( s, height, armZ );
		var concrete = s.Style == PowerPoleStyle.Concrete;

		void Part( string part, string name, string kind, Material m, Action<MeshKit> build ) =>
			root.Add( context.Solid( $"{id}/{part}", name, kind, LineCategory, m, kit => { kit.LiftFlushFaces = false; build( kit ); } ) );

		// The pole: round and tapering, a concrete one square and tapering.
		Part( "pole", "Pole", "PowerPole", poleMat, kit =>
		{
			if ( !concrete ) { kit.Cylinder( at.WithZ( 0.0f ), Radius( s, height, 0 ), Radius( s, height, height ), at.z, top, 16 ); return; }
			Vector3 C( float a, float b, float z, float rr ) => at + forward * a * rr + across * b * rr + Vector3.Up * z;
			var r0 = Radius( s, height, 0 ) * 0.88f;
			var r1 = Radius( s, height, height ) * 0.88f;
			kit.Hex( C( -1, -1, 0, r0 ), C( 1, -1, 0, r0 ), C( 1, 1, 0, r0 ), C( -1, 1, 0, r0 ), C( -1, -1, height, r1 ), C( 1, -1, height, r1 ), C( 1, 1, height, r1 ), C( -1, 1, height, r1 ) );
		} );

		// The crossarm hangs on the side of the pole, just clear of its surface.
		var armFront = armRadius + Air;
		Vector3 A( float along, float side, float z ) => at + forward * along + across * side + Vector3.Up * z;
		Part( "arm", "Crossarm", "PowerArm", poleMat, kit =>
			kit.Hex( A( armFront, -half, armZ - 2.0f ), A( armFront + 4.0f, -half, armZ - 2.0f ), A( armFront + 4.0f, half, armZ - 2.0f ), A( armFront, half, armZ - 2.0f ),
				A( armFront, -half, armZ + 2.0f ), A( armFront + 4.0f, -half, armZ + 2.0f ), A( armFront + 4.0f, half, armZ + 2.0f ), A( armFront, half, armZ + 2.0f ) ) );

		// Two braces from under the arm down to the pole face.
		Part( "braces", "Braces", "PowerArm", poleMat, kit =>
		{
			var low = armZ - half * 0.55f;
			var lowRadius = Radius( s, height, low );
			foreach ( var side in new[] { -1.0f, 1.0f } )
				kit.Beam( A( armFront + 2.0f, side * half * 0.55f, armZ - 2.6f ), A( lowRadius + 1.2f, side * 1.2f, low ), 1.0f, 1.0f );
		} );

		var odd = wires % 2 == 1;
		for ( var w = 0; w < wires; w++ )
		{
			var offset = Offset( w, wires, half * 0.9f );
			var onTop = odd && w == wires / 2;
			var foot = onTop ? at + Vector3.Up * height : A( armFront + 2.0f, offset, armZ + 2.0f );
			var stack = 7.0f;
			Part( $"insulator{w}", "Insulator", "PowerInsulator", wireMat, kit => Insulator( kit, foot, stack ) );
			rests[i, w] = foot + Vector3.Up * (stack + r + Air);
		}

		if ( s.TransformerEvery > 0 && i % s.TransformerEvery == 0 && !guyed )
		{
			var tz = armZ - half * 0.55f - 34.0f;
			var face = Radius( s, height, tz + 12.0f ) + Air;
			// A plate on the back of the pole, the can hangs against it.
			Part( "transformer_bracket", "Transformer Bracket", "PowerArm", poleMat, kit =>
				kit.Hex( A( -face - 1.0f, -6.0f, tz ), A( -face, -6.0f, tz ), A( -face, 6.0f, tz ), A( -face - 1.0f, 6.0f, tz ),
					A( -face - 1.0f, -6.0f, tz + 24.0f ), A( -face, -6.0f, tz + 24.0f ), A( -face, 6.0f, tz + 24.0f ), A( -face - 1.0f, 6.0f, tz + 24.0f ) ) );
			Part( "transformer", "Transformer", "PowerTransformer", poleMat, kit =>
			{
				var center = A( -face - 1.0f - 8.0f - Air, 0.0f, 0.0f );
				kit.Cylinder( center.WithZ( 0.0f ), 8.0f, 8.0f, tz, tz + 22.0f, 16 );
				kit.Cylinder( center.WithZ( 0.0f ), 8.0f, 6.0f, tz + 22.0f, tz + 25.0f, 16 );
			} );
		}
	}

	/// <summary>
	/// A pin insulator standing on <paramref name="foot"/>: a pin and three ribs getting smaller.
	/// </summary>
	static void Insulator( MeshKit kit, Vector3 foot, float height )
	{
		var c = foot.WithZ( 0.0f );
		kit.Lathe( c, [new( 0.0f, foot.z ), new( 0.8f, foot.z ), new( 0.8f, foot.z + 1.5f ), new( 2.6f, foot.z + 2.0f ), new( 1.2f, foot.z + 3.0f ),
			new( 2.2f, foot.z + 3.6f ), new( 1.1f, foot.z + 4.6f ), new( 1.8f, foot.z + 5.2f ), new( 1.0f, foot.z + 6.2f ), new( 0.0f, foot.z + height )], 12 );
	}

	static void Pylon( GenContext context, PartPlan root, int i, Vector3 at, Vector3 forward, Vector3 across, float height, float half, int wires, float r,
		Material poleMat, Material wireMat, Vector3[,] rests )
	{
		var id = $"pole{i:00}";
		var baseHalf = MathF.Max( height * 0.09f, 12.0f );
		var topHalf = baseHalf * 0.3f;
		const float member = 2.0f;
		Vector3 Leg( float a, float b, float z ) { var h = baseHalf + (topHalf - baseHalf) * (z / height); return at + forward * a * h + across * b * h + Vector3.Up * z; }

		// All members of a pylon are one welded part: legs, girts and cross bracing.
		root.Add( context.Solid( $"{id}/pylon", "Pylon", "PowerPylon", LineCategory, poleMat, kit =>
		{
			kit.LiftFlushFaces = false;
			var corners = new[] { (-1.0f, -1.0f), (1.0f, -1.0f), (1.0f, 1.0f), (-1.0f, 1.0f) };
			foreach ( var (a, b) in corners ) kit.Beam( Leg( a, b, 0.0f ), Leg( a, b, height - member * 0.75f ), member * 1.5f, member * 1.5f );

			const int levels = 6;
			for ( var k = 1; k <= levels; k++ )
			{
				var z0 = height * (k - 1) / levels;
				var z1 = k == levels ? height - member * 0.5f : height * k / levels;
				for ( var c = 0; c < 4; c++ )
				{
					var (a0, b0) = corners[c];
					var (a1, b1) = corners[(c + 1) % 4];
					kit.Beam( Leg( a0, b0, z1 ), Leg( a1, b1, z1 ), member, member );
					kit.Beam( Leg( a0, b0, z0 ), Leg( a1, b1, z1 ), member * 0.7f, member * 0.7f );
					kit.Beam( Leg( a1, b1, z0 ), Leg( a0, b0, z1 ), member * 0.7f, member * 0.7f );
				}
			}

			// The crossarm: two members out to each side, meeting at the tip past the outermost wire.
			var armZ = height * 0.86f;
			var reach = MathF.Max( half, (baseHalf + (topHalf - baseHalf) * ((armZ - 17.0f) / height)) * 2.0f + 6.0f + ((wires - 1) / 2) * 12.0f + 6.0f );
			foreach ( var side in new[] { -1.0f, 1.0f } )
			{
				kit.Beam( Leg( 0, side, armZ ), at + across * side * reach + Vector3.Up * armZ, member, member );
				kit.Beam( Leg( 0, side, armZ + height * 0.05f ), at + across * side * reach + Vector3.Up * armZ, member * 0.7f, member * 0.7f );
			}
		} ) );

		// Insulator strings hang from the arm, the wires ride at their lower end. The wires take turns
		// on the two sides and stay more than a tower width out: leaving a corner pylon at an angle,
		// a wire closer in would run through the tower.
		var armLow = height * 0.86f - member * 0.5f;
		const float length = 16.0f;
		var towerHalf = baseHalf + (topHalf - baseHalf) * ((armLow - length) / height);
		var first = towerHalf * 2.0f + 6.0f;
		for ( var w = 0; w < wires; w++ )
		{
			var side = w % 2 == 0 ? 1.0f : -1.0f;
			var offset = side * (first + (w / 2) * 12.0f);
			var hang = at + across * offset + Vector3.Up * armLow;
			root.Add( context.Solid( $"{id}/string{w}", "Insulator String", "PowerInsulator", LineCategory, wireMat, kit =>
			{
				var c = hang.WithZ( 0.0f );
				var profile = new List<Vector2> { new( 0.0f, hang.z - length ) };
				for ( var k = 0; k < 6; k++ )
				{
					var z = hang.z - length + 1.0f + k * (length - 2.0f) / 6.0f;
					profile.Add( new( 0.6f, z ) );
					profile.Add( new( 2.4f, z + 0.6f ) );
					profile.Add( new( 0.6f, z + 1.3f ) );
				}
				profile.Add( new( 0.6f, hang.z - Air ) );
				profile.Add( new( 0.0f, hang.z - Air ) );
				kit.Lathe( c, profile, 10 );
			} ) );
			rests[i, w] = hang + Vector3.Down * (length + r + Air);
		}
	}

	/// <summary>
	/// A guy cable from high on the pole down and out to a ground anchor.
	/// </summary>
	static void Guy( GenContext context, PartPlan root, string id, Vector3 at, Vector3 outward, float height, float radius, Material material, bool pylon )
	{
		if ( pylon ) return;
		outward = outward.WithZ( 0.0f ).Normal;
		var anchor = at + outward * height * 0.35f;
		const float cable = 0.4f;
		var start = at + outward * (radius + cable + Air * 3.0f) + Vector3.Up * (height * 0.85f);

		root.Add( context.Solid( id, "Guy Wire", "PowerGuy", LineCategory, material, kit => Cable( kit, [start, anchor + Vector3.Up * 6.0f], 0.0f, cable ) ) );
		root.Add( context.Solid( $"{id}_anchor", "Guy Anchor", "PowerGuy", LineCategory, material, kit =>
			kit.Box( anchor + new Vector3( -4.0f, -4.0f, 0.0f ), anchor + new Vector3( 4.0f, 4.0f, 6.0f - cable - Air ) ) ) );
	}

	/// <summary>
	/// A round cable through rest points, hanging by <paramref name="sag"/> in the middle of every span.
	/// </summary>
	static void Cable( MeshKit kit, IReadOnlyList<Vector3> rests, float sag, float radius )
	{
		var points = new List<Vector3>();
		for ( var i = 0; i + 1 < rests.Count; i++ )
		{
			var steps = sag > 0.0f ? WireSegments : 1;
			for ( var k = i == 0 ? 0 : 1; k <= steps; k++ )
			{
				var t = k / (float)steps;
				points.Add( Vector3.Lerp( rests[i], rests[i + 1], t ) + Vector3.Down * (sag * 4.0f * t * (1.0f - t)) );
			}
		}

		var rings = new Vector3[WireSides][];
		for ( var p = 0; p < WireSides; p++ ) rings[p] = new Vector3[points.Count];

		for ( var k = 0; k < points.Count; k++ )
		{
			var tangent = (points[Math.Min( k + 1, points.Count - 1 )] - points[Math.Max( k - 1, 0 )]).Normal;
			var side = Vector3.Cross( tangent, Vector3.Up );
			side = side.LengthSquared < 1e-6f ? Vector3.Left : side.Normal;
			var up = Vector3.Cross( side, tangent ).Normal;

			for ( var p = 0; p < WireSides; p++ )
			{
				// Clockwise around the direction of travel, the way the sweep closes a body outward.
				var a = -MathF.PI * 2.0f * p / WireSides;
				rings[p][k] = points[k] + side * (MathF.Cos( a ) * radius) + up * (MathF.Sin( a ) * radius);
			}
		}

		kit.Sweep( rings, false, true );
	}
}