mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 11:25:44 -05:00
perf(fill): cut a cold Lattice fill from ~300 s to ~160 s with identical layouts
The first fill of a new drawing was blamed on the best-fit pair search, but on the reported nest that search takes ~3.4 s. The time goes to FillLinear.FillGrid, run thousands of times by the Pairs, Row/Column and Linear strategies: pattern copy-distance slides and the grid overlap check. Every change keeps the output bit for bit (part poses compared on the benchmark nest; best-fit results unchanged). - ConvexDecomposition.Triangulate: unlink clipped vertices from a circular list instead of List.RemoveAt, and remember why a vertex failed the ear test (same neighbors, same blocking vertex still present) instead of retesting it after every ear. Compared bitwise against a frozen pre-change copy, including the blocker-clipped case. - Collision: skip triangles whose box misses the other triangulation's union box (exact: min/max and monotone subtraction), reuse each triangle's open vertices, and let the overlap-only path stop at its first region when there are no holes. Check still returns every region. - SpatialQuery.ExtractEntityVertices presizes its set; FillLinear extracts each entity list's vertices once per copy-distance search rather than once per pair. - StripeFiller converges its stripe angles and tiles its grids in parallel. Dedup claims, cached remnant fills, ranking and progress stay in candidate order, and grids are tiled only for configurations a claim can still win (GridDedup.Contains), so results and reports do not depend on the thread count. - LinearFillStrategy fills its angles in parallel through FillHelpers.BestOverAnglesInParallel; angle results, ranking and progress stay in order. Benchmark (one drawing, 60x120, 0.25 spacing, cleared plate, interleaved runs): FillItem Lattice 291/317 s -> 164/153 s, 132 parts both; PairFiller alone 48 s -> 38-42 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
331234d6f3
commit
bcbcf1e611
10 files changed
+816
-109
No files matched your search
@@ -58,7 +58,7 @@ namespace OpenNest.Geometry
|
||||
// Clipping decides the verdict, including containment (one polygon entirely
|
||||
// inside another has zero edge crossings). Crossing points never affect it,
|
||||
// so this overlap-only path skips them.
|
||||
return OverlapRegions(a, b, holesA, holesB).Count > 0;
|
||||
return OverlapRegions(a, b, holesA, holesB, verdictOnly: true).Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -79,7 +79,7 @@ namespace OpenNest.Geometry
|
||||
if (!BoundingBoxesOverlap(a.BoundingBox, b.BoundingBox))
|
||||
return false;
|
||||
|
||||
return OverlapRegions(trianglesA(), trianglesB(), holesA, holesB).Count > 0;
|
||||
return OverlapRegions(trianglesA(), trianglesB(), holesA, holesB, verdictOnly: true).Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -138,30 +138,26 @@ namespace OpenNest.Geometry
|
||||
Polygon a,
|
||||
Polygon b,
|
||||
List<Polygon> holesA,
|
||||
List<Polygon> holesB
|
||||
) => OverlapRegions(TriangulateWithBounds(a), TriangulateWithBounds(b), holesA, holesB);
|
||||
List<Polygon> holesB,
|
||||
bool verdictOnly = false
|
||||
) => OverlapRegions(TriangulateWithBounds(a), TriangulateWithBounds(b), holesA, holesB, verdictOnly);
|
||||
|
||||
private static List<Polygon> OverlapRegions(
|
||||
List<Polygon> trisA,
|
||||
List<Polygon> trisB,
|
||||
List<Polygon> holesA,
|
||||
List<Polygon> holesB
|
||||
List<Polygon> holesB,
|
||||
bool verdictOnly = false
|
||||
)
|
||||
{
|
||||
var regions = new List<Polygon>();
|
||||
|
||||
foreach (var triA in trisA)
|
||||
{
|
||||
foreach (var triB in trisB)
|
||||
{
|
||||
if (!BoundingBoxesOverlap(triA.BoundingBox, triB.BoundingBox))
|
||||
continue;
|
||||
|
||||
var clipped = ClipConvex(triA, triB);
|
||||
if (clipped != null)
|
||||
regions.Add(clipped);
|
||||
}
|
||||
}
|
||||
// Without holes nothing is subtracted, so for a caller that needs only the verdict
|
||||
// the first clipped region decides it. Check returns the regions, so it gets all.
|
||||
var stopAtFirst = verdictOnly
|
||||
&& (holesA == null || holesA.Count == 0)
|
||||
&& (holesB == null || holesB.Count == 0);
|
||||
ClipTrianglePairs(trisA, trisB, regions, stopAtFirst);
|
||||
|
||||
if (regions.Count > 0)
|
||||
regions = SubtractHoles(regions, holesA, holesB);
|
||||
@@ -169,6 +165,106 @@ namespace OpenNest.Geometry
|
||||
return regions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clips every triangle pair whose bounding boxes overlap, in (A, B) order, adding the
|
||||
/// positive-area regions. A triangle whose box misses the union of the other list's
|
||||
/// boxes overlaps none of them (min/max are exact and subtraction is monotone, so a
|
||||
/// pair's overlap never exceeds the overlap with the union), so it is dropped before
|
||||
/// the pair loop. The pairs clipped, their order and the regions are those of the
|
||||
/// plain double loop.
|
||||
/// </summary>
|
||||
private static void ClipTrianglePairs(
|
||||
List<Polygon> trisA,
|
||||
List<Polygon> trisB,
|
||||
List<Polygon> regions,
|
||||
bool stopAtFirst
|
||||
)
|
||||
{
|
||||
if (trisA.Count == 0 || trisB.Count == 0)
|
||||
return;
|
||||
|
||||
var candidatesA = TrianglesTouching(trisA, UnionBounds(trisB));
|
||||
if (candidatesA.Count == 0)
|
||||
return;
|
||||
|
||||
var candidatesB = TrianglesTouching(trisB, UnionBounds(trisA));
|
||||
if (candidatesB.Count == 0)
|
||||
return;
|
||||
|
||||
// The clip loop only reads its inputs, so each triangle's open vertices serve
|
||||
// every pair it is in.
|
||||
var openB = new List<Vector>[candidatesB.Count];
|
||||
for (var j = 0; j < candidatesB.Count; j++)
|
||||
openB[j] = OpenVertices(candidatesB[j]);
|
||||
|
||||
foreach (var triA in candidatesA)
|
||||
{
|
||||
List<Vector> openA = null;
|
||||
|
||||
for (var j = 0; j < candidatesB.Count; j++)
|
||||
{
|
||||
var triB = candidatesB[j];
|
||||
if (!BoundingBoxesOverlap(triA.BoundingBox, triB.BoundingBox))
|
||||
continue;
|
||||
|
||||
openA ??= OpenVertices(triA);
|
||||
var clipped = ClipConvex(openA, openB[j]);
|
||||
if (clipped == null)
|
||||
continue;
|
||||
|
||||
regions.Add(clipped);
|
||||
if (stopAtFirst)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static (double Left, double Right, double Bottom, double Top) UnionBounds(
|
||||
List<Polygon> polygons
|
||||
)
|
||||
{
|
||||
var left = double.PositiveInfinity;
|
||||
var right = double.NegativeInfinity;
|
||||
var bottom = double.PositiveInfinity;
|
||||
var top = double.NegativeInfinity;
|
||||
|
||||
foreach (var polygon in polygons)
|
||||
{
|
||||
var box = polygon.BoundingBox;
|
||||
left = System.Math.Min(left, box.Left);
|
||||
right = System.Math.Max(right, box.Right);
|
||||
bottom = System.Math.Min(bottom, box.Bottom);
|
||||
top = System.Math.Max(top, box.Top);
|
||||
}
|
||||
|
||||
return (left, right, bottom, top);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The polygons whose boxes pass <see cref="BoundingBoxesOverlap"/> against
|
||||
/// <paramref name="union"/>, in order. Same arithmetic as that test, with the union's
|
||||
/// sides in place of the other box's.
|
||||
/// </summary>
|
||||
private static List<Polygon> TrianglesTouching(
|
||||
List<Polygon> polygons,
|
||||
(double Left, double Right, double Bottom, double Top) union
|
||||
)
|
||||
{
|
||||
var result = new List<Polygon>(polygons.Count);
|
||||
|
||||
foreach (var polygon in polygons)
|
||||
{
|
||||
var box = polygon.BoundingBox;
|
||||
var overlapX = System.Math.Min(box.Right, union.Right) - System.Math.Max(box.Left, union.Left);
|
||||
var overlapY = System.Math.Min(box.Top, union.Top) - System.Math.Max(box.Bottom, union.Bottom);
|
||||
|
||||
if (overlapX > Tolerance.Epsilon && overlapY > Tolerance.Epsilon)
|
||||
result.Add(polygon);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool BoundingBoxesOverlap(Box a, Box b)
|
||||
{
|
||||
var overlapX = System.Math.Min(a.Right, b.Right) - System.Math.Max(a.Left, b.Left);
|
||||
@@ -232,10 +328,9 @@ namespace OpenNest.Geometry
|
||||
/// Sutherland-Hodgman polygon clipping. Clips subject against each edge
|
||||
/// of clip. Both must be convex. Returns null if no overlap.
|
||||
/// </summary>
|
||||
private static Polygon ClipConvex(Polygon subject, Polygon clip)
|
||||
private static Polygon ClipConvex(List<Vector> subjectVerts, List<Vector> clipVerts)
|
||||
{
|
||||
var output = OpenVertices(subject);
|
||||
var clipVerts = OpenVertices(clip);
|
||||
var output = subjectVerts;
|
||||
for (var i = 0; i < clipVerts.Count && output.Count >= 3; i++)
|
||||
{
|
||||
output = ClipHalfSpace(output, clipVerts[i], clipVerts[(i + 1) % clipVerts.Count], true);
|
||||
|
||||
@@ -33,32 +33,57 @@ namespace OpenNest.Geometry
|
||||
if (SignedArea(verts) < 0)
|
||||
verts.Reverse();
|
||||
|
||||
// Build a linked list of vertex indices.
|
||||
var indices = new List<int>(verts.Count);
|
||||
// The remaining vertices, in their original order, as a circular linked list: head is
|
||||
// the first remaining vertex (scan position 0), and cutting an ear unlinks one vertex
|
||||
// instead of shifting every later index down.
|
||||
var count = verts.Count;
|
||||
var nextOf = new int[count];
|
||||
var prevOf = new int[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
nextOf[i] = (i + 1) % count;
|
||||
prevOf[i] = (i + count - 1) % count;
|
||||
}
|
||||
|
||||
for (var i = 0; i < verts.Count; i++)
|
||||
indices.Add(i);
|
||||
var head = 0;
|
||||
var n = count;
|
||||
|
||||
var n = indices.Count;
|
||||
// The scan restarts at the first remaining vertex after every ear. A vertex that
|
||||
// failed the ear test keeps failing it while its two neighbors are unchanged and
|
||||
// the vertex found inside its triangle (or none, when it was not convex) remains,
|
||||
// since the test depends on nothing else. Remembering why it failed skips repeating
|
||||
// the O(n) test; the vertices visited, their verdicts and the ears cut are the same.
|
||||
var removed = new bool[count];
|
||||
var failedPrev = new int[count];
|
||||
var failedNext = new int[count];
|
||||
var failedBlocker = new int[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
failedPrev[i] = -1;
|
||||
|
||||
// Safety counter to avoid infinite loop on degenerate polygons.
|
||||
var maxIterations = n * n;
|
||||
var iterations = 0;
|
||||
var i0 = 0;
|
||||
var cursor = head;
|
||||
|
||||
while (n > 2 && iterations < maxIterations)
|
||||
{
|
||||
iterations++;
|
||||
|
||||
var prevIdx = (i0 + n - 1) % n;
|
||||
var currIdx = i0 % n;
|
||||
var nextIdx = (i0 + 1) % n;
|
||||
var currVertex = cursor;
|
||||
var prevVertex = prevOf[currVertex];
|
||||
var nextVertex = nextOf[currVertex];
|
||||
|
||||
var prev = verts[indices[prevIdx]];
|
||||
var curr = verts[indices[currIdx]];
|
||||
var next = verts[indices[nextIdx]];
|
||||
var knownNotEar =
|
||||
failedPrev[currVertex] == prevVertex
|
||||
&& failedNext[currVertex] == nextVertex
|
||||
&& (failedBlocker[currVertex] < 0 || !removed[failedBlocker[currVertex]]);
|
||||
|
||||
if (IsEar(prev, curr, next, verts, indices, n))
|
||||
var prev = verts[prevVertex];
|
||||
var curr = verts[currVertex];
|
||||
var next = verts[nextVertex];
|
||||
|
||||
var blocker = failedBlocker[currVertex];
|
||||
if (!knownNotEar && IsEar(prev, curr, next, verts, nextOf, head, n, out blocker))
|
||||
{
|
||||
var tri = new Polygon();
|
||||
tri.Vertices.Add(prev);
|
||||
@@ -67,16 +92,25 @@ namespace OpenNest.Geometry
|
||||
tri.Close();
|
||||
triangles.Add(tri);
|
||||
|
||||
indices.RemoveAt(currIdx);
|
||||
nextOf[prevVertex] = nextVertex;
|
||||
prevOf[nextVertex] = prevVertex;
|
||||
removed[currVertex] = true;
|
||||
if (head == currVertex)
|
||||
head = nextVertex;
|
||||
n--;
|
||||
i0 = 0;
|
||||
cursor = head;
|
||||
}
|
||||
else
|
||||
{
|
||||
i0++;
|
||||
if (!knownNotEar)
|
||||
{
|
||||
failedPrev[currVertex] = prevVertex;
|
||||
failedNext[currVertex] = nextVertex;
|
||||
failedBlocker[currVertex] = blocker;
|
||||
}
|
||||
|
||||
if (i0 >= n)
|
||||
i0 = 0;
|
||||
// The last remaining vertex links back to head, as position n wraps to 0.
|
||||
cursor = nextVertex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,25 +119,31 @@ namespace OpenNest.Geometry
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the vertex at curr forms an ear (a convex vertex whose
|
||||
/// triangle contains no other polygon vertices).
|
||||
/// triangle contains no other polygon vertices). A vertex that fails reports the
|
||||
/// first remaining vertex found inside its triangle, or -1 when it is not convex.
|
||||
/// </summary>
|
||||
private static bool IsEar(
|
||||
Vector prev,
|
||||
Vector curr,
|
||||
Vector next,
|
||||
List<Vector> verts,
|
||||
List<int> indices,
|
||||
int n
|
||||
int[] nextOf,
|
||||
int head,
|
||||
int n,
|
||||
out int blocker
|
||||
)
|
||||
{
|
||||
// The vertex found inside the triangle, or -1 when the vertex is not convex.
|
||||
blocker = -1;
|
||||
|
||||
// Must be convex (CCW turn).
|
||||
if (Cross(prev, curr, next) <= 0)
|
||||
return false;
|
||||
|
||||
// Check that no other vertex lies inside the triangle.
|
||||
for (var i = 0; i < n; i++)
|
||||
for (int i = 0, index = head; i < n; i++, index = nextOf[index])
|
||||
{
|
||||
var v = verts[indices[i]];
|
||||
var v = verts[index];
|
||||
|
||||
if (v.X == prev.X && v.Y == prev.Y)
|
||||
continue;
|
||||
@@ -113,7 +153,10 @@ namespace OpenNest.Geometry
|
||||
continue;
|
||||
|
||||
if (PointInTriangle(v, prev, curr, next))
|
||||
{
|
||||
blocker = index;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -743,16 +743,40 @@ namespace OpenNest.Geometry
|
||||
Vector direction,
|
||||
SlideContactClassifier contacts
|
||||
)
|
||||
{
|
||||
return DirectionalDistance(
|
||||
movingEntities,
|
||||
ExtractEntityVertices(movingEntities),
|
||||
stationaryEntities,
|
||||
ExtractEntityVertices(stationaryEntities),
|
||||
direction,
|
||||
contacts
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DirectionalDistance(List{Entity}, List{Entity}, Vector, SlideContactClassifier)"/>
|
||||
/// with each list's <see cref="ExtractEntityVertices"/> result supplied by a caller that
|
||||
/// slides one list against several others. The vertices must come from these exact lists.
|
||||
/// </summary>
|
||||
public static double DirectionalDistance(
|
||||
List<Entity> movingEntities,
|
||||
Vector[] movingVertices,
|
||||
List<Entity> stationaryEntities,
|
||||
Vector[] stationaryVertices,
|
||||
Vector direction,
|
||||
SlideContactClassifier contacts
|
||||
)
|
||||
{
|
||||
// Phases: vertex rays both ways, arc-to-line closest points (vertex sampling
|
||||
// misses interior arc contact), then native curve tangency.
|
||||
var source = new EntitySlideEvents(
|
||||
movingEntities,
|
||||
ExtractEntityVertices(movingEntities),
|
||||
movingVertices,
|
||||
0,
|
||||
0,
|
||||
stationaryEntities,
|
||||
ExtractEntityVertices(stationaryEntities),
|
||||
stationaryVertices,
|
||||
direction.X,
|
||||
direction.Y,
|
||||
arcToLine: true
|
||||
@@ -762,7 +786,9 @@ namespace OpenNest.Geometry
|
||||
|
||||
public static Vector[] ExtractEntityVertices(List<Entity> entities)
|
||||
{
|
||||
var vertices = new HashSet<Vector>();
|
||||
// Presized: a set without removals enumerates in insertion order whatever its
|
||||
// capacity, so this only saves the rehashes of growing from empty.
|
||||
var vertices = new HashSet<Vector>(entities.Count * 2);
|
||||
|
||||
for (var i = 0; i < entities.Count; i++)
|
||||
{
|
||||
|
||||
@@ -148,6 +148,9 @@ namespace OpenNest.Engine.Fill
|
||||
var movingBoxes = new Box[parts.Count];
|
||||
var stationaryEntities = new List<Entity>[parts.Count];
|
||||
var movingEntities = new List<Entity>[parts.Count];
|
||||
// Each list slides against several others; extract its vertices once.
|
||||
var stationaryVertices = new Vector[parts.Count][];
|
||||
var movingVertices = new Vector[parts.Count][];
|
||||
|
||||
for (var i = 0; i < parts.Count; i++)
|
||||
{
|
||||
@@ -174,13 +177,25 @@ namespace OpenNest.Engine.Fill
|
||||
if (!SpatialQuery.PerpendicularOverlap(movingBox, stationaryBox, dirVec))
|
||||
continue;
|
||||
|
||||
stationaryEntities[i] ??= cache.AtLocation(parts[i].Program, parts[i].Location);
|
||||
movingEntities[j] ??= cache.AtLocation(parts[j].Program, parts[j].Location + offset);
|
||||
if (stationaryEntities[i] == null)
|
||||
{
|
||||
stationaryEntities[i] = cache.AtLocation(parts[i].Program, parts[i].Location);
|
||||
stationaryVertices[i] = SpatialQuery.ExtractEntityVertices(stationaryEntities[i]);
|
||||
}
|
||||
|
||||
if (movingEntities[j] == null)
|
||||
{
|
||||
movingEntities[j] = cache.AtLocation(parts[j].Program, parts[j].Location + offset);
|
||||
movingVertices[j] = SpatialQuery.ExtractEntityVertices(movingEntities[j]);
|
||||
}
|
||||
|
||||
var slideDistance = SpatialQuery.DirectionalDistance(
|
||||
movingEntities[j],
|
||||
movingVertices[j],
|
||||
stationaryEntities[i],
|
||||
pushDir
|
||||
stationaryVertices[i],
|
||||
dirVec,
|
||||
new SlideContactClassifier(movingEntities[j], stationaryEntities[i])
|
||||
);
|
||||
|
||||
if (slideDistance >= double.MaxValue || slideDistance < 0)
|
||||
|
||||
@@ -24,6 +24,13 @@ public class GridDedup
|
||||
return _seen.TryAdd(key, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if <see cref="TryAdd"/> already accepted this configuration. Claims are never
|
||||
/// removed, so a configuration seen now stays rejected.
|
||||
/// </summary>
|
||||
public bool Contains(Box patternBox, Box workArea, NestDirection dir) =>
|
||||
_seen.ContainsKey(new GridKey(patternBox, workArea, dir));
|
||||
|
||||
public int Count => _seen.Count;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using OpenNest.Engine;
|
||||
using OpenNest.Engine.BestFit;
|
||||
using OpenNest.Engine.Strategies;
|
||||
@@ -41,58 +44,36 @@ public class StripeFiller
|
||||
return new List<Part>();
|
||||
|
||||
var workArea = _context.WorkArea;
|
||||
var spacing = _context.Plate.PartSpacing;
|
||||
var drawing = _context.Item.Drawing;
|
||||
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
||||
|
||||
var layouts = ComputeLayouts(bestFits, drawing);
|
||||
|
||||
List<Part> bestParts = null;
|
||||
|
||||
for (var i = 0; i < bestFits.Count; i++)
|
||||
{
|
||||
_context.Token.ThrowIfCancellationRequested();
|
||||
|
||||
var candidate = bestFits[i];
|
||||
var pairParts = candidate.BuildParts(drawing);
|
||||
|
||||
foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
||||
for (var a = 0; a < Axes.Length; a++)
|
||||
{
|
||||
var perpAxis =
|
||||
axis == NestDirection.Horizontal
|
||||
? NestDirection.Vertical
|
||||
: NestDirection.Horizontal;
|
||||
var sheetSpan = GetDimension(workArea, axis);
|
||||
var axis = Axes[a];
|
||||
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
||||
|
||||
var expandResult = ConvergeStripeAngle(
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token,
|
||||
_context.MaxQuantity
|
||||
);
|
||||
var shrinkResult = ConvergeStripeAngleShrink(
|
||||
pairParts,
|
||||
sheetSpan,
|
||||
spacing,
|
||||
axis,
|
||||
_context.Token,
|
||||
_context.MaxQuantity
|
||||
);
|
||||
|
||||
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
||||
for (var v = 0; v < VariantsPerAxis; v++)
|
||||
{
|
||||
if (count <= 0)
|
||||
var layout = layouts[LayoutIndex(i, a, v)];
|
||||
if (layout.Count <= 0)
|
||||
continue;
|
||||
|
||||
var result = BuildGrid(pairParts, angle, axis, perpAxis);
|
||||
var result = ClaimGrid(layout, axis);
|
||||
|
||||
if (result == null || result.Count == 0)
|
||||
continue;
|
||||
|
||||
Debug.WriteLine(
|
||||
$"[StripeFiller] {strategyName} candidate {i} {dirLabel}: "
|
||||
+ $"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, "
|
||||
+ $"angle={Angle.ToDegrees(layout.Angle):F1}°, N={layout.Count}, waste={layout.Waste:F2}, "
|
||||
+ $"grid={result.Count} parts"
|
||||
);
|
||||
|
||||
@@ -112,23 +93,149 @@ public class StripeFiller
|
||||
return bestParts ?? new List<Part>();
|
||||
}
|
||||
|
||||
private List<Part> BuildGrid(
|
||||
List<Part> pairParts,
|
||||
double angle,
|
||||
NestDirection primaryAxis,
|
||||
NestDirection perpAxis
|
||||
)
|
||||
private static readonly NestDirection[] Axes = { NestDirection.Horizontal, NestDirection.Vertical };
|
||||
|
||||
/// <summary>Expanding (<see cref="ConvergeStripeAngle"/>) then shrinking convergence.</summary>
|
||||
private const int VariantsPerAxis = 2;
|
||||
|
||||
private static int LayoutIndex(int candidate, int axis, int variant) =>
|
||||
(candidate * Axes.Length + axis) * VariantsPerAxis + variant;
|
||||
|
||||
/// <summary>
|
||||
/// A converged stripe angle and, when it holds a pattern, the rotated pattern and the grid
|
||||
/// it tiles before the order-dependent steps (<see cref="GridDedup"/> claim, cached remnant
|
||||
/// fill).
|
||||
/// </summary>
|
||||
private sealed class StripeLayout
|
||||
{
|
||||
public double Angle;
|
||||
public double Waste;
|
||||
public int Count;
|
||||
public Pattern Pattern;
|
||||
public bool GridReady;
|
||||
public List<Part> Grid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converges every candidate's stripe angle per axis and variant, then tiles the grids that
|
||||
/// <see cref="Fill"/> will claim, each step in parallel. Every layout is a pure function of
|
||||
/// its candidate, axis and variant (fresh part clones, per-call <see cref="FillLinear"/>
|
||||
/// state), so the results do not depend on the thread count. A grid is tiled only when its
|
||||
/// configuration is not yet claimed and no earlier layout shares it: claims are only ever
|
||||
/// added, so exactly those are the grids the sequential loop would have tiled.
|
||||
/// <see cref="Fill"/> then claims, fills remnants and ranks in the original order.
|
||||
/// </summary>
|
||||
private StripeLayout[] ComputeLayouts(List<BestFitResult> bestFits, Drawing drawing)
|
||||
{
|
||||
var layouts = new StripeLayout[bestFits.Count * Axes.Length * VariantsPerAxis];
|
||||
|
||||
RunParallel(layouts.Length, index =>
|
||||
{
|
||||
var variant = index % VariantsPerAxis;
|
||||
var axis = Axes[index / VariantsPerAxis % Axes.Length];
|
||||
var candidate = bestFits[index / VariantsPerAxis / Axes.Length];
|
||||
layouts[index] = Converge(candidate.BuildParts(drawing), axis, variant);
|
||||
});
|
||||
|
||||
var batch = new GridDedup();
|
||||
var toTile = new List<int>();
|
||||
for (var index = 0; index < layouts.Length; index++)
|
||||
{
|
||||
var layout = layouts[index];
|
||||
if (layout.Count <= 0)
|
||||
continue;
|
||||
|
||||
var axis = Axes[index / VariantsPerAxis % Axes.Length];
|
||||
var box = layout.Pattern.BoundingBox;
|
||||
if (!_dedup.Contains(box, _context.WorkArea, axis) && batch.TryAdd(box, _context.WorkArea, axis))
|
||||
toTile.Add(index);
|
||||
}
|
||||
|
||||
RunParallel(toTile.Count, k =>
|
||||
{
|
||||
var index = toTile[k];
|
||||
var layout = layouts[index];
|
||||
layout.Grid = BuildGrid(layout.Pattern, Axes[index / VariantsPerAxis % Axes.Length]);
|
||||
layout.GridReady = true;
|
||||
});
|
||||
|
||||
return layouts;
|
||||
}
|
||||
|
||||
private void RunParallel(int count, Action<int> body)
|
||||
{
|
||||
try
|
||||
{
|
||||
Parallel.For(0, count, new ParallelOptions { CancellationToken = _context.Token }, body);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
// Surface the failure itself, cancellation included, as the sequential loop did.
|
||||
ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private StripeLayout Converge(List<Part> pairParts, NestDirection axis, int variant)
|
||||
{
|
||||
var spacing = _context.Plate.PartSpacing;
|
||||
var sheetSpan = GetDimension(_context.WorkArea, axis);
|
||||
|
||||
var (angle, waste, count) = variant == 0
|
||||
? ConvergeStripeAngle(pairParts, sheetSpan, spacing, axis, _context.Token, _context.MaxQuantity)
|
||||
: ConvergeStripeAngleShrink(pairParts, sheetSpan, spacing, axis, _context.Token, _context.MaxQuantity);
|
||||
|
||||
var layout = new StripeLayout { Angle = angle, Waste = waste, Count = count };
|
||||
if (count > 0)
|
||||
layout.Pattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The order-dependent half of a stripe grid: claims its configuration and adds the cached
|
||||
/// remnant fill. Null when the configuration was already evaluated or no grid fits.
|
||||
/// </summary>
|
||||
private List<Part> ClaimGrid(StripeLayout layout, NestDirection primaryAxis)
|
||||
{
|
||||
if (!_dedup.TryAdd(layout.Pattern.BoundingBox, _context.WorkArea, primaryAxis))
|
||||
return null;
|
||||
|
||||
// ComputeLayouts tiled every grid a claim can succeed for; tile here should it not have.
|
||||
var gridParts = layout.GridReady ? layout.Grid : BuildGrid(layout.Pattern, primaryAxis);
|
||||
if (gridParts == null)
|
||||
return null;
|
||||
|
||||
var allParts = new List<Part>(gridParts);
|
||||
|
||||
// A capped fill that already holds the quantity needs no remnant parts.
|
||||
if (_context.MaxQuantity > 0 && gridParts.Count >= _context.MaxQuantity)
|
||||
return allParts;
|
||||
|
||||
var remnantParts = FillRemnant(gridParts, primaryAxis);
|
||||
if (remnantParts != null)
|
||||
{
|
||||
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
||||
allParts.AddRange(remnantParts);
|
||||
}
|
||||
|
||||
return allParts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tiles the rotated pattern into a stripe and the stripe into a grid. Pure: reads only the
|
||||
/// pattern and the context's fixed inputs.
|
||||
/// </summary>
|
||||
private List<Part> BuildGrid(Pattern rotatedPattern, NestDirection primaryAxis)
|
||||
{
|
||||
var perpAxis =
|
||||
primaryAxis == NestDirection.Horizontal ? NestDirection.Vertical : NestDirection.Horizontal;
|
||||
var workArea = _context.WorkArea;
|
||||
var spacing = _context.Plate.PartSpacing;
|
||||
|
||||
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
||||
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
||||
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
||||
|
||||
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
||||
return null;
|
||||
|
||||
var stripeEngine = new FillLinear(stripeBox, spacing)
|
||||
{
|
||||
Label = "Stripe",
|
||||
@@ -187,20 +294,7 @@ public class StripeFiller
|
||||
return null;
|
||||
}
|
||||
|
||||
var allParts = new List<Part>(gridParts);
|
||||
|
||||
// A capped fill that already holds the quantity needs no remnant parts.
|
||||
if (_context.MaxQuantity > 0 && gridParts.Count >= _context.MaxQuantity)
|
||||
return allParts;
|
||||
|
||||
var remnantParts = FillRemnant(gridParts, primaryAxis);
|
||||
if (remnantParts != null)
|
||||
{
|
||||
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
||||
allParts.AddRange(remnantParts);
|
||||
}
|
||||
|
||||
return allParts;
|
||||
return gridParts;
|
||||
}
|
||||
|
||||
private List<BestFitResult> GetPairCandidates()
|
||||
|
||||
@@ -169,6 +169,55 @@ namespace OpenNest.Engine.Strategies
|
||||
return best ?? new List<Part>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="BestOverAngles"/> for a pure per-angle fill: every angle is filled in
|
||||
/// parallel, then <paramref name="recordResult"/>, the ranking and the progress reports run
|
||||
/// once per angle in angle order, so the result does not depend on the thread count.
|
||||
/// <paramref name="fillAtAngle"/> must not touch shared state; side effects of an angle's
|
||||
/// result belong in <paramref name="recordResult"/>.
|
||||
/// </summary>
|
||||
public static List<Part> BestOverAnglesInParallel(
|
||||
FillContext context,
|
||||
IReadOnlyList<double> angles,
|
||||
Func<double, List<Part>> fillAtAngle,
|
||||
Action<double, List<Part>> recordResult,
|
||||
string phaseLabel
|
||||
)
|
||||
{
|
||||
var results = new List<Part>[angles.Count];
|
||||
|
||||
try
|
||||
{
|
||||
Parallel.For(
|
||||
0,
|
||||
angles.Count,
|
||||
new ParallelOptions { CancellationToken = context.Token },
|
||||
i => results[i] = fillAtAngle(angles[i])
|
||||
);
|
||||
}
|
||||
catch (AggregateException ex)
|
||||
{
|
||||
// Surface the failure itself, cancellation included, as the sequential sweep did.
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo
|
||||
.Capture(ex.InnerExceptions[0])
|
||||
.Throw();
|
||||
throw;
|
||||
}
|
||||
|
||||
var next = 0;
|
||||
return BestOverAngles(
|
||||
context,
|
||||
angles,
|
||||
angle =>
|
||||
{
|
||||
var result = results[next++];
|
||||
recordResult(angle, result);
|
||||
return result;
|
||||
},
|
||||
phaseLabel
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if any pair of parts geometrically overlap. Uses bounding box
|
||||
/// pre-filtering for performance, then falls back to shape intersection.
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace OpenNest.Engine.Strategies
|
||||
var comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
||||
var preferred = context.Policy?.PreferredDirection;
|
||||
|
||||
return FillHelpers.BestOverAngles(
|
||||
// Each angle's fill is independent; its angle result is recorded in angle order.
|
||||
return FillHelpers.BestOverAnglesInParallel(
|
||||
context,
|
||||
angles,
|
||||
angle =>
|
||||
@@ -29,13 +30,15 @@ namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
Label = "Linear",
|
||||
};
|
||||
var result = FillHelpers.FillWithDirectionPreference(
|
||||
return FillHelpers.FillWithDirectionPreference(
|
||||
dir => engine.Fill(context.Item.Drawing, angle, dir),
|
||||
preferred,
|
||||
comparer,
|
||||
workArea
|
||||
);
|
||||
|
||||
},
|
||||
(angle, result) =>
|
||||
{
|
||||
if (result != null && result.Count > 0)
|
||||
{
|
||||
context.AngleResults.Add(
|
||||
@@ -47,8 +50,6 @@ namespace OpenNest.Engine.Strategies
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
"Linear"
|
||||
);
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
using OpenNest.Geometry;
|
||||
using static OpenNest.Tests.Geometry.NoFitPolygonTests;
|
||||
|
||||
namespace OpenNest.Tests.Geometry;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ConvexDecomposition.Triangulate"/> skips repeating ear tests whose verdict cannot
|
||||
/// have changed. These tests compare it bit for bit against the frozen pre-change
|
||||
/// <see cref="LegacyConvexDecomposition"/> (commit a9fc9f0a): same triangles, same order.
|
||||
/// </summary>
|
||||
public class ConvexDecompositionEquivalenceTests
|
||||
{
|
||||
[Fact]
|
||||
public void SeededPolygons_MatchLegacyBitwise()
|
||||
{
|
||||
var random = new Random(7112026);
|
||||
var triangles = 0;
|
||||
|
||||
for (var i = 0; i < 3000; i++)
|
||||
{
|
||||
var polygon = MakePolygon(random, i);
|
||||
var expected = LegacyConvexDecomposition.Triangulate(polygon);
|
||||
var actual = ConvexDecomposition.Triangulate(polygon);
|
||||
|
||||
Assert.True(
|
||||
TriangleBits(expected).SequenceEqual(TriangleBits(actual)),
|
||||
$"polygon {i} (kind {i % 8}, {polygon.Vertices.Count} vertices)"
|
||||
);
|
||||
triangles += expected.Count;
|
||||
}
|
||||
|
||||
Assert.True(triangles > 50000);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Small stars on a coarse grid. Here a vertex's triangle often holds a vertex that is cut
|
||||
/// as an ear later while the vertex keeps its neighbors, the case the remembered verdict
|
||||
/// must notice.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SmallGridStars_MatchLegacyBitwise()
|
||||
{
|
||||
var random = new Random(1);
|
||||
|
||||
for (var i = 0; i < 20000; i++)
|
||||
{
|
||||
var count = 5 + random.Next(8);
|
||||
var angles = Enumerable.Range(0, count)
|
||||
.Select(_ => random.NextDouble() * 2 * System.Math.PI)
|
||||
.OrderBy(a => a)
|
||||
.ToArray();
|
||||
var points = angles
|
||||
.Select(a =>
|
||||
{
|
||||
var r = 0.2 + random.NextDouble() * 3;
|
||||
return (System.Math.Round(r * System.Math.Cos(a), 1), System.Math.Round(r * System.Math.Sin(a), 1));
|
||||
})
|
||||
.ToArray();
|
||||
var polygon = Ring(points);
|
||||
|
||||
Assert.True(
|
||||
TriangleBits(LegacyConvexDecomposition.Triangulate(polygon))
|
||||
.SequenceEqual(TriangleBits(ConvexDecomposition.Triangulate(polygon))),
|
||||
$"star {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DegeneratePolygons_MatchLegacyBitwise()
|
||||
{
|
||||
var cases = new List<Polygon>
|
||||
{
|
||||
new Polygon(),
|
||||
Ring((0, 0), (1, 0)),
|
||||
Ring((0, 0), (1, 0), (2, 0)),
|
||||
Ring((0, 0), (1, 0), (2, 0), (3, 0), (1, 1e-12)),
|
||||
// Repeated vertices and a spike back along an edge.
|
||||
Ring((0, 0), (2, 0), (2, 0), (2, 2), (1, 1), (2, 2), (0, 2)),
|
||||
// Self-intersecting bow tie: the scan stops at its safety limit.
|
||||
Ring((0, 0), (2, 2), (2, 0), (0, 2)),
|
||||
// The vertex at (0.1, -0.6) is blocked by (-0.3, -0.3) until that vertex is cut.
|
||||
Ring((1.7, 0.8), (-1.6, -1.3), (-0.3, -0.3), (0.1, -0.6), (1.4, -1.2)),
|
||||
// Clockwise input is reversed first.
|
||||
Ring((0, 0), (0, 3), (1, 3), (1, 1), (3, 1), (3, 0)),
|
||||
};
|
||||
|
||||
foreach (var polygon in cases)
|
||||
{
|
||||
Assert.Equal(
|
||||
TriangleBits(LegacyConvexDecomposition.Triangulate(polygon)),
|
||||
TriangleBits(ConvexDecomposition.Triangulate(polygon))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Triangulate_DoesNotMutateInput()
|
||||
{
|
||||
var polygon = MakePolygon(new Random(3), 1);
|
||||
var before = polygon.Vertices.ToList();
|
||||
|
||||
ConvexDecomposition.Triangulate(polygon);
|
||||
|
||||
Assert.Equal(before, polygon.Vertices);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simple polygons of several families: radial stars with many reflex vertices, combs,
|
||||
/// flattened circles (long runs of nearly collinear vertices), collinear edge points,
|
||||
/// clockwise copies and far-from-origin offsets.
|
||||
/// </summary>
|
||||
private static Polygon MakePolygon(Random random, int index)
|
||||
{
|
||||
var polygon = (index % 8) switch
|
||||
{
|
||||
0 => Star(random),
|
||||
1 => Radial(random, 6 + random.Next(120), 0.2),
|
||||
2 => Radial(random, 6 + random.Next(60), 0.9),
|
||||
3 => Comb(random),
|
||||
4 => Circle(random),
|
||||
5 => WithEdgePoints(random, Radial(random, 5 + random.Next(20), 0.6)),
|
||||
6 => Reverse(Radial(random, 8 + random.Next(80), 0.5)),
|
||||
_ => Square(1 + random.NextDouble()),
|
||||
};
|
||||
|
||||
return index % 3 == 0
|
||||
? Move(polygon, random.NextDouble() * 400 - 200, random.NextDouble() * 400 - 200)
|
||||
: polygon;
|
||||
}
|
||||
|
||||
private static Polygon Radial(Random random, int count, double jitter)
|
||||
{
|
||||
var points = new (double X, double Y)[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var angle = 2 * System.Math.PI * i / count;
|
||||
var radius = 1 + random.NextDouble() * jitter * 3;
|
||||
points[i] = (radius * System.Math.Cos(angle), radius * System.Math.Sin(angle));
|
||||
}
|
||||
|
||||
return Ring(points);
|
||||
}
|
||||
|
||||
private static Polygon Comb(Random random)
|
||||
{
|
||||
var teeth = 2 + random.Next(12);
|
||||
var width = 0.2 + random.NextDouble();
|
||||
var points = new List<(double X, double Y)> { (0, 0), (teeth * width * 2, 0) };
|
||||
for (var t = teeth - 1; t >= 0; t--)
|
||||
{
|
||||
var x = t * width * 2;
|
||||
points.Add((x + width * 2, 1 + random.NextDouble()));
|
||||
points.Add((x + width, 1 + random.NextDouble()));
|
||||
points.Add((x + width, 0.3));
|
||||
points.Add((x, 0.3));
|
||||
}
|
||||
|
||||
return Ring(points.ToArray());
|
||||
}
|
||||
|
||||
private static Polygon Circle(Random random)
|
||||
{
|
||||
var shape = new Shape();
|
||||
var radius = 0.5 + random.NextDouble() * 3;
|
||||
shape.Entities.Add(new Arc(0, 0, radius, 0, System.Math.PI));
|
||||
shape.Entities.Add(new Arc(0, 0, radius, System.Math.PI, 2 * System.Math.PI));
|
||||
return ClipperBridge.Flatten(shape, 0.001 + random.NextDouble() * 0.05, circumscribe: false);
|
||||
}
|
||||
|
||||
private static Polygon WithEdgePoints(Random random, Polygon polygon)
|
||||
{
|
||||
var points = new List<(double X, double Y)>();
|
||||
var vertices = polygon.Vertices;
|
||||
var count = vertices.Count - 1;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var a = vertices[i];
|
||||
var b = vertices[i + 1];
|
||||
points.Add((a.X, a.Y));
|
||||
var extra = random.Next(3);
|
||||
for (var k = 1; k <= extra; k++)
|
||||
{
|
||||
var t = k / (extra + 1.0);
|
||||
points.Add((a.X + (b.X - a.X) * t, a.Y + (b.Y - a.Y) * t));
|
||||
}
|
||||
}
|
||||
|
||||
return Ring(points.ToArray());
|
||||
}
|
||||
|
||||
private static Polygon Reverse(Polygon polygon)
|
||||
{
|
||||
var points = polygon.Vertices.Take(polygon.Vertices.Count - 1).Reverse().Select(v => (v.X, v.Y));
|
||||
return Ring(points.ToArray());
|
||||
}
|
||||
|
||||
private static long[] TriangleBits(List<Polygon> triangles) =>
|
||||
triangles
|
||||
.SelectMany(t =>
|
||||
t.Vertices.SelectMany(v => new[]
|
||||
{
|
||||
BitConverter.DoubleToInt64Bits(v.X),
|
||||
BitConverter.DoubleToInt64Bits(v.Y),
|
||||
})
|
||||
.Append(t.Vertices.Count)
|
||||
)
|
||||
.Append(triangles.Count)
|
||||
.ToArray();
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
#nullable disable
|
||||
// Frozen, test-only copy of OpenNest.Core/Geometry/ConvexDecomposition.cs at commit
|
||||
// a9fc9f0a (before the linked-list ear scan that remembers why a vertex failed).
|
||||
// Only #nullable disable, the namespace, type name and visibility differ from the original.
|
||||
// Do not edit: differential tests use it as the independent pre-change oracle.
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests.Geometry
|
||||
{
|
||||
/// <summary>
|
||||
/// Decomposes concave polygons into convex sub-polygons using ear-clipping
|
||||
/// triangulation. Produces O(n-2) triangles per polygon.
|
||||
/// </summary>
|
||||
internal static class LegacyConvexDecomposition
|
||||
{
|
||||
/// <summary>
|
||||
/// Decomposes a polygon into a list of convex triangles using ear-clipping.
|
||||
/// The input polygon must be simple (non-self-intersecting).
|
||||
/// Returns a list of triangles, each represented as a Polygon with 3 vertices (closed).
|
||||
/// </summary>
|
||||
public static List<Polygon> Triangulate(Polygon polygon)
|
||||
{
|
||||
var triangles = new List<Polygon>();
|
||||
var verts = new List<Vector>(polygon.Vertices);
|
||||
|
||||
// Remove closing vertex if polygon is closed.
|
||||
if (
|
||||
verts.Count > 1
|
||||
&& verts[0].X == verts[verts.Count - 1].X
|
||||
&& verts[0].Y == verts[verts.Count - 1].Y
|
||||
)
|
||||
verts.RemoveAt(verts.Count - 1);
|
||||
|
||||
if (verts.Count < 3)
|
||||
return triangles;
|
||||
|
||||
// Ensure counter-clockwise winding for ear detection.
|
||||
if (SignedArea(verts) < 0)
|
||||
verts.Reverse();
|
||||
|
||||
// Build a linked list of vertex indices.
|
||||
var indices = new List<int>(verts.Count);
|
||||
|
||||
for (var i = 0; i < verts.Count; i++)
|
||||
indices.Add(i);
|
||||
|
||||
var n = indices.Count;
|
||||
|
||||
// Safety counter to avoid infinite loop on degenerate polygons.
|
||||
var maxIterations = n * n;
|
||||
var iterations = 0;
|
||||
var i0 = 0;
|
||||
|
||||
while (n > 2 && iterations < maxIterations)
|
||||
{
|
||||
iterations++;
|
||||
|
||||
var prevIdx = (i0 + n - 1) % n;
|
||||
var currIdx = i0 % n;
|
||||
var nextIdx = (i0 + 1) % n;
|
||||
|
||||
var prev = verts[indices[prevIdx]];
|
||||
var curr = verts[indices[currIdx]];
|
||||
var next = verts[indices[nextIdx]];
|
||||
|
||||
if (IsEar(prev, curr, next, verts, indices, n))
|
||||
{
|
||||
var tri = new Polygon();
|
||||
tri.Vertices.Add(prev);
|
||||
tri.Vertices.Add(curr);
|
||||
tri.Vertices.Add(next);
|
||||
tri.Close();
|
||||
triangles.Add(tri);
|
||||
|
||||
indices.RemoveAt(currIdx);
|
||||
n--;
|
||||
i0 = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
i0++;
|
||||
|
||||
if (i0 >= n)
|
||||
i0 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return triangles;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests whether the vertex at curr forms an ear (a convex vertex whose
|
||||
/// triangle contains no other polygon vertices).
|
||||
/// </summary>
|
||||
private static bool IsEar(
|
||||
Vector prev,
|
||||
Vector curr,
|
||||
Vector next,
|
||||
List<Vector> verts,
|
||||
List<int> indices,
|
||||
int n
|
||||
)
|
||||
{
|
||||
// Must be convex (CCW turn).
|
||||
if (Cross(prev, curr, next) <= 0)
|
||||
return false;
|
||||
|
||||
// Check that no other vertex lies inside the triangle.
|
||||
for (var i = 0; i < n; i++)
|
||||
{
|
||||
var v = verts[indices[i]];
|
||||
|
||||
if (v.X == prev.X && v.Y == prev.Y)
|
||||
continue;
|
||||
if (v.X == curr.X && v.Y == curr.Y)
|
||||
continue;
|
||||
if (v.X == next.X && v.Y == next.Y)
|
||||
continue;
|
||||
|
||||
if (PointInTriangle(v, prev, curr, next))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns positive value if A→B→C is a CCW (left) turn.
|
||||
/// </summary>
|
||||
internal static double Cross(Vector a, Vector b, Vector c)
|
||||
{
|
||||
return (b.X - a.X) * (c.Y - a.Y) - (b.Y - a.Y) * (c.X - a.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if point p is strictly inside triangle (a, b, c).
|
||||
/// Assumes CCW winding.
|
||||
/// </summary>
|
||||
private static bool PointInTriangle(Vector p, Vector a, Vector b, Vector c)
|
||||
{
|
||||
var d1 = Cross(a, b, p);
|
||||
var d2 = Cross(b, c, p);
|
||||
var d3 = Cross(c, a, p);
|
||||
|
||||
var hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0);
|
||||
var hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0);
|
||||
|
||||
return !(hasNeg && hasPos);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Signed area of a polygon. Positive = CCW, negative = CW.
|
||||
/// </summary>
|
||||
private static double SignedArea(List<Vector> verts)
|
||||
{
|
||||
// World-coordinate products can erase the sign of a small polygon's area
|
||||
// far from the origin, leaving CW outlines/holes untriangulated. Measure
|
||||
// relative to a vertex, just as the clipping kernel measures its fragments.
|
||||
var area = 0.0;
|
||||
for (var i = 1; i + 1 < verts.Count; i++)
|
||||
area += Cross(verts[0], verts[i], verts[i + 1]);
|
||||
|
||||
return area * 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user