Editor/Stair/ArchApproachPlacement.cs

Editor utility that computes placement for approach/seat parts and finds the nearest exterior wall. Seat(...) snaps dimensions to a grid and returns an ArchApproachPart with origin, yaw, width, run, kerbs, rails and splay. NearestExteriorWall(...) iterates buildings/rooms/walls to find the closest outward-facing wall on a given floor and returns the wall plus out parameters for building, room, along, distance and outward vector.

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

namespace Sunless.Architecture;

public static class ArchApproachPlacement
{
	public static ArchApproachPart Seat(
		ArchWall wall,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		float along,
		ApproachKind kind,
		float width,
		float run,
		bool kerbs,
		bool rails,
		float splay )
	{
		var grid = new ArchGridService();
		var unit = grid.SubgridSize( 4 );
		var maximumWidth = MathF.Max( unit, MathF.Floor( wall.Length / unit ) * unit );
		var snappedWidth = Math.Clamp( grid.Subgrid( MathF.Max( unit, width ), 4 ), unit, maximumWidth );
		var snappedAlong = grid.Subgrid( along, 4 );
		var centreAlong = Math.Clamp( snappedAlong, snappedWidth * 0.5f, wall.Length - snappedWidth * 0.5f );
		var thickness = wall.Thickness > 0f ? wall.Thickness : kit.WallThickness;
		var outward = ArchWallFaces.Outward( wall, room, building );

		return new ArchApproachPart
		{
			Kind = kind,
			Origin = wall.PointAt( centreAlong ) + outward * (thickness * 0.5f + MathF.Max( 0f, kit.FoundationOversize )),
			Yaw = MathF.Atan2( outward.y, outward.x ).RadianToDegree(),
			Width = snappedWidth,
			Run = run > 0f ? MathF.Max( unit, grid.Subgrid( run, 4 ) ) : 0f,
			Kerbs = kerbs,
			Rails = rails,
			Splay = splay
		};
	}

	public static ArchWall NearestExteriorWall(
		IEnumerable<ArchBuilding> buildings,
		int level,
		Vector2 point,
		out ArchBuilding building,
		out ArchRoom room,
		out float along,
		out float distance,
		out Vector2 outward )
	{
		ArchWall best = null;
		building = null;
		room = null;
		along = 0f;
		distance = float.MaxValue;
		outward = default;

		foreach ( var candidateBuilding in buildings )
		{
			foreach ( var candidateRoom in candidateBuilding.Rooms )
			{
				if ( candidateRoom.Floor != level )
				{
					continue;
				}

				foreach ( var candidateWall in candidateRoom.Walls )
				{
					var length = candidateWall.Length;

					if ( length < 0.5f || !ArchWallFaces.TryOutward( candidateWall, candidateRoom, candidateBuilding, out var candidateOutward ) )
					{
						continue;
					}

					var projected = Math.Clamp( Vector2.Dot( point - candidateWall.Start, candidateWall.Direction ), 0f, length );
					var gap = (point - candidateWall.PointAt( projected )).Length;

					if ( gap >= distance )
					{
						continue;
					}

					best = candidateWall;
					building = candidateBuilding;
					room = candidateRoom;
					along = projected;
					distance = gap;
					outward = candidateOutward;
				}
			}
		}

		return best;
	}
}