Editor/Stair/ArchStairShape.cs
namespace Sunless.Architecture;
public sealed class ArchStairRun {
public int Index { get; init; }
public ArchStairAxes Axes { get; init; }
public int Steps { get; init; }
public float Going { get; init; }
public float Rise { get; init; }
public float Width { get; init; }
public float BaseHeight { get; init; }
public StairTurn Turn { get; init; }
public bool Last { get; init; }
public float Drawn { get; init; }
public float DrawnWidth { get; init; }
public int Level { get; init; }
public bool WalledRight { get; init; }
public bool WalledLeft { get; init; }
// Resolved once — the run's own railing rows override the stair's Guard/WallRail
public bool GuardLeft { get; init; }
public bool GuardRight { get; init; }
public bool HandrailLeft { get; init; }
public bool HandrailRight { get; init; }
public StairRailing RailingLeft { get; init; }
public StairRailing RailingRight { get; init; }
public Vector2 SpanLeft { get; init; } = new( 0f, 1f );
public Vector2 SpanRight { get; init; } = new( 0f, 1f );
public float Length => Steps * Going;
public float TopHeight => BaseHeight + Steps * Rise;
public float StepTop( int step ) => BaseHeight + (step + 1) * Rise;
public float Rake( float along ) => BaseHeight + MathX.Clamp( along / MathF.Max( 1f, Length ), 0f, 1f ) * (TopHeight - BaseHeight);
public float Nosing( float along ) => MathF.Min( Rake( along ) + Rise, TopHeight );
public float Under( float clearance ) {
return Length * ArchClearance.Cross( BaseHeight + clearance, TopHeight + clearance, TopHeight ).At;
}
public List<Vector2> Loop() => Loop( 0f );
public List<Vector2> Loop( float from ) => Axes.Rect( Math.Clamp( from, 0f, MathF.Max( 0f, Length - 1f ) ), Length, 0f, Width );
}
public sealed class ArchStairPad {
// -1 for synthesised arrival pads
public int Index { get; init; } = -1;
public ArchStairAxes Axes { get; init; }
public float AlongFrom { get; set; }
public float AlongTo { get; set; }
public float AcrossFrom { get; set; }
public float AcrossTo { get; set; }
public float Height { get; init; }
public StairTurn Turn { get; init; }
public bool Arrival { get; init; }
public int? Level { get; init; }
public int Storey { get; init; }
public bool WalledFrom { get; set; }
public bool WalledTo { get; set; }
public bool WalledHead { get; set; }
// Null = landing defers to the turn direction
public bool? RailedFrom { get; init; }
public bool? RailedTo { get; init; }
public bool? RailedHead { get; init; }
public bool RailsFrom => RailedFrom ?? (!WalledFrom && Turn != StairTurn.Right);
public bool RailsTo => RailedTo ?? (!WalledTo && Turn != StairTurn.Left);
public bool RailsHead => RailedHead ?? (!WalledHead && Turn != StairTurn.None);
public List<Vector2> Loop() => Axes.Rect( AlongFrom, AlongTo, AcrossFrom, AcrossTo );
}
public sealed class ArchStairWell {
public int Level { get; init; }
public float Height { get; init; }
public ArchRoom Probe { get; init; }
public List<List<Vector2>> Loops { get; } = new();
}
public sealed partial class ArchStairShape {
public List<ArchStairRun> Runs { get; } = new();
public List<ArchStairPad> Pads { get; } = new();
public List<ArchStairWell> Levels { get; } = new();
public List<List<Vector2>> AutomaticTopOpening { get; } = new();
public float Width { get; init; }
public float Going { get; init; }
public float Rise { get; init; }
public float BaseHeight { get; init; }
public float TopHeight { get; private set; }
public int TopLevel { get; private set; }
public float OpenEdge { get; private set; }
public float ClosedEdge { get; private set; }
public bool GuardLeft { get; init; }
public bool GuardRight { get; init; }
public static float StoreyHeight( ArchBuilding building, ArchKit kit ) => ArchBuild.StoreyHeight( building, kit );
public static float FloorOf( ArchBuilding building, ArchKit kit, int level ) => ArchBuild.FloorOf( building, kit, level );
public static int FloorIndex( ArchBuilding building, ArchKit kit, float height ) {
return Math.Clamp( (int)MathF.Round( (height - kit.GroundClearance) / MathF.Max( 1f, StoreyHeight( building, kit ) ) ), -64, 64 );
}
public static IReadOnlyList<ArchStairLane> Standing( ArchStairPart stair, ArchStairCore core ) {
if ( stair.Lanes.Count > 0 ) {
return stair.Lanes;
}
return new List<ArchStairLane>
{
new()
{
AlongFrom = 0f,
AlongTo = MathF.Max( 1f, core.Length ),
AcrossFrom = 0f,
AcrossTo = MathF.Max( 1f, core.Width ),
Walk = StairWalk.Ahead
}
};
}
public static ArchStairShape Resolve( ArchStairPart stair, ArchRoom room, ArchKit kit, IEnumerable<ArchRoom> storey = null, ArchBuilding building = null ) {
var walls = storey ?? building?.Rooms;
var core = stair.Core ?? new ArchStairCore();
var lanes = Standing( stair, core );
var going = stair.StepGoing > 0.5f ? stair.StepGoing : kit.StepGoing;
// Stair's own riser beats kit — carved flights bend it to arrive at the host's coping
var riser = stair.StepRise > 0.5f ? stair.StepRise : kit.StepRise;
var guardLeft = stair.Guard is StairGuard.Left or StairGuard.Both;
var guardRight = stair.Guard is StairGuard.Right or StairGuard.Both;
var openLeft = guardLeft || !guardRight;
var kinds = ArchKinds.Load();
var climbs = ArchStairLanes.Climbs( core, lanes );
var lead = lanes.FirstOrDefault( lane => lane.Climbs ) ?? lanes[0];
var width = MathF.Max( 1f, lead.Width > 1f ? lead.Width : stair.Width > 1f ? stair.Width : kit.StairWidth );
var housings = new ArchStairHousings( walls, kit, kinds );
var fits = Fitting( core, lanes, climbs, housings, building, kit, stair.BaseHeight );
var totalSteps = 0;
for ( var index = 0; index < lanes.Count; index++ ) {
if ( lanes[index].Climbs ) {
totalSteps += ArchStairLanes.Steps( climbs[index], riser, fits[index].AlongTo - fits[index].AlongFrom, going );
}
}
var shape = new ArchStairShape {
Width = width,
Going = going,
Rise = ArchStairLanes.Climb( core, lanes ) / Math.Max( 1, totalSteps ),
BaseHeight = stair.BaseHeight,
GuardLeft = guardLeft,
GuardRight = guardRight,
OpenEdge = openLeft ? width : 0f,
ClosedEdge = openLeft ? 0f : width
};
var startLevel = FloorIndex( building, kit, stair.BaseHeight );
var baseProbe = Probe( room, walls, startLevel, room, kinds );
var height = stair.BaseHeight;
var frame = core.Axes;
var arrived = (ArchStairRun)null;
for ( var index = 0; index < lanes.Count; index++ ) {
var lane = lanes[index];
var level = FloorIndex( building, kit, height );
var probe = Probe( room, walls, level, baseProbe, kinds );
if ( !lane.Climbs ) {
height += climbs[index];
var stood = fits[index];
var landing = new ArchStairPad {
Index = index,
RailedFrom = Railed( lane, StairEdge.Right ),
RailedTo = Railed( lane, StairEdge.Left ),
RailedHead = Railed( lane, StairEdge.Head ),
Axes = frame,
AlongFrom = stood.AlongFrom,
AlongTo = stood.AlongTo,
AcrossFrom = stood.AcrossFrom,
AcrossTo = stood.AcrossTo,
Height = height,
Turn = arrived?.Turn ?? StairTurn.None,
Arrival = NextFlight( lanes, index ) is null,
Level = PadLevel( building, kit, height ),
Storey = FloorIndex( building, kit, height )
};
Corner( landing, probe, kit, kinds );
shape.Pads.Add( landing );
continue;
}
var next = NextFlight( lanes, index );
var last = next is null;
var climb = climbs[index];
var drawn = MathF.Max( 1f, lane.Length );
var seated = fits[index];
var length = seated.AlongTo - seated.AlongFrom;
var steps = ArchStairLanes.Steps( climb, riser, length, going );
var laneWidth = seated.AcrossTo - seated.AcrossFrom;
var walked = lane.Axes( core );
var axes = new ArchStairAxes { Origin = walked.Flat( 0f, seated.AcrossFrom ), Yaw = walked.Yaw };
var walledRight = Walled( probe, kit, axes, length * 0.5f, 0f, -1f, kinds );
var walledLeft = Walled( probe, kit, axes, length * 0.5f, laneWidth, 1f, kinds );
var run = new ArchStairRun {
Index = index,
Axes = axes,
Steps = steps,
Drawn = drawn,
DrawnWidth = MathF.Max( 1f, lane.Width ),
Going = length / steps,
Rise = climb / steps,
Width = laneWidth,
BaseHeight = height,
Turn = last ? StairTurn.None : TurnBetween( core, lane, next ),
Last = last,
Level = level,
WalledRight = walledRight,
WalledLeft = walledLeft,
GuardLeft = Carries( lane, StairEdge.Left ) ?? (stair.AutoRailings && guardLeft),
GuardRight = Carries( lane, StairEdge.Right ) ?? (stair.AutoRailings && guardRight),
RailingLeft = Kind( lane, StairEdge.Left ) ?? stair.LeftRailing,
RailingRight = Kind( lane, StairEdge.Right ) ?? stair.RightRailing,
HandrailLeft = walledLeft && (Carries( lane, StairEdge.Left, StairRailing.Handrail ) ?? (stair.AutoRailings && stair.WallRail)),
HandrailRight = walledRight && (Carries( lane, StairEdge.Right, StairRailing.Handrail ) ?? (stair.AutoRailings && stair.WallRail)),
SpanLeft = Spanned( lane, StairEdge.Left ),
SpanRight = Spanned( lane, StairEdge.Right )
};
shape.Runs.Add( run );
height = run.TopHeight;
frame = axes;
arrived = run;
if ( last && stair.TopLanding && lanes[^1].Climbs ) {
var arrival = housings.On( FloorIndex( building, kit, height ) )
.Fit( axes, length, length + MathF.Max( going, stair.TopLandingDepth ), 0f, laneWidth );
shape.Pads.Add( new ArchStairPad {
Axes = axes,
AlongFrom = arrival.AlongFrom,
AlongTo = arrival.AlongTo,
AcrossFrom = arrival.AcrossFrom,
AcrossTo = arrival.AcrossTo,
Height = height,
Turn = StairTurn.None,
Arrival = true,
Level = PadLevel( building, kit, height ),
Storey = FloorIndex( building, kit, height )
} );
}
}
shape.TopHeight = height;
shape.TopLevel = FloorIndex( building, kit, height );
if ( shape.Runs.Count > 0 && shape.Runs[0].WalledLeft != shape.Runs[0].WalledRight ) {
var walledLeft = shape.Runs[0].WalledLeft;
shape.ClosedEdge = walledLeft ? width : 0f;
shape.OpenEdge = walledLeft ? 0f : width;
}
for ( var level = startLevel + 1; level <= shape.TopLevel; level++ ) {
var well = new ArchStairWell {
Level = level,
Height = FloorOf( building, kit, level ),
Probe = Probe( room, walls, level, baseProbe, kinds )
};
var soffit = well.Height - kit.FloorThickness;
var headroom = MathF.Max( 1f, kit.StairHeadroom );
foreach ( var run in shape.Runs ) {
if ( run.BaseHeight + 0.5f >= well.Height ) {
continue;
}
var reach = ArchClearance.Cross( run.BaseHeight + headroom, run.TopHeight + headroom, soffit );
if ( reach.Clears ) {
continue;
}
var openingFrom = reach.Blocked ? 0f : Riser( run, reach.At * run.Length );
if ( level == shape.TopLevel ) {
openingFrom = MathF.Max( 0f, run.Length - (run.Length - openingFrom) * 1.25f );
}
well.Loops.Add( run.Loop( openingFrom ) );
}
foreach ( var pad in shape.Pads ) {
if ( PadWell( building, kit, pad ) == level && pad.Height + headroom > soffit ) {
well.Loops.Add( pad.Loop() );
}
}
if ( well.Loops.Count > 0 ) {
shape.Levels.Add( well );
}
}
if ( shape.Levels.Find( well => well.Level == shape.TopLevel ) is { } top ) {
shape.AutomaticTopOpening.AddRange( top.Loops.Select( loop => loop.ToList() ) );
if ( stair.TopOpening is { Count: >= 3 } ) {
top.Loops.Clear();
top.Loops.Add( stair.TopOpening.Select( point => core.Flat( point.x, point.y ) ).ToList() );
}
}
return shape;
}
// Null when the lane has no explicit guards — falls through to stair-level settings
static bool? Carries( ArchStairLane lane, StairEdge edge, StairRailing railing ) {
if ( lane.Guards.Count == 0 ) {
return null;
}
return lane.Guards.Any( guard => guard.Edge == edge && guard.Railing == railing );
}
static bool? Carries( ArchStairLane lane, StairEdge edge ) {
if ( lane.Guards.Count == 0 ) {
return null;
}
return lane.Guards.Any( guard => guard.Edge == edge );
}
static StairRailing? Kind( ArchStairLane lane, StairEdge edge ) {
if ( Carries( lane, edge ) != true ) {
return null;
}
return lane.Guards.Any( guard => guard.Edge == edge && guard.Railing == StairRailing.Balustrade )
? StairRailing.Balustrade
: StairRailing.Handrail;
}
static Vector2 Spanned( ArchStairLane lane, StairEdge edge ) {
var start = 1f;
var end = 0f;
var found = false;
foreach ( var guard in lane.Guards ) {
if ( guard.Edge != edge || !guard.Stands ) {
continue;
}
start = MathF.Min( start, guard.Start );
end = MathF.Max( end, guard.End );
found = true;
}
return found ? new Vector2( start, end ) : new Vector2( 0f, 1f );
}
static bool? Railed( ArchStairLane lane, StairEdge edge ) {
return lane.Guards.Count == 0 ? null : lane.Guarding( edge );
}
static ArchStairLane NextFlight( IReadOnlyList<ArchStairLane> lanes, int after ) {
for ( var index = after + 1; index < lanes.Count; index++ ) {
if ( lanes[index].Climbs ) {
return lanes[index];
}
}
return null;
}
static void Framed( ArchStairAxes frame, ArchStairCore core, ArchStairLane landing, out float alongFrom, out float alongTo, out float acrossFrom, out float acrossTo ) {
alongFrom = float.MaxValue;
alongTo = float.MinValue;
acrossFrom = float.MaxValue;
acrossTo = float.MinValue;
foreach ( var corner in landing.Loop( core ) ) {
var local = corner - frame.Origin;
var along = Vector2.Dot( local, frame.Along );
var across = Vector2.Dot( local, frame.Across );
alongFrom = MathF.Min( alongFrom, along );
alongTo = MathF.Max( alongTo, along );
acrossFrom = MathF.Min( acrossFrom, across );
acrossTo = MathF.Max( acrossTo, across );
}
}
static StairTurn TurnBetween( ArchStairCore core, ArchStairLane here, ArchStairLane next ) {
return TurnKind( AngleDelta( next.Yaw( core ) - here.Yaw( core ) ) );
}
}