feat: replace XML nest file format with JSON (v2)
Replace three separate XML metadata files (info, drawing-info, plate-info) and per-plate G-code placement files with a single nest.json inside the ZIP archive. Programs remain as G-code text under a programs/ folder. This eliminates ~400 lines of hand-written XML read/write code and fragile ID-based dictionary linking. Now uses System.Text.Json with DTO records for clean serialization. Also adds Priority and Constraints fields to drawing serialization (previously omitted). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class NestFormat
|
||||||
|
{
|
||||||
|
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 PlateDefaultsDto PlateDefaults { get; init; } = new();
|
||||||
|
public List<DrawingDto> Drawings { get; init; } = new();
|
||||||
|
public List<PlateDto> Plates { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DrawingDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public ColorDto Color { get; init; } = new();
|
||||||
|
public QuantityDto Quantity { get; init; } = new();
|
||||||
|
public int Priority { get; init; }
|
||||||
|
public ConstraintsDto Constraints { get; init; } = new();
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SourceDto Source { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public int Quantity { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
public List<PartDto> Parts { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PartDto
|
||||||
|
{
|
||||||
|
public int DrawingId { get; init; }
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
public double Rotation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SizeDto
|
||||||
|
{
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+109
-426
@@ -1,45 +1,28 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.Json;
|
||||||
using System.Xml;
|
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Geometry;
|
using OpenNest.Geometry;
|
||||||
using OpenNest.Math;
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
{
|
{
|
||||||
public sealed class NestReader
|
public sealed class NestReader
|
||||||
{
|
{
|
||||||
private ZipArchive zipArchive;
|
private readonly Stream stream;
|
||||||
private Dictionary<int, Plate> plateDict;
|
private readonly ZipArchive zipArchive;
|
||||||
private Dictionary<int, Drawing> drawingDict;
|
|
||||||
private Dictionary<int, Program> programDict;
|
|
||||||
private Dictionary<int, Program> plateProgramDict;
|
|
||||||
private Stream stream;
|
|
||||||
private Nest nest;
|
|
||||||
|
|
||||||
private NestReader()
|
|
||||||
{
|
|
||||||
plateDict = new Dictionary<int, Plate>();
|
|
||||||
drawingDict = new Dictionary<int, Drawing>();
|
|
||||||
programDict = new Dictionary<int, Program>();
|
|
||||||
plateProgramDict = new Dictionary<int, Program>();
|
|
||||||
nest = new Nest();
|
|
||||||
}
|
|
||||||
|
|
||||||
public NestReader(string file)
|
public NestReader(string file)
|
||||||
: this()
|
|
||||||
{
|
{
|
||||||
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
|
||||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
}
|
}
|
||||||
|
|
||||||
public NestReader(Stream stream)
|
public NestReader(Stream stream)
|
||||||
: this()
|
|
||||||
{
|
{
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
|
||||||
@@ -47,52 +30,12 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public Nest Read()
|
public Nest Read()
|
||||||
{
|
{
|
||||||
const string plateExtensionPattern = "plate-\\d\\d\\d";
|
var nestJson = ReadEntry("nest.json");
|
||||||
const string programExtensionPattern = "program-\\d\\d\\d";
|
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
|
||||||
|
|
||||||
foreach (var entry in zipArchive.Entries)
|
var programs = ReadPrograms(dto.Drawings.Count);
|
||||||
{
|
var drawingMap = BuildDrawings(dto, programs);
|
||||||
var memstream = new MemoryStream();
|
var nest = BuildNest(dto, drawingMap);
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
entryStream.CopyTo(memstream);
|
|
||||||
}
|
|
||||||
|
|
||||||
memstream.Position = 0;
|
|
||||||
|
|
||||||
switch (entry.FullName)
|
|
||||||
{
|
|
||||||
case "info":
|
|
||||||
ReadNestInfo(memstream);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "drawing-info":
|
|
||||||
ReadDrawingInfo(memstream);
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case "plate-info":
|
|
||||||
ReadPlateInfo(memstream);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Regex.IsMatch(entry.FullName, programExtensionPattern))
|
|
||||||
{
|
|
||||||
ReadProgram(memstream, entry.FullName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Regex.IsMatch(entry.FullName, plateExtensionPattern))
|
|
||||||
{
|
|
||||||
ReadPlate(memstream, entry.FullName);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LinkProgramsToDrawings();
|
|
||||||
LinkPartsToPlates();
|
|
||||||
|
|
||||||
AddPlatesToNest();
|
|
||||||
AddDrawingsToNest();
|
|
||||||
|
|
||||||
zipArchive.Dispose();
|
zipArchive.Dispose();
|
||||||
stream.Close();
|
stream.Close();
|
||||||
@@ -100,374 +43,114 @@ namespace OpenNest.IO
|
|||||||
return nest;
|
return nest;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadNestInfo(Stream stream)
|
private string ReadEntry(string name)
|
||||||
{
|
{
|
||||||
var reader = XmlReader.Create(stream);
|
var entry = zipArchive.GetEntry(name)
|
||||||
var spacing = new Spacing();
|
?? throw new InvalidDataException($"Nest file is missing required entry '{name}'.");
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
using var reader = new StreamReader(entryStream);
|
||||||
|
return reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
while (reader.Read())
|
private Dictionary<int, Program> ReadPrograms(int count)
|
||||||
|
{
|
||||||
|
var programs = new Dictionary<int, Program>();
|
||||||
|
for (var i = 1; i <= count; i++)
|
||||||
{
|
{
|
||||||
if (!reader.IsStartElement())
|
var entry = zipArchive.GetEntry($"programs/program-{i}");
|
||||||
continue;
|
if (entry == null) continue;
|
||||||
|
|
||||||
switch (reader.Name)
|
using var entryStream = entry.Open();
|
||||||
|
var memStream = new MemoryStream();
|
||||||
|
entryStream.CopyTo(memStream);
|
||||||
|
memStream.Position = 0;
|
||||||
|
|
||||||
|
var reader = new ProgramReader(memStream);
|
||||||
|
programs[i] = reader.Read();
|
||||||
|
}
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<int, Drawing>();
|
||||||
|
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 (programs.TryGetValue(d.Id, out var pgm))
|
||||||
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
map[d.Id] = drawing;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Plate defaults
|
||||||
|
var pd = dto.PlateDefaults;
|
||||||
|
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(pd.Size.Width, pd.Size.Height);
|
||||||
|
nest.PlateDefaults.Thickness = pd.Thickness;
|
||||||
|
nest.PlateDefaults.Quadrant = pd.Quadrant;
|
||||||
|
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
|
||||||
|
nest.PlateDefaults.Material = new Material(pd.Material.Name, pd.Material.Grade, pd.Material.Density);
|
||||||
|
nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
// 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.Height);
|
||||||
|
plate.Thickness = p.Thickness;
|
||||||
|
plate.Quadrant = p.Quadrant;
|
||||||
|
plate.Quantity = p.Quantity;
|
||||||
|
plate.PartSpacing = p.PartSpacing;
|
||||||
|
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density);
|
||||||
|
plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
foreach (var partDto in p.Parts)
|
||||||
{
|
{
|
||||||
case "Nest":
|
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
|
||||||
nest.Name = reader["name"];
|
continue;
|
||||||
break;
|
|
||||||
|
|
||||||
case "Units":
|
var part = new Part(dwg);
|
||||||
Units units;
|
part.Rotate(partDto.Rotation);
|
||||||
TryParseEnum<Units>(reader.ReadString(), out units);
|
part.Offset(new Vector(partDto.X, partDto.Y));
|
||||||
nest.Units = units;
|
plate.Parts.Add(part);
|
||||||
break;
|
|
||||||
|
|
||||||
case "Customer":
|
|
||||||
nest.Customer = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "DateCreated":
|
|
||||||
nest.DateCreated = DateTime.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "DateLastModified":
|
|
||||||
nest.DateLastModified = DateTime.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Notes":
|
|
||||||
nest.Notes = Uri.UnescapeDataString(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Size":
|
|
||||||
nest.PlateDefaults.Size = OpenNest.Geometry.Size.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Thickness":
|
|
||||||
nest.PlateDefaults.Thickness = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Quadrant":
|
|
||||||
nest.PlateDefaults.Quadrant = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "PartSpacing":
|
|
||||||
nest.PlateDefaults.PartSpacing = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
nest.PlateDefaults.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
nest.PlateDefaults.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
nest.PlateDefaults.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Left":
|
|
||||||
spacing.Left = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Right":
|
|
||||||
spacing.Right = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Top":
|
|
||||||
spacing.Top = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Bottom":
|
|
||||||
spacing.Bottom = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
}
|
}
|
||||||
|
|
||||||
reader.Close();
|
return nest;
|
||||||
nest.PlateDefaults.EdgeSpacing = spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadDrawingInfo(Stream stream)
|
|
||||||
{
|
|
||||||
var reader = XmlReader.Create(stream);
|
|
||||||
Drawing drawing = null;
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (!reader.IsStartElement())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Drawing":
|
|
||||||
var id = int.Parse(reader["id"]);
|
|
||||||
var name = reader["name"];
|
|
||||||
|
|
||||||
drawingDict.Add(id, (drawing = new Drawing(name)));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Customer":
|
|
||||||
drawing.Customer = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Color":
|
|
||||||
{
|
|
||||||
var parts = reader.ReadString().Split(',');
|
|
||||||
|
|
||||||
if (parts.Length == 3)
|
|
||||||
{
|
|
||||||
byte r = byte.Parse(parts[0]);
|
|
||||||
byte g = byte.Parse(parts[1]);
|
|
||||||
byte b = byte.Parse(parts[2]);
|
|
||||||
|
|
||||||
drawing.Color = Color.FromArgb(r, g, b);
|
|
||||||
}
|
|
||||||
else if (parts.Length == 4)
|
|
||||||
{
|
|
||||||
byte a = byte.Parse(parts[0]);
|
|
||||||
byte r = byte.Parse(parts[1]);
|
|
||||||
byte g = byte.Parse(parts[2]);
|
|
||||||
byte b = byte.Parse(parts[3]);
|
|
||||||
|
|
||||||
drawing.Color = Color.FromArgb(a, r, g, b);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Required":
|
|
||||||
drawing.Quantity.Required = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
drawing.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
drawing.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
drawing.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Path":
|
|
||||||
drawing.Source.Path = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Offset":
|
|
||||||
{
|
|
||||||
var parts = reader.ReadString().Split(',');
|
|
||||||
|
|
||||||
if (parts.Length != 2)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
drawing.Source.Offset = new Vector(double.Parse(parts[0]), double.Parse(parts[1]));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadPlateInfo(Stream stream)
|
|
||||||
{
|
|
||||||
var reader = XmlReader.Create(stream);
|
|
||||||
var spacing = new Spacing();
|
|
||||||
Plate plate = null;
|
|
||||||
|
|
||||||
while (reader.Read())
|
|
||||||
{
|
|
||||||
if (!reader.IsStartElement())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
switch (reader.Name)
|
|
||||||
{
|
|
||||||
case "Plate":
|
|
||||||
var id = int.Parse(reader["id"]);
|
|
||||||
|
|
||||||
if (plate != null)
|
|
||||||
plate.EdgeSpacing = spacing;
|
|
||||||
|
|
||||||
plateDict.Add(id, (plate = new Plate()));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Size":
|
|
||||||
plate.Size = OpenNest.Geometry.Size.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Qty":
|
|
||||||
plate.Quantity = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Thickness":
|
|
||||||
plate.Thickness = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Quadrant":
|
|
||||||
plate.Quadrant = int.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "PartSpacing":
|
|
||||||
plate.PartSpacing = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Name":
|
|
||||||
plate.Material.Name = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Grade":
|
|
||||||
plate.Material.Grade = reader.ReadString();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Density":
|
|
||||||
plate.Material.Density = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Left":
|
|
||||||
spacing.Left = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Right":
|
|
||||||
spacing.Right = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Top":
|
|
||||||
spacing.Top = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "Bottom":
|
|
||||||
spacing.Bottom = double.Parse(reader.ReadString());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plate != null)
|
|
||||||
plate.EdgeSpacing = spacing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadProgram(Stream stream, string name)
|
|
||||||
{
|
|
||||||
var id = GetProgramId(name);
|
|
||||||
var reader = new ProgramReader(stream);
|
|
||||||
var pgm = reader.Read();
|
|
||||||
programDict.Add(id, pgm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReadPlate(Stream stream, string name)
|
|
||||||
{
|
|
||||||
var id = GetPlateId(name);
|
|
||||||
var reader = new ProgramReader(stream);
|
|
||||||
var pgm = reader.Read();
|
|
||||||
plateProgramDict.Add(id, pgm);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkProgramsToDrawings()
|
|
||||||
{
|
|
||||||
foreach (var drawingItem in drawingDict)
|
|
||||||
{
|
|
||||||
Program pgm;
|
|
||||||
|
|
||||||
if (programDict.TryGetValue(drawingItem.Key, out pgm))
|
|
||||||
drawingItem.Value.Program = pgm;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LinkPartsToPlates()
|
|
||||||
{
|
|
||||||
foreach (var plateProgram in plateProgramDict)
|
|
||||||
{
|
|
||||||
var parts = CreateParts(plateProgram.Value);
|
|
||||||
|
|
||||||
Plate plate;
|
|
||||||
|
|
||||||
if (!plateDict.TryGetValue(plateProgram.Key, out plate))
|
|
||||||
plate = new Plate();
|
|
||||||
|
|
||||||
plate.Parts.AddRange(parts);
|
|
||||||
plateDict[plateProgram.Key] = plate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddPlatesToNest()
|
|
||||||
{
|
|
||||||
var plates = plateDict.OrderBy(i => i.Key).Select(i => i.Value).ToList();
|
|
||||||
nest.Plates.AddRange(plates);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddDrawingsToNest()
|
|
||||||
{
|
|
||||||
var drawings = drawingDict.OrderBy(i => i.Key).Select(i => i.Value).ToList();
|
|
||||||
drawings.ForEach(d => nest.Drawings.Add(d));
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Part> CreateParts(Program pgm)
|
|
||||||
{
|
|
||||||
var parts = new List<Part>();
|
|
||||||
var pos = Vector.Zero;
|
|
||||||
|
|
||||||
for (int i = 0; i < pgm.Codes.Count; i++)
|
|
||||||
{
|
|
||||||
var code = pgm.Codes[i];
|
|
||||||
|
|
||||||
switch (code.Type)
|
|
||||||
{
|
|
||||||
case CodeType.RapidMove:
|
|
||||||
pos = ((RapidMove)code).EndPoint;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case CodeType.SubProgramCall:
|
|
||||||
var subpgm = (SubProgramCall)code;
|
|
||||||
var dwg = drawingDict[subpgm.Id];
|
|
||||||
var part = new Part(dwg);
|
|
||||||
part.Rotate(Angle.ToRadians(subpgm.Rotation));
|
|
||||||
part.Offset(pos);
|
|
||||||
parts.Add(part);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parts;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetPlateId(string name)
|
|
||||||
{
|
|
||||||
return int.Parse(name.Replace("plate-", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int GetProgramId(string name)
|
|
||||||
{
|
|
||||||
return int.Parse(name.Replace("program-", ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T ParseEnum<T>(string value)
|
|
||||||
{
|
|
||||||
return (T)Enum.Parse(typeof(T), value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryParseEnum<T>(string value, out T e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
e = ParseEnum<T>(value);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
e = ParseEnum<T>(typeof(T).GetEnumValues().GetValue(0).ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum NestInfoSection
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
DefaultPlate,
|
|
||||||
Material,
|
|
||||||
EdgeSpacing,
|
|
||||||
Source
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+132
-233
@@ -1,32 +1,22 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Xml;
|
using System.Text.Json;
|
||||||
using OpenNest.CNC;
|
using OpenNest.CNC;
|
||||||
using OpenNest.Math;
|
using OpenNest.Math;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
namespace OpenNest.IO
|
namespace OpenNest.IO
|
||||||
{
|
{
|
||||||
public sealed class NestWriter
|
public sealed class NestWriter
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Number of decimal places the output is round to.
|
|
||||||
/// This number must have more decimal places than Tolerance.Epsilon
|
|
||||||
/// </summary>
|
|
||||||
private const int OutputPrecision = 10;
|
private const int OutputPrecision = 10;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fixed-point format string that avoids scientific notation.
|
|
||||||
/// ProgramReader treats 'E' as a code letter, so "6.66E-08" would be
|
|
||||||
/// split into X:"6.66" and E:"-08", corrupting the parsed value.
|
|
||||||
/// </summary>
|
|
||||||
private const string CoordinateFormat = "0.##########";
|
private const string CoordinateFormat = "0.##########";
|
||||||
|
|
||||||
private readonly Nest nest;
|
private readonly Nest nest;
|
||||||
private ZipArchive zipArchive;
|
|
||||||
private Dictionary<int, Drawing> drawingDict;
|
private Dictionary<int, Drawing> drawingDict;
|
||||||
|
|
||||||
public NestWriter(Nest nest)
|
public NestWriter(Nest nest)
|
||||||
@@ -37,27 +27,21 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
public bool Write(string file)
|
public bool Write(string file)
|
||||||
{
|
{
|
||||||
this.nest.DateLastModified = DateTime.Now;
|
nest.DateLastModified = DateTime.Now;
|
||||||
|
|
||||||
SetDrawingIds();
|
SetDrawingIds();
|
||||||
|
|
||||||
using (var fileStream = new FileStream(file, FileMode.Create))
|
using var fileStream = new FileStream(file, FileMode.Create);
|
||||||
using (zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create))
|
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||||
{
|
|
||||||
AddNestInfo();
|
WriteNestJson(zipArchive);
|
||||||
AddPlates();
|
WritePrograms(zipArchive);
|
||||||
AddPlateInfo();
|
|
||||||
AddDrawings();
|
|
||||||
AddDrawingInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetDrawingIds()
|
private void SetDrawingIds()
|
||||||
{
|
{
|
||||||
int id = 1;
|
var id = 1;
|
||||||
|
|
||||||
foreach (var drawing in nest.Drawings)
|
foreach (var drawing in nest.Drawings)
|
||||||
{
|
{
|
||||||
drawingDict.Add(id, drawing);
|
drawingDict.Add(id, drawing);
|
||||||
@@ -65,241 +49,156 @@ namespace OpenNest.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddNestInfo()
|
private void WriteNestJson(ZipArchive zipArchive)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var dto = BuildNestDto();
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
{
|
|
||||||
Indent = true
|
|
||||||
});
|
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
var entry = zipArchive.CreateEntry("nest.json");
|
||||||
writer.WriteStartElement("Nest");
|
using var stream = entry.Open();
|
||||||
writer.WriteAttributeString("name", nest.Name);
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
writer.WriteElementString("Units", nest.Units.ToString());
|
|
||||||
writer.WriteElementString("Customer", nest.Customer);
|
|
||||||
writer.WriteElementString("DateCreated", nest.DateCreated.ToString());
|
|
||||||
writer.WriteElementString("DateLastModified", nest.DateLastModified.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("DefaultPlate");
|
|
||||||
writer.WriteElementString("Size", nest.PlateDefaults.Size.ToString());
|
|
||||||
writer.WriteElementString("Thickness", nest.PlateDefaults.Thickness.ToString());
|
|
||||||
writer.WriteElementString("Quadrant", nest.PlateDefaults.Quadrant.ToString());
|
|
||||||
writer.WriteElementString("PartSpacing", nest.PlateDefaults.PartSpacing.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", nest.PlateDefaults.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", nest.PlateDefaults.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", nest.PlateDefaults.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("EdgeSpacing");
|
|
||||||
writer.WriteElementString("Left", nest.PlateDefaults.EdgeSpacing.Left.ToString());
|
|
||||||
writer.WriteElementString("Top", nest.PlateDefaults.EdgeSpacing.Top.ToString());
|
|
||||||
writer.WriteElementString("Right", nest.PlateDefaults.EdgeSpacing.Right.ToString());
|
|
||||||
writer.WriteElementString("Bottom", nest.PlateDefaults.EdgeSpacing.Bottom.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteElementString("Notes", Uri.EscapeDataString(nest.Notes));
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // DefaultPlate
|
|
||||||
writer.WriteEndElement(); // Nest
|
|
||||||
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPlates()
|
private NestDto BuildNestDto()
|
||||||
{
|
{
|
||||||
int num = 1;
|
return new NestDto
|
||||||
|
|
||||||
foreach (var plate in nest.Plates)
|
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
Version = 2,
|
||||||
var name = string.Format("plate-{0}", num.ToString().PadLeft(3, '0'));
|
Name = nest.Name ?? "",
|
||||||
|
Units = nest.Units.ToString(),
|
||||||
|
Customer = nest.Customer ?? "",
|
||||||
|
DateCreated = nest.DateCreated.ToString("o"),
|
||||||
|
DateLastModified = nest.DateLastModified.ToString("o"),
|
||||||
|
Notes = nest.Notes ?? "",
|
||||||
|
PlateDefaults = BuildPlateDefaultsDto(),
|
||||||
|
Drawings = BuildDrawingDtos(),
|
||||||
|
Plates = BuildPlateDtos()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
WritePlate(stream, plate);
|
private PlateDefaultsDto BuildPlateDefaultsDto()
|
||||||
|
{
|
||||||
var entry = zipArchive.CreateEntry(name);
|
var pd = nest.PlateDefaults;
|
||||||
using (var entryStream = entry.Open())
|
return new PlateDefaultsDto
|
||||||
|
{
|
||||||
|
Size = new SizeDto { Width = pd.Size.Width, Height = pd.Size.Height },
|
||||||
|
Thickness = pd.Thickness,
|
||||||
|
Quadrant = pd.Quadrant,
|
||||||
|
PartSpacing = pd.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
{
|
{
|
||||||
stream.CopyTo(entryStream);
|
Name = pd.Material.Name ?? "",
|
||||||
|
Grade = pd.Material.Grade ?? "",
|
||||||
|
Density = pd.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = pd.EdgeSpacing.Left,
|
||||||
|
Top = pd.EdgeSpacing.Top,
|
||||||
|
Right = pd.EdgeSpacing.Right,
|
||||||
|
Bottom = pd.EdgeSpacing.Bottom
|
||||||
}
|
}
|
||||||
|
};
|
||||||
num++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddPlateInfo()
|
private List<DrawingDto> BuildDrawingDtos()
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
var list = new List<DrawingDto>();
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
{
|
{
|
||||||
Indent = true
|
var d = kvp.Value;
|
||||||
});
|
list.Add(new DrawingDto
|
||||||
|
{
|
||||||
|
Id = kvp.Key,
|
||||||
|
Name = d.Name ?? "",
|
||||||
|
Customer = d.Customer ?? "",
|
||||||
|
Color = new ColorDto { A = d.Color.A, R = d.Color.R, G = d.Color.G, B = d.Color.B },
|
||||||
|
Quantity = new QuantityDto { Required = d.Quantity.Required },
|
||||||
|
Priority = d.Priority,
|
||||||
|
Constraints = new ConstraintsDto
|
||||||
|
{
|
||||||
|
StepAngle = d.Constraints.StepAngle,
|
||||||
|
StartAngle = d.Constraints.StartAngle,
|
||||||
|
EndAngle = d.Constraints.EndAngle,
|
||||||
|
Allow180Equivalent = d.Constraints.Allow180Equivalent
|
||||||
|
},
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = d.Material.Name ?? "",
|
||||||
|
Grade = d.Material.Grade ?? "",
|
||||||
|
Density = d.Material.Density
|
||||||
|
},
|
||||||
|
Source = new SourceDto
|
||||||
|
{
|
||||||
|
Path = d.Source.Path ?? "",
|
||||||
|
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
private List<PlateDto> BuildPlateDtos()
|
||||||
writer.WriteStartElement("Plates");
|
{
|
||||||
writer.WriteAttributeString("count", nest.Plates.Count.ToString());
|
var list = new List<PlateDto>();
|
||||||
|
for (var i = 0; i < nest.Plates.Count; i++)
|
||||||
for (int i = 0; i < nest.Plates.Count; ++i)
|
|
||||||
{
|
{
|
||||||
var plate = nest.Plates[i];
|
var plate = nest.Plates[i];
|
||||||
|
var parts = new List<PartDto>();
|
||||||
writer.WriteStartElement("Plate");
|
foreach (var part in plate.Parts)
|
||||||
writer.WriteAttributeString("id", (i + 1).ToString());
|
|
||||||
|
|
||||||
writer.WriteElementString("Quadrant", plate.Quadrant.ToString());
|
|
||||||
writer.WriteElementString("Thickness", plate.Thickness.ToString());
|
|
||||||
writer.WriteElementString("Size", plate.Size.ToString());
|
|
||||||
writer.WriteElementString("Qty", plate.Quantity.ToString());
|
|
||||||
writer.WriteElementString("PartSpacing", plate.PartSpacing.ToString());
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", plate.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", plate.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", plate.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("EdgeSpacing");
|
|
||||||
writer.WriteElementString("Left", plate.EdgeSpacing.Left.ToString());
|
|
||||||
writer.WriteElementString("Top", plate.EdgeSpacing.Top.ToString());
|
|
||||||
writer.WriteElementString("Right", plate.EdgeSpacing.Right.ToString());
|
|
||||||
writer.WriteElementString("Bottom", plate.EdgeSpacing.Bottom.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Plate
|
|
||||||
writer.Flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Plates
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("plate-info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AddDrawings()
|
|
||||||
{
|
|
||||||
int num = 1;
|
|
||||||
|
|
||||||
foreach (var dwg in nest.Drawings)
|
|
||||||
{
|
|
||||||
var stream = new MemoryStream();
|
|
||||||
var name = string.Format("program-{0}", num.ToString().PadLeft(3, '0'));
|
|
||||||
|
|
||||||
WriteDrawing(stream, dwg);
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry(name);
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
{
|
||||||
stream.CopyTo(entryStream);
|
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
||||||
|
parts.Add(new PartDto
|
||||||
|
{
|
||||||
|
DrawingId = match.Key,
|
||||||
|
X = part.Location.X,
|
||||||
|
Y = part.Location.Y,
|
||||||
|
Rotation = part.Rotation
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
num++;
|
list.Add(new PlateDto
|
||||||
|
{
|
||||||
|
Id = i + 1,
|
||||||
|
Size = new SizeDto { Width = plate.Size.Width, Height = plate.Size.Height },
|
||||||
|
Thickness = plate.Thickness,
|
||||||
|
Quadrant = plate.Quadrant,
|
||||||
|
Quantity = plate.Quantity,
|
||||||
|
PartSpacing = plate.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = plate.Material.Name ?? "",
|
||||||
|
Grade = plate.Material.Grade ?? "",
|
||||||
|
Density = plate.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = plate.EdgeSpacing.Left,
|
||||||
|
Top = plate.EdgeSpacing.Top,
|
||||||
|
Right = plate.EdgeSpacing.Right,
|
||||||
|
Bottom = plate.EdgeSpacing.Bottom
|
||||||
|
},
|
||||||
|
Parts = parts
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddDrawingInfo()
|
private void WritePrograms(ZipArchive zipArchive)
|
||||||
{
|
{
|
||||||
var stream = new MemoryStream();
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
|
||||||
{
|
{
|
||||||
Indent = true
|
var name = $"programs/program-{kvp.Key}";
|
||||||
});
|
var stream = new MemoryStream();
|
||||||
|
WriteDrawing(stream, kvp.Value);
|
||||||
|
|
||||||
writer.WriteStartDocument();
|
var entry = zipArchive.CreateEntry(name);
|
||||||
writer.WriteStartElement("Drawings");
|
using var entryStream = entry.Open();
|
||||||
writer.WriteAttributeString("count", nest.Drawings.Count.ToString());
|
|
||||||
|
|
||||||
int id = 1;
|
|
||||||
|
|
||||||
foreach (var drawing in nest.Drawings)
|
|
||||||
{
|
|
||||||
writer.WriteStartElement("Drawing");
|
|
||||||
writer.WriteAttributeString("id", id.ToString());
|
|
||||||
writer.WriteAttributeString("name", drawing.Name);
|
|
||||||
|
|
||||||
writer.WriteElementString("Customer", drawing.Customer);
|
|
||||||
writer.WriteElementString("Color", string.Format("{0}, {1}, {2}, {3}", drawing.Color.A, drawing.Color.R, drawing.Color.G, drawing.Color.B));
|
|
||||||
|
|
||||||
writer.WriteStartElement("Quantity");
|
|
||||||
writer.WriteElementString("Required", drawing.Quantity.Required.ToString());
|
|
||||||
writer.WriteElementString("Nested", drawing.Quantity.Nested.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("Material");
|
|
||||||
writer.WriteElementString("Name", drawing.Material.Name);
|
|
||||||
writer.WriteElementString("Grade", drawing.Material.Grade);
|
|
||||||
writer.WriteElementString("Density", drawing.Material.Density.ToString());
|
|
||||||
writer.WriteEndElement();
|
|
||||||
|
|
||||||
writer.WriteStartElement("Source");
|
|
||||||
writer.WriteElementString("Path", drawing.Source.Path);
|
|
||||||
writer.WriteElementString("Offset", string.Format("{0}, {1}",
|
|
||||||
drawing.Source.Offset.X,
|
|
||||||
drawing.Source.Offset.Y));
|
|
||||||
writer.WriteEndElement(); // Source
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Drawing
|
|
||||||
|
|
||||||
id++;
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.WriteEndElement(); // Drawings
|
|
||||||
writer.WriteEndDocument();
|
|
||||||
|
|
||||||
writer.Flush();
|
|
||||||
writer.Close();
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
|
|
||||||
var entry = zipArchive.CreateEntry("drawing-info");
|
|
||||||
using (var entryStream = entry.Open())
|
|
||||||
{
|
|
||||||
stream.CopyTo(entryStream);
|
stream.CopyTo(entryStream);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void WritePlate(Stream stream, Plate plate)
|
|
||||||
{
|
|
||||||
var writer = new StreamWriter(stream);
|
|
||||||
writer.AutoFlush = true;
|
|
||||||
writer.WriteLine("G90");
|
|
||||||
|
|
||||||
foreach (var part in plate.Parts)
|
|
||||||
{
|
|
||||||
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
|
||||||
var id = match.Key;
|
|
||||||
|
|
||||||
writer.WriteLine("G00X{0}Y{1}",
|
|
||||||
part.Location.X.ToString(CoordinateFormat),
|
|
||||||
part.Location.Y.ToString(CoordinateFormat));
|
|
||||||
writer.WriteLine("G65P{0}R{1}", id, Angle.ToDegrees(part.Rotation));
|
|
||||||
}
|
|
||||||
|
|
||||||
stream.Position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteDrawing(Stream stream, Drawing drawing)
|
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||||
{
|
{
|
||||||
var program = drawing.Program;
|
var program = drawing.Program;
|
||||||
@@ -308,7 +207,7 @@ namespace OpenNest.IO
|
|||||||
|
|
||||||
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
for (int i = 0; i < drawing.Program.Length; ++i)
|
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||||
{
|
{
|
||||||
var code = drawing.Program[i];
|
var code = drawing.Program[i];
|
||||||
writer.WriteLine(GetCodeString(code));
|
writer.WriteLine(GetCodeString(code));
|
||||||
|
|||||||
@@ -0,0 +1,767 @@
|
|||||||
|
# Nest File Format v2 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Replace the XML+G-code nest file format with a single `nest.json` metadata file plus `programs/` folder inside the ZIP archive.
|
||||||
|
|
||||||
|
**Architecture:** Add a `NestFormat` static class containing DTO records and shared JSON options. Rewrite `NestWriter` to serialize DTOs to JSON and write programs under `programs/`. Rewrite `NestReader` to deserialize JSON and read programs from `programs/`. Public API unchanged.
|
||||||
|
|
||||||
|
**Tech Stack:** `System.Text.Json` (built into .NET 8, no new packages needed)
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
| Action | File | Responsibility |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| Create | `OpenNest.IO/NestFormat.cs` | DTO records for JSON serialization + shared `JsonSerializerOptions` |
|
||||||
|
| Rewrite | `OpenNest.IO/NestWriter.cs` | Serialize nest to JSON + write programs to `programs/` folder |
|
||||||
|
| Rewrite | `OpenNest.IO/NestReader.cs` | Deserialize JSON + read programs from `programs/` folder |
|
||||||
|
|
||||||
|
No other files change. `ProgramReader.cs`, `DxfImporter.cs`, `DxfExporter.cs`, `Extensions.cs`, all domain model classes, and all caller sites remain untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 1: DTO Records and JSON Options
|
||||||
|
|
||||||
|
### Task 1: Create NestFormat.cs with DTO records
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `OpenNest.IO/NestFormat.cs`
|
||||||
|
|
||||||
|
These DTOs are the JSON shape — flat records that map 1:1 with the spec's JSON schema. They live in `OpenNest.IO` because they're serialization concerns, not domain model.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Create `NestFormat.cs`**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public static class NestFormat
|
||||||
|
{
|
||||||
|
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 PlateDefaultsDto PlateDefaults { get; init; } = new();
|
||||||
|
public List<DrawingDto> Drawings { get; init; } = new();
|
||||||
|
public List<PlateDto> Plates { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DrawingDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public string Name { get; init; } = "";
|
||||||
|
public string Customer { get; init; } = "";
|
||||||
|
public ColorDto Color { get; init; } = new();
|
||||||
|
public QuantityDto Quantity { get; init; } = new();
|
||||||
|
public int Priority { get; init; }
|
||||||
|
public ConstraintsDto Constraints { get; init; } = new();
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SourceDto Source { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PlateDto
|
||||||
|
{
|
||||||
|
public int Id { get; init; }
|
||||||
|
public SizeDto Size { get; init; } = new();
|
||||||
|
public double Thickness { get; init; }
|
||||||
|
public int Quadrant { get; init; } = 1;
|
||||||
|
public int Quantity { get; init; } = 1;
|
||||||
|
public double PartSpacing { get; init; }
|
||||||
|
public MaterialDto Material { get; init; } = new();
|
||||||
|
public SpacingDto EdgeSpacing { get; init; } = new();
|
||||||
|
public List<PartDto> Parts { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PartDto
|
||||||
|
{
|
||||||
|
public int DrawingId { get; init; }
|
||||||
|
public double X { get; init; }
|
||||||
|
public double Y { get; init; }
|
||||||
|
public double Rotation { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record SizeDto
|
||||||
|
{
|
||||||
|
public double Width { get; init; }
|
||||||
|
public double Height { 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify DTOs compile**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.IO/OpenNest.IO.csproj`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestFormat.cs
|
||||||
|
git commit -m "feat: add NestFormat DTOs for JSON nest file format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 2: Rewrite NestWriter
|
||||||
|
|
||||||
|
### Task 2: Rewrite NestWriter to use JSON serialization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Rewrite: `OpenNest.IO/NestWriter.cs`
|
||||||
|
|
||||||
|
The writer keeps the same public API: `NestWriter(Nest nest)` constructor and `bool Write(string file)`. Internally it builds a `NestDto` from the domain model, serializes it to `nest.json`, and writes each drawing's program to `programs/program-N`.
|
||||||
|
|
||||||
|
The G-code writing methods (`WriteDrawing`, `GetCodeString`, `GetLayerString`) are preserved exactly — they write program G-code to streams, which is unchanged. The `WritePlate` method and all XML methods (`AddNestInfo`, `AddPlateInfo`, `AddDrawingInfo`) are removed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite `NestWriter.cs`**
|
||||||
|
|
||||||
|
Replace the entire file. Key changes:
|
||||||
|
- Remove `using System.Xml`
|
||||||
|
- Add `using System.Text.Json`
|
||||||
|
- Remove `AddNestInfo()`, `AddPlateInfo()`, `AddDrawingInfo()`, `AddPlates()`, `WritePlate()` methods
|
||||||
|
- Add `BuildNestDto()` method that maps domain model → DTOs
|
||||||
|
- `Write()` now serializes `NestDto` to `nest.json` and writes programs to `programs/program-N`
|
||||||
|
- Keep `WriteDrawing()`, `GetCodeString()`, `GetLayerString()` exactly as-is
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using OpenNest.CNC;
|
||||||
|
using OpenNest.Math;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public sealed class NestWriter
|
||||||
|
{
|
||||||
|
private const int OutputPrecision = 10;
|
||||||
|
private const string CoordinateFormat = "0.##########";
|
||||||
|
|
||||||
|
private readonly Nest nest;
|
||||||
|
private Dictionary<int, Drawing> drawingDict;
|
||||||
|
|
||||||
|
public NestWriter(Nest nest)
|
||||||
|
{
|
||||||
|
this.drawingDict = new Dictionary<int, Drawing>();
|
||||||
|
this.nest = nest;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Write(string file)
|
||||||
|
{
|
||||||
|
nest.DateLastModified = DateTime.Now;
|
||||||
|
SetDrawingIds();
|
||||||
|
|
||||||
|
using var fileStream = new FileStream(file, FileMode.Create);
|
||||||
|
using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create);
|
||||||
|
|
||||||
|
WriteNestJson(zipArchive);
|
||||||
|
WritePrograms(zipArchive);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDrawingIds()
|
||||||
|
{
|
||||||
|
var id = 1;
|
||||||
|
foreach (var drawing in nest.Drawings)
|
||||||
|
{
|
||||||
|
drawingDict.Add(id, drawing);
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteNestJson(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
var dto = BuildNestDto();
|
||||||
|
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||||
|
|
||||||
|
var entry = zipArchive.CreateEntry("nest.json");
|
||||||
|
using var stream = entry.Open();
|
||||||
|
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||||
|
writer.Write(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NestDto BuildNestDto()
|
||||||
|
{
|
||||||
|
return new NestDto
|
||||||
|
{
|
||||||
|
Version = 2,
|
||||||
|
Name = nest.Name ?? "",
|
||||||
|
Units = nest.Units.ToString(),
|
||||||
|
Customer = nest.Customer ?? "",
|
||||||
|
DateCreated = nest.DateCreated.ToString("o"),
|
||||||
|
DateLastModified = nest.DateLastModified.ToString("o"),
|
||||||
|
Notes = nest.Notes ?? "",
|
||||||
|
PlateDefaults = BuildPlateDefaultsDto(),
|
||||||
|
Drawings = BuildDrawingDtos(),
|
||||||
|
Plates = BuildPlateDtos()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlateDefaultsDto BuildPlateDefaultsDto()
|
||||||
|
{
|
||||||
|
var pd = nest.PlateDefaults;
|
||||||
|
return new PlateDefaultsDto
|
||||||
|
{
|
||||||
|
Size = new SizeDto { Width = pd.Size.Width, Height = pd.Size.Height },
|
||||||
|
Thickness = pd.Thickness,
|
||||||
|
Quadrant = pd.Quadrant,
|
||||||
|
PartSpacing = pd.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = pd.Material.Name ?? "",
|
||||||
|
Grade = pd.Material.Grade ?? "",
|
||||||
|
Density = pd.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = pd.EdgeSpacing.Left,
|
||||||
|
Top = pd.EdgeSpacing.Top,
|
||||||
|
Right = pd.EdgeSpacing.Right,
|
||||||
|
Bottom = pd.EdgeSpacing.Bottom
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<DrawingDto> BuildDrawingDtos()
|
||||||
|
{
|
||||||
|
var list = new List<DrawingDto>();
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var d = kvp.Value;
|
||||||
|
list.Add(new DrawingDto
|
||||||
|
{
|
||||||
|
Id = kvp.Key,
|
||||||
|
Name = d.Name ?? "",
|
||||||
|
Customer = d.Customer ?? "",
|
||||||
|
Color = new ColorDto { A = d.Color.A, R = d.Color.R, G = d.Color.G, B = d.Color.B },
|
||||||
|
Quantity = new QuantityDto { Required = d.Quantity.Required },
|
||||||
|
Priority = d.Priority,
|
||||||
|
Constraints = new ConstraintsDto
|
||||||
|
{
|
||||||
|
StepAngle = d.Constraints.StepAngle,
|
||||||
|
StartAngle = d.Constraints.StartAngle,
|
||||||
|
EndAngle = d.Constraints.EndAngle,
|
||||||
|
Allow180Equivalent = d.Constraints.Allow180Equivalent
|
||||||
|
},
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = d.Material.Name ?? "",
|
||||||
|
Grade = d.Material.Grade ?? "",
|
||||||
|
Density = d.Material.Density
|
||||||
|
},
|
||||||
|
Source = new SourceDto
|
||||||
|
{
|
||||||
|
Path = d.Source.Path ?? "",
|
||||||
|
Offset = new OffsetDto { X = d.Source.Offset.X, Y = d.Source.Offset.Y }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<PlateDto> BuildPlateDtos()
|
||||||
|
{
|
||||||
|
var list = new List<PlateDto>();
|
||||||
|
for (var i = 0; i < nest.Plates.Count; i++)
|
||||||
|
{
|
||||||
|
var plate = nest.Plates[i];
|
||||||
|
var parts = new List<PartDto>();
|
||||||
|
foreach (var part in plate.Parts)
|
||||||
|
{
|
||||||
|
var match = drawingDict.Where(dwg => dwg.Value == part.BaseDrawing).FirstOrDefault();
|
||||||
|
parts.Add(new PartDto
|
||||||
|
{
|
||||||
|
DrawingId = match.Key,
|
||||||
|
X = part.Location.X,
|
||||||
|
Y = part.Location.Y,
|
||||||
|
Rotation = part.Rotation
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
list.Add(new PlateDto
|
||||||
|
{
|
||||||
|
Id = i + 1,
|
||||||
|
Size = new SizeDto { Width = plate.Size.Width, Height = plate.Size.Height },
|
||||||
|
Thickness = plate.Thickness,
|
||||||
|
Quadrant = plate.Quadrant,
|
||||||
|
Quantity = plate.Quantity,
|
||||||
|
PartSpacing = plate.PartSpacing,
|
||||||
|
Material = new MaterialDto
|
||||||
|
{
|
||||||
|
Name = plate.Material.Name ?? "",
|
||||||
|
Grade = plate.Material.Grade ?? "",
|
||||||
|
Density = plate.Material.Density
|
||||||
|
},
|
||||||
|
EdgeSpacing = new SpacingDto
|
||||||
|
{
|
||||||
|
Left = plate.EdgeSpacing.Left,
|
||||||
|
Top = plate.EdgeSpacing.Top,
|
||||||
|
Right = plate.EdgeSpacing.Right,
|
||||||
|
Bottom = plate.EdgeSpacing.Bottom
|
||||||
|
},
|
||||||
|
Parts = parts
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WritePrograms(ZipArchive zipArchive)
|
||||||
|
{
|
||||||
|
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
var name = $"programs/program-{kvp.Key}";
|
||||||
|
var stream = new MemoryStream();
|
||||||
|
WriteDrawing(stream, kvp.Value);
|
||||||
|
|
||||||
|
var entry = zipArchive.CreateEntry(name);
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
stream.CopyTo(entryStream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteDrawing(Stream stream, Drawing drawing)
|
||||||
|
{
|
||||||
|
var program = drawing.Program;
|
||||||
|
var writer = new StreamWriter(stream);
|
||||||
|
writer.AutoFlush = true;
|
||||||
|
|
||||||
|
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
|
||||||
|
|
||||||
|
for (var i = 0; i < drawing.Program.Length; ++i)
|
||||||
|
{
|
||||||
|
var code = drawing.Program[i];
|
||||||
|
writer.WriteLine(GetCodeString(code));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetCodeString(ICode code)
|
||||||
|
{
|
||||||
|
switch (code.Type)
|
||||||
|
{
|
||||||
|
case CodeType.ArcMove:
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var arcMove = (ArcMove)code;
|
||||||
|
|
||||||
|
var x = System.Math.Round(arcMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var y = System.Math.Round(arcMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var i = System.Math.Round(arcMove.CenterPoint.X, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
var j = System.Math.Round(arcMove.CenterPoint.Y, OutputPrecision).ToString(CoordinateFormat);
|
||||||
|
|
||||||
|
if (arcMove.Rotation == RotationType.CW)
|
||||||
|
sb.Append(string.Format("G02X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||||
|
else
|
||||||
|
sb.Append(string.Format("G03X{0}Y{1}I{2}J{3}", x, y, i, j));
|
||||||
|
|
||||||
|
if (arcMove.Layer != LayerType.Cut)
|
||||||
|
sb.Append(GetLayerString(arcMove.Layer));
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.Comment:
|
||||||
|
{
|
||||||
|
var comment = (Comment)code;
|
||||||
|
return ":" + comment.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.LinearMove:
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var linearMove = (LinearMove)code;
|
||||||
|
|
||||||
|
sb.Append(string.Format("G01X{0}Y{1}",
|
||||||
|
System.Math.Round(linearMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||||
|
System.Math.Round(linearMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat)));
|
||||||
|
|
||||||
|
if (linearMove.Layer != LayerType.Cut)
|
||||||
|
sb.Append(GetLayerString(linearMove.Layer));
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.RapidMove:
|
||||||
|
{
|
||||||
|
var rapidMove = (RapidMove)code;
|
||||||
|
|
||||||
|
return string.Format("G00X{0}Y{1}",
|
||||||
|
System.Math.Round(rapidMove.EndPoint.X, OutputPrecision).ToString(CoordinateFormat),
|
||||||
|
System.Math.Round(rapidMove.EndPoint.Y, OutputPrecision).ToString(CoordinateFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SetFeedrate:
|
||||||
|
{
|
||||||
|
var setFeedrate = (Feedrate)code;
|
||||||
|
return "F" + setFeedrate.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SetKerf:
|
||||||
|
{
|
||||||
|
var setKerf = (Kerf)code;
|
||||||
|
|
||||||
|
switch (setKerf.Value)
|
||||||
|
{
|
||||||
|
case KerfType.None: return "G40";
|
||||||
|
case KerfType.Left: return "G41";
|
||||||
|
case KerfType.Right: return "G42";
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case CodeType.SubProgramCall:
|
||||||
|
{
|
||||||
|
var subProgramCall = (SubProgramCall)code;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetLayerString(LayerType layer)
|
||||||
|
{
|
||||||
|
switch (layer)
|
||||||
|
{
|
||||||
|
case LayerType.Display:
|
||||||
|
return ":DISPLAY";
|
||||||
|
|
||||||
|
case LayerType.Leadin:
|
||||||
|
return ":LEADIN";
|
||||||
|
|
||||||
|
case LayerType.Leadout:
|
||||||
|
return ":LEADOUT";
|
||||||
|
|
||||||
|
case LayerType.Scribe:
|
||||||
|
return ":SCRIBE";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify NestWriter compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestWriter.cs
|
||||||
|
git commit -m "feat: rewrite NestWriter to use JSON format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 3: Rewrite NestReader
|
||||||
|
|
||||||
|
### Task 3: Rewrite NestReader to use JSON deserialization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Rewrite: `OpenNest.IO/NestReader.cs`
|
||||||
|
|
||||||
|
The reader keeps the same public API: `NestReader(string file)`, `NestReader(Stream stream)`, and `Nest Read()`. Internally it reads `nest.json`, deserializes to `NestDto`, reads programs from `programs/program-N`, and assembles the domain model.
|
||||||
|
|
||||||
|
All XML parsing, plate G-code parsing, dictionary-linking (`LinkProgramsToDrawings`, `LinkPartsToPlates`), and the helper enums/methods are removed.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Rewrite `NestReader.cs`**
|
||||||
|
|
||||||
|
Replace the entire file:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
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.CNC;
|
||||||
|
using OpenNest.Geometry;
|
||||||
|
using static OpenNest.IO.NestFormat;
|
||||||
|
|
||||||
|
namespace OpenNest.IO
|
||||||
|
{
|
||||||
|
public sealed class NestReader
|
||||||
|
{
|
||||||
|
private readonly Stream stream;
|
||||||
|
private readonly ZipArchive zipArchive;
|
||||||
|
|
||||||
|
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 programs = ReadPrograms(dto.Drawings.Count);
|
||||||
|
var drawingMap = BuildDrawings(dto, programs);
|
||||||
|
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<int, Program> ReadPrograms(int count)
|
||||||
|
{
|
||||||
|
var programs = new Dictionary<int, Program>();
|
||||||
|
for (var i = 1; i <= count; i++)
|
||||||
|
{
|
||||||
|
var entry = zipArchive.GetEntry($"programs/program-{i}");
|
||||||
|
if (entry == null) continue;
|
||||||
|
|
||||||
|
using var entryStream = entry.Open();
|
||||||
|
var memStream = new MemoryStream();
|
||||||
|
entryStream.CopyTo(memStream);
|
||||||
|
memStream.Position = 0;
|
||||||
|
|
||||||
|
var reader = new ProgramReader(memStream);
|
||||||
|
programs[i] = reader.Read();
|
||||||
|
}
|
||||||
|
return programs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, Drawing> BuildDrawings(NestDto dto, Dictionary<int, Program> programs)
|
||||||
|
{
|
||||||
|
var map = new Dictionary<int, Drawing>();
|
||||||
|
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 (programs.TryGetValue(d.Id, out var pgm))
|
||||||
|
drawing.Program = pgm;
|
||||||
|
|
||||||
|
map[d.Id] = drawing;
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Plate defaults
|
||||||
|
var pd = dto.PlateDefaults;
|
||||||
|
nest.PlateDefaults.Size = new Size(pd.Size.Width, pd.Size.Height);
|
||||||
|
nest.PlateDefaults.Thickness = pd.Thickness;
|
||||||
|
nest.PlateDefaults.Quadrant = pd.Quadrant;
|
||||||
|
nest.PlateDefaults.PartSpacing = pd.PartSpacing;
|
||||||
|
nest.PlateDefaults.Material = new Material(pd.Material.Name, pd.Material.Grade, pd.Material.Density);
|
||||||
|
nest.PlateDefaults.EdgeSpacing = new Spacing(pd.EdgeSpacing.Left, pd.EdgeSpacing.Bottom, pd.EdgeSpacing.Right, pd.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
// 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 Size(p.Size.Width, p.Size.Height);
|
||||||
|
plate.Thickness = p.Thickness;
|
||||||
|
plate.Quadrant = p.Quadrant;
|
||||||
|
plate.Quantity = p.Quantity;
|
||||||
|
plate.PartSpacing = p.PartSpacing;
|
||||||
|
plate.Material = new Material(p.Material.Name, p.Material.Grade, p.Material.Density);
|
||||||
|
plate.EdgeSpacing = new Spacing(p.EdgeSpacing.Left, p.EdgeSpacing.Bottom, p.EdgeSpacing.Right, p.EdgeSpacing.Top);
|
||||||
|
|
||||||
|
foreach (var partDto in p.Parts)
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
plate.Parts.Add(part);
|
||||||
|
}
|
||||||
|
|
||||||
|
nest.Plates.Add(plate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Build to verify NestReader compiles**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add OpenNest.IO/NestReader.cs
|
||||||
|
git commit -m "feat: rewrite NestReader to use JSON format v2"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Chunk 4: Smoke Test
|
||||||
|
|
||||||
|
### Task 4: Manual smoke test via OpenNest.Console
|
||||||
|
|
||||||
|
**Files:** None modified — this is a verification step.
|
||||||
|
|
||||||
|
Use the `OpenNest.Console` project (or the MCP server) to verify round-trip: create a nest, save it, reload it, confirm data is intact.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Build the full solution**
|
||||||
|
|
||||||
|
Run: `dotnet build OpenNest.sln`
|
||||||
|
Expected: Build succeeded with no errors.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Round-trip test via MCP tools**
|
||||||
|
|
||||||
|
Use the OpenNest MCP tools to:
|
||||||
|
1. Create a drawing (e.g. a rectangle via `create_drawing`)
|
||||||
|
2. Create a plate via `create_plate`
|
||||||
|
3. Fill the plate via `fill_plate`
|
||||||
|
4. Save the nest via the console app or verify `get_plate_info` shows parts
|
||||||
|
5. If a nest file exists on disk, load it with `load_nest` and verify `get_plate_info` returns the same data
|
||||||
|
|
||||||
|
- [ ] **Step 3: Inspect the ZIP contents**
|
||||||
|
|
||||||
|
Unzip a saved nest file and verify:
|
||||||
|
- `nest.json` exists with correct structure
|
||||||
|
- `programs/program-1` (etc.) exist with G-code content
|
||||||
|
- No `info`, `drawing-info`, `plate-info`, or `plate-NNN` files exist
|
||||||
|
|
||||||
|
- [ ] **Step 4: Commit any fixes**
|
||||||
|
|
||||||
|
If any issues were found and fixed, commit them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -u
|
||||||
|
git commit -m "fix: address issues found during nest format v2 smoke test"
|
||||||
|
```
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Nest File Format v2 Design
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
The current nest file format stores metadata across three separate XML files (`info`, `drawing-info`, `plate-info`) plus per-plate G-code files for part placements inside a ZIP archive. This results in ~400 lines of hand-written XML read/write code, fragile dictionary-linking to reconnect drawings/plates by ID after parsing, and the overhead of running the full G-code parser just to extract part positions.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
|
||||||
|
The nest file remains a ZIP archive. Contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
nest.json
|
||||||
|
programs/
|
||||||
|
program-1
|
||||||
|
program-2
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`nest.json`** — single JSON file containing all metadata and part placements.
|
||||||
|
- **`programs/program-N`** — G-code text for each drawing's CNC program (1-indexed, no zero-padding). Previously stored at the archive root as `program-NNN` (zero-padded). Parsed by `ProgramReader`, written by existing G-code serialization logic. Format unchanged.
|
||||||
|
|
||||||
|
Plate G-code files (`plate-NNN`) are removed. Part placements are stored inline in `nest.json`.
|
||||||
|
|
||||||
|
### JSON Schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"name": "string",
|
||||||
|
"units": "Inches | Millimeters",
|
||||||
|
"customer": "string",
|
||||||
|
"dateCreated": "2026-03-12T10:30:00",
|
||||||
|
"dateLastModified": "2026-03-12T14:00:00",
|
||||||
|
"notes": "string (plain JSON, no URI-escaping)",
|
||||||
|
"plateDefaults": {
|
||||||
|
"size": { "width": 0.0, "height": 0.0 },
|
||||||
|
"thickness": 0.0,
|
||||||
|
"quadrant": 1,
|
||||||
|
"partSpacing": 0.0,
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 }
|
||||||
|
},
|
||||||
|
"drawings": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "string",
|
||||||
|
"customer": "string",
|
||||||
|
"color": { "a": 255, "r": 0, "g": 0, "b": 0 },
|
||||||
|
"quantity": { "required": 0 },
|
||||||
|
"priority": 0,
|
||||||
|
"constraints": {
|
||||||
|
"stepAngle": 0.0,
|
||||||
|
"startAngle": 0.0,
|
||||||
|
"endAngle": 0.0,
|
||||||
|
"allow180Equivalent": false
|
||||||
|
},
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"source": {
|
||||||
|
"path": "string",
|
||||||
|
"offset": { "x": 0.0, "y": 0.0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"plates": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"size": { "width": 0.0, "height": 0.0 },
|
||||||
|
"thickness": 0.0,
|
||||||
|
"quadrant": 1,
|
||||||
|
"quantity": 1,
|
||||||
|
"partSpacing": 0.0,
|
||||||
|
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||||
|
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 },
|
||||||
|
"parts": [
|
||||||
|
{ "drawingId": 1, "x": 0.0, "y": 0.0, "rotation": 0.0 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Key details:
|
||||||
|
- **Version**: `"version": 2` at the top level for future format migration.
|
||||||
|
- Drawing `id` values are 1-indexed, matching `programs/program-N` filenames.
|
||||||
|
- Part `rotation` is stored in **radians** (matches internal domain model, no conversion needed).
|
||||||
|
- Part `drawingId` references the drawing's `id` in the `drawings` array.
|
||||||
|
- **Dates**: local time, serialized via `DateTime.ToString("o")` (ISO 8601 round-trip format with timezone offset).
|
||||||
|
- **Notes**: stored as plain JSON strings. The v1 URI-escaping (`Uri.EscapeDataString`) is not needed since JSON handles special characters natively.
|
||||||
|
- `quantity.required` is the only quantity persisted; `nested` is computed at load time from part placements.
|
||||||
|
- **Units**: enum values match the domain model: `Inches` or `Millimeters`.
|
||||||
|
- **Size**: uses `width`/`height` matching the `OpenNest.Geometry.Size` struct.
|
||||||
|
- **Drawing.Priority** and **Drawing.Constraints** (stepAngle, startAngle, endAngle, allow180Equivalent) are now persisted (v1 omitted these).
|
||||||
|
- **Empty collections**: `drawings` and `plates` arrays are always present (may be empty `[]`). The `programs/` folder is empty when there are no drawings.
|
||||||
|
|
||||||
|
### Serialization Approach
|
||||||
|
|
||||||
|
Use `System.Text.Json` with small DTO (Data Transfer Object) classes for serialization. The DTOs map between the domain model and the JSON structure, keeping serialization concerns out of the domain classes.
|
||||||
|
|
||||||
|
### What Changes
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `NestWriter.cs` | Replace all XML writing and plate G-code writing with JSON serialization. Programs written to `programs/` folder. |
|
||||||
|
| `NestReader.cs` | Replace all XML parsing, plate G-code parsing, and dictionary-linking with JSON deserialization. Programs read from `programs/` folder. |
|
||||||
|
|
||||||
|
### What Stays the Same
|
||||||
|
|
||||||
|
| File | Reason |
|
||||||
|
|------|--------|
|
||||||
|
| `ProgramReader.cs` | G-code parsing for CNC programs is unchanged. |
|
||||||
|
| `NestWriter` G-code writing (`WriteDrawing`, `GetCodeString`) | G-code serialization for programs is unchanged. |
|
||||||
|
| `DxfImporter.cs`, `DxfExporter.cs`, `Extensions.cs` | Unrelated to nest file format. |
|
||||||
|
| Domain model classes | No changes needed. |
|
||||||
|
|
||||||
|
### Public API
|
||||||
|
|
||||||
|
The public API is unchanged:
|
||||||
|
- `NestReader(string file)` and `NestReader(Stream stream)` constructors preserved.
|
||||||
|
- `NestReader.Read()` returns `Nest`.
|
||||||
|
- `NestWriter(Nest nest)` constructor preserved.
|
||||||
|
- `NestWriter.Write(string file)` returns `bool`.
|
||||||
|
|
||||||
|
### Callers (no changes needed)
|
||||||
|
|
||||||
|
- `MainForm.cs:329` — `new NestReader(path)`
|
||||||
|
- `MainForm.cs:363` — `new NestReader(dlg.FileName)`
|
||||||
|
- `EditNestForm.cs:212` — `new NestWriter(Nest)`
|
||||||
|
- `EditNestForm.cs:223` — `new NestWriter(nst)`
|
||||||
|
- `Document.cs:27` — `new NestWriter(Nest)`
|
||||||
|
- `OpenNest.Console/Program.cs:94` — `new NestReader(nestFile)`
|
||||||
|
- `OpenNest.Console/Program.cs:190` — `new NestWriter(nest)`
|
||||||
|
- `OpenNest.Mcp/InputTools.cs:30` — `new NestReader(path)`
|
||||||
Reference in New Issue
Block a user