editor/AdvancedMappingTools/Architecture/FireEscapeEditor.cs

ok

Really Bad Code
namespace AdvancedMappingTools;
public enum FireEscapeStairSide
{
	Left,
	Right
}

public sealed partial class FireEscapeSettings : PropSettings
{
	[Property, Group( "Fire Escape" ), Range( 1, 8, slider: false ), WideMode, Description( "How many platforms the escape has." )]
	public int Storeys { get; set; } = 3;

	[Property, Group( "Fire Escape" ), Title( "Storey Height" ), Range( 32, 512, slider: false ), WideMode, Description( "Distance between two platforms. The first platform is one storey up." )]
	public float StoreyHeight { get; set; } = 128.0f;

	[Property, Group( "Fire Escape" ), Title( "Platform Width" ), Range( 32, 512, slider: false ), WideMode]
	public float PlatformWidth { get; set; } = 120.0f;

	[Property, Group( "Fire Escape" ), Title( "Platform Depth" ), Range( 16, 256, slider: false ), WideMode]
	public float PlatformDepth { get; set; } = 48.0f;

	[Property, Group( "Fire Escape" ), Title( "Stair Side" ), WideMode, Description( "The end where each stair leaves its platform." )]
	public FireEscapeStairSide StairSide { get; set; } = FireEscapeStairSide.Right;

	[Property, Group( "Fire Escape" ), Title( "Hanging Ladder" ), WideMode, Description( "A ladder from the ground to the first platform." )]
	public bool Ladder { get; set; } = true;

	[Property, Group( "Fire Escape" ), Title( "Balusters" ), WideMode, Description( "Upright bars in the railings." )]
	public bool Balusters { get; set; } = true;

	[Property, Group( "Fire Escape" ), Title( "Roof Ladder" ), WideMode, Description( "A ladder from the top platform up to the roof." )]
	public bool RoofLadder { get; set; } = true;

	public override string PropName => "Fire Escape";
}

/// <summary>
/// A fire escape on a wall. Grated platforms on outriggers and knee braces, railings with
/// balusters, steep stairs with stringers and handrails that come up through a hatch in the
/// platform above, a hanging ladder to the ground and a ladder to the roof. The wall is the zero
/// line, the escape reaches out along +Y. No part reaches into another.
/// </summary>
public sealed class FireEscapeGenerator : PropGenerator
{
	public override string Id => "fireescape";
	public override string Title => "Fire Escape";
	public override Type SettingsType => typeof( FireEscapeSettings );

	public const float Deck = 3.0f;
	public const float RailHeight = 36.0f;

	/// <summary>Railings stay clear of the outriggers of the platform above.</summary>
	public static float Rail( float storey ) => MathF.Min( RailHeight, storey - Deck - 4.0f );

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (FireEscapeSettings)settings;
		var storeys = Math.Clamp( s.Storeys, 1, 8 );
		var height = MathF.Max( s.StoreyHeight, 32.0f );
		var width = MathF.Max( s.PlatformWidth, 32.0f );
		var depth = MathF.Max( s.PlatformDepth, 16.0f );
		var half = width * 0.5f;
		var side = s.StairSide == FireEscapeStairSide.Right ? 1.0f : -1.0f;
		// The stair runs along the wall in a band at the front of the platform, from the stair end
		// of the upper platform down to the other end of the lower one.
		var landing = Math.Clamp( width * 0.15f, 8.0f, 20.0f );
		var top = half - landing;
		var run = MathF.Min( width - landing * 2.0f, height * 0.8f );
		var foot = top - run;
		var band1 = depth - 5.0f;
		var band0 = MathF.Max( band1 - Math.Clamp( depth * 0.45f, 6.0f, 24.0f ), 3.5f );
		var hand = MathF.Max( MathF.Min( 34.0f, height * (run - 10.0f) / run - 6.0f ), 6.0f );
		var hatch = MathF.Min( run - 10.0f, MathF.Max( run * (hand + 6.0f) / height, run * 0.5f ) );
		Vector3 P( float u, float y, float z ) => new( side * u, y, z );
		void Box( MeshKit kit, float u0, float y0, float z0, float u1, float y1, float z1 )
		{
			var a = P( u0, y0, z0 );
			var c = P( u1, y1, z1 );
			kit.Box( Vector3.Min( a, c ), Vector3.Max( a, c ) );
		}

		void Profile( MeshKit kit, IEnumerable<Vector2> shape, float y0, float y1 )
		{
			var points = shape.Select( p => new Vector2( side * p.x, p.y ) ).ToList();
			if ( side < 0 )
				points.Reverse();
			kit.Profile( points, y0, y1 );
		}

