Editor/Effigy/MeshHoleRepairCurved.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// Closing a mouth that crosses ANY number of faces, coplanar or not — the cut through a curved
/// surface, and the second cut into a face a first one already took apart.
///
/// WHAT THE OTHER TWO REPAIRS LEAVE. `MeshHoleRepair` wants one coplanar face containing the whole
/// loop. `MeshHoleRepairSpan` wants exactly two, with the loop crossing their shared edge once.
/// Both are right to be that narrow — see their own comments — and both walk away from the two
/// cases that actually turn up on a real part:
///
/// - **A cut through a curved face.** The wall of a cylinder is a ring of flat quads, so the mouth
/// of a hole drilled into it lies across several of them and is not planar at all. There is no
/// loop normal, no containing face, and nothing for a planar test to be run in.
/// - **A second cut through a face the first one already repaired.** The single-face repair splices
/// a hole in by triangulating, so the surface it fixed is now a fan. The next mouth to land there
/// crosses a dozen coplanar triangles, which is neither one face nor two.
///
/// THE GENERALISATION IS PER-FACE, NOT PER-LOOP. Each face the mouth crosses gets the piece of the
/// loop that lies in it — an ARC, a chord with both ends on that face's own boundary — and the face
/// is then re-partitioned by its arcs into the regions they cut it into. A face may get more than
/// one arc: the middle quad of a cylinder wall has the hole passing through it top and bottom, and
/// the strip between those two arcs is the hole rather than material.
///
/// WHICH REGIONS ARE MATERIAL IS READ OFF THE WALL, NOT GUESSED. Every loop edge is used by exactly
/// one face already — the tunnel wall — and two faces sharing an edge traverse it in opposite
/// directions. So the surface's own boundary has to run along each arc the OTHER way from the wall,
/// and a region that traverses one of its arcs the wall's way is on the void side of it. That rule
/// needs no plane, no containment test and no knowledge of which side the solid is on, which is
/// exactly why it survives the curved case where every planar argument stops working.
///
/// IT REFUSES THE SAME WAY THE OTHERS DO, and one refusal is worth naming: an arc endpoint has to
/// already be a vertex. A mouth that crosses from one face to the next somewhere in the middle of a
/// shared edge would need that crossing invented, and inventing it means splitting a neighbouring
/// face this was not asked to touch. Everything it does build is checked before it is kept — see
/// CloseCurvedLoops — because a repair with this much machinery in it must not be able to leave the
/// mesh worse than it found it.
/// </summary>
public static class MeshHoleRepairCurved
{
/// <summary>How far off a face's plane a loop vertex may sit and still count as lying in it. A
/// float-drift tolerance: the mouth is exactly on the surface in exact arithmetic.</summary>
const float PlaneTolerance = 1e-3f;
/// <summary>
/// Close every boundary loop that lies across two or more faces, and return how many.
///
/// Run LAST, so it only ever sees what the single-face and span repairs declined.
///
/// EVERY REPAIR IS CHECKED AND ROLLED BACK IF IT DID NOT HELP. The other two repairs are simple
/// enough to argue about; this one splits polygons by chords and decides materiality from a
/// winding, and the failure mode of getting that wrong is a mesh that is closed, manifold and
/// inside out. So the mesh is measured before and after each loop, and a repair that did not
/// strictly reduce the open boundary — or that introduced a non-manifold edge — is undone.
/// </summary>
public static int CloseCurvedLoops( PolyMesh mesh )
{
if ( mesh is null || mesh.FaceCount == 0 )
return 0;
var closed = 0;
while ( true )
{
var loops = BoundaryLoops( mesh );
var progressed = false;
foreach ( var loop in loops )
{
if ( loop.Count < 3 )
continue;
var before = MeshValidator.Validate( mesh );
var snapshot = mesh.Faces;
// The face list is replaced wholesale rather than edited, so undoing is putting the
// old list back. Positions are never touched - nothing here adds a vertex.
mesh.Faces = CloneFaces( snapshot );
if ( !TryCloseAcrossFaces( mesh, loop ) )
{
mesh.Faces = snapshot;
continue;
}
var after = MeshValidator.Validate( mesh );
if ( after.BoundaryEdges >= before.BoundaryEdges
|| after.NonManifoldEdges > before.NonManifoldEdges
|| !after.IsValid )
{
mesh.Faces = snapshot;
continue;
}
closed++;
progressed = true;
break;
}
if ( !progressed )
return closed;
}
}
static List<Face> CloneFaces( List<Face> faces )
{
var copy = new List<Face>( faces.Count );
foreach ( var f in faces )
copy.Add( new Face( (int[])f.Indices.Clone(), (Vec2[])f.UVs.Clone(), f.Material ) );
return copy;
}
// --- the repair -------------------------------------------------------------------------------
static bool TryCloseAcrossFaces( PolyMesh mesh, List<int> loop )
{
// The direction the SURFACE has to walk the mouth: against the wall, because two faces
// sharing an edge traverse it opposite ways. Everything below is expressed in this order.
if ( !OrderAgainstTheWall( mesh, loop, out var ordered ) )
return false;
var hosts = FindHostFaces( mesh, ordered );
// Fewer than two is the single-face case, which MeshHoleRepair does properly and which must
// not be re-done here: it wants a hole spliced in, not a boundary notched.
if ( hosts.Count < 2 )
return false;
if ( !BuildArcs( mesh, ordered, hosts, out var arcsByFace ) )
return false;
// Every face's replacement is built before any of them is written. A half-applied repair is
// worse than none, and with several faces in play that is a real possibility.
var replacements = new Dictionary<int, List<Face>>();
foreach ( var (faceIndex, arcs) in arcsByFace )
{
if ( !RepartitionFace( mesh, faceIndex, arcs, out var regions ) )
return false;
replacements[faceIndex] = regions;
}
// Rebuilt in index order so the surviving faces keep their relative order, which keeps
// face-index-based references as stable as a repair can leave them.
var rebuilt = new List<Face>( mesh.FaceCount + replacements.Count );
for ( var fi = 0; fi < mesh.FaceCount; fi++ )
{
if ( replacements.TryGetValue( fi, out var regions ) )
rebuilt.AddRange( regions );
else
rebuilt.Add( mesh.Faces[fi] );
}
mesh.Faces = rebuilt;
return true;
}
/// <summary>
/// Put the loop in the order the surface has to walk it, or refuse.
///
/// Each loop edge is used by exactly one face — the wall — and that face traverses it one way or
/// the other. A well-formed mouth is consistent about which: the wall is a strip and its edges
/// all run the same way round it. An inconsistent loop is two openings that met, or a walk that
/// went the wrong way at a fork, and neither is something to guess at.
/// </summary>
static bool OrderAgainstTheWall( PolyMesh mesh, List<int> loop, out List<int> ordered )
{
ordered = null;
var edgeFaces = mesh.BuildEdgeFaces();
var forward = 0;
var backward = 0;
for ( var i = 0; i < loop.Count; i++ )
{
var a = loop[i];
var b = loop[(i + 1) % loop.Count];
if ( !edgeFaces.TryGetValue( new EdgeKey( a, b ), out var faces ) || faces.Count != 1 )
return false;
var wall = mesh.Faces[faces[0]];
if ( !Traverses( wall, a, b, out var wallForward ) )
return false;
if ( wallForward )
forward++;
else
backward++;
}
if ( forward > 0 && backward > 0 )
return false;
// The wall walks it one way, so the surface walks it the other.
ordered = new List<int>( loop );
if ( forward > 0 )
ordered.Reverse();
return true;
}
/// <summary>Whether the face contains the directed edge a→b, b→a, or neither.</summary>
static bool Traverses( Face face, int a, int b, out bool forward )
{
forward = false;
for ( var i = 0; i < face.Count; i++ )
{
var x = face.Indices[i];
var y = face.Indices[(i + 1) % face.Count];
if ( x == a && y == b )
{
forward = true;
return true;
}
if ( x == b && y == a )
return true;
}
return false;
}
/// <summary>
/// The faces the mouth crosses: every face that carries at least one of the loop's vertices in
/// its own plane and inside its own outline.
///
/// A WALL IS A FACE THAT USES ONE OF THE LOOP'S EDGES, and that is the test rather than the one
/// the other two repairs use. They exclude any face that lists a loop VERTEX among its corners,
/// which is correct for them and too strong here: where the mouth crosses from one face to the
/// next, the crossing point is often a corner of both — a shaft driven through a ridge breaks
/// that ridge exactly at the two points the mouth meets it, and by the vertex rule both roof
/// panels would be dismissed as walls and the mouth left open. The edge rule says the same thing
/// about a genuine wall (it owns the loop's edges, which is what makes them boundary edges at
/// all) without dismissing the surface the mouth is in.
/// </summary>
static List<int> FindHostFaces( PolyMesh mesh, List<int> loop )
{
var hosts = new List<int>();
var loopEdges = new HashSet<EdgeKey>();
for ( var i = 0; i < loop.Count; i++ )
loopEdges.Add( new EdgeKey( loop[i], loop[(i + 1) % loop.Count] ) );
for ( var fi = 0; fi < mesh.FaceCount; fi++ )
{
var face = mesh.Faces[fi];
if ( face.Count < 3 )
continue;
var isWall = false;
for ( var i = 0; i < face.Count; i++ )
{
if ( loopEdges.Contains( new EdgeKey( face.Indices[i], face.Indices[(i + 1) % face.Count] ) ) )
{
isWall = true;
break;
}
}
if ( isWall )
continue;
var normal = mesh.FaceNormal( face );
if ( normal.LengthSquared < 1e-20f )
continue;
Basis( normal, out var u, out var v );
var plane = Vec3.Dot( mesh.Positions[face.Indices[0]], normal );
var outline = Polygon( mesh, face, u, v );
var carries = false;
foreach ( var index in loop )
{
if ( MathF.Abs( Vec3.Dot( mesh.Positions[index], normal ) - plane ) > PlaneTolerance )
continue;
var p = Flatten( mesh.Positions[index], u, v );
if ( PointInPolygon( outline, p ) || OnBoundary( outline, p ) )
{
carries = true;
break;
}
}
if ( carries )
hosts.Add( fi );
}
return hosts;
}
/// <summary>
/// Cut the loop into one arc per crossing of one face, and check that the arcs between them
/// account for the whole loop.
///
/// An arc is a maximal run of loop vertices lying in one face, whose two ends lie ON that face's
/// boundary. THE COMPLETENESS CHECK IS THE POINT: a loop edge belonging to no face's arc is a
/// piece of mouth crossing something this repair cannot see, and closing the rest of it would
/// weld a surface shut around an opening that is still there.
/// </summary>
static bool BuildArcs( PolyMesh mesh, List<int> loop, List<int> hosts,
out Dictionary<int, List<List<int>>> arcsByFace )
{
arcsByFace = new Dictionary<int, List<List<int>>>();
// Which face each loop vertex is inside, and which faces' boundaries it sits on. Both are
// needed: an arc runs through the inside and ends on the boundary.
var inside = new Dictionary<int, HashSet<int>>();
var on = new Dictionary<int, HashSet<int>>();
foreach ( var fi in hosts )
{
var face = mesh.Faces[fi];
var normal = mesh.FaceNormal( face );
Basis( normal, out var u, out var v );
var plane = Vec3.Dot( mesh.Positions[face.Indices[0]], normal );
var outline = Polygon( mesh, face, u, v );
for ( var i = 0; i < loop.Count; i++ )
{
if ( MathF.Abs( Vec3.Dot( mesh.Positions[loop[i]], normal ) - plane ) > PlaneTolerance )
continue;
var p = Flatten( mesh.Positions[loop[i]], u, v );
if ( OnBoundary( outline, p ) )
Add( on, i, fi );
else if ( PointInPolygon( outline, p ) )
Add( inside, i, fi );
}
}
// A vertex strictly inside two faces means two surfaces overlap, and which one owns the
// mouth stops being answerable.
foreach ( var (_, faces) in inside )
{
if ( faces.Count > 1 )
return false;
}
// Every loop EDGE has to lie in exactly one face, and that face is what the arcs are built
// from. Working per edge rather than per vertex is what makes the completeness check mean
// something: a vertex can sit on a boundary shared by two faces without telling you which
// side the mouth went.
var edgeFace = new int[loop.Count];
for ( var i = 0; i < loop.Count; i++ )
{
var j = (i + 1) % loop.Count;
// Faces holding BOTH endpoints, split by whether the edge actually goes into the face
// or merely runs along its outline. An edge with both ends on boundaries is the second
// kind - it can sit on a shared edge two faces both own - so a face the edge cuts INTO
// wins over one it only touches.
var cutting = new List<int>();
var touching = new List<int>();
foreach ( var fi in hosts )
{
if ( !LiesIn( inside, on, i, fi ) || !LiesIn( inside, on, j, fi ) )
continue;
if ( Has( inside, i, fi ) || Has( inside, j, fi ) )
cutting.Add( fi );
else
touching.Add( fi );
}
var candidates = cutting.Count > 0 ? cutting : touching;
// Exactly one, or there is no answer. Two faces the mouth cuts into along the same edge
// is two surfaces overlapping, and picking one would seal the wrong side.
if ( candidates.Count != 1 )
return false;
var owner = candidates[0];
edgeFace[i] = owner;
}
// Runs of consecutive edges owned by one face are one arc. Start the walk at a change of
// owner so a run is never split across the wrap.
var start = -1;
for ( var i = 0; i < loop.Count; i++ )
{
if ( edgeFace[i] != edgeFace[(i + loop.Count - 1) % loop.Count] )
{
start = i;
break;
}
}
// No change of owner anywhere means one face owns the whole loop, which is the single-face
// case and not this one.
if ( start < 0 )
return false;
var index = start;
for ( var consumed = 0; consumed < loop.Count; )
{
var owner = edgeFace[index];
var arc = new List<int> { loop[index] };
while ( consumed < loop.Count && edgeFace[index] == owner )
{
index = (index + 1) % loop.Count;
arc.Add( loop[index] );
consumed++;
}
// Both ends of an arc must sit on the face's own boundary, or there is nothing to splice
// it into.
if ( !Has( on, IndexOf( loop, arc[0] ), owner ) || !Has( on, IndexOf( loop, arc[^1] ), owner ) )
return false;
if ( !arcsByFace.TryGetValue( owner, out var list ) )
arcsByFace[owner] = list = new List<List<int>>();
list.Add( arc );
}
return arcsByFace.Count >= 2;
static void Add( Dictionary<int, HashSet<int>> map, int key, int value )
{
if ( !map.TryGetValue( key, out var set ) )
map[key] = set = new HashSet<int>();
set.Add( value );
}
static bool Has( Dictionary<int, HashSet<int>> map, int key, int value ) =>
key >= 0 && map.TryGetValue( key, out var set ) && set.Contains( value );
static bool LiesIn( Dictionary<int, HashSet<int>> inside, Dictionary<int, HashSet<int>> on, int key, int face ) =>
Has( inside, key, face ) || Has( on, key, face );
}
static int IndexOf( List<int> loop, int vertex ) => loop.IndexOf( vertex );
/// <summary>
/// One face, cut into the regions its arcs make of it, with the void ones dropped.
///
/// The arcs are chords: both ends lie on the face's outline, and they do not cross each other
/// because the loop they came from does not cross itself. Cutting a polygon by non-crossing
/// chords is a recursion — split by one chord into two halves, hand each half the chords whose
/// ends it still holds.
///
/// MATERIALITY IS DECIDED ON THE FINISHED REGIONS, NOT CARRIED DOWN THE RECURSION, and that
/// distinction is the whole correctness of this function. It is tempting to mark the half that
/// walks a chord the wall's way as void and let its children inherit that — and it is wrong,
/// because that half is not a region yet. On the middle face of a mouth that crosses three
/// strips, the first chord cuts off the top strip and leaves "the mouth plus the bottom strip"
/// as one lump; the second chord then separates them, and the bottom strip is material despite
/// having come out of the lump. So every finished ring is asked directly: which arcs are on YOUR
/// boundary, and which way do you walk them. All forwards is material, and anything else is
/// hole.
/// </summary>
static bool RepartitionFace( PolyMesh mesh, int faceIndex, List<List<int>> arcs, out List<Face> regions )
{
regions = null;
var face = mesh.Faces[faceIndex];
var normal = mesh.FaceNormal( face );
if ( normal.LengthSquared < 1e-20f )
return false;
Basis( normal, out var u, out var v );
if ( !AugmentedBoundary( mesh, face, arcs, u, v, out var boundary ) )
return false;
var open = new List<(List<int> Ring, List<List<int>> Arcs)>
{
(boundary, arcs)
};
var done = new List<List<int>>();
while ( open.Count > 0 )
{
var (ring, remaining) = open[^1];
open.RemoveAt( open.Count - 1 );
if ( remaining.Count == 0 )
{
done.Add( ring );
continue;
}
var arc = remaining[0];
var rest = remaining.GetRange( 1, remaining.Count - 1 );
var from = ring.IndexOf( arc[0] );
var to = ring.IndexOf( arc[^1] );
if ( from < 0 || to < 0 || from == to )
return false;
// The half that runs from the arc's start round to its end and comes back ALONG the arc
// backwards, and the half that closes by walking it forwards. Which of them is material
// is settled at the end, on the finished rings - see the comment above.
var back = Slice( ring, from, to );
AppendInterior( back, arc, reversed: true );
var forwards = Slice( ring, to, from );
AppendInterior( forwards, arc, reversed: false );
var backArcs = Distribute( rest, back );
var forwardArcs = Distribute( rest, forwards );
// A chord whose ends do not both land in one of the two halves means the arcs cross,
// which a simple loop cannot do - so it is a fault in the arcs rather than a shape.
if ( backArcs.Count + forwardArcs.Count != rest.Count )
return false;
open.Add( (back, backArcs) );
open.Add( (forwards, forwardArcs) );
}
regions = new List<Face>();
foreach ( var ring in done )
{
if ( ring.Count < 3 )
return false;
var seen = new HashSet<int>();
foreach ( var index in ring )
{
if ( !seen.Add( index ) )
return false;
}
if ( !IsMaterial( ring, arcs, out var material ) )
return false;
if ( !material )
continue;
regions.Add( new Face( ring.ToArray(), CarriedUVs( mesh, face, ring, u, v ), face.Material ) );
}
// Every region dropped means the whole face was inside the mouth, which cannot be: a face
// the mouth crosses has material on at least one side of the crossing.
return regions.Count > 0;
}
/// <summary>
/// Whether a finished region is material: it walks every arc on its own boundary the way the
/// SURFACE walks the mouth, rather than the way the wall does.
///
/// A region with no arc on its boundary at all is a fault rather than an answer — a face split by
/// chords has every piece touching one — so it refuses instead of guessing.
/// </summary>
static bool IsMaterial( List<int> ring, List<List<int>> arcs, out bool material )
{
material = true;
var found = false;
foreach ( var arc in arcs )
{
var at = ring.IndexOf( arc[0] );
if ( at < 0 )
continue;
var next = ring[(at + 1) % ring.Count];
var previous = ring[(at + ring.Count - 1) % ring.Count];
if ( next == arc[1] )
{
found = true;
continue;
}
if ( previous == arc[1] )
{
found = true;
material = false;
}
}
return found;
}
/// <summary>The chords whose two ends both appear in this ring.</summary>
static List<List<int>> Distribute( List<List<int>> arcs, List<int> ring )
{
var kept = new List<List<int>>();
foreach ( var arc in arcs )
{
if ( ring.Contains( arc[0] ) && ring.Contains( arc[^1] ) )
kept.Add( arc );
}
return kept;
}
/// <summary>The run of a cyclic ring from one position to another, inclusive of both.</summary>
static List<int> Slice( List<int> ring, int from, int to )
{
var slice = new List<int>();
var i = from;
while ( true )
{
slice.Add( ring[i] );
if ( i == to )
return slice;
i = (i + 1) % ring.Count;
if ( slice.Count > ring.Count )
return slice;
}
}
/// <summary>The arc's inside, in either direction. The ends are already on the ring.</summary>
static void AppendInterior( List<int> ring, List<int> arc, bool reversed )
{
if ( reversed )
{
for ( var i = arc.Count - 2; i >= 1; i-- )
ring.Add( arc[i] );
}
else
{
for ( var i = 1; i < arc.Count - 1; i++ )
ring.Add( arc[i] );
}
}
/// <summary>
/// The face's own boundary with every arc endpoint inserted into the edge it sits on, sorted
/// along that edge.
///
/// The endpoints are vertices that already exist and already lie on the outline — that is the
/// condition this repair refuses without — so nothing is invented here. What it produces is the
/// ring the chord recursion indexes into.
/// </summary>
static bool AugmentedBoundary( PolyMesh mesh, Face face, List<List<int>> arcs, Vec3 u, Vec3 v,
out List<int> boundary )
{
boundary = new List<int>( face.Count + arcs.Count * 2 );
var ends = new List<int>();
foreach ( var arc in arcs )
{
ends.Add( arc[0] );
ends.Add( arc[^1] );
}
for ( var i = 0; i < face.Count; i++ )
{
var a = face.Indices[i];
var b = face.Indices[(i + 1) % face.Count];
boundary.Add( a );
var aFlat = Flatten( mesh.Positions[a], u, v );
var bFlat = Flatten( mesh.Positions[b], u, v );
var onEdge = new List<(float T, int Index)>();
foreach ( var end in ends )
{
if ( end == a || end == b || boundary.Contains( end ) )
continue;
var p = Flatten( mesh.Positions[end], u, v );
if ( !OnSegment( aFlat, bFlat, p ) )
continue;
var along = bFlat - aFlat;
var t = ((p.x - aFlat.x) * along.x + (p.y - aFlat.y) * along.y) / MathF.Max( along.LengthSquared, 1e-12f );
onEdge.Add( (t, end) );
}
onEdge.Sort( ( x, y ) => x.T.CompareTo( y.T ) );
foreach ( var (_, index) in onEdge )
boundary.Add( index );
}
// Every endpoint has to have found a home, or the ring the recursion indexes into does not
// contain the chord it is about to split by.
foreach ( var end in ends )
{
if ( !boundary.Contains( end ) )
return false;
}
return true;
}
/// <summary>
/// UVs for a rebuilt region: the face's own corners keep theirs, and the mouth's vertices get
/// theirs from the same planar map the corners already follow. Same rule and same reason as
/// MeshHoleRepairSpan.NewUVs — a repair must not smear the texture across the surface it fixed.
/// </summary>
static Vec2[] CarriedUVs( PolyMesh mesh, Face face, List<int> ring, Vec3 u, Vec3 v )
{
var known = new Dictionary<int, Vec2>();
for ( var i = 0; i < face.Count; i++ )
known[face.Indices[i]] = face.UVs[i];
var uvs = new Vec2[ring.Count];
var origin = Flatten( mesh.Positions[face.Indices[0]], u, v );
var originUv = face.UVs[0];
var scaleU = 0f;
var scaleV = 0f;
for ( var i = 1; i < face.Count; i++ )
{
var p = Flatten( mesh.Positions[face.Indices[i]], u, v ) - origin;
var duv = new Vec2( face.UVs[i].x - originUv.x, face.UVs[i].y - originUv.y );
if ( MathF.Abs( p.x ) > 1e-6f && scaleU == 0f )
scaleU = duv.x / p.x;
if ( MathF.Abs( p.y ) > 1e-6f && scaleV == 0f )
scaleV = duv.y / p.y;
}
for ( var i = 0; i < ring.Count; i++ )
{
if ( known.TryGetValue( ring[i], out var existing ) )
{
uvs[i] = existing;
continue;
}
var p = Flatten( mesh.Positions[ring[i]], u, v ) - origin;
uvs[i] = new Vec2( originUv.x + p.x * scaleU, originUv.y + p.y * scaleV );
}
return uvs;
}
// --- shared geometry, kept here so this file stands alone -------------------------------------
static List<List<int>> BoundaryLoops( PolyMesh mesh )
{
var atVertex = new Dictionary<int, List<int>>();
foreach ( var (key, faces) in mesh.BuildEdgeFaces() )
{
if ( faces.Count != 1 )
continue;
Link( key.A, key.B );
Link( key.B, key.A );
}
var loops = new List<List<int>>();
var used = new HashSet<EdgeKey>();
foreach ( var start in atVertex.Keys )
{
if ( atVertex[start].Count != 2 )
continue;
var loop = new List<int>();
var current = start;
var previous = -1;
while ( true )
{
loop.Add( current );
var next = -1;
foreach ( var candidate in atVertex[current] )
{
if ( candidate == previous || used.Contains( new EdgeKey( current, candidate ) ) )
continue;
next = candidate;
break;
}
if ( next < 0 )
break;
used.Add( new EdgeKey( current, next ) );
previous = current;
current = next;
if ( current == start )
break;
}
if ( loop.Count >= 3 && current == start )
loops.Add( loop );
}
return loops;
void Link( int from, int to )
{
if ( !atVertex.TryGetValue( from, out var list ) )
{
list = new List<int>( 2 );
atVertex[from] = list;
}
list.Add( to );
}
}
static void Basis( Vec3 normal, out Vec3 u, out Vec3 v )
{
var n = normal.Normal;
var seed = MathF.Abs( n.z ) < 0.9f ? new Vec3( 0, 0, 1 ) : new Vec3( 1, 0, 0 );
u = Vec3.Cross( seed, n ).Normal;
v = Vec3.Cross( n, u ).Normal;
}
static Vec2 Flatten( Vec3 p, Vec3 u, Vec3 v ) => new( Vec3.Dot( p, u ), Vec3.Dot( p, v ) );
static List<Vec2> Polygon( PolyMesh mesh, Face face, Vec3 u, Vec3 v )
{
var polygon = new List<Vec2>( face.Count );
foreach ( var index in face.Indices )
polygon.Add( Flatten( mesh.Positions[index], u, v ) );
return polygon;
}
static bool OnBoundary( List<Vec2> polygon, Vec2 p )
{
for ( var i = 0; i < polygon.Count; i++ )
{
if ( OnSegment( polygon[i], polygon[(i + 1) % polygon.Count], p ) )
return true;
}
return false;
}
static bool OnSegment( Vec2 a, Vec2 b, Vec2 p )
{
var along = b - a;
var length = along.Length;
if ( length < 1e-9f )
return (p - a).Length < 1e-5f;
var cross = along.x * (p.y - a.y) - along.y * (p.x - a.x);
if ( MathF.Abs( cross ) / length > 1e-4f )
return false;
var t = ((p.x - a.x) * along.x + (p.y - a.y) * along.y) / (length * length);
return t >= -1e-5f && t <= 1f + 1e-5f;
}
static bool PointInPolygon( List<Vec2> polygon, Vec2 point )
{
var inside = false;
for ( int i = 0, j = polygon.Count - 1; i < polygon.Count; j = i++ )
{
var a = polygon[i];
var b = polygon[j];
if ( a.y > point.y != b.y > point.y
&& point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x )
{
inside = !inside;
}
}
return inside;
}
}