Editor/Roof/ArchCrossGable.cs

Editor utility for resolving cross-gable junctions between gable roof parts. It finds pairs of gable roofs that form a cross-gable, computes geometric junction data (along/eave/apex positions), provides point conversion helpers, and supplies blockers for abutting geometry.

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

namespace Sunless.Architecture;

public sealed class ArchCrossGableJunction
{
	public ArchRoofPart Host { get; init; }
	public ArchRoofPart Branch { get; init; }
	public bool HostAtMinimum { get; init; }
	public bool BranchAtMinimum { get; init; }
	public float AlongMin { get; init; }
	public float AlongMid { get; init; }
	public float AlongMax { get; init; }
	public float WallAcross { get; init; }
	public float EaveAcross { get; init; }
	public float ApexAcross { get; init; }

	public float HostBoundaryAt( float along )
	{
		if ( along <= AlongMin || along >= AlongMax )
		{
			return EaveAcross;
		}

		var inward = ApexAcross - EaveAcross;

		if ( along <= AlongMid )
		{
			return EaveAcross + inward * (along - AlongMin) / MathF.Max( 0.01f, AlongMid - AlongMin );
		}

		return EaveAcross + inward * (AlongMax - along) / MathF.Max( 0.01f, AlongMax - AlongMid );
	}

	public Vector3 HostPoint( float along, float across, float height )
	{
		return Host.RidgeAlongX
			? new Vector3( along, across, height )
			: new Vector3( across, along, height );
	}

	public Vector3 BranchPoint( float along, float across, float height )
	{
		return Branch.RidgeAlongX
			? new Vector3( along, across, height )
			: new Vector3( across, along, height );
	}
}

public static class ArchCrossGableService
{
	public static List<ArchCrossGableJunction> Resolve( ArchBuilding building, ArchKit kit, ArchKinds table = null )
	{
		var roofs = ArchPlanStore.FiledOn( ArchKind.Roof, building, table )
			.OfType<ArchRoofPart>()
			.Where( roof => roof.Style == RoofStyle.Gable && roof.Outline().Count == 4 )
			.ToList();
		var junctions = new List<ArchCrossGableJunction>();

		for ( var hostIndex = 0; hostIndex < roofs.Count; hostIndex++ )
		{
			for ( var branchIndex = 0; branchIndex < roofs.Count; branchIndex++ )
			{
				if ( hostIndex == branchIndex )
				{
					continue;
				}

				if ( TryResolve( roofs[hostIndex], roofs[branchIndex], kit ) is { } junction )
				{
					junctions.Add( junction );
				}
			}
		}

		return junctions;
	}

	public static bool JoinsBranchEnd(
		ArchRoofPart roof,
		Vector2 from,
		Vector2 to,
		IReadOnlyList<ArchCrossGableJunction> junctions )
	{
		var along = roof.RidgeAlongX
			? (from.x + to.x) * 0.5f
			: (from.y + to.y) * 0.5f;

		return junctions.Any( junction =>
			ReferenceEquals( junction.Branch, roof ) &&
			MathF.Abs( along - junction.WallAcross ) < 0.5f );
	}

	public static List<List<Vector2>> FittingAbutments(
		ArchRoofPart roof,
		ArchKit kit,
		IReadOnlyList<ArchCrossGableJunction> junctions )
	{
		var blockers = new List<List<Vector2>>();

		foreach ( var junction in junctions.Where( junction => ReferenceEquals( junction.Host, roof ) ) )
		{
			var reach = MathF.Max( DeckReach( roof, kit ), DeckReach( junction.Branch, kit ) );
			var outline = ArchFootprint.Wind( junction.Branch.Outline() );

			blockers.AddRange( ArchFootprint.Grow( new[] { outline }, reach ) );
		}

		return blockers;
	}

	static ArchCrossGableJunction TryResolve( ArchRoofPart host, ArchRoofPart branch, ArchKit kit )
	{
		if ( host.Level != branch.Level ||
			host.RidgeAlongX == branch.RidgeAlongX ||
			MathF.Abs( host.BaseHeight - branch.BaseHeight ) > 0.5f )
		{
			return null;
		}

		ArchFootprint.Bounds( host.Outline(), out var hostMin, out var hostMax );
		ArchFootprint.Bounds( branch.Outline(), out var branchMin, out var branchMax );

		var hostAlongMin = host.RidgeAlongX ? hostMin.x : hostMin.y;
		var hostAlongMax = host.RidgeAlongX ? hostMax.x : hostMax.y;
		var hostAcrossMin = host.RidgeAlongX ? hostMin.y : hostMin.x;
		var hostAcrossMax = host.RidgeAlongX ? hostMax.y : hostMax.x;
		var branchAcrossMin = branch.RidgeAlongX ? branchMin.y : branchMin.x;
		var branchAcrossMax = branch.RidgeAlongX ? branchMax.y : branchMax.x;
		var branchAlongMin = branch.RidgeAlongX ? branchMin.x : branchMin.y;
		var branchAlongMax = branch.RidgeAlongX ? branchMax.x : branchMax.y;
		var touchesMinimum = MathF.Abs( branchAlongMax - hostAcrossMin ) < 0.5f;
		var touchesMaximum = MathF.Abs( branchAlongMin - hostAcrossMax ) < 0.5f;

		if ( !touchesMinimum && !touchesMaximum )
		{
			return null;
		}

		if ( branchAcrossMin < hostAlongMin - 0.5f || branchAcrossMax > hostAlongMax + 0.5f )
		{
			return null;
		}

		var hostReach = DeckReach( host, kit );
		var branchReach = DeckReach( branch, kit );
		var alongMin = branchAcrossMin - branchReach;
		var alongMax = branchAcrossMax + branchReach;
		var alongMid = (alongMin + alongMax) * 0.5f;
		var eaveAcross = touchesMinimum ? hostAcrossMin - hostReach : hostAcrossMax + hostReach;
		var inward = touchesMinimum ? 1f : -1f;
		var hostSlope = ArchRoofPlane.Slope( host );
		var branchSlope = ArchRoofPlane.Slope( branch );

		if ( hostSlope < 0.01f || branchSlope < 0.01f )
		{
			return null;
		}

		var branchRise = branchSlope * (alongMax - alongMin) * 0.5f;
		var depth = branchRise / hostSlope;
		var apexAcross = eaveAcross + inward * depth;
		var hostRidge = (hostAcrossMin + hostAcrossMax) * 0.5f;

		if ( touchesMinimum ? apexAcross >= hostRidge - 0.5f : apexAcross <= hostRidge + 0.5f )
		{
			return null;
		}

		return new ArchCrossGableJunction
		{
			Host = host,
			Branch = branch,
			HostAtMinimum = touchesMinimum,
			BranchAtMinimum = touchesMaximum,
			AlongMin = alongMin,
			AlongMid = alongMid,
			AlongMax = alongMax,
			WallAcross = touchesMinimum ? hostAcrossMin : hostAcrossMax,
			EaveAcross = eaveAcross,
			ApexAcross = apexAcross
		};
	}

	static float DeckReach( ArchRoofPart roof, ArchKit kit )
	{
		return roof.Overhang + (roof.Fascia ? ArchProfiles.FasciaDepth( kit ) + 1.5f : 0f);
	}
}