Editor/Stair/ArchStairWallRakes.cs

Editor class that computes wall rakes (trim pieces) produced by stairs and landings. It scans building rooms for stair parts, determines stair shapes and runs, and returns ArchWallRake entries where stairs or landings lie against a given wall.

File Access
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sunless.Architecture;

public sealed class ArchStairWallRakes : IArchWallRakes
{
	public IReadOnlyList<ArchWallRake> Along(
		ArchWall wall,
		ArchRoom room,
		ArchBuilding building,
		ArchPlan plan,
		ArchKit kit,
		float half,
		float wallHeight )
	{
		var rakes = new List<ArchWallRake>();

		if ( building is null )
		{
			return rakes;
		}

		var kinds = ArchKinds.Load();
		var board = MathF.Max( 1f, kit.BaseboardHeight );

		foreach ( var candidate in building.Rooms )
		{
			foreach ( var stair in plan.Filed( ArchKind.Stair, candidate, kinds ).OfType<ArchStairPart>() )
			{
				var shape = ArchStairShape.Resolve( stair, candidate, kit, building.Rooms );

				Landings( rakes, wall, shape, room, kit, half, wallHeight );

				foreach ( var run in shape.Runs )
				{
					if ( !Against( wall, run.Axes, run.Length, run.Width, kit, half, out var foot, out var sign ) )
					{
						continue;
					}

					var slope = MathF.Max( 0.01f, run.Rise / MathF.Max( 1f, run.Going ) );
					var low = run.BaseHeight - room.BaseHeight;
					var start = MathF.Max( -run.Going, (-low - run.Rise - board) / slope );
					var finish = MathF.Min( run.Length - run.Going, (wallHeight - low) / slope );

					if ( finish - start < 1f )
					{
						continue;
					}

					var lowLift = low + start * slope;
					var highLift = low + finish * slope;

					rakes.Add( sign > 0f
						? new ArchWallRake( foot + start, foot + finish, lowLift, highLift, run.Rise, wallHeight )
						: new ArchWallRake( foot - finish, foot - start, highLift, lowLift, run.Rise, wallHeight ) );
				}
			}
		}

		return rakes;
	}

	static void Landings( List<ArchWallRake> rakes, ArchWall wall, ArchStairShape shape, ArchRoom room, ArchKit kit, float half, float wallHeight )
	{
		foreach ( var pad in shape.Pads )
		{
			var lift = pad.Height - room.BaseHeight;

			if ( lift < 1f || lift > wallHeight )
			{
				continue;
			}

			var length = pad.AlongTo - pad.AlongFrom;
			var width = pad.AcrossTo - pad.AcrossFrom;
			var lengthways = new ArchStairAxes { Origin = pad.Axes.Flat( pad.AlongFrom, pad.AcrossFrom ), Yaw = pad.Axes.Yaw };
			var crossways = new ArchStairAxes { Origin = pad.Axes.Flat( pad.AlongTo, pad.AcrossFrom ), Yaw = pad.Axes.Yaw + 90f };

			if ( Against( wall, lengthways, length, width, kit, half, out var foot, out var sign ) )
			{
				rakes.Add( Level( foot, length, lift, sign, wallHeight ) );
				continue;
			}

			if ( Against( wall, crossways, width, length, kit, half, out foot, out sign ) )
			{
				rakes.Add( Level( foot, width, lift, sign, wallHeight ) );
			}
		}
	}

	static ArchWallRake Level( float foot, float length, float lift, float sign, float wallHeight )
	{
		return sign > 0f
			? new ArchWallRake( foot, foot + length, lift, lift, 0f, wallHeight )
			: new ArchWallRake( foot - length, foot, lift, lift, 0f, wallHeight );
	}

	static bool Against( ArchWall wall, ArchStairAxes axes, float length, float width, ArchKit kit, float half, out float foot, out float sign )
	{
		foot = 0f;
		sign = 1f;

		var lean = Vector2.Dot( axes.Along, wall.Direction );

		if ( MathF.Abs( lean ) < 0.98f )
		{
			return false;
		}

		foreach ( var edge in new[] { 0f, width } )
		{
			var middle = axes.Flat( length * 0.5f, edge );
			var offset = Vector2.Dot( middle - wall.Start, wall.Normal );

			if ( offset < -half - 1f || offset > half + ArchProbe.Gap( kit ) )
			{
				continue;
			}

			foot = Vector2.Dot( axes.Flat( 0f, edge ) - wall.Start, wall.Direction );
			sign = lean > 0f ? 1f : -1f;

			return true;
		}

		return false;
	}
}