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:
@@ -1,32 +1,22 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Text.Json;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Math;
|
||||
using static OpenNest.IO.NestFormat;
|
||||
|
||||
namespace OpenNest.IO
|
||||
{
|
||||
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;
|
||||
|
||||
/// <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 readonly Nest nest;
|
||||
private ZipArchive zipArchive;
|
||||
private Dictionary<int, Drawing> drawingDict;
|
||||
|
||||
public NestWriter(Nest nest)
|
||||
@@ -37,27 +27,21 @@ namespace OpenNest.IO
|
||||
|
||||
public bool Write(string file)
|
||||
{
|
||||
this.nest.DateLastModified = DateTime.Now;
|
||||
|
||||
nest.DateLastModified = DateTime.Now;
|
||||
SetDrawingIds();
|
||||
|
||||
using (var fileStream = new FileStream(file, FileMode.Create))
|
||||
using (zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create))
|
||||
{
|
||||
AddNestInfo();
|
||||
AddPlates();
|
||||
AddPlateInfo();
|
||||
AddDrawings();
|
||||
AddDrawingInfo();
|
||||
}
|
||||
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()
|
||||
{
|
||||
int id = 1;
|
||||
|
||||
var id = 1;
|
||||
foreach (var drawing in nest.Drawings)
|
||||
{
|
||||
drawingDict.Add(id, drawing);
|
||||
@@ -65,241 +49,156 @@ namespace OpenNest.IO
|
||||
}
|
||||
}
|
||||
|
||||
private void AddNestInfo()
|
||||
private void WriteNestJson(ZipArchive zipArchive)
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
||||
{
|
||||
Indent = true
|
||||
});
|
||||
var dto = BuildNestDto();
|
||||
var json = JsonSerializer.Serialize(dto, JsonOptions);
|
||||
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Nest");
|
||||
writer.WriteAttributeString("name", nest.Name);
|
||||
|
||||
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);
|
||||
}
|
||||
var entry = zipArchive.CreateEntry("nest.json");
|
||||
using var stream = entry.Open();
|
||||
using var writer = new StreamWriter(stream, Encoding.UTF8);
|
||||
writer.Write(json);
|
||||
}
|
||||
|
||||
private void AddPlates()
|
||||
private NestDto BuildNestDto()
|
||||
{
|
||||
int num = 1;
|
||||
|
||||
foreach (var plate in nest.Plates)
|
||||
return new NestDto
|
||||
{
|
||||
var stream = new MemoryStream();
|
||||
var name = string.Format("plate-{0}", num.ToString().PadLeft(3, '0'));
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
WritePlate(stream, plate);
|
||||
|
||||
var entry = zipArchive.CreateEntry(name);
|
||||
using (var entryStream = entry.Open())
|
||||
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
|
||||
{
|
||||
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 writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
||||
var list = new List<DrawingDto>();
|
||||
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();
|
||||
writer.WriteStartElement("Plates");
|
||||
writer.WriteAttributeString("count", nest.Plates.Count.ToString());
|
||||
|
||||
for (int i = 0; i < nest.Plates.Count; ++i)
|
||||
private List<PlateDto> BuildPlateDtos()
|
||||
{
|
||||
var list = new List<PlateDto>();
|
||||
for (var i = 0; i < nest.Plates.Count; i++)
|
||||
{
|
||||
var plate = nest.Plates[i];
|
||||
|
||||
writer.WriteStartElement("Plate");
|
||||
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())
|
||||
var parts = new List<PartDto>();
|
||||
foreach (var part in plate.Parts)
|
||||
{
|
||||
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();
|
||||
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
|
||||
foreach (var kvp in drawingDict.OrderBy(k => k.Key))
|
||||
{
|
||||
Indent = true
|
||||
});
|
||||
var name = $"programs/program-{kvp.Key}";
|
||||
var stream = new MemoryStream();
|
||||
WriteDrawing(stream, kvp.Value);
|
||||
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("Drawings");
|
||||
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())
|
||||
{
|
||||
var entry = zipArchive.CreateEntry(name);
|
||||
using var entryStream = entry.Open();
|
||||
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)
|
||||
{
|
||||
var program = drawing.Program;
|
||||
@@ -308,7 +207,7 @@ namespace OpenNest.IO
|
||||
|
||||
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];
|
||||
writer.WriteLine(GetCodeString(code));
|
||||
|
||||
Reference in New Issue
Block a user