refactor: extract OpenNest.IO class library from WinForms project

Move DxfImporter, DxfExporter, NestReader, NestWriter, ProgramReader,
and Extensions into a new OpenNest.IO class library. The WinForms project
now references OpenNest.IO instead of ACadSharp directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 15:41:07 -04:00
parent 4196a30791
commit cc286dd9b7
9 changed files with 26 additions and 1 deletions

426
OpenNest.IO/NestWriter.cs Normal file
View File

@@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Xml;
using OpenNest.CNC;
using OpenNest.Math;
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)
{
this.drawingDict = new Dictionary<int, Drawing>();
this.nest = nest;
}
public bool Write(string file)
{
this.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();
}
return true;
}
private void SetDrawingIds()
{
int id = 1;
foreach (var drawing in nest.Drawings)
{
drawingDict.Add(id, drawing);
id++;
}
}
private void AddNestInfo()
{
var stream = new MemoryStream();
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
{
Indent = true
});
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);
}
}
private void AddPlates()
{
int num = 1;
foreach (var plate in nest.Plates)
{
var stream = new MemoryStream();
var name = string.Format("plate-{0}", num.ToString().PadLeft(3, '0'));
WritePlate(stream, plate);
var entry = zipArchive.CreateEntry(name);
using (var entryStream = entry.Open())
{
stream.CopyTo(entryStream);
}
num++;
}
}
private void AddPlateInfo()
{
var stream = new MemoryStream();
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
{
Indent = true
});
writer.WriteStartDocument();
writer.WriteStartElement("Plates");
writer.WriteAttributeString("count", nest.Plates.Count.ToString());
for (int 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())
{
stream.CopyTo(entryStream);
}
num++;
}
}
private void AddDrawingInfo()
{
var stream = new MemoryStream();
var writer = XmlWriter.Create(stream, new XmlWriterSettings()
{
Indent = true
});
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())
{
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;
var writer = new StreamWriter(stream);
writer.AutoFlush = true;
writer.WriteLine(program.Mode == Mode.Absolute ? "G90" : "G91");
for (int 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;
}
}
}
}