Editor/Roof/ArchRoofCapping.cs

Editor utility that generates roof capping geometry. It finds ridge cap profiles, extrudes cap bands along roof creases, places corner boxes at junctions, and computes abutted edge runs for flat decks.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchRoofGen
{
	// By exact name: a kit saved before the cap existed just gets no cap.
	static void Capping( ArchMesh canvas, List<(Vector3 From, Vector3 To)> creases, float thickness, ArchKit kit, ArchBrush brush, ArchPlan plan, int level, int hostId )
	{
		var profile = ArchProfiles.Named( kit, "ridge_cap" );

		if ( profile is null )
		{
			return;
		}

		var lift = Vector3.Up * thickness;
		var reach = MathF.Max( 1f, profile.Max.x );
		var junctions = Junctions( creases );

		// Corners stop short (one block takes them); collinear bands overlap; hip low ends stop at the eave.
		foreach ( var crease in creases )
		{
			var along = (crease.To - crease.From).Normal;
			var flat = MathF.Abs( crease.To.z - crease.From.z ) < 0.1f;

			var from = crease.From + lift + along * (Meets( junctions, crease.From ) ? reach * 0.6f : flat ? -reach * 0.75f : 0f);
			var to = crease.To + lift - along * (Meets( junctions, crease.To ) ? reach * 0.6f : -reach * 0.75f);

			if ( (to - from).Length < 1f )
			{
				continue;
			}

			foreach ( var span in ArchCut.Outside( plan, kit, level, hostId,
				new Vector2( from.x, from.y ), new Vector2( to.x, to.y ),
				MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), ArchCutAffects.Roofs ) )
			{
				canvas.Extrude( new List<Vector3> { Vector3.Lerp( from, to, span.From ), Vector3.Lerp( from, to, span.To ) }, profile, 1f, Rotation.Identity, brush );
			}
		}

		foreach ( var node in junctions )
		{
			var corner = new Vector3( reach, reach, 0f );
			var seat = node + lift;

			// The bands round it break over a cut, so the block that takes their corner has to go with them -
			// left standing it is a cube floating in mid-air over the hole, attached to nothing.
			if ( ArchCut.Covers( plan, kit, level, hostId, seat + Vector3.Up * profile.Min.y, seat + Vector3.Up * profile.Max.y, ArchCutAffects.Roofs ) )
			{
				continue;
			}

			canvas.Box(
				seat + Vector3.Up * profile.Min.y - corner,
				seat + Vector3.Up * profile.Max.y + corner,
				brush );
		}
	}

	// Same-direction ends at one spot are one band split, not a corner - left to overlap.
	static List<Vector3> Junctions( List<(Vector3 From, Vector3 To)> creases )
	{
		var ends = new List<(Vector3 At, Vector3 Along)>();

		foreach ( var crease in creases )
		{
			var along = (crease.To - crease.From).Normal;

			ends.Add( (crease.From, along) );
			ends.Add( (crease.To, along) );
		}

		var junctions = new List<Vector3>();

		foreach ( var end in ends )
		{
			var meeting = ends.Where( other => (other.At - end.At).Length < 1f ).ToList();

			if ( meeting.Count < 2 || meeting.All( other => MathF.Abs( Vector3.Dot( other.Along, end.Along ) ) > 0.99f ) )
			{
				continue;
			}

			if ( !junctions.Any( node => (node - end.At).Length < 1f ) )
			{
				junctions.Add( end.At );
			}
		}

		return junctions;
	}

	static bool Meets( List<Vector3> junctions, Vector3 point )
	{
		return junctions.Any( node => (node - point).Length < 1f );
	}

	// A flat deck's only capping is the flashing where it dies into a taller section.
	static List<(Vector3 From, Vector3 To)> Abutted( List<List<Vector2>> loops, Func<Vector2, Vector2, bool> abuts, float height )
	{
		var lines = new List<(Vector3 From, Vector3 To)>();

		foreach ( var loop in loops )
		{
			var blocked = ArchBrokenRun.Blocked( loop, abuts );

			for ( var index = 0; index < loop.Count; index++ )
			{
				var from = loop[index];
				var to = loop[(index + 1) % loop.Count];

				if ( blocked[index] && (to - from).Length > 4f )
				{
					lines.Add( (new Vector3( from.x, from.y, height ), new Vector3( to.x, to.y, height )) );
				}
			}
		}

		return lines;
	}
}