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.
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);
}
}