11 tasks across 3 chunks: extract AccumulatingProgress, ShrinkFiller, AngleCandidateBuilder, PairFiller, RemnantFiller, then rewire both engines and NestEngineBase. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
53 KiB
Engine Refactor 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: Extract shared algorithms from DefaultNestEngine and StripNestEngine into focused, reusable helper classes.
Architecture: Five helper classes (AccumulatingProgress, ShrinkFiller, AngleCandidateBuilder, PairFiller, RemnantFiller) are extracted from the two engines. Each engine then composes these helpers instead of inlining the logic. No new interfaces — just focused classes with delegate-based decoupling.
Tech Stack: .NET 8, C#, xUnit
Spec: docs/superpowers/specs/2026-03-16-engine-refactor-design.md
File Structure
New files (OpenNest.Engine/):
AccumulatingProgress.cs— IProgress wrapper that prepends prior partsShrinkFiller.cs— Static shrink-to-fit loop + ShrinkAxis enum + ShrinkResult classAngleCandidateBuilder.cs— Angle sweep/ML/pruning with known-good statePairFiller.cs— Pair candidate selection and fill loopRemnantFiller.cs— Iterative remnant-fill loop over RemnantFinder
New test files (OpenNest.Tests/):
AccumulatingProgressTests.csShrinkFillerTests.csAngleCandidateBuilderTests.csPairFillerTests.csRemnantFillerTests.csEngineRefactorSmokeTests.cs— Integration tests verifying engines produce same results
Modified files:
OpenNest.Engine/DefaultNestEngine.cs— Remove extracted code, compose helpersOpenNest.Engine/StripNestEngine.cs— Remove extracted code, compose helpersOpenNest.Engine/NestEngineBase.cs— Replace remnant loop with RemnantFiller
Chunk 1: Leaf Extractions (AccumulatingProgress, ShrinkFiller)
Task 0: Add InternalsVisibleTo for test project
Files:
- Modify:
OpenNest.Engine/OpenNest.Engine.csproj
Several extracted classes are internal. The test project needs access.
- Step 1: Add InternalsVisibleTo to the Engine csproj
Add inside the first <PropertyGroup> or add a new <ItemGroup>:
<ItemGroup>
<InternalsVisibleTo Include="OpenNest.Tests" />
</ItemGroup>
- Step 2: Build to verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
- Step 3: Commit
git add OpenNest.Engine/OpenNest.Engine.csproj
git commit -m "build: add InternalsVisibleTo for OpenNest.Tests"
Task 1: Extract AccumulatingProgress
Files:
-
Create:
OpenNest.Engine/AccumulatingProgress.cs -
Create:
OpenNest.Tests/AccumulatingProgressTests.cs -
Step 1: Write the failing test
In OpenNest.Tests/AccumulatingProgressTests.cs:
namespace OpenNest.Tests;
public class AccumulatingProgressTests
{
private class CapturingProgress : IProgress<NestProgress>
{
public NestProgress Last { get; private set; }
public void Report(NestProgress value) => Last = value;
}
[Fact]
public void Report_PrependsPreviousParts()
{
var inner = new CapturingProgress();
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var accumulating = new AccumulatingProgress(inner, previous);
var newParts = new List<Part> { TestHelpers.MakePartAt(20, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 });
Assert.NotNull(inner.Last);
Assert.Equal(2, inner.Last.BestParts.Count);
Assert.Equal(2, inner.Last.BestPartCount);
}
[Fact]
public void Report_NoPreviousParts_PassesThrough()
{
var inner = new CapturingProgress();
var accumulating = new AccumulatingProgress(inner, new List<Part>());
var newParts = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
accumulating.Report(new NestProgress { BestParts = newParts, BestPartCount = 1 });
Assert.NotNull(inner.Last);
Assert.Single(inner.Last.BestParts);
}
[Fact]
public void Report_NullBestParts_PassesThrough()
{
var inner = new CapturingProgress();
var previous = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var accumulating = new AccumulatingProgress(inner, previous);
accumulating.Report(new NestProgress { BestParts = null });
Assert.NotNull(inner.Last);
Assert.Null(inner.Last.BestParts);
}
}
- Step 2: Run test to verify it fails
Run: dotnet test OpenNest.Tests --filter "AccumulatingProgressTests" -v n
Expected: Build error — AccumulatingProgress class does not exist yet.
- Step 3: Create AccumulatingProgress class
In OpenNest.Engine/AccumulatingProgress.cs:
using System;
using System.Collections.Generic;
namespace OpenNest
{
/// <summary>
/// Wraps an IProgress to prepend previously placed parts to each report,
/// so the UI shows the full picture during incremental fills.
/// </summary>
internal class AccumulatingProgress : IProgress<NestProgress>
{
private readonly IProgress<NestProgress> inner;
private readonly List<Part> previousParts;
public AccumulatingProgress(IProgress<NestProgress> inner, List<Part> previousParts)
{
this.inner = inner;
this.previousParts = previousParts;
}
public void Report(NestProgress value)
{
if (value.BestParts != null && previousParts.Count > 0)
{
var combined = new List<Part>(previousParts.Count + value.BestParts.Count);
combined.AddRange(previousParts);
combined.AddRange(value.BestParts);
value.BestParts = combined;
value.BestPartCount = combined.Count;
}
inner.Report(value);
}
}
}
Note: The class is internal. The test project needs InternalsVisibleTo. Check if OpenNest.Engine.csproj already has it; if not, add [assembly: InternalsVisibleTo("OpenNest.Tests")] in OpenNest.Engine/Properties/AssemblyInfo.cs or as an <InternalsVisibleTo> item in the csproj.
- Step 4: Run test to verify it passes
Run: dotnet test OpenNest.Tests --filter "AccumulatingProgressTests" -v n
Expected: All 3 tests PASS.
- Step 5: Commit
git add OpenNest.Engine/AccumulatingProgress.cs OpenNest.Tests/AccumulatingProgressTests.cs
git commit -m "refactor(engine): extract AccumulatingProgress from StripNestEngine"
Task 2: Extract ShrinkFiller
Files:
-
Create:
OpenNest.Engine/ShrinkFiller.cs -
Create:
OpenNest.Tests/ShrinkFillerTests.cs -
Step 1: Write the failing tests
In OpenNest.Tests/ShrinkFillerTests.cs:
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class ShrinkFillerTests
{
private static Drawing MakeSquareDrawing(double size)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, size)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, size)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing("square", pgm);
}
[Fact]
public void Shrink_ReducesDimension_UntilCountDrops()
{
// 10x10 parts on a 100x50 box with 1.0 spacing.
// Initial fill should place multiple parts. Shrinking height
// should eventually reduce to the tightest strip.
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var engine = new DefaultNestEngine(plate);
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
Assert.True(result.Dimension <= 50, "Dimension should be <= original");
Assert.True(result.Dimension > 0);
}
[Fact]
public void Shrink_Width_ReducesHorizontally()
{
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var engine = new DefaultNestEngine(plate);
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Width);
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
Assert.True(result.Dimension <= 100);
}
[Fact]
public void Shrink_RespectsMaxIterations()
{
var callCount = 0;
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
callCount++;
return new List<Part> { TestHelpers.MakePartAt(0, 0, 5) };
};
var item = new NestItem { Drawing = MakeSquareDrawing(5) };
var box = new Box(0, 0, 100, 100);
ShrinkFiller.Shrink(fillFunc, item, box, 1.0, ShrinkAxis.Height, maxIterations: 3);
// 1 initial + up to 3 shrink iterations = max 4 calls
Assert.True(callCount <= 4);
}
[Fact]
public void Shrink_RespectsCancellation()
{
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
var drawing = MakeSquareDrawing(10);
var item = new NestItem { Drawing = drawing };
var box = new Box(0, 0, 100, 50);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = ShrinkFiller.Shrink(fillFunc, item, box, 1.0,
ShrinkAxis.Height, token: cts.Token);
// Should return initial fill without shrinking
Assert.NotNull(result);
Assert.True(result.Parts.Count > 0);
}
}
- Step 2: Run test to verify it fails
Run: dotnet test OpenNest.Tests --filter "ShrinkFillerTests" -v n
Expected: Build error — ShrinkFiller, ShrinkAxis, ShrinkResult do not exist.
- Step 3: Create ShrinkFiller class
In OpenNest.Engine/ShrinkFiller.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
{
public enum ShrinkAxis { Width, Height }
public class ShrinkResult
{
public List<Part> Parts { get; set; }
public double Dimension { get; set; }
}
/// <summary>
/// Fills a box then iteratively shrinks one axis by the spacing amount
/// until the part count drops. Returns the tightest box that still fits
/// the same number of parts.
/// </summary>
public static class ShrinkFiller
{
public static ShrinkResult Shrink(
Func<NestItem, Box, List<Part>> fillFunc,
NestItem item, Box box,
double spacing,
ShrinkAxis axis,
CancellationToken token = default,
int maxIterations = 20)
{
var parts = fillFunc(item, box);
if (parts == null || parts.Count == 0)
return new ShrinkResult { Parts = parts ?? new List<Part>(), Dimension = 0 };
var targetCount = parts.Count;
var bestParts = parts;
var bestDim = MeasureDimension(parts, box, axis);
for (var i = 0; i < maxIterations; i++)
{
if (token.IsCancellationRequested)
break;
var trialDim = bestDim - spacing;
if (trialDim <= 0)
break;
var trialBox = axis == ShrinkAxis.Width
? new Box(box.X, box.Y, trialDim, box.Length)
: new Box(box.X, box.Y, box.Width, trialDim);
var trialParts = fillFunc(item, trialBox);
if (trialParts == null || trialParts.Count < targetCount)
break;
bestParts = trialParts;
bestDim = MeasureDimension(trialParts, box, axis);
}
return new ShrinkResult { Parts = bestParts, Dimension = bestDim };
}
private static double MeasureDimension(List<Part> parts, Box box, ShrinkAxis axis)
{
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
return axis == ShrinkAxis.Width
? placedBox.Right - box.X
: placedBox.Top - box.Y;
}
}
}
- Step 4: Run test to verify it passes
Run: dotnet test OpenNest.Tests --filter "ShrinkFillerTests" -v n
Expected: All 4 tests PASS.
- Step 5: Commit
git add OpenNest.Engine/ShrinkFiller.cs OpenNest.Tests/ShrinkFillerTests.cs
git commit -m "refactor(engine): extract ShrinkFiller from StripNestEngine"
Chunk 2: AngleCandidateBuilder and PairFiller
Task 3: Extract AngleCandidateBuilder
Files:
-
Create:
OpenNest.Engine/AngleCandidateBuilder.cs -
Create:
OpenNest.Tests/AngleCandidateBuilderTests.cs -
Modify:
OpenNest.Engine/DefaultNestEngine.cs— Add forwarding property -
Step 1: Write the failing tests
In OpenNest.Tests/AngleCandidateBuilderTests.cs:
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class AngleCandidateBuilderTests
{
private static Drawing MakeRectDrawing(double w, double h)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing("rect", pgm);
}
[Fact]
public void Build_ReturnsAtLeastTwoAngles()
{
var builder = new AngleCandidateBuilder();
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 100, 100);
var angles = builder.Build(item, 0, workArea);
Assert.True(angles.Count >= 2);
}
[Fact]
public void Build_NarrowWorkArea_ProducesMoreAngles()
{
var builder = new AngleCandidateBuilder();
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var wideArea = new Box(0, 0, 100, 100);
var narrowArea = new Box(0, 0, 100, 8); // narrower than part's longest side
var wideAngles = builder.Build(item, 0, wideArea);
var narrowAngles = builder.Build(item, 0, narrowArea);
Assert.True(narrowAngles.Count > wideAngles.Count,
$"Narrow ({narrowAngles.Count}) should have more angles than wide ({wideAngles.Count})");
}
[Fact]
public void ForceFullSweep_ProducesFullSweep()
{
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
var item = new NestItem { Drawing = MakeRectDrawing(5, 5) };
var workArea = new Box(0, 0, 100, 100);
var angles = builder.Build(item, 0, workArea);
// Full sweep at 5° steps = ~36 angles (0 to 175), plus base angles
Assert.True(angles.Count > 10);
}
[Fact]
public void RecordProductive_PrunesSubsequentBuilds()
{
var builder = new AngleCandidateBuilder { ForceFullSweep = true };
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 100, 8);
// First build — full sweep
var firstAngles = builder.Build(item, 0, workArea);
// Record some as productive
var productive = new List<AngleResult>
{
new AngleResult { AngleDeg = 0, PartCount = 5 },
new AngleResult { AngleDeg = 45, PartCount = 3 },
};
builder.RecordProductive(productive);
// Second build — should be pruned to known-good + base angles
builder.ForceFullSweep = false;
var secondAngles = builder.Build(item, 0, workArea);
Assert.True(secondAngles.Count < firstAngles.Count,
$"Pruned ({secondAngles.Count}) should be fewer than full ({firstAngles.Count})");
}
}
- Step 2: Run test to verify it fails
Run: dotnet test OpenNest.Tests --filter "AngleCandidateBuilderTests" -v n
Expected: Build error — AngleCandidateBuilder does not exist.
- Step 3: Create AngleCandidateBuilder class
In OpenNest.Engine/AngleCandidateBuilder.cs:
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.ML;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
/// <summary>
/// Builds candidate rotation angles for single-item fill. Encapsulates the
/// full pipeline: base angles, narrow-area sweep, ML prediction, and
/// known-good pruning across fills.
/// </summary>
public class AngleCandidateBuilder
{
private readonly HashSet<double> knownGoodAngles = new();
public bool ForceFullSweep { get; set; }
public List<double> Build(NestItem item, double bestRotation, Box workArea)
{
var angles = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
var testPart = new Part(item.Drawing);
if (!bestRotation.IsEqualTo(0))
testPart.Rotate(bestRotation);
testPart.UpdateBounds();
var partLongestSide = System.Math.Max(testPart.BoundingBox.Width, testPart.BoundingBox.Length);
var workAreaShortSide = System.Math.Min(workArea.Width, workArea.Length);
var needsSweep = workAreaShortSide < partLongestSide || ForceFullSweep;
if (needsSweep)
{
var step = Angle.ToRadians(5);
for (var a = 0.0; a < System.Math.PI; a += step)
{
if (!angles.Any(existing => existing.IsEqualTo(a)))
angles.Add(a);
}
}
if (!ForceFullSweep && angles.Count > 2)
{
var features = FeatureExtractor.Extract(item.Drawing);
if (features != null)
{
var predicted = AnglePredictor.PredictAngles(
features, workArea.Width, workArea.Length);
if (predicted != null)
{
var mlAngles = new List<double>(predicted);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation)))
mlAngles.Add(bestRotation);
if (!mlAngles.Any(a => a.IsEqualTo(bestRotation + Angle.HalfPI)))
mlAngles.Add(bestRotation + Angle.HalfPI);
Debug.WriteLine($"[AngleCandidateBuilder] ML: {angles.Count} angles -> {mlAngles.Count} predicted");
angles = mlAngles;
}
}
}
if (knownGoodAngles.Count > 0 && !ForceFullSweep)
{
var pruned = new List<double> { bestRotation, bestRotation + Angle.HalfPI };
foreach (var a in knownGoodAngles)
{
if (!pruned.Any(existing => existing.IsEqualTo(a)))
pruned.Add(a);
}
Debug.WriteLine($"[AngleCandidateBuilder] Pruned: {angles.Count} -> {pruned.Count} angles (known-good)");
return pruned;
}
return angles;
}
/// <summary>
/// Records angles that produced results. These are used to prune
/// subsequent Build() calls.
/// </summary>
public void RecordProductive(List<AngleResult> angleResults)
{
foreach (var ar in angleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
}
}
}
- Step 4: Run test to verify it passes
Run: dotnet test OpenNest.Tests --filter "AngleCandidateBuilderTests" -v n
Expected: All 4 tests PASS.
- Step 5: Commit
git add OpenNest.Engine/AngleCandidateBuilder.cs OpenNest.Tests/AngleCandidateBuilderTests.cs
git commit -m "refactor(engine): extract AngleCandidateBuilder from DefaultNestEngine"
Task 4: Make BuildRotatedPattern and FillPattern internal static
Files:
- Modify:
OpenNest.Engine/DefaultNestEngine.cs:493-546
This task prepares the pattern helpers for use by PairFiller. No tests needed — existing behavior is unchanged.
- Step 1: Change method signatures to internal static
In DefaultNestEngine.cs, change BuildRotatedPattern (line 493):
- From:
private Pattern BuildRotatedPattern(List<Part> groupParts, double angle) - To:
internal static Pattern BuildRotatedPattern(List<Part> groupParts, double angle)
Change FillPattern (line 513):
- From:
private List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea) - To:
internal static List<Part> FillPattern(FillLinear engine, List<Part> groupParts, List<double> angles, Box workArea)
These methods already have no instance state access — they only use their parameters. Verify this by confirming no this. or instance field/property references in their bodies.
- Step 2: Build to verify no regressions
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
- Step 3: Run all tests
Run: dotnet test OpenNest.Tests -v n
Expected: All tests PASS.
- Step 4: Commit
git add OpenNest.Engine/DefaultNestEngine.cs
git commit -m "refactor(engine): make BuildRotatedPattern and FillPattern internal static"
Task 5: Extract PairFiller
Files:
-
Create:
OpenNest.Engine/PairFiller.cs -
Create:
OpenNest.Tests/PairFillerTests.cs -
Step 1: Write the failing tests
In OpenNest.Tests/PairFillerTests.cs:
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class PairFillerTests
{
private static Drawing MakeRectDrawing(double w, double h)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing("rect", pgm);
}
[Fact]
public void Fill_ReturnsPartsForSimpleDrawing()
{
var plateSize = new Size(120, 60);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 120, 60);
var parts = filler.Fill(item, workArea);
Assert.NotNull(parts);
// Pair filling may or may not find interlocking pairs for rectangles,
// but should return a non-null list.
}
[Fact]
public void Fill_EmptyResult_WhenPartTooLarge()
{
var plateSize = new Size(10, 10);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 20) };
var workArea = new Box(0, 0, 10, 10);
var parts = filler.Fill(item, workArea);
Assert.NotNull(parts);
Assert.Empty(parts);
}
[Fact]
public void Fill_RespectsCancellation()
{
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
var plateSize = new Size(120, 60);
var filler = new PairFiller(plateSize, 0.5);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var workArea = new Box(0, 0, 120, 60);
var parts = filler.Fill(item, workArea, token: cts.Token);
// Should return empty or partial — not throw
Assert.NotNull(parts);
}
}
- Step 2: Run test to verify it fails
Run: dotnet test OpenNest.Tests --filter "PairFillerTests" -v n
Expected: Build error — PairFiller does not exist.
- Step 3: Create PairFiller class
In OpenNest.Engine/PairFiller.cs:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest
{
/// <summary>
/// Fills a work area using interlocking part pairs from BestFitCache.
/// Extracted from DefaultNestEngine.FillWithPairs.
/// </summary>
public class PairFiller
{
private const int MinPairCandidates = 10;
private static readonly TimeSpan PairTimeLimit = TimeSpan.FromSeconds(3);
private readonly Size plateSize;
private readonly double partSpacing;
public PairFiller(Size plateSize, double partSpacing)
{
this.plateSize = plateSize;
this.partSpacing = partSpacing;
}
public List<Part> Fill(NestItem item, Box workArea,
int plateNumber = 0,
CancellationToken token = default,
IProgress<NestProgress> progress = null)
{
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, plateSize.Width, plateSize.Length, partSpacing);
var candidates = SelectPairCandidates(bestFits, workArea);
var remainderPatterns = BuildRemainderPatterns(candidates, item);
Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}");
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Width:F2}x{plateSize.Length:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
List<Part> best = null;
var bestScore = default(FillScore);
var deadline = Stopwatch.StartNew();
try
{
for (var i = 0; i < candidates.Count; i++)
{
token.ThrowIfCancellationRequested();
if (i >= MinPairCandidates && deadline.Elapsed > PairTimeLimit)
{
Debug.WriteLine($"[PairFiller] Time limit at {i + 1}/{candidates.Count} ({deadline.ElapsedMilliseconds}ms)");
break;
}
var result = candidates[i];
var pairParts = result.BuildParts(item.Drawing);
var angles = result.HullAngles;
var engine = new FillLinear(workArea, partSpacing);
engine.RemainderPatterns = remainderPatterns;
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, workArea);
if (filled != null && filled.Count > 0)
{
var score = FillScore.Compute(filled, workArea);
if (best == null || score > bestScore)
{
best = filled;
bestScore = score;
}
}
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
}
}
catch (OperationCanceledException)
{
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
}
Debug.WriteLine($"[PairFiller] Best pair result: {bestScore.Count} parts, density={bestScore.Density:P1} ({deadline.ElapsedMilliseconds}ms)");
return best ?? new List<Part>();
}
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
{
var kept = bestFits.Where(r => r.Keep).ToList();
var top = kept.Take(50).ToList();
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
if (workShortSide < plateShortSide * 0.5)
{
var stripCandidates = bestFits
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
&& r.Utilization >= 0.3)
.OrderByDescending(r => r.Utilization);
var existing = new HashSet<BestFitResult>(top);
foreach (var r in stripCandidates)
{
if (top.Count >= 100)
break;
if (existing.Add(r))
top.Add(r);
}
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
}
return top;
}
private List<Pattern> BuildRemainderPatterns(List<BestFitResult> candidates, NestItem item)
{
var patterns = new List<Pattern>();
foreach (var candidate in candidates.Take(5))
{
var pairParts = candidate.BuildParts(item.Drawing);
var angles = candidate.HullAngles ?? new List<double> { 0 };
foreach (var angle in angles.Take(3))
{
var pattern = DefaultNestEngine.BuildRotatedPattern(pairParts, angle);
if (pattern.Parts.Count > 0)
patterns.Add(pattern);
}
}
return patterns;
}
}
}
Note: NestEngineBase.ReportProgress is currently protected static. It needs to become internal static so PairFiller can call it. Change the access modifier in NestEngineBase.cs line 180 from protected static to internal static.
- Step 4: Change ReportProgress to internal static
In NestEngineBase.cs line 180, change:
-
From:
protected static void ReportProgress( -
To:
internal static void ReportProgress( -
Step 5: Run test to verify it passes
Run: dotnet test OpenNest.Tests --filter "PairFillerTests" -v n
Expected: All 3 tests PASS.
- Step 6: Commit
git add OpenNest.Engine/PairFiller.cs OpenNest.Tests/PairFillerTests.cs OpenNest.Engine/NestEngineBase.cs
git commit -m "refactor(engine): extract PairFiller from DefaultNestEngine"
Chunk 3: RemnantFiller and Engine Rewiring
Task 6: Extract RemnantFiller
Files:
-
Create:
OpenNest.Engine/RemnantFiller.cs -
Create:
OpenNest.Tests/RemnantFillerTests2.cs(using2to avoid conflict with existingRemnantFinderTests.cs) -
Step 1: Write the failing tests
In OpenNest.Tests/RemnantFillerTests2.cs:
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class RemnantFillerTests2
{
private static Drawing MakeSquareDrawing(double size)
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(size, size)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, size)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing("sq", pgm);
}
[Fact]
public void FillItems_PlacesPartsInRemnants()
{
var workArea = new Box(0, 0, 100, 100);
var filler = new RemnantFiller(workArea, 1.0);
// Place a large obstacle leaving a 40x100 strip on the right
filler.AddObstacles(new[] { TestHelpers.MakePartAt(0, 0, 50) });
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var engine = new DefaultNestEngine(plate);
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
var placed = filler.FillItems(items, fillFunc);
Assert.True(placed.Count > 0, "Should place parts in remaining space");
}
[Fact]
public void FillItems_DoesNotMutateItemQuantities()
{
var workArea = new Box(0, 0, 100, 100);
var filler = new RemnantFiller(workArea, 1.0);
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 3 }
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var plate = new Plate(b.Width, b.Length);
var engine = new DefaultNestEngine(plate);
return engine.Fill(ni, b, null, System.Threading.CancellationToken.None);
};
filler.FillItems(items, fillFunc);
Assert.Equal(3, items[0].Quantity);
}
[Fact]
public void FillItems_EmptyItems_ReturnsEmpty()
{
var workArea = new Box(0, 0, 100, 100);
var filler = new RemnantFiller(workArea, 1.0);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => new List<Part>();
var result = filler.FillItems(new List<NestItem>(), fillFunc);
Assert.Empty(result);
}
[Fact]
public void FillItems_RespectsCancellation()
{
var cts = new System.Threading.CancellationTokenSource();
cts.Cancel();
var workArea = new Box(0, 0, 100, 100);
var filler = new RemnantFiller(workArea, 1.0);
var drawing = MakeSquareDrawing(10);
var items = new List<NestItem>
{
new NestItem { Drawing = drawing, Quantity = 5 }
};
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var result = filler.FillItems(items, fillFunc, cts.Token);
// Should not throw, returns whatever was placed
Assert.NotNull(result);
}
}
- Step 2: Run test to verify it fails
Run: dotnet test OpenNest.Tests --filter "RemnantFillerTests2" -v n
Expected: Build error — RemnantFiller class (the new one, not RemnantFinder) does not exist.
- Step 3: Create RemnantFiller class
In OpenNest.Engine/RemnantFiller.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using OpenNest.Geometry;
namespace OpenNest
{
/// <summary>
/// Iteratively fills remnant boxes with items using a RemnantFinder.
/// After each fill, re-discovers free rectangles and tries again
/// until no more items can be placed.
/// </summary>
public class RemnantFiller
{
private readonly RemnantFinder finder;
private readonly double spacing;
public RemnantFiller(Box workArea, double spacing)
{
this.spacing = spacing;
finder = new RemnantFinder(workArea);
}
public void AddObstacles(IEnumerable<Part> parts)
{
foreach (var part in parts)
finder.AddObstacle(part.BoundingBox.Offset(spacing));
}
public List<Part> FillItems(
List<NestItem> items,
Func<NestItem, Box, List<Part>> fillFunc,
CancellationToken token = default,
IProgress<NestProgress> progress = null)
{
if (items == null || items.Count == 0)
return new List<Part>();
var allParts = new List<Part>();
var madeProgress = true;
// Track quantities locally — do not mutate the input NestItem objects.
// NOTE: Keyed by Drawing.Name — duplicate names would collide.
// This matches the original StripNestEngine behavior.
var localQty = new Dictionary<string, int>();
foreach (var item in items)
localQty[item.Drawing.Name] = item.Quantity;
while (madeProgress && !token.IsCancellationRequested)
{
madeProgress = false;
var minRemnantDim = double.MaxValue;
foreach (var item in items)
{
var qty = localQty[item.Drawing.Name];
if (qty <= 0)
continue;
var bb = item.Drawing.Program.BoundingBox();
var dim = System.Math.Min(bb.Width, bb.Length);
if (dim < minRemnantDim)
minRemnantDim = dim;
}
if (minRemnantDim == double.MaxValue)
break;
var freeBoxes = finder.FindRemnants(minRemnantDim);
if (freeBoxes.Count == 0)
break;
foreach (var item in items)
{
if (token.IsCancellationRequested)
break;
var qty = localQty[item.Drawing.Name];
if (qty == 0)
continue;
var itemBbox = item.Drawing.Program.BoundingBox();
var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length);
foreach (var box in freeBoxes)
{
if (System.Math.Min(box.Width, box.Length) < minItemDim)
continue;
var fillItem = new NestItem { Drawing = item.Drawing, Quantity = qty };
var remnantParts = fillFunc(fillItem, box);
if (remnantParts != null && remnantParts.Count > 0)
{
allParts.AddRange(remnantParts);
localQty[item.Drawing.Name] = System.Math.Max(0, qty - remnantParts.Count);
foreach (var p in remnantParts)
finder.AddObstacle(p.BoundingBox.Offset(spacing));
madeProgress = true;
break;
}
}
if (madeProgress)
break;
}
}
return allParts;
}
}
}
- Step 4: Run test to verify it passes
Run: dotnet test OpenNest.Tests --filter "RemnantFillerTests2" -v n
Expected: All 4 tests PASS.
- Step 5: Commit
git add OpenNest.Engine/RemnantFiller.cs OpenNest.Tests/RemnantFillerTests2.cs
git commit -m "refactor(engine): extract RemnantFiller for iterative remnant filling"
Task 7: Rewire DefaultNestEngine
Files:
- Modify:
OpenNest.Engine/DefaultNestEngine.cs
This is the core rewiring. Replace extracted methods with calls to the new helper classes.
- Step 1: Add AngleCandidateBuilder field and ForceFullAngleSweep forwarding property
In DefaultNestEngine.cs, replace:
public bool ForceFullAngleSweep { get; set; }
// Angles that have produced results across multiple Fill calls.
// Populated after each Fill; used to prune subsequent fills.
private readonly HashSet<double> knownGoodAngles = new();
With:
private readonly AngleCandidateBuilder angleBuilder = new();
public bool ForceFullAngleSweep
{
get => angleBuilder.ForceFullSweep;
set => angleBuilder.ForceFullSweep = value;
}
- Step 2: Replace BuildCandidateAngles call in FindBestFill
In FindBestFill (line ~181), replace:
var angles = BuildCandidateAngles(item, bestRotation, workArea);
With:
var angles = angleBuilder.Build(item, bestRotation, workArea);
- Step 3: Replace knownGoodAngles recording in FindBestFill
In FindBestFill, replace the block (around line 243):
// Record productive angles for future fills.
foreach (var ar in AngleResults)
{
if (ar.PartCount > 0)
knownGoodAngles.Add(Angle.ToRadians(ar.AngleDeg));
}
With:
angleBuilder.RecordProductive(AngleResults);
- Step 4: Replace FillWithPairs call in FindBestFill
In FindBestFill, replace:
var pairResult = FillWithPairs(item, workArea, token, progress);
With:
var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
var pairResult = pairFiller.Fill(item, workArea, PlateNumber, token, progress);
- Step 5: Replace FillWithPairs call in Fill(List) overload
In the Fill(List<Part> groupParts, Box workArea, ...) method (around line 137), replace:
var pairResult = FillWithPairs(nestItem, workArea, token, progress);
With:
var pairFiller = new PairFiller(Plate.Size, Plate.PartSpacing);
var pairResult = pairFiller.Fill(nestItem, workArea, PlateNumber, token, progress);
- Step 6: Delete the extracted methods
Remove these methods and fields from DefaultNestEngine:
private readonly HashSet<double> knownGoodAngles(already replaced in Step 1)private List<double> BuildCandidateAngles(...)(entire method, lines 279-347)private List<Part> FillWithPairs(...)(entire method, lines 365-421)private List<BestFitResult> SelectPairCandidates(...)(entire method, lines 428-462)private List<Pattern> BuildRemainderPatterns(...)(entire method, lines 471-489)private const int MinPairCandidates = 10;(line 362)private static readonly TimeSpan PairTimeLimit = TimeSpan.FromSeconds(3);(line 363)
Also remove now-unused using statements if any (e.g., using OpenNest.Engine.BestFit; if no longer referenced, though QuickFillCount still uses BestFitCache).
- Step 7: Build to verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds with no errors.
- Step 8: Run all tests
Run: dotnet test OpenNest.Tests -v n
Expected: All tests PASS.
- Step 9: Commit
git add OpenNest.Engine/DefaultNestEngine.cs
git commit -m "refactor(engine): rewire DefaultNestEngine to use extracted helpers"
Task 8: Rewire StripNestEngine
Files:
-
Modify:
OpenNest.Engine/StripNestEngine.cs -
Step 1: Replace TryOrientation shrink loop with ShrinkFiller
In TryOrientation, replace the shrink loop (lines 188-215) with:
// Shrink to tightest strip.
var shrinkAxis = direction == StripDirection.Bottom
? ShrinkAxis.Height : ShrinkAxis.Width;
Func<NestItem, Box, List<Part>> shrinkFill = (ni, b) =>
{
var trialInner = new DefaultNestEngine(Plate);
return trialInner.Fill(ni, b, progress, token);
};
var shrinkResult = ShrinkFiller.Shrink(shrinkFill,
new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
stripBox, Plate.PartSpacing, shrinkAxis, token);
if (shrinkResult.Parts == null || shrinkResult.Parts.Count == 0)
return result;
bestParts = shrinkResult.Parts;
bestDim = shrinkResult.Dimension;
Remove the local variables that the shrink loop used to compute (actualDim, targetCount, and the for loop itself). Keep the remnant section below it.
Important: The initial fill call (lines 169-172) stays as-is — ShrinkFiller handles both the initial fill AND the shrink loop. So replace from the initial fill through the shrink loop with the single ShrinkFiller.Shrink() call, since Shrink does its own initial fill internally.
Actually — looking more carefully, the initial fill (lines 170-172) uses progress while shrink trials in the original code create a new DefaultNestEngine each time. ShrinkFiller.Shrink does its own initial fill via fillFunc. So the replacement should:
- Remove the initial fill (lines 169-175) and the shrink loop (lines 188-215)
- Replace both with the single
ShrinkFiller.Shrink()call
The fillFunc delegate handles both the initial fill and each shrink trial.
- Step 2: Replace ShrinkFill method with ShrinkFiller calls
Replace the entire ShrinkFill method (lines 358-419) with:
private List<Part> ShrinkFill(NestItem item, Box box,
IProgress<NestProgress> progress, CancellationToken token)
{
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{
var inner = new DefaultNestEngine(Plate);
return inner.Fill(ni, b, null, token);
};
// The original code shrinks width then height independently against the
// original box. The height shrink's result overwrites the width shrink's,
// so only the height result matters. We run both to preserve behavior
// (width shrink is a no-op on the final result but we keep it for parity).
var heightResult = ShrinkFiller.Shrink(fillFunc, item, box,
Plate.PartSpacing, ShrinkAxis.Height, token);
return heightResult.Parts;
}
Note: The original ShrinkFill runs width-shrink then height-shrink sequentially, but the height result always overwrites the width result (both shrink against the original box independently, targetCount never changes). Running only height-shrink is faithful to the original output while avoiding redundant initial fills.
- Step 3: Replace remnant loop with RemnantFiller
In TryOrientation, replace the remnant section (from // Build remnant box with spacing gap through the end of the while (madeProgress ...) loop) with:
// Fill remnants
if (remnantBox.Width > 0 && remnantBox.Length > 0)
{
var remnantProgress = progress != null
? new AccumulatingProgress(progress, allParts)
: (IProgress<NestProgress>)null;
var remnantFiller = new RemnantFiller(workArea, spacing);
remnantFiller.AddObstacles(allParts);
Func<NestItem, Box, List<Part>> remnantFillFunc = (ni, b) =>
ShrinkFill(ni, b, remnantProgress, token);
var additional = remnantFiller.FillItems(effectiveRemainder,
remnantFillFunc, token, remnantProgress);
allParts.AddRange(additional);
}
Keep the effectiveRemainder construction and sorting that precedes this block (lines 239-259). Keep the remnantBox computation (lines 228-233) since we need to check remnantBox.Width > 0.
Remove:
-
The
localQtydictionary (lines 276-278) -
The
while (madeProgress ...)loop (lines 280-342) -
The
AccumulatingProgressnested class (lines 425-449) — now using the standalone version -
Step 4: Build to verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
- Step 5: Run all tests
Run: dotnet test OpenNest.Tests -v n
Expected: All tests PASS.
- Step 6: Commit
git add OpenNest.Engine/StripNestEngine.cs
git commit -m "refactor(engine): rewire StripNestEngine to use extracted helpers"
Task 9: Rewire NestEngineBase.Nest
Files:
-
Modify:
OpenNest.Engine/NestEngineBase.cs:74-97 -
Step 1: Replace the fill loop with RemnantFiller
In NestEngineBase.Nest, replace phase 1 (the foreach (var item in fillItems) loop, lines 74-98) with:
// Phase 1: Fill multi-quantity drawings using RemnantFiller.
if (fillItems.Count > 0)
{
var remnantFiller = new RemnantFiller(workArea, Plate.PartSpacing);
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
FillExact(ni, b, progress, token);
var fillParts = remnantFiller.FillItems(fillItems, fillFunc, token, progress);
if (fillParts.Count > 0)
{
allParts.AddRange(fillParts);
// Deduct placed quantities
foreach (var item in fillItems)
{
var placed = fillParts.Count(p =>
p.BaseDrawing.Name == item.Drawing.Name);
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
// Update workArea for pack phase
var placedObstacles = fillParts.Select(p => p.BoundingBox.Offset(Plate.PartSpacing)).ToList();
var finder = new RemnantFinder(workArea, placedObstacles);
var remnants = finder.FindRemnants();
if (remnants.Count > 0)
workArea = remnants[0];
else
workArea = new Box(0, 0, 0, 0);
}
}
- Step 2: Build to verify
Run: dotnet build OpenNest.sln
Expected: Build succeeds.
- Step 3: Run all tests
Run: dotnet test OpenNest.Tests -v n
Expected: All tests PASS.
- Step 4: Commit
git add OpenNest.Engine/NestEngineBase.cs
git commit -m "refactor(engine): use RemnantFiller in NestEngineBase.Nest"
Task 10: Integration Smoke Tests
Files:
- Create:
OpenNest.Tests/EngineRefactorSmokeTests.cs
These tests verify that the refactored engines produce valid results end-to-end.
- Step 1: Write smoke tests
In OpenNest.Tests/EngineRefactorSmokeTests.cs:
using OpenNest.Geometry;
namespace OpenNest.Tests;
public class EngineRefactorSmokeTests
{
private static Drawing MakeRectDrawing(double w, double h, string name = "rect")
{
var pgm = new OpenNest.CNC.Program();
pgm.Codes.Add(new OpenNest.CNC.RapidMove(new Vector(0, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, 0)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(w, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, h)));
pgm.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(0, 0)));
return new Drawing(name, pgm);
}
[Fact]
public void DefaultEngine_FillNestItem_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "DefaultNestEngine should fill parts");
}
[Fact]
public void DefaultEngine_FillGroupParts_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var drawing = MakeRectDrawing(20, 10);
var groupParts = new List<Part> { new Part(drawing) };
var parts = engine.Fill(groupParts, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "DefaultNestEngine group fill should produce parts");
}
[Fact]
public void DefaultEngine_ForceFullAngleSweep_StillWorks()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
engine.ForceFullAngleSweep = true;
var item = new NestItem { Drawing = MakeRectDrawing(20, 10) };
var parts = engine.Fill(item, plate.WorkArea(), null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "ForceFullAngleSweep should still produce results");
}
[Fact]
public void StripEngine_Nest_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new StripNestEngine(plate);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "large"), Quantity = 10 },
new NestItem { Drawing = MakeRectDrawing(8, 5, "small"), Quantity = 5 },
};
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "StripNestEngine should nest parts");
}
[Fact]
public void DefaultEngine_Nest_ProducesResults()
{
var plate = new Plate(120, 60);
var engine = new DefaultNestEngine(plate);
var items = new List<NestItem>
{
new NestItem { Drawing = MakeRectDrawing(20, 10, "a"), Quantity = 5 },
new NestItem { Drawing = MakeRectDrawing(15, 8, "b"), Quantity = 3 },
};
var parts = engine.Nest(items, null, System.Threading.CancellationToken.None);
Assert.True(parts.Count > 0, "Base Nest method should place parts");
}
[Fact]
public void BruteForceRunner_StillWorks()
{
var plate = new Plate(120, 60);
var drawing = MakeRectDrawing(20, 10);
var result = OpenNest.Engine.ML.BruteForceRunner.Run(drawing, plate, forceFullAngleSweep: true);
Assert.NotNull(result);
Assert.True(result.PartCount > 0);
}
}
- Step 2: Run all smoke tests
Run: dotnet test OpenNest.Tests --filter "EngineRefactorSmokeTests" -v n
Expected: All 6 tests PASS.
- Step 3: Run the full test suite
Run: dotnet test OpenNest.Tests -v n
Expected: All tests PASS.
- Step 4: Commit
git add OpenNest.Tests/EngineRefactorSmokeTests.cs
git commit -m "test(engine): add integration smoke tests for engine refactor"
Task 11: Clean up unused imports
Files:
-
Modify:
OpenNest.Engine/DefaultNestEngine.cs -
Modify:
OpenNest.Engine/StripNestEngine.cs -
Step 1: Remove unused using statements from DefaultNestEngine
After extracting the pair/angle code, check which using statements are no longer needed. Likely candidates:
using OpenNest.Engine.BestFit;— may still be needed byQuickFillCountusing OpenNest.Engine.ML;— no longer needed (moved to AngleCandidateBuilder)
Build to verify: dotnet build OpenNest.sln
- Step 2: Remove AccumulatingProgress nested class from StripNestEngine if still present
Verify StripNestEngine.cs no longer contains the AccumulatingProgress nested class definition. It should have been removed in Task 8.
- Step 3: Build and run full test suite
Run: dotnet build OpenNest.sln && dotnet test OpenNest.Tests -v n
Expected: Build succeeds, all tests PASS.
- Step 4: Commit
git add OpenNest.Engine/DefaultNestEngine.cs OpenNest.Engine/StripNestEngine.cs
git commit -m "refactor(engine): clean up unused imports after extraction"