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.
This commit is contained in:
aj committed 2026-10-22 19:34:52 -04:00
1 parent 6e3705eb89
commit b45e9df593
4 files changed
+68

No files matched your search

@@ -0,0 +1,55 @@
using OpenNest.Bending;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.TabbedCutouts;
namespace OpenNest.IO.Tests;
/// <summary>A supplied blank's display perimeter must survive save/reload as never-cut geometry.</summary>
public class DisplayPerimeterPersistenceTests
{
private static Drawing Door()
{
var outline = new List<Entity>
{
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<Entity>(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<Bend>(), 1, null, null, null, new List<TabbedCutout>());
}
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));
}
}
+2
View File
@@ -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);
}
+3
View File
@@ -720,6 +720,9 @@ namespace OpenNest.IO
case LayerType.Tab:
return ":TAB";
case LayerType.DisplayPerimeter:
return ":DISPLAYPERIMETER";
default:
return string.Empty;
}
+8
View File
@@ -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;