using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.IO.Compression; using System.Linq; using System.Text.Json; using OpenNest.Bending; using OpenNest.CNC; using OpenNest.Converters; using OpenNest.Engine.BestFit; using OpenNest.Geometry; using static OpenNest.IO.NestFormat; namespace OpenNest.IO { public sealed class NestReader { private readonly Stream stream; private readonly ZipArchive zipArchive; private readonly Dictionary drawingHashes = new(); private readonly List warnings = new(); /// Part programs that could not be restored; other parts still load. public IReadOnlyList Warnings => warnings.AsReadOnly(); 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(nestJson, JsonOptions); var programs = ReadPrograms(dto.Drawings.Count); var entitySets = ReadEntitySets(dto.Drawings.Count); var drawingMap = BuildDrawings(dto, programs, entitySets); ReadBestFits(drawingMap); var nest = BuildNest(dto, drawingMap); 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(); } private Dictionary ReadPrograms(int count) { var programs = new Dictionary(); for (var i = 1; i <= count; i++) { var name = $"programs/program-{i}"; if (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()) 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."); } 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 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 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 entities, HashSet suppressed)> ReadEntitySets( int count ) { var result = new Dictionary, HashSet)>(); for (var i = 1; i <= count; i++) { var entry = zipArchive.GetEntry($"entities/entities-{i}"); if (entry == null) continue; using var entryStream = entry.Open(); using var reader = new StreamReader(entryStream); var json = reader.ReadToEnd(); var dto = JsonSerializer.Deserialize(json, JsonOptions); result[i] = EntitySerializer.FromDto(dto); } return result; } private Dictionary BuildDrawings( NestDto dto, Dictionary programs, Dictionary entities, HashSet suppressed)> entitySets ) { var map = new Dictionary(); foreach (var d in dto.Drawings) { 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.Quantity.Required = d.Quantity.Required; drawing.Priority = d.Priority; 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( 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; // Older files saved etch marks as cut moves; restore their Scribe layer so // nesting ignores them. The program was mutated in place, so refresh. if ( drawing.Program != null && ScribeLayerRepair.Apply(drawing.Program, drawing.SourceEntities, drawing.Source.Offset) > 0 ) { drawing.UpdateArea(); drawing.RecomputeCanonicalAngle(); } } map[d.Id] = drawing; } return map; } private void ReadBestFits(Dictionary drawingMap) { 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>(json, JsonOptions); if (sets == null) continue; PopulateBestFitSets(kvp.Value, sets); } } private void PopulateBestFitSets(Drawing drawing, List sets) { foreach (var set in sets) { var results = set .Results.Select(r => new BestFitResult { Candidate = new PairCandidate { Drawing = drawing, Part1Rotation = r.Part1Rotation, Part2Rotation = r.Part2Rotation, Part2Offset = new Vector(r.Part2OffsetX, r.Part2OffsetY), StrategyIndex = r.StrategyType, TestNumber = r.TestNumber, Spacing = r.CandidateSpacing, }, RotatedArea = r.RotatedArea, BoundingWidth = r.BoundingWidth, BoundingHeight = r.BoundingHeight, OptimalRotation = r.OptimalRotation, Keep = r.Keep, Reason = r.Reason, TrueArea = r.TrueArea, HullAngles = r.HullAngles, }) .ToList(); BestFitCache.Populate( drawing, set.PlateWidth, set.PlateHeight, set.Spacing, results ); } } private Nest BuildNest(NestDto dto, Dictionary 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(dto.Status, true, out var status) ? status : NestStatus.Quote; nest.MadeBy = dto.MadeBy ?? ""; // 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 ); // Plate optimizer settings nest.SalvageRate = dto.SalvageRate; 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); 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); plate.Parts.Add(part); } // Cut-offs if (p.CutOffs != null) { var sequence = new Dictionary(); 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, }; plate.CutOffs.Add(cutoff); if (cutoffDto.Sequence is int index) sequence[cutoff] = index; } plate.RegenerateCutOffs(new CutOffSettings(), sequence); } nest.Plates.Add(plate); } return nest; } } }