Editor/Wall/ArchOpeningClearance.cs

Utility class for editor wall/arch openings. Computes horizontal side clearance and vertical headroom for an arch opening using kit, room, wall, building and roof data, and finds the roof covering a wall midpoint.

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

namespace Sunless.Architecture;

// What a wall will actually take, asked in one place so the drag ghost, the placed opening and the
// report cannot disagree: how much of the run each corner claims, and how high a head may reach
// before it runs into the moulding hanging off the ceiling.
public static class ArchOpeningClearance
{
	// The corner takes half a wall - that is where the return face lands - and the architrave needs
	// its own width beyond it, or the casing dies into the wall it turns at.
	public static float Sides( ArchOpeningPreset preset, ArchKit kit )
	{
		var casing = preset is { Cased: true }
			? (preset.CasingWidth > 0f ? preset.CasingWidth : kit.CasingWidth)
			: 0f;

		return kit.WallThickness * 0.5f + MathF.Max( 0f, casing );
	}

	// The room's OWN ceiling hangs deeper than the roof's slab, so a head measured off the roof would run
	// into it. Whichever body is actually overhead decides the headroom.
	public static float Head( ArchRoom room, ArchBuilding building, ArchWall wall, ArchKit kit, float wallHeight )
	{
		if ( room.HasCeiling && !ArchFloorGen.CeilingHeldAbove( building, room ) )
		{
			return wallHeight - ArchFloorGen.CeilingDepth( room, kit ) - Drop( kit );
		}

		var roof = Covering( building, room, wall );

		if ( roof is null || !Corniced( roof ) )
		{
			return wallHeight;
		}

		var ceiling = roof.BaseHeight - kit.FloorThickness - room.BaseHeight;

		return MathF.Min( wallHeight, ceiling - Drop( kit ) );
	}

	// A vault carries no cornice, and a walkway deck can be told to run without one.
	static bool Corniced( ArchRoofPart roof )
	{
		return roof.Ceiling && !roof.VaultedCeiling && (!roof.Walkway || roof.Cornice);
	}

	// The profile hangs BELOW the run line it is swept on, so its drop is how far under zero it reaches.
	static float Drop( ArchKit kit )
	{
		var profile = ArchProfiles.Named( kit, "cornice" );

		return profile is null ? 0f : MathF.Max( 0f, -profile.Min.y );
	}

	// The centreline sits on the outline; grown by 4 before the midpoint test.
	public static ArchRoofPart Covering( ArchBuilding building, ArchRoom room, ArchWall wall )
	{
		if ( building is null || room is null )
		{
			return null;
		}

		var mid = (wall.Start + wall.End) * 0.5f;
		var slack = new Vector2( 4f, 4f );

		return building.Roofs.FirstOrDefault( roof =>
			roof.Level == room.Floor &&
			ArchFloorGen.Contains( ArchFootprint.Rect( roof.Min - slack, roof.Max + slack ), mid ) );
	}
}