Files
OpenNest/OpenNest.Tests/CutOffs/CutOffSerializationTests.cs
T
ajandClaude Opus 5.5 d29f919aa2 fix(io): save nests safely and reopen them with their cut-off settings
Reopening a nest regenerated its cut-offs with built-in defaults (no
overtravel, a different minimum segment) rather than the machine's
settings it was made with, so a nest saved, reopened and posted could
cut scrap that never separates. Plates now record the settings their
cut-offs were last generated with, and files carry them; older files
still load with the defaults.

Saving over a nest file truncated it before writing, so any failure
part way lost the only good copy, and the error reached the user as an
unhandled exception after the save path had already changed. Nest and
settings files are now written beside the target and swapped in, and a
failed save is reported with the old file left intact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:51:11 -05:00

247 lines
9.9 KiB
C#

using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.CutOffs;
public class CutOffSerializationTests
{
[Fact]
public void RoundTrip_SegmentsKeepTheirOwnPlacesInTheSequence()
{
var nest = new Nest();
var drawing = TestHelpers.MakeSquareDrawing();
nest.Drawings.Add(drawing);
var lower = new Part(drawing, new Vector(10, 10));
var upper = new Part(drawing, new Vector(10, 30));
var plate = TestHelpers.MakePlate(60, 120, lower, upper);
nest.Plates.Add(plate);
var cutOff = new CutOff(new Vector(15, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(cutOff);
plate.RegenerateCutOffs(new CutOffSettings());
var segments = CutOffSegments.Of(plate, cutOff).Select(s => s.Part).ToArray();
plate.Parts.Reorder([segments[0], lower, segments[1], upper, segments[2]]);
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest).Write(stream));
stream.Position = 0;
using (var archive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Read, leaveOpen: true))
using (var reader = new StreamReader(archive.GetEntry("nest.json")!.Open()))
using (var json = System.Text.Json.JsonDocument.Parse(reader.ReadToEnd()))
{
var saved = Assert.Single(Assert.Single(json.RootElement.GetProperty("plates").EnumerateArray())
.GetProperty("cutOffs").EnumerateArray());
Assert.Equal(0, saved.GetProperty("sequence").GetInt32());
Assert.Equal(new[] { 0, 2, 4 }, saved.GetProperty("segments").EnumerateArray().Select(e => e.GetInt32()));
}
stream.Position = 0;
var loaded = new NestReader(stream).Read();
var loadedPlate = Assert.Single(loaded.Plates);
var loadedCutOff = Assert.Single(loadedPlate.CutOffs);
var loadedSegments = CutOffSegments.Of(loadedPlate, loadedCutOff);
Assert.Equal(3, loadedSegments.Count);
Assert.Equal(new[] { true, false, true, false, true }, loadedPlate.Parts.Select(p => p.BaseDrawing.IsCutOff));
Assert.Equal(new[] { 0, 2, 4 }, loadedSegments.Select(s => loadedPlate.Parts.IndexOf(s.Part)));
Assert.True(loadedSegments[0].End < loadedSegments[1].Start && loadedSegments[1].End < loadedSegments[2].Start);
}
[Fact]
public void RoundTrip_GapsAreKeptAndWrittenOnlyWhenPresent()
{
var nest = new Nest();
var plate = TestHelpers.MakePlate(48, 96);
nest.Plates.Add(plate);
var gapped = new CutOff(new Vector(0, 20), CutOffAxis.Horizontal) { Gaps = [new CutOffGap(35, 68.6)] };
var plain = new CutOff(new Vector(35, 0), CutOffAxis.Vertical);
plate.CutOffs.Add(gapped);
plate.CutOffs.Add(plain);
plate.RegenerateCutOffs(new CutOffSettings());
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest) { KeepEmptyPlates = true }.Write(stream));
stream.Position = 0;
using (var archive = new System.IO.Compression.ZipArchive(stream, System.IO.Compression.ZipArchiveMode.Read, leaveOpen: true))
using (var reader = new StreamReader(archive.GetEntry("nest.json")!.Open()))
using (var json = System.Text.Json.JsonDocument.Parse(reader.ReadToEnd()))
{
var saved = Assert.Single(json.RootElement.GetProperty("plates").EnumerateArray())
.GetProperty("cutOffs").EnumerateArray().ToArray();
var gap = Assert.Single(saved[0].GetProperty("gaps").EnumerateArray());
Assert.Equal(35, gap.GetProperty("start").GetDouble());
Assert.Equal(68.6, gap.GetProperty("end").GetDouble());
Assert.False(saved[1].TryGetProperty("gaps", out _));
}
stream.Position = 0;
var loaded = Assert.Single(new NestReader(stream).Read().Plates);
Assert.Equal(new[] { new CutOffGap(35, 68.6) }, loaded.CutOffs[0].Gaps);
Assert.Empty(loaded.CutOffs[1].Gaps);
Assert.Equal(2, CutOffSegments.Of(loaded, loaded.CutOffs[0]).Count);
}
[Fact]
public void RoundTrip_RegeneratesWithTheSavedSettingsNotTheDefaults()
{
var nest = new Nest();
var plate = TestHelpers.MakePlate(48, 96);
nest.Plates.Add(plate);
plate.CutOffs.Add(new CutOff(new Vector(35, 0), CutOffAxis.Vertical));
var machine = new CutOffSettings
{
Overtravel = 0.75,
MinSegmentLength = 0.5,
PartClearance = 0.1,
CutDirection = CutDirection.TowardOrigin,
};
plate.RegenerateCutOffs(machine);
var before = Program(plate);
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest) { KeepEmptyPlates = true }.Write(stream));
stream.Position = 0;
var loaded = Assert.Single(new NestReader(stream).Read().Plates);
// The defaults have no overtravel, so a reader using them would not cut through.
Assert.Equal(before, Program(loaded));
Assert.Equal(0.75, loaded.CutOffSettings.Overtravel);
Assert.Equal(CutDirection.TowardOrigin, loaded.CutOffSettings.CutDirection);
static string Program(Plate plate) => string.Join(";", plate.Parts.Where(p => p.BaseDrawing.IsCutOff)
.SelectMany(p => p.Program.Codes).OfType<Motion>().Select(m => m.EndPoint.ToString()));
}
[Fact]
public void Read_WithoutSavedSettings_UsesTheDefaults()
{
var nest = new Nest();
var plate = TestHelpers.MakePlate(48, 96);
nest.Plates.Add(plate);
plate.CutOffs.Add(new CutOff(new Vector(35, 0), CutOffAxis.Vertical));
plate.RegenerateCutOffs(new CutOffSettings { Overtravel = 0.75 });
plate.CutOffSettings = null;
using var stream = new MemoryStream();
Assert.True(new NestWriter(nest) { KeepEmptyPlates = true }.Write(stream));
stream.Position = 0;
var loaded = Assert.Single(new NestReader(stream).Read().Plates);
Assert.Equal(0, loaded.CutOffSettings.Overtravel);
}
[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);
}
}