editor/AdvancedMappingTools/Architecture/SiloEditor.cs

Editor tool that generates a row of industrial steel silos as map geometry. SiloSettings holds diameter, height, count, spacing, roof type, hopper, ladder and ring options; SiloGenerator builds the body courses, stiffener rings, eave ring, cone/dome/flat roof, top platform and hatch, hopper with columns and ring beam or concrete pad, caged ladder, and walkways between silos. SiloEditor wires it into the primitive tool system under the Industrial category.

namespace AdvancedMappingTools;

public enum SiloRoof
{
	Cone,
	Dome,
	Flat
}

public sealed class SiloSettings : PropSettings
{
	[Property, Group( "Silo" ), Range( 16, 1024, slider: false ), WideMode]
	public float Diameter { get; set; } = 144.0f;

	[Property, Group( "Silo" ), Range( 32, 2048, slider: false ), WideMode, Description( "Height of the body, roof not counted." )]
	public float Height { get; set; } = 360.0f;

	[Property, Group( "Silo" ), Range( 1, 6, slider: false ), WideMode, Description( "Silos in one row." )]
	public int Count { get; set; } = 1;

	[Property, HideIf( nameof( Count ), 1 ), Group( "Silo" ), Title( "Row Spacing" ), Range( 32, 2048, slider: false ), WideMode, Description( "Distance between the centres of two silos, at least the diameter and a little." )]
	public float Spacing { get; set; } = 192.0f;

	[Property, Group( "Silo" ), Title( "Roof" ), WideMode]
	public SiloRoof Roof { get; set; } = SiloRoof.Cone;

	[Property, Group( "Silo" ), Title( "Funnel And Legs" ), WideMode, Description( "A hopper below the body on columns. Off stands the body on a concrete pad." )]
	public bool Funnel { get; set; } = true;

	[Property, Group( "Silo" ), WideMode, Description( "A ladder with a safety cage up the side." )]
	public bool Ladder { get; set; } = true;

	[Property, Group( "Silo" ), Title( "Stiffener Rings" ), Range( 0, 16, slider: false ), WideMode, Description( "Rings around the body between its courses." )]
	public int Rings { get; set; } = 5;

	[Property, ShowIf( nameof( Funnel ), true ), Group( "Silo" ), Title( "Leg Height" ), Range( 24, 256, slider: false ), WideMode, Description( "Clear height under a hopper silo." )]
	public float LegHeight { get; set; } = 84.0f;

	public override string PropName => "Silo";
}

/// <summary>
/// A row of steel silos. A body in courses with stiffener rings, a cone, dome or flat roof with an
/// eave ring, a hatch and a top platform, walkways between the roofs of a row, a hopper with an
/// outlet gate on columns with a ring beam and girts, or a concrete pad, and a caged ladder.
/// No part reaches into another.
/// </summary>
public sealed class SiloGenerator : PropGenerator
{
	public override string Id => "silo";
	public override string Title => "Silo";
	public override Type SettingsType => typeof( SiloSettings );

	const int Sides = 32;
	const float Air = 0.1f;

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (SiloSettings)settings;
		var radius = MathF.Max( s.Diameter, 16.0f ) * 0.5f;
		var height = MathF.Max( s.Height, 32.0f );
		var count = Math.Clamp( s.Count, 1, 6 );
		var spacing = MathF.Max( s.Spacing, radius * 2.0f + 24.0f );
		var total = (count - 1) * spacing;
		var legs = s.Funnel ? MathF.Max( s.LegHeight, 24.0f ) : 0.0f;
		var hopper = s.Funnel ? radius * 0.9f : 0.0f;
		var bottom = s.Funnel ? legs + hopper : 8.0f;
		var top = bottom + height;
		var roofRise = s.Roof switch { SiloRoof.Flat => 2.0f, SiloRoof.Dome => radius * 0.4f, _ => radius * 0.45f };
		var peak = top + 3.0f + roofRise;
		var deck = radius * 0.22f;

