From 2f19f47a851143a0e9e9cad6079f142151d04726 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 22 Mar 2026 19:40:01 -0400 Subject: [PATCH] feat: serialize/deserialize cut-off definitions in nest file format Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.IO/NestFormat.cs | 10 ++ OpenNest.IO/NestReader.cs | 19 ++++ OpenNest.IO/NestWriter.cs | 18 +++- OpenNest.Tests/CutOffSerializationTests.cs | 119 +++++++++++++++++++++ 4 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 OpenNest.Tests/CutOffSerializationTests.cs diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs index 7cdf4f3..c21b550 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -62,6 +62,7 @@ namespace OpenNest.IO public MaterialDto Material { get; init; } = new(); public SpacingDto EdgeSpacing { get; init; } = new(); public List Parts { get; init; } = new(); + public List CutOffs { get; init; } = new(); } public record PartDto @@ -72,6 +73,15 @@ namespace OpenNest.IO public double Rotation { get; init; } } + public record CutOffDto + { + public double X { get; init; } + public double Y { get; init; } + public string Axis { get; init; } = "vertical"; + public double? StartLimit { get; init; } + public double? EndLimit { get; init; } + } + public record SizeDto { public double Width { get; init; } diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index 5936b3b..6072d9a 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -197,6 +197,25 @@ namespace OpenNest.IO plate.Parts.Add(part); } + // Cut-offs + if (p.CutOffs != null) + { + foreach (var cutoffDto in p.CutOffs) + { + var axis = cutoffDto.Axis?.ToLowerInvariant() == "horizontal" + ? CutOffAxis.Horizontal + : CutOffAxis.Vertical; + var cutoff = new CutOff(new Vector(cutoffDto.X, cutoffDto.Y), axis) + { + StartLimit = cutoffDto.StartLimit, + EndLimit = cutoffDto.EndLimit + }; + plate.CutOffs.Add(cutoff); + } + + plate.RegenerateCutOffs(new CutOffSettings()); + } + nest.Plates.Add(plate); } diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index ffa5f77..f8bfea2 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -152,7 +152,7 @@ namespace OpenNest.IO { var plate = nest.Plates[i]; var parts = new List(); - foreach (var part in plate.Parts) + foreach (var part in plate.Parts.Where(p => !p.BaseDrawing.IsCutOff)) { var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault(); parts.Add(new PartDto @@ -164,6 +164,19 @@ namespace OpenNest.IO }); } + var cutoffs = new List(); + foreach (var cutoff in plate.CutOffs) + { + cutoffs.Add(new CutOffDto + { + X = cutoff.Position.X, + Y = cutoff.Position.Y, + Axis = cutoff.Axis == CutOffAxis.Vertical ? "vertical" : "horizontal", + StartLimit = cutoff.StartLimit, + EndLimit = cutoff.EndLimit + }); + } + list.Add(new PlateDto { Id = i + 1, @@ -185,7 +198,8 @@ namespace OpenNest.IO Right = plate.EdgeSpacing.Right, Bottom = plate.EdgeSpacing.Bottom }, - Parts = parts + Parts = parts, + CutOffs = cutoffs }); } return list; diff --git a/OpenNest.Tests/CutOffSerializationTests.cs b/OpenNest.Tests/CutOffSerializationTests.cs new file mode 100644 index 0000000..b4d13fd --- /dev/null +++ b/OpenNest.Tests/CutOffSerializationTests.cs @@ -0,0 +1,119 @@ +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.IO; + +namespace OpenNest.Tests; + +public class CutOffSerializationTests +{ + [Fact] + public void RoundTrip_CutOffsPreserved() + { + var nest = new Nest(); + nest.Name = "test"; + nest.DateCreated = DateTime.Now; + nest.DateLastModified = DateTime.Now; + + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(10, 10))); + var drawing = new Drawing("part1", pgm); + nest.Drawings.Add(drawing); + + var plate = new Plate(100, 50); + plate.Parts.Add(new Part(drawing)); + plate.CutOffs.Add(new CutOff(new Vector(62.0, 24.0), CutOffAxis.Vertical)); + plate.CutOffs.Add(new CutOff(new Vector(48.0, 30.0), CutOffAxis.Horizontal)); + plate.RegenerateCutOffs(new CutOffSettings()); + nest.Plates.Add(plate); + + using var stream = new MemoryStream(); + var writer = new NestWriter(nest); + writer.Write(stream); + + stream.Position = 0; + var reader = new NestReader(stream); + var loaded = reader.Read(); + + Assert.Single(loaded.Plates); + var loadedPlate = loaded.Plates[0]; + + Assert.Equal(2, loadedPlate.CutOffs.Count); + Assert.Equal(CutOffAxis.Vertical, loadedPlate.CutOffs[0].Axis); + Assert.Equal(62.0, loadedPlate.CutOffs[0].Position.X, 5); + Assert.Equal(24.0, loadedPlate.CutOffs[0].Position.Y, 5); + Assert.Equal(CutOffAxis.Horizontal, loadedPlate.CutOffs[1].Axis); + + Assert.Single(loadedPlate.Parts.Where(p => !p.BaseDrawing.IsCutOff)); + Assert.Single(loaded.Drawings); + } + + [Fact] + public void NestWriter_SkipsCutOffPartsInPartsList() + { + var nest = new Nest(); + nest.Name = "test"; + nest.DateCreated = DateTime.Now; + nest.DateLastModified = DateTime.Now; + + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(10, 10))); + var drawing = new Drawing("part1", pgm); + nest.Drawings.Add(drawing); + + var plate = new Plate(100, 50); + plate.Parts.Add(new Part(drawing)); + plate.CutOffs.Add(new CutOff(new Vector(50, 25), CutOffAxis.Vertical)); + plate.RegenerateCutOffs(new CutOffSettings()); + nest.Plates.Add(plate); + + Assert.Equal(2, plate.Parts.Count); + + using var stream = new MemoryStream(); + var writer = new NestWriter(nest); + writer.Write(stream); + + stream.Position = 0; + var reader = new NestReader(stream); + var loaded = reader.Read(); + + Assert.Single(loaded.Plates[0].Parts.Where(p => !p.BaseDrawing.IsCutOff)); + } + + [Fact] + public void RoundTrip_LimitsPreserved() + { + var nest = new Nest(); + nest.Name = "test"; + nest.DateCreated = DateTime.Now; + nest.DateLastModified = DateTime.Now; + + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(10, 10))); + var drawing = new Drawing("part1", pgm); + nest.Drawings.Add(drawing); + + var plate = new Plate(100, 50); + plate.Parts.Add(new Part(drawing)); + plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Horizontal) { EndLimit = 85.0 }); + plate.CutOffs.Add(new CutOff(new Vector(85, 30), CutOffAxis.Vertical) { StartLimit = 30.0 }); + plate.RegenerateCutOffs(new CutOffSettings()); + nest.Plates.Add(plate); + + using var stream = new MemoryStream(); + var writer = new NestWriter(nest); + writer.Write(stream); + + stream.Position = 0; + var reader = new NestReader(stream); + var loaded = reader.Read(); + + var loadedPlate = loaded.Plates[0]; + Assert.Equal(85.0, loadedPlate.CutOffs[0].EndLimit); + Assert.Null(loadedPlate.CutOffs[0].StartLimit); + Assert.Equal(30.0, loadedPlate.CutOffs[1].StartLimit); + Assert.Null(loadedPlate.CutOffs[1].EndLimit); + } +}