From 04d538b0a4a84ea06462e6bdb2c662a2198c3258 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Tue, 13 Oct 2026 17:07:46 -0400 Subject: [PATCH] fix(io): omit removedClamps from sheets without a removal Every plate was written with "removedClamps": [], so files without the feature changed. The key is now omitted unless a clamp was removed, like the other additive sections; nest-file-format.md documents removedClamps and removalMinutes and their lenient reading. PM 6ded8ac4-84fd-4e8b-9a3b-6f41e494542c item (c) --- OpenNest.IO.Tests/ClampRailPersistenceTests.cs | 16 ++++++++++++++-- OpenNest.IO/NestFormat.cs | 5 +++-- OpenNest.IO/NestWriter.cs | 12 ++++++++---- docs/nest-file-format.md | 8 ++++++++ 4 files changed, 33 insertions(+), 8 deletions(-) diff --git a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs index 33e0b63..49e0c8f 100644 --- a/OpenNest.IO.Tests/ClampRailPersistenceTests.cs +++ b/OpenNest.IO.Tests/ClampRailPersistenceTests.cs @@ -236,8 +236,10 @@ public class ClampRailPersistenceTests [Fact] public void LegacyFileWithoutRemovedClampsKeyLoadsWithNoneRemoved() { - var withoutKey = RewriteNestJson(Write(RailNest()), - text => RemoveJsonProperty(text, "removedClamps")); + // Sheets without a removal are written without the key, exactly like a file from + // before the feature. + var withoutKey = Write(RailNest()); + Assert.DoesNotContain("\"removedClamps\"", ReadNestJson(withoutKey), StringComparison.Ordinal); var reopened = new NestReader(withoutKey).Read(); @@ -309,6 +311,16 @@ public class ClampRailPersistenceTests Assert.NotNull(plate.ClampRail); } + // PM 6ded8ac4 (c): sheets without a removal write no removedClamps key at all. + [Fact] + public void SheetsWithoutRemovedClampsWriteNoRemovedClampsKey() + { + var json = ReadNestJson(Write(RailNest())); + + Assert.DoesNotContain("\"removedClamps\"", json, StringComparison.Ordinal); + Assert.Contains("\"removedClamps\"", ReadNestJson(RemovedClamp3Nest()), StringComparison.Ordinal); + } + /// 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 3a12483..e9c9b83 100644 --- a/OpenNest.IO/NestFormat.cs +++ b/OpenNest.IO/NestFormat.cs @@ -206,11 +206,12 @@ namespace OpenNest.IO public List Clamps { get; init; } = new(); /// - /// Rail clamp ids removed from this sheet as a last resort (D8). Missing or empty - /// means none were removed; already excludes them. Read + /// Rail clamp ids removed from this sheet as a last resort (D8). Omitted when none + /// were removed; missing or empty means none; already excludes them. Read /// leniently: a wrongly shaped value reads as none removed, non-string entries /// are skipped, and the saved zones load either way. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonConverter(typeof(LenientStringListConverter))] public List RemovedClamps { get; init; } = new(); } diff --git a/OpenNest.IO/NestWriter.cs b/OpenNest.IO/NestWriter.cs index c6d2015..2eec5f3 100644 --- a/OpenNest.IO/NestWriter.cs +++ b/OpenNest.IO/NestWriter.cs @@ -334,10 +334,14 @@ namespace OpenNest.IO Width = z.Bounds.Width, }) .ToList(), - RemovedClamps = plate.RemovedClamps - .OrderBy(ClampIndex) - .ThenBy(id => id, StringComparer.Ordinal) - .ToList(), + // Omitted unless a clamp was removed (files without the feature + // stay unchanged). + RemovedClamps = plate.RemovedClamps.Count == 0 + ? null + : plate.RemovedClamps + .OrderBy(ClampIndex) + .ThenBy(id => id, StringComparer.Ordinal) + .ToList(), } ); } diff --git a/docs/nest-file-format.md b/docs/nest-file-format.md index e1ac906..d454ced 100644 --- a/docs/nest-file-format.md +++ b/docs/nest-file-format.md @@ -80,6 +80,14 @@ malformed rail loads as none. On load a plate follows the rail again only when its saved `clamps` are exactly the rail's zones for that sheet; any other saved zones are kept as written. +The rail's optional `removalMinutes` is the time to take one clamp off and put +it back; a missing or invalid value loads as five minutes. A plate on which +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. + 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 new plate from the saved defaults would recreate it (same size and quadrant,