Editor-side geometry audit utilities for architecture pieces. It provides overlap and intersection tests for facets and flattened polygons, buried-face detection via ray traces, area/centroid/basis helpers, sampling and spatial heuristics, and reporting helpers used to produce ArchFinding entries.
using System;
using System.Collections.Generic;
using System.Linq;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchAudit
{
static bool Overlapping( Facet a, Facet b )
{
return Overlapping( a.Corners, b.Corners, a.Normal, a.Centre );
}
// Sampled, not clipped: concave faces break a convex clip; centre-pulled points skip shared edges.
static bool Overlapping( Vector3[] a, Vector3[] b, Vector3 normal, Vector3 origin )
{
if ( !Near( a, b ) )
{
return false;
}
Basis( ArchMeshContactService.Canonical( normal ), out var right, out var up );
// Both flattened about the SAME origin, or the two 2D loops are in unrelated frames.
var flatA = Flatten( a, origin, right, up );
var flatB = Flatten( b, origin, right, up );
return Samples( flatA ).Count( point => Contains( flatB, point ) ) >= 2
|| Samples( flatB ).Count( point => Contains( flatA, point ) ) >= 2
|| Crossing( flatA, flatB );
}
// Sampling alone only sees an overlap wide enough for centre-pulled points to fall inside it, so a SLIVER -
// a ceiling cap lapping an inch over a lintel, a run four inches wide down a jamb - read as clean and was
// fixed by eye over and over. Two loops whose interiors meet partially always cross an edge, so the crossing
// is the honest test. Strictly interior on both segments, or every pair of butting faces reports as a fight.
static bool Crossing( Vector2[] a, Vector2[] b )
{
for ( var one = 0; one < a.Length; one++ )
{
for ( var two = 0; two < b.Length; two++ )
{
if ( Crosses( a[one], a[(one + 1) % a.Length], b[two], b[(two + 1) % b.Length] ) )
{
return true;
}
}
}
return false;
}
static bool Crosses( Vector2 from, Vector2 to, Vector2 other, Vector2 otherTo )
{
var run = to - from;
var otherRun = otherTo - other;
var denominator = run.x * otherRun.y - run.y * otherRun.x;
if ( MathF.Abs( denominator ) < 0.0001f )
{
return false;
}
var gap = other - from;
var along = (gap.x * otherRun.y - gap.y * otherRun.x) / denominator;
var across = (gap.x * run.y - gap.y * run.x) / denominator;
return along > Grazing && along < 1f - Grazing && across > Grazing && across < 1f - Grazing;
}
// A crossing this close to either end is two faces meeting at a corner, which is contact, not a fight.
const float Grazing = 0.0005f;
static bool Near( Vector3[] a, Vector3[] b )
{
var reach = 0.5f;
return a.Min( corner => corner.x ) - reach <= b.Max( corner => corner.x )
&& a.Max( corner => corner.x ) + reach >= b.Min( corner => corner.x )
&& a.Min( corner => corner.y ) - reach <= b.Max( corner => corner.y )
&& a.Max( corner => corner.y ) + reach >= b.Min( corner => corner.y )
&& a.Min( corner => corner.z ) - reach <= b.Max( corner => corner.z )
&& a.Max( corner => corner.z ) + reach >= b.Min( corner => corner.z );
}
static IEnumerable<Vector2> Samples( Vector2[] flat )
{
var centre = Vector2.Zero;
foreach ( var point in flat )
{
centre += point;
}
centre /= flat.Length;
yield return centre;
foreach ( var point in flat )
{
yield return Vector2.Lerp( point, centre, 0.25f );
yield return Vector2.Lerp( point, centre, 0.6f );
}
}
static Vector2[] Flatten( Vector3[] corners, Vector3 origin, Vector3 right, Vector3 up )
{
var flat = new Vector2[corners.Length];
for ( var index = 0; index < corners.Length; index++ )
{
var offset = corners[index] - origin;
flat[index] = new Vector2( Vector3.Dot( offset, right ), Vector3.Dot( offset, up ) );
}
return flat;
}
static bool Contains( Vector2[] loop, Vector2 point ) => ArchFootprint.Contains( loop, point );
// Coarse on purpose: parts are meant to abut, so only a most-of-the-smaller overlap counts.
static void Intersections( List<Piece> pieces, List<ArchFinding> findings, Dictionary<string, int> totals )
{
var found = 0;
for ( var a = 0; a < pieces.Count; a++ )
{
for ( var b = a + 1; b < pieces.Count; b++ )
{
var one = pieces[a];
var two = pieces[b];
if ( one.Faces.Count == 0 || two.Faces.Count == 0 )
{
continue;
}
var low = Vector3.Max( one.Bounds.Mins, two.Bounds.Mins );
var high = Vector3.Min( one.Bounds.Maxs, two.Bounds.Maxs );
var overlap = high - low;
if ( overlap.x <= 1f || overlap.y <= 1f || overlap.z <= 1f )
{
continue;
}
var smaller = Vector3.Min( one.Bounds.Size, two.Bounds.Size );
var share = MathF.Min( 1f, (overlap.x * overlap.y * overlap.z) / MathF.Max( 1f, smaller.x * smaller.y * smaller.z ) );
if ( share < 0.25f )
{
continue;
}
found++;
findings.Add( new ArchFinding
{
Check = "intersections",
Where = $"{one.Name} vs {two.Name}",
What = $"solids interpenetrate over {overlap.x:0.#} x {overlap.y:0.#} x {overlap.z:0.#} in, {share * 100f:0}% of the smaller part",
At = Say( (low + high) * 0.5f ),
Severity = 0.4f + share * 0.4f
} );
}
}
totals["interpenetrating parts"] = found;
}
// ArchCull's rays, reported not acted on - a rebuild undoes the clean pass.
static void Buried( Scene scene, List<Piece> pieces, List<ArchFinding> findings, Dictionary<string, int> totals )
{
var buried = 0;
foreach ( var piece in pieces )
{
var hidden = 0;
var somewhere = Vector3.Zero;
foreach ( var face in piece.Faces )
{
if ( face.Normal.Length < 0.5f || face.Area < MinArea )
{
continue;
}
var surface = face.Centre + face.Normal * Skin;
var outward = face.Centre + face.Normal * Reach;
if ( !scene.Trace.Ray( outward, surface ).Run().Hit || scene.Trace.Ray( surface, outward ).Run().Hit )
{
continue;
}
hidden++;
somewhere = face.Centre;
}
if ( hidden == 0 )
{
continue;
}
buried += hidden;
findings.Add( new ArchFinding
{
Check = "buried",
Where = piece.Name,
What = $"{hidden} faces are sealed inside other geometry - arch_clean_hidden_faces would remove them",
At = Say( somewhere ),
Severity = 0.2f
} );
}
totals["buried faces"] = buried;
}
static void Basis( Vector3 normal, out Vector3 right, out Vector3 up )
{
var reference = MathF.Abs( normal.z ) > 0.9f ? Vector3.Forward : Vector3.Up;
right = Vector3.Cross( reference, normal ).Normal;
up = Vector3.Cross( normal, right ).Normal;
}
static Vector3 Centroid( Vector3[] corners )
{
var total = Vector3.Zero;
foreach ( var corner in corners )
{
total += corner;
}
return total / corners.Length;
}
static float Area( Vector3[] corners, Vector3 normal )
{
var total = Vector3.Zero;
for ( var index = 0; index < corners.Length; index++ )
{
total += Vector3.Cross( corners[index], corners[(index + 1) % corners.Length] );
}
return MathF.Abs( Vector3.Dot( total * 0.5f, normal.Normal ) );
}
static long Key( Vector3 point )
{
var x = (long)MathF.Round( point.x * Grain );
var y = (long)MathF.Round( point.y * Grain );
var z = (long)MathF.Round( point.z * Grain );
return x * 73856093L ^ y * 19349663L ^ z * 83492791L;
}
public static string Path( GameObject node )
{
var names = new List<string>();
while ( node.IsValid() && node.Name != ArchScene.RootName )
{
names.Add( node.Name );
node = node.Parent;
}
names.Reverse();
return string.Join( "/", names );
}
static string Say( Vector3 point ) => $"{point.x:0.#},{point.y:0.#},{point.z:0.#}";
}