From 35be95b84306dc85e971f8b76952e6407892ff2e Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 29 Oct 2026 14:32:30 -0400 Subject: [PATCH] fix(cutoffs): leave leads out of the open-contour trim hull When a part's outline is open (a tab gap, say), cut-off trimming falls back to a convex hull. That hull still included lead-in/lead-out points, so it ran from the lead's pierce point back to the part and stopped cut-offs far short of the material. Cutting through a lead is harmless, so the hull now uses the material contour only, as the closed-outline path already did. Co-Authored-By: Claude Opus 5.5 --- OpenNest.Core/Plate.cs | 9 ++++--- .../CutOffs/CutOffLeadClearanceTests.cs | 24 +++++++++++++++++++ docs/automatic-scrap-cutoffs.md | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index 94375a34..239ef3e0 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -535,10 +535,13 @@ namespace OpenNest // Leads are cutting paths in scrap, not boundary edges. Chaining them // into the outline can make a closed part appear open and replace its - // recesses with a convex hull. Keep the full geometry for the existing - // conservative fallback when the material contour really is open. + // recesses with a convex hull. A cut-off may cross a lead, so the open- + // contour hull also leaves them out: hulling a lead's pierce point would + // stop cut-offs far short of the part. var outline = entities.Where(e => e.Layer != SpecialLayers.Leadin && e.Layer != SpecialLayers.Leadout).ToList(); + if (outline.Count == 0) + return null; var profile = new Geometry.ShapeProfile(outline); if (profile.Perimeter.IsClosed()) @@ -548,7 +551,7 @@ namespace OpenNest return perimeter; } - var points = entities.CollectPoints(); + var points = outline.CollectPoints(); if (points.Count < 3) return null; diff --git a/OpenNest.Tests/CutOffs/CutOffLeadClearanceTests.cs b/OpenNest.Tests/CutOffs/CutOffLeadClearanceTests.cs index 482f4d4a..5ec9f9d8 100644 --- a/OpenNest.Tests/CutOffs/CutOffLeadClearanceTests.cs +++ b/OpenNest.Tests/CutOffs/CutOffLeadClearanceTests.cs @@ -138,4 +138,28 @@ public class CutOffLeadClearanceTests Assert.True(Assert.IsType(codes[1]).EndPoint.Y <= 19.98); Assert.True(Assert.IsType(codes[2]).EndPoint.Y >= 25.02); } + + [Fact] + public void OpenContour_FallbackHullIgnoresLeads() + { + // A tab gap leaves the outline open. The fallback hull must follow the material, + // not run from the lead-in's pierce point, or a cut-off passing the part stops far short. + var program = new Program(); + program.MoveTo(50, -5); + program.Codes.Add(new LinearMove(40, 5) { Layer = LayerType.Leadin }); + program.LineTo(40, 15); + program.LineTo(0, 20); + program.LineTo(0, 0); + program.LineTo(39, 4.875); + var plate = new Plate(100, 100); + var part = new Part(new Drawing("open-tapered", program), new Vector(20, 20)); + plate.Parts.Add(part); + var cutoff = new CutOff(new Vector(50, 0), CutOffAxis.Vertical); + plate.CutOffs.Add(cutoff); + plate.RegenerateCutOffs(new CutOffSettings()); + var codes = cutoff.Drawing.Program.Codes; + Assert.Equal(4, codes.Count); + // The material's bottom edge crosses x = 50 at y = 23.75. + Assert.InRange(Assert.IsType(codes[1]).EndPoint.Y, 23, 23.75 - 0.02); + } } diff --git a/docs/automatic-scrap-cutoffs.md b/docs/automatic-scrap-cutoffs.md index b38747bf..6557a637 100644 --- a/docs/automatic-scrap-cutoffs.md +++ b/docs/automatic-scrap-cutoffs.md @@ -20,7 +20,7 @@ A proposed tail shorter than **Minimum tail to keep** is left attached to the sc Only real parts contribute to the occupied envelope; existing cutoff parts do not. When there is room, the final separator is beyond the furthest occupied extent by the larger of plate part spacing and cutoff part clearance, plus numerical tolerance. Repeated skeleton cuts stop before that separator. The larger margin affects only the separator: repeated cuts keep the existing manual-cutoff clearance behavior. -Cutoff trimming measures clearance from the part contour, excluding tagged lead-in and lead-out moves when building the closed outline. Leads must not turn a recessed outline into a convex hull and leave oversized gaps elsewhere. Genuinely open contours still use the conservative hull/bounds fallback. With a nonzero clearance, a line passing exactly through a corner of the clearance outline is trimmed to the outline like any other line. Automatic strip lines sit on a regular lattice and often land on such corners; they once fell back to the part's bounding box there, and the strip search preferred those lines because the box fallback measured as less cutting. The conservative curve/offset approximation can leave a small extra gap (about 0.0003 in on a straight edge at the default 0.02-in clearance), not an extra quarter inch. +Cutoff trimming measures clearance from the part contour, excluding tagged lead-in and lead-out moves when building the closed outline. Leads must not turn a recessed outline into a convex hull and leave oversized gaps elsewhere. Genuinely open contours (a tab gap, for example) still use the conservative hull/bounds fallback, but the hull is built from the material contour only: a cut-off may cross a lead, and hulling a lead's pierce point stopped cut-offs far short of the part. With a nonzero clearance, a line passing exactly through a corner of the clearance outline is trimmed to the outline like any other line. Automatic strip lines sit on a regular lattice and often land on such corners; they once fell back to the part's bounding box there, and the strip search preferred those lines because the box fallback measured as less cutting. The conservative curve/offset approximation can leave a small extra gap (about 0.0003 in on a straight edge at the default 0.02-in clearance), not an extra quarter inch. If no safe separator fits inside the sheet, there is no retained-tail claim and the nominal cuts may cover the full sheet length. A proposed tail is not reported as separated unless its generated program spans the full sheet width. An active clamp blocks any proposed full-width line whose X coordinate falls in the clamp's whole footprint plus bottom-edge clearance, including a nominal grid line or an equivalent existing line. A tolerance-close existing line is tested at its own X as well as the proposed X, because that existing line is what would be certified. A same-line manual cut with different limits still blocks the whole plan, even on a clamp. Blocked lines are skipped with a clamp diagnostic; a blocked tail also reports `NoSafeTailSeparator` rather than claiming separation. Removed clamps on a sheet are absent from its active zones and do not block cuts. Stored zones are relative to the sheet's lower-left corner; the planner maps them into the plate's quadrant frame (the same frame as part and cut-off coordinates) before comparing, so the rule holds in all four quadrants. Obstructed or minimum-length-filtered lines produce diagnostics; they are not proof of a completed partition.