mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 22:45:47 -05:00
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>
211 lines
7.3 KiB
C#
211 lines
7.3 KiB
C#
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();
|
|
}
|