Editor/Stair/ArchStairShape.Plan.cs

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.

File Access
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;
	}
}