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;
///
/// 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.
///
[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 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(pattern.Parts);
raw.AddRange((List)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 { 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 { 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 { 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));
}
}
}
[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)
{
// 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);
List grid;
if (kind == "single")
grid = filler.Fill(Fixture("arc"), 0.37, NestDirection.Horizontal);
else
{
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));
grid = filler.Fill(FillHelpers.BuildRotatedPattern(new List { first, second }, 0.37),
NestDirection.Horizontal);
}
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();
}
}
#endif
private static void AssertMatchesLegacy(List 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(Func 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 parts)
{
var hit = Invoke(parts, out var a, out var b);
return (hit, a, b);
}
private static bool Invoke(List 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 parts, Action 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)
{
var field = typeof(PartOverlapChecker).GetField("worldPolygons",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
return ((Dictionary)field.GetValue(checker)!)[part];
}
/// Every other part replaced by a copy moved by (-shift, -shift).
private static List Shifted(List grid, double shift)
{
var result = new List(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 { 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())
.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();
}