Editor-side mesh boolean helper. Converts two PolygonMeshes into BSP trees of triangle fragments, clips each by the other, and rebuilds a single PolygonMesh with new faces at the cut, preserving untouched faces' material and texture parameters. Has open-sheet handling, crack/blister/needle cleanup passes, and a retry wrapper that nudges the cutter to avoid hairline defects.
namespace AdvancedMappingTools;
/// <summary>
/// Cuts one mesh out of another. The boolean of the engine is clean for simple convex shapes but
/// breaks on a curved cutter, so this does it itself: both meshes become BSP trees, each is
/// clipped by the other, and the pieces are joined into one closed mesh again. Pieces of two
/// faces rarely end on the same points, so corners are added along the edges until every
/// edge has a partner. Faces the cut does not touch stay exactly as they were, with their
/// material and texture placement. It works in double precision, a curved tunnel makes many
/// nearly parallel planes.
/// </summary>
public static class MeshCutter
{
const double PlaneEpsilon = 5e-4;
const double WeldDistance = 0.01;
const double OnEdgeDistance = 0.01;
const double CrumbArea = 0.01;
const int CrumbPasses = 4;
const double SameDirection = 500.0;
const double TurnEpsilon = 1e-7;
const double StraightCorner = 0.02;
const int JoinPasses = 4;
const int BlisterPasses = 4;
const int NeedlePasses = 8;
const int CrackPasses = 4;
const int CrackPoints = 96;
const double CrackLength = 64.0;
const double NeedleWidth = 0.01;
const double NeedleEdge = 0.05;
const double BlisterArea = 16.0;
const int StackSize = 256 * 1024 * 1024;
readonly record struct Vec( double X, double Y, double Z )
{
public static Vec operator +( Vec a, Vec b ) => new( a.X + b.X, a.Y + b.Y, a.Z + b.Z );
public static Vec operator -( Vec a, Vec b ) => new( a.X - b.X, a.Y - b.Y, a.Z - b.Z );
public static Vec operator *( Vec a, double s ) => new( a.X * s, a.Y * s, a.Z * s );
public double Dot( Vec b ) => X * b.X + Y * b.Y + Z * b.Z;
public Vec Cross( Vec b ) => new( Y * b.Z - Z * b.Y, Z * b.X - X * b.Z, X * b.Y - Y * b.X );
public double Length => Math.Sqrt( Dot( this ) );
public Vec Normal => this * (1.0 / Math.Max( Length, 1e-300 ));
public static Vec From( Vector3 v ) => new( v.x, v.y, v.z );
public Vector3 ToVector() => new( (float)X, (float)Y, (float)Z );
}
/// <summary>
/// A record of how a triangle was divided. Tree planes run on forever and split far more than
/// the cut needs: where both halves of a split survive, they become one again.
/// <see cref="Face"/> is the face it came from in the mesh being cut, or -1 for the cutter.
/// </summary>
sealed class Fragment
{
public Vec[] Points;
public int Face;
public int Plate;
public Vec Direction;
public Fragment First;
public Fragment Second;
public bool Kept;
/// <summary>
/// True when all of this fragment survived. Halves that did are put back together here.
/// </summary>
public bool Settle()
{
if ( First is null ) return Kept;
var first = First.Settle();
var second = Second.Settle();
if ( !first || !second ) return false;
First = null;
Second = null;
Kept = true;
return true;
}
public void Collect( List<Fragment> into )
{
if ( First is not null )
{
First.Collect( into );
Second.Collect( into );
}
else if ( Kept )
{
into.Add( this );
}
}
}
/// <summary>
/// A convex piece of a face on its way through the trees.
/// </summary>
sealed class Piece
{
public Vec[] Points;
public Vec Normal;
public double Offset;
public Fragment Fragment;
public bool Inverted;
public Piece Flipped()
{
var points = (Vec[])Points.Clone();
Array.Reverse( points );
return new Piece { Points = points, Normal = Normal * -1.0, Offset = -Offset, Fragment = Fragment, Inverted = !Inverted };
}
public Piece Part( Vec[] points, Fragment fragment ) =>
new() { Points = points, Normal = Normal, Offset = Offset, Fragment = fragment, Inverted = Inverted };
/// <summary>
/// A record for a part of this piece, in the direction of the original face.
/// </summary>
public Fragment Record( Vec[] points )
{
var stored = (Vec[])points.Clone();
if ( Inverted ) Array.Reverse( stored );
return new Fragment { Points = stored, Face = Fragment.Face, Plate = Fragment.Plate, Direction = Fragment.Direction };
}
}
const int Coplanar = 0;
const int Front = 1;
const int Back = 2;
const int Spanning = 3;
static void Split( Vec normal, double offset, Piece piece, List<Piece> coplanarFront, List<Piece> coplanarBack, List<Piece> front, List<Piece> back )
{
var count = piece.Points.Length;
var sides = new int[count];
var kind = 0;
for ( var i = 0; i < count; i++ )
{
var t = normal.Dot( piece.Points[i] ) - offset;
var side = t < -PlaneEpsilon ? Back : t > PlaneEpsilon ? Front : Coplanar;
kind |= side;
sides[i] = side;
}
switch ( kind )
{
case Coplanar:
(normal.Dot( piece.Normal ) > 0.0 ? coplanarFront : coplanarBack).Add( piece );
return;
case Front:
front.Add( piece );
return;
case Back:
back.Add( piece );
return;
}
var f = new List<Vec>( count + 2 );
var b = new List<Vec>( count + 2 );
for ( var i = 0; i < count; i++ )
{
var j = (i + 1) % count;
var si = sides[i];
var sj = sides[j];
var vi = piece.Points[i];
var vj = piece.Points[j];
if ( si != Back ) f.Add( vi );
if ( si != Front ) b.Add( vi );
if ( (si | sj) != Spanning ) continue;
var t = (offset - normal.Dot( vi )) / normal.Dot( vj - vi );
var v = vi + (vj - vi) * t;
f.Add( v );
b.Add( v );
}
// Only a real division is recorded, a sliver on one side leaves the piece as it was.
if ( f.Count < 3 || b.Count < 3 )
{
(f.Count >= 3 ? front : back).Add( piece );
return;
}
var frontPoints = f.ToArray();
var backPoints = b.ToArray();
var frontRecord = piece.Record( frontPoints );
var backRecord = piece.Record( backPoints );
piece.Fragment.First = frontRecord;
piece.Fragment.Second = backRecord;
front.Add( piece.Part( frontPoints, frontRecord ) );
back.Add( piece.Part( backPoints, backRecord ) );
}
sealed class Node
{
Vec _normal;
double _offset;
bool _hasPlane;
Node _front;
Node _back;
readonly List<Piece> _pieces = [];
public void Build( List<Piece> pieces )
{
if ( pieces.Count == 0 ) return;
if ( !_hasPlane )
{
_normal = pieces[0].Normal;
_offset = pieces[0].Offset;
_hasPlane = true;
}
var front = new List<Piece>();
var back = new List<Piece>();
foreach ( var piece in pieces )
Split( _normal, _offset, piece, _pieces, _pieces, front, back );
if ( front.Count > 0 ) (_front ??= new Node()).Build( front );
if ( back.Count > 0 ) (_back ??= new Node()).Build( back );
}
public void Invert()
{
for ( var i = 0; i < _pieces.Count; i++ )
_pieces[i] = _pieces[i].Flipped();
_normal *= -1.0;
_offset = -_offset;
_front?.Invert();
_back?.Invert();
(_front, _back) = (_back, _front);
}
public List<Piece> Clip( List<Piece> pieces )
{
if ( !_hasPlane ) return [.. pieces];
var front = new List<Piece>();
var back = new List<Piece>();
foreach ( var piece in pieces )
Split( _normal, _offset, piece, front, back, front, back );
if ( _front is not null ) front = _front.Clip( front );
if ( _back is not null ) back = _back.Clip( back );
else back = [];
front.AddRange( back );
return front;
}
public void ClipTo( Node other )
{
var clipped = other.Clip( _pieces );
_pieces.Clear();
_pieces.AddRange( clipped );
_front?.ClipTo( other );
_back?.ClipTo( other );
}
public void Collect( List<Piece> into )
{
into.AddRange( _pieces );
_front?.Collect( into );
_back?.Collect( into );
}
}
static Piece MakePiece( Vec a, Vec b, Vec c, int face, int plate )
{
var normal = (b - a).Cross( c - a );
if ( normal.Length < 1e-9 ) return null;
normal = normal.Normal;
return new Piece { Points = [a, b, c], Normal = normal, Offset = normal.Dot( a ), Fragment = new Fragment { Points = [a, b, c], Face = face, Plate = plate, Direction = normal } };
}
static List<Piece> Pieces( PolygonMesh mesh, Transform transform, bool keepFaces, out List<HalfEdgeMesh.FaceHandle> handles )
{
var pieces = new List<Piece>();
handles = [.. mesh.FaceHandles];
if ( handles.Count > 0 && mesh.CreateFace( handles[0], transform, Color.White ) is null )
mesh.Rebuild();
for ( var f = 0; f < handles.Count; f++ )
{
var vertices = mesh.CreateFace( handles[f], transform, Color.White );
if ( vertices is null ) continue;
for ( var i = 0; i + 2 < vertices.Length; i += 3 )
{
var piece = MakePiece( Vec.From( vertices[i].Position ), Vec.From( vertices[i + 1].Position ), Vec.From( vertices[i + 2].Position ), keepFaces ? f : -1, keepFaces ? f : -1 - f );
if ( piece is not null ) pieces.Add( piece );
}
}
return pieces;
}
/// <summary>
/// A fixed shuffle. Faces of a swept shape lie in rings, and a tree built in that order is
/// as deep as the mesh is large and splits far more than it has to.
/// </summary>
static List<Piece> Shuffled( List<Piece> pieces )
{
var result = new List<Piece>( pieces );
var state = 2463534242u;
for ( var i = result.Count - 1; i > 0; i-- )
{
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
var j = (int)(state % (uint)(i + 1));
(result[i], result[j]) = (result[j], result[i]);
}
return result;
}
/// <summary>
/// A closed mesh has a partner for every edge. Only then has it an inside, and only then
/// does a cut through it get walls of its own. A sheet just gets a hole.
/// </summary>
static bool IsClosed( PolygonMesh mesh ) => !mesh.HalfEdgeHandles.Any( mesh.IsEdgeOpen );
/// <summary>
/// The mesh with the cutter taken out of it, or nothing if the two do not touch. The cutter
/// is given in its own space, <paramref name="relative"/> takes it into that of the mesh.
/// New faces inside the cut get <paramref name="material"/>.
/// </summary>
public static PolygonMesh Subtract( PolygonMesh mesh, PolygonMesh cutter, Transform relative, Material material )
{
// Bounds of the vertices themselves. A mesh made in the editor carries a transform of its own
// for its texture mapping, which CalculateBounds would add; the cut works in vertex space.
var meshBounds = BBox.FromPoints( mesh.VertexHandles.Select( v => mesh.GetVertexPosition( v ) ) );
var cutterBounds = BBox.FromPoints( cutter.VertexHandles.Select( v => relative.PointToWorld( cutter.GetVertexPosition( v ) ) ) );
if ( !meshBounds.Overlaps( cutterBounds ) ) return null;
var solid = IsClosed( mesh );
var body = Pieces( mesh, new Transform( Vector3.Zero, Rotation.Identity ), true, out var faces );
var tool = Pieces( cutter, relative, false, out _ );
var roots = body.Concat( tool ).Select( x => x.Fragment ).ToList();
// Trees go as deep as they like, so this runs on a stack that is large enough for it.
var worker = new System.Threading.Thread( () =>
{
foreach ( var piece in Cut( body, tool, solid ) )
piece.Fragment.Kept = true;
foreach ( var root in roots )
root.Settle();
}, StackSize );
worker.Start();
worker.Join();
return Assemble( mesh, faces, roots, material );
}
/// <summary>
/// A cut that passes a corner by a hair can come out with a crack. Moved by less than anyone
/// can see the same cut misses that corner, so where the result is not clean it is tried
/// again a little beside itself, and the cleanest result counts.
/// </summary>
public static PolygonMesh SubtractClean( PolygonMesh mesh, PolygonMesh cutter, Transform relative, Material material )
{
var best = default( PolygonMesh );
var fewest = int.MaxValue;
var closed = IsClosed( mesh );
foreach ( var nudge in Nudges )
{
var moved = relative;
moved.Position += nudge;
var result = Subtract( mesh, cutter, moved, material );
if ( result is null ) return best;
var report = MeshDiagnostics.Analyze( result );
var faults = report.Errors + report.Warnings;
// A hole in an open sheet has an open edge all around it, as it should.
if ( !closed ) faults -= report.Count( MeshIssueKind.OpenEdge ) + report.Count( MeshIssueKind.UnweldedSeam );
if ( faults < fewest )
{
fewest = faults;
best = result;
}
if ( faults <= 0 ) break;
}
return best;
}
static readonly Vector3[] Nudges =
[
Vector3.Zero,
new( 0.031f, 0.017f, 0.023f ),
new( -0.027f, 0.041f, -0.013f ),
new( 0.019f, -0.036f, 0.044f ),
new( -0.043f, -0.021f, -0.037f ),
new( 0.053f, 0.047f, -0.029f )
];
static List<Piece> Cut( List<Piece> body, List<Piece> tool, bool solid )
{
// Subtracting is adding the cutter to the inverted mesh and inverting that.
var a = new Node();
var b = new Node();
a.Build( Shuffled( body ) );
b.Build( Shuffled( tool ) );
a.Invert();
a.ClipTo( b );
var kept = new List<Piece>();
if ( solid )
{
b.ClipTo( a );
b.Invert();
b.ClipTo( a );
b.Invert();
b.Collect( kept );
}
a.Collect( kept );
return kept;
}
sealed class Ring
{
public int[] Points;
public int Face;
public int Plate;
public Vec Direction;
}
/// <summary>
/// Rebuild a mesh from what is left. A face that lost nothing comes back as it was, the rest
/// is pieces. Every piece then gets a corner wherever another piece ends on one of its edges.
/// </summary>
static PolygonMesh Assemble( PolygonMesh source, List<HalfEdgeMesh.FaceHandle> faces, List<Fragment> roots, Material material )
{
var touched = new HashSet<int>();
var pieces = new List<Fragment>();
foreach ( var root in roots )
{
if ( root.Face >= 0 && !root.Kept ) touched.Add( root.Face );
root.Collect( pieces );
}
// Nothing was taken away and nothing was added: the cutter passed by.
if ( touched.Count == 0 && !pieces.Any( x => x.Face < 0 ) ) return null;
var points = new List<Vec>();
var grid = new Dictionary<(long, long, long), int>();
int Point( Vec p )
{
var key = ((long)Math.Round( p.X / WeldDistance ), (long)Math.Round( p.Y / WeldDistance ), (long)Math.Round( p.Z / WeldDistance ));
for ( var x = -1; x <= 1; x++ )
for ( var y = -1; y <= 1; y++ )
for ( var z = -1; z <= 1; z++ )
{
if ( !grid.TryGetValue( (key.Item1 + x, key.Item2 + y, key.Item3 + z), out var near ) ) continue;
if ( (points[near] - p).Length <= WeldDistance ) return near;
}
grid[key] = points.Count;
points.Add( p );
return points.Count - 1;
}
var rings = new List<Ring>();
void AddRing( IEnumerable<Vec> positions, int face, int plate, Vec direction )
{
var ring = new List<int>();
foreach ( var position in positions )
{
var id = Point( position );
if ( ring.Count == 0 || ring[^1] != id ) ring.Add( id );
}
if ( ring.Count > 1 && ring[^1] == ring[0] ) ring.RemoveAt( ring.Count - 1 );
if ( ring.Count >= 3 ) rings.Add( new Ring { Points = [.. ring], Face = face, Plate = plate, Direction = direction } );
}
for ( var f = 0; f < faces.Count; f++ )
{
if ( touched.Contains( f ) ) continue;
AddRing( source.GetFaceVertices( faces[f] ).Select( x => Vec.From( source.GetVertexPosition( x ) ) ), f, f, new Vec() );
}
foreach ( var piece in pieces )
{
if ( piece.Face >= 0 && !touched.Contains( piece.Face ) ) continue;
// What is left of the cutter looks into the cut.
AddRing( piece.Face >= 0 ? piece.Points : piece.Points.Reverse(), piece.Face, piece.Plate, piece.Face >= 0 ? piece.Direction : piece.Direction * -1.0 );
}
FillEdges( rings, points );
foreach ( var ring in rings )
ring.Points = WithoutSpikes( ring.Points );
rings.RemoveAll( x => x.Points.Length < 3 );
CollapseCrumbs( rings, points );
rings = Merge( rings, points );
rings = [.. rings.SelectMany( x => x.Face >= 0 && !touched.Contains( x.Face ) ? [x] : Split( x, points ) )];
rings = WithoutBlisters( rings, points );
rings = WithoutNeedles( rings, points );
for ( var pass = 0; pass < CrackPasses; pass++ )
{
var before = rings.Count;
rings = WithoutCracks( rings, points, IsClosed( source ) ? double.MaxValue : CrackLength );
rings = WithoutNeedles( rings, points );
if ( rings.Count == before ) break;
}
rings = WithoutPillows( rings );
return Build( source, faces, rings, points, material );
}
/// <summary>
/// Two faces on the same corners, back to back, have nothing between them and nothing to show.
/// </summary>
static List<Ring> WithoutPillows( List<Ring> rings )
{
var gone = new HashSet<Ring>();
foreach ( var same in rings.GroupBy( x => string.Join( ' ', x.Points.OrderBy( p => p ) ) ).Where( x => x.Count() > 1 ) )
{
var group = same.ToList();
for ( var i = 0; i < group.Count; i++ )
{
for ( var j = i + 1; j < group.Count; j++ )
{
var a = group[i].Points;
var b = group[j].Points;
if ( gone.Contains( group[i] ) || gone.Contains( group[j] ) ) continue;
var at = Array.IndexOf( b, a[0] );
if ( b[(at + b.Length - 1) % b.Length] != a[1] ) continue;
gone.Add( group[i] );
gone.Add( group[j] );
}
}
}
return gone.Count == 0 ? rings : [.. rings.Where( x => !gone.Contains( x ) )];
}
/// <summary>
/// What is still open at this point is a crack a few points around, between edges that missed
/// each other. It gets a face of its own. Most of those have no width and go again as needles,
/// which is how a corner on one side of a crack finally reaches the other side. A body that
/// was closed has no open edges at all, and the real edge of an open sheet is far longer.
/// </summary>
static List<Ring> WithoutCracks( List<Ring> rings, List<Vec> points, double longest )
{
var used = new HashSet<(int, int)>();
foreach ( var ring in rings )
{
for ( var i = 0; i < ring.Points.Length; i++ )
used.Add( (ring.Points[i], ring.Points[(i + 1) % ring.Points.Length]) );
}
var open = used.Where( x => !used.Contains( (x.Item2, x.Item1) ) ).ToList();
if ( open.Count == 0 ) return rings;
var arriving = new Dictionary<int, List<int>>();
foreach ( var (from, to) in open )
{
if ( !arriving.TryGetValue( to, out var list ) ) arriving[to] = list = [];
list.Add( from );
}
var done = new HashSet<(int, int)>();
var added = new List<Ring>();
foreach ( var (first, second) in open )
{
if ( !done.Add( (first, second) ) ) continue;
// The new face runs against the open edges.
var loop = new List<int> { second, first };
var current = first;
var around = (points[first] - points[second]).Length;
var closed = false;
while ( loop.Count < CrackPoints && around < longest )
{
if ( !arriving.TryGetValue( current, out var options ) ) break;
var next = options.FindIndex( x => !done.Contains( (x, current) ) );
if ( next < 0 ) break;
var point = options[next];
done.Add( (point, current) );
around += (points[point] - points[current]).Length;
if ( point == second )
{
closed = true;
break;
}
loop.Add( point );
current = point;
}
if ( closed && around < longest )
Divide( loop, -1, points, added );
}
return [.. rings, .. added];
}
/// <summary>
/// Triangles without any width are left where three points lie on a line. With a short edge such
/// a needle gives up that edge. Without one its middle point lies on the long edge, so the
/// face on the other side of that edge takes the point over, and the needle has no reason to exist.
/// </summary>
static List<Ring> WithoutNeedles( List<Ring> rings, List<Vec> points )
{
for ( var pass = 0; pass < NeedlePasses; pass++ )
{
var edges = new Dictionary<(int, int), Ring>();
foreach ( var ring in rings )
{
for ( var i = 0; i < ring.Points.Length; i++ )
edges[(ring.Points[i], ring.Points[(i + 1) % ring.Points.Length])] = ring;
}
var moved = new Dictionary<int, int>();
var gone = new HashSet<Ring>();
var grown = new HashSet<Ring>();
foreach ( var ring in rings )
{
if ( ring.Points.Length != 3 || gone.Contains( ring ) || grown.Contains( ring ) ) continue;
var sides = Enumerable.Range( 0, 3 )
.Select( i => (A: ring.Points[i], B: ring.Points[(i + 1) % 3], Other: ring.Points[(i + 2) % 3]) )
.OrderBy( x => (points[x.A] - points[x.B]).Length )
.ToArray();
var longest = sides[2];
var length = (points[longest.A] - points[longest.B]).Length;
var area = Area( ring.Points, points );
if ( area >= CrumbArea && area * 2.0 / Math.Max( length, 1e-12 ) >= NeedleWidth ) continue;
var shortest = sides[0];
if ( (points[shortest.A] - points[shortest.B]).Length < NeedleEdge )
{
if ( !moved.ContainsKey( shortest.A ) && !moved.ContainsValue( shortest.A ) && !moved.ContainsKey( shortest.B ) )
moved[shortest.A] = shortest.B;
continue;
}
if ( !edges.TryGetValue( (longest.B, longest.A), out var neighbour ) || gone.Contains( neighbour ) || grown.Contains( neighbour ) ) continue;
var at = Array.IndexOf( neighbour.Points, longest.B );
if ( at < 0 || neighbour.Points[(at + 1) % neighbour.Points.Length] != longest.A ) continue;
var taken = neighbour.Points.ToList();
taken.Insert( at + 1, longest.Other );
neighbour.Points = [.. taken];
grown.Add( neighbour );
gone.Add( ring );
}
if ( moved.Count == 0 && gone.Count == 0 ) break;
rings = [.. rings.Where( x => !gone.Contains( x ) )];
foreach ( var ring in rings )
ring.Points = WithoutSpikes( [.. ring.Points.Select( x => moved.TryGetValue( x, out var to ) ? to : x )] );
rings = [.. rings.Where( x => x.Points.Length >= 3 ).SelectMany( x => grown.Contains( x ) ? Split( x, points ) : [x] )];
rings = WithoutBlisters( rings, points );
}
return rings;
}
/// <summary>
/// Where a cut runs along a face at the width of a hair, the two sides can leave a blister: thin
/// faces back to back, closed in on themselves and hanging on the real surface by edges
/// that then have four faces instead of two. Such an edge has no place in a mesh.
/// A real part of a body never hangs on like that, and these are far too small to be one.
/// </summary>
static List<Ring> WithoutBlisters( List<Ring> rings, List<Vec> points )
{
for ( var pass = 0; pass < BlisterPasses; pass++ )
{
var uses = new Dictionary<(int, int), List<int>>();
for ( var r = 0; r < rings.Count; r++ )
{
var ring = rings[r].Points;
for ( var i = 0; i < ring.Length; i++ )
{
var key = (ring[i], ring[(i + 1) % ring.Length]);
if ( !uses.TryGetValue( key, out var list ) ) uses[key] = list = [];
list.Add( r );
}
}
var parents = Enumerable.Range( 0, rings.Count ).ToArray();
int Find( int x )
{
while ( parents[x] != x ) x = parents[x] = parents[parents[x]];
return x;
}
var pinched = new HashSet<int>();
foreach ( var ((a, b), forward) in uses )
{
uses.TryGetValue( (b, a), out var backward );
if ( forward.Count == 1 && backward?.Count == 1 )
{
parents[Find( forward[0] )] = Find( backward[0] );
}
else if ( forward.Count > 1 || (backward?.Count ?? 0) > 1 )
{
pinched.UnionWith( forward );
if ( backward is not null ) pinched.UnionWith( backward );
}
}
if ( pinched.Count == 0 ) return rings;
var areas = new Dictionary<int, double>();
for ( var r = 0; r < rings.Count; r++ )
areas[Find( r )] = areas.GetValueOrDefault( Find( r ) ) + Area( rings[r].Points, points );
var blisters = pinched.Select( Find ).Where( x => areas[x] < BlisterArea ).ToHashSet();
if ( blisters.Count == 0 ) return rings;
rings = [.. rings.Where( ( _, r ) => !blisters.Contains( Find( r ) ) )];
}
return rings;
}
/// <summary>
/// Tree planes run on forever, so a face comes out of the trees in many pieces that were
/// divided for no reason, and those are where slivers come from. All pieces of a face in one plane
/// become one again: what they share goes, and what is left is the true outline of what
/// survived the cut, with any holes it has.
/// </summary>
static List<Ring> Merge( List<Ring> rings, List<Vec> points )
{
var result = new List<Ring>( rings.Count );
foreach ( var plate in rings.GroupBy( x => (x.Plate, (long)Math.Round( x.Direction.X * SameDirection ), (long)Math.Round( x.Direction.Y * SameDirection ), (long)Math.Round( x.Direction.Z * SameDirection )) ) )
{
var group = plate.ToList();
if ( group.Count == 1 )
{
result.AddRange( group );
continue;
}
result.AddRange( Outlines( group, points ) ?? group );
}
return result;
}
/// <summary>
/// The outlines of a group of rings in one plane as new rings, or nothing if they do not make any.
/// </summary>
static List<Ring> Outlines( List<Ring> group, List<Vec> points )
{
var edges = new HashSet<(int From, int To)>();
foreach ( var ring in group )
{
for ( var i = 0; i < ring.Points.Length; i++ )
{
var a = ring.Points[i];
var b = ring.Points[(i + 1) % ring.Points.Length];
if ( edges.Remove( (b, a) ) ) continue;
if ( !edges.Add( (a, b) ) ) return null;
}
}
if ( edges.Count < 3 ) return null;
var first = group[0];
var normal = first.Direction;
var side = Math.Abs( normal.X ) < 0.7 ? new Vec( 1.0, 0.0, 0.0 ) : new Vec( 0.0, 1.0, 0.0 );
var axisU = normal.Cross( side ).Normal;
var axisV = normal.Cross( axisU ).Normal;
(double X, double Y) Flat( int point ) => (points[point].Dot( axisU ), points[point].Dot( axisV ));
var leaving = new Dictionary<int, List<int>>();
foreach ( var (from, to) in edges )
{
if ( !leaving.TryGetValue( from, out var list ) ) leaving[from] = list = [];
list.Add( to );
}
var loops = new List<List<int>>();
while ( edges.Count > 0 )
{
var (start, next) = edges.First();
var loop = new List<int> { start };
var before = start;
var current = next;
edges.Remove( (start, next) );
leaving[start].Remove( next );
while ( current != start )
{
loop.Add( current );
if ( !leaving.TryGetValue( current, out var options ) || options.Count == 0 ) return null;
// Where two outlines touch in one point this keeps each of them to itself.
var chosen = options[0];
if ( options.Count > 1 )
{
var (px, py) = Flat( before );
var (cx, cy) = Flat( current );
var incoming = Math.Atan2( cy - py, cx - px );
chosen = options.OrderBy( option =>
{
var (nx, ny) = Flat( option );
var turn = Math.Atan2( ny - cy, nx - cx ) - incoming;
while ( turn <= -Math.PI ) turn += Math.PI * 2.0;
while ( turn > Math.PI ) turn -= Math.PI * 2.0;
return turn;
} ).First();
}
edges.Remove( (current, chosen) );
options.Remove( chosen );
before = current;
current = chosen;
}
if ( loop.Count >= 3 ) loops.Add( loop );
}
double Signed( List<int> loop )
{
var sum = 0.0;
for ( var i = 0; i < loop.Count; i++ )
{
var (ax, ay) = Flat( loop[i] );
var (bx, by) = Flat( loop[(i + 1) % loop.Count] );
sum += ax * by - bx * ay;
}
return sum * 0.5;
}
var outers = loops.Where( x => Signed( x ) > 0.0 ).ToList();
var holes = loops.Where( x => Signed( x ) <= 0.0 ).ToList();
if ( outers.Count == 0 ) return null;
var result = new List<Ring>();
var wanted = 0.0;
var made = 0.0;
double Corners( int a, int b, int c )
{
var (ax, ay) = Flat( a );
var (bx, by) = Flat( b );
var (cx, cy) = Flat( c );
return ((bx - ax) * (cy - ay) - (by - ay) * (cx - ax)) * 0.5;
}
foreach ( var outer in outers )
{
var loop = outer;
wanted += Signed( outer );
foreach ( var hole in holes.Where( h => Encloses( outer, Flat( h[0] ), Flat ) ).OrderByDescending( h => Signed( h ) ).ToList() )
{
wanted += Signed( hole );
loop = Bridged( loop, hole, Flat );
if ( loop is null ) return null;
holes.Remove( hole );
}
var plain = Plain( loop, Flat );
var triangles = plain ? [[.. loop]] : EarClip( loop, Flat );
if ( triangles is null ) return null;
foreach ( var ring in triangles )
{
for ( var i = 1; i + 1 < ring.Length; i++ )
{
var area = Corners( ring[0], ring[i], ring[i + 1] );
if ( area <= 0.0 ) return null;
made += area;
}
result.Add( new Ring { Points = ring, Face = first.Face, Plate = first.Plate, Direction = first.Direction } );
}
}
// What comes out has to cover what went in, or the outlines were not what they seemed.
if ( holes.Count != 0 || Math.Abs( made - wanted ) > Math.Max( wanted * 1e-4, 1e-3 ) ) return null;
return result;
}
static bool Encloses( List<int> loop, (double X, double Y) point, Func<int, (double X, double Y)> flat )
{
var inside = false;
for ( var i = 0; i < loop.Count; i++ )
{
var (ax, ay) = flat( loop[i] );
var (bx, by) = flat( loop[(i + 1) % loop.Count] );
if ( ay > point.Y != by > point.Y && point.X < ax + (point.Y - ay) / (by - ay) * (bx - ax) )
inside = !inside;
}
return inside;
}
/// <summary>
/// A loop that is all outward corners, which a face can take as it is.
/// </summary>
static bool Plain( List<int> loop, Func<int, (double X, double Y)> flat )
{
for ( var i = 0; i < loop.Count; i++ )
{
var (ax, ay) = flat( loop[(i + loop.Count - 1) % loop.Count] );
var (bx, by) = flat( loop[i] );
var (cx, cy) = flat( loop[(i + 1) % loop.Count] );
var first = Math.Sqrt( (bx - ax) * (bx - ax) + (by - ay) * (by - ay) );
var second = Math.Sqrt( (cx - bx) * (cx - bx) + (cy - by) * (cy - by) );
if ( ((bx - ax) * (cy - by) - (by - ay) * (cx - bx)) / Math.Max( first * second, 1e-12 ) < StraightCorner ) return false;
}
return true;
}
/// <summary>
/// A hole joined to its outline by a cut from one to the other, so the two are one loop.
/// </summary>
static List<int> Bridged( List<int> outer, List<int> hole, Func<int, (double X, double Y)> flat )
{
bool Crosses( (double X, double Y) a, (double X, double Y) b, List<int> loop, int skipA, int skipB )
{
for ( var i = 0; i < loop.Count; i++ )
{
var u = loop[i];
var v = loop[(i + 1) % loop.Count];
if ( u == skipA || v == skipA || u == skipB || v == skipB ) continue;
var (ux, uy) = flat( u );
var (vx, vy) = flat( v );
var d1 = (b.X - a.X) * (uy - a.Y) - (b.Y - a.Y) * (ux - a.X);
var d2 = (b.X - a.X) * (vy - a.Y) - (b.Y - a.Y) * (vx - a.X);
var d3 = (vx - ux) * (a.Y - uy) - (vy - uy) * (a.X - ux);
var d4 = (vx - ux) * (b.Y - uy) - (vy - uy) * (b.X - ux);
if ( d1 * d2 <= 0.0 && d3 * d4 <= 0.0 ) return true;
}
return false;
}
var best = (Outer: -1, Hole: -1);
var shortest = double.MaxValue;
for ( var o = 0; o < outer.Count; o++ )
{
var a = flat( outer[o] );
for ( var h = 0; h < hole.Count; h++ )
{
var b = flat( hole[h] );
var length = (a.X - b.X) * (a.X - b.X) + (a.Y - b.Y) * (a.Y - b.Y);
if ( length >= shortest ) continue;
if ( Crosses( a, b, outer, outer[o], hole[h] ) || Crosses( a, b, hole, outer[o], hole[h] ) ) continue;
shortest = length;
best = (o, h);
}
}
if ( best.Outer < 0 ) return null;
var joined = new List<int>( outer.Count + hole.Count + 2 );
for ( var i = 0; i <= best.Outer; i++ ) joined.Add( outer[i] );
for ( var i = 0; i <= hole.Count; i++ ) joined.Add( hole[(best.Hole + i) % hole.Count] );
for ( var i = best.Outer; i < outer.Count; i++ ) joined.Add( outer[i] );
return joined;
}
/// <summary>
/// Cut a loop into triangles, the best shaped corners first. A corner of a triangle may only go
/// when no other point of the loop lies in it or on its edges, so every corner on a straight
/// edge stays a corner of something and no neighbour loses its partner.
/// </summary>
static List<int[]> EarClip( List<int> loop, Func<int, (double X, double Y)> flat )
{
var left = new List<int>( loop );
var result = new List<int[]>( loop.Count );
while ( left.Count > 3 )
{
var chosen = -1;
var bestShape = 0.0;
for ( var i = 0; i < left.Count; i++ )
{
var previous = left[(i + left.Count - 1) % left.Count];
var corner = left[i];
var following = left[(i + 1) % left.Count];
if ( previous == following ) continue;
var a = flat( previous );
var b = flat( corner );
var c = flat( following );
var area = (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
var longest = Math.Max( Math.Max( (b.X - a.X) * (b.X - a.X) + (b.Y - a.Y) * (b.Y - a.Y), (c.X - b.X) * (c.X - b.X) + (c.Y - b.Y) * (c.Y - b.Y) ), (a.X - c.X) * (a.X - c.X) + (a.Y - c.Y) * (a.Y - c.Y) );
var shape = area / Math.Max( longest, 1e-12 );
if ( shape <= TurnEpsilon || shape <= bestShape ) continue;
var blocked = false;
var ab = Math.Sqrt( (b.X - a.X) * (b.X - a.X) + (b.Y - a.Y) * (b.Y - a.Y) );
var bc = Math.Sqrt( (c.X - b.X) * (c.X - b.X) + (c.Y - b.Y) * (c.Y - b.Y) );
var ca = Math.Sqrt( (a.X - c.X) * (a.X - c.X) + (a.Y - c.Y) * (a.Y - c.Y) );
for ( var k = 0; k < left.Count && !blocked; k++ )
{
var other = left[k];
if ( other == previous || other == corner || other == following ) continue;
var p = flat( other );
var d1 = ((b.X - a.X) * (p.Y - a.Y) - (b.Y - a.Y) * (p.X - a.X)) / Math.Max( ab, 1e-9 );
var d2 = ((c.X - b.X) * (p.Y - b.Y) - (c.Y - b.Y) * (p.X - b.X)) / Math.Max( bc, 1e-9 );
var d3 = ((a.X - c.X) * (p.Y - c.Y) - (a.Y - c.Y) * (p.X - c.X)) / Math.Max( ca, 1e-9 );
blocked = d1 >= -OnEdgeDistance && d2 >= -OnEdgeDistance && d3 >= -OnEdgeDistance;
}
if ( blocked ) continue;
bestShape = shape;
chosen = i;
}
if ( chosen < 0 ) return null;
result.Add( [left[(chosen + left.Count - 1) % left.Count], left[chosen], left[(chosen + 1) % left.Count]] );
left.RemoveAt( chosen );
}
result.Add( [.. left] );
return result;
}
/// <summary>
/// A piece with a corner on a straight line has to be divided here: left to itself it
/// can fall into a triangle without area.
/// </summary>
static IEnumerable<Ring> Split( Ring ring, List<Vec> points )
{
var loop = ring.Points;
bool Straight( int i )
{
var a = points[loop[(i + loop.Length - 1) % loop.Length]];
var b = points[loop[i]];
var c = points[loop[(i + 1) % loop.Length]];
return (b - a).Normal.Cross( (c - b).Normal ).Length < StraightCorner;
}
if ( !Enumerable.Range( 0, loop.Length ).Any( Straight ) ) return [ring];
var result = new List<Ring>();
Divide( [.. loop], ring.Face, points, result );
return result;
}
/// <summary>
/// Divide a flat piece along the diagonal that leaves both sides as far from thin as they can be.
/// A diagonal between points of one straight edge leaves a side without area and so never wins.
/// </summary>
static void Divide( List<int> loop, int face, List<Vec> points, List<Ring> into )
{
if ( loop.Count <= 3 )
{
if ( loop.Count == 3 ) into.Add( new Ring { Points = [.. loop], Face = face, Plate = face } );
return;
}
var count = loop.Count;
var bestFirst = 0;
var bestSecond = 2;
var bestShape = double.MinValue;
for ( var i = 0; i < count; i++ )
{
for ( var j = i + 2; j < count; j++ )
{
if ( i == 0 && j == count - 1 ) continue;
var shape = Math.Min( Fatness( loop, i, j, points ), Fatness( loop, j, i + count, points ) );
if ( shape > bestShape )
{
bestShape = shape;
bestFirst = i;
bestSecond = j;
}
}
}
var first = new List<int>();
var second = new List<int>();
for ( var k = bestFirst; k <= bestSecond; k++ ) first.Add( loop[k] );
for ( var k = bestSecond; k <= bestFirst + count; k++ ) second.Add( loop[k % count] );
Divide( first, face, points, into );
Divide( second, face, points, into );
}
/// <summary>
/// The area of a stretch of a loop closed by its last point, against the size of it.
/// </summary>
static double Fatness( List<int> loop, int from, int to, List<Vec> points )
{
var count = loop.Count;
var origin = points[loop[from % count]];
var sum = new Vec();
var longest = 0.0;
for ( var k = from + 1; k < to; k++ )
{
sum += (points[loop[k % count]] - origin).Cross( points[loop[(k + 1) % count]] - origin );
longest = Math.Max( longest, (points[loop[k % count]] - origin).Length );
}
longest = Math.Max( longest, (points[loop[to % count]] - origin).Length );
return sum.Length / Math.Max( longest * longest, 1e-12 );
}
/// <summary>
/// Where a cut passes a corner very closely it leaves crumbs, faces far too small to be seen
/// or used. Each one gives up its shortest edge, which takes the face away and leaves its
/// neighbours connected.
/// </summary>
static void CollapseCrumbs( List<Ring> rings, List<Vec> points )
{
for ( var pass = 0; pass < CrumbPasses; pass++ )
{
var moved = new Dictionary<int, int>();
foreach ( var ring in rings )
{
if ( Area( ring.Points, points ) >= CrumbArea ) continue;
if ( ring.Points.Any( moved.ContainsKey ) ) continue;
var (from, to) = Enumerable.Range( 0, ring.Points.Length )
.Select( i => (A: ring.Points[i], B: ring.Points[(i + 1) % ring.Points.Length]) )
.OrderBy( x => (points[x.A] - points[x.B]).Length )
.First();
if ( !moved.ContainsValue( from ) ) moved[from] = to;
}
if ( moved.Count == 0 ) return;
foreach ( var ring in rings )
ring.Points = WithoutSpikes( [.. ring.Points.Select( x => moved.TryGetValue( x, out var to ) ? to : x )] );
rings.RemoveAll( x => x.Points.Length < 3 );
}
}
static double Area( int[] ring, List<Vec> points )
{
var sum = new Vec();
for ( var i = 1; i + 1 < ring.Length; i++ )
sum += (points[ring[i]] - points[ring[0]]).Cross( points[ring[i + 1]] - points[ring[0]] );
return sum.Length * 0.5;
}
/// <summary>
/// A thin piece can lose all its width to welding and then runs out and straight back.
/// </summary>
static int[] WithoutSpikes( int[] ring )
{
var points = ring.ToList();
var changed = true;
while ( changed && points.Count >= 3 )
{
changed = false;
for ( var i = 0; i < points.Count; i++ )
{
var before = points[(i + points.Count - 1) % points.Count];
var after = points[(i + 1) % points.Count];
if ( before == points[i] )
{
points.RemoveAt( i );
changed = true;
break;
}
if ( before != after ) continue;
points.RemoveAt( i );
points.RemoveAt( i % points.Count );
changed = true;
break;
}
}
return [.. points];
}
/// <summary>
/// Give every edge the points that lie on it. Two pieces next to each other can be split in
/// different places, their shared edge then has corners on one side only.
/// </summary>
static void FillEdges( List<Ring> rings, List<Vec> points )
{
var order = Enumerable.Range( 0, points.Count ).OrderBy( i => points[i].X ).ToArray();
var xs = order.Select( i => points[i].X ).ToArray();
foreach ( var ring in rings )
{
var filled = new List<int>( ring.Points.Length );
for ( var i = 0; i < ring.Points.Length; i++ )
{
var a = ring.Points[i];
var b = ring.Points[(i + 1) % ring.Points.Length];
filled.Add( a );
var pa = points[a];
var pb = points[b];
var delta = pb - pa;
var length = delta.Length;
if ( length < 1e-9 ) continue;
var direction = delta * (1.0 / length);
var minX = Math.Min( pa.X, pb.X ) - OnEdgeDistance;
var maxX = Math.Max( pa.X, pb.X ) + OnEdgeDistance;
var first = Array.BinarySearch( xs, minX );
if ( first < 0 ) first = ~first;
var found = new List<(double T, int Point)>();
for ( var k = first; k < order.Length && xs[k] <= maxX; k++ )
{
var c = order[k];
if ( c == a || c == b ) continue;
var offset = points[c] - pa;
var t = offset.Dot( direction );
if ( t <= OnEdgeDistance || t >= length - OnEdgeDistance ) continue;
if ( (offset - direction * t).Length > OnEdgeDistance ) continue;
found.Add( (t, c) );
}
found.Sort( ( x, y ) => x.T.CompareTo( y.T ) );
foreach ( var (_, point) in found )
filled.Add( point );
}
ring.Points = [.. filled];
}
}
/// <summary>
/// The rings in an order where each one shares an edge with one before it.
/// </summary>
static List<Ring> InOrder( List<Ring> rings )
{
var byEdge = new Dictionary<(int, int), List<int>>();
for ( var r = 0; r < rings.Count; r++ )
{
var ring = rings[r].Points;
for ( var i = 0; i < ring.Length; i++ )
{
var a = ring[i];
var b = ring[(i + 1) % ring.Length];
var key = a < b ? (a, b) : (b, a);
if ( !byEdge.TryGetValue( key, out var list ) ) byEdge[key] = list = [];
list.Add( r );
}
}
var seen = new bool[rings.Count];
var ordered = new List<Ring>( rings.Count );
var queue = new Queue<int>();
for ( var start = 0; start < rings.Count; start++ )
{
if ( seen[start] ) continue;
seen[start] = true;
queue.Enqueue( start );
while ( queue.Count > 0 )
{
var current = queue.Dequeue();
var ring = rings[current].Points;
ordered.Add( rings[current] );
for ( var i = 0; i < ring.Length; i++ )
{
var a = ring[i];
var b = ring[(i + 1) % ring.Length];
foreach ( var next in byEdge[a < b ? (a, b) : (b, a)] )
{
if ( seen[next] ) continue;
seen[next] = true;
queue.Enqueue( next );
}
}
}
}
return ordered;
}
static PolygonMesh Build( PolygonMesh source, List<HalfEdgeMesh.FaceHandle> faces, List<Ring> rings, List<Vec> points, Material material )
{
var mesh = new PolygonMesh();
mesh.SetTransform( source.Transform );
var handles = new HalfEdgeMesh.VertexHandle?[points.Count];
var newFaces = new List<HalfEdgeMesh.FaceHandle>();
void Finish( HalfEdgeMesh.FaceHandle face, Ring ring )
{
if ( ring.Face >= 0 )
{
var from = faces[ring.Face];
source.GetFaceTextureParameters( from, out var axisU, out var axisV, out var scale );
mesh.SetFaceMaterial( face, source.GetFaceMaterial( from ) );
mesh.SetFaceTextureParameters( face, axisU, axisV, scale );
mesh.SetTextureOffset( face, source.GetTextureOffset( from ) );
}
else
{
mesh.SetFaceMaterial( face, material );
newFaces.Add( face );
}
}
// A face can only join at a corner that still has a free side, and what is free
// changes with every face that joins. What did not fit gets another turn after the rest.
var waiting = InOrder( rings );
for ( var pass = 0; pass < JoinPasses && waiting.Count > 0; pass++ )
{
var later = new List<Ring>();
foreach ( var ring in waiting )
{
var loop = new HalfEdgeMesh.VertexHandle[ring.Points.Length];
for ( var i = 0; i < loop.Length; i++ )
loop[i] = handles[ring.Points[i]] ??= mesh.AddVertex( points[ring.Points[i]].ToVector() );
var face = mesh.AddFace( loop );
if ( face.IsValid ) Finish( face, ring );
else later.Add( ring );
}
if ( later.Count == waiting.Count ) break;
waiting = later;
}
foreach ( var ring in waiting )
{
var face = mesh.AddFace( mesh.AddVertices( [.. ring.Points.Select( x => points[x].ToVector() )] ) );
if ( face.IsValid ) Finish( face, ring );
}
mesh.SetSmoothingAngle( 40.0f );
mesh.ComputeFaceTextureCoordinatesFromParameters();
foreach ( var face in newFaces )
mesh.TextureAlignToGrid( Transform.Zero, face );
return mesh;
}
}