Files
OpenNest/docs/superpowers/plans/2026-03-19-nest-api.md
T
aj 4be0b0db09 docs: add Nest API implementation plan
9-task plan covering: project skeleton, CutParameters migration, request/response
types, NestResult rename, .opnest→.nest rename, NestWriter Stream overload,
NestResponse persistence, NestRunner with multi-plate loop, and verification.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 07:59:23 -04:00

1040 lines
32 KiB
Markdown

# Nest API Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Create `OpenNest.Api` project with `NestRequest`/`NestResponse` facade, rename internal types, and standardize file extensions.
**Architecture:** New `OpenNest.Api` class library wraps Engine + IO + Timing behind a stateless `NestRunner.RunAsync()`. Renames `NestResult``OptimizationResult`, replaces old `CutParameters` in Core with new `OpenNest.Api` namespace version, and renames `.opnest``.nest` across the solution.
**Tech Stack:** .NET 8, C#, xUnit, System.IO.Compression (ZIP), System.Text.Json
**Spec:** `docs/superpowers/specs/2026-03-19-nest-api-design.md`
**Out of scope:** Consumer integration (MCP `nest_and_quote` tool, Console app simplification) — covered in a follow-up plan.
---
## File Map
### New Files (OpenNest.Api)
| File | Responsibility |
|------|---------------|
| `OpenNest.Api/OpenNest.Api.csproj` | Project file referencing Core, Engine, IO |
| `OpenNest.Api/NestStrategy.cs` | Strategy enum (Auto only for now) |
| `OpenNest.Api/NestRequestPart.cs` | Single part input (DXF path + quantity + rotation + priority) |
| `OpenNest.Api/NestRequest.cs` | Immutable nesting request |
| `OpenNest.Api/NestResponse.cs` | Immutable nesting response with SaveAsync/LoadAsync |
| `OpenNest.Api/NestRunner.cs` | Stateless orchestrator |
### New Files (Tests)
| File | Responsibility |
|------|---------------|
| `OpenNest.Tests/Api/CutParametersTests.cs` | CutParameters defaults and construction |
| `OpenNest.Tests/Api/NestRequestTests.cs` | Request validation and immutability |
| `OpenNest.Tests/Api/NestRunnerTests.cs` | Runner integration tests (DXF import → nest → response) |
| `OpenNest.Tests/Api/NestResponsePersistenceTests.cs` | SaveAsync/LoadAsync round-trip |
### Modified Files (Renames)
| File | Change |
|------|--------|
| `OpenNest.Core/CutParameters.cs` | Replace contents: change namespace to `OpenNest.Api`, add new properties |
| `OpenNest.Core/Timing.cs:70` | Add `using OpenNest.Api;` |
| `OpenNest.Engine/INestOptimizer.cs:10,34` | Rename `NestResult``OptimizationResult` |
| `OpenNest.Engine/SimulatedAnnealing.cs:20,31,108` | Update all `NestResult` references |
| `OpenNest/OpenNest.csproj` | Add ProjectReference to OpenNest.Api |
| `OpenNest/Forms/CutParametersForm.cs` | Add `using OpenNest.Api;` |
| `OpenNest/Forms/TimingForm.cs` | Add `using OpenNest.Api;` |
| `OpenNest/Forms/EditNestForm.cs` | Add `using OpenNest.Api;` |
| `OpenNest.IO/NestFormat.cs:8-9` | Change `.opnest``.nest` |
| `OpenNest.IO/NestWriter.cs` | Add `Write(Stream)` overload |
| `OpenNest.Console/Program.cs:194,390,393,395,403` | Update `.opnest` string literals to `.nest` |
| `OpenNest.Mcp/Tools/TestTools.cs:20,23` | Update `.opnest` references |
| `OpenNest.Mcp/Tools/InputTools.cs:24-25` | Update `.opnest` references |
| `OpenNest.Training/Program.cs:302` | Update `.opnest` reference |
| `OpenNest.Tests/OpenNest.Tests.csproj` | Add ProjectReference to OpenNest.Api |
| `OpenNest.sln` | Add OpenNest.Api project |
---
### Task 1: Create OpenNest.Api Project Skeleton
**Files:**
- Create: `OpenNest.Api/OpenNest.Api.csproj`
- Modify: `OpenNest.sln`
- Modify: `OpenNest.Tests/OpenNest.Tests.csproj`
- [ ] **Step 1: Create the project file**
```xml
<!-- OpenNest.Api/OpenNest.Api.csproj -->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<RootNamespace>OpenNest.Api</RootNamespace>
<AssemblyName>OpenNest.Api</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\OpenNest.Core\OpenNest.Core.csproj" />
<ProjectReference Include="..\OpenNest.Engine\OpenNest.Engine.csproj" />
<ProjectReference Include="..\OpenNest.IO\OpenNest.IO.csproj" />
</ItemGroup>
</Project>
```
- [ ] **Step 2: Add project to solution**
```bash
cd "C:/Users/aisaacs/Desktop/Projects/OpenNest"
dotnet sln OpenNest.sln add OpenNest.Api/OpenNest.Api.csproj
```
- [ ] **Step 3: Add test project reference**
Add to `OpenNest.Tests/OpenNest.Tests.csproj` inside the `<ItemGroup>` with other ProjectReferences:
```xml
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
```
- [ ] **Step 4: Build to verify**
```bash
dotnet build OpenNest.sln
```
Expected: Build succeeds with no errors.
- [ ] **Step 5: Commit**
```bash
git add OpenNest.Api/OpenNest.Api.csproj OpenNest.sln OpenNest.Tests/OpenNest.Tests.csproj
git commit -m "chore: add OpenNest.Api project skeleton"
```
---
### Task 2: Replace CutParameters in Core with OpenNest.Api Namespace
The existing `OpenNest.CutParameters` in Core gets replaced in-place: same physical file, but new namespace (`OpenNest.Api`) and additional properties. This avoids circular references (Core doesn't reference Api) while giving all consumers the `OpenNest.Api.CutParameters` type.
**Files:**
- Modify: `OpenNest.Core/CutParameters.cs` (replace contents)
- Modify: `OpenNest.Core/Timing.cs` (add using)
- Modify: `OpenNest/OpenNest.csproj` (add Api reference)
- Modify: `OpenNest/Forms/CutParametersForm.cs` (add using)
- Modify: `OpenNest/Forms/TimingForm.cs` (add using)
- Modify: `OpenNest/Forms/EditNestForm.cs` (add using)
- Create: `OpenNest.Tests/Api/CutParametersTests.cs`
- [ ] **Step 1: Write the failing test**
```csharp
// OpenNest.Tests/Api/CutParametersTests.cs
using OpenNest.Api;
namespace OpenNest.Tests.Api;
public class CutParametersTests
{
[Fact]
public void Default_HasExpectedValues()
{
var cp = CutParameters.Default;
Assert.Equal(100, cp.Feedrate);
Assert.Equal(300, cp.RapidTravelRate);
Assert.Equal(TimeSpan.FromSeconds(0.5), cp.PierceTime);
Assert.Equal(Units.Inches, cp.Units);
}
[Fact]
public void Properties_AreSettable()
{
var cp = new CutParameters
{
Feedrate = 200,
RapidTravelRate = 500,
PierceTime = TimeSpan.FromSeconds(1.0),
LeadInLength = 0.25,
PostProcessor = "CL-707",
Units = Units.Millimeters
};
Assert.Equal(200, cp.Feedrate);
Assert.Equal(0.25, cp.LeadInLength);
Assert.Equal("CL-707", cp.PostProcessor);
}
}
```
- [ ] **Step 2: Replace CutParameters.cs contents**
Replace the entire contents of `OpenNest.Core/CutParameters.cs`:
```csharp
using System;
namespace OpenNest.Api;
public class CutParameters
{
public double Feedrate { get; set; }
public double RapidTravelRate { get; set; }
public TimeSpan PierceTime { get; set; }
public double LeadInLength { get; set; }
public string PostProcessor { get; set; }
public Units Units { get; set; }
public static CutParameters Default => new()
{
Feedrate = 100,
RapidTravelRate = 300,
PierceTime = TimeSpan.FromSeconds(0.5),
Units = OpenNest.Units.Inches
};
}
```
**Note:** Properties use `{ get; set; }` (not `init`) to match existing mutation patterns in the codebase. `Units` in the `Default` property is fully qualified as `OpenNest.Units.Inches` to avoid ambiguity with the `Units` property.
- [ ] **Step 3: Update Timing.cs**
Add `using OpenNest.Api;` to the top of `OpenNest.Core/Timing.cs`. The `CalculateTime(TimingInfo info, CutParameters cutParams)` method at line 70 now resolves to the new type — same property names, no body changes needed.
- [ ] **Step 4: Add Api reference to WinForms project**
Add to `OpenNest/OpenNest.csproj` inside the `<ItemGroup>` with other ProjectReferences:
```xml
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
```
- [ ] **Step 5: Update WinForms forms**
Add `using OpenNest.Api;` to each of these files:
- `OpenNest/Forms/CutParametersForm.cs`
- `OpenNest/Forms/TimingForm.cs`
- `OpenNest/Forms/EditNestForm.cs`
No other changes — same property names, same construction pattern.
- [ ] **Step 6: Build and run tests**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~CutParametersTests"
```
Expected: 2 tests pass. Full build also succeeds with no errors.
- [ ] **Step 7: Commit**
```bash
git add OpenNest.Core/CutParameters.cs OpenNest.Core/Timing.cs OpenNest/OpenNest.csproj OpenNest/Forms/CutParametersForm.cs OpenNest/Forms/TimingForm.cs OpenNest/Forms/EditNestForm.cs OpenNest.Tests/Api/CutParametersTests.cs
git commit -m "refactor: move CutParameters to OpenNest.Api namespace with new properties"
```
---
### Task 3: NestStrategy, NestRequestPart, and NestRequest
**Files:**
- Create: `OpenNest.Api/NestStrategy.cs`
- Create: `OpenNest.Api/NestRequestPart.cs`
- Create: `OpenNest.Api/NestRequest.cs`
- Create: `OpenNest.Tests/Api/NestRequestTests.cs`
- [ ] **Step 1: Write the failing tests**
```csharp
// OpenNest.Tests/Api/NestRequestTests.cs
using OpenNest.Api;
using OpenNest.Geometry;
namespace OpenNest.Tests.Api;
public class NestRequestTests
{
[Fact]
public void Default_Request_HasSensibleDefaults()
{
var request = new NestRequest();
Assert.Empty(request.Parts);
Assert.Equal(60, request.SheetSize.Width);
Assert.Equal(120, request.SheetSize.Length);
Assert.Equal("Steel, A1011 HR", request.Material);
Assert.Equal(0.06, request.Thickness);
Assert.Equal(0.1, request.Spacing);
Assert.Equal(NestStrategy.Auto, request.Strategy);
Assert.NotNull(request.Cutting);
}
[Fact]
public void Parts_Accessible_AfterConstruction()
{
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }]
};
Assert.Single(request.Parts);
Assert.Equal("test.dxf", request.Parts[0].DxfPath);
Assert.Equal(5, request.Parts[0].Quantity);
}
[Fact]
public void NestRequestPart_Defaults()
{
var part = new NestRequestPart { DxfPath = "part.dxf" };
Assert.Equal(1, part.Quantity);
Assert.True(part.AllowRotation);
Assert.Equal(0, part.Priority);
}
}
```
- [ ] **Step 2: Run tests to verify they fail**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests" --no-build
```
Expected: FAIL — types do not exist.
- [ ] **Step 3: Write the implementations**
```csharp
// OpenNest.Api/NestStrategy.cs
namespace OpenNest.Api;
public enum NestStrategy { Auto }
```
```csharp
// OpenNest.Api/NestRequestPart.cs
namespace OpenNest.Api;
public class NestRequestPart
{
public string DxfPath { get; init; }
public int Quantity { get; init; } = 1;
public bool AllowRotation { get; init; } = true;
public int Priority { get; init; } = 0;
}
```
```csharp
// OpenNest.Api/NestRequest.cs
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest.Api;
public class NestRequest
{
public IReadOnlyList<NestRequestPart> Parts { get; init; } = [];
public Size SheetSize { get; init; } = new(60, 120);
public string Material { get; init; } = "Steel, A1011 HR";
public double Thickness { get; init; } = 0.06;
public double Spacing { get; init; } = 0.1;
public NestStrategy Strategy { get; init; } = NestStrategy.Auto;
public CutParameters Cutting { get; init; } = CutParameters.Default;
}
```
- [ ] **Step 4: Build and run tests**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests"
```
Expected: 3 tests pass.
- [ ] **Step 5: Commit**
```bash
git add OpenNest.Api/NestStrategy.cs OpenNest.Api/NestRequestPart.cs OpenNest.Api/NestRequest.cs OpenNest.Tests/Api/NestRequestTests.cs
git commit -m "feat(api): add NestStrategy, NestRequestPart, NestRequest"
```
---
### Task 4: Rename NestResult → OptimizationResult
**Files:**
- Modify: `OpenNest.Engine/INestOptimizer.cs` (class name + return type)
- Modify: `OpenNest.Engine/SimulatedAnnealing.cs` (all references)
- [ ] **Step 1: Rename in INestOptimizer.cs**
In `OpenNest.Engine/INestOptimizer.cs`:
- Line 10: `public class NestResult``public class OptimizationResult`
- Line 34: return type `NestResult``OptimizationResult`
- [ ] **Step 2: Rename in SimulatedAnnealing.cs**
In `OpenNest.Engine/SimulatedAnnealing.cs`:
- Line 20: return type `NestResult``OptimizationResult`
- Line 31: `new NestResult``new OptimizationResult`
- Line 108: `new NestResult``new OptimizationResult`
- [ ] **Step 3: Build and run all tests**
```bash
dotnet build OpenNest.sln && dotnet test OpenNest.Tests
```
Expected: Build succeeds, all existing tests pass. If there are additional `NestResult` references the build will reveal them — fix any that appear.
- [ ] **Step 4: Commit**
```bash
git add OpenNest.Engine/INestOptimizer.cs OpenNest.Engine/SimulatedAnnealing.cs
git commit -m "refactor(engine): rename NestResult to OptimizationResult"
```
---
### Task 5: Rename .opnest → .nest File Extension
**Files:**
- Modify: `OpenNest.IO/NestFormat.cs:8-9`
- Modify: `OpenNest.Console/Program.cs:194,390,393,395,403`
- Modify: `OpenNest.Mcp/Tools/TestTools.cs:20,23`
- Modify: `OpenNest.Mcp/Tools/InputTools.cs:24-25`
- Modify: `OpenNest.Training/Program.cs:302`
- [ ] **Step 1: Update NestFormat.cs**
In `OpenNest.IO/NestFormat.cs`:
- Line 8: `".opnest"``".nest"`
- Line 9: `"Nest Files (*.opnest)|*.opnest"``"Nest Files (*.nest)|*.nest"`
- [ ] **Step 2: Update Console/Program.cs**
Replace all `.opnest` string literals with `.nest`:
- Line 194: `(.opnest)``(.nest)`
- Line 390: `.opnest``.nest`
- Line 393: `<nest.opnest>``<nest.nest>`
- Line 395: `<nest.opnest>``<nest.nest>`
- Line 403: `-result.opnest``-result.nest`
- [ ] **Step 3: Update MCP tools**
In `OpenNest.Mcp/Tools/TestTools.cs`:
- Line 20: change `.opnest``.nest` in default path
- Line 23: change `.opnest``.nest` in description
In `OpenNest.Mcp/Tools/InputTools.cs`:
- Line 24: change `.opnest``.nest` in description
- Line 25: change `.opnest``.nest` in description
- [ ] **Step 4: Update Training/Program.cs**
- Line 302: change `.opnest``.nest`
- [ ] **Step 5: Verify no remaining .opnest references in code**
```bash
grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
```
Expected: No results.
- [ ] **Step 6: Build to verify**
```bash
dotnet build OpenNest.sln
```
Expected: Build succeeds.
- [ ] **Step 7: Commit**
```bash
git add OpenNest.IO/NestFormat.cs OpenNest.Console/Program.cs OpenNest.Mcp/Tools/TestTools.cs OpenNest.Mcp/Tools/InputTools.cs OpenNest.Training/Program.cs
git commit -m "refactor: rename .opnest file extension to .nest"
```
---
### Task 6: Add NestWriter.Write(Stream) Overload
The existing `NestWriter.Write(string)` creates a `FileStream` and `ZipArchive` internally. We need a `Write(Stream)` overload so `NestResponse.SaveAsync` can write the `.nest` payload to a `MemoryStream` for embedding inside the `.nestquote` ZIP.
**Files:**
- Modify: `OpenNest.IO/NestWriter.cs`
- [ ] **Step 1: Add Write(Stream) overload**
In `OpenNest.IO/NestWriter.cs`, add a new method after the existing `Write(string)` method (after line 43):
```csharp
public bool Write(Stream stream)
{
nest.DateLastModified = DateTime.Now;
SetDrawingIds();
using var zipArchive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true);
WriteNestJson(zipArchive);
WritePrograms(zipArchive);
WriteBestFits(zipArchive);
return true;
}
```
**Note:** `leaveOpen: true` is critical — without it, `ZipArchive.Dispose()` would close the underlying `MemoryStream`, preventing the caller from reading it afterwards.
- [ ] **Step 2: Refactor Write(string) to delegate**
Replace the existing `Write(string)` method body to delegate to the new overload:
```csharp
public bool Write(string file)
{
using var fileStream = new FileStream(file, FileMode.Create);
return Write(fileStream);
}
```
- [ ] **Step 3: Build and run all tests**
```bash
dotnet build OpenNest.sln && dotnet test OpenNest.Tests
```
Expected: Build succeeds, all existing tests pass (NestWriter behavior unchanged for file-based callers).
- [ ] **Step 4: Commit**
```bash
git add OpenNest.IO/NestWriter.cs
git commit -m "feat(io): add NestWriter.Write(Stream) overload"
```
---
### Task 7: NestResponse Type with SaveAsync/LoadAsync
**Files:**
- Create: `OpenNest.Api/NestResponse.cs`
- Create: `OpenNest.Tests/Api/NestResponsePersistenceTests.cs`
- [ ] **Step 1: Write the round-trip test**
```csharp
// OpenNest.Tests/Api/NestResponsePersistenceTests.cs
using System;
using System.IO;
using System.Threading.Tasks;
using OpenNest.Api;
using OpenNest.Geometry;
namespace OpenNest.Tests.Api;
public class NestResponsePersistenceTests
{
[Fact]
public async Task SaveAsync_LoadAsync_RoundTrips()
{
var nest = new Nest("test-nest");
var plate = new Plate(new Size(60, 120));
nest.Plates.Add(plate);
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = "test.dxf", Quantity = 5 }],
SheetSize = new Size(60, 120),
Material = "Steel",
Thickness = 0.125,
Spacing = 0.1
};
var original = new NestResponse
{
SheetCount = 1,
Utilization = 0.75,
CutTime = TimeSpan.FromMinutes(12.5),
Elapsed = TimeSpan.FromSeconds(3.2),
Nest = nest,
Request = request
};
var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.nestquote");
try
{
await original.SaveAsync(path);
var loaded = await NestResponse.LoadAsync(path);
Assert.Equal(original.SheetCount, loaded.SheetCount);
Assert.Equal(original.Utilization, loaded.Utilization, precision: 4);
Assert.Equal(original.CutTime, loaded.CutTime);
Assert.Equal(original.Elapsed, loaded.Elapsed);
Assert.Equal(original.Request.Material, loaded.Request.Material);
Assert.Equal(original.Request.Thickness, loaded.Request.Thickness);
Assert.Equal(original.Request.Parts.Count, loaded.Request.Parts.Count);
Assert.Equal(original.Request.Parts[0].DxfPath, loaded.Request.Parts[0].DxfPath);
Assert.Equal(original.Request.Parts[0].Quantity, loaded.Request.Parts[0].Quantity);
Assert.NotNull(loaded.Nest);
Assert.Equal(1, loaded.Nest.Plates.Count);
}
finally
{
File.Delete(path);
}
}
}
```
- [ ] **Step 2: Write NestResponse implementation**
```csharp
// OpenNest.Api/NestResponse.cs
using System;
using System.IO;
using System.IO.Compression;
using System.Text.Json;
using System.Threading.Tasks;
using OpenNest.IO;
namespace OpenNest.Api;
public class NestResponse
{
public int SheetCount { get; init; }
public double Utilization { get; init; }
public TimeSpan CutTime { get; init; }
public TimeSpan Elapsed { get; init; }
public Nest Nest { get; init; }
public NestRequest Request { get; init; }
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
IncludeFields = true // Required for OpenNest.Geometry.Size (public fields)
};
public async Task SaveAsync(string path)
{
using var fs = new FileStream(path, FileMode.Create);
using var zip = new ZipArchive(fs, ZipArchiveMode.Create);
// Write request.json
var requestEntry = zip.CreateEntry("request.json");
await using (var stream = requestEntry.Open())
{
await JsonSerializer.SerializeAsync(stream, Request, JsonOptions);
}
// Write response.json (metrics only)
var metrics = new
{
SheetCount,
Utilization,
CutTimeTicks = CutTime.Ticks,
ElapsedTicks = Elapsed.Ticks
};
var responseEntry = zip.CreateEntry("response.json");
await using (var stream = responseEntry.Open())
{
await JsonSerializer.SerializeAsync(stream, metrics, JsonOptions);
}
// Write embedded nest.nest via NestWriter → MemoryStream → ZIP entry
var nestEntry = zip.CreateEntry("nest.nest");
using var nestMs = new MemoryStream();
var writer = new NestWriter(Nest);
writer.Write(nestMs);
nestMs.Position = 0;
await using (var stream = nestEntry.Open())
{
await nestMs.CopyToAsync(stream);
}
}
public static async Task<NestResponse> LoadAsync(string path)
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var zip = new ZipArchive(fs, ZipArchiveMode.Read);
// Read request.json
var requestEntry = zip.GetEntry("request.json");
NestRequest request;
await using (var stream = requestEntry!.Open())
{
request = await JsonSerializer.DeserializeAsync<NestRequest>(stream, JsonOptions);
}
// Read response.json
var responseEntry = zip.GetEntry("response.json");
JsonElement metricsJson;
await using (var stream = responseEntry!.Open())
{
metricsJson = await JsonSerializer.DeserializeAsync<JsonElement>(stream, JsonOptions);
}
// Read embedded nest.nest via NestReader(Stream)
var nestEntry = zip.GetEntry("nest.nest");
Nest nest;
using (var nestMs = new MemoryStream())
{
await using (var stream = nestEntry!.Open())
{
await stream.CopyToAsync(nestMs);
}
nestMs.Position = 0;
var reader = new NestReader(nestMs);
nest = reader.Read();
}
return new NestResponse
{
SheetCount = metricsJson.GetProperty("sheetCount").GetInt32(),
Utilization = metricsJson.GetProperty("utilization").GetDouble(),
CutTime = TimeSpan.FromTicks(metricsJson.GetProperty("cutTimeTicks").GetInt64()),
Elapsed = TimeSpan.FromTicks(metricsJson.GetProperty("elapsedTicks").GetInt64()),
Nest = nest,
Request = request
};
}
}
```
**Key details:**
- `IncludeFields = true` in `JsonOptions` — required because `OpenNest.Geometry.Size` is a struct with public fields (`Width`, `Length`), not properties. Without this, Size serializes as `{}`.
- `NestReader(Stream)` constructor exists at `OpenNest.IO/NestReader.cs:26`. Its `Read()` method disposes the stream, which is fine since we use a separate `MemoryStream`.
- `NestWriter.Write(Stream)` was added in Task 6.
- [ ] **Step 3: Build and run tests**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestResponsePersistenceTests"
```
Expected: 1 test passes.
- [ ] **Step 4: Commit**
```bash
git add OpenNest.Api/NestResponse.cs OpenNest.Tests/Api/NestResponsePersistenceTests.cs
git commit -m "feat(api): add NestResponse with SaveAsync/LoadAsync"
```
---
### Task 8: NestRunner Implementation
**Files:**
- Create: `OpenNest.Api/NestRunner.cs`
- Create: `OpenNest.Tests/Api/NestRunnerTests.cs`
- [ ] **Step 1: Write the failing tests**
```csharp
// OpenNest.Tests/Api/NestRunnerTests.cs
using System;
using System.IO;
using System.Threading.Tasks;
using OpenNest.Api;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.Api;
public class NestRunnerTests
{
[Fact]
public async Task RunAsync_SinglePart_ProducesResponse()
{
var dxfPath = CreateTempSquareDxf(2, 2);
try
{
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = dxfPath, Quantity = 4 }],
SheetSize = new Size(10, 10),
Spacing = 0.1
};
var response = await NestRunner.RunAsync(request);
Assert.NotNull(response);
Assert.NotNull(response.Nest);
Assert.True(response.SheetCount >= 1);
Assert.True(response.Utilization > 0);
Assert.Equal(request, response.Request);
}
finally
{
File.Delete(dxfPath);
}
}
[Fact]
public async Task RunAsync_BadDxfPath_Throws()
{
var request = new NestRequest
{
Parts = [new NestRequestPart { DxfPath = "nonexistent.dxf", Quantity = 1 }]
};
await Assert.ThrowsAsync<FileNotFoundException>(
() => NestRunner.RunAsync(request));
}
[Fact]
public async Task RunAsync_EmptyParts_Throws()
{
var request = new NestRequest { Parts = [] };
await Assert.ThrowsAsync<ArgumentException>(
() => NestRunner.RunAsync(request));
}
private static string CreateTempSquareDxf(double width, double height)
{
var shape = new Shape();
shape.Entities.Add(new Line(new Vector(0, 0), new Vector(width, 0)));
shape.Entities.Add(new Line(new Vector(width, 0), new Vector(width, height)));
shape.Entities.Add(new Line(new Vector(width, height), new Vector(0, height)));
shape.Entities.Add(new Line(new Vector(0, height), new Vector(0, 0)));
var pgm = ConvertGeometry.ToProgram(shape);
var path = Path.Combine(Path.GetTempPath(), $"test-{Guid.NewGuid()}.dxf");
var exporter = new DxfExporter();
exporter.ExportProgram(pgm, path);
return path;
}
}
```
**API notes:**
- `Shape()` — parameterless constructor, then add to `shape.Entities` list
- `ConvertGeometry.ToProgram(Shape)` — takes a Shape directly
- `DxfExporter.ExportProgram(Program, string)` — exports a program to a DXF file path
- [ ] **Step 2: Run tests to verify they fail**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests" --no-build
```
Expected: FAIL — `NestRunner` does not exist.
- [ ] **Step 3: Write NestRunner implementation**
```csharp
// OpenNest.Api/NestRunner.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Api;
public static class NestRunner
{
public static Task<NestResponse> RunAsync(
NestRequest request,
IProgress<NestProgress> progress = null,
CancellationToken token = default)
{
if (request.Parts.Count == 0)
throw new ArgumentException("Request must contain at least one part.", nameof(request));
var sw = Stopwatch.StartNew();
// 1. Import DXFs → Drawings
var drawings = new List<Drawing>();
var importer = new DxfImporter();
foreach (var part in request.Parts)
{
if (!File.Exists(part.DxfPath))
throw new FileNotFoundException($"DXF file not found: {part.DxfPath}", part.DxfPath);
if (!importer.GetGeometry(part.DxfPath, out var geometry) || geometry.Count == 0)
throw new InvalidOperationException($"Failed to import DXF: {part.DxfPath}");
var pgm = ConvertGeometry.ToProgram(geometry);
var name = Path.GetFileNameWithoutExtension(part.DxfPath);
var drawing = new Drawing(name);
drawing.Program = pgm;
drawings.Add(drawing);
}
// 2. Build NestItems
var items = new List<NestItem>();
for (var i = 0; i < request.Parts.Count; i++)
{
var part = request.Parts[i];
items.Add(new NestItem
{
Drawing = drawings[i],
Quantity = part.Quantity,
Priority = part.Priority,
StepAngle = part.AllowRotation ? 0 : OpenNest.Math.Angle.TwoPI,
});
}
// 3. Multi-plate loop
var nest = new Nest();
var remaining = items.Select(item => item.Quantity).ToList();
while (remaining.Any(q => q > 0))
{
token.ThrowIfCancellationRequested();
var plate = new Plate(request.SheetSize)
{
Thickness = request.Thickness,
PartSpacing = request.Spacing
};
// Build items for this pass with remaining quantities
var passItems = new List<NestItem>();
for (var i = 0; i < items.Count; i++)
{
if (remaining[i] <= 0) continue;
passItems.Add(new NestItem
{
Drawing = items[i].Drawing,
Quantity = remaining[i],
Priority = items[i].Priority,
StepAngle = items[i].StepAngle,
});
}
// Run engine
var engine = NestEngineRegistry.Create(plate);
var parts = engine.Nest(passItems, progress, token);
if (parts.Count == 0)
break; // No progress — part doesn't fit on fresh sheet
// Add parts to plate and nest
foreach (var p in parts)
plate.Parts.Add(p);
nest.Plates.Add(plate);
// Deduct placed quantities
foreach (var p in parts)
{
var idx = drawings.IndexOf(p.BaseDrawing);
if (idx >= 0)
remaining[idx]--;
}
}
// 4. Compute timing (placeholder — Timing will be reworked later)
var timingInfo = Timing.GetTimingInfo(nest);
var cutTime = Timing.CalculateTime(timingInfo, request.Cutting);
sw.Stop();
// 5. Build response
var response = new NestResponse
{
SheetCount = nest.Plates.Count,
Utilization = nest.Plates.Count > 0
? nest.Plates.Average(p => p.Utilization())
: 0,
CutTime = cutTime,
Elapsed = sw.Elapsed,
Nest = nest,
Request = request
};
return Task.FromResult(response);
}
}
```
**Key API details:**
- `Drawing(string name)` constructor (confirmed in `NestReader.cs:81`), then set `drawing.Program = pgm`
- `Part.BaseDrawing` (public readonly field, confirmed in `Part.cs:24` and `NestWriter.cs:154`)
- `ConvertGeometry.ToProgram(IList<Entity>)` for the DXF geometry list
- `engine.Nest(List<NestItem>, IProgress<NestProgress>, CancellationToken)` returns `List<Part>`
- Method returns `Task.FromResult` since the actual work is synchronous (engine is CPU-bound, not async). The `Task` return type keeps the API async-friendly for future use.
- [ ] **Step 4: Build and run tests**
```bash
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests"
```
Expected: 3 tests pass.
- [ ] **Step 5: Commit**
```bash
git add OpenNest.Api/NestRunner.cs OpenNest.Tests/Api/NestRunnerTests.cs
git commit -m "feat(api): add NestRunner with multi-plate loop"
```
---
### Task 9: Full Integration Test and Final Verification
**Files:**
- None new — verification only
- [ ] **Step 1: Run full solution build**
```bash
cd "C:/Users/aisaacs/Desktop/Projects/OpenNest"
dotnet build OpenNest.sln
```
Expected: Build succeeds with no errors.
- [ ] **Step 2: Run all tests**
```bash
dotnet test OpenNest.Tests
```
Expected: All tests pass — both existing and new Api tests.
- [ ] **Step 3: Verify no remaining .opnest in code**
```bash
grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
```
Expected: No results.
- [ ] **Step 4: Verify no remaining old NestResult in code**
```bash
grep -r "\bNestResult\b" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest" | grep -v "StripNestResult" | grep -v "OptimizationResult" | grep -v "NestResponse"
```
Expected: No results (StripNestResult is a separate internal type, fine to keep).
- [ ] **Step 5: Verify CutParameters namespace**
```bash
grep -rn "namespace OpenNest;" --include="CutParameters.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
```
Expected: No results — the file should have `namespace OpenNest.Api;`.