Editor/Effigy/Triangulate.cs
using System;
using System.Collections.Generic;
using System.Linq;
namespace Effigy;
/// <summary>
/// Ear clipping for simple polygons — the triangulation every part of the tool that turns an
/// n-gon face into triangles needs.
///
/// WHY THIS EXISTS: everything used to fan from corner 0, on the stated grounds that "Effigy's
/// faces are convex". That is true of primitives and of extrude side walls, and it is NOT true of
/// an extrude cap, which is whatever closed region the user drew. Fanning a concave profile fills
/// its notches in: draw a dart and the extrude comes back as a quadrilateral with the concave
/// corner swallowed. Ear clipping handles any simple polygon, convex or not.
///
/// Holes ARE supported, by WithHoles below — inner loops are spliced into the outer one along a
/// bridge and the whole thing is ear clipped as a single ring. This comment said the opposite for
/// some time after that landed, which is the failure mode this repo keeps hitting: a stated
/// limitation outliving the limitation itself.
/// </summary>
public static class Triangulate
{
/// <summary>
/// Triangulate a polygon given in plane coordinates. Returns index triples INTO THE INPUT
/// LIST, wound the same way the input is, so a caller can map them straight back onto its own
/// vertices without worrying about which way the face faces.
/// </summary>
/// <summary>
/// Triangulate a loop that has already had its holes SPLICED IN along bridges — one boundary
/// that runs out to an inner ring, round it, and back along the same seam.
///
/// WHY THIS IS NOT Polygon(). Polygon assumes a SIMPLE polygon: no repeated vertex, no edge
/// travelled twice. A bridged loop breaks both, and Polygon does not fail on one - it returns
/// overlapping triangles that cover more area than the outline encloses, which renders as a
/// hole that has been filled in. WithHoles never hit this because it builds its own bridges and
/// finishes through ClipRing, the clipper that tolerates them; Polygon is simply the wrong door
/// and nothing had ever knocked on it with a bridged loop before.
///
/// What knocks now is s&box's mesh boolean. A half-edge face is one closed loop, so a cut that
/// leaves a hole in a face can only hand it back bridged, and that is the shape a cut arrives
/// in — see EffigyMeshBoolean.
///
/// Winding follows the input, the same contract Polygon gives.
/// </summary>
public static List<(int A, int B, int C)> BridgedLoop( IReadOnlyList<Vec2> points )
{
if ( points is null || points.Count < 3 )
return new List<(int, int, int)>();
// WELDING IS THE WHOLE TRICK, and getting it wrong looks like success. ClipRing tolerates a
// bridge only when the seam's two visits are THE SAME INDEX - that identity is what lets its
// ear test recognise the doubled edge instead of measuring a zero-area corner and rejecting
// every candidate. Handed a ring whose visits are two different indices at identical
// positions, it finds no ear at all and falls back to a fan, which returns a plausible pile
// of triangles covering the hole and everything else besides.
//
// WithHoles never meets this because it splices indices into a shared point list and the
// repeat is literal. A loop arriving from outside - the shape s&box's boolean returns - has
// to be put into that same form first.
WeldRing( points, out var welded, out var ring, out var representative );
var triangles = ClipRing( welded, ring, reversed: RingSignedArea( welded, ring ) < 0f );
for ( var i = 0; i < triangles.Count; i++ )
{
var (a, b, c) = triangles[i];
triangles[i] = (representative[a], representative[b], representative[c]);
}
return triangles;
}
/// <summary>Two points this close together in plane units are the same point. Loose enough for
/// a seam the engine reports twice, far below anything anyone draws.</summary>
const float WeldTolerance = 1e-5f;
/// <summary>Signed area of a ring of indices, rather than of a bare point list.</summary>
static float RingSignedArea( IReadOnlyList<Vec2> points, IReadOnlyList<int> ring )
{
var sum = 0f;
for ( var i = 0; i < ring.Count; i++ )
{
var a = points[ring[i]];
var b = points[ring[(i + 1) % ring.Count]];
sum += a.x * b.y - b.x * a.y;
}
return sum * 0.5f;
}
/// <summary>The 3D form of <see cref="BridgedLoop"/>, flattened onto the loop's own Newell
/// normal exactly as <see cref="Face"/> does.</summary>
public static List<(int A, int B, int C)> BridgedFace( IReadOnlyList<Vec3> positions )
{
if ( positions is null || positions.Count < 3 )
return new List<(int, int, int)>();
return BridgedLoop( Flatten( positions ) );
}
/// <summary>
/// Collapse a loop's coincident corners so a seam's two visits become THE SAME INDEX, which is
/// the form both the ear clipper and the splitter need. Shared by BridgedLoop and
/// SplitBridgedLoop rather than written twice: they must agree exactly on which corners are the
/// same point, or one of them will find a bridge the other cannot.
/// </summary>
static void WeldRing( IReadOnlyList<Vec2> points, out List<Vec2> welded, out List<int> ring,
out List<int> representative )
{
welded = new List<Vec2>( points.Count );
ring = new List<int>( points.Count );
representative = new List<int>( points.Count );
for ( var i = 0; i < points.Count; i++ )
{
var index = -1;
for ( var j = 0; j < welded.Count; j++ )
{
if ( MathF.Abs( welded[j].x - points[i].x ) > WeldTolerance
|| MathF.Abs( welded[j].y - points[i].y ) > WeldTolerance )
continue;
index = j;
break;
}
if ( index < 0 )
{
index = welded.Count;
welded.Add( points[i] );
// Which position in the CALLER'S list this welded point stands for, so the results
// come back indexed the way the caller handed its loop in.
representative.Add( i );
}
ring.Add( index );
}
}
/// <summary>
/// Split a bridged loop into TWO simple polygons instead of triangulating it. Returns index
/// loops into the caller's list, in the caller's own winding, or null when it will not do it.
///
/// WHY, given BridgedLoop already works. Because a Face is the unit of SELECTION and of
/// material assignment, and triangulating spends that unit freely. A 24-gon cap with a pocket
/// cut into it comes back as 29 triangles; clicking it to paint it paints one of them. The
/// topology genuinely forbids ONE face here - a face is one loop of corners and a face with a
/// hole has two boundaries - but TWO is available, and two is what someone means when they say
/// the cut should not have broken the face up.
///
/// THE SECOND BRIDGE IS THE WHOLE IDEA. The loop already carries one bridge joining the outer
/// boundary to the hole. Cut the ring a second time somewhere else and the annulus falls into
/// two ordinary n-gons. No new vertices, no triangles, and the quads everywhere else on the
/// model were never involved.
///
/// N HOLES GIVE N+1 FACES. Each hole is cut against whichever face it landed in, twice, so one
/// face becomes two and the count climbs by one per hole. The one-hole case is the same code
/// run once and is still by far the common one.
///
/// WHAT IT REFUSES, and why refusing matters more than succeeding: a loop whose repeated visits
/// do not sit where a bridge puts them, a hole that lands in no face, a hole with no valid pair
/// of bridges. Each returns null and the caller falls back to BridgedLoop, which is never wrong
/// - only coarse. A WRONG split is a self-intersecting face that is closed, manifold and
/// Euler-correct, which is precisely the class of defect the cut work already lost a day to.
/// </summary>
public static List<List<int>> SplitBridgedLoop( IReadOnlyList<Vec2> points )
{
// Three corners of outer boundary, three of hole, and the bridge's two repeated visits.
if ( points is null || points.Count < 8 )
return null;
WeldRing( points, out var welded, out var ring, out var representative );
if ( !RecoverBridges( ring, out var outer, out var holes ) || holes.Count == 0 )
return null;
var loops = SplitIntoFaces( welded, outer, holes );
if ( loops is null )
return null;
foreach ( var loop in loops )
{
for ( var i = 0; i < loop.Count; i++ )
loop[i] = representative[loop[i]];
}
return loops;
}
/// <summary>The 3D form of <see cref="SplitBridgedLoop"/>, flattened exactly as
/// <see cref="BridgedFace"/> does.</summary>
public static List<List<int>> SplitBridgedFace( IReadOnlyList<Vec3> positions )
{
if ( positions is null || positions.Count < 8 )
return null;
return SplitBridgedLoop( Flatten( positions ) );
}
/// <summary>
/// Take a welded ring apart into its outer boundary and every hole bridged into it.
///
/// ONE BRIDGE AT A TIME, INNERMOST FIRST. A bridge is walked out and back, so it leaves a
/// contiguous run - out to the hole, round it, back along the same seam - and peeling that run
/// off leaves a ring with one bridge fewer. Taking the shortest run first is what makes the
/// peel unambiguous when there are several, since a shorter run cannot contain a longer one.
///
/// Everything about the shape is checked rather than assumed at every peel: these loops come
/// from the engine, not from Bridge() above, and the cost of guessing wrong is a face that
/// passes validation while overlapping itself. Whatever is left when no more runs can be peeled
/// has to be a plain simple ring, or the whole recovery is refused.
/// </summary>
static bool RecoverBridges( List<int> ring, out List<int> outer, out List<List<int>> holes )
{
outer = new List<int>( ring );
holes = new List<List<int>>();
while ( PeelBridge( outer, out var reduced, out var hole ) )
{
outer = reduced;
holes.Add( hole );
}
return outer.Count >= 3 && outer.Distinct().Count() == outer.Count;
}
/// <summary>
/// Peel the innermost bridged hole off a ring. False when there is none left to take, which is
/// both the success case for a plain ring and the refusal for a ring whose repeats are not
/// bridges - <see cref="RecoverBridges"/> tells them apart by what is left over.
/// </summary>
static bool PeelBridge( List<int> ring, out List<int> reduced, out List<int> hole )
{
reduced = null;
hole = null;
var visits = new Dictionary<int, List<int>>();
for ( var i = 0; i < ring.Count; i++ )
{
if ( !visits.TryGetValue( ring[i], out var at ) )
{
at = new List<int>();
visits[ring[i]] = at;
}
at.Add( i );
}
// Shortest span first: an innermost run cannot have another nested inside it, so the
// pattern check below is being applied to a bridge and its own hole and nothing else.
var candidates = visits.Values
.Where( at => at.Count == 2 )
.OrderBy( at => at[1] - at[0] )
.ToList();
foreach ( var span in candidates )
{
var p = span[0];
var q = span[1];
// The hole's seam sits one step inside the outer's on both sides. Anything else is not
// a bridge, whatever else it may be.
if ( q - p < 4 || ring[p + 1] != ring[q - 1] )
continue;
var candidate = new List<int>();
for ( var i = p + 1; i < q - 1; i++ )
candidate.Add( ring[i] );
if ( candidate.Count < 3 || candidate.Distinct().Count() != candidate.Count )
continue;
// A hole's vertices belong to the hole and nowhere else. One that also turns up out on
// the outer boundary means the run being peeled is not the self-contained thing a
// bridge leaves behind.
var inside = candidate.ToHashSet();
var elsewhere = false;
// The run being skipped is p+1..q-1: the hole itself, and the seam repeat that closes
// it back onto its first vertex. Position q is the outer vertex the bridge left from
// and belongs to the boundary, so it is scanned like any other.
for ( var i = 0; i < ring.Count && !elsewhere; i++ )
{
if ( i > p && i < q )
continue;
elsewhere = inside.Contains( ring[i] );
}
if ( elsewhere )
continue;
reduced = new List<int>();
for ( var i = 0; i <= p; i++ )
reduced.Add( ring[i] );
for ( var i = q + 1; i < ring.Count; i++ )
reduced.Add( ring[i] );
if ( reduced.Count < 3 )
continue;
hole = candidate;
return true;
}
return false;
}
/// <summary>
/// Cut a boundary and the holes inside it into simple faces, one more face per hole.
///
/// EACH HOLE IS CUT AGAINST THE FACE IT LANDED IN, which is what makes n holes work as the
/// one-hole case run n times rather than as a different algorithm. The first hole splits the
/// outer boundary in two; the second splits whichever of those two it sits inside; and so on.
/// Holes still waiting their turn are obstacles while the cuts for this one are chosen - a
/// bridge laid across a hole not yet placed would leave that hole straddling two faces, with
/// nowhere to put it.
///
/// Rightmost hole first, the same order WithHoles bridges them in, so the result does not
/// depend on the order the profile finder happened to discover loops in.
/// </summary>
static List<List<int>> SplitIntoFaces( List<Vec2> points, List<int> outer, List<List<int>> holes )
{
var pending = holes
.OrderByDescending( h => h.Max( i => points[i].x ) )
.ToList();
var faces = new List<List<int>> { new List<int>( outer ) };
for ( var k = 0; k < pending.Count; k++ )
{
var hole = pending[k];
var others = pending.Skip( k + 1 ).ToList();
var index = faces.FindIndex( face => LoopInside( points, face, hole ) );
if ( index < 0 )
return null;
if ( !SplitFaceOnHole( points, faces[index], hole, others, out var first, out var second ) )
return null;
faces[index] = first;
faces.Add( second );
}
return faces;
}
/// <summary>
/// One face and one hole inside it become two faces, cut on two bridges.
///
/// The first bridge is the shortest valid one, the same choice <see cref="Bridge"/> makes; the
/// second starts opposite it and swings outward, because a bridge next door to the first splits
/// off a sliver beside a nearly whole face, which is two faces in the sense that a paper cut is
/// surgery.
/// </summary>
static bool SplitFaceOnHole( List<Vec2> points, List<int> face, List<int> hole,
List<List<int>> others, out List<int> first, out List<int> second )
{
first = null;
second = null;
if ( !FirstBridge( points, face, hole, others, out var a1, out var b1 ) )
return false;
if ( !SecondBridge( points, face, hole, others, a1, b1, out var a2, out var b2 ) )
return false;
first = WalkPair( face, hole, a1, a2, b2, b1 );
second = WalkPair( face, hole, a2, a1, b1, b2 );
return true;
}
/// <summary>The shortest bridge from face to hole that crosses nothing and runs through the
/// material rather than out in space or down the hole.</summary>
static bool FirstBridge( List<Vec2> points, List<int> face, List<int> hole,
List<List<int>> others, out int a, out int b )
{
a = -1;
b = -1;
var best = float.MaxValue;
for ( var i = 0; i < face.Count; i++ )
{
for ( var j = 0; j < hole.Count; j++ )
{
var length = (points[hole[j]] - points[face[i]]).LengthSquared;
if ( length >= best || length < 1e-12f )
continue;
if ( !SplitIsClear( points, face, hole, others, face[i], hole[j] ) )
continue;
best = length;
a = i;
b = j;
}
}
return a >= 0;
}
/// <summary>Whether a hole sits wholly within a face. One vertex inside and no edge crossing
/// the face's boundary is enough for two loops that are each simple: a loop with a point inside
/// and no way through the wall has no way out.</summary>
static bool LoopInside( List<Vec2> points, List<int> face, List<int> hole )
{
if ( !Contains( points, face, points[hole[0]] ) )
return false;
for ( var i = 0; i < hole.Count; i++ )
{
var a = points[hole[i]];
var b = points[hole[(i + 1) % hole.Count]];
if ( Crosses( points, face, a, b, hole[i], hole[(i + 1) % hole.Count] ) )
return false;
}
return true;
}
/// <summary>
/// Find a second bridge to cut the ring on, preferring the far side of it.
///
/// A bridge next door to the first one is perfectly valid and splits off a sliver beside a
/// nearly whole face, which is two faces in the sense that a paper cut is surgery. Starting
/// opposite and walking outward takes the balanced cut when there is one and still finds the
/// awkward one when there is not.
/// </summary>
static bool SecondBridge( List<Vec2> welded, List<int> outer, List<int> hole,
List<List<int>> others, int a1, int b1, out int a2, out int b2 )
{
a2 = -1;
b2 = -1;
var firstA = welded[outer[a1]];
var firstB = welded[hole[b1]];
for ( var step = 0; step < outer.Count; step++ )
{
var swing = step % 2 == 0 ? step / 2 : -(step / 2 + 1);
var candidate = ((a1 + outer.Count / 2 + swing) % outer.Count + outer.Count) % outer.Count;
if ( candidate == a1 )
continue;
var anchor = welded[outer[candidate]];
foreach ( var j in Enumerable.Range( 0, hole.Count )
.OrderBy( j => (welded[hole[j]] - anchor).LengthSquared ) )
{
// The first bridge's own landing: reusing it leaves one of the two faces with no
// hole side at all.
if ( j == b1 )
continue;
if ( !SplitIsClear( welded, outer, hole, others, outer[candidate], hole[j] ) )
continue;
// Two crossing bridges cut the ring into a figure of eight, and both halves come
// back self-intersecting.
if ( SegmentsCross( anchor, welded[hole[j]], firstA, firstB ) )
continue;
a2 = candidate;
b2 = j;
return true;
}
}
return false;
}
/// <summary>
/// The same three conditions <see cref="BridgeIsClear"/> demands - crossing no edge of either
/// ring, and a midpoint in the material rather than out in space or down the hole - plus one
/// the n-hole case adds: it must clear every hole still waiting to be placed. A cut laid across
/// one of those leaves it straddling two faces with nowhere to go, and a cut that merely ends
/// up inside one puts material where a hole is.
/// </summary>
static bool SplitIsClear( List<Vec2> points, List<int> outer, List<int> hole,
List<List<int>> others, int from, int to )
{
var a = points[from];
var b = points[to];
if ( Crosses( points, outer, a, b, from, to ) || Crosses( points, hole, a, b, from, to ) )
return false;
var mid = (a + b) * 0.5f;
if ( !Contains( points, outer, mid ) || Contains( points, hole, mid ) )
return false;
if ( others is null )
return true;
foreach ( var other in others )
{
if ( Crosses( points, other, a, b, from, to ) || Contains( points, other, mid ) )
return false;
}
return true;
}
/// <summary>One of the two halves: the outer boundary walked forward between the bridges, then
/// the hole walked forward back to where it started. Both rings are walked in the order the
/// original loop gave them, which is what carries the caller's winding through untouched.</summary>
static List<int> WalkPair( List<int> outer, List<int> hole, int aFrom, int aTo, int bFrom, int bTo )
{
var loop = new List<int>( outer.Count + hole.Count );
for ( var i = aFrom; ; i = (i + 1) % outer.Count )
{
loop.Add( outer[i] );
if ( i == aTo )
break;
}
for ( var j = bFrom; ; j = (j + 1) % hole.Count )
{
loop.Add( hole[j] );
if ( j == bTo )
break;
}
return loop;
}
public static List<(int A, int B, int C)> Polygon( IReadOnlyList<Vec2> points )
{
var triangles = new List<(int, int, int)>( Math.Max( points.Count - 2, 0 ) );
if ( points.Count < 3 )
return triangles;
// Ear clipping needs a known winding to tell "convex corner" from "reflex corner". Work
// counter-clockwise internally and let the caller keep whatever winding it had.
//
// THAT SECOND HALF IS NOT FREE, and it read as free for as long as nothing exercised it.
// Walking a clockwise polygon backwards makes every emitted triple counter-clockwise, so
// the output silently disagreed with the input about which way the surface faces — and a
// caller mapping those indices back onto its own vertices got a face pointing the wrong
// way, which under backface culling is an invisible one. It never showed because every
// caller today feeds counter-clockwise: Face() flattens onto the face's own Newell normal,
// which is counter-clockwise by construction, and ProfileFinder orients outer loops the
// same way. Hole loops are clockwise, and they are the next thing to arrive here.
//
// So the ring is walked backwards to find the ears, and each triple is emitted back in the
// caller's own winding.
var indices = new List<int>( points.Count );
var reversed = SignedArea( points ) < 0f;
if ( reversed )
{
for ( var i = points.Count - 1; i >= 0; i-- )
indices.Add( i );
}
else
{
for ( var i = 0; i < points.Count; i++ )
indices.Add( i );
}
return ClipRing( points, indices, reversed );
}
/// <summary>
/// Ear-clip a ring of indices that is already walked the right way round.
///
/// Shared by Polygon and WithHoles, which differ only in the ring they arrive with: a plain
/// polygon's is its own points, a holed one's has each hole spliced in along a bridge. Once the
/// ring exists there is one algorithm, and having one copy of it is what keeps a holed cap and
/// a plain one from ever disagreeing about winding.
/// </summary>
static List<(int A, int B, int C)> ClipRing( IReadOnlyList<Vec2> points, List<int> indices, bool reversed )
{
var triangles = new List<(int, int, int)>( Math.Max( indices.Count - 2, 0 ) );
void Emit( int a, int b, int c ) => triangles.Add( reversed ? (c, b, a) : (a, b, c) );
// Each pass round the remaining ring clips at most one ear, so this cannot run longer than
// n passes over an n-gon. The guard is for degenerate input - repeated or collinear points
// can leave a ring with no ear at all, and a silent infinite loop inside a viewport frame
// is not something anyone gets to debug comfortably.
var guard = indices.Count * indices.Count + 16;
while ( indices.Count > 3 && guard-- > 0 )
{
var clipped = false;
for ( var i = 0; i < indices.Count; i++ )
{
var prev = indices[(i - 1 + indices.Count) % indices.Count];
var cur = indices[i];
var next = indices[(i + 1) % indices.Count];
if ( !IsEar( points, indices, prev, cur, next ) )
continue;
Emit( prev, cur, next );
indices.RemoveAt( i );
clipped = true;
break;
}
if ( clipped )
continue;
// No ear anywhere: the polygon is self-intersecting or degenerate. Fall back to a fan
// so the caller still gets a surface rather than nothing at all.
break;
}
if ( indices.Count == 3 )
{
Emit( indices[0], indices[1], indices[2] );
return triangles;
}
for ( var i = 2; i < indices.Count; i++ )
Emit( indices[0], indices[i - 1], indices[i] );
return triangles;
}
/// <summary>
/// Triangulate a polygon that has holes in it — a plate with bolt holes, a washer, a flange.
///
/// THE TRICK IS THAT THERE IS NO TRICK. Ear clipping only works on a SIMPLE polygon, so each
/// hole is spliced into the outer loop along a "bridge": a segment from an outer vertex to a
/// hole vertex, walked out and back, which turns a ring-with-a-hole into one boundary that
/// happens to visit the bridge twice. After that it is an ordinary ear clip, and the doubled
/// bridge edge takes care of itself because IsEar already refuses a zero-area corner.
///
/// This is why a holed profile never needed a boolean. Capping around a hole is a 2D
/// triangulation problem and it always was; the old refusal called it "really the same problem
/// as a boolean subtract", which is what kept a rectangle with a circle in it unbuildable long
/// after ear clipping arrived.
///
/// BRIDGE CHOICE. The textbook answer is Eberly's: take the hole's rightmost vertex, cast a ray
/// and find a visible outer vertex. This takes the shortest bridge that is actually valid
/// instead — no crossing of any edge, and a midpoint genuinely inside the material — which is
/// more work per hole and far less to get subtly wrong. Sketch profiles have tens of points, not
/// thousands, so the cost does not matter and the certainty does.
///
/// Returns triples indexing a CONCATENATED list: the outer loop's points first, then each hole's
/// in the order given. The caller adds its vertices in that same order and maps straight across.
/// Empty when a hole cannot be bridged, which the caller should report rather than ignore.
/// </summary>
public static List<(int A, int B, int C)> WithHoles( IReadOnlyList<Vec2> outer,
IReadOnlyList<IReadOnlyList<Vec2>> holes )
{
if ( outer is null || outer.Count < 3 )
return new List<(int, int, int)>();
if ( holes is null || holes.Count == 0 )
return Polygon( outer );
if ( !BridgedRing( outer, holes, out var points, out var merged ) )
return new List<(int, int, int)>();
return ClipRing( points, merged, reversed: SignedArea( outer ) < 0f );
}
/// <summary>
/// The same profile as <see cref="WithHoles"/>, capped as TWO N-GONS instead of a pile of
/// triangles. Null when it will not do it, and then the caller falls back to WithHoles.
///
/// This is the sketch-side twin of <see cref="SplitBridgedLoop"/>, which already does it for a
/// face a CUT left a hole in. Both start from the same shape - one bridged ring - and the
/// reason is the same one: a Face is the unit of SELECTION and of material assignment, so a
/// washer capped as 29 triangles is a cap you paint one twenty-ninth of per click. The
/// difference was only ever where the ring came from. A cut's arrives from the boolean; this
/// one WithHoles builds for itself and used to hand straight to the ear clipper.
///
/// N HOLES, N+1 FACES. A profile does not have to be bridged first - it arrives as loops, which
/// is what the splitter wants anyway - so this hands them over directly rather than splicing a
/// ring for the sake of taking it apart again. Two bolt holes in a plate come back as three
/// faces.
///
/// Indexes the CONCATENATED list WithHoles promises - outer first, then each hole in order - and
/// comes back wound the way WithHoles winds its triples, so a caller swaps one for the other
/// without touching how it maps or which way it faces.
/// </summary>
public static List<List<int>> SplitWithHoles( IReadOnlyList<Vec2> outer,
IReadOnlyList<IReadOnlyList<Vec2>> holes )
{
if ( outer is null || outer.Count < 3 || holes is null || holes.Count == 0 )
return null;
// The concatenated layout the caller was promised, built here rather than borrowed from
// BridgedRing: the loops go in as loops and only their indices need to line up.
var points = new List<Vec2>( outer );
var rings = new List<List<int>>();
var offset = outer.Count;
foreach ( var hole in holes )
{
if ( hole is null || hole.Count < 3 )
return null;
var ring = new List<int>( hole.Count );
for ( var i = 0; i < hole.Count; i++ )
ring.Add( offset + i );
points.AddRange( hole );
offset = points.Count;
// A hole is walked the OPPOSITE way to the outer loop, the same as in BridgedRing: it
// is what keeps the material on one side the whole way round each face that comes out.
if ( SignedArea( hole ) > 0f == SignedArea( outer ) > 0f )
ring.Reverse();
rings.Add( ring );
}
var face = new List<int>( outer.Count );
for ( var i = 0; i < outer.Count; i++ )
face.Add( i );
var loops = SplitIntoFaces( points, face, rings );
if ( loops is null )
return null;
// ClipRing normalises a clockwise profile's output by emitting its triples backwards, so
// these have to turn the same way for the same reason - a caller that swapped one for the
// other would otherwise get a cap facing the other way.
if ( SignedArea( outer ) < 0f )
{
foreach ( var loop in loops )
loop.Reverse();
}
return loops;
}
/// <summary>
/// The one boundary a holed profile becomes: the outer loop with each hole spliced into it
/// along a bridge, indexing the concatenated point list WithHoles promises its callers.
///
/// Shared by WithHoles and SplitWithHoles rather than written twice, for the same reason
/// WeldRing is shared by the two loop readers: they must agree exactly on which ring they are
/// looking at, or the splitter will refuse a ring the clipper would have accepted and nobody
/// will be able to say why.
/// </summary>
static bool BridgedRing( IReadOnlyList<Vec2> outer, IReadOnlyList<IReadOnlyList<Vec2>> holes,
out List<Vec2> points, out List<int> merged )
{
// One flat list, in the order the caller was promised.
points = new List<Vec2>( outer );
merged = new List<int>( outer.Count );
var holeRings = new List<List<int>>();
var offset = outer.Count;
foreach ( var hole in holes )
{
if ( hole is null || hole.Count < 3 )
{
// A degenerate hole is skipped rather than fatal, but its points still go into the
// list: the caller's vertex layout is fixed and must not shift underneath it.
points.AddRange( hole ?? Array.Empty<Vec2>() );
offset = points.Count;
continue;
}
var ring = new List<int>( hole.Count );
for ( var i = 0; i < hole.Count; i++ )
ring.Add( offset + i );
points.AddRange( hole );
offset = points.Count;
// A hole is walked the OPPOSITE way to the outer loop. That is what makes the spliced
// boundary keep the material on one side the whole way round.
if ( SignedArea( hole ) > 0f == SignedArea( outer ) > 0f )
ring.Reverse();
holeRings.Add( ring );
}
for ( var i = 0; i < outer.Count; i++ )
merged.Add( i );
// Rightmost hole first, which is the conventional order and keeps the result deterministic
// rather than depending on the order the profile finder happened to discover loops in.
var built = points;
holeRings.Sort( ( a, b ) => b.Max( i => built[i].x ).CompareTo( a.Max( i => built[i].x ) ) );
foreach ( var hole in holeRings )
{
if ( !Bridge( points, merged, hole ) )
return false;
}
return true;
}
/// <summary>
/// Splice one hole into the working boundary along the shortest valid bridge.
///
/// Valid means three things, and all three are needed: the segment crosses no edge of the
/// boundary as it currently stands (including holes merged before this one), it crosses no edge
/// of the hole itself, and its midpoint is inside the material. The last is what rules out a
/// bridge that runs cleanly around the outside of everything and crosses nothing at all.
/// </summary>
static bool Bridge( List<Vec2> points, List<int> boundary, List<int> hole )
{
var bestOuter = -1;
var bestHole = -1;
var bestLength = float.MaxValue;
for ( var i = 0; i < boundary.Count; i++ )
{
for ( var j = 0; j < hole.Count; j++ )
{
var a = points[boundary[i]];
var b = points[hole[j]];
var length = (b - a).LengthSquared;
if ( length >= bestLength || length < 1e-12f )
continue;
if ( !BridgeIsClear( points, boundary, hole, boundary[i], hole[j] ) )
continue;
bestLength = length;
bestOuter = i;
bestHole = j;
}
}
if ( bestOuter < 0 )
return false;
// ...outer[i], hole[j], hole[j+1] ... all the way round ... hole[j], outer[i], outer[i+1]...
// The two repeated indices are the bridge, walked out and back.
var spliced = new List<int>( boundary.Count + hole.Count + 2 );
for ( var i = 0; i <= bestOuter; i++ )
spliced.Add( boundary[i] );
for ( var j = 0; j < hole.Count; j++ )
spliced.Add( hole[(bestHole + j) % hole.Count] );
spliced.Add( hole[bestHole] );
spliced.Add( boundary[bestOuter] );
for ( var i = bestOuter + 1; i < boundary.Count; i++ )
spliced.Add( boundary[i] );
boundary.Clear();
boundary.AddRange( spliced );
return true;
}
static bool BridgeIsClear( List<Vec2> points, List<int> boundary, List<int> hole, int from, int to )
{
var a = points[from];
var b = points[to];
if ( Crosses( points, boundary, a, b, from, to ) || Crosses( points, hole, a, b, from, to ) )
return false;
// Inside the material: within the boundary, and not down the hole it is bridging to.
var mid = (a + b) * 0.5f;
return Contains( points, boundary, mid ) && !Contains( points, hole, mid );
}
/// <summary>Whether a segment properly crosses any edge of a ring. Edges touching either end of
/// the segment are skipped by INDEX — a bridge necessarily meets the two edges at each of its
/// own endpoints, and that is not a crossing.</summary>
static bool Crosses( List<Vec2> points, List<int> ring, Vec2 a, Vec2 b, int from, int to )
{
for ( var i = 0; i < ring.Count; i++ )
{
var p = ring[i];
var q = ring[(i + 1) % ring.Count];
if ( p == from || q == from || p == to || q == to )
continue;
if ( SegmentsCross( a, b, points[p], points[q] ) )
return true;
}
return false;
}
/// <summary>Strict crossing: the two segments meet at a point interior to both. Touching at an
/// endpoint does not count, which is what makes this usable on a ring where consecutive edges
/// share a vertex.</summary>
static bool SegmentsCross( Vec2 a, Vec2 b, Vec2 c, Vec2 d )
{
var d1 = Vec2.Cross( b - a, c - a );
var d2 = Vec2.Cross( b - a, d - a );
var d3 = Vec2.Cross( d - c, a - c );
var d4 = Vec2.Cross( d - c, b - c );
return ((d1 > 0f && d2 < 0f) || (d1 < 0f && d2 > 0f))
&& ((d3 > 0f && d4 < 0f) || (d3 < 0f && d4 > 0f));
}
/// <summary>Even-odd point-in-ring test.</summary>
static bool Contains( List<Vec2> points, List<int> ring, Vec2 p )
{
var inside = false;
for ( int i = 0, j = ring.Count - 1; i < ring.Count; j = i++ )
{
var a = points[ring[i]];
var b = points[ring[j]];
if ( a.y > p.y != b.y > p.y
&& p.x < (b.x - a.x) * (p.y - a.y) / (b.y - a.y) + a.x )
{
inside = !inside;
}
}
return inside;
}
/// <summary>
/// Triangulate a face given as 3D positions, by flattening it onto its own best-fit plane
/// first. Newell's method rather than a cross product of the first three points: three
/// consecutive corners of a real face are often nearly collinear, and their cross product is
/// then numerical noise pointing anywhere.
/// </summary>
public static List<(int A, int B, int C)> Face( IReadOnlyList<Vec3> positions )
{
if ( positions.Count < 3 )
return new List<(int, int, int)>();
return Polygon( Flatten( positions ) );
}
/// <summary>
/// Drop a planar 3D loop onto its own plane, keeping the order and the winding.
///
/// Shared by Face and BridgedFace rather than written twice: the two differ only in which
/// clipper they hand the result to, and a second copy of this projection would be a second
/// place for the seed-axis choice below to drift.
/// </summary>
static List<Vec2> Flatten( IReadOnlyList<Vec3> positions )
{
var normal = NewellNormal( positions );
if ( normal.LengthSquared < 1e-20f )
normal = new Vec3( 0, 0, 1 );
normal = normal.Normal;
// Any two axes perpendicular to the normal will do; pick the one furthest from it to seed
// them so the cross product is well conditioned.
var seed = MathF.Abs( normal.z ) < 0.9f ? new Vec3( 0, 0, 1 ) : new Vec3( 1, 0, 0 );
var u = Vec3.Cross( seed, normal ).Normal;
var v = Vec3.Cross( normal, u );
var flat = new List<Vec2>( positions.Count );
foreach ( var p in positions )
flat.Add( new Vec2( Vec3.Dot( p, u ), Vec3.Dot( p, v ) ) );
return flat;
}
static Vec3 NewellNormal( IReadOnlyList<Vec3> points )
{
var n = new Vec3( 0, 0, 0 );
for ( var i = 0; i < points.Count; i++ )
{
var a = points[i];
var b = points[(i + 1) % points.Count];
n = new Vec3(
n.x + (a.y - b.y) * (a.z + b.z),
n.y + (a.z - b.z) * (a.x + b.x),
n.z + (a.x - b.x) * (a.y + b.y) );
}
return n;
}
static float SignedArea( IReadOnlyList<Vec2> points )
{
var sum = 0f;
for ( var i = 0; i < points.Count; i++ )
{
var a = points[i];
var b = points[(i + 1) % points.Count];
sum += a.x * b.y - b.x * a.y;
}
return sum * 0.5f;
}
/// <summary>A corner is an ear when it turns the same way the polygon does and no other
/// remaining corner is inside the triangle it would cut off. The second half is the part that
/// makes this work on concave polygons - without it, ear clipping is just a fan.</summary>
static bool IsEar( IReadOnlyList<Vec2> points, List<int> ring, int prev, int cur, int next )
{
var a = points[prev];
var b = points[cur];
var c = points[next];
var cross = Vec2.Cross( b - a, c - b );
// Reflex, or a zero-area sliver: not an ear.
if ( cross <= 1e-12f )
return false;
foreach ( var index in ring )
{
if ( index == prev || index == cur || index == next )
continue;
if ( InsideTriangle( points[index], a, b, c ) )
return false;
}
return true;
}
static bool InsideTriangle( Vec2 p, Vec2 a, Vec2 b, Vec2 c )
{
// All three edge tests agreeing in sign means inside. Points exactly ON an edge count as
// inside: clipping an ear through one would leave a T-junction in the surface.
var ab = Vec2.Cross( b - a, p - a );
var bc = Vec2.Cross( c - b, p - b );
var ca = Vec2.Cross( a - c, p - c );
return ab >= 0f && bc >= 0f && ca >= 0f;
}
}