mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-30 00:01:06 -04:00
Undo will keep whole-nest states and restore by reading one back. NestSnapshot.Capture writes the same archive a save writes, with three differences Phase 0 found necessary: it does not stamp DateLastModified on the live nest, it leaves best fits out (they are a cache), and it keeps empty plates, which files drop, so an undo does not shift plate indices or lose a plate the user added. File output is unchanged. Restore reuses each live drawing whose saved content is unchanged, matched by a fingerprint over its nest.json entry, program, hole sub-programs and source entities. BestFitCache and FillResultCache are keyed by the Drawing object, so a plain read would make the next fill recompute every best-fit pair; reuse keeps those caches, colours and Drawing.Id, and skips parsing the reused programs. Nested counts are recomputed for the restored nest, and given back to the live one if the read fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
757 lines
31 KiB
C#
757 lines
31 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Nodes;
|
|
using OpenNest.Bending;
|
|
using OpenNest.BestFit;
|
|
using OpenNest.Breakoffs;
|
|
using OpenNest.CNC;
|
|
using OpenNest.CNC.CuttingStrategy;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Support;
|
|
using OpenNest.TabbedCutouts;
|
|
using static OpenNest.IO.NestFormat;
|
|
|
|
namespace OpenNest.IO
|
|
{
|
|
public sealed class NestReader
|
|
{
|
|
private readonly Stream stream;
|
|
private readonly ZipArchive zipArchive;
|
|
private readonly Dictionary<int, string> drawingHashes = new();
|
|
private readonly List<string> warnings = new();
|
|
|
|
/// <summary>Part programs and library links that could not be restored; everything else still loads.</summary>
|
|
public IReadOnlyList<string> Warnings => warnings.AsReadOnly();
|
|
|
|
/// <summary>
|
|
/// Receives saved best-fit sets. Without a store, <c>bestfits/</c> entries are skipped
|
|
/// unread; they are only a cache.
|
|
/// </summary>
|
|
public IBestFitStore BestFitStore { get; init; }
|
|
|
|
/// <summary>
|
|
/// Snapshot restore only: live drawings by <see cref="NestWriter.GetDrawingFingerprints"/>,
|
|
/// used in place of saved drawings with the same fingerprint.
|
|
/// </summary>
|
|
internal IReadOnlyDictionary<string, Drawing> ReusableDrawings { get; init; }
|
|
|
|
public NestReader(string file)
|
|
{
|
|
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
|
}
|
|
|
|
public NestReader(Stream stream)
|
|
{
|
|
this.stream = stream;
|
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
|
}
|
|
|
|
public Nest Read()
|
|
{
|
|
var nestJson = ReadEntry("nest.json");
|
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
|
|
|
var reused = MatchReusableDrawings(nestJson);
|
|
var programs = ReadPrograms(dto.Drawings.Count, reused);
|
|
var entitySets = ReadEntitySets(dto.Drawings.Count, reused);
|
|
var drawingMap = BuildDrawings(dto, programs, entitySets, reused);
|
|
ReadBestFits(drawingMap);
|
|
var nest = BuildNest(dto, drawingMap);
|
|
// Nests converted from PEP kept etch marks only in the placed programs; give them
|
|
// back to the drawing so removing lead-ins does not remove the marks too.
|
|
DrawingMarkRecovery.Apply(nest);
|
|
|
|
zipArchive.Dispose();
|
|
stream.Close();
|
|
|
|
return nest;
|
|
}
|
|
|
|
private string ReadEntry(string name)
|
|
{
|
|
var entry =
|
|
zipArchive.GetEntry(name)
|
|
?? throw new InvalidDataException($"Nest file is missing required entry '{name}'.");
|
|
using var entryStream = entry.Open();
|
|
using var reader = new StreamReader(entryStream);
|
|
return reader.ReadToEnd();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Matches saved drawings to <see cref="ReusableDrawings"/> by fingerprint. A matched
|
|
/// drawing is used as it is: its program and entities are not parsed, though its
|
|
/// program hash is still recorded so saved part programs restore against it.
|
|
/// </summary>
|
|
private Dictionary<int, Drawing> MatchReusableDrawings(string nestJson)
|
|
{
|
|
var reused = new Dictionary<int, Drawing>();
|
|
if (ReusableDrawings == null || ReusableDrawings.Count == 0)
|
|
return reused;
|
|
|
|
var drawings = JsonNode.Parse(nestJson)?["drawings"]?.AsArray();
|
|
if (drawings == null)
|
|
return reused;
|
|
|
|
foreach (var node in drawings)
|
|
{
|
|
var json = node.AsObject();
|
|
var id = json["id"].GetValue<int>();
|
|
var name = $"programs/program-{id}";
|
|
if (zipArchive.GetEntry(name) == null)
|
|
continue;
|
|
|
|
var text = ReadEntry(name);
|
|
var subs = zipArchive.GetEntry(name + "-subs") == null ? "" : ReadEntry(name + "-subs");
|
|
var entitiesName = $"entities/entities-{id}";
|
|
var entities = zipArchive.GetEntry(entitiesName) == null ? null : ReadEntry(entitiesName);
|
|
var fingerprint = NestWriter.GetDrawingFingerprint(json, text, subs, entities);
|
|
if (ReusableDrawings.TryGetValue(fingerprint, out var live))
|
|
{
|
|
reused[id] = live;
|
|
drawingHashes[id] = NestWriter.GetDrawingHash(text, subs);
|
|
}
|
|
}
|
|
return reused;
|
|
}
|
|
|
|
private Dictionary<int, Program> ReadPrograms(int count, Dictionary<int, Drawing> reused)
|
|
{
|
|
var programs = new Dictionary<int, Program>();
|
|
for (var i = 1; i <= count; i++)
|
|
{
|
|
var name = $"programs/program-{i}";
|
|
if (reused.ContainsKey(i) || zipArchive.GetEntry(name) == null)
|
|
continue;
|
|
programs[i] = ReadProgram(name, out var hash);
|
|
drawingHashes[i] = hash;
|
|
}
|
|
return programs;
|
|
}
|
|
|
|
private Program ReadProgram(string name, out string hash)
|
|
{
|
|
var text = ReadEntry(name);
|
|
var subs = zipArchive.GetEntry(name + "-subs") == null ? "" : ReadEntry(name + "-subs");
|
|
hash = NestWriter.GetDrawingHash(text, subs);
|
|
using var programStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
|
|
var program = new ProgramReader(programStream).Read();
|
|
if (subs.Length > 0)
|
|
{
|
|
using var subsStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(subs));
|
|
ReadSubPrograms(program, subsStream);
|
|
}
|
|
return program;
|
|
}
|
|
|
|
private void RestorePartProgram(Part part, PartDto dto, int plateId, int partIndex)
|
|
{
|
|
// Old files had only transient flags. They still load clean and silently.
|
|
if (string.IsNullOrEmpty(dto.Program))
|
|
return;
|
|
|
|
// A missing hash cannot establish that this program belongs to the current drawing.
|
|
if (!drawingHashes.TryGetValue(dto.DrawingId, out var hash)
|
|
|| !string.Equals(dto.DrawingHash, hash, StringComparison.Ordinal))
|
|
return;
|
|
|
|
try
|
|
{
|
|
var program = ReadProgram(dto.Program, out _);
|
|
foreach (var call in program.Codes.OfType<SubProgramCall>())
|
|
if (call.Program == null || !call.Program.Codes.Any(c => c is Motion))
|
|
throw new InvalidDataException($"Missing or empty hole sub-program {call.Id}.");
|
|
|
|
if (!part.RestoreLeadInProgram(program, dto.LeadInsLocked))
|
|
throw new InvalidDataException("The saved part program has no motion.");
|
|
// After the install, which clears it: the settings belong to this program.
|
|
part.CuttingParameters = CuttingParametersSerializer.FromDto(dto.CuttingParameters);
|
|
}
|
|
catch (Exception ex) when (ex is IOException || ex is InvalidDataException || ex is FormatException
|
|
|| ex is OverflowException || ex is ArgumentException || ex is InvalidOperationException)
|
|
{
|
|
warnings.Add($"Plate {plateId}, part {partIndex} ('{part.BaseDrawing.Name}'): "
|
|
+ $"could not restore '{dto.Program}'; loaded the clean drawing without lead-ins or tabs. {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private static void ReadSubPrograms(Program parent, Stream stream)
|
|
{
|
|
using var reader = new StreamReader(stream);
|
|
var currentId = (int?)null;
|
|
var lines = new List<string>();
|
|
|
|
string line;
|
|
while ((line = reader.ReadLine()) != null)
|
|
{
|
|
var trimmed = line.Trim();
|
|
|
|
if (trimmed.StartsWith(":") && int.TryParse(trimmed.Substring(1), out var id))
|
|
{
|
|
// Flush previous sub-program
|
|
if (currentId.HasValue && lines.Count > 0)
|
|
parent.SubPrograms[currentId.Value] = ParseSubProgram(lines);
|
|
|
|
currentId = id;
|
|
lines.Clear();
|
|
}
|
|
else if (trimmed == "M99")
|
|
{
|
|
if (currentId.HasValue && lines.Count > 0)
|
|
parent.SubPrograms[currentId.Value] = ParseSubProgram(lines);
|
|
|
|
currentId = null;
|
|
lines.Clear();
|
|
}
|
|
else
|
|
{
|
|
lines.Add(trimmed);
|
|
}
|
|
}
|
|
|
|
// Wire up SubProgramCall.Program references
|
|
foreach (var code in parent.Codes)
|
|
{
|
|
if (
|
|
code is SubProgramCall call
|
|
&& parent.SubPrograms.TryGetValue(call.Id, out var sub)
|
|
)
|
|
call.Program = sub;
|
|
}
|
|
}
|
|
|
|
private static Program ParseSubProgram(List<string> lines)
|
|
{
|
|
var text = string.Join("\n", lines);
|
|
var memStream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(text));
|
|
var reader = new ProgramReader(memStream);
|
|
return reader.Read();
|
|
}
|
|
|
|
private Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed, Dictionary<Guid, EntityRole> roles)> ReadEntitySets(
|
|
int count,
|
|
Dictionary<int, Drawing> reused
|
|
)
|
|
{
|
|
var result = new Dictionary<int, (List<Entity>, HashSet<Guid>, Dictionary<Guid, EntityRole>)>();
|
|
for (var i = 1; i <= count; i++)
|
|
{
|
|
var entry = zipArchive.GetEntry($"entities/entities-{i}");
|
|
if (entry == null || reused.ContainsKey(i))
|
|
continue;
|
|
|
|
using var entryStream = entry.Open();
|
|
using var reader = new StreamReader(entryStream);
|
|
var json = reader.ReadToEnd();
|
|
var dto = JsonSerializer.Deserialize<EntitySetDto>(json, JsonOptions);
|
|
var (entities, suppressed) = EntitySerializer.FromDto(dto);
|
|
result[i] = (entities, suppressed, EntitySerializer.RolesFromDto(dto));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private Dictionary<int, Drawing> BuildDrawings(
|
|
NestDto dto,
|
|
Dictionary<int, Program> programs,
|
|
Dictionary<int, (List<Entity> entities, HashSet<Guid> suppressed, Dictionary<Guid, EntityRole> roles)> entitySets,
|
|
Dictionary<int, Drawing> reused
|
|
)
|
|
{
|
|
var map = new Dictionary<int, Drawing>();
|
|
foreach (var d in dto.Drawings)
|
|
{
|
|
if (reused.TryGetValue(d.Id, out var live))
|
|
{
|
|
map[d.Id] = live;
|
|
continue;
|
|
}
|
|
|
|
var drawing = new Drawing(d.Name);
|
|
drawing.Customer = d.Customer;
|
|
drawing.Color = Color.FromArgb(d.Color.A, d.Color.R, d.Color.G, d.Color.B);
|
|
drawing.HasCustomColor = d.CustomColor;
|
|
drawing.Quantity.Required = d.Quantity.Required;
|
|
drawing.Priority = d.Priority;
|
|
drawing.CutFirst = d.CutFirst;
|
|
drawing.Constraints.StepAngle = d.Constraints.StepAngle;
|
|
drawing.Constraints.StartAngle = d.Constraints.StartAngle;
|
|
drawing.Constraints.EndAngle = d.Constraints.EndAngle;
|
|
drawing.Constraints.Allow180Equivalent = d.Constraints.Allow180Equivalent;
|
|
drawing.Material = new Material(
|
|
d.Material.Name,
|
|
d.Material.Grade,
|
|
d.Material.Density
|
|
);
|
|
drawing.Source.Path = d.Source.Path;
|
|
drawing.Source.Offset = new Vector(d.Source.Offset.X, d.Source.Offset.Y);
|
|
|
|
if (d.Bends != null)
|
|
{
|
|
foreach (var b in d.Bends)
|
|
{
|
|
drawing.Bends.Add(
|
|
new Bend
|
|
{
|
|
StartPoint = new Vector(b.StartX, b.StartY),
|
|
EndPoint = new Vector(b.EndX, b.EndY),
|
|
Direction = Enum.TryParse<BendDirection>(
|
|
b.Direction,
|
|
true,
|
|
out var dir
|
|
)
|
|
? dir
|
|
: BendDirection.Unknown,
|
|
Angle = b.Angle,
|
|
Radius = b.Radius,
|
|
NoteText = b.NoteText,
|
|
}
|
|
);
|
|
}
|
|
}
|
|
|
|
if (programs.TryGetValue(d.Id, out var pgm))
|
|
drawing.Program = pgm;
|
|
|
|
if (entitySets.TryGetValue(d.Id, out var entitySet))
|
|
{
|
|
drawing.SourceEntities = entitySet.entities;
|
|
drawing.SuppressedEntityIds = entitySet.suppressed;
|
|
drawing.EntityRoles = entitySet.roles;
|
|
|
|
// Older files saved etch marks as cut moves; restore their Scribe layer so
|
|
// nesting ignores them. The program was mutated in place, so refresh. Marks
|
|
// the user set to cut or display were not etched, so they are not repair sources.
|
|
var marks = drawing.SourceEntities.Where(e =>
|
|
EntityRoles.Of(e, drawing.EntityRoles) == EntityRole.Etch);
|
|
if (
|
|
drawing.Program != null
|
|
&& ScribeLayerRepair.Apply(drawing.Program, marks, drawing.Source.Offset) > 0
|
|
)
|
|
{
|
|
drawing.UpdateArea();
|
|
drawing.RecomputeCanonicalAngle();
|
|
}
|
|
}
|
|
|
|
drawing.Library = ReadLibraryLink(d);
|
|
drawing.CutPins = ReadCutPins(d);
|
|
drawing.Breakoffs = ReadBreakoffs(d);
|
|
drawing.TabbedCutouts = ReadTabbedCutouts(d);
|
|
map[d.Id] = drawing;
|
|
}
|
|
return map;
|
|
}
|
|
|
|
// A link that cannot be fully trusted is dropped: the drawing loads as a plain nest
|
|
// drawing, which is what it is without a verifiable library revision.
|
|
private DrawingLibraryLink ReadLibraryLink(DrawingDto d)
|
|
{
|
|
var link = d.Library;
|
|
if (link == null)
|
|
return null;
|
|
|
|
if (!link.Malformed
|
|
&& !string.IsNullOrWhiteSpace(link.ServerKey)
|
|
&& link.Id != Guid.Empty
|
|
&& link.Revision >= 1
|
|
&& TryParseName<CutType>(link.CutType, out var cutType)
|
|
&& TryParseName<Units>(link.Units, out var units)
|
|
&& double.IsFinite(link.Thickness)
|
|
&& link.Thickness >= 0)
|
|
{
|
|
return new DrawingLibraryLink(link.ServerKey, link.Id, link.Revision, cutType, link.Thickness,
|
|
link.MaterialName, link.MaterialGrade, units, link.Notes, link.PackageSha256);
|
|
}
|
|
|
|
warnings.Add($"Drawing '{d.Name}': its drawing library link could not be read; "
|
|
+ "loaded it as a nest drawing without a library revision.");
|
|
return null;
|
|
}
|
|
|
|
// Pins are independent: one that cannot be trusted is skipped and the rest still apply.
|
|
private List<ContourCutPin> ReadCutPins(DrawingDto d)
|
|
{
|
|
var pins = new List<ContourCutPin>();
|
|
if (d.CutPins == null)
|
|
return pins;
|
|
|
|
for (var i = 0; i < d.CutPins.Count; i++)
|
|
{
|
|
var pin = CutPinFormat.Read(d.CutPins[i], out var problem);
|
|
if (pin != null)
|
|
pins.Add(pin);
|
|
else
|
|
warnings.Add($"Drawing '{d.Name}': cut pin {i + 1} could not be read ({problem}); "
|
|
+ "skipped it, so that contour uses the nest's cutting settings.");
|
|
}
|
|
return pins;
|
|
}
|
|
|
|
// The program already has breakoffs applied; a dropped definition only loses editability.
|
|
private List<Breakoff> ReadBreakoffs(DrawingDto d)
|
|
{
|
|
var breakoffs = new List<Breakoff>();
|
|
if (d.Breakoffs == null)
|
|
return breakoffs;
|
|
|
|
for (var i = 0; i < d.Breakoffs.Count; i++)
|
|
{
|
|
var dto = d.Breakoffs[i];
|
|
if (dto?.EdgeIds is { Count: > 0 } && dto.TabCenters != null
|
|
&& dto.TabWidth > 0 && double.IsFinite(dto.TabWidth)
|
|
&& dto.TabCenters.All(double.IsFinite))
|
|
{
|
|
breakoffs.Add(new Breakoff
|
|
{
|
|
EdgeIds = dto.EdgeIds.ToList(),
|
|
Flip = dto.Flip,
|
|
TabWidth = dto.TabWidth,
|
|
TabCenters = dto.TabCenters.ToList(),
|
|
});
|
|
}
|
|
else
|
|
{
|
|
warnings.Add($"Drawing '{d.Name}': breakoff {i + 1} could not be read; its cut geometry "
|
|
+ "is kept, but it can no longer be edited as a breakoff.");
|
|
}
|
|
}
|
|
return breakoffs;
|
|
}
|
|
|
|
// Like breakoffs: the program already has the tabs applied, so a dropped definition only
|
|
// loses editability.
|
|
private List<TabbedCutout> ReadTabbedCutouts(DrawingDto d)
|
|
{
|
|
var cutouts = new List<TabbedCutout>();
|
|
if (d.TabbedCutouts == null)
|
|
return cutouts;
|
|
|
|
for (var i = 0; i < d.TabbedCutouts.Count; i++)
|
|
{
|
|
var dto = d.TabbedCutouts[i];
|
|
if (dto?.EdgeIds is { Count: > 0 } && dto.TabCenters != null
|
|
&& dto.TabWidth > 0 && double.IsFinite(dto.TabWidth)
|
|
&& dto.TabCenters.All(double.IsFinite))
|
|
{
|
|
cutouts.Add(new TabbedCutout
|
|
{
|
|
EdgeIds = dto.EdgeIds.ToList(),
|
|
TabWidth = dto.TabWidth,
|
|
TabCenters = dto.TabCenters.ToList(),
|
|
});
|
|
}
|
|
else
|
|
{
|
|
warnings.Add($"Drawing '{d.Name}': tabbed cutout {i + 1} could not be read; its cut "
|
|
+ "geometry is kept, but it can no longer be edited as a tabbed cutout.");
|
|
}
|
|
}
|
|
return cutouts;
|
|
}
|
|
|
|
private bool ReadPinsOff(PartDto dto, int plateId, int partIndex, Part part)
|
|
{
|
|
if (dto.DrawingPinsOff is bool pinsOff)
|
|
return pinsOff;
|
|
|
|
warnings.Add($"Plate {plateId}, part {partIndex} ('{part.BaseDrawing.Name}'): its drawing pins "
|
|
+ "setting could not be read; the drawing's cut pins apply.");
|
|
return false;
|
|
}
|
|
|
|
// Single enum names only: Enum.TryParse also accepts numbers and comma lists.
|
|
private static bool TryParseName<T>(string text, out T value)
|
|
where T : struct, Enum
|
|
{
|
|
value = default;
|
|
return !string.IsNullOrEmpty(text)
|
|
&& text.All(char.IsAsciiLetter)
|
|
&& Enum.TryParse(text, ignoreCase: false, out value)
|
|
&& Enum.IsDefined(value);
|
|
}
|
|
|
|
private void ReadBestFits(Dictionary<int, Drawing> drawingMap)
|
|
{
|
|
if (BestFitStore == null)
|
|
return;
|
|
|
|
foreach (var kvp in drawingMap)
|
|
{
|
|
var entry = zipArchive.GetEntry($"bestfits/bestfit-{kvp.Key}");
|
|
if (entry == null)
|
|
continue;
|
|
|
|
using var entryStream = entry.Open();
|
|
using var reader = new StreamReader(entryStream);
|
|
var json = reader.ReadToEnd();
|
|
|
|
var sets = JsonSerializer.Deserialize<List<BestFitSetDto>>(json, JsonOptions);
|
|
if (sets == null)
|
|
continue;
|
|
|
|
BestFitStore.Import(kvp.Value, sets.Select(FromDto).ToList());
|
|
}
|
|
}
|
|
|
|
private static SavedPairSet FromDto(BestFitSetDto set) =>
|
|
new(
|
|
set.PlateWidth,
|
|
set.PlateHeight,
|
|
set.Spacing,
|
|
set.Results.Select(r => new SavedPairResult(
|
|
r.Part1Rotation,
|
|
r.Part2Rotation,
|
|
r.Part2OffsetX,
|
|
r.Part2OffsetY,
|
|
r.StrategyType,
|
|
r.TestNumber,
|
|
r.CandidateSpacing,
|
|
r.RotatedArea,
|
|
r.BoundingWidth,
|
|
r.BoundingHeight,
|
|
r.OptimalRotation,
|
|
r.Keep,
|
|
r.Reason,
|
|
r.TrueArea,
|
|
r.HullAngles
|
|
)).ToList(),
|
|
set.CanonicalAngle
|
|
);
|
|
|
|
/// <summary>
|
|
/// The saved machine clamp rail, or null when absent, empty or malformed (a bad rail
|
|
/// is ignored rather than failing the nest; each plate's saved zones still load). A
|
|
/// missing or invalid removal-minutes value falls back to <see cref="ClampRail.DefaultRemovalMinutes"/>.
|
|
/// </summary>
|
|
/// <summary>The saved support bed, or null when absent or invalid (a bad bed never fails the nest).</summary>
|
|
private static SupportBed ReadSupportBed(SupportBedDto dto)
|
|
{
|
|
if (dto == null)
|
|
return null;
|
|
try
|
|
{
|
|
if (dto.Type != "slats")
|
|
return new SupportGrid(dto.PitchX, dto.PitchY, dto.OffsetX, dto.OffsetY,
|
|
dto.RegistrationTolerance, dto.StabilityMargin);
|
|
SlatAxis? axis = dto.Axis switch
|
|
{
|
|
"x" => SlatAxis.X,
|
|
"y" => SlatAxis.Y,
|
|
_ => null,
|
|
};
|
|
return axis is { } along
|
|
? new SupportSlats(along, dto.Pitch, dto.Thickness, dto.Offset,
|
|
dto.RegistrationTolerance, dto.StabilityMargin)
|
|
: null;
|
|
}
|
|
catch (ArgumentOutOfRangeException)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private static ClampRail ReadClampRail(ClampRailDto dto)
|
|
{
|
|
if (dto == null)
|
|
return null;
|
|
var clamps = Edge(dto.Centers, ClampEdge.OriginSide)
|
|
.Concat(Edge(dto.FarSideCenters, ClampEdge.FarSide))
|
|
.Concat(Edge(dto.OriginEndCenters, ClampEdge.OriginEnd))
|
|
.Concat(Edge(dto.FarEndCenters, ClampEdge.FarEnd))
|
|
.ToList();
|
|
if (clamps.Count == 0)
|
|
return null;
|
|
try
|
|
{
|
|
// An invalid removal time (negative or not finite) falls back to the default
|
|
// rather than dropping the rail, which would also discard every sheet's
|
|
// removed-clamp state.
|
|
var minutes = dto.RemovalMinutes is double m && double.IsFinite(m) && m >= 0
|
|
? m
|
|
: ClampRail.DefaultRemovalMinutes;
|
|
return new ClampRail(dto.Width, dto.Depth, clamps, minutes);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
static IEnumerable<RailClamp> Edge(List<double> centers, ClampEdge edge) =>
|
|
centers?.Select(c => new RailClamp(edge, c)) ?? Enumerable.Empty<RailClamp>();
|
|
}
|
|
|
|
private Nest BuildNest(NestDto dto, Dictionary<int, Drawing> drawingMap)
|
|
{
|
|
var nest = new Nest();
|
|
nest.Name = dto.Name;
|
|
|
|
Units units;
|
|
if (Enum.TryParse(dto.Units, true, out units))
|
|
nest.Units = units;
|
|
|
|
nest.Customer = dto.Customer;
|
|
nest.DateCreated = DateTime.Parse(dto.DateCreated);
|
|
nest.DateLastModified = DateTime.Parse(dto.DateLastModified);
|
|
nest.Notes = dto.Notes;
|
|
nest.AssistGas = dto.AssistGas ?? "";
|
|
// Additive v2 fields: absent or unknown values keep the defaults.
|
|
nest.Status = Enum.TryParse<NestStatus>(dto.Status, true, out var status)
|
|
? status
|
|
: NestStatus.Quote;
|
|
nest.MadeBy = dto.MadeBy ?? "";
|
|
nest.Machine = dto.Machine is { } machine ? new NestMachine(machine.Id, machine.Name) : null;
|
|
|
|
// Nest-level material and thickness (fall back to PlateDefaults for old files)
|
|
var pd = dto.PlateDefaults;
|
|
var matDto = dto.Material ?? pd.Material;
|
|
nest.Thickness = dto.Thickness > 0 ? dto.Thickness : pd.Thickness;
|
|
nest.Material = new Material(matDto.Name, matDto.Grade, matDto.Density);
|
|
|
|
// Plate defaults
|
|
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(pd.Size.Width, pd.Size.Length);
|
|
nest.PlateDefaults.Quadrant = pd.Quadrant;
|
|
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
|
|
nest.PlateDefaults.EdgeSpacing = new Spacing(
|
|
pd.EdgeSpacing.Left,
|
|
pd.EdgeSpacing.Bottom,
|
|
pd.EdgeSpacing.Right,
|
|
pd.EdgeSpacing.Top
|
|
);
|
|
var clampRail = ReadClampRail(pd.ClampRail);
|
|
nest.PlateDefaults.ClampRail = clampRail;
|
|
// Plates read below follow the nest's bed as they are added.
|
|
nest.SetSupportBed(ReadSupportBed(pd.SupportBed));
|
|
|
|
// Stock options: sheet sizes and salvage rate
|
|
nest.SalvageRate = dto.SalvageRate;
|
|
nest.MinimumRemnantDimension = double.IsFinite(dto.MinimumRemnantDimension)
|
|
&& dto.MinimumRemnantDimension > 0
|
|
? dto.MinimumRemnantDimension
|
|
: Nest.DefaultMinimumRemnantDimension;
|
|
if (dto.PlateOptions != null)
|
|
{
|
|
nest.PlateOptions = dto
|
|
.PlateOptions.Select(o => new PlateOption
|
|
{
|
|
Width = o.Width,
|
|
Length = o.Length,
|
|
Cost = o.Cost,
|
|
})
|
|
.ToList();
|
|
}
|
|
|
|
// Drawings
|
|
foreach (var d in drawingMap.OrderBy(k => k.Key))
|
|
nest.Drawings.Add(d.Value);
|
|
|
|
// Plates
|
|
foreach (var p in dto.Plates.OrderBy(p => p.Id))
|
|
{
|
|
var plate = new Plate();
|
|
plate.Size = new OpenNest.Geometry.Size(p.Size.Width, p.Size.Length);
|
|
plate.Quadrant = p.Quadrant;
|
|
plate.Quantity = p.Quantity;
|
|
plate.PartSpacing = p.PartSpacing;
|
|
plate.EdgeSpacing = new Spacing(
|
|
p.EdgeSpacing.Left,
|
|
p.EdgeSpacing.Bottom,
|
|
p.EdgeSpacing.Right,
|
|
p.EdgeSpacing.Top
|
|
);
|
|
plate.GrainAngle = p.GrainAngle;
|
|
plate.CuttingParameters = CuttingParametersSerializer.FromDto(p.CuttingParameters);
|
|
|
|
// Clamp zones: older files have no key at all; an individually invalid
|
|
// entry is dropped rather than poisoning the whole nest.
|
|
if (p.Clamps != null)
|
|
{
|
|
foreach (var c in p.Clamps)
|
|
{
|
|
try
|
|
{
|
|
plate.ClampZones.Add(
|
|
new ClampZone(c.Id, new OpenNest.Geometry.Box(c.X, c.Y, c.Length, c.Width))
|
|
);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
// Invalid zone definition; ignore this one entry.
|
|
}
|
|
}
|
|
}
|
|
|
|
// A plate follows the nest rail only when its saved zones are exactly the
|
|
// rail's for this sheet minus any saved removed-clamp ids (C13); otherwise
|
|
// the saved zones are kept as static, explicit data.
|
|
var removedClamps = p.RemovedClamps?
|
|
.Where(id => !string.IsNullOrEmpty(id))
|
|
.ToHashSet(StringComparer.Ordinal)
|
|
?? new HashSet<string>();
|
|
// A sheet saved with its own rail (not the nest default) follows that one.
|
|
var plateRail = ReadClampRail(p.ClampRail) ?? clampRail;
|
|
if (plateRail != null && plateRail.Matches(plate, removedClamps))
|
|
plate.SetClampRail(plateRail, removedClamps);
|
|
|
|
// Codes name sheets in order, so a blank entry ends the coded sheets.
|
|
if (p.StockItemCodes != null)
|
|
plate.StockItemCodes = p.StockItemCodes.TakeWhile(code => !string.IsNullOrWhiteSpace(code)).ToList();
|
|
|
|
for (var partIndex = 0; partIndex < p.Parts.Count; partIndex++)
|
|
{
|
|
var partDto = p.Parts[partIndex];
|
|
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
|
|
continue;
|
|
|
|
var part = new Part(dwg);
|
|
part.Rotate(partDto.Rotation);
|
|
part.Offset(new Vector(partDto.X, partDto.Y));
|
|
RestorePartProgram(part, partDto, p.Id, partIndex);
|
|
part.DrawingPinsOff = ReadPinsOff(partDto, p.Id, partIndex, part);
|
|
plate.Parts.Add(part);
|
|
}
|
|
|
|
// Cut-offs
|
|
if (p.CutOffs != null)
|
|
{
|
|
var sequence = new Dictionary<CutOff, IReadOnlyList<int>>();
|
|
|
|
foreach (var cutoffDto in p.CutOffs)
|
|
{
|
|
var axis =
|
|
cutoffDto.Axis?.ToLowerInvariant() == "horizontal"
|
|
? CutOffAxis.Horizontal
|
|
: CutOffAxis.Vertical;
|
|
var cutoff = new CutOff(new Vector(cutoffDto.X, cutoffDto.Y), axis)
|
|
{
|
|
StartLimit = cutoffDto.StartLimit,
|
|
EndLimit = cutoffDto.EndLimit,
|
|
// A nonpositive or nonfinite overcut is no head-locked overcut.
|
|
HeadLockOvercut = double.IsFinite(cutoffDto.HeadLockOvercut)
|
|
&& cutoffDto.HeadLockOvercut > 0 ? cutoffDto.HeadLockOvercut : 0,
|
|
FarEdgeFirst = cutoffDto.FarEdgeFirst,
|
|
};
|
|
plate.CutOffs.Add(cutoff);
|
|
|
|
if (cutoffDto.Segments is { Length: > 0 } segments)
|
|
sequence[cutoff] = segments;
|
|
else if (cutoffDto.Sequence is int index)
|
|
sequence[cutoff] = [index];
|
|
}
|
|
|
|
plate.RegenerateCutOffs(new CutOffSettings(), sequence);
|
|
}
|
|
|
|
nest.Plates.Add(plate);
|
|
}
|
|
|
|
return nest;
|
|
}
|
|
}
|
|
}
|