Editor tool that generates a cell tower prop. Builds a tapered lattice mast or monopole with ring platforms, panel antennas, microwave dishes, a ladder, equipment cabinets, a chain-link fence and a beacon, all from a settings object. Placed via a primitive tool.
namespace AdvancedMappingTools;
public enum CellTowerStyle
{
[Title( "Lattice Mast" )] Lattice,
[Title( "Monopole" )] Monopole
}
public sealed class CellTowerSettings : PropSettings
{
[Property, Group( "Cell Tower" ), Range( 32, 512, slider: false ), WideMode]
public float Height { get; set; } = 240.0f;
[Property, Group( "Cell Tower" ), Title( "Build" ), WideMode]
public CellTowerStyle Style { get; set; } = CellTowerStyle.Lattice;
[Property, Group( "Cell Tower" ), Title( "Platforms" ), Range( 1, 5, slider: false ), WideMode, Description( "Antenna platforms from the top down, as many as fit the upper part of the mast." )]
public int Platforms { get; set; } = 3;
[Property, Group( "Cell Tower" ), Range( 0, 8, slider: false ), WideMode, Description( "Panel antennas on each platform." )]
public int Antennas { get; set; } = 3;
[Property, Group( "Cell Tower" ), WideMode, Description( "A chain-link fence round the foot." )]
public bool Fence { get; set; } = true;
[Property, Group( "Cell Tower" ), Title( "Dishes" ), Range( 0, 4, slider: false ), WideMode, Description( "Microwave dishes below the platforms, where there is room." )]
public int Dishes { get; set; } = 2;
[Property, Group( "Cell Tower" ), Title( "Ladder" ), WideMode]
public bool Ladder { get; set; } = true;
[Property, Group( "Cell Tower" ), Title( "Cabinets" ), WideMode, Description( "Equipment cabinets on a plinth next to the mast." )]
public bool Cabinets { get; set; } = true;
[Property, Group( "Cell Tower" ), Title( "Beacon" ), WideMode, Description( "A lightning rod and an obstruction light on top." )]
public bool Beacon { get; set; } = true;
public override string PropName => "Cell Tower";
}
/// <summary>
/// A cell tower. A tapered lattice mast with struts and crossed braces on a concrete pad, or a
/// tapered monopole on a base plate. Ring platforms with railings and supports carry panel
/// antennas on arms, microwave dishes hang below, a ladder leads up, cabinets and a chain-link
/// fence stand at the foot. No part reaches into another.
/// </summary>
public sealed class CellTowerGenerator : PropGenerator
{
public override string Id => "celltower";
public override string Title => "Cell Tower";
public override Type SettingsType => typeof( CellTowerSettings );
const int Sides = 32;
public const float Pad = 4.0f;
public const float PlatformSpacing = 44.0f;
const float DeckWidth = 22.0f;
const float RailHeight = 40.0f;
const float Air = 0.05f;
/// <summary>Heights of the platform decks, from the top down.</summary>
public static List<float> PlatformLevels( CellTowerSettings s )
{
var height = MathF.Max( s.Height, 32.0f );
var levels = Enumerable.Range( 0, Math.Clamp( s.Platforms, 1, 5 ) ).Select( i => height - 8.0f - i * PlatformSpacing ).ToList();
var kept = levels.Where( z => z >= MathF.Max( height * 0.35f, 30.0f ) ).ToList();
return kept.Count > 0 ? kept : [levels[0]];
}
protected override void Build( PropSettings settings, PropBuilder b )
{
var s = (CellTowerSettings)settings;
var height = MathF.Max( s.Height, 32.0f );
var lattice = s.Style == CellTowerStyle.Lattice;
var levels = PlatformLevels( s );
// The mast: half width of the lattice, or radius of the pole, at a height.
var foot = MathF.Max( 14.0f, height * 0.06f );
var top = MathF.Max( 4.0f, foot * 0.3f );
float Half( float z ) => foot + (top - foot) * Math.Clamp( (z - Pad) / (height - Pad), 0.0f, 1.0f );
var r0 = MathF.Max( 6.0f, height * 0.03f );
var poleBase = Pad + 2.0f;
float Radius( float z ) => r0 + (r0 * 0.5f - r0) * Math.Clamp( (z - poleBase) / (height - poleBase), 0.0f, 1.0f );
var corners = new[] { new Vector3( -1, -1, 0 ), new Vector3( 1, -1, 0 ), new Vector3( 1, 1, 0 ), new Vector3( -1, 1, 0 ) };
Vector3 Leg( int k, float z ) => (corners[k % 4] * Half( z )).WithZ( z );
Vector3 Out( int k ) => corners[k % 4].Normal;
// Where a member may start next to the mast at a height, along a direction out of it.
Vector3 Clear( int k, float z ) => lattice ? Leg( k, z ) + Out( k ) * 2.8f : (Out( k ) * (Radius( z - 1.0f ) + 1.0f)).WithZ( z );
float Around( float z ) => lattice ? Half( z ) * MathF.Sqrt( 2.0f ) + 3.5f : Radius( z - 2.0f ) + 1.0f;
b.Box( "pad", "Foundation", b.Main, new Vector3( -foot - 6, -foot - 6, 0 ), new Vector3( foot + 6, foot + 6, Pad ) );
if ( lattice ) Lattice( b, height, foot, top, Leg );
else
{
b.Round( "base_plate", "Base Plate", b.Metal, 0, 0, r0 + 6.0f, r0 + 6.0f, Pad, poleBase, Sides );
b.Part( "anchor_bolts", "Anchor Bolts", b.Metal, kit =>
{
for ( var k = 0; k < 8; k++ )
{
var a = MathF.PI * 2.0f * (k + 0.5f) / 8;
kit.Cylinder( new Vector3( MathF.Cos( a ), MathF.Sin( a ), 0 ) * (r0 + 3.5f), 0.8f, 0.8f, poleBase, poleBase + 2.0f, 8 );
}
} );
b.Round( "mast", "Monopole", b.Main, 0, 0, Radius( poleBase ), Radius( height ), poleBase, height, Sides );
}
for ( var i = 0; i < levels.Count; i++ )
Platform( b, s, i, levels[i], Around( levels[i] ), Clear, lattice ? Pad + 4.0f : poleBase + 4.0f );
var dishAt = MathF.Min( height * 0.55f, levels[^1] - 56.0f );
if ( s.Dishes > 0 && dishAt >= 24.0f )
b.Part( "dish_arms", "Dish Arms", b.Metal, kit =>
{
for ( var k = 0; k < Math.Clamp( s.Dishes, 0, 4 ); k++ )
{
var from = Clear( k, dishAt );
kit.Beam( from, from + Out( k ) * 12.0f, 1.2f, 1.2f );
}
} );
if ( s.Dishes > 0 && dishAt >= 24.0f )
b.Part( "dishes", "Microwave Dishes", b.Detail, kit =>
{
for ( var k = 0; k < Math.Clamp( s.Dishes, 0, 4 ); k++ )
{
kit.Frame = new Transform( Clear( k, dishAt ) + Out( k ) * (12.0f + Air), Rotation.LookAt( Vector3.Up, Out( k ) ) );
kit.Lathe( Vector3.Zero, [new( 0, 0 ), new( 9, 3 ), new( 9, 4 ), new( 0, 1.2f )], 20 );
}
} );
if ( s.Ladder ) Ladder( b, lattice, levels[^1], Half, Radius, r0, lattice ? Pad : poleBase );
if ( s.Beacon )
{
var cap = lattice ? height + 1.5f : height;
b.Round( "lightning_rod", "Lightning Rod", b.Metal, 0, 0, 0.6f, 0.3f, cap, cap + 30.0f, 8 );
var at = lattice ? top + 0.5f : 0.0f;
b.Part( "beacon", "Obstruction Light", b.Detail, kit => kit.Lathe( new Vector3( at, lattice ? at : r0 * 0.25f + 1.2f, 0 ),
[new( 0, cap ), new( 1.6f, cap ), new( 1.6f, cap + 1.5f ), new( 1.3f, cap + 3.0f ), new( 0, cap + 3.5f )], 12 ) );
}
// A platform low over the foot pushes the cabinets and the fence out past its antennas.
var reach = levels[^1] < 64.0f + 48.0f ? Around( levels[^1] ) + DeckWidth + 12.0f : 0.0f;
var cabinetAt = MathF.Max( foot + 10.0f, reach );
var fence = MathF.Max( MathF.Max( foot + 34.0f, 48.0f ), MathF.Max( reach + 6.0f, s.Cabinets ? cabinetAt + 24.0f : 0.0f ) );
if ( s.Cabinets ) Cabinets( b, cabinetAt );
if ( s.Fence ) Fence( b, fence );
}
/// <summary>
/// Four tapered legs, struts round the faces at each level and crossed braces in every panel,
/// a cap plate on top.
/// </summary>
static void Lattice( PropBuilder b, float height, float foot, float top, Func<int, float, Vector3> leg )
{
// How far the corners of a leg's end reach above or below its axis: the leg leans a little
// in X and in Y, and its square section turns with it.
var dir = (leg( 0, height ) - leg( 0, Pad )).Normal;
var sink = 1.5f * (MathF.Abs( dir.x ) + MathF.Abs( dir.y )) + Air;
b.Part( "legs", "Legs", b.Main, kit =>
{
for ( var k = 0; k < 4; k++ )
kit.Beam( leg( k, Pad + sink ), leg( k, height - sink ), 3.0f, 3.0f );
} );
b.Box( "cap", "Cap Plate", b.Metal, new Vector3( -top - 2, -top - 2, height ), new Vector3( top + 2, top + 2, height + 1.5f ) );
var n = Math.Max( 2, (int)MathF.Round( (height - Pad) / 36.0f ) );
var levels = Enumerable.Range( 1, n - 1 ).Select( l => Pad + (height - Pad) * l / n ).ToList();
var bounds = new[] { Pad + 2.0f }.Concat( levels ).Append( height - 4.0f ).ToList();
b.Part( "struts", "Struts", b.Metal, kit =>
{
for ( var k = 0; k < 4; k++ )
foreach ( var z in levels )
{
var a = leg( k, z ); var c = leg( k + 1, z ); var u = (c - a).Normal;
kit.Beam( a + u * 2.8f, c - u * 2.8f, 1.2f, 1.2f );
}
} );
b.Part( "braces", "Braces", b.Metal, kit =>
{
for ( var k = 0; k < 4; k++ )
for ( var l = 0; l + 1 < bounds.Count; l++ )
{
var z0 = bounds[l] + 1.4f;
var z1 = bounds[l + 1] - 1.4f;
if ( z1 - z0 < 4.0f ) continue;
var u = (leg( k + 1, z0 ) - leg( k, z0 )).WithZ( 0 ).Normal;
var o = ((leg( k, z0 ) + leg( k + 1, z0 )) * 0.5f).WithZ( 0 ).Normal * 0.7f;
kit.Beam( leg( k, z0 ) + u * 2.8f - o, leg( k + 1, z1 ) - u * 2.8f - o, 0.8f, 0.8f );
kit.Beam( leg( k + 1, z0 ) - u * 2.8f + o, leg( k, z1 ) + u * 2.8f + o, 0.8f, 0.8f );
}
} );
}
/// <summary>
/// A ring deck round the mast on four supports, a railing, and panel antennas on arms outside it.
/// </summary>
static void Platform( PropBuilder b, CellTowerSettings s, int i, float z, float inner, Func<int, float, Vector3> clear, float lowest )
{
var outer = inner + DeckWidth;
b.Part( $"platform{i}", "Antenna Platform", b.Metal, kit => kit.Ring( Vector3.Zero, outer, inner, z - 1.5f, z, Sides ) );
var start = MathF.Max( z - 22.0f, lowest );
if ( z - 1.5f - 1.6f - start >= 4.0f )
b.Part( $"platform{i}_supports", "Platform Supports", b.Metal, kit =>
{
for ( var k = 0; k < 4; k++ )
{
var from = clear( k, start );
var dir = from.WithZ( 0 ).Normal;
kit.Beam( from, (dir * (outer - 4.0f)).WithZ( z - 1.5f - 1.6f ), 1.2f, 1.2f );
}
} );
var posts = Math.Max( 12, (int)(MathF.PI * 2.0f * outer / 30.0f) );
b.Part( $"platform{i}_railing", "Platform Railing", b.Metal, kit =>
{
for ( var k = 0; k < posts; k++ )
{
kit.Frame = new Transform( Vector3.Zero, Rotation.FromYaw( 360.0f * k / posts ) );
kit.Box( new Vector3( outer - 1.8f, -0.8f, z ), new Vector3( outer - 0.2f, 0.8f, z + RailHeight - 1.5f ) );
}
kit.Frame = new Transform( Vector3.Zero, Rotation.Identity );
kit.Ring( Vector3.Zero, outer, outer - 2.0f, z + RailHeight - 1.5f, z + RailHeight, Sides );
} );
b.Part( $"platform{i}_knee_rail", "Knee Rail", b.Metal, kit => kit.Ring( Vector3.Zero, outer - 2.1f, outer - 3.0f, z + RailHeight * 0.5f - 0.5f, z + RailHeight * 0.5f + 0.5f, Sides ) );
var antennas = Math.Clamp( s.Antennas, 0, 8 );
if ( antennas == 0 ) return;
float Yaw( int k ) => 360.0f * (k + (i % 2) * 0.5f) / antennas + 15.0f;
b.Part( $"platform{i}_mounts", "Antenna Mounts", b.Metal, kit =>
{
kit.LiftFlushFaces = false;
for ( var k = 0; k < antennas; k++ )
{
kit.Frame = new Transform( Vector3.Zero, Rotation.FromYaw( Yaw( k ) ) );
kit.Box( new Vector3( outer + Air, -1.0f, z - 1.5f ), new Vector3( outer + 8.0f, 1.0f, z ) );
kit.Cylinder( new Vector3( outer + 6.0f, 0, 0 ), 1.0f, 1.0f, z, z + 42.0f, 10 );
kit.Box( new Vector3( outer + 2.5f, -3.0f, z + 6.0f ), new Vector3( outer + 4.95f, 3.0f, z + 16.0f ) );
}
} );
b.Part( $"platform{i}_antennas", "Panel Antennas", b.Detail, kit =>
{
for ( var k = 0; k < antennas; k++ )
{
kit.Frame = new Transform( Vector3.Zero, Rotation.FromYaw( Yaw( k ) ) );
kit.Box( new Vector3( outer + 7.05f, -5.0f, z + 3.0f ), new Vector3( outer + 10.0f, 5.0f, z + 41.0f ) );
}
} );
}
/// <summary>
/// A ladder up the -Y side: on the lattice face up to the supports of the lowest platform, on
/// the monopole up to its deck.
/// </summary>
static void Ladder( PropBuilder b, bool lattice, float lowest, Func<float, float> half, Func<float, float> radius, float r0, float from )
{
var to = lattice ? lowest - 24.0f : lowest - 1.5f;
if ( to < from + 20.0f ) return;
float Y( float z ) => lattice ? -half( z ) - 3.0f : -r0 - 4.0f;
foreach ( var (name, x) in new[] { ("left", -7.0f), ("right", 7.0f) } )
b.Part( $"ladder_{name}", "Ladder Rail", b.Metal, kit =>
{
if ( lattice ) kit.Beam( new Vector3( x, Y( from ), from + 0.75f ), new Vector3( x, Y( to ), to - 0.75f ), 1.5f, 1.5f );
else kit.Box( new Vector3( x - 0.75f, Y( 0 ) - 0.75f, from ), new Vector3( x + 0.75f, Y( 0 ) + 0.75f, to ) );
} );
b.Part( "ladder_rungs", "Ladder Rungs", b.Metal, kit =>
{
for ( var z = from + 10.0f; z + 2.0f < to; z += 12.0f )
kit.Box( new Vector3( -6.25f, Y( z ) - 0.4f, z ), new Vector3( 6.25f, Y( z ) + 0.4f, z + 1.0f ) );
} );
}
/// <summary>
/// Two equipment cabinets on a plinth beside the pad.
/// </summary>
static void Cabinets( PropBuilder b, float x0 )
{
b.Box( "cabinet_plinth", "Plinth", b.Main, new Vector3( x0, -15, 0 ), new Vector3( x0 + 18, 15, 3 ) );
b.Part( "cabinets", "Equipment Cabinets", b.Metal, kit =>
{
kit.Box( new Vector3( x0 + 1, -14, 3 ), new Vector3( x0 + 15, -1, 32 ) );
kit.Box( new Vector3( x0 + 1, 1, 3 ), new Vector3( x0 + 15, 14, 26 ) );
} );
}
/// <summary>
/// Posts, a top rail and mesh panels round a square.
/// </summary>
static void Fence( PropBuilder b, float half )
{
const float h = 48.0f;
var bays = Math.Max( 1, (int)MathF.Ceiling( half * 2.0f / 40.0f ) );
var stops = Enumerable.Range( 0, bays + 1 ).Select( k => -half + half * 2.0f * k / bays ).ToList();
// Posts along the X sides take the corners; the Y sides only get the posts between.
b.Part( "fence_posts", "Fence Posts", b.Metal, kit =>
{
foreach ( var y in new[] { -half, half - 2.0f } )
foreach ( var x in stops )
{
var x0 = Math.Clamp( x - 1.0f, -half, half - 2.0f );
kit.Box( new Vector3( x0, y, 0 ), new Vector3( x0 + 2.0f, y + 2.0f, h ) );
}
foreach ( var x in new[] { -half, half - 2.0f } )
foreach ( var y in stops.Skip( 1 ).SkipLast( 1 ) )
kit.Box( new Vector3( x, y - 1.0f, 0 ), new Vector3( x + 2.0f, y + 1.0f, h ) );
} );
var gaps = new List<(Vector3 Min, Vector3 Max)>();
for ( var k = 0; k < bays; k++ )
{
var a = Math.Clamp( stops[k] - 1.0f, -half, half - 2.0f ) + 2.0f;
var c = Math.Clamp( stops[k + 1] - 1.0f, -half, half - 2.0f );
foreach ( var y in new[] { -half + 0.85f, half - 1.15f } ) gaps.Add( (new Vector3( a, y, 0 ), new Vector3( c, y + 0.3f, 0 )) );
var a2 = k == 0 ? -half + 2.0f : stops[k] + 1.0f;
var c2 = k + 1 == bays ? half - 2.0f : stops[k + 1] - 1.0f;
foreach ( var x in new[] { -half + 0.85f, half - 1.15f } ) gaps.Add( (new Vector3( x, a2, 0 ), new Vector3( x + 0.3f, c2, 0 )) );
}
b.Part( "fence_rails", "Fence Rails", b.Metal, kit =>
{
foreach ( var (min, max) in gaps )
{
var wide = max.x - min.x > max.y - min.y;
var lo = wide ? new Vector3( min.x, min.y - 0.85f, h - 2.0f ) : new Vector3( min.x - 0.85f, min.y, h - 2.0f );
var hi = wide ? new Vector3( max.x, max.y + 0.85f, h ) : new Vector3( max.x + 0.85f, max.y, h );
kit.Box( lo, hi );
}
} );
b.Part( "fence_mesh", "Chain-Link Mesh", b.Detail, kit =>
{
foreach ( var (min, max) in gaps )
kit.Box( min.WithZ( 2.0f ), max.WithZ( h - 2.0f ) );
} );
}
}
/// <summary>
/// Places a cell tower. One click, the mast stands with its platforms and fence.
/// </summary>
[ToolCategory( "Columns & Towers" )]
[Title( "Cell Tower" ), Icon( "cell_tower" )]
public sealed class CellTowerEditor( PrimitiveTool tool ) : PropEditor( tool )
{
readonly CellTowerGenerator _generator = new();
public CellTowerSettings Settings { get; } = new();
protected override IPartGenerator Generator => _generator;
public override PropSettings Props => Settings;
}