fix(io): keep a sheet's own clamp rail and its removals through save and reload

Commit can attach the stock's rail to a sheet whose nest has no (or another)
default rail, but only the default rail was saved and the reader re-attached
sheets only to it, so the removed clamp and its time vanished on reload. A
sheet following a rail other than the default now saves it in an optional
per-plate clampRail (read leniently, omitted otherwise), and the reader
re-attaches the sheet to it.

PM 22daec3d-dabb-4aae-95f7-c1d090d5dcdf
This commit is contained in:
aj committed 2026-10-13 18:15:50 -04:00
1 parent 4226c0f902
commit d9273c889f
5 files changed
+58 -12

No files matched your search

@@ -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"]);
}
/// <summary>Removes a named property wherever it appears in the document, simulating a
/// key an older writer never emitted. Rebuilds via <see cref="JsonNode"/> so the result
/// is always valid JSON regardless of whether the property was first, last or only one
+9
View File
@@ -214,6 +214,15 @@ namespace OpenNest.IO
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientStringListConverter))]
public List<string> RemovedClamps { get; init; } = new();
/// <summary>
/// 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 <see cref="PlateDefaultsDto.ClampRail"/>.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientClampRailConverter))]
public ClampRailDto ClampRail { get; init; }
}
public record ClampZoneDto
+4 -2
View File
@@ -452,8 +452,10 @@ namespace OpenNest.IO
.Where(id => !string.IsNullOrEmpty(id))
.ToHashSet(StringComparer.Ordinal)
?? new HashSet<string>();
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++)
{
+16 -9
View File
@@ -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,
};
/// <summary>Numeric suffix of a rail clamp id ("clamp-3" -&gt; 3), for a natural sort of
/// <see cref="PlateDto.RemovedClamps"/> instead of lexical ("clamp-10" before "clamp-2").
/// A non-matching id sorts last by its own ordinal text.</summary>
@@ -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),
}
);
}
+3 -1
View File
@@ -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