		for ( var i = 0; i < count; i++ )
		{
			var x = i * spacing - total * 0.5f;
			var id = $"silo{i}";

			// Courses of the body with a stiffener ring between two.
			var courses = Math.Clamp( s.Rings, 0, 16 ) + 1;
			for ( var c = 0; c < courses; c++ )
			{
				var z0 = bottom + height * c / courses + (c > 0 ? 1.5f : 0.0f);
				var z1 = bottom + height * (c + 1) / courses;
				b.Round( $"{id}_course{c}", "Silo Course", b.Main, x, 0, radius, radius, z0, z1, Sides );
				if ( c + 1 < courses ) b.Round( $"{id}_ring{c}", "Stiffener Ring", b.Metal, x, 0, radius + 1.5f, radius + 1.5f, z1, z1 + 1.5f, Sides );
			}

			// Eave ring, roof and the top: a hatch and a small platform with a rail post at each side.
			b.Round( $"{id}_eave", "Eave Ring", b.Metal, x, 0, radius + 2.5f, radius + 2.5f, top, top + 3.0f, Sides );
			switch ( s.Roof )
			{
				case SiloRoof.Dome:
					b.Part( $"{id}_roof", "Silo Roof", b.Metal, kit => kit.Lathe( new Vector3( x, 0, 0 ),
						[new( 0, top + 3.0f ), new( radius + 2.0f, top + 3.0f ), new( radius * 0.95f, top + 3.0f + roofRise * 0.35f ), new( radius * 0.78f, top + 3.0f + roofRise * 0.68f ),
						 new( radius * 0.5f, top + 3.0f + roofRise * 0.9f ), new( deck, top + 3.0f + roofRise ), new( 0, top + 3.0f + roofRise )], Sides ) );
					break;
				case SiloRoof.Flat:
					b.Round( $"{id}_roof", "Silo Roof", b.Metal, x, 0, radius + 2.0f, radius + 2.0f, top + 3.0f, top + 3.0f + roofRise, Sides );
					break;
				default:
					b.Round( $"{id}_roof", "Silo Roof", b.Metal, x, 0, radius + 2.0f, deck, top + 3.0f, top + 3.0f + roofRise, Sides );
					break;
			}

			b.Round( $"{id}_platform", "Top Platform", b.Metal, x, 0, deck, deck, peak, peak + 1.5f, 16 );
			b.Round( $"{id}_hatch", "Roof Hatch", b.Metal, x, 0, deck * 0.45f, deck * 0.45f, peak + 1.5f, peak + 5.0f, 12 );

			if ( s.Funnel ) Hopper( b, id, x, radius, legs, hopper );
			else b.Round( $"{id}_pad", "Concrete Pad", b.Main, x, 0, radius + 12.0f, radius + 12.0f, 0.0f, 8.0f, 12 );

			// The ladder runs up to the eave, its rails reach on past it as a hand hold.
			if ( s.Ladder ) Ladder( b, id, x, radius, s.Funnel ? 0.0f : 8.0f, top + 3.0f );
		}

