diff --git a/docs/superpowers/plans/2026-03-20-nfp-bestfit-strategy.md b/docs/superpowers/plans/2026-03-20-nfp-bestfit-strategy.md new file mode 100644 index 0000000..f9ef5cc --- /dev/null +++ b/docs/superpowers/plans/2026-03-20-nfp-bestfit-strategy.md @@ -0,0 +1,704 @@ +# NFP Best-Fit Strategy 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:** Replace the brute-force slide-based best-fit pair sampling with NFP-based candidate generation that produces mathematically exact interlocking positions. + +**Architecture:** New `NfpSlideStrategy : IBestFitStrategy` generates `PairCandidate` offsets from NFP boundary vertices/edges. Shared polygon helper extracted from `AutoNester` to avoid duplication. `BestFitFinder.BuildStrategies` swaps to the new strategy. Everything downstream (evaluator, filter, tiling) stays unchanged. + +**Tech Stack:** C# / .NET 8, xunit, existing `NoFitPolygon` (Minkowski sum via Clipper2), `ShapeProfile`, `ConvertProgram` + +**Spec:** `docs/superpowers/specs/2026-03-20-nfp-bestfit-strategy-design.md` + +--- + +### Task 1: Extract `PolygonHelper` from `AutoNester` + +**Files:** +- Create: `OpenNest.Engine/BestFit/PolygonHelper.cs` +- Modify: `OpenNest.Engine/Nfp/AutoNester.cs:204-343` +- Test: `OpenNest.Tests/PolygonHelperTests.cs` + +- [ ] **Step 1: Add shared test helpers and write tests for `PolygonHelper`** + +Add `MakeSquareDrawing` and `MakeLShapeDrawing` to `OpenNest.Tests/TestHelpers.cs`: + +```csharp +public static Drawing MakeSquareDrawing(double size = 10) +{ + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(size, 0))); + pgm.Codes.Add(new LinearMove(new Vector(size, size))); + pgm.Codes.Add(new LinearMove(new Vector(0, size))); + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); + return new Drawing("square", pgm); +} + +public static Drawing MakeLShapeDrawing() +{ + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + pgm.Codes.Add(new LinearMove(new Vector(10, 0))); + pgm.Codes.Add(new LinearMove(new Vector(10, 5))); + pgm.Codes.Add(new LinearMove(new Vector(5, 5))); + pgm.Codes.Add(new LinearMove(new Vector(5, 10))); + pgm.Codes.Add(new LinearMove(new Vector(0, 10))); + pgm.Codes.Add(new LinearMove(new Vector(0, 0))); + return new Drawing("lshape", pgm); +} +``` + +Then create `OpenNest.Tests/PolygonHelperTests.cs`: + +```csharp +using OpenNest.CNC; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Tests; + +public class PolygonHelperTests +{ + + [Fact] + public void ExtractPerimeterPolygon_ReturnsPolygon_ForValidDrawing() + { + var drawing = TestHelpers.MakeSquareDrawing(); + var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0); + Assert.NotNull(result.Polygon); + Assert.True(result.Polygon.Vertices.Count >= 4); + } + + [Fact] + public void ExtractPerimeterPolygon_InflatesPolygon_WhenSpacingNonZero() + { + var drawing = TestHelpers.MakeSquareDrawing(10); + var noSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 0); + var withSpacing = PolygonHelper.ExtractPerimeterPolygon(drawing, 1); + + // Inflated polygon should have a larger bounding box. + noSpacing.Polygon.UpdateBounds(); + withSpacing.Polygon.UpdateBounds(); + Assert.True(withSpacing.Polygon.BoundingBox.Width > noSpacing.Polygon.BoundingBox.Width); + } + + [Fact] + public void ExtractPerimeterPolygon_ReturnsNull_ForEmptyDrawing() + { + var pgm = new Program(); + var drawing = new Drawing("empty", pgm); + var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0); + Assert.Null(result.Polygon); + } + + [Fact] + public void ExtractPerimeterPolygon_CorrectionVector_ReflectsOriginDifference() + { + // Square drawing: program bbox starts at (0,0) due to rapid move, + // perimeter bbox also starts at (0,0) — correction should be near zero. + var drawing = TestHelpers.MakeSquareDrawing(); + var result = PolygonHelper.ExtractPerimeterPolygon(drawing, 0); + Assert.NotNull(result.Polygon); + // For a simple square starting at origin, correction should be small. + Assert.True(System.Math.Abs(result.Correction.X) < 1); + Assert.True(System.Math.Abs(result.Correction.Y) < 1); + } + + [Fact] + public void RotatePolygon_AtZero_ReturnsSamePolygon() + { + var polygon = new Polygon(); + polygon.Vertices.Add(new Vector(0, 0)); + polygon.Vertices.Add(new Vector(10, 0)); + polygon.Vertices.Add(new Vector(10, 10)); + polygon.Vertices.Add(new Vector(0, 10)); + polygon.UpdateBounds(); + + var rotated = PolygonHelper.RotatePolygon(polygon, 0); + Assert.Same(polygon, rotated); + } + + [Fact] + public void RotatePolygon_At90Degrees_SwapsDimensions() + { + var polygon = new Polygon(); + polygon.Vertices.Add(new Vector(0, 0)); + polygon.Vertices.Add(new Vector(20, 0)); + polygon.Vertices.Add(new Vector(20, 10)); + polygon.Vertices.Add(new Vector(0, 10)); + polygon.UpdateBounds(); + + var rotated = PolygonHelper.RotatePolygon(polygon, Angle.HalfPI); + rotated.UpdateBounds(); + + // Width and height should swap (approximately). + Assert.True(System.Math.Abs(rotated.BoundingBox.Width - 10) < 0.1); + Assert.True(System.Math.Abs(rotated.BoundingBox.Length - 20) < 0.1); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~PolygonHelperTests" --no-build 2>&1 || dotnet test OpenNest.Tests --filter "FullyQualifiedName~PolygonHelperTests"` +Expected: Build error — `PolygonHelper` does not exist yet. + +- [ ] **Step 3: Create `PolygonHelper.cs`** + +Create `OpenNest.Engine/BestFit/PolygonHelper.cs`: + +```csharp +using OpenNest.Converters; +using OpenNest.Geometry; + +namespace OpenNest.Engine.BestFit +{ + public static class PolygonHelper + { + public static PolygonExtractionResult ExtractPerimeterPolygon(Drawing drawing, double halfSpacing) + { + var entities = ConvertProgram.ToGeometry(drawing.Program) + .Where(e => e.Layer != SpecialLayers.Rapid) + .ToList(); + + if (entities.Count == 0) + return new PolygonExtractionResult(null, Vector.Zero); + + var definedShape = new ShapeProfile(entities); + var perimeter = definedShape.Perimeter; + + if (perimeter == null) + return new PolygonExtractionResult(null, Vector.Zero); + + // Compute the perimeter bounding box before inflation for coordinate correction. + perimeter.UpdateBounds(); + var perimeterBb = perimeter.BoundingBox; + + // Inflate by half-spacing if spacing is non-zero. + Shape inflated; + + if (halfSpacing > 0) + { + var offsetEntity = perimeter.OffsetEntity(halfSpacing, OffsetSide.Left); + inflated = offsetEntity as Shape ?? perimeter; + } + else + { + inflated = perimeter; + } + + // Convert to polygon with circumscribed arcs for tight nesting. + var polygon = inflated.ToPolygonWithTolerance(0.01, circumscribe: true); + + if (polygon.Vertices.Count < 3) + return new PolygonExtractionResult(null, Vector.Zero); + + // Compute correction: difference between program origin and perimeter origin. + // Part.CreateAtOrigin normalizes to program bbox; polygon normalizes to perimeter bbox. + var programBb = drawing.Program.BoundingBox(); + var correction = new Vector( + perimeterBb.Left - programBb.Location.X, + perimeterBb.Bottom - programBb.Location.Y); + + // Normalize: move reference point to origin. + polygon.UpdateBounds(); + var bb = polygon.BoundingBox; + polygon.Offset(-bb.Left, -bb.Bottom); + + return new PolygonExtractionResult(polygon, correction); + } + + public static Polygon RotatePolygon(Polygon polygon, double angle) + { + if (angle.IsEqualTo(0)) + return polygon; + + var result = new Polygon(); + var cos = System.Math.Cos(angle); + var sin = System.Math.Sin(angle); + + foreach (var v in polygon.Vertices) + { + result.Vertices.Add(new Vector( + v.X * cos - v.Y * sin, + v.X * sin + v.Y * cos)); + } + + // Re-normalize to origin. + result.UpdateBounds(); + var bb = result.BoundingBox; + result.Offset(-bb.Left, -bb.Bottom); + + return result; + } + } + + public record PolygonExtractionResult(Polygon Polygon, Vector Correction); +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~PolygonHelperTests"` +Expected: All 6 tests PASS. + +- [ ] **Step 5: Update `AutoNester` to delegate to `PolygonHelper`** + +In `OpenNest.Engine/Nfp/AutoNester.cs`, replace the private `ExtractPerimeterPolygon` and `RotatePolygon` methods (lines 204-343) with delegates to `PolygonHelper`: + +```csharp +private static Polygon ExtractPerimeterPolygon(Drawing drawing, double halfSpacing) +{ + return BestFit.PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing).Polygon; +} + +private static Polygon RotatePolygon(Polygon polygon, double angle) +{ + return BestFit.PolygonHelper.RotatePolygon(polygon, angle); +} +``` + +- [ ] **Step 6: Build solution to verify no regressions** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeds with no errors. + +- [ ] **Step 7: Commit** + +```bash +git add OpenNest.Engine/BestFit/PolygonHelper.cs OpenNest.Tests/PolygonHelperTests.cs OpenNest.Tests/TestHelpers.cs OpenNest.Engine/Nfp/AutoNester.cs +git commit -m "refactor: extract PolygonHelper from AutoNester for shared polygon operations" +``` + +--- + +### Task 2: Create `NfpSlideStrategy` + +**Files:** +- Create: `OpenNest.Engine/BestFit/NfpSlideStrategy.cs` +- Test: `OpenNest.Tests/NfpSlideStrategyTests.cs` + +- [ ] **Step 1: Write tests for `NfpSlideStrategy`** + +Create `OpenNest.Tests/NfpSlideStrategyTests.cs`: + +```csharp +using OpenNest.CNC; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; +using OpenNest.Math; + +namespace OpenNest.Tests; + +public class NfpSlideStrategyTests +{ + [Fact] + public void GenerateCandidates_ReturnsNonEmpty_ForSquare() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.NotEmpty(candidates); + } + + [Fact] + public void GenerateCandidates_AllCandidatesHaveCorrectDrawing() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.All(candidates, c => Assert.Same(drawing, c.Drawing)); + } + + [Fact] + public void GenerateCandidates_Part1RotationIsAlwaysZero() + { + var strategy = new NfpSlideStrategy(Angle.HalfPI, 1, "90 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.All(candidates, c => Assert.Equal(0, c.Part1Rotation)); + } + + [Fact] + public void GenerateCandidates_Part2RotationMatchesStrategy() + { + var rotation = Angle.HalfPI; + var strategy = new NfpSlideStrategy(rotation, 1, "90 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.All(candidates, c => Assert.Equal(rotation, c.Part2Rotation)); + } + + [Fact] + public void GenerateCandidates_NoDuplicateOffsets() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + + var uniqueOffsets = candidates + .Select(c => (System.Math.Round(c.Part2Offset.X, 6), System.Math.Round(c.Part2Offset.Y, 6))) + .Distinct() + .Count(); + Assert.Equal(candidates.Count, uniqueOffsets); + } + + [Fact] + public void GenerateCandidates_MoreCandidates_WithSmallerStepSize() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var largeStep = strategy.GenerateCandidates(drawing, 0.25, 5.0); + var smallStep = strategy.GenerateCandidates(drawing, 0.25, 0.5); + // Smaller step should add more edge samples. + Assert.True(smallStep.Count >= largeStep.Count); + } + + [Fact] + public void GenerateCandidates_ReturnsEmpty_ForEmptyDrawing() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var pgm = new Program(); + var drawing = new Drawing("empty", pgm); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.Empty(candidates); + } + + [Fact] + public void GenerateCandidates_LShape_ProducesMoreCandidates_ThanSquare() + { + var strategy = new NfpSlideStrategy(0, 1, "0 deg NFP"); + var square = TestHelpers.MakeSquareDrawing(); + var lshape = TestHelpers.MakeLShapeDrawing(); + + var squareCandidates = strategy.GenerateCandidates(square, 0.25, 0.25); + var lshapeCandidates = strategy.GenerateCandidates(lshape, 0.25, 0.25); + + // L-shape NFP has more vertices/edges than square NFP. + Assert.True(lshapeCandidates.Count > squareCandidates.Count); + } + + [Fact] + public void GenerateCandidates_At180Degrees_ProducesCandidates() + { + var strategy = new NfpSlideStrategy(System.Math.PI, 1, "180 deg NFP"); + var drawing = TestHelpers.MakeSquareDrawing(); + var candidates = strategy.GenerateCandidates(drawing, 0.25, 0.25); + Assert.NotEmpty(candidates); + Assert.All(candidates, c => Assert.Equal(System.Math.PI, c.Part2Rotation)); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~NfpSlideStrategyTests" --no-build 2>&1 || dotnet test OpenNest.Tests --filter "FullyQualifiedName~NfpSlideStrategyTests"` +Expected: Build error — `NfpSlideStrategy` does not exist yet. + +- [ ] **Step 3: Create `NfpSlideStrategy.cs`** + +Create `OpenNest.Engine/BestFit/NfpSlideStrategy.cs`: + +```csharp +using OpenNest.Geometry; +using OpenNest.Math; +using System.Collections.Generic; + +namespace OpenNest.Engine.BestFit +{ + public class NfpSlideStrategy : IBestFitStrategy + { + private readonly double _part2Rotation; + + public NfpSlideStrategy(double part2Rotation, int type, string description) + { + _part2Rotation = part2Rotation; + Type = type; + Description = description; + } + + public int Type { get; } + public string Description { get; } + + public List GenerateCandidates(Drawing drawing, double spacing, double stepSize) + { + var candidates = new List(); + + var halfSpacing = spacing / 2; + + // Extract stationary polygon (Part1 at rotation 0), with spacing applied. + var stationaryResult = PolygonHelper.ExtractPerimeterPolygon(drawing, halfSpacing); + + if (stationaryResult.Polygon == null) + return candidates; + + var stationaryPoly = stationaryResult.Polygon; + + // Extract orbiting polygon (Part2 at _part2Rotation). + // Reuse stationary result if rotation is 0, otherwise rotate. + var orbitingPoly = PolygonHelper.RotatePolygon(stationaryResult.Polygon, _part2Rotation); + + // Compute NFP. + var nfp = NoFitPolygon.Compute(stationaryPoly, orbitingPoly); + + if (nfp == null || nfp.Vertices.Count < 3) + return candidates; + + // Coordinate correction: NFP offsets are in polygon-space. + // Part.CreateAtOrigin uses program bbox origin. + var correction = stationaryResult.Correction; + + // Walk NFP boundary — vertices + edge samples. + var verts = nfp.Vertices; + var vertCount = nfp.IsClosed() ? verts.Count - 1 : verts.Count; + var testNumber = 0; + + for (var i = 0; i < vertCount; i++) + { + // Add vertex candidate. + var offset = ApplyCorrection(verts[i], correction); + candidates.Add(MakeCandidate(drawing, offset, spacing, testNumber++)); + + // Add edge samples for long edges. + var next = (i + 1) % vertCount; + var dx = verts[next].X - verts[i].X; + var dy = verts[next].Y - verts[i].Y; + var edgeLength = System.Math.Sqrt(dx * dx + dy * dy); + + if (edgeLength > stepSize) + { + var steps = (int)(edgeLength / stepSize); + for (var s = 1; s < steps; s++) + { + var t = (double)s / steps; + var sample = new Vector( + verts[i].X + dx * t, + verts[i].Y + dy * t); + var sampleOffset = ApplyCorrection(sample, correction); + candidates.Add(MakeCandidate(drawing, sampleOffset, spacing, testNumber++)); + } + } + } + + return candidates; + } + + private static Vector ApplyCorrection(Vector nfpVertex, Vector correction) + { + return new Vector(nfpVertex.X + correction.X, nfpVertex.Y + correction.Y); + } + + private PairCandidate MakeCandidate(Drawing drawing, Vector offset, double spacing, int testNumber) + { + return new PairCandidate + { + Drawing = drawing, + Part1Rotation = 0, + Part2Rotation = _part2Rotation, + Part2Offset = offset, + StrategyType = Type, + TestNumber = testNumber, + Spacing = spacing + }; + } + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~NfpSlideStrategyTests"` +Expected: All 9 tests PASS. + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Engine/BestFit/NfpSlideStrategy.cs OpenNest.Tests/NfpSlideStrategyTests.cs +git commit -m "feat: add NfpSlideStrategy for NFP-based best-fit candidate generation" +``` + +--- + +### Task 3: Wire `NfpSlideStrategy` into `BestFitFinder` + +**Files:** +- Modify: `OpenNest.Engine/BestFit/BestFitFinder.cs:78-91` +- Test: `OpenNest.Tests/NfpBestFitIntegrationTests.cs` + +- [ ] **Step 1: Write integration test** + +Create `OpenNest.Tests/NfpBestFitIntegrationTests.cs`: + +```csharp +using OpenNest.CNC; +using OpenNest.Engine.BestFit; +using OpenNest.Geometry; + +namespace OpenNest.Tests; + +public class NfpBestFitIntegrationTests +{ + [Fact] + public void FindBestFits_ReturnsKeptResults_ForSquare() + { + var finder = new BestFitFinder(120, 60); + var drawing = TestHelpers.MakeSquareDrawing(); + var results = finder.FindBestFits(drawing); + Assert.NotEmpty(results); + Assert.NotEmpty(results.Where(r => r.Keep)); + } + + [Fact] + public void FindBestFits_ResultsHaveValidDimensions() + { + var finder = new BestFitFinder(120, 60); + var drawing = TestHelpers.MakeSquareDrawing(); + var results = finder.FindBestFits(drawing); + + foreach (var result in results.Where(r => r.Keep)) + { + Assert.True(result.BoundingWidth > 0); + Assert.True(result.BoundingHeight > 0); + Assert.True(result.RotatedArea > 0); + } + } + + [Fact] + public void FindBestFits_LShape_HasBetterUtilization_ThanBoundingBox() + { + var finder = new BestFitFinder(120, 60); + var drawing = TestHelpers.MakeLShapeDrawing(); + var results = finder.FindBestFits(drawing); + + // At least one kept result should have >50% utilization + // (L-shapes interlock well, bounding box alone would be ~50%). + var bestUtilization = results + .Where(r => r.Keep) + .Max(r => r.Utilization); + Assert.True(bestUtilization > 0.5); + } + + [Fact] + public void FindBestFits_NoOverlaps_InKeptResults() + { + var finder = new BestFitFinder(120, 60); + var drawing = TestHelpers.MakeSquareDrawing(); + var results = finder.FindBestFits(drawing); + + // All kept results should be non-overlapping (verified by PairEvaluator). + Assert.All(results.Where(r => r.Keep), r => + Assert.Equal("Valid", r.Reason)); + } +} +``` + +- [ ] **Step 2: Swap `BuildStrategies` to use `NfpSlideStrategy`** + +In `OpenNest.Engine/BestFit/BestFitFinder.cs`, replace the `BuildStrategies` method (lines 78-91): + +```csharp +private List BuildStrategies(Drawing drawing) +{ + var angles = GetRotationAngles(drawing); + var strategies = new List(); + var type = 1; + + foreach (var angle in angles) + { + var desc = $"{Angle.ToDegrees(angle):F1} deg NFP"; + strategies.Add(new NfpSlideStrategy(angle, type++, desc)); + } + + return strategies; +} +``` + +Add `using OpenNest.Math;` to the top of the file if not already present. + +- [ ] **Step 3: Run integration tests** + +Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~NfpBestFitIntegrationTests"` +Expected: All 4 tests PASS with the NFP pipeline. + +- [ ] **Step 4: Run all tests to check for regressions** + +Run: `dotnet test OpenNest.Tests` +Expected: All tests PASS. + +- [ ] **Step 5: Build full solution** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeds. + +- [ ] **Step 6: Commit** + +```bash +git add OpenNest.Engine/BestFit/BestFitFinder.cs OpenNest.Tests/NfpBestFitIntegrationTests.cs +git commit -m "feat: wire NfpSlideStrategy into BestFitFinder pipeline" +``` + +--- + +### Task 4: Remove `AutoNester.Optimize` calls + +The `Optimize` calls are dead weight for single-drawing fills (as discussed). Now that NFP is properly integrated via best-fit, remove the no-op optimization passes. + +**Files:** +- Modify: `OpenNest.Engine/NestEngineBase.cs:133-134` +- Modify: `OpenNest.Engine/NfpNestEngine.cs:52-53` +- Modify: `OpenNest.Engine/StripNestEngine.cs:126-127` +- Modify: `OpenNest/Controls/PlateView.cs` (the line calling `AutoNester.Optimize`) + +- [ ] **Step 1: Remove `AutoNester.Optimize` call from `NestEngineBase.cs`** + +In `OpenNest.Engine/NestEngineBase.cs`, remove lines 133-134: +```csharp +// NFP optimization pass — re-place parts using geometry-aware BLF. +allParts = AutoNester.Optimize(allParts, Plate); +``` + +- [ ] **Step 2: Remove `AutoNester.Optimize` call from `NfpNestEngine.cs`** + +In `OpenNest.Engine/NfpNestEngine.cs`, remove lines 52-53: +```csharp +// NFP optimization pass — re-place parts using geometry-aware BLF. +parts = AutoNester.Optimize(parts, Plate); +``` + +- [ ] **Step 3: Remove `AutoNester.Optimize` call from `StripNestEngine.cs`** + +In `OpenNest.Engine/StripNestEngine.cs`, remove lines 126-127: +```csharp +// NFP optimization pass — re-place parts using geometry-aware BLF. +allParts = AutoNester.Optimize(allParts, Plate); +``` + +- [ ] **Step 4: Remove `AutoNester.Optimize` call from `PlateView.cs`** + +In `OpenNest/Controls/PlateView.cs`, find the line calling `AutoNester.Optimize(result, workArea, spacing)` and replace: +```csharp +return AutoNester.Optimize(result, workArea, spacing); +``` +with: +```csharp +return result; +``` + +- [ ] **Step 5: Run all tests** + +Run: `dotnet test OpenNest.Tests` +Expected: All tests PASS. + +- [ ] **Step 6: Build full solution** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeds. + +- [ ] **Step 7: Commit** + +```bash +git add OpenNest.Engine/NestEngineBase.cs OpenNest.Engine/NfpNestEngine.cs OpenNest.Engine/StripNestEngine.cs OpenNest/Controls/PlateView.cs +git commit -m "perf: remove no-op AutoNester.Optimize calls from fill pipelines" +```