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.
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 ) );
}
}