Editor/Stair/ArchStairSteps.cs
namespace Sunless.Architecture;
public readonly record struct ArchStairStation( Vector2 At, StairWalk Walk );
public static class ArchStairSteps {
public static ArchStairPart Owner( ArchPlan plan, ArchStairLane lane ) {
if ( plan is null || lane is null ) {
return null;
}
return plan.Parts<ArchStairPart>().FirstOrDefault( stair => stair.Lanes.Contains( lane ) );
}
public static int IndexOf( ArchStairPart stair, ArchStairLane lane ) {
return stair?.Lanes.IndexOf( lane ) ?? -1;
}
public static ArchRoom Home( ArchPlan plan, ArchStairPart stair ) {
if ( plan is null || stair is null ) {
return null;
}
var kinds = ArchKinds.Load();
return plan.AllRooms().FirstOrDefault( room => plan.Filed<ArchStairPart>( room, kinds ).Contains( stair ) );
}
// Single-step stairs move the shaft rise directly; multi-step stairs pin the lane.
public static void Lift( ArchStairPart stair, ArchStairLane lane, float climb ) {
if ( stair.Lanes.Count == 1 && stair.Core is { } core ) {
core.Rise = MathF.Max( 1f, climb );
lane.Rise = 0f;
return;
}
lane.Rise = MathF.Max( 0f, climb );
}
public static int Ordinal( ArchStairPart stair, ArchStairLane lane ) {
var count = 0;
foreach ( var step in stair.Lanes ) {
if ( step.Climbs == lane.Climbs ) {
count++;
}
if ( step == lane ) {
return count;
}
}
return count;
}
static void Stand( ArchStairLane lane, ArchStairLane onto, StairWalk walk, float run, float width ) {
Lay( lane, Seated( onto, walk, width ), walk, run, width, onto.Bearing );
}
static Vector2 Seated( ArchStairLane onto, StairWalk walk, float width ) {
var heading = onto.Heading;
var hand = onto.Leftward;
var head = onto.Seat + heading * onto.Length;
return onto.Walk.Between( walk ) switch {
StairTurn.Left => head + hand * onto.Width,
StairTurn.Right => onto.Seat + heading * MathF.Max( 0f, onto.Length - width ),
StairTurn.Back => head + hand * (onto.Width + width),
_ => head
};
}
static void Lay( ArchStairLane lane, Vector2 seat, StairWalk walk, float run, float width, float bearing ) {
var reach = seat + walk.Heading() * MathF.Max( ArchStairLanes.MinLane, run )
+ walk.Leftward() * MathF.Max( ArchStairLanes.MinLane, width );
lane.Walk = walk;
lane.Bearing = bearing;
lane.AlongFrom = MathF.Min( seat.x, reach.x );
lane.AlongTo = MathF.Max( seat.x, reach.x );
lane.AcrossFrom = MathF.Min( seat.y, reach.y );
lane.AcrossTo = MathF.Max( seat.y, reach.y );
}
public static ArchStairLane Add( ArchPlan plan, ArchStairPart stair, ArchStairLane after ) {
var index = after is null ? stair.Lanes.Count - 1 : IndexOf( stair, after );
if ( index < 0 || stair.Core is null ) {
return null;
}
var below = stair.Lanes[index];
var flight = new ArchStairLane { Id = plan.AllocateId(), Step = StairStep.Flight };
var chain = ArchStairChain.Of( stair );
Stand( flight, below, below.Walk, Run( stair, index ), below.Width );
stair.Lanes.Insert( index + 1, flight );
chain.Insert( index + 1 );
Relay( stair, index + 2, chain );
ArchStairLanes.Reseat( stair.Core, stair.Lanes );
return flight;
}
static float Run( ArchStairPart stair, int index ) {
for ( var step = index; step >= 0; step-- ) {
if ( stair.Lanes[step].Climbs ) {
return stair.Lanes[step].Length;
}
}
return MathF.Max( ArchStairLanes.MinLane, stair.Core.Length );
}
public static bool Aim( ArchStairPart stair, ArchStairLane lane, StairTurn turn ) {
var index = IndexOf( stair, lane );
if ( index <= 0 || stair.Core is null ) {
return false;
}
var below = stair.Lanes[index - 1];
var chain = ArchStairChain.Of( stair );
Stand( lane, below, below.Walk.Turned( turn ), lane.Length, lane.Width );
Relay( stair, index + 1, chain );
ArchStairLanes.Reseat( stair.Core, stair.Lanes );
return true;
}
public static bool Segment( ArchPlan plan, ArchStairPart stair, ArchStairLane lane, int count ) {
return Segment( plan, stair, lane, count, 0f );
}
public static bool Curve( ArchPlan plan, ArchStairPart stair, ArchStairLane lane, int count, float sweep ) {
return count >= 2 && Segment( plan, stair, lane, count, sweep );
}
static bool Segment( ArchPlan plan, ArchStairPart stair, ArchStairLane lane, int count, float sweep ) {
var index = IndexOf( stair, lane );
if ( index < 0 || !lane.Climbs || count < 2 || lane.Length / count < ArchStairLanes.MinLane ) {
return false;
}
var guards = lane.Guards.ToList();
var chain = ArchStairChain.Of( stair );
var run = lane.Length / count;
var width = lane.Width;
var walk = lane.Walk;
var bearing = lane.Bearing;
var pinned = lane.Rise;
var turn = sweep / (count - 1);
var cut = new List<ArchStairLane>();
for ( var part = 0; part < count; part++ ) {
var piece = part == 0 ? lane : new ArchStairLane { Id = plan.AllocateId(), Step = StairStep.Flight };
if ( part == 0 ) {
Lay( piece, lane.Seat, walk, run, width, bearing );
} else {
Stand( piece, cut[part - 1], walk, run, width );
piece.Bearing = bearing + turn * part;
}
piece.Rise = pinned / count;
piece.Guards = Slice( plan, guards, part, count );
cut.Add( piece );
}
stair.Lanes.RemoveAt( index );
stair.Lanes.InsertRange( index, cut );
chain.Insert( index + 1, count - 1 );
Relay( stair, index + count, chain );
ArchStairLanes.Reseat( stair.Core, stair.Lanes );
return true;
}
static List<ArchStairGuardPart> Slice( ArchPlan plan, IReadOnlyList<ArchStairGuardPart> guards, int part, int count ) {
var from = part / (float)count;
var to = (part + 1) / (float)count;
var kept = new List<ArchStairGuardPart>();
foreach ( var guard in guards ) {
var start = MathF.Max( guard.Start, from );
var end = MathF.Min( guard.End, to );
if ( end - start < 0.01f ) {
continue;
}
var slice = guard.Copy();
slice.Id = plan.AllocateId();
slice.From = (start - from) * count;
slice.To = (end - from) * count;
kept.Add( slice );
}
return kept;
}
// Captures each step's relative joint and bearing so edits below propagate correctly above.
readonly struct ArchStairChain {
public List<StairTurn> Joints { get; init; }
public List<float> Bearings { get; init; }
public static ArchStairChain Of( ArchStairPart stair ) {
var chain = new ArchStairChain {
Joints = new List<StairTurn> { StairTurn.None },
Bearings = new List<float> { stair.Lanes.Count > 0 ? stair.Lanes[0].Bearing : 0f }
};
for ( var index = 1; index < stair.Lanes.Count; index++ ) {
chain.Joints.Add( stair.Lanes[index - 1].Walk.Between( stair.Lanes[index].Walk ) );
chain.Bearings.Add( stair.Lanes[index].Bearing - stair.Lanes[index - 1].Bearing );
}
return chain;
}
public void Insert( int index, int count = 1 ) {
Joints.InsertRange( index, Enumerable.Repeat( StairTurn.None, count ) );
Bearings.InsertRange( index, Enumerable.Repeat( 0f, count ) );
}
public float Bearing( ArchStairPart stair, int index ) {
return index < 1 ? Bearings[0] : stair.Lanes[index - 1].Bearing + Bearings[index];
}
}
static void Relay( ArchStairPart stair, int from, ArchStairChain chain ) {
for ( var index = Math.Max( 1, from ); index < stair.Lanes.Count; index++ ) {
var lane = stair.Lanes[index];
var below = stair.Lanes[index - 1];
Stand( lane, below, below.Walk.Turned( chain.Joints[index] ), lane.Length, lane.Width );
lane.Bearing = chain.Bearing( stair, index );
}
}
public static ArchStairLane AddLanding( ArchPlan plan, ArchStairPart stair, ArchStairLane after ) {
var index = IndexOf( stair, after );
if ( index < 0 ) {
return null;
}
var depth = MathF.Min( MathF.Max( ArchStairLanes.MinLane, after.Width ), after.Length * 0.4f );
if ( ArchStairWalk.Split( after, depth ) is not { } landing ) {
return null;
}
landing.Id = plan.AllocateId();
stair.Lanes.Insert( index + 1, landing );
return landing;
}
public static void Move( ArchStairPart stair, ArchStairLane lane, Vector2 to ) {
var index = IndexOf( stair, lane );
if ( index < 0 ) {
return;
}
var chain = ArchStairChain.Of( stair );
ArchStairEdges.Shift( lane, to - lane.Frame.Flat( lane.Length * 0.5f, lane.Width * 0.5f ) );
Relay( stair, index + 1, chain );
}
public static IEnumerable<ArchStairStation> Stations( ArchStairLane below, float width ) {
var frame = below.Frame;
var span = MathF.Max( ArchStairLanes.MinLane, width );
foreach ( var station in Along( below.Width, span ) ) {
yield return new ArchStairStation( frame.Flat( below.Length, station ), below.Walk );
yield return new ArchStairStation( frame.Flat( 0f, station ), below.Walk.Turned( StairTurn.Back ) );
}
foreach ( var station in Along( below.Length, span ) ) {
yield return new ArchStairStation( frame.Flat( station, below.Width ), below.Walk.Turned( StairTurn.Left ) );
yield return new ArchStairStation( frame.Flat( station, 0f ), below.Walk.Turned( StairTurn.Right ) );
}
}
static IEnumerable<float> Along( float side, float width ) {
var count = Math.Clamp( (int)MathF.Floor( side / width ), 1, MostStations );
for ( var station = 0; station < count; station++ ) {
yield return side * (station + 0.5f) / count;
}
}
public const int MostStations = 4;
public static bool JoinTo( ArchStairPart stair, ArchStairLane lane, ArchStairStation station ) {
var index = IndexOf( stair, lane );
if ( index < 1 ) {
return false;
}
var chain = ArchStairChain.Of( stair );
var below = stair.Lanes[index - 1];
var run = lane.Length;
var width = lane.Width;
var frame = new ArchStairAxes { Yaw = Cardinal( station.Walk ) + below.Bearing };
Lay( lane, station.At - frame.Across * (width * 0.5f), station.Walk, run, width, below.Bearing );
Relay( stair, index + 1, chain );
return true;
}
static float Cardinal( StairWalk walk ) => walk switch {
StairWalk.Back => 180f,
StairWalk.Left => 90f,
StairWalk.Right => -90f,
_ => 0f
};
public static void Settle( ArchStairPart stair, ArchStairLane lane ) {
var index = IndexOf( stair, lane );
if ( index < 0 ) {
return;
}
var chain = ArchStairChain.Of( stair );
if ( index > 0 ) {
Meet( stair.Lanes[index - 1], lane );
}
Relay( stair, index + 1, chain );
}
public static void Bend( ArchStairPart stair, ArchStairLane lane, float bearing ) {
var chain = ArchStairChain.Of( stair );
lane.Bearing = bearing;
Relay( stair, IndexOf( stair, lane ) + 1, chain );
}
// Only extends toward the step above when they share the same walk direction.
static void Meet( ArchStairLane below, ArchStairLane above ) {
if ( below.Walk != above.Walk ) {
return;
}
ArchStairEdges.Lengthen( below, ArchStairEdges.Along( below, above.Seat ) );
}
public static bool Remove( ArchStairPart stair, ArchStairLane lane ) {
var index = IndexOf( stair, lane );
if ( index < 0 || stair.Lanes.Count <= 1 ) {
return false;
}
stair.Lanes.RemoveAt( index );
if ( index > 0 ) {
Absorb( stair.Lanes[index - 1], lane );
}
ArchStairLanes.Fit( stair.Core, stair.Lanes );
return true;
}
static void Absorb( ArchStairLane below, ArchStairLane gone ) {
switch ( below.Walk ) {
case StairWalk.Ahead when Overlaps( below.AcrossFrom, below.AcrossTo, gone.AcrossFrom, gone.AcrossTo ):
below.AlongTo = MathF.Max( below.AlongTo, gone.AlongTo );
break;
case StairWalk.Back when Overlaps( below.AcrossFrom, below.AcrossTo, gone.AcrossFrom, gone.AcrossTo ):
below.AlongFrom = MathF.Min( below.AlongFrom, gone.AlongFrom );
break;
case StairWalk.Left when Overlaps( below.AlongFrom, below.AlongTo, gone.AlongFrom, gone.AlongTo ):
below.AcrossTo = MathF.Max( below.AcrossTo, gone.AcrossTo );
break;
case StairWalk.Right when Overlaps( below.AlongFrom, below.AlongTo, gone.AlongFrom, gone.AlongTo ):
below.AcrossFrom = MathF.Min( below.AcrossFrom, gone.AcrossFrom );
break;
}
}
static bool Overlaps( float from, float to, float otherFrom, float otherTo ) {
return from < otherTo - 0.05f && otherFrom < to - 0.05f;
}
}