I'll check the mesh/kit APIs this generator uses before deciding on bugs.
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 );
}
}