merge: resolve .gitignore conflict, keep both entries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 07:22:45 -04:00
70 changed files with 4008 additions and 795 deletions
@@ -0,0 +1,134 @@
# Contour Re-Indexing Design
## Overview
Add entity-splitting primitives and a `Shape.ReindexAt` method so that a closed contour can be reordered to start (and end) at an arbitrary point. Then wire this into `ContourCuttingStrategy.Apply()` to replace the `NotImplementedException` stubs.
All geometry additions live on existing classes in `OpenNest.Geometry`. The strategy wiring is a change to the existing `ContourCuttingStrategy` in `OpenNest.CNC.CuttingStrategy`.
## Entity Splitting Primitives
### Line.SplitAt(Vector point)
```csharp
public (Line first, Line second) SplitAt(Vector point)
```
- Returns two lines: `StartPoint → point` and `point → EndPoint`.
- If the point is at `StartPoint` (within `Tolerance.Epsilon` distance), `first` is null.
- If the point is at `EndPoint` (within `Tolerance.Epsilon` distance), `second` is null.
- The point is assumed to lie on the line (caller is responsible — it comes from `ClosestPointTo`).
### Arc.SplitAt(Vector point)
```csharp
public (Arc first, Arc second) SplitAt(Vector point)
```
- Computes `splitAngle = Center.AngleTo(point)`, normalized via `Angle.NormalizeRad`.
- First arc: same center, radius, direction — `StartAngle → splitAngle`.
- Second arc: same center, radius, direction — `splitAngle → EndAngle`.
- **Endpoint tolerance**: compare `point.DistanceTo(arc.StartPoint())` and `point.DistanceTo(arc.EndPoint())` rather than comparing angles directly. This avoids wrap-around issues at the 0/2π boundary.
- If the point is at `StartPoint()` (within `Tolerance.Epsilon` distance), `first` is null.
- If the point is at `EndPoint()` (within `Tolerance.Epsilon` distance), `second` is null.
### Circle — no conversion needed
Circles are kept as-is in `ReindexAt`. The `ConvertShapeToMoves` method handles circles directly by emitting an `ArcMove` from the start point back to itself (a full circle), matching the existing `ConvertGeometry.AddCircle` pattern. This avoids the problem of constructing a "full-sweep arc" where `StartAngle == EndAngle` would produce zero sweep.
## Shape.ReindexAt
```csharp
public Shape ReindexAt(Vector point, Entity entity)
```
- `point`: the start/end point for the reindexed contour (from `ClosestPointTo`).
- `entity`: the entity containing `point` (from `ClosestPointTo`'s `out` parameter).
- Returns a **new** Shape (does not modify the original). The new shape shares entity references with the original for unsplit entities — callers must not mutate either.
- Throws `ArgumentException` if `entity` is not found in `Entities`.
### Algorithm
1. If `entity` is a `Circle`:
- Return a new Shape with that single `Circle` entity and `point` stored for `ConvertShapeToMoves` to use as the start point.
2. Find the index `i` of `entity` in `Entities`. Throw `ArgumentException` if not found.
3. Split the entity at `point`:
- `Line``line.SplitAt(point)``(firstHalf, secondHalf)`
- `Arc``arc.SplitAt(point)``(firstHalf, secondHalf)`
4. Build the new entity list (skip null entries):
- `secondHalf` (if not null)
- `Entities[i+1]`, `Entities[i+2]`, ..., `Entities[count-1]` (after the split)
- `Entities[0]`, `Entities[1]`, ..., `Entities[i-1]` (before the split, wrapping around)
- `firstHalf` (if not null)
5. Return a new Shape with this entity list.
### Edge Cases
- **Point lands on entity boundary** (start/end of an entity): one half of the split is null. The reordering still works — it just starts from the next full entity.
- **Single-entity shape that is an Arc**: split produces two arcs, reorder is just `[secondHalf, firstHalf]`.
- **Single-entity Circle**: handled by step 1 — kept as Circle, converted to a full-circle ArcMove in `ConvertShapeToMoves`.
## Wiring into ContourCuttingStrategy
### Entity-to-ICode Conversion
Add a private method to `ContourCuttingStrategy`:
```csharp
private List<ICode> ConvertShapeToMoves(Shape shape, Vector startPoint)
```
The `startPoint` parameter is needed for the Circle case (to know where the full-circle ArcMove starts).
Iterates `shape.Entities` and converts each to cutting moves using **absolute coordinates** (consistent with `ConvertGeometry`):
- `Line``LinearMove(line.EndPoint)`
- `Arc``ArcMove(arc.EndPoint(), arc.Center, arc.IsReversed ? RotationType.CW : RotationType.CCW)`
- `Circle``ArcMove(startPoint, circle.Center, circle.Rotation)` — full circle from start point back to itself, matching `ConvertGeometry.AddCircle`
- Any other entity type → throw `InvalidOperationException`
No `RapidMove` between entities — they are contiguous in a reindexed shape. The lead-in already positions the head at the shape's start point.
### Replace NotImplementedException
In `ContourCuttingStrategy.Apply()`, replace the two `throw new NotImplementedException(...)` blocks:
**Cutout loop** (uses `cutout` shape variable):
```csharp
var reindexed = cutout.ReindexAt(closestPt, entity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, closestPt));
```
**Perimeter block** (uses `profile.Perimeter`):
```csharp
var reindexed = profile.Perimeter.ReindexAt(perimeterPt, perimeterEntity);
result.Codes.AddRange(ConvertShapeToMoves(reindexed, perimeterPt));
```
The full sequence for each contour becomes:
1. Lead-in codes (rapid to pierce point, cutting moves to contour start)
2. Contour body (reindexed entity moves from `ConvertShapeToMoves`)
3. Lead-out codes (overcut moves away from contour)
### MicrotabLeadOut Handling
When the lead-out is `MicrotabLeadOut`, the last cutting move must be trimmed by `GapSize`. This is a separate concern from re-indexing — stub it with a TODO comment for now. The trimming logic will shorten the last `LinearMove` or `ArcMove` in the contour body.
## Files Modified
| File | Change |
|------|--------|
| `OpenNest.Core/Geometry/Line.cs` | Add `SplitAt(Vector)` method |
| `OpenNest.Core/Geometry/Arc.cs` | Add `SplitAt(Vector)` method |
| `OpenNest.Core/Geometry/Shape.cs` | Add `ReindexAt(Vector, Entity)` method |
| `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` | Add `ConvertShapeToMoves`, replace `NotImplementedException` blocks |
## Out of Scope
- **MicrotabLeadOut trimming** (trim last move by gap size — stubbed with TODO)
- **Tab insertion** (inserting tab codes mid-contour — already stubbed)
- **Lead-in editor UI** (interactive start point selection — separate feature)
- **Contour re-indexing for open shapes** (only closed contours supported)
@@ -0,0 +1,420 @@
# CNC Cutting Strategy Design
## Overview
Add lead-in, lead-out, and tab classes to `OpenNest.Core` that generate `ICode` instructions for CNC cutting approach/exit geometry. The strategy runs at nest-time — `ContourCuttingStrategy.Apply()` produces a new `Program` with lead-ins, lead-outs, start points, and contour ordering baked in. This modified program is what gets saved to the nest file and later fed to the post-processor for machine-specific G-code translation. The original `Drawing.Program` stays untouched; the strategy output lives on the `Part`.
All new code lives in `OpenNest.Core/CNC/CuttingStrategy/`.
## File Structure
```
OpenNest.Core/CNC/CuttingStrategy/
├── LeadIns/
│ ├── LeadIn.cs
│ ├── NoLeadIn.cs
│ ├── LineLeadIn.cs
│ ├── LineArcLeadIn.cs
│ ├── ArcLeadIn.cs
│ ├── LineLineLeadIn.cs
│ └── CleanHoleLeadIn.cs
├── LeadOuts/
│ ├── LeadOut.cs
│ ├── NoLeadOut.cs
│ ├── LineLeadOut.cs
│ ├── ArcLeadOut.cs
│ └── MicrotabLeadOut.cs
├── Tabs/
│ ├── Tab.cs
│ ├── NormalTab.cs
│ ├── BreakerTab.cs
│ └── MachineTab.cs
├── ContourType.cs
├── CuttingParameters.cs
├── ContourCuttingStrategy.cs
├── SequenceParameters.cs
└── AssignmentParameters.cs
```
## Namespace
All classes use `namespace OpenNest.CNC.CuttingStrategy`.
## Type Mappings from Original Spec
The original spec used placeholder names. These are the correct codebase types:
| Spec type | Actual type | Notes |
|-----------|------------|-------|
| `PointD` | `Vector` | `OpenNest.Geometry.Vector` — struct with `X`, `Y` fields |
| `CircularMove` | `ArcMove` | Constructor: `ArcMove(Vector endPoint, Vector centerPoint, RotationType rotation)` |
| `CircularDirection` | `RotationType` | Enum with `CW`, `CCW` |
| `value.ToRadians()` | `Angle.ToRadians(value)` | Static method on `OpenNest.Math.Angle` |
| `new Program(codes)` | Build manually | Create `Program()`, add to `.Codes` list |
## LeadIn Hierarchy
### Abstract Base: `LeadIn`
```csharp
public abstract class LeadIn
{
public abstract List<ICode> Generate(Vector contourStartPoint, double contourNormalAngle,
RotationType winding = RotationType.CW);
public abstract Vector GetPiercePoint(Vector contourStartPoint, double contourNormalAngle);
}
```
- `contourStartPoint`: where the contour cut begins (first point of the part profile).
- `contourNormalAngle`: normal angle (radians) at the contour start point, pointing **away from the part material** (outward from perimeter, into scrap for cutouts).
- `winding`: contour winding direction — arc-based lead-ins use this for their `ArcMove` rotation.
- `Generate` returns ICode instructions starting with a `RapidMove` to the pierce point, followed by cutting moves to reach the contour start.
- `GetPiercePoint` computes where the head rapids to before firing — useful for visualization and collision detection.
### NoLeadIn (Type 0)
Pierce directly on the contour start point. Returns a single `RapidMove(contourStartPoint)`.
### LineLeadIn (Type 1)
Straight line approach.
Properties:
- `Length` (double): distance from pierce point to contour start (inches)
- `ApproachAngle` (double): approach angle in degrees relative to contour tangent. 90 = perpendicular, 135 = acute angle (common for plasma). Default: 90.
Pierce point offset: `contourStartPoint + Length` along `contourNormalAngle + Angle.ToRadians(ApproachAngle)`.
Generates: `RapidMove(piercePoint)``LinearMove(contourStartPoint)`.
> **Note:** Properties are named `ApproachAngle` (not `Angle`) to avoid shadowing the `OpenNest.Math.Angle` static class. This applies to all lead-in/lead-out/tab classes.
### LineArcLeadIn (Type 2)
Line followed by tangential arc meeting the contour. Most common for plasma.
Properties:
- `LineLength` (double): straight approach segment length
- `ApproachAngle` (double): line angle relative to contour. Default: 135.
- `ArcRadius` (double): radius of tangential arc
Geometry: Pierce → [Line] → Arc start → [Arc] → Contour start. Arc center is at `contourStartPoint + ArcRadius` along normal. Arc rotation direction matches contour winding (CW for CW contours, CCW for CCW).
Generates: `RapidMove(piercePoint)``LinearMove(arcStart)``ArcMove(contourStartPoint, arcCenter, rotation)`.
### ArcLeadIn (Type 3)
Pure arc approach, no straight line segment.
Properties:
- `Radius` (double): arc radius
Pierce point is diametrically opposite the contour start on the arc circle. Arc center at `contourStartPoint + Radius` along normal.
Arc rotation direction matches contour winding.
Generates: `RapidMove(piercePoint)``ArcMove(contourStartPoint, arcCenter, rotation)`.
### LineLineLeadIn (Type 5)
Two-segment straight line approach.
Properties:
- `Length1` (double): first segment length
- `ApproachAngle1` (double): first segment angle. Default: 90.
- `Length2` (double): second segment length
- `ApproachAngle2` (double): direction change. Default: 90.
Generates: `RapidMove(piercePoint)``LinearMove(midPoint)``LinearMove(contourStartPoint)`.
### CleanHoleLeadIn
Specialized for precision circular holes. Same geometry as `LineArcLeadIn` but with hard-coded 135° angle and a `Kerf` property. The overcut (cutting past start to close the hole) is handled at the lead-out, not here.
Properties:
- `LineLength` (double)
- `ArcRadius` (double)
- `Kerf` (double)
## LeadOut Hierarchy
### Abstract Base: `LeadOut`
```csharp
public abstract class LeadOut
{
public abstract List<ICode> Generate(Vector contourEndPoint, double contourNormalAngle,
RotationType winding = RotationType.CW);
}
```
- `contourEndPoint`: where the contour cut ends. For closed contours, same as start.
- Returns ICode instructions appended after the contour's last cut point.
### NoLeadOut (Type 0)
Returns empty list. Cut ends exactly at contour end.
### LineLeadOut (Type 1)
Straight line overcut past contour end.
Properties:
- `Length` (double): overcut distance
- `ApproachAngle` (double): direction relative to contour tangent. Default: 90.
Generates: `LinearMove(endPoint)` where endPoint is offset from contourEndPoint.
### ArcLeadOut (Type 3)
Arc overcut curving away from the part.
Properties:
- `Radius` (double)
Arc center at `contourEndPoint + Radius` along normal. End point is a quarter turn away. Arc rotation direction matches contour winding.
Generates: `ArcMove(endPoint, arcCenter, rotation)`.
### MicrotabLeadOut (Type 4)
Stops short of contour end, leaving an uncut bridge. Laser only.
Properties:
- `GapSize` (double): uncut material length. Default: 0.03".
Does NOT add instructions — returns empty list. The `ContourCuttingStrategy` detects this type and trims the last cutting move by `GapSize` instead.
## Tab Hierarchy
Tabs are mid-contour features that temporarily lift the beam to leave bridges holding the part in place.
### Abstract Base: `Tab`
```csharp
public abstract class Tab
{
public double Size { get; set; } = 0.03;
public LeadIn TabLeadIn { get; set; }
public LeadOut TabLeadOut { get; set; }
public abstract List<ICode> Generate(
Vector tabStartPoint, Vector tabEndPoint, double contourNormalAngle);
}
```
### NormalTab
Standard tab: cut up to tab start, lift/rapid over gap, resume cutting.
Additional properties:
- `CutoutMinWidth`, `CutoutMinHeight` (double): minimum cutout size to receive this tab
- `CutoutMaxWidth`, `CutoutMaxHeight` (double): maximum cutout size to receive this tab
- `AppliesToCutout(double width, double height)` method for size filtering
Generates: TabLeadOut codes → `RapidMove(tabEndPoint)` → TabLeadIn codes.
### BreakerTab
Like NormalTab but adds a scoring cut into the part at the tab location to make snapping easier.
Additional properties:
- `BreakerDepth` (double): how far the score cuts into the part
- `BreakerLeadInLength` (double)
- `BreakerAngle` (double)
Generates: TabLeadOut codes → `LinearMove(scoreEnd)``RapidMove(tabEndPoint)` → TabLeadIn codes.
### MachineTab
Tab behavior configured at the CNC controller level. OpenNest just signals the controller.
Additional properties:
- `MachineTabId` (int): passed to post-processor for M-code translation
Returns a placeholder `RapidMove(tabEndPoint)` — the post-processor plugin replaces this with machine-specific commands.
## CuttingParameters
One instance per material/machine combination. Ties everything together.
```csharp
public class CuttingParameters
{
public int Id { get; set; }
// Material/Machine identification
public string MachineName { get; set; }
public string MaterialName { get; set; }
public string Grade { get; set; }
public double Thickness { get; set; }
// Kerf and spacing
public double Kerf { get; set; }
public double PartSpacing { get; set; }
// External contour lead-in/out
public LeadIn ExternalLeadIn { get; set; } = new NoLeadIn();
public LeadOut ExternalLeadOut { get; set; } = new NoLeadOut();
// Internal contour lead-in/out
public LeadIn InternalLeadIn { get; set; } = new LineLeadIn { Length = 0.125, Angle = 90 };
public LeadOut InternalLeadOut { get; set; } = new NoLeadOut();
// Arc/circle specific (overrides internal for circular features)
public LeadIn ArcCircleLeadIn { get; set; } = new NoLeadIn();
public LeadOut ArcCircleLeadOut { get; set; } = new NoLeadOut();
// Tab configuration
public Tab TabConfig { get; set; }
public bool TabsEnabled { get; set; } = false;
// Sequencing and assignment
public SequenceParameters Sequencing { get; set; } = new SequenceParameters();
public AssignmentParameters Assignment { get; set; } = new AssignmentParameters();
}
```
## SequenceParameters and AssignmentParameters
```csharp
// Values match PEP Technology's numbering scheme (value 6 intentionally skipped)
public enum SequenceMethod
{
RightSide = 1, LeastCode = 2, Advanced = 3,
BottomSide = 4, EdgeStart = 5, LeftSide = 7, RightSideAlt = 8
}
public class SequenceParameters
{
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
public double SmallCutoutWidth { get; set; } = 1.5;
public double SmallCutoutHeight { get; set; } = 1.5;
public double MediumCutoutWidth { get; set; } = 8.0;
public double MediumCutoutHeight { get; set; } = 8.0;
public double DistanceMediumSmall { get; set; }
public bool AlternateRowsColumns { get; set; } = true;
public bool AlternateCutoutsWithinRowColumn { get; set; } = true;
public double MinDistanceBetweenRowsColumns { get; set; } = 0.25;
}
public class AssignmentParameters
{
public SequenceMethod Method { get; set; } = SequenceMethod.Advanced;
public string Preference { get; set; } = "ILAT";
public double MinGeometryLength { get; set; } = 0.01;
}
```
## ContourCuttingStrategy
The orchestrator. Uses `ShapeProfile` to decompose a part into perimeter + cutouts, then sequences and applies cutting parameters using nearest-neighbor chaining from an exit point.
### Exit Point from Plate Quadrant
The exit point is the **opposite corner** of the plate from the quadrant origin. This is where the head ends up after traversing the plate, and is the starting point for backwards nearest-neighbor sequencing.
| Quadrant | Origin | Exit Point |
|----------|--------|------------|
| 1 | TopRight | BottomLeft (0, 0) |
| 2 | TopLeft | BottomRight (width, 0) |
| 3 | BottomLeft | TopRight (width, length) |
| 4 | BottomRight | TopLeft (0, length) |
The exit point is derived from `Plate.Quadrant` and `Plate.Size` — not passed in manually.
### Approach
Instead of requiring `Program.GetStartPoint()` / `GetNormalAtStart()` (which don't exist), the strategy:
1. Computes the **exit point** from the plate's quadrant and size
2. Converts the program to geometry via `Program.ToGeometry()`
3. Builds a `ShapeProfile` from the geometry — gives `Perimeter` (Shape) and `Cutouts` (List&lt;Shape&gt;)
4. Uses `Shape.ClosestPointTo(point, out Entity entity)` to find lead-in points and the entity for normal computation
5. Chains cutouts by nearest-neighbor distance from the perimeter closest point
6. Reverses the chain → cut order is cutouts first (nearest-last), perimeter last
### Contour Re-Indexing
After `ClosestPointTo` finds the lead-in point on a shape, the shape's entity list must be reordered so that cutting starts at that point. This means:
1. Find which entity in `Shape.Entities` contains the closest point
2. Split that entity at the closest point into two segments
3. Reorder: second half of split entity → remaining entities in order → first half of split entity
4. The contour now starts and ends at the lead-in point (for closed contours)
This produces the `List<ICode>` for the contour body that goes between the lead-in and lead-out codes.
### ContourType Detection
- `ShapeProfile.Perimeter``ContourType.External`
- Each cutout in `ShapeProfile.Cutouts`:
- If single entity and entity is `Circle``ContourType.ArcCircle`
- Otherwise → `ContourType.Internal`
### Normal Angle Computation
Derived from the `out Entity` returned by `ClosestPointTo`:
- **Line**: normal is perpendicular to line direction. Use the line's tangent angle, then add π/2 for the normal pointing away from the part interior.
- **Arc/Circle**: normal is radial direction from arc center to the closest point: `closestPoint.AngleFrom(arc.Center)`.
Normal direction convention: always points **away from the part material** (outward from perimeter, inward toward scrap for cutouts). The lead-in approaches from this direction.
### Arc Rotation Direction
Lead-in/lead-out arcs must match the **contour winding direction**, not be hardcoded CW. Determine winding from the shape's entity traversal order. Pass the appropriate `RotationType` to `ArcMove`.
### Method Signature
```csharp
public class ContourCuttingStrategy
{
public CuttingParameters Parameters { get; set; }
/// <summary>
/// Apply cutting strategy to a part's program.
/// </summary>
/// <param name="partProgram">Original part program (unmodified).</param>
/// <param name="plate">Plate for quadrant/size to compute exit point.</param>
/// <returns>New Program with lead-ins, lead-outs, and tabs applied. Cutouts first, perimeter last.</returns>
public Program Apply(Program partProgram, Plate plate)
{
// 1. Compute exit point from plate quadrant + size
// 2. Convert to geometry, build ShapeProfile
// 3. Find closest point on perimeter from exitPoint
// 4. Chain cutouts by nearest-neighbor from perimeter point
// 5. Reverse chain → cut order
// 6. For each contour:
// a. Re-index shape entities to start at closest point
// b. Detect ContourType
// c. Compute normal angle from entity
// d. Select lead-in/out from CuttingParameters by ContourType
// e. Generate lead-in codes + contour body + lead-out codes
// 7. Handle MicrotabLeadOut by trimming last segment
// 8. Assemble and return new Program
}
}
```
### ContourType Enum
```csharp
public enum ContourType
{
External,
Internal,
ArcCircle
}
```
## Integration Point
`ContourCuttingStrategy.Apply()` runs at nest-time (when parts are placed or cutting parameters are assigned), not at post-processing time. The output `Program` — with lead-ins, lead-outs, start points, and contour ordering — is stored on the `Part` and saved through the normal `NestWriter` path. The post-processor receives this already-complete program and only translates it to machine-specific G-code.
## Out of Scope (Deferred)
- **Serialization** of CuttingParameters (JSON/XML discriminators)
- **UI integration** (parameter editor forms in WinForms app)
- **Part.CutProgram property** (storing the strategy-applied program on `Part`, separate from `Drawing.Program`)
- **Tab insertion logic** (`InsertTabs` / `TrimLastSegment` — stubbed with `NotImplementedException`)
@@ -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)`