ok
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;
}