		// Walkways from platform to platform along the row, with a rail on both sides.
		for ( var i = 0; i + 1 < count; i++ )
		{
			var x0 = i * spacing - total * 0.5f + deck + Air;
			var x1 = (i + 1) * spacing - total * 0.5f - deck - Air;
			b.Box( $"walkway{i}", "Walkway", b.Metal, new Vector3( x0, -12.0f, peak ), new Vector3( x1, 12.0f, peak + 1.5f ) );
			foreach ( var side in new[] { -1.0f, 1.0f } )
			{
				b.Box( $"walkway{i}_rail{(side > 0 ? "l" : "r")}", "Walkway Rail", b.Metal, new Vector3( x0, side * 12.0f - 0.75f, peak + 40.0f ), new Vector3( x1, side * 12.0f + 0.75f, peak + 41.5f ) );
				var posts = Math.Max( 2, (int)MathF.Ceiling( (x1 - x0) / 48.0f ) + 1 );
				for ( var p = 0; p < posts; p++ )
				{
					var px = x0 + 0.75f + p * ((x1 - x0 - 1.5f) / (posts - 1));
					b.Box( $"walkway{i}_post{(side > 0 ? "l" : "r")}{p}", "Walkway Post", b.Metal, new Vector3( px - 0.75f, side * 12.0f - 0.75f, peak + 1.5f ), new Vector3( px + 0.75f, side * 12.0f + 0.75f, peak + 40.0f ) );
				}
			}
		}
	}

	/// <summary>
	/// The hopper under the body: a cone down to an outlet with a gate, a ring beam round its top on
	/// columns, and girts between the columns.
	/// </summary>
	static void Hopper( PropBuilder b, string id, float x, float radius, float legs, float hopper )
	{
		var outlet = MathF.Max( radius * 0.14f, 4.0f );
		b.Round( $"{id}_hopper", "Hopper", b.Main, x, 0, outlet, radius, legs, legs + hopper, Sides );
		b.Round( $"{id}_outlet", "Outlet", b.Metal, x, 0, outlet, outlet, legs - 10.0f, legs, 16 );
		b.Box( $"{id}_gate", "Outlet Gate", b.Metal, new Vector3( x - outlet - 4.0f, -outlet - 1.0f, legs - 16.0f ), new Vector3( x + outlet + 4.0f, outlet + 1.0f, legs - 10.0f ) );

		// The ring beam sits on the columns and carries the hopper rim and the body.
		var beam = MathF.Max( radius * 0.05f, 3.0f );
		b.Part( $"{id}_ring_beam", "Ring Beam", b.Metal, kit => kit.Ring( new Vector3( x, 0, 0 ), radius + beam, radius + Air, legs + hopper - beam * 2.0f, legs + hopper, Sides ) );

		var column = beam;
		var count = radius > 60.0f ? 8 : 4;
		var at = radius + beam * 0.5f;
		var tops = legs + hopper - beam * 2.0f;
		for ( var k = 0; k < count; k++ )
		{
			var a = MathF.PI * 2.0f * (k + 0.5f) / count;
			var cx = x + MathF.Cos( a ) * at;
			var cy = MathF.Sin( a ) * at;
			b.Box( $"{id}_foot{k}", "Column Foot", b.Main, new Vector3( cx - column * 2.0f, cy - column * 2.0f, 0.0f ), new Vector3( cx + column * 2.0f, cy + column * 2.0f, 2.0f ) );
			b.Box( $"{id}_column{k}", "Column", b.Metal, new Vector3( cx - column, cy - column, 2.0f ), new Vector3( cx + column, cy + column, tops ) );
		}
	}

	/// <summary>
	/// A ladder up the side facing -Y: two rails, rungs and cage hoops above head height.
	/// </summary>
	static void Ladder( PropBuilder b, string id, float x, float radius, float from, float to )
	{
		const float half = 8.0f;
		var y = -(radius + 6.0f);
		var top = to + 40.0f;
		b.Box( $"{id}_ladder_left", "Ladder Rail", b.Metal, new Vector3( x - half - 1.0f, y - 1.0f, from ), new Vector3( x - half, y + 1.0f, top ) );
		b.Box( $"{id}_ladder_right", "Ladder Rail", b.Metal, new Vector3( x + half, y - 1.0f, from ), new Vector3( x + half + 1.0f, y + 1.0f, top ) );
		var rungs = (int)((top - from - 6.0f) / 12.0f);
		b.Part( $"{id}_ladder_rungs", "Ladder Rungs", b.Metal, kit =>
		{
			for ( var k = 0; k <= rungs; k++ )
			{
				var z = from + 6.0f + k * 12.0f;
				kit.Box( new Vector3( x - half, y - 0.5f, z ), new Vector3( x + half, y + 0.5f, z + 1.0f ) );
			}
		} );

		// Hoops from above head height: one bent bar each, from rail to rail around the front.
		var hoopFrom = from + 84.0f;
		b.Part( $"{id}_cage", "Ladder Cage", b.Metal, kit =>
		{
			const int steps = 12;
			const float bar = 0.5f;
			var spread = half + 1.0f + bar + Air;
			for ( var z = hoopFrom; z < top - 6.0f; z += 30.0f )
			{
				var path = Enumerable.Range( 0, steps + 1 ).Select( k =>
				{
					var a = MathF.PI * k / steps;
					return new Vector3( x + MathF.Cos( a ) * spread, y - 1.0f - MathF.Sin( a ) * 14.0f, z );
				} ).ToList();

				var rings = new Vector3[4][];
				for ( var p = 0; p < 4; p++ ) rings[p] = new Vector3[path.Count];
				for ( var k = 0; k < path.Count; k++ )
				{
					var tangent = (path[Math.Min( k + 1, path.Count - 1 )] - path[Math.Max( k - 1, 0 )]).Normal;
					var side = Vector3.Cross( tangent, Vector3.Up ).Normal;
					var corners = new[] { (-1.0f, -1.0f), (-1.0f, 1.0f), (1.0f, 1.0f), (1.0f, -1.0f) };
					for ( var p = 0; p < 4; p++ )
						rings[p][k] = path[k] + side * (corners[p].Item1 * bar) + Vector3.Up * (corners[p].Item2 * bar * 1.5f);
				}
				kit.Sweep( rings, false, true );
			}
		} );
	}
}

/// <summary>
/// Places a row of silos. One click, the row stands on its columns or pads.
/// </summary>
[ToolCategory( "Industrial" )]
[Title( "Silo" ), Icon( "storage" )]
public sealed class SiloEditor( PrimitiveTool tool ) : PropEditor( tool )
{
	readonly SiloGenerator _generator = new();

	public SiloSettings Settings { get; } = new();

	protected override IPartGenerator Generator => _generator;

	public override PropSettings Props => Settings;
}