# 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 net8.0-windows OpenNest.Api OpenNest.Api ``` - [ ] **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 `` with other ProjectReferences: ```xml ``` - [ ] **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 `` with other ProjectReferences: ```xml ``` - [ ] **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 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: `` → `` - Line 395: `` → `` - 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 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(stream, JsonOptions); } // Read response.json var responseEntry = zip.GetEntry("response.json"); JsonElement metricsJson; await using (var stream = responseEntry!.Open()) { metricsJson = await JsonSerializer.DeserializeAsync(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( () => NestRunner.RunAsync(request)); } [Fact] public async Task RunAsync_EmptyParts_Throws() { var request = new NestRequest { Parts = [] }; await Assert.ThrowsAsync( () => 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 RunAsync( NestRequest request, IProgress 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(); 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(); 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(); 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)` for the DXF geometry list - `engine.Nest(List, IProgress, CancellationToken)` returns `List` - 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;`.