Editor audit utilities for architecture meshes. Contains methods to find border gaps, check reachability to nearby faces, detect coverage on facets, build border lists, detect degenerate faces and coplanar overlapping faces, and accumulate findings/totals.
using System;
using System.Collections.Generic;
using System.Linq;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchAudit
{
// Sampled along its length, so a border half covered by the part next door still reports.
static float? Unmet( Border border, List<Piece> pieces )
{
var nearest = float.MaxValue;
foreach ( var fraction in new[] { 0.25f, 0.5f, 0.75f } )
{
var point = border.Along( fraction );
var gap = Reaches( point, border.Face, pieces );
if ( gap <= Closing )
{
return null;
}
nearest = MathF.Min( nearest, gap );
}
return nearest;
}
// How far this point is from the nearest surface other than the border's own. Inside the face's
// own outline, or a border running past a wall's end would read as landing on it.
static float Reaches( Vector3 point, Facet owner, List<Piece> pieces )
{
var nearest = float.MaxValue;
foreach ( var piece in pieces )
{
if ( Outside( piece.Bounds, point, MissReach ) )
{
continue;
}
foreach ( var face in piece.Faces )
{
if ( ReferenceEquals( face, owner ) || face.Normal.Length < 0.5f || face.Area < MinArea )
{
continue;
}
var gap = MathF.Abs( Vector3.Dot( point - face.Centre, face.Normal ) );
if ( gap >= nearest || gap > MissReach || !Covers( face, point ) )
{
continue;
}
nearest = gap;
}
}
return nearest;
}
// Inside the outline OR on it. A mitre's two cut faces meet exactly, so the contact pass deletes
// one of them as wholly covered - and the border it leaves behind lies on the survivor's own
// EDGE, which a strict containment test calls open. That is a met mitre, not a hole.
static bool Covers( Facet face, Vector3 point )
{
Basis( ArchMeshContactService.Canonical( face.Normal ), out var right, out var up );
var flat = Flatten( face.Corners, face.Centre, right, up );
var offset = point - face.Centre;
var flattened = new Vector2( Vector3.Dot( offset, right ), Vector3.Dot( offset, up ) );
return Contains( flat, flattened ) || Rims( flat, flattened );
}
static bool Rims( Vector2[] loop, Vector2 point )
{
for ( var index = 0; index < loop.Length; index++ )
{
var from = loop[index];
var run = loop[(index + 1) % loop.Length] - from;
var length = run.Length;
var along = length < 0.01f ? 0f : Math.Clamp( Vector2.Dot( point - from, run / length ), 0f, length );
// A hair, not a Closing: the border a deleted mitre leaves lies exactly ON the survivor's
// edge. Anything wider and every edge in the mesh closes every border near it.
if ( (point - (from + (length < 0.01f ? Vector2.Zero : run / length * along))).Length <= 0.05f )
{
return true;
}
}
return false;
}
static bool Outside( BBox bounds, Vector3 point, float reach )
{
return point.x < bounds.Mins.x - reach || point.x > bounds.Maxs.x + reach
|| point.y < bounds.Mins.y - reach || point.y > bounds.Maxs.y + reach
|| point.z < bounds.Mins.z - reach || point.z > bounds.Maxs.z + reach;
}
static List<Border> Borders( List<Piece> pieces )
{
var borders = new List<Border>();
foreach ( var piece in pieces )
{
var walked = new Dictionary<(long, long), (Facet Face, Vector3 From, Vector3 To)>();
var counts = new Dictionary<(long, long), int>();
foreach ( var face in piece.Faces )
{
for ( var index = 0; index < face.Corners.Length; index++ )
{
var one = face.Corners[index];
var two = face.Corners[(index + 1) % face.Corners.Length];
var from = Key( one );
var to = Key( two );
if ( from == to )
{
continue;
}
var edge = from < to ? (from, to) : (to, from);
counts[edge] = counts.GetValueOrDefault( edge ) + 1;
walked[edge] = (face, one, two);
}
}
foreach ( var (edge, _) in counts.Where( entry => entry.Value == 1 ) )
{
var (face, from, to) = walked[edge];
borders.Add( new Border { Piece = piece, Face = face, From = from, To = to } );
}
}
return borders;
}
static (long, long) Span( Border border )
{
var from = Key( border.From );
var to = Key( border.To );
return from < to ? (from, to) : (to, from);
}
static void Degenerate( List<Piece> pieces, List<ArchFinding> findings, Dictionary<string, int> totals )
{
var slivers = 0;
var normals = 0;
foreach ( var piece in pieces )
{
foreach ( var face in piece.Faces )
{
if ( face.Normal.Length < 0.5f || !float.IsFinite( face.Normal.x ) )
{
normals++;
findings.Add( new ArchFinding
{
Check = "degenerate",
Where = piece.Name,
What = "face has no usable normal - its corners are collinear",
At = Say( face.Centre ),
Severity = 0.8f
} );
continue;
}
if ( face.Area >= MinArea )
{
continue;
}
slivers++;
findings.Add( new ArchFinding
{
Check = "degenerate",
Where = piece.Name,
What = $"sliver face, area {face.Area:0.###} sq in",
At = Say( face.Centre ),
Severity = 0.35f
} );
}
}
totals["sliver faces"] = slivers;
totals["bad normals"] = normals;
}
// Overlapping faces on one plane - the z-fight the generators' constants avoid.
static void Coplanar( List<Piece> pieces, List<ArchFinding> findings, Dictionary<string, int> totals, List<string> truncated )
{
var planes = new Dictionary<(long, long, long, long), List<(Piece Piece, Facet Face)>>();
foreach ( var piece in pieces )
{
foreach ( var face in piece.Faces )
{
if ( face.Normal.Length < 0.5f || face.Area < MinArea )
{
continue;
}
// Unsigned: back-to-back is the commonest z-fight, and those normals are opposite.
var key = ArchMeshContactService.PlaneKey( face.Normal, face.Centre );
if ( !planes.TryGetValue( key, out var group ) )
{
group = new List<(Piece, Facet)>();
planes[key] = group;
}
group.Add( (piece, face) );
}
}
var overlaps = 0;
var budget = PairBudget;
foreach ( var group in planes.Values.Where( group => group.Count > 1 ) )
{
for ( var a = 0; a < group.Count; a++ )
{
for ( var b = a + 1; b < group.Count; b++ )
{
if ( budget-- <= 0 )
{
truncated.Add( "coplanar comparison budget reached - narrow the target for a complete answer." );
totals["coplanar overlaps"] = overlaps;
return;
}
var (pieceA, faceA) = group[a];
var (pieceB, faceB) = group[b];
if ( !Overlapping( faceA, faceB ) )
{
continue;
}
overlaps++;
var same = ReferenceEquals( pieceA, pieceB );
var facing = Vector3.Dot( faceA.Normal, faceB.Normal ) < 0f ? "back to back" : "stacked";
findings.Add( new ArchFinding
{
Check = "coplanar",
Where = same ? pieceA.Name : $"{pieceA.Name} vs {pieceB.Name}",
What = $"{facing} faces share a plane and overlap - these will z-fight",
At = Say( faceA.Centre ),
Severity = same ? 0.7f : 0.85f
} );
}
}
}
totals["coplanar overlaps"] = overlaps;
}
}