From 17fc9c6cab8e19b1e3dc0df9fb7e675a41e9e584 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 22 Mar 2026 22:44:15 -0400 Subject: [PATCH] feat: RegenerateCutOffs uses geometry-based perimeter cache Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Plate.cs | 4 +++- OpenNest.Tests/CutOffGeometryTests.cs | 30 +++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index c07586a..16c5189 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -112,10 +112,12 @@ namespace OpenNest Parts.RemoveAt(i); } + var cache = BuildPerimeterCache(this); + // Regenerate and materialize each cut-off foreach (var cutoff in CutOffs) { - cutoff.Regenerate(this, settings); + cutoff.Regenerate(this, settings, cache); if (cutoff.Drawing.Program.Codes.Count == 0) continue; diff --git a/OpenNest.Tests/CutOffGeometryTests.cs b/OpenNest.Tests/CutOffGeometryTests.cs index 3e20d3a..fe48709 100644 --- a/OpenNest.Tests/CutOffGeometryTests.cs +++ b/OpenNest.Tests/CutOffGeometryTests.cs @@ -357,6 +357,36 @@ public class CutOffGeometryTests Assert.NotNull(cache[part]); } + [Fact] + public void RegenerateCutOffs_UsesGeometryExclusions() + { + // Circle radius=10 at origin. Vertical cut at X=2. + // With geometry: tighter exclusion than BB. + var drawing = new Drawing("circ", MakeCircle(10)); + var plate = new Plate(100, 100); + var part = Part.CreateAtOrigin(drawing); + plate.Parts.Add(part); + + var cutoff = new CutOff(new Vector(2, 0), CutOffAxis.Vertical); + plate.CutOffs.Add(cutoff); + plate.RegenerateCutOffs(new CutOffSettings { PartClearance = 0 }); + + // Find the materialized cut-off part + var cutPart = plate.Parts.First(p => p.BaseDrawing.IsCutOff); + var totalCutLength = 0.0; + for (var i = 0; i < cutPart.BaseDrawing.Program.Codes.Count - 1; i += 2) + { + if (cutPart.BaseDrawing.Program.Codes[i] is RapidMove rapid && + cutPart.BaseDrawing.Program.Codes[i + 1] is LinearMove linear) + { + totalCutLength += System.Math.Abs(rapid.EndPoint.Y - linear.EndPoint.Y); + } + } + + // BB would give 80 (100 - 20). Geometry should give more. + Assert.True(totalCutLength > 80, $"RegenerateCutOffs should use geometry. Got {totalCutLength:F2}"); + } + [Fact] public void ShapeProfile_SelectsLargestShapeAsPerimeter() {