Editor-side stair core and lane model types for the Arch stair system. Defines enums for walk direction, fill, step; extension methods to get headings, turns and relations; ArchStairCore that describes the shaft (origin, yaw, size, rise) and factory for straight cores; and ArchStairLane that describes an individual flight or landing with geometry helpers and copying.
using System.Text.Json.Serialization;
namespace Sunless.Architecture;
// Which way a flight is walked inside its core. The rectangle says WHERE the flight stands; this says how it
// is crossed, so a short wide lane can still be a flight rather than a landing.
public enum StairWalk
{
Ahead,
Back,
Left,
Right
}
// Which way a walk points in the core's own frame, and what turning it comes to. A corner is a rotation of these
// two vectors rather than sixteen cases, which is what lets one placement answer stand every step in the climb.
public static class StairWalks
{
public static Vector2 Heading( this StairWalk walk ) => walk switch
{
StairWalk.Back => new Vector2( -1f, 0f ),
StairWalk.Left => new Vector2( 0f, 1f ),
StairWalk.Right => new Vector2( 0f, -1f ),
_ => new Vector2( 1f, 0f )
};
// Width runs to the LEFT of travel, so this is the axis a step's own width is measured out on.
public static Vector2 Leftward( this StairWalk walk )
{
var heading = walk.Heading();
return new Vector2( -heading.y, heading.x );
}
public static StairWalk Reversed( this StairWalk walk ) => walk switch
{
StairWalk.Back => StairWalk.Ahead,
StairWalk.Left => StairWalk.Right,
StairWalk.Right => StairWalk.Left,
_ => StairWalk.Back
};
public static StairWalk Turned( this StairWalk walk, StairTurn turn ) => turn switch
{
StairTurn.Left => walk switch
{
StairWalk.Ahead => StairWalk.Left,
StairWalk.Left => StairWalk.Back,
StairWalk.Back => StairWalk.Right,
_ => StairWalk.Ahead
},
StairTurn.Right => walk switch
{
StairWalk.Ahead => StairWalk.Right,
StairWalk.Right => StairWalk.Back,
StairWalk.Back => StairWalk.Left,
_ => StairWalk.Ahead
},
StairTurn.Back => walk.Reversed(),
_ => walk
};
// The turn one walk makes off another, which is what a joint in the climb IS - so the drawing can show which
// way a step already turns rather than only offering to turn it again.
public static StairTurn Between( this StairWalk from, StairWalk to )
{
if ( from == to )
{
return StairTurn.None;
}
if ( from.Turned( StairTurn.Left ) == to )
{
return StairTurn.Left;
}
if ( from.Turned( StairTurn.Right ) == to )
{
return StairTurn.Right;
}
return StairTurn.Back;
}
}
// What a flight is made of. Open is treads on stringers over an open raked soffit; Solid is a stepped mass
// standing on the core floor, which is the concrete stair and what a carved platform used to be needed for.
public enum StairFill
{
Open,
Solid
}
// What one step in the climb IS. A flight rakes and takes a share of the climb; a landing is level and takes
// none. Both are rectangles in the shaft's frame, which is why one ordered list is the whole stair.
public enum StairStep
{
Flight,
Landing
}
// The shaft. Every flight and every landing in the stair is measured in this box's frame, which is why two
// flights either abut or they do not - there is no shared square to hunt for and nothing to settle.
public sealed class ArchStairCore
{
public Vector2 Origin { get; set; }
public float Yaw { get; set; }
public float Length { get; set; } = 240f;
public float Width { get; set; } = 120f;
// The whole climb, dragged on the box's top face or its head arrow. Pins that exceed it raise it.
public float Rise { get; set; } = 128f;
public StairFill Fill { get; set; }
public ArchStairAxes Axes => new() { Origin = Origin, Yaw = Yaw };
public Vector2 Flat( float along, float across ) => Axes.Flat( along, across );
public List<Vector2> Outline() => Axes.Rect( 0f, Length, 0f, Width );
public ArchStairCore Copy() => new()
{
Origin = Origin,
Yaw = Yaw,
Length = Length,
Width = Width,
Rise = Rise,
Fill = Fill
};
// The one-lane core every code-built flight stands on - a porch step, a walkway drop, an exterior run,
// a carved notch. They all draw one straight run, so none of them needs to know a lane from a pattern.
public static (ArchStairCore Core, List<ArchStairLane> Lanes) Straight( Vector2 origin, Vector2 along, float length, float width, float rise )
{
var direction = along.IsNearZeroLength ? new Vector2( 1f, 0f ) : along.Normal;
var core = new ArchStairCore
{
Origin = origin,
Yaw = MathF.Atan2( direction.y, direction.x ).RadianToDegree(),
Length = MathF.Max( 1f, length ),
Width = MathF.Max( 1f, width ),
Rise = MathF.Max( 1f, rise )
};
var lanes = new List<ArchStairLane>
{
new() { AlongFrom = 0f, AlongTo = core.Length, AcrossFrom = 0f, AcrossTo = core.Width, Walk = StairWalk.Ahead }
};
return (core, lanes);
}
}
// One step of the climb - a flight or a landing - in the core's own frame. Climb order is list order, so the
// list IS the stair: flight, platform, flight, and the layer stack is a view straight onto it.
public sealed class ArchStairLane
{
// Its own, so a railing can name the step it guards and a layer row survives being reordered.
public int Id { get; set; }
public StairStep Step { get; set; }
public float AlongFrom { get; set; }
public float AlongTo { get; set; }
public float AcrossFrom { get; set; }
public float AcrossTo { get; set; }
public StairWalk Walk { get; set; }
// How far the step is turned off the cardinal its walk names, about the corner the walk leaves from. Zero for
// every step that ever stood in a shaft before, and what a CURVE is made of: a flight cut into segments, each
// carrying a share of the sweep, so a swept stair is a chain of straight runs that meet at their own angles
// rather than a second kind of geometry. The four numbers stay the step's unturned footprint - the run and the
// width survive the turn, and the corner it turns about does not move.
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
public float Bearing { get; set; }
// Zero divides what the pinned flights left, the way a pillar with no height stands up to what covers it.
// Dragging this flight's lift arrow is what pins it, and pinning one is what a mansion stair IS: three
// risers onto a broad landing, then fourteen more. A landing takes no share unless it is pinned, which is
// what makes it a split level rather than a turn.
public float Rise { get; set; }
// The railings standing on this step's own edges. Empty means the stair's Guard setting decides, which is what
// every stair authored before railings were rows still says.
public List<ArchStairGuardPart> Guards { get; set; } = new();
public bool Climbs => Step == StairStep.Flight;
public bool Guarding( StairEdge edge ) => Guards.Any( guard => guard.Edge == edge );
// What the landing after this flight was worth before a landing was a step of its own. Read once by
// ArchStairLanes.Settle and never written again - Normalize turns it into a real landing lane.
[JsonIgnore( Condition = JsonIgnoreCondition.WhenWritingDefault )]
public float LandingDepth { get; set; }
public float AlongSpan => MathF.Max( 0f, AlongTo - AlongFrom );
public float AcrossSpan => MathF.Max( 0f, AcrossTo - AcrossFrom );
public bool Sideways => Walk is StairWalk.Left or StairWalk.Right;
// The run's own length and width, which swap with the walk: a lane crossed sideways is as long as it is wide.
public float Length => Sideways ? AcrossSpan : AlongSpan;
public float Width => Sideways ? AlongSpan : AcrossSpan;
// The corner the walk leaves from, in the core's own numbers. Every placement is measured out of it and a
// bearing turns about it, so it is the one point on a step that a turn leaves where it was.
public Vector2 Seat => Walk switch
{
StairWalk.Back => new Vector2( AlongTo, AcrossTo ),
StairWalk.Left => new Vector2( AlongTo, AcrossFrom ),
StairWalk.Right => new Vector2( AlongFrom, AcrossTo ),
_ => new Vector2( AlongFrom, AcrossFrom )
};
// The corner the walk leaves from, so the run's rect covers exactly this lane travelling along its own axis
// and across from 0 to Width - the convention every reader of ArchStairRun already keeps.
public Vector2 Foot( ArchStairCore core ) => core.Flat( Seat.x, Seat.y );
public float Cardinal => Walk switch
{
StairWalk.Back => 180f,
StairWalk.Left => 90f,
StairWalk.Right => -90f,
_ => 0f
};
public float Yaw( ArchStairCore core ) => core.Yaw + Cardinal + Bearing;
public ArchStairAxes Axes( ArchStairCore core ) => new() { Origin = Foot( core ), Yaw = Yaw( core ) };
// The step read in the CORE's own frame, turned however it is turned - the one answer to where its four corners
// actually are, so a drawing, a hit test and a shaft's bounds all read the same quad as the generator.
public ArchStairAxes Frame => new() { Origin = Seat, Yaw = Cardinal + Bearing };
public List<Vector2> Corners() => Frame.Rect( 0f, Length, 0f, Width );
// Where the walk arrives, in core coordinates - the edge a landing is derived against.
public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) HeadEdge() => Walk switch
{
StairWalk.Back => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo),
StairWalk.Left => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
StairWalk.Right => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
_ => (AlongTo, AlongTo, AcrossFrom, AcrossTo)
};
public (float AlongFrom, float AlongTo, float AcrossFrom, float AcrossTo) FootEdge() => Walk switch
{
StairWalk.Back => (AlongTo, AlongTo, AcrossFrom, AcrossTo),
StairWalk.Left => (AlongFrom, AlongTo, AcrossFrom, AcrossFrom),
StairWalk.Right => (AlongFrom, AlongTo, AcrossTo, AcrossTo),
_ => (AlongFrom, AlongFrom, AcrossFrom, AcrossTo)
};
// Which way the walk carries in core coordinates, so a derived landing knows where to grow.
public Vector2 Heading => Frame.Along;
// Width runs to the LEFT of travel, which is the axis the step's own width is measured out on.
public Vector2 Leftward => Frame.Across;
public List<Vector2> Loop( ArchStairCore core ) => Axes( core ).Rect( 0f, Length, 0f, Width );
public StairWalk Reversed => Walk.Reversed();
public ArchStairLane Copy() => new()
{
Id = Id,
Step = Step,
AlongFrom = AlongFrom,
AlongTo = AlongTo,
AcrossFrom = AcrossFrom,
AcrossTo = AcrossTo,
Walk = Walk,
Bearing = Bearing,
Rise = Rise,
Guards = Guards.Select( guard => guard.Copy() ).ToList()
};
}