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; } = "";
///
/// The machine the nest is cut on. Omitted when none is assigned; older files and
/// older readers simply have none.
///
[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 Drawings { get; init; } = new();
public List Plates { get; init; } = new();
public List PlateOptions { get; init; } = new();
public double SalvageRate { get; init; } = 0.5;
///
/// Smallest remnant side in model units. Missing in older files, which load the Auto
/// Nest default.
///
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();
///
/// 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.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientClampRailConverter))]
public ClampRailDto ClampRail { get; init; }
///
/// 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.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientSupportBedConverter))]
public SupportBedDto SupportBed { get; init; }
}
/// A machine configuration's id and the name it had when the nest was saved.
public record MachineDto
{
public Guid Id { get; init; }
public string Name { get; init; } = "";
}
///
/// 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.
///
public sealed class LenientMachineConverter : JsonConverter
{
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();
}
}
///
/// A support bed layout. names the layout: grid, a regular grid
/// of contact points (pitch and offset along X and Y), or slats, straight slats
/// running along (, ,
/// ). Lengths are in the nest's units.
///
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; }
}
///
/// 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.
///
public sealed class LenientSupportBedConverter : JsonConverter
{
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);
}
}
///
/// 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.
///
public sealed class LenientClampRailConverter : JsonConverter
{
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[OtherEdgeKeys.Length];
for (var i = 0; i < others.Length; i++)
{
others[i] = new List();
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;
return new ClampRailDto
{
Width = width,
Depth = depth,
Centers = list,
FarSideCenters = others[0],
OriginEndCenters = others[1],
FarEndCenters = others[2],
RemovalMinutes = removalMinutes,
};
}
private static readonly string[] OtherEdgeKeys = { "farSideCenters", "originEndCenters", "farEndCenters" };
private static void WriteCenters(Utf8JsonWriter writer, string key, List centers)
{
if (centers == null || centers.Count == 0)
return;
writer.WriteStartArray(key);
foreach (var center in centers)
writer.WriteNumberValue(center);
writer.WriteEndArray();
}
private static List ReadCenters(JsonElement centers)
{
var list = new List();
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())
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);
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);
}
}
///
/// 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.
///
/// Writes derived data and skips it on read, whatever its shape.
public sealed class WriteOnlyConverter : JsonConverter
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>
{
public override List Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
using var document = JsonDocument.ParseValue(ref reader);
var list = new List();
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 value,
JsonSerializerOptions options
)
{
writer.WriteStartArray();
foreach (var item in value)
writer.WriteStringValue(item);
writer.WriteEndArray();
}
}
public record ClampRailDto
{
/// Clamp extent along its edge.
public double Width { get; init; }
/// Clamp intrusion into the sheet from its edge.
public double Depth { get; init; }
/// Origin-side clamp centers along X, measured from the plate origin.
public List Centers { get; init; } = new();
/// Far-side clamp centers along X from the origin; omitted when none.
public List FarSideCenters { get; init; } = new();
/// Origin-end clamp centers along Y from the origin; omitted when none.
public List OriginEndCenters { get; init; } = new();
/// Far-end clamp centers along Y from the origin; omitted when none.
public List FarEndCenters { get; init; } = new();
///
/// Minutes to remove one clamp and put it back after cutting (D9). Null in older
/// files and readers fall back to .
///
public double? RemovalMinutes { 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();
///
/// The color was chosen on purpose and survives scheme changes. Omitted when false;
/// readers recolor such drawings from the active palette.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool CustomColor { get; init; }
public QuantityDto Quantity { get; init; } = new();
public int Priority { get; init; }
/// Sequence this drawing's parts first. Omitted when false.
[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 Bends { get; init; } = new();
///
/// 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 and the reader drops it with a warning.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientDrawingLibraryLinkConverter))]
public DrawingLibraryLinkDto Library { get; init; }
///
/// Drawing cut pins in the source frame. Omitted when there are none. Read leniently:
/// an entry that cannot be read becomes and the
/// reader skips it with a warning; the other pins load.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientCutPinListConverter))]
public List CutPins { get; init; }
///
/// Breakoffs in the source frame, already applied to the stored program. Omitted when
/// there are none.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List Breakoffs { get; init; }
///
/// Tabbed cutouts in the source frame, already applied to the stored program. Omitted
/// when there are none.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List TabbedCutouts { get; init; }
}
public record TabbedCutoutDto
{
public List EdgeIds { get; init; } = new();
public double TabWidth { get; init; }
public List TabCenters { get; init; } = new();
}
public record BreakoffDto
{
public List EdgeIds { get; init; } = new();
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool Flip { get; init; }
public double TabWidth { get; init; }
public List 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; }
/// Omitted for a closed contour.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public CuttingParametersDto.TabDto Tab { get; init; }
/// Set only by the lenient reader for an entry it could not read.
[JsonIgnore]
public bool Malformed { get; init; }
}
///
/// 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.
///
public sealed class LenientCutPinListConverter : JsonConverter>
{
public override List Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
using var document = JsonDocument.ParseValue(ref reader);
if (document.RootElement.ValueKind != JsonValueKind.Array)
return new List { new() { Malformed = true } };
var list = new List();
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(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 value,
JsonSerializerOptions options
)
{
writer.WriteStartArray();
foreach (var pin in value)
JsonSerializer.Serialize(writer, pin, options);
writer.WriteEndArray();
}
}
///
/// 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.
///
public sealed class LenientFlagConverter : JsonConverter
{
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; } = "";
/// Set only by the lenient reader for a value it could not read.
[JsonIgnore]
public bool Malformed { get; init; }
}
///
/// 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.
///
public sealed class LenientDrawingLibraryLinkConverter : JsonConverter
{
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(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 Parts { get; init; } = new();
public List CutOffs { get; init; } = new();
///
/// Machine clamp keep-out rectangles in plate coordinates. Missing in older
/// files, which load with no zones.
///
public List Clamps { get; init; } = new();
///
/// 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();
///
/// 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.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(LenientStringListConverter))]
public List StockItemCodes { 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; }
///
/// Derived at save from the layout () 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.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
[JsonConverter(typeof(WriteOnlyConverter))]
public ExpectedRemnantDto ExpectedRemnant { get; init; }
}
/// A plate's expected rectangular drop, with the shorter side.
public record ExpectedRemnantDto
{
public double Width { get; init; }
public double Length { get; init; }
/// cutoff or offcut.
public string Source { get; init; } = "";
}
public record ClampZoneDto
{
public string Id { get; init; } = "";
public double X { get; init; }
public double Y { get; init; }
/// Extent along X.
public double Length { get; init; }
/// Extent along Y (depth from the y = 0 operator-side edge).
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; }
///
/// 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.
///
[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; }
///
/// Zero-based place in the plate's cut sequence (Plate.Parts order,
/// cut-offs included) of the cut-off's first segment. Null in older files, which
/// load it at the end.
///
public int? Sequence { get; init; }
///
/// 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 .
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int[] Segments { get; init; }
///
/// How far past the nominal far sheet edge the cut-off cuts with the head height
/// locked (CutOff.HeadLockOvercut). Omitted when zero, as in older files.
///
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public double HeadLockOvercut { get; init; }
///
/// Whether the segment reaching the far edge is cut from it toward the origin
/// (CutOff.FarEdgeFirst). Omitted when false, as in older files.
///
[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 Entities { get; init; } = new();
public List Suppressed { get; init; } = new();
/// Role overrides by entity ID ("cut", "etch" or "display"); omitted when none.
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public Dictionary 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 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 HullAngles { get; init; } = new();
}
}
}