Files
OpenNest/OpenNest.Tests/Fill/PartOverlapCheckerTests.cs
T
aj 1b23ad79f2 perf(fill): reuse part triangulations within an overlap check
After 1b, triangulating both polygons on every pair was the largest
remaining overlap cost (27% of main-thread samples on the corpus job).
PartOverlapChecker now triangulates each part at most once per check,
lazily after the bounding-box gate, and passes the triangles to a new
internal Collision.HasOverlap overload that runs the unchanged
OverlapRegions body. Triangles are only read by clipping and hole
subtraction, so reuse gives identical verdicts.

Verification:
- 49,000 seeded decisions with reused triangles match LegacyCollision;
  triangles stay bit-identical to a fresh triangulation afterwards.
- Debug PolygonTriangulations: 246 -> 40 and 64 -> 36 per grid check;
  sharing triangles per Program instead fails 23 tests.
- Corpus job (169 parts, --engines Default --parallel 1): median
  13,398 -> 12,702 ms over 4+4 alternating runs vs 1b, identical
  outcomes; serialized layout byte-identical to the base.

Also records the Follow-up B' (Slices 1a, 1b, 2a) measurements in
docs/performance/fill-performance.md.
2026-09-27 13:49:46 -04:00

435 lines
19 KiB
C#

