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(); } } }