Editor partial class for ArchStairShape. Implements helper routines for stair planning: detecting corner wall contacts, computing whether landing sides are walled, fitting stair lanes into housings over climb sequence, building a probe room from storey walls, and classifying turn kinds and angle deltas.
namespace Sunless.Architecture;
public sealed partial class ArchStairShape
{
// Which sides of the landing face a wall - the rail asks this. The pad is NOT grown to meet them:
// a landing is the space the core has left between two flights, and reaching it out to the nearest
// walls would move it off the square the generator builds.
static void Corner( ArchStairPad pad, ArchRoom room, ArchKit kit, ArchKinds kinds )
{
if ( room is null )
{
return;
}
var middle = (pad.AlongFrom + pad.AlongTo) * 0.5f;
pad.WalledFrom = Walled( room, kit, pad.Axes, middle, pad.AcrossFrom, -1f, kinds );
pad.WalledTo = Walled( room, kit, pad.Axes, middle, pad.AcrossTo, 1f, kinds );
// The far end of a turn landing is usually the corner of the stairwell, so it is asked the same
// question the flanks are: a rail run into plaster is balusters standing inside a wall.
pad.WalledHead = ArchProbe.Against( ArchPlanStore.FiledOn( ArchKind.Wall, room, kinds ).OfType<ArchWall>(), kit,
pad.Axes.Flat( pad.AlongTo, (pad.AcrossFrom + pad.AcrossTo) * 0.5f ), pad.Axes.Along );
}
// Measured, not authored: a walled side is a plaster rail and the socket for wall-hung treads.
static bool Walled( ArchRoom room, ArchKit kit, ArchStairAxes axes, float along, float across, float sign, ArchKinds kinds )
{
return room is not null && ArchProbe.Against( ArchPlanStore.FiledOn( ArchKind.Wall, room, kinds ).OfType<ArchWall>(), kit,
axes.Flat( along, across ), axes.Across * sign );
}
// Where every step in the climb may stand once the shell has had what crosses it. Walked in climb order,
// because a step is measured in the frame of the flight below it and the storey it is bounded by is whatever
// the climb has got up to - the same two things the resolve itself carries, so the two walks cannot disagree.
static ArchStairFit[] Fitting(
ArchStairCore core,
IReadOnlyList<ArchStairLane> lanes,
IReadOnlyList<float> climbs,
ArchStairHousings housings,
ArchBuilding building,
ArchKit kit,
float baseHeight )
{
var fits = new ArchStairFit[lanes.Count];
var frame = core.Axes;
var height = baseHeight;
for ( var index = 0; index < lanes.Count; index++ )
{
var lane = lanes[index];
var housing = housings.On( FloorIndex( building, kit, height ) );
if ( lane.Climbs )
{
var axes = lane.Axes( core );
fits[index] = housing.Fit( axes, 0f, MathF.Max( 1f, lane.Length ), 0f, MathF.Max( 1f, lane.Width ) );
frame = axes;
}
else
{
Framed( frame, core, lane, out var alongFrom, out var alongTo, out var acrossFrom, out var acrossTo );
fits[index] = housing.Fit( frame, alongFrom, alongTo, acrossFrom, acrossTo );
}
height += climbs[index];
}
return fits;
}
// One synthetic room so a probe finds a wall whoever owns it, on whatever floor the part stands.
static ArchRoom Probe( ArchRoom room, IEnumerable<ArchRoom> storey, int level, ArchRoom fallback, ArchKinds kinds )
{
if ( storey is null )
{
return room;
}
var walls = storey
.Where( entry => entry.Floor == level )
.SelectMany( entry => ArchPlanStore.FiledOn( ArchKind.Wall, entry, kinds ) )
.OfType<ArchWall>()
.ToList();
if ( walls.Count == 0 )
{
return fallback;
}
return new ArchRoom { Walls = walls };
}
static StairTurn TurnKind( float angle )
{
var turn = MathF.Abs( angle );
if ( turn < 15f )
{
return StairTurn.None;
}
if ( turn > 165f )
{
return StairTurn.Back;
}
return angle > 0f ? StairTurn.Left : StairTurn.Right;
}
internal static float AngleDelta( float degrees )
{
var delta = degrees % 360f;
if ( delta > 180f )
{
delta -= 360f;
}
if ( delta <= -180f )
{
delta += 360f;
}
return delta;
}
}