Editor/Roof/ArchRoofTrim.cs

Roof trimming and decorative mesh generation helpers for the editor. Contains static methods that build soffits, fascia, parapets, walkway cornices and generic bands by creating ArchMesh geometry, extruding profiles and applying cuts and brushes according to kit/plan data.

File AccessNative Interop
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;

		// Bite into the plate, not a fascia depth: there is no eave behind a parapet.
		Band( canvas, outline, thickness * -0.5f, thickness, ArchContact.Bury( kit, roof.BaseHeight, 1f ), top, wall, abuts, kit, plan, roof.Level, hostId, ArchCutAffects.Trims );

		var profile = kit.FindProfile( "parapet_cap" );

		if ( profile is null || !profile.IsUsable )
		{
			return;
		}

		foreach ( var run in Runs( ArchFootprint.Wind( outline.ToList() ), top, abuts ) )
		{
			canvas.Extrude( run.Raised(), profile, 1f, Rotation.Identity, cap, run.Closed );
		}
	}

	// 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.Cornice )
		{
			return;
		}

		var bite = ArchContact.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 );
			}
		}
	}
}