		for ( var j = 0; j < storeys; j++ )
		{
			var z = (j + 1) * height;
			var hasHatch = j > 0;
			// The grating: a frame round the edge, bars across, a framed hatch over the stair below.
			b.Part( $"deck{j}", "Platform", b.Metal, kit =>
			{
				kit.LiftFlushFaces = false;
				Box( kit, -half, 0.0f, z - Deck, half, 2.0f, z );
				Box( kit, -half, depth - 2.0f, z - Deck, half, depth, z );
				Box( kit, -half, 2.0f, z - Deck, -half + 2.0f, depth - 2.0f, z );
				Box( kit, half - 2.0f, 2.0f, z - Deck, half, depth - 2.0f, z );
				if ( hasHatch )
				{
					Box( kit, top - hatch - 1.5f, band0 - 1.5f, z - Deck, top + 1.5f, band0, z );
					Box( kit, top - hatch - 1.5f, band0, z - Deck, top - hatch, depth - 2.0f, z );
					Box( kit, top, band0, z - Deck, top + 1.5f, depth - 2.0f, z );
				}

				for ( var u = -half + 4.0f; u + 2.0f < half - 2.0f; u += 4.0f )
				{
					var overHatch = hasHatch && u + 1.0f > top - hatch - 1.5f && u < top + 1.5f;
					Box( kit, u, 2.0f, z - 1.5f, u + 1.0f, overHatch ? band0 - 1.5f : depth - 2.0f, z );
				}
			} );
			// Outriggers under the ends, knee braces from the wall up to them.
			b.Part( $"outriggers{j}", "Outrigger", b.Metal, kit =>
			{
				Box( kit, -half, 0.0f, z - Deck - 3.0f, -half + 2.0f, depth, z - Deck );
				Box( kit, half - 2.0f, 0.0f, z - Deck - 3.0f, half, depth, z - Deck );
			} );
			var drop = MathF.Min( depth * 0.7f, height - Deck - 3.0f - Rail( height ) - 8.0f );
			if ( drop >= 6.0f )
				b.Part( $"braces{j}", "Knee Brace", b.Metal, kit =>
				{
					foreach ( var u in new[]
					{
						-half + 1.0f,
						half - 1.0f
					}

	 )
					{
						// Flat ends meet the wall and the underside of the outrigger.
						// A square-cut diagonal beam stopped short of both supports.
						var underside = z - Deck - 3.0f;
						Vector3 V( float y, float h, float x ) => P( u + x, y, h );
						var section = new[]
						{
							new Vector2( 0, underside - drop ),
							new Vector2( depth - 3, underside - 2 ),
							new Vector2( depth - 3, underside ),
							new Vector2( depth - 5, underside ),
							new Vector2( 0, underside - drop + 2 )
						};
						kit.BeginPart();
						var a = section.Select( p => V( p.x, p.y, -0.75f ) ).ToArray();
						var c = section.Select( p => V( p.x, p.y, 0.75f ) ).ToArray();
						if ( side < 0 )
							(a, c) = (c, a);
						kit.Face( a.Reverse().ToArray() );
						kit.Face( c );
						for ( var k = 0; k < a.Length; k++ )
							kit.Face( a[k], a[(k + 1) % a.Length], c[(k + 1) % a.Length], c[k] );
					}
				} );
			Railing( b, s, j, z, half, depth, Rail( height ), j == 0 && s.Ladder, P );
			if ( j == 0 )
				continue;
			// The stair from this platform down to the one below.
			var low = z - height;
			float Line( float u ) => low + (u - foot) / run * height;
			var lift = 5.0f * run / height;
			b.Part( $"stair{j - 1}", "Stair Stringers", b.Metal, kit =>
			{
				foreach ( var(y0, y1)in new[]
				{
					(band0, band0 + 1.5f),
					(band1 - 1.5f, band1)
				}

	 )
					Profile( kit, [new( foot, low ), new( foot + lift, low ), new( top, z - 5.0f ), new( top, z + 1.5f ), new( foot, low + 1.5f )], y0, y1 );
			} );
			var steps = Math.Max( 3, (int)MathF.Round( height / 9.0f ) );
			var go = run / steps;
			b.Part( $"treads{j - 1}", "Stair Treads", b.Metal, kit =>
			{
				for ( var k = 1; k < steps; k++ )
				{
					var zk = low + height * k / steps;
					var uc = foot + go * k;
					Box( kit, uc - go * 0.5f, band0 + 1.5f, zk - 1.2f, uc + go * 0.5f, band1 - 1.5f, zk );
				}
			} );
			b.Part( $"stair_rails{j - 1}", "Stair Handrail", b.Metal, kit =>
			{
				kit.LiftFlushFaces = false;
				foreach ( var(y0, y1)in new[]
				{
					(band0, band0 + 1.5f),
					(band1 - 1.5f, band1)
				}

	 )
				{
					Profile( kit, [new( foot, Line( foot ) + hand ), new( top, Line( top ) + hand ), new( top, Line( top ) + hand + 1.5f ), new( foot, Line( foot ) + hand + 1.5f )], y0, y1 );
					foreach ( var u in new[]
					{
						foot + 1.0f,
						(foot + top) * 0.5f,
						top - 2.5f
					}

	 )
						Profile( kit, [new( u, Line( u ) + 1.5f ), new( u + 1.5f, Line( u + 1.5f ) + 1.5f ), new( u + 1.5f, Line( u + 1.5f ) + hand ), new( u, Line( u ) + hand )], y0, y1 );
				}
			} );
		}

