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>
32 KiB
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
<!-- 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
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:
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
- Step 4: Build to verify
dotnet build OpenNest.sln
Expected: Build succeeds with no errors.
- Step 5: Commit
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
// 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:
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:
<ProjectReference Include="..\OpenNest.Api\OpenNest.Api.csproj" />
- Step 5: Update WinForms forms
Add using OpenNest.Api; to each of these files:
OpenNest/Forms/CutParametersForm.csOpenNest/Forms/TimingForm.csOpenNest/Forms/EditNestForm.cs
No other changes — same property names, same construction pattern.
- Step 6: Build and run tests
dotnet test OpenNest.Tests --filter "FullyQualifiedName~CutParametersTests"
Expected: 2 tests pass. Full build also succeeds with no errors.
- Step 7: Commit
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
// 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
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests" --no-build
Expected: FAIL — types do not exist.
- Step 3: Write the implementations
// OpenNest.Api/NestStrategy.cs
namespace OpenNest.Api;
public enum NestStrategy { Auto }
// 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;
}
// 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
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRequestTests"
Expected: 3 tests pass.
- Step 5: Commit
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
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
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→.nestin default path - Line 23: change
.opnest→.nestin description
In OpenNest.Mcp/Tools/InputTools.cs:
-
Line 24: change
.opnest→.nestin description -
Line 25: change
.opnest→.nestin description -
Step 4: Update Training/Program.cs
-
Line 302: change
.opnest→.nest -
Step 5: Verify no remaining .opnest references in code
grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
Expected: No results.
- Step 6: Build to verify
dotnet build OpenNest.sln
Expected: Build succeeds.
- Step 7: Commit
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):
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:
public bool Write(string file)
{
using var fileStream = new FileStream(file, FileMode.Create);
return Write(fileStream);
}
- Step 3: Build and run all tests
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
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
// 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
// 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 = trueinJsonOptions— required becauseOpenNest.Geometry.Sizeis a struct with public fields (Width,Length), not properties. Without this, Size serializes as{}. -
NestReader(Stream)constructor exists atOpenNest.IO/NestReader.cs:26. ItsRead()method disposes the stream, which is fine since we use a separateMemoryStream. -
NestWriter.Write(Stream)was added in Task 6. -
Step 3: Build and run tests
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestResponsePersistenceTests"
Expected: 1 test passes.
- Step 4: Commit
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
// 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 toshape.Entitieslist -
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
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests" --no-build
Expected: FAIL — NestRunner does not exist.
- Step 3: Write NestRunner implementation
// 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 inNestReader.cs:81), then setdrawing.Program = pgm -
Part.BaseDrawing(public readonly field, confirmed inPart.cs:24andNestWriter.cs:154) -
ConvertGeometry.ToProgram(IList<Entity>)for the DXF geometry list -
engine.Nest(List<NestItem>, IProgress<NestProgress>, CancellationToken)returnsList<Part> -
Method returns
Task.FromResultsince the actual work is synchronous (engine is CPU-bound, not async). TheTaskreturn type keeps the API async-friendly for future use. -
Step 4: Build and run tests
dotnet test OpenNest.Tests --filter "FullyQualifiedName~NestRunnerTests"
Expected: 3 tests pass.
- Step 5: Commit
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
cd "C:/Users/aisaacs/Desktop/Projects/OpenNest"
dotnet build OpenNest.sln
Expected: Build succeeds with no errors.
- Step 2: Run all tests
dotnet test OpenNest.Tests
Expected: All tests pass — both existing and new Api tests.
- Step 3: Verify no remaining .opnest in code
grep -r "\.opnest" --include="*.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
Expected: No results.
- Step 4: Verify no remaining old NestResult in code
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
grep -rn "namespace OpenNest;" --include="CutParameters.cs" "C:/Users/aisaacs/Desktop/Projects/OpenNest"
Expected: No results — the file should have namespace OpenNest.Api;.