Editor/Roof/ArchRoofTrim.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchRoofGen {
static void Soffit( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<List<Vector2>> plates, ArchKit kit, ArchBrush brush, Func<Vector2, Vector2, bool> abuts, ArchPlan plan, int hostId ) {
var height = roof.BaseHeight - kit.FasciaHeight;
var thickness = Math.Max( 1f, kit.CasingDepth );
foreach ( var loop in plates ) {
Band( canvas, loop, 0f, roof.Overhang, height - thickness, height, brush, abuts, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );
}
}
static void Fascia( ArchMesh canvas, ArchRoofPart roof, List<List<Vector2>> eaves, ArchKit kit, ArchBrush brush, Func<Vector2, Vector2, bool> abuts, ArchPlan plan, int hostId ) {
var height = kit.FasciaHeight;
if ( height <= 0.1f ) {
return;
}
var top = roof.BaseHeight;
// Closed on top: the deck climbs from BaseHeight, so an open top reads as a slot.
foreach ( var loop in eaves ) {
Band( canvas, loop, 0f, ArchProfiles.FasciaDepth( kit ), top - height, top, brush, abuts, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );
}
}
// Wall carried up past the deck: how a low section butts a taller one without an eave.
static void Parapet( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<Vector2> outline, ArchKit kit, ArchBrush wall, ArchBrush cap, Func<Vector2, Vector2, bool> abuts, ArchPlan plan, int hostId ) {
var height = MathF.Max( 4f, roof.ParapetHeight );
var thickness = MathF.Max( 2f, kit.WallThickness );
var top = roof.BaseHeight + height;
var crowned = kit.FindCornice( roof.Cornice ) is { Stands: true };
// Bite into the plate, not a fascia depth: there is no eave behind a parapet. Under a crown the head laps
// UP into the entablature's own top course as well, or the two share the parapet's top plane all the way
// round and the crown reads as a band with a second wall standing on it.
Band( canvas, outline, thickness * -0.5f, thickness, ArchLap.Bury( kit, roof.BaseHeight, 1f ),
crowned ? ArchLap.Bury( kit, top, 1f ) : top, wall, abuts, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );
var profile = kit.FindProfile( "parapet_cap" );
// A crowned deck is finished by the cornice's own top course, exactly as a crowned wall head is.
if ( profile is null || !profile.IsUsable || crowned ) {
return;
}
foreach ( var run in Runs( ArchFootprint.Wind( outline.ToList() ), top, abuts ) ) {
canvas.Extrude( run.Raised(), profile, 1f, Rotation.Identity, cap, run.Closed );
}
}
// The entablature the deck EDGE wears - the same RING a storey head wears, hung from the parapet top where one
// stands and from the eave line where none does. One emitter, two callers.
static void Crown(
ArchMesh canvas,
ArchRoofPart roof,
IReadOnlyList<Vector2> outline,
ArchKit kit,
ArchStyle style,
ArchPalette[] chain,
Func<Vector2, Vector2, bool> abuts,
ArchPlan plan,
int hostId ) {
new ArchCorniceRing {
Crown = kit.FindCornice( roof.Cornice ),
Outline = outline,
Head = roof.Parapet ? roof.BaseHeight + MathF.Max( 4f, roof.ParapetHeight ) : roof.BaseHeight,
Thickness = MathF.Max( 2f, kit.WallThickness ),
Abuts = abuts,
Kit = kit,
Style = style,
Chain = chain,
Plan = plan,
Level = roof.Level,
HostId = hostId
}.Lay( canvas );
}
// A walkway deck is open at the mouths, so its cornice runs only the two long walls.
static void WalkwayCornice( ArchMesh canvas, ArchRoofPart roof, IReadOnlyList<ArchFloorCutout> wells, ArchKit kit, float height, ArchBrush brush ) {
var profile = ArchProfiles.Named( kit, "cornice" );
if ( profile is null || !roof.CeilingCornice ) {
return;
}
var bite = ArchLap.Bite( kit );
// Eroded ACROSS the run only, to bring the moulding onto the passage face. Eroding along it
// too - which a square inset does - stopped the cornice half a wall short of each mouth,
// where the run has to reach the host wall's inner face and cap on it.
var erode = roof.RidgeAlongX
? new Vector2( 0f, kit.WallThickness * 0.5f - bite )
: new Vector2( kit.WallThickness * 0.5f - bite, 0f );
ArchCornice.Sides( canvas, new[]
{
new Vector2( roof.Min.x + erode.x, roof.Min.y + erode.y ),
new Vector2( roof.Max.x - erode.x, roof.Min.y + erode.y ),
new Vector2( roof.Max.x - erode.x, roof.Max.y - erode.y ),
new Vector2( roof.Min.x + erode.x, roof.Max.y - erode.y )
}, roof.RidgeAlongX, wells, kit, height, brush );
}
static void Band(
ArchMesh canvas,
IReadOnlyList<Vector2> loop,
float shift,
float depth,
float bottom,
float top,
ArchBrush brush,
Func<Vector2, Vector2, bool> abuts,
ArchKit kit = null,
ArchPlan plan = null,
int level = 0,
int hostId = 0,
ArchCutAffects target = ArchCutAffects.Trims ) {
if ( loop.Count < 3 ) {
return;
}
var section = new ArchBandSection {
Shift = shift,
Depth = depth,
Bottom = bottom,
Top = top
};
// Leveled at the band's own bottom, the way a gutter run is leveled at the channel: a cut has to
// reach the band's lower edge before it can break the run, so a shaft that only opens the storey
// below leaves a fascia running over its head.
foreach ( var run in ArchBrokenRun.Of( loop, abuts ).Level( bottom ).Resolve() ) {
foreach ( var surviving in ArchCut.Runs( plan, kit, level, hostId, run, target ) ) {
section.Emit( canvas, surviving, brush );
}
}
}
}