using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Engine.Strategies;
using OpenNest.Geometry;
using OpenNest.Shapes;
using OpenNest.Tests.BestFit;
using Xunit.Abstractions;
namespace OpenNest.Tests.Fill;
/// <summary>
/// PartOverlapChecker, and both in-fill HasOverlappingParts checks built on it, must reproduce
/// the frozen pre-change nested loop exactly: the same verdict and the same first overlapping
/// (a, b) indices. The only change is that each distinct Program is prepared once per check.
/// </summary>
[Collection(nameof(FillCacheCollection))]
public class PartOverlapCheckerTests
{
private static readonly string[] Shapes = { "rectangle", "concave", "arc", "circle", "ring" };
private readonly ITestOutputHelper output;
public PartOverlapCheckerTests(ITestOutputHelper output) => this.output = output;
public static IEnumerable<object[]> GridCases()
{
foreach (var shape in Shapes)
foreach (var spacing in new[] { 0.0, 0.5 })
foreach (var angle in new[] { 0.0, System.Math.PI / 2, 0.37 })
yield return new object[] { shape, spacing, angle };
}
[Theory]
[MemberData(nameof(GridCases))]
public void FillLinearGrids_MatchFrozenLoops(string shape, double spacing, double angle)
{
var area = new Box(3.1, 5.3, 61, 37);
var filler = new FillLinear(area, spacing);
var drawing = Fixture(shape);
foreach (var direction in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{
var grid = filler.Fill(drawing, angle, direction);
Assert.NotEmpty(grid);
AssertMatchesLegacy(grid);
// Nudge copies onto their neighbours so overlapping verdicts are exercised too.
foreach (var shift in new[] { 0.05, 1e-9, -1e-9 })
AssertMatchesLegacy(Shifted(grid, shift));
}
}
[Theory]
[InlineData("shared")]
[InlineData("rotated")]
[InlineData("built-pair")]
public void PatternGrids_MatchFrozenLoops(string kind)
{
foreach (var shape in Shapes)
{
var pattern = MakePattern(kind, Fixture(shape));
var filler = new FillLinear(new Box(3.1, 5.3, 61, 37), 0.5);
foreach (var direction in new[] { NestDirection.Horizontal, NestDirection.Vertical })
{
var grid = filler.Fill(pattern, direction);
AssertMatchesLegacy(grid);
AssertMatchesLegacy(Shifted(grid, 0.05));
}
}
}
[Fact]
public void OverlappingSeedTiling_MatchesFrozenLoops()
{
var first = new Part(Fixture("rectangle"));
var pattern = new Pattern();
pattern.Parts.AddRange(new[] { first, first.CloneAtOffset(new Vector(0.25, 0.25)) });
pattern.UpdateBounds();
var legacy = new LegacyFillLinear(new Box(0, 0, 35, 8.25), 0.5);
var raw = new List<Part>(pattern.Parts);
raw.AddRange((List<Part>)FillExtentsTests.Invoke(legacy, "TilePattern", pattern, NestDirection.Horizontal));
Assert.True(LegacyPartOverlap.FillHelpersHasOverlappingParts(raw));
AssertMatchesLegacy(raw);
}
[Theory]
[InlineData(0.0)]
[InlineData(1e-12)]
[InlineData(1e-9)]
[InlineData(-1e-9)]
[InlineData(1e-7)]
[InlineData(-1e-7)]
public void TouchingAndEpsilonEdgeOffsets_MatchFrozenLoops(double gap)
{
foreach (var shape in Shapes)
{
var drawing = Fixture(shape);
var a = Part.CreateAtOrigin(drawing, 0);
var right = a.CloneAtOffset(new Vector(a.BoundingBox.Length + gap, 0));
var above = a.CloneAtOffset(new Vector(0, a.BoundingBox.Width + gap));
var diagonal = a.CloneAtOffset(new Vector(a.BoundingBox.Length + gap, a.BoundingBox.Width + gap));
AssertMatchesLegacy(new List<Part> { a, right, above, diagonal });
}
}
[Fact]
public void NonMaterialAndEmptyPrograms_MatchFrozenLoops()
{
var square = Part.CreateAtOrigin(Fixture("rectangle"), 0);
var scribeOnly = new Program();
scribeOnly.Codes.Add(new RapidMove(new Vector(0, 0)));
scribeOnly.Codes.Add(new LinearMove(new Vector(4, 0)) { Layer = LayerType.Scribe });
scribeOnly.Codes.Add(new LinearMove(new Vector(4, 4)) { Layer = LayerType.Scribe });
scribeOnly.Codes.Add(new LinearMove(new Vector(0, 0)) { Layer = LayerType.Scribe });
var rapidOnly = new Program();
rapidOnly.Codes.Add(new RapidMove(new Vector(0, 0)));
rapidOnly.Codes.Add(new RapidMove(new Vector(5, 5)));
foreach (var program in new[] { scribeOnly, rapidOnly })
{
var odd = new Part(new Drawing("odd", program));
Assert.Empty(Part.MaterialEntities(odd.Program));
var parts = new List<Part> { square, odd, square.CloneAtOffset(new Vector(2, 2)) };
AssertMatchesLegacy(parts);
Assert.False(new PartOverlapChecker().Overlaps(square, odd));
Assert.False(new PartOverlapChecker().Overlaps(odd, odd));
}
// An empty program has no material entities, so both paths return false before ShapeProfile.
var empty = new Part(new Drawing("empty", new Program()));
Assert.Empty(Part.MaterialEntities(empty.Program));
Assert.Equal(Capture(() => LegacyPartOverlap.Intersects(square, empty, out _)),
Capture(() => new PartOverlapChecker().Overlaps(square, empty)));
Assert.Equal(Capture(() => LegacyPartOverlap.Intersects(empty, square, out _)),
Capture(() => new PartOverlapChecker().Overlaps(empty, square)));
AssertMatchesLegacy(new List<Part> { square, empty, square.CloneAtOffset(new Vector(20, 0)) });
}
[Fact]
public void SharedProgram_PartsTestedAgainstEachOtherAndItself()
{
var a = Part.CreateAtOrigin(Fixture("concave"), 0);
var b = a.CloneAtOffset(new Vector(1, 1));
var c = a.CloneAtOffset(new Vector(30, 0));
Assert.Same(a.Program, b.Program);
var checker = new PartOverlapChecker();
Assert.Equal(LegacyPartOverlap.Intersects(a, b, out _), checker.Overlaps(a, b));
Assert.Equal(LegacyPartOverlap.Intersects(a, c, out _), checker.Overlaps(a, c));
Assert.Equal(LegacyPartOverlap.Intersects(b, a, out _), checker.Overlaps(b, a));
Assert.Equal(LegacyPartOverlap.Intersects(a, a, out _), checker.Overlaps(a, a));
Assert.True(checker.Overlaps(a, b));
Assert.False(checker.Overlaps(a, c));
}
[Fact]
public void WorldPolygons_MatchPreChangeBitwise()
{
foreach (var shape in Shapes)
{
var part = Part.CreateAtOrigin(Fixture(shape), 0.37);
part.Offset(new Vector(11.1, -3.3));
var clone = part.CloneAtOffset(new Vector(7.77, 1.234567));
var checker = new PartOverlapChecker();
// Prepare the shared Program through the clone first, then build the original.
checker.Overlaps(clone, part);
foreach (var p in new[] { part, clone })
{
var expected = LegacyPartOverlap.WorldPolygon(p);
var actual = WorldPolygon(checker, p);
Assert.Equal(PolygonBits(expected), PolygonBits(actual));
// Cached triangles equal a fresh triangulation of the pre-change world polygon.
var expectedTriangles = Collision.Triangulate(expected);
var actualTriangles = WorldTriangles(checker, p);
Assert.Equal(expectedTriangles.Count, actualTriangles.Count);
for (var t = 0; t < expectedTriangles.Count; t++)
Assert.Equal(PolygonBits(expectedTriangles[t]), PolygonBits(actualTriangles[t]));
}
}
}
[Fact]
public void Check_DoesNotMutateParts()
{
var pattern = MakePattern("rotated", Fixture("arc"));
var grid = new FillLinear(new Box(3.1, 5.3, 61, 37), 0.5).Fill(pattern, NestDirection.Horizontal);
var before = grid.Select(Snapshot).ToArray();
FillHelpers.HasOverlappingParts(grid);
Invoke(grid, out _, out _);
var checker = new PartOverlapChecker();
for (var i = 0; i + 1 < grid.Count; i++)
checker.Overlaps(grid[i], grid[i + 1]);
Assert.Equal(before, grid.Select(Snapshot).ToArray());
}
[Fact]
public void ConcurrentChecks_MatchSequential()
{
var grids = Shapes.SelectMany(shape => new[]
{
Shifted(new FillLinear(new Box(3.1, 5.3, 61, 37), 0.5).Fill(Fixture(shape), 0.37, NestDirection.Horizontal), 0.05),
new FillLinear(new Box(3.1, 5.3, 61, 37), 0.5).Fill(MakePattern("built-pair", Fixture(shape)), NestDirection.Vertical),
}).ToArray();
var expected = grids.Select(g => (Helpers: FillHelpers.HasOverlappingParts(g), Linear: Indices(g))).ToArray();
Parallel.For(0, grids.Length * 4, new ParallelOptions { MaxDegreeOfParallelism = 4 }, i =>
{
var grid = grids[i % grids.Length];
Assert.Equal(expected[i % grids.Length].Helpers, FillHelpers.HasOverlappingParts(grid));
Assert.Equal(expected[i % grids.Length].Linear, Indices(grid));
});
}
#if DEBUG
[Theory]
[InlineData("single", 1)]
[InlineData("pair", 2)]
public void Work_PreparesEachDistinctProgramOncePerCheck(string kind, long distinctPrograms)
{
var grid = WorkGrid(kind);
Assert.Equal(distinctPrograms, grid.Select(g => g.Program).Distinct(ReferenceEqualityComparer.Instance).Count());
PerfCounters.Reset();
try
{
var legacyVerdict = LegacyPartOverlap.FillHelpersHasOverlappingParts(grid);
Assert.Equal(0, PerfCounters.OverlapPolygonPreparations);
// Old path: two preparations per exact test, through Part.Intersects.
var exactTests = 0;
var legacyPreparations = 0L;
PerfCounters.Reset();
ForEachBoxOverlappingPair(grid, (a, b) =>
{
exactTests++;
a.Intersects(b, out _);
});
legacyPreparations = PerfCounters.OverlapPolygonPreparations;
PerfCounters.Reset();
var verdict = FillHelpers.HasOverlappingParts(grid);
var preparations = PerfCounters.OverlapPolygonPreparations;
output.WriteLine($"work {kind}: parts={grid.Count}; exact tests={exactTests}; old preparations={legacyPreparations}; new={preparations}; verdict={verdict}");
Assert.False(legacyVerdict);
Assert.False(verdict);
Assert.True(exactTests > 2);
Assert.Equal(2L * exactTests, legacyPreparations);
Assert.Equal(distinctPrograms, preparations);
}
finally
{
PerfCounters.Reset();
}
}
[Theory]
[InlineData("single")]
[InlineData("pair")]
public void Work_TriangulatesEachPartAtMostOncePerCheck(string kind)
{
var grid = WorkGrid(kind);
// Parts that reach an exact test: their part boxes and their world polygon boxes overlap.
var reaching = new HashSet<Part>(ReferenceEqualityComparer.Instance);
var exactTests = 0;
ForEachBoxOverlappingPair(grid, (a, b) =>
{
if (!PolygonBoxesOverlap(LegacyPartOverlap.WorldPolygon(a), LegacyPartOverlap.WorldPolygon(b)))
return;
exactTests++;
reaching.Add(a);
reaching.Add(b);
});
PerfCounters.Reset();
try
{
ForEachBoxOverlappingPair(grid, (a, b) => a.Intersects(b, out _));
var legacy = PerfCounters.PolygonTriangulations;
PerfCounters.Reset();
Assert.False(FillHelpers.HasOverlappingParts(grid));
var actual = PerfCounters.PolygonTriangulations;
output.WriteLine($"triangulations {kind}: parts={grid.Count}; exact tests={exactTests}; parts reaching={reaching.Count}; old={legacy}; new={actual}");
Assert.True(exactTests > 2);
Assert.Equal(2L * exactTests, legacy);
Assert.Equal(reaching.Count, actual);
}
finally
{
PerfCounters.Reset();
}
}
#endif
private static List<Part> WorkGrid(string kind)
{
// Rotated grids: neighbouring boxes overlap but parts do not, so every box-overlapping
// pair reaches an exact test and there is no early exit.
var filler = new FillLinear(new Box(3.1, 5.3, 96, 48), 0.5);
if (kind == "single")
return filler.Fill(Fixture("arc"), 0.37, NestDirection.Horizontal);
var first = Part.CreateAtOrigin(Fixture("concave"), 0);
var second = Part.CreateAtOrigin(Fixture("concave"), System.Math.PI);
second.Offset(new Vector(first.Right + 0.5, first.Bottom));
return filler.Fill(FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37),
NestDirection.Horizontal);
}
private static bool PolygonBoxesOverlap(Polygon a, Polygon b)
{
var overlapX = System.Math.Min(a.BoundingBox.Right, b.BoundingBox.Right) - System.Math.Max(a.BoundingBox.Left, b.BoundingBox.Left);
var overlapY = System.Math.Min(a.BoundingBox.Top, b.BoundingBox.Top) - System.Math.Max(a.BoundingBox.Bottom, b.BoundingBox.Bottom);
return overlapX > OpenNest.Math.Tolerance.Epsilon && overlapY > OpenNest.Math.Tolerance.Epsilon;
}
private static void AssertMatchesLegacy(List<Part> parts)
{
var legacy = Capture(() => LegacyPartOverlap.FillHelpersHasOverlappingParts(parts));
var actual = Capture(() => FillHelpers.HasOverlappingParts(parts));
Assert.Equal(legacy, actual);
var legacyIndices = Capture(() =>
{
var hit = LegacyPartOverlap.FillLinearHasOverlappingParts(parts, out var a, out var b);
return (hit, a, b);
});
var actualIndices = Capture(() => Indices(parts));
Assert.Equal(legacyIndices, actualIndices);
}
private static (T Value, Type? Exception) Capture<T>(Func<T> run)
{
try
{
return (run(), null);
}
catch (Exception exception)
{
return (default!, (exception as System.Reflection.TargetInvocationException)?.InnerException?.GetType()
?? exception.GetType());
}
}
private static (bool Hit, int A, int B) Indices(List<Part> parts)
{
var hit = Invoke(parts, out var a, out var b);
return (hit, a, b);
}
private static bool Invoke(List<Part> parts, out int a, out int b)
{
var method = typeof(FillLinear).GetMethod("HasOverlappingParts",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static)!;
var args = new object[] { parts, -1, -1 };
var hit = (bool)method.Invoke(null, args)!;
a = (int)args[1];
b = (int)args[2];
return hit;
}
private static void ForEachBoxOverlappingPair(List<Part> parts, Action<Part, Part> action)
{
for (var i = 0; i < parts.Count; i++)
for (var j = i + 1; j < parts.Count; j++)
{
var b1 = parts[i].BoundingBox;
var b2 = parts[j].BoundingBox;
var overlapX = System.Math.Min(b1.Right, b2.Right) - System.Math.Max(b1.Left, b2.Left);
var overlapY = System.Math.Min(b1.Top, b2.Top) - System.Math.Max(b1.Bottom, b2.Bottom);
if (overlapX > OpenNest.Math.Tolerance.Epsilon && overlapY > OpenNest.Math.Tolerance.Epsilon)
action(parts[i], parts[j]);
}
}
private static Polygon WorldPolygon(PartOverlapChecker checker, Part part) =>
(Polygon)PreparedPartMember(checker, part, "Polygon");
private static List<Polygon> WorldTriangles(PartOverlapChecker checker, Part part) =>
((Func<List<Polygon>>)PreparedPartMember(checker, part, "Triangles"))();
private static object PreparedPartMember(PartOverlapChecker checker, Part part, string property)
{
var field = typeof(PartOverlapChecker).GetField("preparedParts",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
var prepared = ((System.Collections.IDictionary)field.GetValue(checker)!)[part]!;
return prepared.GetType().GetProperty(property)!.GetValue(prepared)!;
}
/// <summary>Every other part replaced by a copy moved by (-shift, -shift).</summary>
private static List<Part> Shifted(List<Part> grid, double shift)
{
var result = new List<Part>(grid.Count);
for (var i = 0; i < grid.Count; i++)
result.Add(i % 2 == 0 ? grid[i] : grid[i].CloneAtOffset(new Vector(-shift, -shift)));
return result;
}
private static Drawing Fixture(string shape) => shape switch
{
"circle" => new RingShape { OuterDiameter = 8, InnerDiameter = 0 }.GetDrawing(),
"ring" => new RingShape { OuterDiameter = 8, InnerDiameter = 3 }.GetDrawing(),
_ => FillExtentsTests.MakeFixture(shape),
};
private static Pattern MakePattern(string kind, Drawing drawing)
{
var first = Part.CreateAtOrigin(drawing, 0);
first.Offset(new Vector(11.1, -3.3));
var second = kind == "shared" ? first.CloneAtOffset(new Vector(first.BoundingBox.Length + 0.5, 0))
: Part.CreateAtOrigin(drawing, System.Math.PI / 2);
if (kind != "shared")
second.Offset(new Vector(first.Right + 0.5, first.Bottom));
if (kind == "built-pair")
return FillHelpers.BuildRotatedPattern(new List<Part> { first, second }, 0.37);
var pattern = new Pattern();
pattern.Parts.AddRange(new[] { first, second });
pattern.UpdateBounds();
Assert.Equal(kind == "shared", ReferenceEquals(first.Program, second.Program));
return pattern;
}
private static long Bits(double value) => BitConverter.DoubleToInt64Bits(value);
private static long[] PolygonBits(Polygon polygon) => new[]
{
Bits(polygon.BoundingBox.X), Bits(polygon.BoundingBox.Y),
Bits(polygon.BoundingBox.Length), Bits(polygon.BoundingBox.Width),
}
.Concat(polygon.Vertices.SelectMany(v => new[] { Bits(v.X), Bits(v.Y) }))
.ToArray();
private static object[] Snapshot(Part part) => new object[]
{
part, part.Program, part.Program.Codes.Count, Bits(part.Location.X), Bits(part.Location.Y),
Bits(part.Rotation), Bits(part.BoundingBox.X), Bits(part.BoundingBox.Y),
Bits(part.BoundingBox.Length), Bits(part.BoundingBox.Width),
}
.Concat(part.Program.Codes.Cast<object>())
.Concat(ConvertProgram.ToGeometry(part.Program).SelectMany(e => new object[] { e.GetType(), e.Layer,
Bits(e.BoundingBox.X), Bits(e.BoundingBox.Y), Bits(e.BoundingBox.Length), Bits(e.BoundingBox.Width) }))
.ToArray();
}