From d58a446eac76794fd498dafe4754e6735fbce6ef Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 22 Mar 2026 19:36:05 -0400 Subject: [PATCH] feat: add Plate.CutOffs collection with materialization and transform support Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Plate.cs | 83 +++++++++++++++++++++++++++++++++-- OpenNest.Tests/CutOffTests.cs | 48 ++++++++++++++++++++ 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/OpenNest.Core/Plate.cs b/OpenNest.Core/Plate.cs index 8430f98..5e3a1eb 100644 --- a/OpenNest.Core/Plate.cs +++ b/OpenNest.Core/Plate.cs @@ -47,6 +47,7 @@ namespace OpenNest Parts = new ObservableList(); Parts.ItemAdded += Parts_PartAdded; Parts.ItemRemoved += Parts_PartRemoved; + CutOffs = new ObservableList(); Quadrant = 1; } @@ -92,6 +93,38 @@ namespace OpenNest /// public ObservableList Parts { get; set; } + /// + /// The cut-off lines defined on this plate. + /// + public ObservableList CutOffs { get; set; } + + /// + /// Regenerates all cut-off drawings and materializes them as parts. + /// Existing cut-off parts are removed first, then each cut-off is + /// regenerated and added back if it produces any geometry. + /// + public void RegenerateCutOffs(CutOffSettings settings) + { + // Remove existing cut-off parts + for (var i = Parts.Count - 1; i >= 0; i--) + { + if (Parts[i].BaseDrawing.IsCutOff) + Parts.RemoveAt(i); + } + + // Regenerate and materialize each cut-off + foreach (var cutoff in CutOffs) + { + cutoff.Regenerate(this, settings); + + if (cutoff.Drawing.Program.Codes.Count == 0) + continue; + + var part = new Part(cutoff.Drawing); + Parts.Add(part); + } + } + /// /// The number of times to cut the plate. /// @@ -242,11 +275,20 @@ namespace OpenNest /// public void Rotate(double angle) { - for (int i = 0; i < Parts.Count; ++i) + for (var i = Parts.Count - 1; i >= 0; i--) + { + if (Parts[i].BaseDrawing.IsCutOff) + Parts.RemoveAt(i); + } + + for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Rotate(angle); } + + foreach (var cutoff in CutOffs) + cutoff.Position = cutoff.Position.Rotate(angle); } /// @@ -256,11 +298,24 @@ namespace OpenNest /// public void Rotate(double angle, Vector origin) { - for (int i = 0; i < Parts.Count; ++i) + for (var i = Parts.Count - 1; i >= 0; i--) + { + if (Parts[i].BaseDrawing.IsCutOff) + Parts.RemoveAt(i); + } + + for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Rotate(angle, origin); } + + foreach (var cutoff in CutOffs) + { + var pos = cutoff.Position - origin; + pos = pos.Rotate(angle); + cutoff.Position = pos + origin; + } } /// @@ -270,11 +325,22 @@ namespace OpenNest /// public void Offset(double x, double y) { - for (int i = 0; i < Parts.Count; ++i) + // Remove cut-off parts before transforming + for (var i = Parts.Count - 1; i >= 0; i--) + { + if (Parts[i].BaseDrawing.IsCutOff) + Parts.RemoveAt(i); + } + + for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Offset(x, y); } + + // Transform cut-off positions + foreach (var cutoff in CutOffs) + cutoff.Position = new Vector(cutoff.Position.X + x, cutoff.Position.Y + y); } /// @@ -283,11 +349,20 @@ namespace OpenNest /// public void Offset(Vector voffset) { - for (int i = 0; i < Parts.Count; ++i) + for (var i = Parts.Count - 1; i >= 0; i--) + { + if (Parts[i].BaseDrawing.IsCutOff) + Parts.RemoveAt(i); + } + + for (var i = 0; i < Parts.Count; ++i) { var part = Parts[i]; part.Offset(voffset); } + + foreach (var cutoff in CutOffs) + cutoff.Position = new Vector(cutoff.Position.X + voffset.X, cutoff.Position.Y + voffset.Y); } /// diff --git a/OpenNest.Tests/CutOffTests.cs b/OpenNest.Tests/CutOffTests.cs index e3cd936..440afb3 100644 --- a/OpenNest.Tests/CutOffTests.cs +++ b/OpenNest.Tests/CutOffTests.cs @@ -215,4 +215,52 @@ public class CutOffTests Assert.True(hCut.Drawing.Program.Codes.Count > 0); Assert.True(vCut.Drawing.Program.Codes.Count > 0); } + + [Fact] + public void Plate_RegenerateCutOffs_MaterializesParts() + { + var plate = new Plate(100, 50); + var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical); + plate.CutOffs.Add(cutoff); + + plate.RegenerateCutOffs(new CutOffSettings()); + + Assert.Single(plate.Parts); + Assert.True(plate.Parts[0].BaseDrawing.IsCutOff); + } + + [Fact] + public void Plate_RegenerateCutOffs_ReplacesOldParts() + { + var plate = new Plate(100, 50); + var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical); + plate.CutOffs.Add(cutoff); + + var settings = new CutOffSettings(); + plate.RegenerateCutOffs(settings); + plate.RegenerateCutOffs(settings); + + Assert.Single(plate.Parts); + } + + [Fact] + public void Plate_RegenerateCutOffs_DoesNotAffectRegularParts() + { + var pgm = new OpenNest.CNC.Program(); + pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Geometry.Vector(0, 0))); + pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Geometry.Vector(5, 5))); + var drawing = new Drawing("real", pgm); + + var plate = new Plate(100, 50); + plate.Parts.Add(new Part(drawing)); + + var cutoff = new CutOff(new Geometry.Vector(25, 10), CutOffAxis.Vertical); + plate.CutOffs.Add(cutoff); + + plate.RegenerateCutOffs(new CutOffSettings()); + + Assert.Equal(2, plate.Parts.Count); + Assert.False(plate.Parts[0].BaseDrawing.IsCutOff); + Assert.True(plate.Parts[1].BaseDrawing.IsCutOff); + } }