mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-20 07:11:38 -04:00
fix(io): harden clamp rail persistence
Omit an empty clamp-carrying plate only when a new plate from the saved defaults would recreate it: same size and quadrant and zones the saved rail recreates. A rail-following empty sheet with another size or quadrant is written again instead of being lost. Read plateDefaults.clampRail through a lenient converter so a wrongly typed rail (a string or null width, non-numeric centers, an array in place of the object) loads as no rail while every plate's saved zones still load, as the file format documents, instead of failing the whole nest. Review findings 4 and 5, Project Memory 4e5f566d.
This commit is contained in:
1 parent
76b49677c4
commit
1e37c661ff
4 files changed
+106
-8
No files matched your search
@@ -71,6 +71,25 @@ public class ClampRailPersistenceTests
|
||||
Assert.Null(kept.ClampRail); // preserved as saved, not re-resolved
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EmptyRailSheetThatDiffersFromThePlateDefaultsIsKept()
|
||||
{
|
||||
// The rail recreates its zones, but nothing recreates this sheet's size and quadrant.
|
||||
var nest = RailNest(withEmptyTail: false);
|
||||
var odd = nest.CreatePlate();
|
||||
odd.Size = new Size(48, 60);
|
||||
odd.Quadrant = 3;
|
||||
|
||||
var reopened = RoundTrip(nest);
|
||||
|
||||
Assert.Equal(2, reopened.Plates.Count);
|
||||
var kept = reopened.Plates[1];
|
||||
Assert.Equal(new Size(48, 60), kept.Size);
|
||||
Assert.Equal(3, kept.Quadrant);
|
||||
Assert.Equal(4, kept.ClampZones.Count);
|
||||
Assert.Same(reopened.PlateDefaults.ClampRail, kept.ClampRail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SavedZonesThatDifferFromTheRailArePreservedExactly()
|
||||
{
|
||||
@@ -116,6 +135,12 @@ public class ClampRailPersistenceTests
|
||||
[Theory]
|
||||
[InlineData("\"width\": 1.5", "\"width\": -1.5")]
|
||||
[InlineData("\"centers\": [", "\"centers\": [-6, ")]
|
||||
[InlineData("\"width\": 1.5", "\"width\": \"bad\"")]
|
||||
[InlineData("\"width\": 1.5", "\"width\": null")]
|
||||
[InlineData("\"centers\": [", "\"centers\": [\"bad\", ")]
|
||||
[InlineData("\"centers\": [", "\"centers\": [null, ")]
|
||||
[InlineData("\"centers\": [", "\"centers\": 6, \"unused\": [")]
|
||||
[InlineData("\"clampRail\": {", "\"clampRail\": [6], \"unused\": {")]
|
||||
public void MalformedRailIsIgnoredWhileTheNestLoads(string find, string replace)
|
||||
{
|
||||
var tampered = RewriteNestJson(Write(RailNest()), json =>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -50,9 +51,68 @@ namespace OpenNest.IO
|
||||
/// has none; older files and older readers simply have no rail.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
[JsonConverter(typeof(LenientClampRailConverter))]
|
||||
public ClampRailDto ClampRail { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads a malformed or wrongly typed clamp rail as no rail instead of failing the
|
||||
/// whole nest; the reader still validates the values (sign, size) it receives.
|
||||
/// </summary>
|
||||
public sealed class LenientClampRailConverter : JsonConverter<ClampRailDto>
|
||||
{
|
||||
public override ClampRailDto Read(
|
||||
ref Utf8JsonReader reader,
|
||||
Type typeToConvert,
|
||||
JsonSerializerOptions options
|
||||
)
|
||||
{
|
||||
using var document = JsonDocument.ParseValue(ref reader);
|
||||
var rail = document.RootElement;
|
||||
if (
|
||||
rail.ValueKind != JsonValueKind.Object
|
||||
|| !TryNumber(rail, "width", out var width)
|
||||
|| !TryNumber(rail, "depth", out var depth)
|
||||
|| !rail.TryGetProperty("centers", out var centers)
|
||||
|| centers.ValueKind != JsonValueKind.Array
|
||||
)
|
||||
return null;
|
||||
|
||||
var list = new List<double>();
|
||||
foreach (var center in centers.EnumerateArray())
|
||||
{
|
||||
if (center.ValueKind != JsonValueKind.Number || !center.TryGetDouble(out var x))
|
||||
return null;
|
||||
list.Add(x);
|
||||
}
|
||||
return new ClampRailDto { Width = width, Depth = depth, Centers = list };
|
||||
}
|
||||
|
||||
public override void Write(
|
||||
Utf8JsonWriter writer,
|
||||
ClampRailDto value,
|
||||
JsonSerializerOptions options
|
||||
)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
writer.WriteNumber("width", value.Width);
|
||||
writer.WriteNumber("depth", value.Depth);
|
||||
writer.WriteStartArray("centers");
|
||||
foreach (var center in value.Centers ?? new List<double>())
|
||||
writer.WriteNumberValue(center);
|
||||
writer.WriteEndArray();
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
private static bool TryNumber(JsonElement rail, string name, out double value)
|
||||
{
|
||||
value = 0;
|
||||
return rail.TryGetProperty(name, out var element)
|
||||
&& element.ValueKind == JsonValueKind.Number
|
||||
&& element.TryGetDouble(out value);
|
||||
}
|
||||
}
|
||||
|
||||
public record ClampRailDto
|
||||
{
|
||||
/// <summary>Clamp extent along the origin-side edge (X).</summary>
|
||||
|
||||
@@ -205,8 +205,19 @@ namespace OpenNest.IO
|
||||
return list;
|
||||
}
|
||||
|
||||
private bool RailRecreates(Plate plate) =>
|
||||
nest.PlateDefaults.ClampRail is { } rail && rail.Matches(plate);
|
||||
/// <summary>
|
||||
/// True when a plate created from the saved defaults would be this sheet's clamp
|
||||
/// configuration: same size and quadrant, and zones the saved rail recreates.
|
||||
/// </summary>
|
||||
private bool DefaultsRecreateClamps(Plate plate)
|
||||
{
|
||||
var defaults = nest.PlateDefaults;
|
||||
return defaults.ClampRail is { } rail
|
||||
&& plate.Size.Width == defaults.Size.Width
|
||||
&& plate.Size.Length == defaults.Size.Length
|
||||
&& plate.Quadrant == defaults.Quadrant
|
||||
&& rail.Matches(plate);
|
||||
}
|
||||
|
||||
private List<PlateDto> BuildPlateDtos()
|
||||
{
|
||||
@@ -216,12 +227,12 @@ namespace OpenNest.IO
|
||||
{
|
||||
var plate = nest.Plates[i];
|
||||
|
||||
// An empty plate is omitted (this also drops the editor's trailing sentinel)
|
||||
// unless that would lose clamp zones the saved rail cannot recreate.
|
||||
// An empty plate is omitted unless that would lose a clamp configuration the
|
||||
// saved defaults cannot recreate; the editor's trailing empty sheet is dropped.
|
||||
if (
|
||||
plate.Parts.Count(p => !p.BaseDrawing.IsCutOff) == 0
|
||||
&& plate.CutOffs.Count == 0
|
||||
&& (plate.ClampZones.Count == 0 || RailRecreates(plate))
|
||||
&& (plate.ClampZones.Count == 0 || DefaultsRecreateClamps(plate))
|
||||
)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -80,9 +80,11 @@ its saved `clamps` are exactly the rail's zones for that sheet; any other saved
|
||||
zones are kept as written.
|
||||
|
||||
An empty plate (no parts, no cut-offs) is omitted from the file unless that
|
||||
would lose clamp zones: a plate whose zones the saved rail recreates is
|
||||
omitted like any other empty plate (this keeps the editor's trailing empty
|
||||
sheet out of the file), while one carrying other zones is written.
|
||||
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,
|
||||
zones the saved rail recreates), which keeps the editor's trailing empty sheet
|
||||
out of the file. An empty plate with other zones, or a rail-following one
|
||||
whose size or quadrant differs from the defaults, is written.
|
||||
|
||||
## Drawing edits
|
||||
|
||||
|
||||
Reference in new issue
Block a user