From e43a7c99d001ed15e5d277d963c844538631531e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 5 Oct 2026 00:27:31 -0400 Subject: [PATCH] fix(cutting): find nested parts from material bounds The containment prefilter used the inner part's whole clean-program bounds, which include rapid endpoints and scribe marks. A remote rapid or mark could push those bounds outside the host and drop a genuine inner-before-host prerequisite. Candidate pairs now use the material extent only (cut and display motions); containment is still proven on native material. --- .../CuttingPlanning/CuttingPlanService.cs | 7 ++-- .../CuttingPlanning/CuttingDependencyTests.cs | 33 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs index 2e5a758..eea3eac 100644 --- a/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs +++ b/OpenNest.Engine/CuttingPlanning/CuttingPlanService.cs @@ -97,8 +97,11 @@ public static class CuttingPlanService // Exact placed/proposed payloads still use lossless OwnedProgramCopy. var ownedClean = (Program)source.BaseDrawing.Program.Clone(); ownedClean.Rotate(source.Rotation - source.BaseDrawing.Program.Rotation); - var cleanBounds = ownedClean.BoundingBox(); - cleanBounds.Offset(source.Location); + // Material extent only: remote rapids and scribe marks must not widen it, or a + // genuine insert could fail the containment prefilter. + var cleanBounds = ExecutionMotionReader.ReadSupported(ownedClean, source.Location, null, token).Motions + .Where(m => !m.Rapid && m.Layer is LayerType.Cut or LayerType.Display && m.Curve != null) + .Select(m => m.Curve.ToEntity().BoundingBox).ToList().GetBoundingBox(); var location = source.Location; if (request.ConfirmedParameters != null) { diff --git a/OpenNest.Tests/CuttingPlanning/CuttingDependencyTests.cs b/OpenNest.Tests/CuttingPlanning/CuttingDependencyTests.cs index 86dd311..eb89727 100644 --- a/OpenNest.Tests/CuttingPlanning/CuttingDependencyTests.cs +++ b/OpenNest.Tests/CuttingPlanning/CuttingDependencyTests.cs @@ -137,6 +137,22 @@ public class CuttingDependencyTests Assert.Contains("nested in its cutout", finding.Message); } + [Fact] + public void Dependencies_NonmaterialMotionsCannotHideANestedPart() + { + // Same insert as above plus a remote rapid and scribe mark: material is unchanged, so + // the inner-before-host prerequisite must not depend on whole-program bounds. + var (plate, host, inner) = NestedPlate(Marked(Rectangle(2.7, 2.7, 0.6, 0.6))); + var snapshot = CuttingPlanService.Capture(new CuttingPlanRequest(plate)); + Assert.Null(snapshot.Findings.FirstOrDefault()?.Message); + Assert.True(inner.BoundingBox.Left < host.BoundingBox.Left); + + Assert.Equal(new[] { 1 }, snapshot.Dependencies.PrerequisitesOf(0)); + var result = CuttingPlanService.Plan(snapshot); + Assert.True(result.Status == CuttingPlanStatus.Ready, Describe(result)); + Assert.Equal(new[] { inner, host }, result.ProposedOrder.Select(o => o.SourcePart)); + } + [Fact] public void Capture_PartStraddlingAHostCutoutEdge_IsAmbiguousContainment() { @@ -234,6 +250,23 @@ public class CuttingDependencyTests return (plate, host, inner); } + // Prefixes a remote rapid and scribe mark to both the clean and placed programs. + private static Part Marked(Part part) + { + static Program Prefix(Program source) + { + var program = new Program(); + program.Codes.Add(new RapidMove(-20, -20)); + program.Codes.Add(new LinearMove(-19, -20) { Layer = LayerType.Scribe }); + program.Codes.AddRange(source.Codes); + return program; + } + + var marked = new Part(new Drawing("same", Prefix(part.BaseDrawing.Program)), part.Location); + Assert.True(marked.RestoreLeadInProgram(Prefix(part.Program), false)); + return marked; + } + // A U-shaped part open at the top between x 3 and 7, lead on its right edge. private static Part Pocketed() {