Editor/Geometry/ArchCornice.cs

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.

File Access
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);
			}
		}
	}
}