Editor utility for generating cornice mouldings around room ceilings and passage edges. It finds a named cornice profile, computes inset loops, and emits mesh runs (sampled and broken over wells) to an ArchMesh using provided kit, height and brush.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// The moulding belongs to the CEILING, not to the roof over it. A room that carries its own ceiling asks for it
// here rather than growing a second sweeper, and a deck that trims itself asks for the same runs.
public static class ArchCornice
{
public static void Around( ArchMesh canvas, IReadOnlyList<List<Vector2>> region, IReadOnlyList<ArchFloorCutout> wells, ArchKit kit, float height, ArchBrush brush )
{
foreach ( var outline in ArchFootprint.Outer( region ) )
{
Around( canvas, outline, wells, kit, height, brush );
}
}
// Runs on the inner wall face (erode half a wall) and breaks over stairwells - no ceiling, nothing to trim.
public static void Around( ArchMesh canvas, IReadOnlyList<Vector2> outline, IReadOnlyList<ArchFloorCutout> wells, ArchKit kit, float height, ArchBrush brush )
{
if ( ArchProfiles.Named( kit, "cornice" ) is not { } profile )
{
return;
}
// Bitten into wall and slab, or the moulding's back fights the surfaces it trims.
var bite = ArchContact.Bite( kit );
var section = new ArchProfileSection { Profile = profile, Height = height + bite };
foreach ( var loop in ArchFootprint.Shrink( new[] { outline }, kit.WallThickness * 0.5f - bite ) )
{
if ( loop.Count < 3 )
{
continue;
}
ArchRun.Around( loop ).Sampled( 8f ).BreakOver( wells ).Emit( canvas, section, brush );
}
}
// The ceiling a PASSAGE carries itself, trimmed the way the deck over it is: sides only. A room spanning two
// buildings is open at both mouths, so a loop taken all the way round runs a board across each joint and dies
// inside the host wall it opens into - the defect this exists to prevent.
public static void AlongPassage( ArchMesh canvas, IReadOnlyList<List<Vector2>> region, bool alongX,
IReadOnlyList<ArchFloorCutout> wells, ArchKit kit, float height, ArchBrush brush )
{
if ( ArchProfiles.Named( kit, "cornice" ) is null )
{
return;
}
var inset = kit.WallThickness * 0.5f - ArchContact.Bite( kit );
foreach ( var outline in ArchFootprint.Outer( region ) )
{
foreach ( var loop in ArchFootprint.Shrink( new[] { outline }, inset ) )
{
if ( loop.Count >= 3 )
{
Sides( canvas, loop, alongX, wells, kit, height, brush );
}
}
}
}
// Held a bite SHORT of the host wall's face at each mouth, never lapped into it. A cap landing exactly on that
// face is a coplanar pair and flickers; a cap driven past it is a moulding standing inside somebody's room.
// Of the two, the run gives way.
public static void Sides( ArchMesh canvas, IReadOnlyList<Vector2> loop, bool alongX,
IReadOnlyList<ArchFloorCutout> wells, ArchKit kit, float height, ArchBrush brush )
{
var bite = ArchContact.Bite( kit );
var section = new ArchProfileSection { Profile = ArchProfiles.Named( kit, "cornice" ), Height = height + bite };
foreach ( var edge in RunEdges( alongX, loop ) )
{
var run = (edge.To - edge.From).Normal;
ArchRun.Along( new List<Vector2> { edge.From + run * bite, edge.To - run * bite } )
.Sampled( 8f )
.BreakOver( wells )
.Emit( canvas, section, brush );
}
}
// The edges that follow the run - a mouth's own edge carries no wall to trim. Read off each edge's own
// direction rather than a corner index, because a shrunk loop is wound by the footprint service and a passage
// cut about by a bool need not be four corners at all.
static IEnumerable<(Vector2 From, Vector2 To)> RunEdges( bool alongX, IReadOnlyList<Vector2> loop )
{
for ( var index = 0; index < loop.Count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % loop.Count];
var run = to - from;
if ( alongX == MathF.Abs( run.x ) >= MathF.Abs( run.y ) )
{
yield return (from, to);
}
}
}
}