Editor/Output/ArchAudit.Gaps.cs
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( ArchFaceOverlap.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 = ArchFaceOverlap.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;
}
}