From 71bd9643449d925915f2e0c978af5b0d5bc9ef53 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Fri, 9 Oct 2026 17:53:22 -0400 Subject: [PATCH] test(cutouts): trace accepted lattice and NFP poses in one run --- .../Jobs/Cutouts/CutoutRouterTests.cs | 21 ++++++++++++------- OpenNest.Engine/Jobs/Cutouts/CutoutRouter.cs | 15 ++++++++++++- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/OpenNest.Engine.Tests/Jobs/Cutouts/CutoutRouterTests.cs b/OpenNest.Engine.Tests/Jobs/Cutouts/CutoutRouterTests.cs index 6930a49..1f37fe1 100644 --- a/OpenNest.Engine.Tests/Jobs/Cutouts/CutoutRouterTests.cs +++ b/OpenNest.Engine.Tests/Jobs/Cutouts/CutoutRouterTests.cs @@ -24,10 +24,14 @@ public class CutoutRouterTests var frame = JobBuilder.Part("frame", TestShapes.Ring(20, 10), 1); var small = JobBuilder.Part("small", TestShapes.Rectangle(1, 1), 3); var large = JobBuilder.Rectangle("large", 3, 3, 1, RotationPolicy.Fixed(0)); - Assert.Equal(3, CutoutLatticeFill.Fill(frame, 0, small, 3, 0.25).Count); - var poses = CutoutRouter.Fill(frame, 0, new[] { small, large }, 0.25); + var origins = new List<(NestJobPlacement Pose, bool Lattice)>(); + var poses = CutoutRouter.Fill(frame, 0, new[] { small, large }, 0.25, + (pose, lattice) => origins.Add((pose, lattice))); Assert.Equal(3, poses.Count(p => p.PartId == small.Id)); Assert.Single(poses, p => p.PartId == large.Id); + Assert.Contains(origins, row => row.Pose.PartId == small.Id && row.Lattice); + Assert.Contains(origins, row => row.Pose.PartId == large.Id && !row.Lattice); + Assert.Equal(poses, origins.Select(row => row.Pose)); Assert.Equal(new[] { 0, 1, 2 }, poses.Where(p => p.PartId == small.Id).Select(p => p.InstanceIndex)); AssertPhysical(frame, new[] { small, large }, poses); } @@ -38,10 +42,10 @@ public class CutoutRouterTests var frame = Frame(); var small = JobBuilder.Rectangle("small", 1, 1, 3, RotationPolicy.Fixed(0)); var impossible = JobBuilder.Rectangle("impossible", 9.6, 9.6, 1, RotationPolicy.Fixed(0)); - var alone = CutoutRouter.Fill(frame, 0, new[] { small }, 0.25); var together = CutoutRouter.Fill(frame, 0, new[] { small, impossible }, 0.25); - Assert.Equal(alone, together); Assert.Equal(3, together.Count); + Assert.All(together, p => Assert.Equal(small.Id, p.PartId)); + Assert.Equal(Enumerable.Range(0, 3), together.Select(p => p.InstanceIndex)); AssertPhysical(frame, new[] { small, impossible }, together); } @@ -74,10 +78,13 @@ public class CutoutRouterTests // lattice keeps 11; an independently certified NFP pose occupies its leftover. var frame = JobBuilder.Part("frame", TestShapes.Ring(18, 8), 1); var insert = JobBuilder.Part("insert", TestShapes.Rectangle(2, 1), 12); - var lattice = CutoutLatticeFill.Fill(frame, 0, insert, 12, 0.25); - Assert.Equal(11, lattice.Count); - var poses = CutoutRouter.Fill(frame, 0, new[] { insert }, 0.25); + var origins = new List<(NestJobPlacement Pose, bool Lattice)>(); + var poses = CutoutRouter.Fill(frame, 0, new[] { insert }, 0.25, + (pose, lattice) => origins.Add((pose, lattice))); Assert.Equal(12, poses.Count); + Assert.Equal(11, origins.Count(row => row.Lattice)); + Assert.Single(origins, row => !row.Lattice); + Assert.Equal(poses, origins.Select(row => row.Pose)); Assert.Equal(Enumerable.Range(0, 12), poses.Select(p => p.InstanceIndex)); AssertPhysical(frame, new[] { insert }, poses); } diff --git a/OpenNest.Engine/Jobs/Cutouts/CutoutRouter.cs b/OpenNest.Engine/Jobs/Cutouts/CutoutRouter.cs index 78fdf9d..53caeaa 100644 --- a/OpenNest.Engine/Jobs/Cutouts/CutoutRouter.cs +++ b/OpenNest.Engine/Jobs/Cutouts/CutoutRouter.cs @@ -19,7 +19,14 @@ internal static class CutoutRouter internal const double LargeRatio = 0.35; internal static IReadOnlyList Fill(NestJobPart frame, int cutoutIndex, - IReadOnlyList inserts, double spacing, CancellationToken token = default) + IReadOnlyList inserts, double spacing, CancellationToken token = default) => + Fill(frame, cutoutIndex, inserts, spacing, null, token); + + /// Optional internal diagnostic receives only poses from the chosen proposal, + /// after indexing; true marks a retained lattice seed, false a residual NFP pose. + internal static IReadOnlyList Fill(NestJobPart frame, int cutoutIndex, + IReadOnlyList inserts, double spacing, + Action? observeAccepted, CancellationToken token = default) { ArgumentNullException.ThrowIfNull(frame); ArgumentNullException.ThrowIfNull(inserts); @@ -61,11 +68,14 @@ internal static class CutoutRouter CutoutLatticeFill.DefaultShiftSteps, occupied, token) : Array.Empty(); IReadOnlyList chosen; + var retainedLattice = 0; if (compareBoth || (nfpFirst && lattice.Count > 0)) { nfp ??= Nfp(Array.Empty()); var fromLattice = Complete(lattice); chosen = fromLattice.Count > nfp.Count ? fromLattice : nfp; + if (ReferenceEquals(chosen, fromLattice)) + retainedLattice = lattice.Count; } else if (nfpFirst) chosen = nfp!; @@ -77,14 +87,17 @@ internal static class CutoutRouter chosen = Complete(seed); if (chosen.Count == 0 && lattice.Count == 1) chosen = lattice; + retainedLattice = ReferenceEquals(chosen, lattice) ? lattice.Count : seed.Count; } + var localIndex = 0; foreach (var pose in chosen) { token.ThrowIfCancellationRequested(); var indexed = pose with { InstanceIndex = accepted.Count(p => p.PartId == part.Id) }; accepted.Add(indexed); occupied.Add((geometry, indexed)); + observeAccepted?.Invoke(indexed, localIndex++ < retainedLattice); } IReadOnlyList Complete(IReadOnlyList seed)