merge: resolve .gitignore conflict, keep both entries
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1218
docs/superpowers/plans/2026-03-12-cutting-strategy.md
Normal file
1218
docs/superpowers/plans/2026-03-12-cutting-strategy.md
Normal file
File diff suppressed because it is too large
Load Diff
767
docs/superpowers/plans/2026-03-12-nest-file-format-v2.md
Normal file
767
docs/superpowers/plans/2026-03-12-nest-file-format-v2.md
Normal file
@@ -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"
|
||||
```
|
||||
134
docs/superpowers/specs/2026-03-12-contour-reindexing-design.md
Normal file
134
docs/superpowers/specs/2026-03-12-contour-reindexing-design.md
Normal file
@@ -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)
|
||||
420
docs/superpowers/specs/2026-03-12-cutting-strategy-design.md
Normal file
420
docs/superpowers/specs/2026-03-12-cutting-strategy-design.md
Normal file
@@ -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<Shape>)
|
||||
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`)
|
||||
134
docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md
Normal file
134
docs/superpowers/specs/2026-03-12-nest-file-format-v2-design.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Nest File Format v2 Design
|
||||
|
||||
## Problem
|
||||
|
||||
The current nest file format stores metadata across three separate XML files (`info`, `drawing-info`, `plate-info`) plus per-plate G-code files for part placements inside a ZIP archive. This results in ~400 lines of hand-written XML read/write code, fragile dictionary-linking to reconnect drawings/plates by ID after parsing, and the overhead of running the full G-code parser just to extract part positions.
|
||||
|
||||
## Design
|
||||
|
||||
### File Structure
|
||||
|
||||
The nest file remains a ZIP archive. Contents:
|
||||
|
||||
```
|
||||
nest.json
|
||||
programs/
|
||||
program-1
|
||||
program-2
|
||||
...
|
||||
```
|
||||
|
||||
- **`nest.json`** — single JSON file containing all metadata and part placements.
|
||||
- **`programs/program-N`** — G-code text for each drawing's CNC program (1-indexed, no zero-padding). Previously stored at the archive root as `program-NNN` (zero-padded). Parsed by `ProgramReader`, written by existing G-code serialization logic. Format unchanged.
|
||||
|
||||
Plate G-code files (`plate-NNN`) are removed. Part placements are stored inline in `nest.json`.
|
||||
|
||||
### JSON Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"name": "string",
|
||||
"units": "Inches | Millimeters",
|
||||
"customer": "string",
|
||||
"dateCreated": "2026-03-12T10:30:00",
|
||||
"dateLastModified": "2026-03-12T14:00:00",
|
||||
"notes": "string (plain JSON, no URI-escaping)",
|
||||
"plateDefaults": {
|
||||
"size": { "width": 0.0, "height": 0.0 },
|
||||
"thickness": 0.0,
|
||||
"quadrant": 1,
|
||||
"partSpacing": 0.0,
|
||||
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 }
|
||||
},
|
||||
"drawings": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "string",
|
||||
"customer": "string",
|
||||
"color": { "a": 255, "r": 0, "g": 0, "b": 0 },
|
||||
"quantity": { "required": 0 },
|
||||
"priority": 0,
|
||||
"constraints": {
|
||||
"stepAngle": 0.0,
|
||||
"startAngle": 0.0,
|
||||
"endAngle": 0.0,
|
||||
"allow180Equivalent": false
|
||||
},
|
||||
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||
"source": {
|
||||
"path": "string",
|
||||
"offset": { "x": 0.0, "y": 0.0 }
|
||||
}
|
||||
}
|
||||
],
|
||||
"plates": [
|
||||
{
|
||||
"id": 1,
|
||||
"size": { "width": 0.0, "height": 0.0 },
|
||||
"thickness": 0.0,
|
||||
"quadrant": 1,
|
||||
"quantity": 1,
|
||||
"partSpacing": 0.0,
|
||||
"material": { "name": "string", "grade": "string", "density": 0.0 },
|
||||
"edgeSpacing": { "left": 0.0, "top": 0.0, "right": 0.0, "bottom": 0.0 },
|
||||
"parts": [
|
||||
{ "drawingId": 1, "x": 0.0, "y": 0.0, "rotation": 0.0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Key details:
|
||||
- **Version**: `"version": 2` at the top level for future format migration.
|
||||
- Drawing `id` values are 1-indexed, matching `programs/program-N` filenames.
|
||||
- Part `rotation` is stored in **radians** (matches internal domain model, no conversion needed).
|
||||
- Part `drawingId` references the drawing's `id` in the `drawings` array.
|
||||
- **Dates**: local time, serialized via `DateTime.ToString("o")` (ISO 8601 round-trip format with timezone offset).
|
||||
- **Notes**: stored as plain JSON strings. The v1 URI-escaping (`Uri.EscapeDataString`) is not needed since JSON handles special characters natively.
|
||||
- `quantity.required` is the only quantity persisted; `nested` is computed at load time from part placements.
|
||||
- **Units**: enum values match the domain model: `Inches` or `Millimeters`.
|
||||
- **Size**: uses `width`/`height` matching the `OpenNest.Geometry.Size` struct.
|
||||
- **Drawing.Priority** and **Drawing.Constraints** (stepAngle, startAngle, endAngle, allow180Equivalent) are now persisted (v1 omitted these).
|
||||
- **Empty collections**: `drawings` and `plates` arrays are always present (may be empty `[]`). The `programs/` folder is empty when there are no drawings.
|
||||
|
||||
### Serialization Approach
|
||||
|
||||
Use `System.Text.Json` with small DTO (Data Transfer Object) classes for serialization. The DTOs map between the domain model and the JSON structure, keeping serialization concerns out of the domain classes.
|
||||
|
||||
### What Changes
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `NestWriter.cs` | Replace all XML writing and plate G-code writing with JSON serialization. Programs written to `programs/` folder. |
|
||||
| `NestReader.cs` | Replace all XML parsing, plate G-code parsing, and dictionary-linking with JSON deserialization. Programs read from `programs/` folder. |
|
||||
|
||||
### What Stays the Same
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `ProgramReader.cs` | G-code parsing for CNC programs is unchanged. |
|
||||
| `NestWriter` G-code writing (`WriteDrawing`, `GetCodeString`) | G-code serialization for programs is unchanged. |
|
||||
| `DxfImporter.cs`, `DxfExporter.cs`, `Extensions.cs` | Unrelated to nest file format. |
|
||||
| Domain model classes | No changes needed. |
|
||||
|
||||
### Public API
|
||||
|
||||
The public API is unchanged:
|
||||
- `NestReader(string file)` and `NestReader(Stream stream)` constructors preserved.
|
||||
- `NestReader.Read()` returns `Nest`.
|
||||
- `NestWriter(Nest nest)` constructor preserved.
|
||||
- `NestWriter.Write(string file)` returns `bool`.
|
||||
|
||||
### Callers (no changes needed)
|
||||
|
||||
- `MainForm.cs:329` — `new NestReader(path)`
|
||||
- `MainForm.cs:363` — `new NestReader(dlg.FileName)`
|
||||
- `EditNestForm.cs:212` — `new NestWriter(Nest)`
|
||||
- `EditNestForm.cs:223` — `new NestWriter(nst)`
|
||||
- `Document.cs:27` — `new NestWriter(Nest)`
|
||||
- `OpenNest.Console/Program.cs:94` — `new NestReader(nestFile)`
|
||||
- `OpenNest.Console/Program.cs:190` — `new NestWriter(nest)`
|
||||
- `OpenNest.Mcp/InputTools.cs:30` — `new NestReader(path)`
|
||||
Reference in New Issue
Block a user