From b45e9df5934938be3f31cc06afd7d8ca974564fd Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 22 Oct 2026 19:34:52 -0400 Subject: [PATCH] feat(io): save and reload display perimeters A display perimeter written without its own tag reloaded as an ordinary cut, so a saved blank would come back with its outline to be cut. Programs now carry it as :DISPLAYPERIMETER and the reader maps it back, and stored source entities keep the DISPLAY PERIMETER layer so editing the drawing rebuilds the same never-cut outline. Builds older than this read the unknown tag as a cut. Open nests with display perimeters only in builds that include this change. --- .../DisplayPerimeterPersistenceTests.cs | 55 +++++++++++++++++++ OpenNest.IO/EntitySerializer.cs | 2 + OpenNest.IO/NestWriter.cs | 3 + OpenNest.IO/ProgramReader.cs | 8 +++ 4 files changed, 68 insertions(+) create mode 100644 OpenNest.IO.Tests/DisplayPerimeterPersistenceTests.cs diff --git a/OpenNest.IO.Tests/DisplayPerimeterPersistenceTests.cs b/OpenNest.IO.Tests/DisplayPerimeterPersistenceTests.cs new file mode 100644 index 00000000..8c3f6680 --- /dev/null +++ b/OpenNest.IO.Tests/DisplayPerimeterPersistenceTests.cs @@ -0,0 +1,55 @@ +using OpenNest.Bending; +using OpenNest.CNC; +using OpenNest.Geometry; +using OpenNest.TabbedCutouts; + +namespace OpenNest.IO.Tests; + +/// A supplied blank's display perimeter must survive save/reload as never-cut geometry. +public class DisplayPerimeterPersistenceTests +{ + private static Drawing Door() + { + var outline = new List + { + new Line(0, 0, 30, 0), new Line(30, 0, 30, 20), new Line(30, 20, 0, 20), new Line(0, 20, 0, 0), + }; + foreach (var line in outline) + line.Layer = SpecialLayers.DisplayPerimeter; + var entities = new List(outline) + { + new Line(5, 5, 10, 5), new Line(10, 5, 10, 10), new Line(10, 10, 5, 10), new Line(5, 10, 5, 5), + }; + return CadImporter.BuildDrawing(new CadImportResult { Entities = entities, Name = "door" }, + entities, new List(), 1, null, null, null, new List()); + } + + private static int DisplayPerimeterMoves(Program program) => + program.Codes.Count(code => code is LinearMove { Layer: LayerType.DisplayPerimeter } or ArcMove { Layer: LayerType.DisplayPerimeter }); + + [Fact] + public void DisplayPerimeter_SurvivesSaveAndReloadOnTheDrawingAndItsPlacements() + { + var drawing = Door(); + Assert.Equal(4, DisplayPerimeterMoves(drawing.Program)); + var nest = new Nest("door") { Units = Units.Inches }; + nest.Drawings.Add(drawing); + var plate = new Plate(new Size(20, 30)); + plate.Parts.Add(new Part(drawing, Vector.Zero)); + nest.Plates.Add(plate); + using var stream = new MemoryStream(); + Assert.True(new NestWriter(nest).Write(stream)); + + var reader = new NestReader(new MemoryStream(stream.ToArray())); + var restored = reader.Read(); + + Assert.Empty(reader.Warnings); + var reread = Assert.Single(restored.Drawings); + Assert.Equal(4, DisplayPerimeterMoves(reread.Program)); + var outline = CadImporter.EditableEntities(reread).Where(e => e.Length > 19.9).ToList(); + Assert.Equal(4, outline.Count); + Assert.All(outline, e => Assert.Same(SpecialLayers.DisplayPerimeter, e.Layer)); + var part = Assert.Single(restored.Plates.SelectMany(p => p.Parts)); + Assert.Equal(4, DisplayPerimeterMoves(part.Program)); + } +} diff --git a/OpenNest.IO/EntitySerializer.cs b/OpenNest.IO/EntitySerializer.cs index 0e1e5ae6..5dd07b4a 100644 --- a/OpenNest.IO/EntitySerializer.cs +++ b/OpenNest.IO/EntitySerializer.cs @@ -191,6 +191,8 @@ namespace OpenNest.IO return SpecialLayers.Break; if (string.Equals(name, SpecialLayers.Tab.Name, StringComparison.OrdinalIgnoreCase)) return SpecialLayers.Tab; + if (string.Equals(name, SpecialLayers.DisplayPerimeter.Name, StringComparison.OrdinalIgnoreCase)) + return SpecialLayers.DisplayPerimeter; return new Layer(name); } diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index f0435b61..9ead35b8 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -720,6 +720,9 @@ namespace OpenNest.IO case LayerType.Tab: return ":TAB"; + case LayerType.DisplayPerimeter: + return ":DISPLAYPERIMETER"; + default: return string.Empty; } diff --git a/OpenNest.IO/ProgramReader.cs b/OpenNest.IO/ProgramReader.cs index 773d4783..d1154705 100644 --- a/OpenNest.IO/ProgramReader.cs +++ b/OpenNest.IO/ProgramReader.cs @@ -288,6 +288,10 @@ namespace OpenNest.IO layer = LayerType.Tab; break; + case "DISPLAYPERIMETER": + layer = LayerType.DisplayPerimeter; + break; + case "SUPPRESSED": suppressed = true; break; @@ -394,6 +398,10 @@ namespace OpenNest.IO layer = LayerType.Tab; break; + case "DISPLAYPERIMETER": + layer = LayerType.DisplayPerimeter; + break; + case "SUPPRESSED": suppressed = true; break;