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.
This commit is contained in:
aj committed 2026-10-05 00:27:31 -04:00
1 parent b9b475ab77
commit e43a7c99d0
2 files changed
+38 -2

No files matched your search

@@ -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)
{
@@ -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()
{