Editor/Stair/ArchStairLanes.cs
namespace Sunless.Architecture;
public static class ArchStairLanes {
const float Nothing = 0.05f;
public const float MinLane = 18f;
public static float Climb( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
var pinned = 0f;
foreach ( var lane in lanes ) {
pinned += MathF.Max( 0f, lane.Rise );
}
return MathF.Max( MathF.Max( 1f, core?.Rise ?? 0f ), pinned );
}
// Pinned lanes take their exact rise; unpinned flights share the remainder proportionally
public static float[] Climbs( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
var climbs = new float[lanes.Count];
if ( lanes.Count == 0 ) {
return climbs;
}
var pinned = 0f;
var openSpan = 0f;
var openCount = 0;
foreach ( var lane in lanes ) {
if ( lane.Rise > Nothing ) {
pinned += lane.Rise;
continue;
}
if ( !lane.Climbs ) {
continue;
}
openSpan += lane.Length;
openCount++;
}
var free = MathF.Max( 0f, Climb( core, lanes ) - pinned );
for ( var index = 0; index < lanes.Count; index++ ) {
var lane = lanes[index];
if ( lane.Rise > Nothing ) {
climbs[index] = lane.Rise;
continue;
}
if ( !lane.Climbs ) {
continue;
}
climbs[index] = openSpan > Nothing
? free * lane.Length / openSpan
: free / MathF.Max( 1, openCount );
}
return climbs;
}
// Whichever of riser or going needs more steps wins
public static int Steps( float climb, float riser, float length, float going ) {
var byRise = (int)MathF.Round( climb / MathF.Max( 1f, riser ) );
var byRun = (int)MathF.Round( length / MathF.Max( 1f, going ) );
return Math.Max( 1, Math.Max( byRise, Math.Min( byRun, (int)MathF.Floor( climb / ShallowestRiser ) ) ) );
}
public const float ShallowestRiser = 2f;
public static Vector2 Local( ArchStairCore core, Vector2 point ) {
var axes = core.Axes;
var offset = point - core.Origin;
return new Vector2( Vector2.Dot( offset, axes.Along ), Vector2.Dot( offset, axes.Across ) );
}
static (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) Bounds( IReadOnlyList<ArchStairLane> lanes ) {
var alongFrom = float.MaxValue;
var alongTo = float.MinValue;
var acrossFrom = float.MaxValue;
var acrossTo = float.MinValue;
foreach ( var corner in lanes.SelectMany( lane => lane.Corners() ) ) {
alongFrom = MathF.Min( alongFrom, corner.x );
alongTo = MathF.Max( alongTo, corner.x );
acrossFrom = MathF.Min( acrossFrom, corner.y );
acrossTo = MathF.Max( acrossTo, corner.y );
}
return (alongFrom, alongTo, acrossFrom, acrossTo);
}
// Grows the core to fit its lanes — the core frames what's inside it
public static bool Fit( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
if ( core is null || lanes.Count == 0 ) {
return false;
}
var (alongFrom, alongTo, acrossFrom, acrossTo) = Bounds( lanes );
alongFrom = MathF.Min( alongFrom, 0f );
acrossFrom = MathF.Min( acrossFrom, 0f );
alongTo = MathF.Max( alongTo, core.Length );
acrossTo = MathF.Max( acrossTo, core.Width );
if ( alongFrom > -Nothing && acrossFrom > -Nothing
&& alongTo - core.Length < Nothing && acrossTo - core.Width < Nothing ) {
return false;
}
core.Origin = core.Flat( alongFrom, acrossFrom );
core.Length = alongTo - alongFrom;
core.Width = acrossTo - acrossFrom;
foreach ( var lane in lanes ) {
lane.AlongFrom -= alongFrom;
lane.AlongTo -= alongFrom;
lane.AcrossFrom -= acrossFrom;
lane.AcrossTo -= acrossFrom;
}
return true;
}
// Reseats the core tightly onto its lanes (Fit only grows; this also shrinks)
public static void Reseat( ArchStairCore core, IReadOnlyList<ArchStairLane> lanes ) {
if ( core is null || lanes.Count == 0 ) {
return;
}
var (alongFrom, alongTo, acrossFrom, acrossTo) = Bounds( lanes );
core.Origin = core.Flat( alongFrom, acrossFrom );
core.Length = MathF.Max( MinLane, alongTo - alongFrom );
core.Width = MathF.Max( MinLane, acrossTo - acrossFrom );
foreach ( var lane in lanes ) {
lane.AlongFrom -= alongFrom;
lane.AlongTo -= alongFrom;
lane.AcrossFrom -= acrossFrom;
lane.AcrossTo -= acrossFrom;
}
}
public static void Number( ArchPlan plan, ArchStairPart stair ) {
foreach ( var lane in stair.Lanes.Where( lane => lane.Id == 0 ) ) {
lane.Id = plan.AllocateId();
}
}
public static bool DeriveLandings( ArchPlan plan, ArchStairPart stair ) {
if ( stair.Core is not { } core || stair.Lanes.Count == 0 || stair.Lanes.Any( lane => !lane.Climbs ) ) {
return false;
}
var settled = new List<ArchStairLane>();
for ( var index = 0; index < stair.Lanes.Count; index++ ) {
var lane = stair.Lanes[index];
settled.Add( lane );
if ( index + 1 >= stair.Lanes.Count || Between( core, lane, stair.Lanes[index + 1] ) is not { } pad ) {
continue;
}
settled.Add( new ArchStairLane {
Id = plan.AllocateId(),
Step = StairStep.Landing,
AlongFrom = pad.AlongFrom,
AlongTo = pad.AlongTo,
AcrossFrom = pad.AcrossFrom,
AcrossTo = pad.AcrossTo,
Walk = lane.Walk
} );
}
foreach ( var lane in settled ) {
lane.LandingDepth = 0f;
}
stair.Lanes.Clear();
stair.Lanes.AddRange( settled );
return true;
}
readonly struct ArchStairLanding {
public float AlongFrom { get; init; }
public float AlongTo { get; init; }
public float AcrossFrom { get; init; }
public float AcrossTo { get; init; }
}
static ArchStairLanding? Between( ArchStairCore core, ArchStairLane lane, ArchStairLane next ) {
var head = lane.HeadEdge();
var foot = next.FootEdge();
var alongFrom = Least( head.AlongFrom, head.AlongTo, foot.AlongFrom, foot.AlongTo );
var alongTo = Most( head.AlongFrom, head.AlongTo, foot.AlongFrom, foot.AlongTo );
var acrossFrom = Least( head.AcrossFrom, head.AcrossTo, foot.AcrossFrom, foot.AcrossTo );
var acrossTo = Most( head.AcrossFrom, head.AcrossTo, foot.AcrossFrom, foot.AcrossTo );
var heading = lane.Heading;
var alongAxis = MathF.Abs( heading.x ) > 0.5f;
if ( alongAxis && alongTo - alongFrom < Nothing ) {
var line = head.AlongFrom;
var depth = Depth( lane, Reach( core, lane, next, line, heading.x, acrossFrom, acrossTo, true ) );
if ( depth < Nothing ) {
return null;
}
alongFrom = heading.x > 0f ? line : line - depth;
alongTo = heading.x > 0f ? line + depth : line;
} else if ( !alongAxis && acrossTo - acrossFrom < Nothing ) {
var line = head.AcrossFrom;
var depth = Depth( lane, Reach( core, lane, next, line, heading.y, alongFrom, alongTo, false ) );
if ( depth < Nothing ) {
return null;
}
acrossFrom = heading.y > 0f ? line : line - depth;
acrossTo = heading.y > 0f ? line + depth : line;
}
if ( alongTo - alongFrom < Nothing || acrossTo - acrossFrom < Nothing ) {
return null;
}
return new ArchStairLanding {
AlongFrom = alongFrom,
AlongTo = alongTo,
AcrossFrom = acrossFrom,
AcrossTo = acrossTo
};
}
static float Depth( ArchStairLane lane, float reach ) {
return lane.LandingDepth > Nothing ? MathF.Min( lane.LandingDepth, reach ) : reach;
}
static float Reach( ArchStairCore core, ArchStairLane lane, ArchStairLane next, float line, float direction, float from, float to, bool alongAxis ) {
var limit = direction > 0f ? alongAxis ? core.Length : core.Width : 0f;
foreach ( var candidate in new[] { lane, next } ) {
var crossFrom = alongAxis ? candidate.AcrossFrom : candidate.AlongFrom;
var crossTo = alongAxis ? candidate.AcrossTo : candidate.AlongTo;
if ( crossTo <= from + Nothing || crossFrom >= to - Nothing ) {
continue;
}
var near = alongAxis ? candidate.AlongFrom : candidate.AcrossFrom;
var far = alongAxis ? candidate.AlongTo : candidate.AcrossTo;
if ( direction > 0f && near >= line - Nothing ) {
limit = MathF.Min( limit, near );
}
if ( direction < 0f && far <= line + Nothing ) {
limit = MathF.Max( limit, far );
}
}
return MathF.Max( 0f, MathF.Abs( limit - line ) );
}
public static (ArchStairCore Core, List<ArchStairLane> Lanes) FromLegs( IReadOnlyList<ArchStairLeg> legs ) {
var core = new ArchStairCore { Yaw = legs.Count > 0 ? legs[0].Yaw : 0f, Origin = legs.Count > 0 ? legs[0].Start : Vector2.Zero };
var lanes = new List<ArchStairLane>( legs.Count );
if ( legs.Count == 0 ) {
return (core, lanes);
}
var frame = core.Axes;
var alongFrom = float.MaxValue;
var alongTo = float.MinValue;
var acrossFrom = float.MaxValue;
var acrossTo = float.MinValue;
var boxes = new List<(float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo, StairWalk Walk)>();
foreach ( var leg in legs ) {
var width = leg.Width > 1f ? leg.Width : 48f;
var corners = leg.Axes.Rect( 0f, leg.Length, 0f, width );
var lowAlong = float.MaxValue;
var highAlong = float.MinValue;
var lowAcross = float.MaxValue;
var highAcross = float.MinValue;
foreach ( var corner in corners ) {
var offset = corner - core.Origin;
var along = Vector2.Dot( offset, frame.Along );
var across = Vector2.Dot( offset, frame.Across );
lowAlong = MathF.Min( lowAlong, along );
highAlong = MathF.Max( highAlong, along );
lowAcross = MathF.Min( lowAcross, across );
highAcross = MathF.Max( highAcross, across );
}
var travel = leg.Span.IsNearZeroLength ? frame.Along : leg.Span.Normal;
var forward = Vector2.Dot( travel, frame.Along );
var sideways = Vector2.Dot( travel, frame.Across );
boxes.Add( (lowAlong, highAlong, lowAcross, highAcross, MathF.Abs( sideways ) > MathF.Abs( forward )
? sideways >= 0f ? StairWalk.Left : StairWalk.Right
: forward >= 0f ? StairWalk.Ahead : StairWalk.Back) );
alongFrom = MathF.Min( alongFrom, lowAlong );
alongTo = MathF.Max( alongTo, highAlong );
acrossFrom = MathF.Min( acrossFrom, lowAcross );
acrossTo = MathF.Max( acrossTo, highAcross );
}
core.Origin = frame.Flat( alongFrom, acrossFrom );
core.Length = MathF.Max( 1f, alongTo - alongFrom );
core.Width = MathF.Max( 1f, acrossTo - acrossFrom );
foreach ( var box in boxes ) {
lanes.Add( new ArchStairLane {
AlongFrom = box.AlongFrom - alongFrom,
AlongTo = box.AlongTo - alongFrom,
AcrossFrom = box.AcrossFrom - acrossFrom,
AcrossTo = box.AcrossTo - acrossFrom,
Walk = box.Walk
} );
}
return (core, lanes);
}
static float Least( float a, float b, float c, float d ) => MathF.Min( MathF.Min( a, b ), MathF.Min( c, d ) );
static float Most( float a, float b, float c, float d ) => MathF.Max( MathF.Max( a, b ), MathF.Max( c, d ) );
}