Editor service that finds and removes redundant mesh faces between architectural parts. It collects faces from built parts, groups faces by approximate plane, tests pairwise overlap/projection to determine coverage, and removes fully covered faces from their meshes.
using System;
using System.Collections.Generic;
using System.Linq;
using HalfEdgeMesh;
using Sandbox;
namespace Sunless.Architecture;
// One built face, with the plane it lies in already keyed - the key is how a face finds the only faces it could
// ever be covered by, and it is what an incremental build uses to decide which parts still have to be compared.
sealed class ArchContactFace
{
public PolygonMesh Mesh { get; init; }
public FaceHandle Handle { get; init; }
public Vector3[] Corners { get; init; }
public Vector3 Normal { get; init; }
public Vector3 Centre { get; init; }
public float Area { get; init; }
public int Part { get; init; }
public (long, long, long, long) Plane { get; init; }
}
public sealed class ArchMeshContactService
{
const float PlaneAngle = 0.02f;
const float PlaneOffset = 0.06f;
public int RemoveRedundantContacts( IReadOnlyList<ArchBuiltPart> parts )
{
return Resolve( Collect( parts, null ) );
}
// Faces in EMISSION order, and a subset is a filter rather than a re-ordering: the pass breaks a mutual cover
// by area but breaks everything else by which face it reached first, so a reordered subset could kill the other
// half of a pair and a warm build would stop matching a cold one.
internal List<ArchContactFace> Collect( IReadOnlyList<ArchBuiltPart> parts, IReadOnlySet<int> only )
{
var faces = new List<ArchContactFace>();
for ( var partIndex = 0; partIndex < parts.Count; partIndex++ )
{
if ( only is not null && !only.Contains( partIndex ) )
{
continue;
}
faces.AddRange( Faces( parts[partIndex], partIndex ) );
}
return faces;
}
internal static IEnumerable<ArchContactFace> Faces( ArchBuiltPart part, int partIndex )
{
var mesh = part.Canvas.Finish();
foreach ( var handle in mesh.FaceHandles.ToList() )
{
var corners = mesh.GetFaceVertexPositions( handle, part.Canvas.Projection ).ToArray();
if ( corners.Length < 3 )
{
continue;
}
mesh.ComputeFaceNormal( handle, out var local );
var normal = part.Canvas.Projection.NormalToWorld( local ).Normal;
var centre = corners.Aggregate( Vector3.Zero, ( total, point ) => total + point ) / corners.Length;
yield return new ArchContactFace
{
Mesh = mesh,
Handle = handle,
Corners = corners,
Normal = normal,
Centre = centre,
Area = Area( corners ),
Part = partIndex,
Plane = PlaneKey( normal, centre )
};
}
}
internal int Resolve( List<ArchContactFace> faces )
{
var groups = faces.GroupBy( face => face.Plane );
var dead = new Dictionary<PolygonMesh, HashSet<FaceHandle>>();
foreach ( var group in groups )
{
var candidates = group.ToList();
var extents = candidates.Select( Bounds ).ToArray();
for ( var first = 0; first < candidates.Count; first++ )
{
var a = candidates[first];
for ( var second = first + 1; second < candidates.Count; second++ )
{
var b = candidates[second];
if ( a.Part == b.Part && ReferenceEquals( a.Mesh, b.Mesh ) && (IsDead( dead, a ) || IsDead( dead, b )) )
{
continue;
}
if ( !Touches( extents[first], extents[second] ) )
{
continue;
}
var remove = Redundant( a, b );
if ( remove is null || IsDead( dead, remove ) )
{
continue;
}
(dead.TryGetValue( remove.Mesh, out var handles ) ? handles : dead[remove.Mesh] = new HashSet<FaceHandle>()).Add( remove.Handle );
}
}
}
foreach ( var entry in dead )
{
entry.Key.RemoveFaces( entry.Value.ToList() );
}
return dead.Values.Sum( handles => handles.Count );
}
internal static (long, long, long, long) PlaneKey( Vector3 normal, Vector3 centre )
{
var canonical = Canonical( normal );
return (
(long)MathF.Round( canonical.x / PlaneAngle ),
(long)MathF.Round( canonical.y / PlaneAngle ),
(long)MathF.Round( canonical.z / PlaneAngle ),
(long)MathF.Round( Vector3.Dot( canonical, centre ) / PlaneOffset ) );
}
// Only a face its partner covers WHOLE - a partial dip leaves the rest out in the open.
static ArchContactFace Redundant( ArchContactFace a, ArchContactFace b )
{
var normal = Canonical( a.Normal );
var flatA = Project( a.Corners, normal, a.Centre );
var flatB = Project( b.Corners, normal, a.Centre );
var aCovered = Covers( flatB, flatA );
var bCovered = Covers( flatA, flatB );
if ( aCovered && bCovered )
{
return a.Area <= b.Area ? a : b;
}
return aCovered ? a : bCovered ? b : null;
}
// Measured once per face rather than six LINQ passes per PAIR - a plane group is compared pairwise, so the
// same face's extents were being walked again for every other face standing in its plane.
internal static BBox Bounds( ArchContactFace face ) => BBox.FromPoints( face.Corners );
// A plane key is coarse on purpose: EVERY ground slab in a plan lands in the z=0 bucket. What decides whether
// two faces in one plane can affect each other is whether they are anywhere near each other, so an incremental
// build has to ask the same question the pass asks rather than settle for the bucket.
internal static bool Touches( BBox a, BBox b )
{
return a.Mins.x <= b.Maxs.x + 0.5f && a.Maxs.x >= b.Mins.x - 0.5f
&& a.Mins.y <= b.Maxs.y + 0.5f && a.Maxs.y >= b.Mins.y - 0.5f
&& a.Mins.z <= b.Maxs.z + 0.5f && a.Maxs.z >= b.Mins.z - 0.5f;
}
static bool Covers( IReadOnlyList<Vector2> cover, IReadOnlyList<Vector2> face )
{
return Samples( face ).All( point => Contains( cover, point ) );
}
static bool IsDead( Dictionary<PolygonMesh, HashSet<FaceHandle>> dead, ArchContactFace face )
{
return dead.TryGetValue( face.Mesh, out var handles ) && handles.Contains( face.Handle );
}
internal static Vector3 Canonical( Vector3 normal )
{
if ( normal.x < -0.001f || normal.x is > -0.001f and < 0.001f && normal.y < -0.001f || normal.x is > -0.001f and < 0.001f && normal.y is > -0.001f and < 0.001f && normal.z < 0f )
{
return -normal;
}
return normal;
}
static Vector2[] Project( IReadOnlyList<Vector3> corners, Vector3 normal, Vector3 origin )
{
var result = new Vector2[corners.Count];
var axis = MathF.Abs( normal.x ) >= MathF.Abs( normal.y ) && MathF.Abs( normal.x ) >= MathF.Abs( normal.z ) ? 0 : MathF.Abs( normal.y ) >= MathF.Abs( normal.z ) ? 1 : 2;
for ( var index = 0; index < corners.Count; index++ )
{
var point = corners[index] - origin;
result[index] = axis switch
{
0 => new Vector2( point.y, point.z ),
1 => new Vector2( point.x, point.z ),
_ => new Vector2( point.x, point.y )
};
}
return result;
}
static IEnumerable<Vector2> Samples( IReadOnlyList<Vector2> corners )
{
var centre = corners.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / corners.Count;
yield return centre;
foreach ( var corner in corners )
{
yield return Vector2.Lerp( corner, centre, 0.25f );
yield return Vector2.Lerp( corner, centre, 0.6f );
}
}
static bool Contains( IReadOnlyList<Vector2> loop, Vector2 point ) => ArchFootprint.Contains( loop, point );
static float Area( IReadOnlyList<Vector3> corners )
{
var normal = Vector3.Zero;
for ( var index = 0; index < corners.Count; index++ )
{
normal += Vector3.Cross( corners[index], corners[(index + 1) % corners.Count] );
}
return normal.Length * 0.5f;
}
}