mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-01 08:01:34 -05:00
A clamp rail can now carry its own Clearance, the gap parts keep from every clamp footprint, independent of part spacing. Unset, it keeps the sheet's bottom edge spacing as before. Clamp blocks drawn under the sheet can use close to zero so the laser only stays off them. Plate.ClampClearance and NestPlateStock.ClampClearance resolve it once; the engines, interactive fill/pack/push, the layout check, Expander walls and automatic cut-off placement all read it. The interactive push now holds clamps at their square clearance keep-out (the layout check's shape) instead of the part spacing, so parts no longer tuck into its corners; the clamped fill characterization digests are re-pinned after checking the new layouts validate. Persisted as an optional `clearance` on the .nest clamp rail and the machine clamp settings; edited in Nest > Clamps and the machine's Clamps tab. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1076 lines
46 KiB
C#
1076 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace OpenNest.IO
|
|
{
|
|
public static class NestFormat
|
|
{
|
|
public const string FileExtension = ".nest";
|
|
public const string FileFilter = "Nest Files (*.nest)|*.nest";
|
|
|
|
public static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
WriteIndented = true,
|
|
};
|
|
|
|
public record NestDto
|
|
{
|
|
public int Version { get; init; } = 2;
|
|
public string Name { get; init; } = "";
|
|
public string Units { get; init; } = "Inches";
|
|
public string Customer { get; init; } = "";
|
|
public string DateCreated { get; init; } = "";
|
|
public string DateLastModified { get; init; } = "";
|
|
public string Notes { get; init; } = "";
|
|
public string AssistGas { get; init; } = "";
|
|
public string Status { get; init; } = "Quote";
|
|
public string MadeBy { get; init; } = "";
|
|
|
|
/// <summary>
|
|
/// The machine the nest is cut on. Omitted when none is assigned; older files and
|
|
/// older readers simply have none.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientMachineConverter))]
|
|
public MachineDto Machine { get; init; }
|
|
|
|
public double Thickness { get; init; }
|
|
public MaterialDto Material { get; init; } = new();
|
|
public PlateDefaultsDto PlateDefaults { get; init; } = new();
|
|
public List<DrawingDto> Drawings { get; init; } = new();
|
|
public List<PlateDto> Plates { get; init; } = new();
|
|
public List<PlateOptionDto> PlateOptions { get; init; } = new();
|
|
public double SalvageRate { get; init; } = 0.5;
|
|
|
|
/// <summary>
|
|
/// Smallest remnant side in model units. Missing in older files, which load the Auto
|
|
/// Nest default.
|
|
/// </summary>
|
|
public double MinimumRemnantDimension { get; init; } = Nest.DefaultMinimumRemnantDimension;
|
|
}
|
|
|
|
public record PlateDefaultsDto
|
|
{
|
|
public SizeDto Size { get; init; } = new();
|
|
public double Thickness { get; init; }
|
|
public int Quadrant { get; init; } = 1;
|
|
public double PartSpacing { get; init; }
|
|
public MaterialDto Material { get; init; } = new();
|
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// The nest's machine clamp rail, which new plates follow. Omitted when the nest
|
|
/// 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>
|
|
/// The machine support bed every sheet of the nest rests on (docs/support-tabs.md).
|
|
/// Omitted when the nest has none; older files and older readers simply have none.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientSupportBedConverter))]
|
|
public SupportBedDto SupportBed { get; init; }
|
|
}
|
|
|
|
/// <summary>A machine configuration's id and the name it had when the nest was saved.</summary>
|
|
public record MachineDto
|
|
{
|
|
public Guid Id { get; init; }
|
|
public string Name { get; init; } = "";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a malformed or wrongly typed machine, or one without a valid id, as none instead
|
|
/// of failing the whole nest. A missing or wrongly typed name reads as empty.
|
|
/// </summary>
|
|
public sealed class LenientMachineConverter : JsonConverter<MachineDto>
|
|
{
|
|
public override MachineDto Read(ref Utf8JsonReader reader, Type typeToConvert,
|
|
JsonSerializerOptions options)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
var machine = document.RootElement;
|
|
if (machine.ValueKind != JsonValueKind.Object
|
|
|| !machine.TryGetProperty("id", out var id) || id.ValueKind != JsonValueKind.String
|
|
|| !Guid.TryParse(id.GetString(), out var guid) || guid == Guid.Empty)
|
|
return null;
|
|
var name = machine.TryGetProperty("name", out var nameElement) && nameElement.ValueKind == JsonValueKind.String
|
|
? nameElement.GetString()
|
|
: "";
|
|
return new MachineDto { Id = guid, Name = name };
|
|
}
|
|
|
|
public override void Write(Utf8JsonWriter writer, MachineDto value, JsonSerializerOptions options)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("id", value.Id);
|
|
writer.WriteString("name", value.Name ?? "");
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A support bed layout. <see cref="Type"/> names the layout: <c>grid</c>, a regular grid
|
|
/// of contact points (pitch and offset along X and Y), or <c>slats</c>, straight slats
|
|
/// running along <see cref="Axis"/> (<see cref="Pitch"/>, <see cref="Thickness"/>,
|
|
/// <see cref="Offset"/>). Lengths are in the nest's units.
|
|
/// </summary>
|
|
public record SupportBedDto
|
|
{
|
|
public string Type { get; init; } = "grid";
|
|
public double PitchX { get; init; }
|
|
public double PitchY { get; init; }
|
|
public double OffsetX { get; init; }
|
|
public double OffsetY { get; init; }
|
|
public string Axis { get; init; }
|
|
public double Pitch { get; init; }
|
|
public double Thickness { get; init; }
|
|
public double Offset { get; init; }
|
|
public double RegistrationTolerance { get; init; }
|
|
public double StabilityMargin { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a malformed, wrongly typed or unknown support bed as none instead of failing the
|
|
/// whole nest; the reader still validates the values it receives. Optional lengths
|
|
/// default to zero.
|
|
/// </summary>
|
|
public sealed class LenientSupportBedConverter : JsonConverter<SupportBedDto>
|
|
{
|
|
private static readonly string[] Shared = { "registrationTolerance", "stabilityMargin" };
|
|
private static readonly string[] GridOffsets = { "offsetX", "offsetY" };
|
|
private static readonly string[] SlatOffset = { "offset" };
|
|
|
|
public override SupportBedDto Read(ref Utf8JsonReader reader, Type typeToConvert,
|
|
JsonSerializerOptions options)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
var bed = document.RootElement;
|
|
if (bed.ValueKind != JsonValueKind.Object
|
|
|| !bed.TryGetProperty("type", out var type) || type.ValueKind != JsonValueKind.String
|
|
|| !TryOptional(bed, Shared, out var shared))
|
|
return null;
|
|
switch (type.GetString())
|
|
{
|
|
case "grid":
|
|
if (!TryNumber(bed, "pitchX", out var pitchX) || !TryNumber(bed, "pitchY", out var pitchY)
|
|
|| !TryOptional(bed, GridOffsets, out var offsets))
|
|
return null;
|
|
return new SupportBedDto
|
|
{
|
|
PitchX = pitchX,
|
|
PitchY = pitchY,
|
|
OffsetX = offsets[0],
|
|
OffsetY = offsets[1],
|
|
RegistrationTolerance = shared[0],
|
|
StabilityMargin = shared[1],
|
|
};
|
|
case "slats":
|
|
if (!bed.TryGetProperty("axis", out var axis) || axis.ValueKind != JsonValueKind.String
|
|
|| !TryNumber(bed, "pitch", out var pitch) || !TryNumber(bed, "thickness", out var thickness)
|
|
|| !TryOptional(bed, SlatOffset, out var offset))
|
|
return null;
|
|
return new SupportBedDto
|
|
{
|
|
Type = "slats",
|
|
Axis = axis.GetString(),
|
|
Pitch = pitch,
|
|
Thickness = thickness,
|
|
Offset = offset[0],
|
|
RegistrationTolerance = shared[0],
|
|
StabilityMargin = shared[1],
|
|
};
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public override void Write(Utf8JsonWriter writer, SupportBedDto value, JsonSerializerOptions options)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteString("type", value.Type);
|
|
if (value.Type == "slats")
|
|
{
|
|
writer.WriteString("axis", value.Axis);
|
|
writer.WriteNumber("pitch", value.Pitch);
|
|
writer.WriteNumber("thickness", value.Thickness);
|
|
writer.WriteNumber("offset", value.Offset);
|
|
}
|
|
else
|
|
{
|
|
writer.WriteNumber("pitchX", value.PitchX);
|
|
writer.WriteNumber("pitchY", value.PitchY);
|
|
writer.WriteNumber("offsetX", value.OffsetX);
|
|
writer.WriteNumber("offsetY", value.OffsetY);
|
|
}
|
|
writer.WriteNumber("registrationTolerance", value.RegistrationTolerance);
|
|
writer.WriteNumber("stabilityMargin", value.StabilityMargin);
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
// Optional lengths default to zero; one that is present must be a number.
|
|
private static bool TryOptional(JsonElement bed, string[] names, out double[] values)
|
|
{
|
|
values = new double[names.Length];
|
|
for (var i = 0; i < names.Length; i++)
|
|
if (bed.TryGetProperty(names[i], out _) && !TryNumber(bed, names[i], out values[i]))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
private static bool TryNumber(JsonElement bed, string name, out double value)
|
|
{
|
|
value = 0;
|
|
return bed.TryGetProperty(name, out var element)
|
|
&& element.ValueKind == JsonValueKind.Number
|
|
&& element.TryGetDouble(out value);
|
|
}
|
|
}
|
|
|
|
/// <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 = ReadCenters(centers);
|
|
if (list == null)
|
|
return null;
|
|
// Clamps on the other edges arrived later: a missing key means none there.
|
|
var others = new List<double>[OtherEdgeKeys.Length];
|
|
for (var i = 0; i < others.Length; i++)
|
|
{
|
|
others[i] = new List<double>();
|
|
if (!rail.TryGetProperty(OtherEdgeKeys[i], out var edge))
|
|
continue;
|
|
others[i] = edge.ValueKind == JsonValueKind.Array ? ReadCenters(edge) : null;
|
|
if (others[i] == null)
|
|
return null;
|
|
}
|
|
// Missing or unreadable -> null; the reader then falls back to the default.
|
|
double? removalMinutes = TryNumber(rail, "removalMinutes", out var minutes)
|
|
? minutes
|
|
: null;
|
|
double? clearance = TryNumber(rail, "clearance", out var gap) ? gap : null;
|
|
return new ClampRailDto
|
|
{
|
|
Width = width,
|
|
Depth = depth,
|
|
Centers = list,
|
|
FarSideCenters = others[0],
|
|
OriginEndCenters = others[1],
|
|
FarEndCenters = others[2],
|
|
RemovalMinutes = removalMinutes,
|
|
Clearance = clearance,
|
|
};
|
|
}
|
|
|
|
private static readonly string[] OtherEdgeKeys = { "farSideCenters", "originEndCenters", "farEndCenters" };
|
|
|
|
private static void WriteCenters(Utf8JsonWriter writer, string key, List<double> centers)
|
|
{
|
|
if (centers == null || centers.Count == 0)
|
|
return;
|
|
writer.WriteStartArray(key);
|
|
foreach (var center in centers)
|
|
writer.WriteNumberValue(center);
|
|
writer.WriteEndArray();
|
|
}
|
|
|
|
private static List<double> ReadCenters(JsonElement centers)
|
|
{
|
|
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 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();
|
|
// Omitted when empty so origin-side-only rails write exactly as before.
|
|
WriteCenters(writer, OtherEdgeKeys[0], value.FarSideCenters);
|
|
WriteCenters(writer, OtherEdgeKeys[1], value.OriginEndCenters);
|
|
WriteCenters(writer, OtherEdgeKeys[2], value.FarEndCenters);
|
|
if (value.RemovalMinutes is double removalMinutes)
|
|
writer.WriteNumber("removalMinutes", removalMinutes);
|
|
if (value.Clearance is double clearance)
|
|
writer.WriteNumber("clearance", clearance);
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads an optional list of strings without failing the nest: a non-array value
|
|
/// reads as an empty list and non-string entries are skipped.
|
|
/// </summary>
|
|
/// <summary>Writes derived data and skips it on read, whatever its shape.</summary>
|
|
public sealed class WriteOnlyConverter<T> : JsonConverter<T>
|
|
where T : class
|
|
{
|
|
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
{
|
|
reader.Skip();
|
|
return null;
|
|
}
|
|
|
|
// The converter sits on the property, not the type, so this does not recurse.
|
|
public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) =>
|
|
JsonSerializer.Serialize(writer, value, options);
|
|
}
|
|
|
|
public sealed class LenientStringListConverter : JsonConverter<List<string>>
|
|
{
|
|
public override List<string> Read(
|
|
ref Utf8JsonReader reader,
|
|
Type typeToConvert,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
var list = new List<string>();
|
|
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
|
return list;
|
|
foreach (var element in document.RootElement.EnumerateArray())
|
|
if (element.ValueKind == JsonValueKind.String)
|
|
list.Add(element.GetString());
|
|
return list;
|
|
}
|
|
|
|
public override void Write(
|
|
Utf8JsonWriter writer,
|
|
List<string> value,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
writer.WriteStartArray();
|
|
foreach (var item in value)
|
|
writer.WriteStringValue(item);
|
|
writer.WriteEndArray();
|
|
}
|
|
}
|
|
|
|
public record ClampRailDto
|
|
{
|
|
/// <summary>Clamp extent along its edge.</summary>
|
|
public double Width { get; init; }
|
|
|
|
/// <summary>Clamp intrusion into the sheet from its edge.</summary>
|
|
public double Depth { get; init; }
|
|
|
|
/// <summary>Origin-side clamp centers along X, measured from the plate origin.</summary>
|
|
public List<double> Centers { get; init; } = new();
|
|
|
|
/// <summary>Far-side clamp centers along X from the origin; omitted when none.</summary>
|
|
public List<double> FarSideCenters { get; init; } = new();
|
|
|
|
/// <summary>Origin-end clamp centers along Y from the origin; omitted when none.</summary>
|
|
public List<double> OriginEndCenters { get; init; } = new();
|
|
|
|
/// <summary>Far-end clamp centers along Y from the origin; omitted when none.</summary>
|
|
public List<double> FarEndCenters { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Minutes to remove one clamp and put it back after cutting (D9). Null in older
|
|
/// files and readers fall back to <see cref="OpenNest.ClampRail.DefaultRemovalMinutes"/>.
|
|
/// </summary>
|
|
public double? RemovalMinutes { get; init; }
|
|
|
|
/// <summary>
|
|
/// The gap parts keep from every clamp (<see cref="OpenNest.ClampRail.Clearance"/>).
|
|
/// Omitted when the rail uses the sheet's bottom edge spacing, as older files do.
|
|
/// </summary>
|
|
public double? Clearance { get; init; }
|
|
}
|
|
|
|
public record DrawingDto
|
|
{
|
|
public int Id { get; init; }
|
|
public string Name { get; init; } = "";
|
|
public string Customer { get; init; } = "";
|
|
public ColorDto Color { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// The color was chosen on purpose and survives scheme changes. Omitted when false;
|
|
/// readers recolor such drawings from the active palette.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public bool CustomColor { get; init; }
|
|
|
|
public QuantityDto Quantity { get; init; } = new();
|
|
public int Priority { get; init; }
|
|
|
|
/// <summary>Sequence this drawing's parts first. Omitted when false.</summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public bool CutFirst { get; init; }
|
|
|
|
public ConstraintsDto Constraints { get; init; } = new();
|
|
public MaterialDto Material { get; init; } = new();
|
|
public SourceDto Source { get; init; } = new();
|
|
public List<BendDto> Bends { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// The drawing library revision this drawing was copied from. Omitted when the
|
|
/// drawing did not come from a library. Read leniently: a malformed value reads
|
|
/// as <see cref="DrawingLibraryLinkDto.Malformed"/> and the reader drops it with a warning.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientDrawingLibraryLinkConverter))]
|
|
public DrawingLibraryLinkDto Library { get; init; }
|
|
|
|
/// <summary>
|
|
/// Drawing cut pins in the source frame. Omitted when there are none. Read leniently:
|
|
/// an entry that cannot be read becomes <see cref="CutPinDto.Malformed"/> and the
|
|
/// reader skips it with a warning; the other pins load.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientCutPinListConverter))]
|
|
public List<CutPinDto> CutPins { get; init; }
|
|
|
|
/// <summary>
|
|
/// Breakoffs in the source frame, already applied to the stored program. Omitted when
|
|
/// there are none.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public List<BreakoffDto> Breakoffs { get; init; }
|
|
|
|
/// <summary>
|
|
/// Tabbed cutouts in the source frame, already applied to the stored program. Omitted
|
|
/// when there are none.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public List<TabbedCutoutDto> TabbedCutouts { get; init; }
|
|
}
|
|
|
|
public record TabbedCutoutDto
|
|
{
|
|
public List<Guid> EdgeIds { get; init; } = new();
|
|
public double TabWidth { get; init; }
|
|
public List<double> TabCenters { get; init; } = new();
|
|
}
|
|
|
|
public record BreakoffDto
|
|
{
|
|
public List<Guid> EdgeIds { get; init; } = new();
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public bool Flip { get; init; }
|
|
|
|
public double TabWidth { get; init; }
|
|
public List<double> TabCenters { get; init; } = new();
|
|
}
|
|
|
|
public record CutPinDto
|
|
{
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
public CuttingParametersDto.LeadInDto LeadIn { get; init; }
|
|
public CuttingParametersDto.LeadOutDto LeadOut { get; init; }
|
|
|
|
/// <summary>Omitted for a closed contour.</summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public CuttingParametersDto.TabDto Tab { get; init; }
|
|
|
|
/// <summary>Set only by the lenient reader for an entry it could not read.</summary>
|
|
[JsonIgnore]
|
|
public bool Malformed { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads each cut pin on its own, so one wrongly shaped entry does not lose the rest or
|
|
/// fail the nest. A value that is not an array reads as a single malformed entry.
|
|
/// </summary>
|
|
public sealed class LenientCutPinListConverter : JsonConverter<List<CutPinDto>>
|
|
{
|
|
public override List<CutPinDto> Read(
|
|
ref Utf8JsonReader reader,
|
|
Type typeToConvert,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
if (document.RootElement.ValueKind != JsonValueKind.Array)
|
|
return new List<CutPinDto> { new() { Malformed = true } };
|
|
|
|
var list = new List<CutPinDto>();
|
|
foreach (var element in document.RootElement.EnumerateArray())
|
|
list.Add(ReadPin(element, options));
|
|
return list;
|
|
}
|
|
|
|
private static CutPinDto ReadPin(JsonElement element, JsonSerializerOptions options)
|
|
{
|
|
if (element.ValueKind != JsonValueKind.Object)
|
|
return new CutPinDto { Malformed = true };
|
|
|
|
try
|
|
{
|
|
return element.Deserialize<CutPinDto>(options) ?? new CutPinDto { Malformed = true };
|
|
}
|
|
catch (Exception ex) when (ex is JsonException || ex is FormatException || ex is InvalidOperationException)
|
|
{
|
|
return new CutPinDto { Malformed = true };
|
|
}
|
|
}
|
|
|
|
public override void Write(
|
|
Utf8JsonWriter writer,
|
|
List<CutPinDto> value,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
writer.WriteStartArray();
|
|
foreach (var pin in value)
|
|
JsonSerializer.Serialize(writer, pin, options);
|
|
writer.WriteEndArray();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads only a JSON boolean. Anything else, null included, reads as null so the reader
|
|
/// can warn; the property's initializer covers a missing value.
|
|
/// </summary>
|
|
public sealed class LenientFlagConverter : JsonConverter<bool?>
|
|
{
|
|
public override bool HandleNull => true;
|
|
|
|
public override bool? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
{
|
|
if (reader.TokenType == JsonTokenType.True)
|
|
return true;
|
|
if (reader.TokenType == JsonTokenType.False)
|
|
return false;
|
|
reader.Skip();
|
|
return null;
|
|
}
|
|
|
|
public override void Write(Utf8JsonWriter writer, bool? value, JsonSerializerOptions options)
|
|
{
|
|
if (value is bool flag)
|
|
writer.WriteBooleanValue(flag);
|
|
else
|
|
writer.WriteNullValue();
|
|
}
|
|
}
|
|
|
|
public record DrawingLibraryLinkDto
|
|
{
|
|
public string ServerKey { get; init; } = "";
|
|
public Guid Id { get; init; }
|
|
public int Revision { get; init; }
|
|
public string CutType { get; init; } = "";
|
|
public double Thickness { get; init; }
|
|
public string MaterialName { get; init; } = "";
|
|
public string MaterialGrade { get; init; } = "";
|
|
public string Units { get; init; } = "";
|
|
public string Notes { get; init; } = "";
|
|
public string PackageSha256 { get; init; } = "";
|
|
|
|
/// <summary>Set only by the lenient reader for a value it could not read.</summary>
|
|
[JsonIgnore]
|
|
public bool Malformed { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a wrongly shaped or typed library link as a malformed marker instead of
|
|
/// failing the whole nest; the nest reader validates the values it receives.
|
|
/// </summary>
|
|
public sealed class LenientDrawingLibraryLinkConverter : JsonConverter<DrawingLibraryLinkDto>
|
|
{
|
|
public override DrawingLibraryLinkDto Read(
|
|
ref Utf8JsonReader reader,
|
|
Type typeToConvert,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
|
return new DrawingLibraryLinkDto { Malformed = true };
|
|
|
|
try
|
|
{
|
|
return document.RootElement.Deserialize<DrawingLibraryLinkDto>(options)
|
|
?? new DrawingLibraryLinkDto { Malformed = true };
|
|
}
|
|
catch (Exception ex) when (ex is JsonException || ex is FormatException || ex is InvalidOperationException)
|
|
{
|
|
return new DrawingLibraryLinkDto { Malformed = true };
|
|
}
|
|
}
|
|
|
|
// The converter sits on the property, not the type, so this does not recurse.
|
|
public override void Write(
|
|
Utf8JsonWriter writer,
|
|
DrawingLibraryLinkDto value,
|
|
JsonSerializerOptions options
|
|
) => JsonSerializer.Serialize(writer, value, options);
|
|
}
|
|
|
|
public record PlateDto
|
|
{
|
|
public int Id { get; init; }
|
|
public SizeDto Size { get; init; } = new();
|
|
public int Quadrant { get; init; } = 1;
|
|
public int Quantity { get; init; } = 1;
|
|
public double PartSpacing { get; init; }
|
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
|
public double GrainAngle { get; init; }
|
|
public CuttingParametersDto CuttingParameters { get; init; }
|
|
public List<PartDto> Parts { get; init; } = new();
|
|
public List<CutOffDto> CutOffs { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Machine clamp keep-out rectangles in plate coordinates. Missing in older
|
|
/// files, which load with no zones.
|
|
/// </summary>
|
|
public List<ClampZoneDto> Clamps { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// Rail clamp ids removed from this sheet as a last resort (D8). Omitted when none
|
|
/// were removed; missing or empty means none; <see cref="Clamps"/> 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.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientStringListConverter))]
|
|
public List<string> RemovedClamps { get; init; } = new();
|
|
|
|
/// <summary>
|
|
/// The inventory item code of each physical sheet, in sheet order; sheets past the
|
|
/// end are uncoded. Omitted when no sheet is coded; missing means none. Read
|
|
/// leniently: a wrongly shaped value reads as none, and non-string entries are skipped.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientStringListConverter))]
|
|
public List<string> StockItemCodes { 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; }
|
|
|
|
/// <summary>
|
|
/// Derived at save from the layout (<see cref="PlateRemnant.Expected"/>) for readers
|
|
/// without geometry, such as the server. Omitted when the plate leaves none; never read
|
|
/// back, so a stale value cannot reach the model.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(WriteOnlyConverter<ExpectedRemnantDto>))]
|
|
public ExpectedRemnantDto ExpectedRemnant { get; init; }
|
|
|
|
/// <summary>
|
|
/// The sheet's material when it is not a rectangle (<see cref="SheetOutline"/>), in
|
|
/// plate coordinates; omitted for a rectangular sheet, which is every sheet in older
|
|
/// files. Read leniently: a wrongly shaped value loads as a rectangle with a reader
|
|
/// warning (see <see cref="NestReader.Warnings"/>).
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientSheetOutlineConverter))]
|
|
public SheetOutlineDto Outline { get; init; }
|
|
}
|
|
|
|
/// <summary>A non-rectangular sheet: its vertices and the vertices of each obstacle.</summary>
|
|
public record SheetOutlineDto
|
|
{
|
|
public List<PointDto> Points { get; init; } = new();
|
|
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public List<List<PointDto>> Obstacles { get; init; }
|
|
|
|
/// <summary>Why the value could not be read, when it was malformed; never written.</summary>
|
|
[JsonIgnore]
|
|
public string Problem { get; init; }
|
|
}
|
|
|
|
public record PointDto
|
|
{
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads a sheet outline leniently: a value that is not an object, a missing or non-array
|
|
/// <c>points</c>, or a vertex that is not an object with numeric <c>x</c> and <c>y</c>
|
|
/// yields a DTO carrying the <see cref="SheetOutlineDto.Problem"/> instead of failing the
|
|
/// file; a non-array obstacle list or entry is reported the same way. Geometry is not
|
|
/// checked here: <see cref="SheetOutline"/> does that when the plate is built.
|
|
/// </summary>
|
|
public sealed class LenientSheetOutlineConverter : JsonConverter<SheetOutlineDto>
|
|
{
|
|
public override SheetOutlineDto Read(
|
|
ref Utf8JsonReader reader,
|
|
Type typeToConvert,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
using var document = JsonDocument.ParseValue(ref reader);
|
|
var root = document.RootElement;
|
|
if (root.ValueKind == JsonValueKind.Null)
|
|
return null;
|
|
if (root.ValueKind != JsonValueKind.Object)
|
|
return new SheetOutlineDto { Problem = "it is not an object" };
|
|
if (!root.TryGetProperty("points", out var list))
|
|
return new SheetOutlineDto { Problem = "'points' is missing" };
|
|
if (!TryPointList(list, out var points, out var problem))
|
|
return new SheetOutlineDto { Problem = problem };
|
|
List<List<PointDto>> obstacles = null;
|
|
if (root.TryGetProperty("obstacles", out var entries) && entries.ValueKind != JsonValueKind.Null)
|
|
{
|
|
if (entries.ValueKind != JsonValueKind.Array)
|
|
return new SheetOutlineDto { Problem = "'obstacles' is not an array" };
|
|
obstacles = new List<List<PointDto>>();
|
|
var index = 0;
|
|
foreach (var entry in entries.EnumerateArray())
|
|
{
|
|
index++;
|
|
if (!TryPointList(entry, out var obstacle, out problem))
|
|
return new SheetOutlineDto { Problem = $"obstacle {index}: {problem}" };
|
|
obstacles.Add(obstacle);
|
|
}
|
|
}
|
|
return new SheetOutlineDto { Points = points, Obstacles = obstacles };
|
|
}
|
|
|
|
private static bool TryPointList(JsonElement list, out List<PointDto> points, out string problem)
|
|
{
|
|
points = null;
|
|
problem = null;
|
|
if (list.ValueKind != JsonValueKind.Array)
|
|
{
|
|
problem = "the vertex list is not an array";
|
|
return false;
|
|
}
|
|
points = new List<PointDto>();
|
|
foreach (var element in list.EnumerateArray())
|
|
{
|
|
if (element.ValueKind != JsonValueKind.Object
|
|
|| !TryNumber(element, "x", out var x)
|
|
|| !TryNumber(element, "y", out var y))
|
|
{
|
|
problem = $"vertex {points.Count + 1} is not an object with numeric x and y";
|
|
return false;
|
|
}
|
|
points.Add(new PointDto { X = x, Y = y });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public override void Write(
|
|
Utf8JsonWriter writer,
|
|
SheetOutlineDto value,
|
|
JsonSerializerOptions options
|
|
)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteStartArray("points");
|
|
foreach (var point in value.Points)
|
|
WritePoint(writer, point);
|
|
writer.WriteEndArray();
|
|
if (value.Obstacles is { Count: > 0 })
|
|
{
|
|
writer.WriteStartArray("obstacles");
|
|
foreach (var obstacle in value.Obstacles)
|
|
{
|
|
writer.WriteStartArray();
|
|
foreach (var point in obstacle)
|
|
WritePoint(writer, point);
|
|
writer.WriteEndArray();
|
|
}
|
|
writer.WriteEndArray();
|
|
}
|
|
writer.WriteEndObject();
|
|
}
|
|
|
|
private static bool TryNumber(JsonElement point, string name, out double value)
|
|
{
|
|
value = 0;
|
|
return point.TryGetProperty(name, out var element)
|
|
&& element.ValueKind == JsonValueKind.Number
|
|
&& element.TryGetDouble(out value);
|
|
}
|
|
|
|
private static void WritePoint(Utf8JsonWriter writer, PointDto point)
|
|
{
|
|
writer.WriteStartObject();
|
|
writer.WriteNumber("x", point.X);
|
|
writer.WriteNumber("y", point.Y);
|
|
writer.WriteEndObject();
|
|
}
|
|
}
|
|
|
|
/// <summary>A plate's expected rectangular drop, with <see cref="Width"/> the shorter side.</summary>
|
|
public record ExpectedRemnantDto
|
|
{
|
|
public double Width { get; init; }
|
|
public double Length { get; init; }
|
|
|
|
/// <summary><c>cutoff</c> or <c>offcut</c>.</summary>
|
|
public string Source { get; init; } = "";
|
|
}
|
|
|
|
public record ClampZoneDto
|
|
{
|
|
public string Id { get; init; } = "";
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
|
|
/// <summary>Extent along X.</summary>
|
|
public double Length { get; init; }
|
|
|
|
/// <summary>Extent along Y (depth from the y = 0 operator-side edge).</summary>
|
|
public double Width { get; init; }
|
|
}
|
|
|
|
public record PartDto
|
|
{
|
|
public int DrawingId { get; init; }
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
public double Rotation { get; init; }
|
|
public bool HasManualLeadIns { get; init; }
|
|
public bool LeadInsLocked { get; init; }
|
|
public string Program { get; init; }
|
|
public string DrawingHash { get; init; }
|
|
|
|
/// <summary>
|
|
/// The cutting settings this part's saved program was built with. Written only with
|
|
/// <see cref="Program"/>; missing in older files and for clean parts, which read as
|
|
/// null so reviews fall back to the plate's settings.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public CuttingParametersDto CuttingParameters { get; init; }
|
|
|
|
/// <summary>
|
|
/// True when this placement ignores its drawing's cut pins. Written only when true;
|
|
/// missing means false. Any value other than a boolean reads as null, which the
|
|
/// reader reports and treats as false.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
[JsonConverter(typeof(LenientFlagConverter))]
|
|
public bool? DrawingPinsOff { get; init; } = false;
|
|
}
|
|
|
|
public record CutOffDto
|
|
{
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
public string Axis { get; init; } = "vertical";
|
|
public double? StartLimit { get; init; }
|
|
public double? EndLimit { get; init; }
|
|
|
|
/// <summary>
|
|
/// Zero-based place in the plate's cut sequence (<c>Plate.Parts</c> order,
|
|
/// cut-offs included) of the cut-off's first segment. Null in older files, which
|
|
/// load it at the end.
|
|
/// </summary>
|
|
public int? Sequence { get; init; }
|
|
|
|
/// <summary>
|
|
/// The place of each of the cut-off's trimmed segments in the cut sequence, in
|
|
/// ascending order along its line, when they are not consecutive. Omitted for one
|
|
/// segment or consecutive segments; readers without it place the segments
|
|
/// consecutively from <see cref="Sequence"/>.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public int[] Segments { get; init; }
|
|
|
|
/// <summary>
|
|
/// How far past the nominal far sheet edge the cut-off cuts with the head height
|
|
/// locked (<c>CutOff.HeadLockOvercut</c>). Omitted when zero, as in older files.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public double HeadLockOvercut { get; init; }
|
|
|
|
/// <summary>
|
|
/// Whether the segment reaching the far edge is cut from it toward the origin
|
|
/// (<c>CutOff.FarEdgeFirst</c>). Omitted when false, as in older files.
|
|
/// </summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
|
public bool FarEdgeFirst { get; init; }
|
|
}
|
|
|
|
public record SizeDto
|
|
{
|
|
public double Width { get; init; }
|
|
public double Length { get; init; }
|
|
}
|
|
|
|
public record MaterialDto
|
|
{
|
|
public string Name { get; init; } = "";
|
|
public string Grade { get; init; } = "";
|
|
public double Density { get; init; }
|
|
}
|
|
|
|
public record SpacingDto
|
|
{
|
|
public double Left { get; init; }
|
|
public double Top { get; init; }
|
|
public double Right { get; init; }
|
|
public double Bottom { get; init; }
|
|
}
|
|
|
|
public record ColorDto
|
|
{
|
|
public int A { get; init; } = 255;
|
|
public int R { get; init; }
|
|
public int G { get; init; }
|
|
public int B { get; init; }
|
|
}
|
|
|
|
public record QuantityDto
|
|
{
|
|
public int Required { get; init; }
|
|
}
|
|
|
|
public record ConstraintsDto
|
|
{
|
|
public double StepAngle { get; init; }
|
|
public double StartAngle { get; init; }
|
|
public double EndAngle { get; init; }
|
|
public bool Allow180Equivalent { get; init; }
|
|
}
|
|
|
|
public record SourceDto
|
|
{
|
|
public string Path { get; init; } = "";
|
|
public OffsetDto Offset { get; init; } = new();
|
|
}
|
|
|
|
public record OffsetDto
|
|
{
|
|
public double X { get; init; }
|
|
public double Y { get; init; }
|
|
}
|
|
|
|
public record BendDto
|
|
{
|
|
public double StartX { get; init; }
|
|
public double StartY { get; init; }
|
|
public double EndX { get; init; }
|
|
public double EndY { get; init; }
|
|
public string Direction { get; init; } = "Unknown";
|
|
public double? Angle { get; init; }
|
|
public double? Radius { get; init; }
|
|
public string NoteText { get; init; } = "";
|
|
}
|
|
|
|
public record PlateOptionDto
|
|
{
|
|
public double Width { get; init; }
|
|
public double Length { get; init; }
|
|
public double Cost { get; init; }
|
|
}
|
|
|
|
public record EntitySetDto
|
|
{
|
|
public List<EntityDto> Entities { get; init; } = new();
|
|
public List<string> Suppressed { get; init; } = new();
|
|
|
|
/// <summary>Role overrides by entity ID ("cut", "etch" or "display"); omitted when none.</summary>
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public Dictionary<string, string> Roles { get; init; }
|
|
}
|
|
|
|
public record EntityDto
|
|
{
|
|
public string Id { get; init; } = "";
|
|
public string Type { get; init; } = "";
|
|
public string Layer { get; init; } = "";
|
|
public string LineType { get; init; } = "";
|
|
|
|
// Line
|
|
public double X1 { get; init; }
|
|
public double Y1 { get; init; }
|
|
public double X2 { get; init; }
|
|
public double Y2 { get; init; }
|
|
|
|
// Arc / Circle
|
|
public double CX { get; init; }
|
|
public double CY { get; init; }
|
|
public double R { get; init; }
|
|
public double StartAngle { get; init; }
|
|
public double EndAngle { get; init; }
|
|
public bool Reversed { get; init; }
|
|
public string Rotation { get; init; } = "";
|
|
}
|
|
|
|
public record BestFitSetDto
|
|
{
|
|
public double PlateWidth { get; init; }
|
|
public double PlateHeight { get; init; }
|
|
public double Spacing { get; init; }
|
|
public double? CanonicalAngle { get; init; }
|
|
public List<BestFitResultDto> Results { get; init; } = new();
|
|
}
|
|
|
|
public record BestFitResultDto
|
|
{
|
|
public double Part1Rotation { get; init; }
|
|
public double Part2Rotation { get; init; }
|
|
public double Part2OffsetX { get; init; }
|
|
public double Part2OffsetY { get; init; }
|
|
public int StrategyType { get; init; }
|
|
public int TestNumber { get; init; }
|
|
public double CandidateSpacing { get; init; }
|
|
public double RotatedArea { get; init; }
|
|
public double BoundingWidth { get; init; }
|
|
public double BoundingHeight { get; init; }
|
|
public double OptimalRotation { get; init; }
|
|
public bool Keep { get; init; }
|
|
public string Reason { get; init; } = "";
|
|
public double TrueArea { get; init; }
|
|
public List<double> HullAngles { get; init; } = new();
|
|
}
|
|
}
|
|
}
|