		if ( s.Ladder )
			Ladder( b, "ladder", P, s.LadderAt, depth, height + Rail( height ) );
		if ( s.RoofLadder )
			Ladder( b, "roof_ladder", P, -half + landing + 12.0f, 2.5f, storeys * height + MathF.Min( height, 96.0f ) + 24.0f, storeys * height );
	}

	/// <summary>
	/// Posts, a top rail and balusters along the front and both ends. A gap in the balusters lets
	/// the hanging ladder through.
	/// </summary>
	static void Railing( PropBuilder b, FireEscapeSettings s, int j, float z, float half, float depth, float rail, bool ladderGap, Func<float, float, float, Vector3> P )
	{
		void Box( MeshKit kit, float u0, float y0, float z0, float u1, float y1, float z1 )
		{
			var a = P( u0, y0, z0 );
			var c = P( u1, y1, z1 );
			kit.Box( Vector3.Min( a, c ), Vector3.Max( a, c ) );
		}

		bool Free( float u ) => !ladderGap || MathF.Abs( u - s.LadderAt ) > 11.0f;
		var bays = Math.Max( 1, (int)MathF.Ceiling( (half * 2.0f - 2.0f) / 30.0f ) );
		var posts = Enumerable.Range( 0, bays + 1 ).Select( k => -half + 1.0f + (half * 2.0f - 2.0f) * k / bays ).Where( u => Free( u ) || MathF.Abs( u ) > half - 2.0f ).ToList();
		b.Part( $"railing{j}", "Railing", b.Metal, kit =>
		{
			kit.LiftFlushFaces = false;
			Box( kit, -half, depth - 2.0f, z + rail - 2.0f, half, depth, z + rail );
			foreach ( var u in new[]
			{
				-half,
				half - 2.0f
			}

	 )
			{
				Box( kit, u, 2.0f, z + rail - 2.0f, u + 2.0f, depth - 2.0f, z + rail );
				Box( kit, u, 2.0f, z, u + 2.0f, 4.0f, z + rail - 2.0f );
			}

			foreach ( var u in posts )
				Box( kit, u - 1.0f, depth - 2.0f, z, u + 1.0f, depth, z + rail - 2.0f );
		} );
		if ( !s.Balusters || s.DetailLevel == StructureDetail.Low )
			return;
		var bar = Math.Clamp( s.BalusterThickness, .25f, 1.5f ) * .5f;
		var spacing = Math.Clamp( s.BalusterSpacing, 5, 24 ) * (s.DetailLevel == StructureDetail.High ? .75f : 1);
		b.Part( $"balusters{j}", "Balusters", b.Metal, kit =>
		{
			for ( var u = -half + 4.5f; u < half - 4.0f; u += spacing )
				if ( Free( u ) && posts.All( p => MathF.Abs( p - u ) > 1.5f + bar ) )
					Box( kit, u - bar, depth - 1.0f - bar, z, u + bar, depth - 1.0f + bar, z + rail - 2.0f );
			foreach ( var u in new[]
			{
				-half + 1.0f,
				half - 1.0f
			}

	 )
				for ( var y = 8.0f; y < depth - 4.0f; y += spacing )
					Box( kit, u - bar, y - bar, z, u + bar, y + bar, z + rail - 2.0f );
		} );
	}

	/// <summary>
	/// Two rails and rungs. The hanging ladder stands against the front of the first platform, the
	/// roof ladder on the top platform against the wall.
	/// </summary>
	static void Ladder( PropBuilder b, string id, Func<float, float, float, Vector3> P, float u, float y, float to, float from = 0.0f )
	{
		void Box( string part, string label, Vector3 a, Vector3 c ) => b.Box( part, label, b.Metal, Vector3.Min( a, c ), Vector3.Max( a, c ) );
		Box( $"{id}_left", "Ladder Rail", P( u - 9.5f, y, from ), P( u - 8.0f, y + 2.0f, to ) );
		Box( $"{id}_right", "Ladder Rail", P( u + 8.0f, y, from ), P( u + 9.5f, y + 2.0f, to ) );
		b.Part( $"{id}_rungs", "Ladder Rungs", b.Metal, kit =>
		{
			for ( var z = from + 10.0f; z + 1.0f < to - 4.0f; z += 12.0f )
			{
				var a = P( u - 8.0f, y + 0.5f, z ); var c = P( u + 8.0f, y + 1.5f, z + 1.0f );
				kit.Box( Vector3.Min( a, c ), Vector3.Max( a, c ) );
			}
		} );
	}
}

/// <summary>
/// Places a fire escape. One click at the wall, the escape reaches out from it.
/// </summary>
[ToolCategory( "Buildings" )]
[Title( "Fire Escape" ), Icon( "whatshot" )]
public sealed class FireEscapeEditor( PrimitiveTool tool ) : PropEditor( tool )
{
	readonly FireEscapeGenerator _generator = new();

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

	protected override IPartGenerator Generator => _generator;

	public override PropSettings Props => Settings;
}