diff --git a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs index 800a6553..8fec6b8c 100644 --- a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs +++ b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs @@ -424,6 +424,32 @@ public class ClampRailPersistenceTests Assert.Contains("\"removedClamps\"", ReadNestJson(RemovedClamp3Nest()), StringComparison.Ordinal); } + // PM 22daec3d: a sheet following a rail other than the nest default keeps its rail, + // removed clamps and removal time through save/reload. + [Fact] + public void SheetRailDifferentFromTheDefaultsSurvivesReload() + { + var nest = new Nest("no-default-rail"); + nest.PlateDefaults.Size = new Size(48, 120); + var plate = nest.CreatePlate(); + plate.SetClampRail(Machine(), new[] { "clamp-3" }); + + var reopened = Assert.Single(RoundTrip(nest).Plates); + + Assert.Equal(Machine(), reopened.ClampRail); + Assert.Equal(new[] { "clamp-3" }, reopened.RemovedClamps); + Assert.Equal(ClampRail.DefaultRemovalMinutes, reopened.ClampRemovalMinutes); + Assert.Null(RoundTrip(nest).PlateDefaults.ClampRail); + } + + [Fact] + public void SheetsFollowingTheDefaultRailWriteNoPerSheetRail() + { + var json = ReadNestJson(RemovedClamp3Nest()); + var root = JsonNode.Parse(json)!; + Assert.Null(root["plates"]![0]!["clampRail"]); + } + /// Removes a named property wherever it appears in the document, simulating a /// key an older writer never emitted. Rebuilds via so the result /// is always valid JSON regardless of whether the property was first, last or only one diff --git a/OpenNest.IO/NestFormat.cs b/OpenNest.IO/NestFormat.cs index e9c9b832..51262f5a 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -214,6 +214,15 @@ namespace OpenNest.IO [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonConverter(typeof(LenientStringListConverter))] public List RemovedClamps { get; init; } = new(); + + /// + /// The machine clamp rail this sheet follows, written only when it is not the + /// nest's default rail (otherwise the sheet re-attaches to that one on load). + /// Read leniently like . + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonConverter(typeof(LenientClampRailConverter))] + public ClampRailDto ClampRail { get; init; } } public record ClampZoneDto diff --git a/OpenNest.IO/NestReader.cs b/OpenNest.IO/NestReader.cs index 9aff8a66..a54ba1e8 100644 --- a/OpenNest.IO/NestReader.cs +++ b/OpenNest.IO/NestReader.cs @@ -452,8 +452,10 @@ namespace OpenNest.IO .Where(id => !string.IsNullOrEmpty(id)) .ToHashSet(StringComparer.Ordinal) ?? new HashSet(); - if (clampRail != null && clampRail.Matches(plate, removedClamps)) - plate.SetClampRail(clampRail, removedClamps); + // A sheet saved with its own rail (not the nest default) follows that one. + var plateRail = ReadClampRail(p.ClampRail) ?? clampRail; + if (plateRail != null && plateRail.Matches(plate, removedClamps)) + plate.SetClampRail(plateRail, removedClamps); for (var partIndex = 0; partIndex < p.Parts.Count; partIndex++) { diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index 20e4df94..463ce888 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -135,15 +135,7 @@ namespace OpenNest.IO Right = pd.EdgeSpacing.Right, Bottom = pd.EdgeSpacing.Bottom, }, - ClampRail = pd.ClampRail == null - ? null - : new ClampRailDto - { - Width = pd.ClampRail.Width, - Depth = pd.ClampRail.Depth, - Centers = pd.ClampRail.Centers.ToList(), - RemovalMinutes = pd.ClampRail.RemovalMinutes, - }, + ClampRail = ToDto(pd.ClampRail), }; } @@ -222,6 +214,17 @@ namespace OpenNest.IO && rail.Matches(plate); } + private static ClampRailDto ToDto(ClampRail rail) => + rail == null + ? null + : new ClampRailDto + { + Width = rail.Width, + Depth = rail.Depth, + Centers = rail.Centers.ToList(), + RemovalMinutes = rail.RemovalMinutes, + }; + /// Numeric suffix of a rail clamp id ("clamp-3" -> 3), for a natural sort of /// instead of lexical ("clamp-10" before "clamp-2"). /// A non-matching id sorts last by its own ordinal text. @@ -343,6 +346,10 @@ namespace OpenNest.IO .OrderBy(ClampIndex) .ThenBy(id => id, StringComparer.Ordinal) .ToList(), + // Only a rail other than the nest default needs saving per sheet. + ClampRail = Equals(plate.ClampRail, nest.PlateDefaults.ClampRail) + ? null + : ToDto(plate.ClampRail), } ); } diff --git a/docs/nest-file-format.md b/docs/nest-file-format.md index 2040d3ff..865eddac 100644 --- a/docs/nest-file-format.md +++ b/docs/nest-file-format.md @@ -86,7 +86,9 @@ Auto Nest removed clamps as a last resort lists their rail ids in an optional `removedClamps` array (omitted when none were removed); its `clamps` already exclude them, and it follows the rail minus those ids. A malformed `removedClamps` value reads as none removed (non-string entries are skipped); -the saved `clamps` load either way. +the saved `clamps` load either way. A plate that follows a rail other than the nest's default +saves it in its own optional `clampRail` (same shape), so its removal state and +time survive a reload; plates following the default rail omit it. An empty plate (no parts, no cut-offs) is omitted from the file unless that would lose clamp zones: it is omitted like any other empty plate only when a