Compare commits

...

5 Commits

Author SHA1 Message Date
aj e4e1d9b5a3 refactor: rename DefinedShape to ShapeProfile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:19:49 -04:00
aj 0c395421cd chore: add .claude/ to .gitignore
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:02:54 -04:00
aj 612b540d9d refactor: rename Size.Height to Size.Length across codebase
"Length" is more natural than "height" for flat plate materials.
Renames the field on OpenNest.Geometry.Size, Box.Height property,
and all references across 38 files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 22:01:40 -04:00
aj 7e0edd112a refactor: use 'length' instead of 'height' in nest JSON format
Rename SizeDto.Height to SizeDto.Length so the serialized JSON
uses "width"/"length" which is more natural for plate materials.
The core Size struct still uses Height internally.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 21:50:53 -04:00
aj c2534ef08b 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>
2026-03-12 18:44:43 -04:00
46 changed files with 1407 additions and 795 deletions
+3
View File
@@ -198,3 +198,6 @@ FakesAssemblies/
# Visual Studio 6 workspace options file
*.opt
# Claude Code
.claude/
+1 -1
View File
@@ -135,7 +135,7 @@ if (!keepParts)
plate.Parts.Clear();
Console.WriteLine($"Nest: {nest.Name}");
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Height:F1}), spacing={plate.PartSpacing:F2}");
Console.WriteLine($"Plate: {plateIndex} ({plate.Size.Width:F1} x {plate.Size.Length:F1}), spacing={plate.PartSpacing:F2}");
Console.WriteLine($"Drawing: {drawing.Name}");
if (!keepParts)
+1 -1
View File
@@ -388,7 +388,7 @@ namespace OpenNest.Geometry
boundingBox.X = minX;
boundingBox.Y = minY;
boundingBox.Width = maxX - minX;
boundingBox.Height = maxY - minY;
boundingBox.Length = maxY - minY;
}
public override Entity OffsetEntity(double distance, OffsetSide side)
+1 -1
View File
@@ -13,7 +13,7 @@ namespace OpenNest.Geometry
double minX = boxes[0].X;
double minY = boxes[0].Y;
double maxX = boxes[0].X + boxes[0].Width;
double maxY = boxes[0].Y + boxes[0].Height;
double maxY = boxes[0].Y + boxes[0].Length;
foreach (var box in boxes)
{
+10 -10
View File
@@ -15,14 +15,14 @@ namespace OpenNest.Geometry
{
Location = new Vector(x, y);
Width = w;
Height = h;
Length = h;
}
public Vector Location;
public Vector Center
{
get { return new Vector(X + Width * 0.5, Y + Height * 0.5); }
get { return new Vector(X + Width * 0.5, Y + Length * 0.5); }
}
public Size Size;
@@ -45,10 +45,10 @@ namespace OpenNest.Geometry
set { Size.Width = value; }
}
public double Height
public double Length
{
get { return Size.Height; }
set { Size.Height = value; }
get { return Size.Length; }
set { Size.Length = value; }
}
public void MoveTo(double x, double y)
@@ -86,7 +86,7 @@ namespace OpenNest.Geometry
public double Top
{
get { return Y + Height; }
get { return Y + Length; }
}
public double Bottom
@@ -96,12 +96,12 @@ namespace OpenNest.Geometry
public double Area()
{
return Width * Height;
return Width * Length;
}
public double Perimeter()
{
return Width * 2 + Height * 2;
return Width * 2 + Length * 2;
}
public bool Intersects(Box box)
@@ -197,12 +197,12 @@ namespace OpenNest.Geometry
public Box Offset(double d)
{
return new Box(X - d, Y - d, Width + d * 2, Height + d * 2);
return new Box(X - d, Y - d, Width + d * 2, Length + d * 2);
}
public override string ToString()
{
return string.Format("[Box: X={0}, Y={1}, Width={2}, Height={3}]", X, Y, Width, Height);
return string.Format("[Box: X={0}, Y={1}, Width={2}, Length={3}]", X, Y, Width, Length);
}
}
}
+2 -2
View File
@@ -23,7 +23,7 @@
var x = large.Left;
var y = large.Bottom;
var w = small.Left - x;
var h = large.Height;
var h = large.Length;
return new Box(x, y, w, h);
}
@@ -49,7 +49,7 @@
var x = small.Right;
var y = large.Bottom;
var w = large.Right - x;
var h = large.Height;
var h = large.Length;
return new Box(x, y, w, h);
}
+1 -1
View File
@@ -263,7 +263,7 @@ namespace OpenNest.Geometry
boundingBox.X = Center.X - Radius;
boundingBox.Y = Center.Y - Radius;
boundingBox.Width = Diameter;
boundingBox.Height = Diameter;
boundingBox.Length = Diameter;
}
public override Entity OffsetEntity(double distance, OffsetSide side)
+2 -2
View File
@@ -381,12 +381,12 @@ namespace OpenNest.Geometry
if (StartPoint.Y < EndPoint.Y)
{
boundingBox.Y = StartPoint.Y;
boundingBox.Height = EndPoint.Y - StartPoint.Y;
boundingBox.Length = EndPoint.Y - StartPoint.Y;
}
else
{
boundingBox.Y = EndPoint.Y;
boundingBox.Height = StartPoint.Y - EndPoint.Y;
boundingBox.Length = StartPoint.Y - EndPoint.Y;
}
}
+1 -1
View File
@@ -312,7 +312,7 @@ namespace OpenNest.Geometry
boundingBox.X = minX;
boundingBox.Y = minY;
boundingBox.Width = maxX - minX;
boundingBox.Height = maxY - minY;
boundingBox.Length = maxY - minY;
}
public override Entity OffsetEntity(double distance, OffsetSide side)
+1 -1
View File
@@ -398,7 +398,7 @@ namespace OpenNest.Geometry
public override Entity OffsetEntity(double distance, OffsetSide side)
{
var offsetShape = new Shape();
var definedShape = new DefinedShape(this);
var definedShape = new ShapeProfile(this);
Entity lastEntity = null;
Entity lastOffsetEntity = null;
@@ -2,14 +2,14 @@
namespace OpenNest.Geometry
{
public class DefinedShape
public class ShapeProfile
{
public DefinedShape(Shape shape)
public ShapeProfile(Shape shape)
{
Update(shape.Entities);
}
public DefinedShape(List<Entity> entities)
public ShapeProfile(List<Entity> entities)
{
Update(entities);
}
+8 -8
View File
@@ -1,16 +1,16 @@
using System;
using System;
namespace OpenNest.Geometry
{
public struct Size
{
public Size(double width, double height)
public Size(double width, double length)
{
Height = height;
Length = length;
Width = width;
}
public double Height;
public double Length;
public double Width;
@@ -21,10 +21,10 @@ namespace OpenNest.Geometry
if (a.Length > 2)
throw new FormatException("Invalid size format.");
var height = double.Parse(a[0]);
var length = double.Parse(a[0]);
var width = double.Parse(a[1]);
return new Size(width, height);
return new Size(width, length);
}
public static bool TryParse(string s, out Size size)
@@ -44,12 +44,12 @@ namespace OpenNest.Geometry
public override string ToString()
{
return string.Format("{0} x {1}", Height, Width);
return string.Format("{0} x {1}", Length, Width);
}
public string ToString(int decimalPlaces)
{
return string.Format("{0} x {1}", System.Math.Round(Height, decimalPlaces), System.Math.Round(Width, decimalPlaces));
return string.Format("{0} x {1}", System.Math.Round(Length, decimalPlaces), System.Math.Round(Width, decimalPlaces));
}
}
}
+1 -1
View File
@@ -220,7 +220,7 @@ namespace OpenNest
var part = new Part(BaseDrawing, clonedProgram,
location + offset,
new Box(BoundingBox.X + offset.X, BoundingBox.Y + offset.Y,
BoundingBox.Width, BoundingBox.Height));
BoundingBox.Width, BoundingBox.Length));
return part;
}
+23 -23
View File
@@ -117,7 +117,7 @@ namespace OpenNest
{
const double oneAndHalfPI = System.Math.PI * 1.5;
Size = new Size(Size.Height, Size.Width);
Size = new Size(Size.Length, Size.Width);
if (rotationDirection == RotationType.CW)
{
@@ -128,7 +128,7 @@ namespace OpenNest
switch (Quadrant)
{
case 1:
Offset(0, Size.Height);
Offset(0, Size.Length);
break;
case 2:
@@ -136,7 +136,7 @@ namespace OpenNest
break;
case 3:
Offset(0, -Size.Height);
Offset(0, -Size.Length);
break;
case 4:
@@ -165,7 +165,7 @@ namespace OpenNest
break;
case 2:
Offset(0, Size.Height);
Offset(0, Size.Length);
break;
case 3:
@@ -173,7 +173,7 @@ namespace OpenNest
break;
case 4:
Offset(0, -Size.Height);
Offset(0, -Size.Length);
break;
default:
@@ -200,19 +200,19 @@ namespace OpenNest
switch (Quadrant)
{
case 1:
centerpt = new Vector(Size.Width * 0.5, Size.Height * 0.5);
centerpt = new Vector(Size.Width * 0.5, Size.Length * 0.5);
break;
case 2:
centerpt = new Vector(-Size.Width * 0.5, Size.Height * 0.5);
centerpt = new Vector(-Size.Width * 0.5, Size.Length * 0.5);
break;
case 3:
centerpt = new Vector(-Size.Width * 0.5, -Size.Height * 0.5);
centerpt = new Vector(-Size.Width * 0.5, -Size.Length * 0.5);
break;
case 4:
centerpt = new Vector(Size.Width * 0.5, -Size.Height * 0.5);
centerpt = new Vector(Size.Width * 0.5, -Size.Length * 0.5);
break;
default:
@@ -308,12 +308,12 @@ namespace OpenNest
case 3:
plateBox.X = (float)-Size.Width;
plateBox.Y = (float)-Size.Height;
plateBox.Y = (float)-Size.Length;
break;
case 4:
plateBox.X = 0;
plateBox.Y = (float)-Size.Height;
plateBox.Y = (float)-Size.Length;
break;
default:
@@ -321,7 +321,7 @@ namespace OpenNest
}
plateBox.Width = Size.Width;
plateBox.Height = Size.Height;
plateBox.Length = Size.Length;
if (!includeParts)
return plateBox;
@@ -341,7 +341,7 @@ namespace OpenNest
? partsBox.Right - boundingBox.X
: plateBox.Right - boundingBox.X;
boundingBox.Height = partsBox.Top > plateBox.Top
boundingBox.Length = partsBox.Top > plateBox.Top
? partsBox.Top - boundingBox.Y
: plateBox.Top - boundingBox.Y;
@@ -359,7 +359,7 @@ namespace OpenNest
box.X += EdgeSpacing.Left;
box.Y += EdgeSpacing.Bottom;
box.Width -= EdgeSpacing.Left + EdgeSpacing.Right;
box.Height -= EdgeSpacing.Top + EdgeSpacing.Bottom;
box.Length -= EdgeSpacing.Top + EdgeSpacing.Bottom;
return box;
}
@@ -383,28 +383,28 @@ namespace OpenNest
var bounds = Parts.GetBoundingBox();
double width;
double height;
double length;
switch (Quadrant)
{
case 1:
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
height = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
break;
case 2:
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
height = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
length = System.Math.Abs(bounds.Top) + EdgeSpacing.Top;
break;
case 3:
width = System.Math.Abs(bounds.Left) + EdgeSpacing.Left;
height = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
break;
case 4:
width = System.Math.Abs(bounds.Right) + EdgeSpacing.Right;
height = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
length = System.Math.Abs(bounds.Bottom) + EdgeSpacing.Bottom;
break;
default:
@@ -413,7 +413,7 @@ namespace OpenNest
Size = new Size(
Helper.RoundUpToNearest(width, roundingFactor),
Helper.RoundUpToNearest(height, roundingFactor));
Helper.RoundUpToNearest(length, roundingFactor));
}
/// <summary>
@@ -422,7 +422,7 @@ namespace OpenNest
/// <returns></returns>
public double Area()
{
return Size.Width * Size.Height;
return Size.Width * Size.Length;
}
/// <summary>
@@ -503,7 +503,7 @@ namespace OpenNest
if (maxRight < work.Right)
{
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Height);
var strip = new Box(maxRight, work.Bottom, work.Right - maxRight, work.Length);
if (strip.Area() > 1.0)
results.Add(strip);
}
@@ -548,7 +548,7 @@ namespace OpenNest
if (minLeft > work.Left)
{
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Height);
var strip = new Box(work.Left, work.Bottom, minLeft - work.Left, work.Length);
if (strip.Area() > 1.0)
results.Add(strip);
}
+1 -1
View File
@@ -57,7 +57,7 @@ namespace OpenNest.Engine.BestFit
var combinedBox = ((IEnumerable<IBoundable>)new IBoundable[] { part1, part2 }).GetBoundingBox();
bestArea = combinedBox.Area();
bestWidth = combinedBox.Width;
bestHeight = combinedBox.Height;
bestHeight = combinedBox.Length;
bestRotation = 0;
}
@@ -64,15 +64,15 @@ namespace OpenNest.Engine.BestFit
if (isHorizontalPush)
{
perpMin = -(bbox2.Height + spacing);
perpMax = bbox1.Height + bbox2.Height + spacing;
perpMin = -(bbox2.Length + spacing);
perpMax = bbox1.Length + bbox2.Length + spacing;
pushStartOffset = bbox1.Width + bbox2.Width + spacing * 2;
}
else
{
perpMin = -(bbox2.Width + spacing);
perpMax = bbox1.Width + bbox2.Width + spacing;
pushStartOffset = bbox1.Height + bbox2.Height + spacing * 2;
pushStartOffset = bbox1.Length + bbox2.Length + spacing * 2;
}
// Pre-compute part1's offset lines (half-spacing outward)
@@ -9,7 +9,7 @@ namespace OpenNest.Engine.BestFit.Tiling
public TileResult Evaluate(BestFitResult bestFit, Plate plate)
{
var plateWidth = plate.Size.Width - plate.EdgeSpacing.Left - plate.EdgeSpacing.Right;
var plateHeight = plate.Size.Height - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
var plateHeight = plate.Size.Length - plate.EdgeSpacing.Top - plate.EdgeSpacing.Bottom;
var result1 = TryTile(bestFit, plateWidth, plateHeight, false);
var result2 = TryTile(bestFit, plateWidth, plateHeight, true);
+3 -3
View File
@@ -17,10 +17,10 @@ namespace OpenNest.CirclePacking
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
var rows = System.Math.Floor((Bin.Height + Tolerance.Epsilon) / (item.Diameter));
var rows = System.Math.Floor((Bin.Length + Tolerance.Epsilon) / (item.Diameter));
var diameter = item.Diameter;
var remaining = Bin.Height - diameter * rows;
var remaining = Bin.Length - diameter * rows;
var radius = diameter * 0.5;
if (remaining < radius)
@@ -47,7 +47,7 @@ namespace OpenNest.CirclePacking
}
else
{
var yoffset = (Bin.Height - diameter) / (2 * rows - 1);
var yoffset = (Bin.Length - diameter) / (2 * rows - 1);
var xoffset = Trigonometry.Base(yoffset, diameter);
var yodd = Bin.Y + yoffset;
+2 -2
View File
@@ -71,12 +71,12 @@ namespace OpenNest.CirclePacking
Bin.Right - item.BoundingBox.Right + Tolerance.Epsilon,
Bin.Top - item.BoundingBox.Top + Tolerance.Epsilon);
var count = System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Diameter);
var count = System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Diameter);
if (count == 0)
return bin;
var yoffset = (bin.Height - item.Diameter) / (count - 1);
var yoffset = (bin.Length - item.Diameter) / (count - 1);
var xoffset = Trigonometry.Base(yoffset * 0.5, item.Diameter);
int column = 0;
+5 -5
View File
@@ -9,7 +9,7 @@ namespace OpenNest
public FillLinear(Box workArea, double partSpacing)
{
PartSpacing = partSpacing;
WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Height);
WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Length);
}
public Box WorkArea { get; }
@@ -34,7 +34,7 @@ namespace OpenNest
private static double GetDimension(Box box, NestDirection direction)
{
return direction == NestDirection.Horizontal ? box.Width : box.Height;
return direction == NestDirection.Horizontal ? box.Width : box.Length;
}
private static double GetStart(Box box, NestDirection direction)
@@ -321,7 +321,7 @@ namespace OpenNest
template.Offset(WorkArea.Location - template.BoundingBox.Location);
if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
template.BoundingBox.Height > WorkArea.Height + Tolerance.Epsilon)
template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
return pattern;
pattern.Parts.Add(template);
@@ -472,7 +472,7 @@ namespace OpenNest
if (width <= Tolerance.Epsilon)
return new List<Part>();
remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Height);
remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Length);
}
// Build rotation set: always try cardinal orientations (0° and 90°),
@@ -601,7 +601,7 @@ namespace OpenNest
var basePattern = pattern.Clone(offset);
if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon ||
basePattern.BoundingBox.Height > WorkArea.Height + Tolerance.Epsilon)
basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon)
return new List<Part>();
return FillRecursive(basePattern, primaryAxis, depth: 0);
+1 -1
View File
@@ -80,7 +80,7 @@ namespace OpenNest
if (maxRight < workArea.Right)
{
var width = workArea.Right - maxRight;
var height = workArea.Height;
var height = workArea.Length;
if (System.Math.Min(width, height) >= MinRemnantDimension)
largest = System.Math.Max(largest, width * height);
+10 -10
View File
@@ -70,8 +70,8 @@ namespace OpenNest
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Height);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Height);
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
if (workAreaShortSide < partLongestSide)
{
@@ -113,7 +113,7 @@ namespace OpenNest
}
var bestLinearScore = best != null ? FillScore.Compute(best, workArea) : default;
Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Height:F1} | Angles: {angles.Count}");
Debug.WriteLine($"[FindBestFill] Linear: {bestLinearScore.Count} parts, density={bestLinearScore.Density:P1} | WorkArea: {workArea.Width:F1}x{workArea.Length:F1} | Angles: {angles.Count}");
// Try rectangle best-fit (mixes orientations to fill remnant strips).
var rectResult = FillRectangleBestFit(item, workArea);
@@ -143,7 +143,7 @@ namespace OpenNest
var angles = RotationAnalysis.FindHullEdgeAngles(groupParts);
var best = FillPattern(engine, groupParts, angles, workArea);
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Height:F1}");
Debug.WriteLine($"[Fill(groupParts,Box)] Linear: {best?.Count ?? 0} parts | WorkArea: {workArea.Width:F1}x{workArea.Length:F1}");
if (groupParts.Count == 1)
{
@@ -213,7 +213,7 @@ namespace OpenNest
private List<Part> FillWithPairs(NestItem item, Box workArea)
{
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, Plate.Size.Width, Plate.Size.Height,
item.Drawing, Plate.Size.Width, Plate.Size.Length,
Plate.PartSpacing);
var candidates = SelectPairCandidates(bestFits, workArea);
@@ -260,8 +260,8 @@ namespace OpenNest
var kept = bestFits.Where(r => r.Keep).ToList();
var top = kept.Take(50).ToList();
var workShortSide = System.Math.Min(workArea.Width, workArea.Height);
var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Height);
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
var plateShortSide = System.Math.Min(Plate.Size.Width, Plate.Size.Length);
// When the work area is significantly narrower than the plate,
// include all pairs that fit the narrow dimension.
@@ -356,7 +356,7 @@ namespace OpenNest
var refDim = horizontal
? sorted.Max(p => p.BoundingBox.Width)
: sorted.Max(p => p.BoundingBox.Height);
: sorted.Max(p => p.BoundingBox.Length);
var gapThreshold = refDim * 0.5;
var clusters = new List<List<Part>>();
@@ -425,7 +425,7 @@ namespace OpenNest
if (stripWidth <= 0)
return null;
stripBox = new Box(stripLeft, workArea.Y, stripWidth, workArea.Height);
stripBox = new Box(stripLeft, workArea.Y, stripWidth, workArea.Length);
}
else
{
@@ -438,7 +438,7 @@ namespace OpenNest
stripBox = new Box(workArea.X, stripBottom, workArea.Width, stripHeight);
}
Debug.WriteLine($"[TryStripRefill] Strip: {stripBox.Width:F1}x{stripBox.Height:F1} at ({stripBox.X:F1},{stripBox.Y:F1})");
Debug.WriteLine($"[TryStripRefill] Strip: {stripBox.Width:F1}x{stripBox.Length:F1} at ({stripBox.X:F1},{stripBox.Y:F1})");
var stripParts = FindBestFill(item, stripBox);
@@ -15,7 +15,7 @@ namespace OpenNest.RectanglePacking
};
bin.Width += partSpacing;
bin.Height += partSpacing;
bin.Length += partSpacing;
return bin;
}
@@ -25,7 +25,7 @@ namespace OpenNest.RectanglePacking
var box = item.Drawing.Program.BoundingBox();
box.Width += partSpacing;
box.Height += partSpacing;
box.Length += partSpacing;
return new Item
{
@@ -44,11 +44,11 @@ namespace OpenNest.RectanglePacking
int normalColumns = 0;
int rotateColumns = 0;
if (!BestCombination.FindFrom2(item.Width, item.Height, bin.Width, out normalColumns, out rotateColumns))
if (!BestCombination.FindFrom2(item.Width, item.Length, bin.Width, out normalColumns, out rotateColumns))
return bin;
var normalRows = (int)System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Height);
var rotateRows = (int)System.Math.Floor((bin.Height + Tolerance.Epsilon) / item.Width);
var normalRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Length);
var rotateRows = (int)System.Math.Floor((bin.Length + Tolerance.Epsilon) / item.Width);
item.Location = bin.Location;
@@ -69,17 +69,17 @@ namespace OpenNest.RectanglePacking
int normalRows = 0;
int rotateRows = 0;
if (!BestCombination.FindFrom2(item.Height, item.Width, Bin.Height, out normalRows, out rotateRows))
if (!BestCombination.FindFrom2(item.Length, item.Width, Bin.Length, out normalRows, out rotateRows))
return bin;
var normalColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
var rotateColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Height);
var rotateColumns = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Length);
item.Location = bin.Location;
bin.Items.AddRange(FillGrid(item, normalRows, normalColumns, int.MaxValue));
item.Location.Y += item.Height * normalRows;
item.Location.Y += item.Length * normalRows;
item.Rotate();
bin.Items.AddRange(FillGrid(item, rotateRows, rotateColumns, int.MaxValue));
@@ -28,7 +28,7 @@ namespace OpenNest.RectanglePacking
for (var j = 0; j < innerCount; j++)
{
var x = (columnMajor ? i : j) * item.Width + item.X;
var y = (columnMajor ? j : i) * item.Height + item.Y;
var y = (columnMajor ? j : i) * item.Length + item.Y;
var clone = item.Clone() as Item;
clone.Location = new Vector(x, y);
@@ -15,7 +15,7 @@ namespace OpenNest.RectanglePacking
public override void Fill(Item item)
{
var ycount = (int)System.Math.Floor((Bin.Height + Tolerance.Epsilon) / item.Height);
var ycount = (int)System.Math.Floor((Bin.Length + Tolerance.Epsilon) / item.Length);
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
for (int i = 0; i < xcount; i++)
@@ -24,7 +24,7 @@ namespace OpenNest.RectanglePacking
for (int j = 0; j < ycount; j++)
{
var y = item.Height * j + Bin.Y;
var y = item.Length * j + Bin.Y;
var addedItem = item.Clone() as Item;
addedItem.Location = new Vector(x, y);
@@ -36,7 +36,7 @@ namespace OpenNest.RectanglePacking
public override void Fill(Item item, int maxCount)
{
var ycount = (int)System.Math.Floor((Bin.Height + Tolerance.Epsilon) / item.Height);
var ycount = (int)System.Math.Floor((Bin.Length + Tolerance.Epsilon) / item.Length);
var xcount = (int)System.Math.Floor((Bin.Width + Tolerance.Epsilon) / item.Width);
var count = ycount * xcount;
@@ -60,7 +60,7 @@ namespace OpenNest.RectanglePacking
columns = (int)System.Math.Ceiling((double)maxCount / rows);
}
Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Height));
Bin.Items.AddRange(FillGrid(item, rows, columns, maxCount, columnMajor: item.Width > item.Length));
}
}
}
+2 -2
View File
@@ -12,7 +12,7 @@ namespace OpenNest.RectanglePacking
public void Rotate()
{
Generic.Swap(ref Size.Width, ref Size.Height);
Generic.Swap(ref Size.Width, ref Size.Length);
IsRotated = !IsRotated;
}
@@ -38,7 +38,7 @@ namespace OpenNest.RectanglePacking
double minX = items[0].X;
double minY = items[0].Y;
double maxX = items[0].X + items[0].Width;
double maxY = items[0].Y + items[0].Height;
double maxY = items[0].Y + items[0].Length;
foreach (var box in items)
{
@@ -16,11 +16,11 @@ namespace OpenNest.RectanglePacking
public override void Pack(List<Item> items)
{
items = items.OrderBy(i => -i.Height).ToList();
items = items.OrderBy(i => -i.Length).ToList();
foreach (var item in items)
{
if (item.Height > Bin.Height)
if (item.Length > Bin.Length)
continue;
var level = FindLevel(item);
@@ -36,7 +36,7 @@ namespace OpenNest.RectanglePacking
{
foreach (var level in levels)
{
if (level.Height < item.Height)
if (level.Height < item.Length)
continue;
if (level.RemainingWidth < item.Width)
@@ -58,12 +58,12 @@ namespace OpenNest.RectanglePacking
var remaining = Bin.Top - y;
if (remaining < item.Height)
if (remaining < item.Length)
return null;
var level = new Level(Bin);
level.Y = y;
level.Height = item.Height;
level.Height = item.Length;
levels.Add(level);
+8 -8
View File
@@ -145,29 +145,29 @@ namespace OpenNest.IO
{
case 1:
pt1 = new XYZ(0, 0, 0);
pt2 = new XYZ(0, plate.Size.Height, 0);
pt3 = new XYZ(plate.Size.Width, plate.Size.Height, 0);
pt2 = new XYZ(0, plate.Size.Length, 0);
pt3 = new XYZ(plate.Size.Width, plate.Size.Length, 0);
pt4 = new XYZ(plate.Size.Width, 0, 0);
break;
case 2:
pt1 = new XYZ(0, 0, 0);
pt2 = new XYZ(0, plate.Size.Height, 0);
pt3 = new XYZ(-plate.Size.Width, plate.Size.Height, 0);
pt2 = new XYZ(0, plate.Size.Length, 0);
pt3 = new XYZ(-plate.Size.Width, plate.Size.Length, 0);
pt4 = new XYZ(-plate.Size.Width, 0, 0);
break;
case 3:
pt1 = new XYZ(0, 0, 0);
pt2 = new XYZ(0, -plate.Size.Height, 0);
pt3 = new XYZ(-plate.Size.Width, -plate.Size.Height, 0);
pt2 = new XYZ(0, -plate.Size.Length, 0);
pt3 = new XYZ(-plate.Size.Width, -plate.Size.Length, 0);
pt4 = new XYZ(-plate.Size.Width, 0, 0);
break;
case 4:
pt1 = new XYZ(0, 0, 0);
pt2 = new XYZ(0, -plate.Size.Height, 0);
pt3 = new XYZ(plate.Size.Width, -plate.Size.Height, 0);
pt2 = new XYZ(0, -plate.Size.Length, 0);
pt3 = new XYZ(plate.Size.Width, -plate.Size.Length, 0);
pt4 = new XYZ(plate.Size.Width, 0, 0);
break;
+126
View File
@@ -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 Length { 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
View File
@@ -1,45 +1,28 @@
using System;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using System.Text.Json;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using static OpenNest.IO.NestFormat;
namespace OpenNest.IO
{
public sealed class NestReader
{
private ZipArchive zipArchive;
private Dictionary<int, Plate> plateDict;
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();
}
private readonly Stream stream;
private readonly ZipArchive zipArchive;
public NestReader(string file)
: this()
{
stream = new FileStream(file, FileMode.Open, FileAccess.Read);
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
}
public NestReader(Stream stream)
: this()
{
this.stream = stream;
zipArchive = new ZipArchive(stream, ZipArchiveMode.Read);
@@ -47,52 +30,12 @@ namespace OpenNest.IO
public Nest Read()
{
const string plateExtensionPattern = "plate-\\d\\d\\d";
const string programExtensionPattern = "program-\\d\\d\\d";
var nestJson = ReadEntry("nest.json");
var dto = JsonSerializer.Deserialize<NestDto>(nestJson, JsonOptions);
foreach (var entry in zipArchive.Entries)
{
var memstream = new MemoryStream();
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();
var programs = ReadPrograms(dto.Drawings.Count);
var drawingMap = BuildDrawings(dto, programs);
var nest = BuildNest(dto, drawingMap);
zipArchive.Dispose();
stream.Close();
@@ -100,374 +43,114 @@ namespace OpenNest.IO
return nest;
}
private void ReadNestInfo(Stream stream)
private string ReadEntry(string name)
{
var reader = XmlReader.Create(stream);
var spacing = new Spacing();
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();
}
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())
continue;
var entry = zipArchive.GetEntry($"programs/program-{i}");
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.Length);
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.Length);
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":
nest.Name = reader["name"];
break;
if (!drawingMap.TryGetValue(partDto.DrawingId, out var dwg))
continue;
case "Units":
Units units;
TryParseEnum<Units>(reader.ReadString(), out units);
nest.Units = units;
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;
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);
}
reader.Close();
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
return nest;
}
}
}
+132 -233
View File
@@ -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, Length = pd.Size.Length },
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, Length = plate.Size.Length },
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));
+5 -5
View File
@@ -40,10 +40,10 @@ namespace OpenNest.Mcp.Tools
{
var plate = nest.Plates[i];
var work = plate.WorkArea();
sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Height:F1}, " +
sb.AppendLine($" Plate {i}: {plate.Size.Width:F1} x {plate.Size.Length:F1}, " +
$"parts={plate.Parts.Count}, " +
$"utilization={plate.Utilization():P1}, " +
$"work area={work.Width:F1} x {work.Height:F1}");
$"work area={work.Width:F1} x {work.Length:F1}");
}
sb.AppendLine($"Drawings: {nest.Drawings.Count}");
@@ -51,7 +51,7 @@ namespace OpenNest.Mcp.Tools
foreach (var dwg in nest.Drawings)
{
var bbox = dwg.Program.BoundingBox();
sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Height:F2}, " +
sb.AppendLine($" {dwg.Name}: bbox={bbox.Width:F2} x {bbox.Length:F2}, " +
$"required={dwg.Quantity.Required}, nested={dwg.Quantity.Nested}");
}
@@ -85,7 +85,7 @@ namespace OpenNest.Mcp.Tools
_session.Drawings.Add(drawing);
var bbox = pgm.BoundingBox();
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
return $"Imported drawing '{drawingName}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
}
[McpServerTool(Name = "create_drawing")]
@@ -134,7 +134,7 @@ namespace OpenNest.Mcp.Tools
_session.Drawings.Add(drawing);
var bbox = pgm.BoundingBox();
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Height:F2}";
return $"Created drawing '{name}': bbox={bbox.Width:F2} x {bbox.Length:F2}";
}
private static CncProgram CreateRectangle(double width, double height)
+4 -4
View File
@@ -32,13 +32,13 @@ namespace OpenNest.Mcp.Tools
var sb = new StringBuilder();
sb.AppendLine($"Plate {plateIndex}:");
sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
sb.AppendLine($" Size: {plate.Size.Width:F1} x {plate.Size.Length:F1}");
sb.AppendLine($" Quadrant: {plate.Quadrant}");
sb.AppendLine($" Thickness: {plate.Thickness:F2}");
sb.AppendLine($" Material: {plate.Material.Name}");
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Height:F1}");
sb.AppendLine($" Work area: {work.X:F1},{work.Y:F1} {work.Width:F1}x{work.Length:F1}");
sb.AppendLine($" Parts: {plate.Parts.Count}");
sb.AppendLine($" Utilization: {plate.Utilization():P1}");
sb.AppendLine($" Quantity: {plate.Quantity}");
@@ -57,7 +57,7 @@ namespace OpenNest.Mcp.Tools
for (var i = 0; i < remnants.Count; i++)
{
var r = remnants[i];
sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Height:F1}, area={r.Area():F1}");
sb.AppendLine($" Remnant {i}: ({r.X:F1},{r.Y:F1}) {r.Width:F1}x{r.Length:F1}, area={r.Area():F1}");
}
return sb.ToString();
@@ -90,7 +90,7 @@ namespace OpenNest.Mcp.Tools
sb.AppendLine($" [{i}] {part.BaseDrawing.Name}: " +
$"loc=({part.Location.X:F2},{part.Location.Y:F2}), " +
$"rot={rotDeg:F1} deg, " +
$"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Height:F2})");
$"bbox=({bbox.X:F2},{bbox.Y:F2} {bbox.Width:F2}x{bbox.Length:F2})");
}
if (plate.Parts.Count > limit)
+1 -1
View File
@@ -121,7 +121,7 @@ namespace OpenNest.Mcp.Tools
var added = plate.Parts.Count - countBefore;
totalAdded += added;
sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Height:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
sb.AppendLine($" Remnant {i}: ({remnant.X:F1},{remnant.Y:F1} {remnant.Width:F1}x{remnant.Length:F1}) -> {added} parts {(success ? "" : "(no fit)")}");
}
sb.AppendLine($"Total parts added: {totalAdded}");
+2 -2
View File
@@ -41,11 +41,11 @@ namespace OpenNest.Mcp.Tools
var work = plate.WorkArea();
var sb = new StringBuilder();
sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Height:F1}");
sb.AppendLine($"Created plate {index}: {plate.Size.Width:F1} x {plate.Size.Length:F1}");
sb.AppendLine($" Quadrant: {plate.Quadrant}");
sb.AppendLine($" Part spacing: {plate.PartSpacing:F2}");
sb.AppendLine($" Edge spacing: L={plate.EdgeSpacing.Left:F2} B={plate.EdgeSpacing.Bottom:F2} R={plate.EdgeSpacing.Right:F2} T={plate.EdgeSpacing.Top:F2}");
sb.AppendLine($" Work area: {work.Width:F1} x {work.Height:F1}");
sb.AppendLine($" Work area: {work.Width:F1} x {work.Length:F1}");
return sb.ToString();
}
+1 -1
View File
@@ -95,7 +95,7 @@ namespace OpenNest.Actions
var location = plateView.PointWorldToGraph(SelectedArea.Location);
var size = new SizeF(
plateView.LengthWorldToGui(SelectedArea.Width),
plateView.LengthWorldToGui(SelectedArea.Height));
plateView.LengthWorldToGui(SelectedArea.Length));
var rect = new System.Drawing.RectangleF(location.X, location.Y - size.Height, size.Width, size.Height);
+1 -1
View File
@@ -205,7 +205,7 @@ namespace OpenNest.Controls
public virtual void ZoomToArea(Box box, bool redraw = true)
{
ZoomToArea(box.X, box.Y, box.Width, box.Height, redraw);
ZoomToArea(box.X, box.Y, box.Width, box.Length, redraw);
}
public virtual void ZoomToArea(double x, double y, double width, double height, bool redraw = true)
+7 -7
View File
@@ -384,13 +384,13 @@ namespace OpenNest.Controls
var plateRect = new RectangleF
{
Width = LengthWorldToGui(Plate.Size.Width),
Height = LengthWorldToGui(Plate.Size.Height)
Height = LengthWorldToGui(Plate.Size.Length)
};
var edgeSpacingRect = new RectangleF
{
Width = LengthWorldToGui(Plate.Size.Width - Plate.EdgeSpacing.Left - Plate.EdgeSpacing.Right),
Height = LengthWorldToGui(Plate.Size.Height - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom)
Height = LengthWorldToGui(Plate.Size.Length - Plate.EdgeSpacing.Top - Plate.EdgeSpacing.Bottom)
};
switch (Plate.Quadrant)
@@ -410,17 +410,17 @@ namespace OpenNest.Controls
break;
case 3:
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Height);
plateRect.Location = PointWorldToGraph(-Plate.Size.Width, -Plate.Size.Length);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left - Plate.Size.Width,
Plate.EdgeSpacing.Bottom - Plate.Size.Height);
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
break;
case 4:
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Height);
plateRect.Location = PointWorldToGraph(0, -Plate.Size.Length);
edgeSpacingRect.Location = PointWorldToGraph(
Plate.EdgeSpacing.Left,
Plate.EdgeSpacing.Bottom - Plate.Size.Height);
Plate.EdgeSpacing.Bottom - Plate.Size.Length);
break;
default:
@@ -590,7 +590,7 @@ namespace OpenNest.Controls
{
Location = PointWorldToGraph(box.Location),
Width = LengthWorldToGui(box.Width),
Height = LengthWorldToGui(box.Height)
Height = LengthWorldToGui(box.Length)
};
g.DrawRectangle(ColorScheme.BoundingBoxPen, rect.X, rect.Y - rect.Height, rect.Width, rect.Height);
+1 -1
View File
@@ -56,7 +56,7 @@ namespace OpenNest.Forms
var sw = Stopwatch.StartNew();
var results = BestFitCache.GetOrCompute(
drawing, plate.Size.Width, plate.Size.Height, plate.PartSpacing);
drawing, plate.Size.Width, plate.Size.Length, plate.PartSpacing);
var findMs = sw.ElapsedMilliseconds;
var total = results.Count;
+1 -1
View File
@@ -112,7 +112,7 @@ namespace OpenNest.Forms
drawing.Source.Path = item.Path;
drawing.Quantity.Required = item.Quantity;
var shape = new DefinedShape(entities);
var shape = new ShapeProfile(entities);
SetRotation(shape.Perimeter, RotationType.CW);
+1 -1
View File
@@ -173,7 +173,7 @@ namespace OpenNest.Forms
return;
}
if (TopSpacing + BottomSpacing >= size.Height)
if (TopSpacing + BottomSpacing >= size.Length)
{
applyButton.Enabled = false;
return;
+1 -1
View File
@@ -145,7 +145,7 @@ namespace OpenNest.Forms
return;
}
if (TopSpacing + BottomSpacing >= size.Height)
if (TopSpacing + BottomSpacing >= size.Length)
{
applyButton.Enabled = false;
return;
+2 -2
View File
@@ -43,8 +43,8 @@ namespace OpenNest.Forms
UpdateStatus();
UpdateGpuStatus();
if (GpuEvaluatorFactory.GpuAvailable)
BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
//if (GpuEvaluatorFactory.GpuAvailable)
// BestFitCache.CreateEvaluator = (drawing, spacing) => GpuEvaluatorFactory.Create(drawing, spacing);
}
private string GetNestName(DateTime date, int id)
@@ -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)`