diff --git a/docs/superpowers/plans/2026-03-16-plate-processor.md b/docs/superpowers/plans/2026-03-16-plate-processor.md new file mode 100644 index 0000000..13c4c38 --- /dev/null +++ b/docs/superpowers/plans/2026-03-16-plate-processor.md @@ -0,0 +1,1760 @@ +# Plate Processor 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:** Build a plate-level orchestrator that sequences parts, assigns per-part lead-ins based on approach direction, and plans safe rapid paths between parts. + +**Architecture:** Three-stage pipeline in OpenNest.Engine — IPartSequencer (cut order) → ContourCuttingStrategy (lead-ins) → IRapidPlanner (safe rapids) — wired by PlateProcessor. Non-destructive: results stored in PlateResult, original Part.Program untouched. + +**Tech Stack:** .NET 8, xUnit (new test project), OpenNest.Core, OpenNest.Engine + +**Spec:** `docs/superpowers/specs/2026-03-15-plate-processor-design.md` + +--- + +## File Structure + +| Action | File | Responsibility | +|--------|------|----------------| +| Create | `OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj` | xUnit test project | +| Create | `OpenNest.Engine.Tests/TestHelpers.cs` | Shared test helpers (MakePartAt, MakePlate) | +| Modify | `OpenNest.sln` | Add test project to solution | +| Modify | `OpenNest.Core/Part.cs` | Add `HasManualLeadIns` property | +| Create | `OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs` | Return type for ContourCuttingStrategy.Apply | +| Modify | `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` | Change Apply signature to accept Vector approachPoint | +| Create | `OpenNest.Engine/Sequencing/IPartSequencer.cs` | Interface + SequencedPart struct | +| Create | `OpenNest.Engine/Sequencing/PartSequencerFactory.cs` | Maps SequenceMethod to IPartSequencer | +| Create | `OpenNest.Engine/Sequencing/RightSideSequencer.cs` | Sort by X descending | +| Create | `OpenNest.Engine/Sequencing/LeftSideSequencer.cs` | Sort by X ascending | +| Create | `OpenNest.Engine/Sequencing/BottomSideSequencer.cs` | Sort by Y ascending | +| Create | `OpenNest.Engine/Sequencing/EdgeStartSequencer.cs` | Sort by distance from nearest plate edge | +| Create | `OpenNest.Engine/Sequencing/PlateHelper.cs` | Shared exit point calculation | +| Create | `OpenNest.Engine/Sequencing/LeastCodeSequencer.cs` | Nearest-neighbor + 2-opt | +| Create | `OpenNest.Engine/Sequencing/AdvancedSequencer.cs` | Row/column grouping with serpentine ordering | +| Create | `OpenNest.Engine/RapidPlanning/IRapidPlanner.cs` | Interface | +| Create | `OpenNest.Engine/RapidPlanning/RapidPath.cs` | Result struct | +| Create | `OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs` | Always head-up | +| Create | `OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs` | Head-down if clear, head-up if blocked | +| Create | `OpenNest.Engine/PlateProcessor.cs` | Orchestrator | +| Create | `OpenNest.Engine/PlateResult.cs` | Result types (PlateResult, ProcessedPart) | + +--- + +## Chunk 1: Foundation + +### Task 1: Create xUnit test project + +**Files:** +- Create: `OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj` +- Modify: `OpenNest.sln` + +- [ ] **Step 1: Create the test project** + +```bash +cd C:/Users/AJ/Desktop/Projects/OpenNest +dotnet new xunit -n OpenNest.Engine.Tests --framework net8.0-windows +dotnet sln add OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj +dotnet add OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj reference OpenNest.Core/OpenNest.Core.csproj +dotnet add OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj reference OpenNest.Engine/OpenNest.Engine.csproj +``` + +- [ ] **Step 2: Verify the project builds** + +Run: `dotnet build OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj` +Expected: Build succeeded + +- [ ] **Step 3: Delete the generated UnitTest1.cs** + +Delete `OpenNest.Engine.Tests/UnitTest1.cs` — we'll create our own test files. + +- [ ] **Step 4: Commit** + +```bash +git add OpenNest.Engine.Tests/ OpenNest.sln +git commit -m "chore: add OpenNest.Engine.Tests xUnit project" +``` + +--- + +### Task 2: Shared test helper + +**Files:** +- Create: `OpenNest.Engine.Tests/TestHelpers.cs` + +- [ ] **Step 1: Create TestHelpers.cs** + +This helper is used by nearly every test in the plan. Creates simple 1x1 or 2x2 square parts at known positions. + +```csharp +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Tests; + +internal static class TestHelpers +{ + public static Part MakePartAt(double x, double y, double size = 1) + { + 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))); + var drawing = new Drawing("test", pgm); + return new Part(drawing, new Vector(x, y)); + } + + public static Plate MakePlate(double width = 60, double length = 120, params Part[] parts) + { + var plate = new Plate(width, length); + foreach (var p in parts) + plate.Parts.Add(p); + return plate; + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest.Engine.Tests/OpenNest.Engine.Tests.csproj` +Expected: Build succeeded + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest.Engine.Tests/TestHelpers.cs +git commit -m "chore: add shared test helpers for Engine tests" +``` + +--- + +### Task 3: CuttingResult struct + +**Files:** +- Create: `OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs` +- Test: `OpenNest.Engine.Tests/CuttingResultTests.cs` + +- [ ] **Step 1: Write the test** + +```csharp +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests; + +public class CuttingResultTests +{ + [Fact] + public void CuttingResult_StoresValues() + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(1, 2))); + var point = new Vector(3, 4); + + var result = new CuttingResult { Program = pgm, LastCutPoint = point }; + + Assert.Same(pgm, result.Program); + Assert.Equal(3, result.LastCutPoint.X); + Assert.Equal(4, result.LastCutPoint.Y); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `dotnet test OpenNest.Engine.Tests --filter CuttingResult_StoresValues -v n` +Expected: FAIL — `CuttingResult` type does not exist + +- [ ] **Step 3: Create CuttingResult.cs** + +```csharp +using OpenNest.CNC; +using OpenNest.Geometry; + +namespace OpenNest.CNC.CuttingStrategy +{ + public readonly struct CuttingResult + { + public Program Program { get; init; } + public Vector LastCutPoint { get; init; } + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `dotnet test OpenNest.Engine.Tests --filter CuttingResult_StoresValues -v n` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Core/CNC/CuttingStrategy/CuttingResult.cs OpenNest.Engine.Tests/CuttingResultTests.cs +git commit -m "feat: add CuttingResult struct" +``` + +--- + +### Task 4: Part.HasManualLeadIns flag + +**Files:** +- Modify: `OpenNest.Core/Part.cs` +- Test: `OpenNest.Engine.Tests/PartFlagTests.cs` + +- [ ] **Step 1: Write the test** + +```csharp +using OpenNest.CNC; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests; + +public class PartFlagTests +{ + [Fact] + public void HasManualLeadIns_DefaultsFalse() + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + var drawing = new Drawing("test", pgm); + var part = new Part(drawing); + + Assert.False(part.HasManualLeadIns); + } + + [Fact] + public void HasManualLeadIns_CanBeSet() + { + var pgm = new Program(); + pgm.Codes.Add(new RapidMove(new Vector(0, 0))); + var drawing = new Drawing("test", pgm); + var part = new Part(drawing); + + part.HasManualLeadIns = true; + + Assert.True(part.HasManualLeadIns); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `dotnet test OpenNest.Engine.Tests --filter HasManualLeadIns -v n` +Expected: FAIL — `HasManualLeadIns` property does not exist + +- [ ] **Step 3: Add the property to Part.cs** + +In `OpenNest.Core/Part.cs`, after the `Program` property (line 52), add: + +```csharp +public bool HasManualLeadIns { get; set; } +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `dotnet test OpenNest.Engine.Tests --filter HasManualLeadIns -v n` +Expected: 2 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Core/Part.cs OpenNest.Engine.Tests/PartFlagTests.cs +git commit -m "feat: add Part.HasManualLeadIns flag" +``` + +--- + +### Task 5: ContourCuttingStrategy signature change + +**Files:** +- Modify: `OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs` + +This changes the existing `Apply` method signature and return type. No new tests needed — this modifies an existing class that currently has no callers (it was recently built). + +- [ ] **Step 1: Change the Apply signature** + +In `ContourCuttingStrategy.cs`, change line 10: + +```csharp +// Before: +public Program Apply(Program partProgram, Plate plate) +// After: +public CuttingResult Apply(Program partProgram, Vector approachPoint) +``` + +- [ ] **Step 2: Replace GetExitPoint usage with approachPoint** + +Replace line 12: +```csharp +// Before: +var exitPoint = GetExitPoint(plate); +// After: +var exitPoint = approachPoint; +``` + +- [ ] **Step 3: Delete the GetExitPoint method** + +Delete lines 66-79 (the `GetExitPoint(Plate plate)` method). This logic moves to `PlateProcessor` later. + +- [ ] **Step 4: Track the last cut point and return CuttingResult** + +`perimeterPt` is declared inside a bare block (lines 48-61) and goes out of scope at the closing brace. Declare a `lastCutPoint` variable before the block and assign it inside. + +Before the `// Perimeter last` block (before line 48), add: +```csharp +var lastCutPoint = exitPoint; +``` + +Inside the `// Perimeter last` block, after the perimeter point is computed (after line 49), add: +```csharp +lastCutPoint = perimeterPt; +``` + +Replace line 63 (`return result;`): +```csharp +return new CuttingResult +{ + Program = result, + LastCutPoint = lastCutPoint +}; +``` + +- [ ] **Step 5: Remove the unused Plate using directive if needed** + +Check if `Plate` is still referenced. The `using OpenNest.Geometry` is still needed for `Vector`. + +- [ ] **Step 6: Build to verify** + +Run: `dotnet build OpenNest.Core/OpenNest.Core.csproj` +Expected: Build succeeded + +- [ ] **Step 7: Commit** + +```bash +git add OpenNest.Core/CNC/CuttingStrategy/ContourCuttingStrategy.cs +git commit -m "refactor: change ContourCuttingStrategy.Apply to accept approachPoint" +``` + +--- + +## Chunk 2: Part Sequencing + +### Task 6: IPartSequencer interface and SequencedPart + +**Files:** +- Create: `OpenNest.Engine/Sequencing/IPartSequencer.cs` + +- [ ] **Step 1: Create the interface file** + +```csharp +using System.Collections.Generic; + +namespace OpenNest.Engine.Sequencing +{ + public readonly struct SequencedPart + { + public Part Part { get; init; } + } + + public interface IPartSequencer + { + List Sequence(IReadOnlyList parts, Plate plate); + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj` +Expected: Build succeeded + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest.Engine/Sequencing/ +git commit -m "feat: add IPartSequencer interface and SequencedPart" +``` + +--- + +### Task 7: Directional sequencers (RightSide, LeftSide, BottomSide) + +**Files:** +- Create: `OpenNest.Engine/Sequencing/RightSideSequencer.cs` +- Create: `OpenNest.Engine/Sequencing/LeftSideSequencer.cs` +- Create: `OpenNest.Engine/Sequencing/BottomSideSequencer.cs` +- Test: `OpenNest.Engine.Tests/Sequencing/DirectionalSequencerTests.cs` + +- [ ] **Step 1: Write the tests** + +Create a helper method to build simple parts at known positions, then test all three directional sequencers. + +```csharp +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.Sequencing; + +public class DirectionalSequencerTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y); + private static Plate MakePlate(params Part[] parts) => TestHelpers.MakePlate(60, 120, parts); + + [Fact] + public void RightSide_SortsXDescending() + { + var a = MakePartAt(10, 5); + var b = MakePartAt(30, 5); + var c = MakePartAt(20, 5); + var plate = MakePlate(a, b, c); + + var sequencer = new RightSideSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + Assert.Same(b, result[0].Part); + Assert.Same(c, result[1].Part); + Assert.Same(a, result[2].Part); + } + + [Fact] + public void LeftSide_SortsXAscending() + { + var a = MakePartAt(10, 5); + var b = MakePartAt(30, 5); + var c = MakePartAt(20, 5); + var plate = MakePlate(a, b, c); + + var sequencer = new LeftSideSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + Assert.Same(a, result[0].Part); + Assert.Same(c, result[1].Part); + Assert.Same(b, result[2].Part); + } + + [Fact] + public void BottomSide_SortsYAscending() + { + var a = MakePartAt(5, 20); + var b = MakePartAt(5, 5); + var c = MakePartAt(5, 10); + var plate = MakePlate(a, b, c); + + var sequencer = new BottomSideSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + Assert.Same(b, result[0].Part); + Assert.Same(c, result[1].Part); + Assert.Same(a, result[2].Part); + } + + [Fact] + public void RightSide_TiesBrokenByPerpendicularAxis() + { + var a = MakePartAt(10, 20); + var b = MakePartAt(10, 5); + var plate = MakePlate(a, b); + + var sequencer = new RightSideSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + // Same X, tie broken by Y — lower Y first for RightSide + Assert.Same(b, result[0].Part); + Assert.Same(a, result[1].Part); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter DirectionalSequencerTests -v n` +Expected: FAIL — sequencer classes don't exist + +- [ ] **Step 3: Implement RightSideSequencer** + +```csharp +using System.Collections.Generic; +using System.Linq; + +namespace OpenNest.Engine.Sequencing +{ + public class RightSideSequencer : IPartSequencer + { + public List Sequence(IReadOnlyList parts, Plate plate) + { + return parts + .OrderByDescending(p => p.BoundingBox.Center.X) + .ThenBy(p => p.BoundingBox.Center.Y) + .Select(p => new SequencedPart { Part = p }) + .ToList(); + } + } +} +``` + +- [ ] **Step 4: Implement LeftSideSequencer** + +```csharp +using System.Collections.Generic; +using System.Linq; + +namespace OpenNest.Engine.Sequencing +{ + public class LeftSideSequencer : IPartSequencer + { + public List Sequence(IReadOnlyList parts, Plate plate) + { + return parts + .OrderBy(p => p.BoundingBox.Center.X) + .ThenBy(p => p.BoundingBox.Center.Y) + .Select(p => new SequencedPart { Part = p }) + .ToList(); + } + } +} +``` + +- [ ] **Step 5: Implement BottomSideSequencer** + +```csharp +using System.Collections.Generic; +using System.Linq; + +namespace OpenNest.Engine.Sequencing +{ + public class BottomSideSequencer : IPartSequencer + { + public List Sequence(IReadOnlyList parts, Plate plate) + { + return parts + .OrderBy(p => p.BoundingBox.Center.Y) + .ThenBy(p => p.BoundingBox.Center.X) + .Select(p => new SequencedPart { Part = p }) + .ToList(); + } + } +} +``` + +- [ ] **Step 6: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter DirectionalSequencerTests -v n` +Expected: 4 tests PASS + +- [ ] **Step 7: Commit** + +```bash +git add OpenNest.Engine/Sequencing/ OpenNest.Engine.Tests/Sequencing/ +git commit -m "feat: add directional part sequencers (RightSide, LeftSide, BottomSide)" +``` + +--- + +### Task 8: EdgeStartSequencer + +**Files:** +- Create: `OpenNest.Engine/Sequencing/EdgeStartSequencer.cs` +- Test: `OpenNest.Engine.Tests/Sequencing/EdgeStartSequencerTests.cs` + +- [ ] **Step 1: Write the test** + +```csharp +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.Sequencing; + +public class EdgeStartSequencerTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y); + + [Fact] + public void SortsByDistanceFromNearestEdge() + { + // Plate is 60x120, Q1 (origin at 0,0) + var plate = new Plate(60, 120); + var edgePart = MakePartAt(1, 1); // 1 unit from left and bottom edges + var centerPart = MakePartAt(25, 55); // ~25 from nearest edge + var midPart = MakePartAt(10, 10); // 10 from left and bottom edges + plate.Parts.Add(edgePart); + plate.Parts.Add(centerPart); + plate.Parts.Add(midPart); + + var sequencer = new EdgeStartSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + Assert.Same(edgePart, result[0].Part); + Assert.Same(midPart, result[1].Part); + Assert.Same(centerPart, result[2].Part); + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `dotnet test OpenNest.Engine.Tests --filter EdgeStartSequencerTests -v n` +Expected: FAIL + +- [ ] **Step 3: Implement EdgeStartSequencer** + +```csharp +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Sequencing +{ + public class EdgeStartSequencer : IPartSequencer + { + public List Sequence(IReadOnlyList parts, Plate plate) + { + var plateBox = plate.BoundingBox(false); + + return parts + .OrderBy(p => MinEdgeDistance(p.BoundingBox, plateBox)) + .ThenBy(p => p.BoundingBox.Center.X) + .Select(p => new SequencedPart { Part = p }) + .ToList(); + } + + private static double MinEdgeDistance(Box partBox, Box plateBox) + { + var center = partBox.Center; + var distLeft = System.Math.Abs(center.X - plateBox.Left); + var distRight = System.Math.Abs(center.X - plateBox.Right); + var distBottom = System.Math.Abs(center.Y - plateBox.Bottom); + var distTop = System.Math.Abs(center.Y - plateBox.Top); + return System.Math.Min(System.Math.Min(distLeft, distRight), + System.Math.Min(distBottom, distTop)); + } + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `dotnet test OpenNest.Engine.Tests --filter EdgeStartSequencerTests -v n` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Engine/Sequencing/EdgeStartSequencer.cs OpenNest.Engine.Tests/Sequencing/EdgeStartSequencerTests.cs +git commit -m "feat: add EdgeStartSequencer" +``` + +--- + +### Task 9: LeastCodeSequencer (nearest-neighbor + 2-opt) + +**Files:** +- Create: `OpenNest.Engine/Sequencing/LeastCodeSequencer.cs` +- Test: `OpenNest.Engine.Tests/Sequencing/LeastCodeSequencerTests.cs` + +- [ ] **Step 1: Write the tests** + +```csharp +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.Sequencing; + +public class LeastCodeSequencerTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y); + + [Fact] + public void NearestNeighbor_FromExitPoint() + { + // Q1 plate: exit point is (60, 120) (top-right corner) + var plate = new Plate(60, 120); + var farPart = MakePartAt(5, 5); + var nearPart = MakePartAt(55, 115); + plate.Parts.Add(farPart); + plate.Parts.Add(nearPart); + + var sequencer = new LeastCodeSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + // nearPart is closer to exit point (60,120), should come first + Assert.Same(nearPart, result[0].Part); + Assert.Same(farPart, result[1].Part); + } + + [Fact] + public void PreservesAllParts() + { + var plate = new Plate(60, 120); + for (var i = 0; i < 10; i++) + plate.Parts.Add(MakePartAt(i * 5, i * 10)); + + var sequencer = new LeastCodeSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + Assert.Equal(10, result.Count); + } + + [Fact] + public void TwoOpt_ImprovesSolution() + { + // Create a scenario where nearest-neighbor produces a crossing + // that 2-opt should fix + var plate = new Plate(100, 100); + // Exit point at (100, 100) for Q1 + // Parts arranged so NN would cross but 2-opt should uncross + var a = MakePartAt(90, 90); // nearest to exit + var b = MakePartAt(10, 80); // NN picks this 2nd (closest to a) + var c = MakePartAt(80, 10); // NN picks this 3rd — crosses! + var d = MakePartAt(5, 5); // last + plate.Parts.Add(a); + plate.Parts.Add(b); + plate.Parts.Add(c); + plate.Parts.Add(d); + + var sequencer = new LeastCodeSequencer(); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + // Just verify all parts present and result is deterministic + Assert.Equal(4, result.Count); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter LeastCodeSequencerTests -v n` +Expected: FAIL + +- [ ] **Step 3: Implement LeastCodeSequencer** + +The exit point logic (opposite corner from quadrant origin) is needed here and will also be used by `PlateProcessor`. Put it as a static helper. + +```csharp +using System.Collections.Generic; +using System.Linq; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Sequencing +{ + public class LeastCodeSequencer : IPartSequencer + { + private readonly int _maxIterations; + + public LeastCodeSequencer(int maxIterations = 100) + { + _maxIterations = maxIterations; + } + + public List Sequence(IReadOnlyList parts, Plate plate) + { + if (parts.Count == 0) + return new List(); + + var exitPoint = PlateHelper.GetExitPoint(plate); + var order = NearestNeighbor(parts, exitPoint); + TwoOpt(order, exitPoint); + + return order.Select(p => new SequencedPart { Part = p }).ToList(); + } + + private static List NearestNeighbor(IReadOnlyList parts, Vector startPoint) + { + var remaining = new List(parts); + var ordered = new List(); + var currentPoint = startPoint; + + while (remaining.Count > 0) + { + var nearest = remaining[0]; + var nearestDist = nearest.BoundingBox.Center.DistanceTo(currentPoint); + + for (var i = 1; i < remaining.Count; i++) + { + var dist = remaining[i].BoundingBox.Center.DistanceTo(currentPoint); + if (dist < nearestDist) + { + nearest = remaining[i]; + nearestDist = dist; + } + } + + ordered.Add(nearest); + remaining.Remove(nearest); + currentPoint = nearest.BoundingBox.Center; + } + + return ordered; + } + + private void TwoOpt(List order, Vector startPoint) + { + var improved = true; + var iterations = 0; + + while (improved && iterations < _maxIterations) + { + improved = false; + iterations++; + + for (var i = 0; i < order.Count - 1; i++) + { + for (var j = i + 1; j < order.Count; j++) + { + var delta = TwoOptDelta(order, startPoint, i, j); + if (delta < -OpenNest.Math.Tolerance.Epsilon) + { + Reverse(order, i, j); + improved = true; + } + } + } + } + } + + private static double TwoOptDelta(List order, Vector startPoint, int i, int j) + { + var prevI = i == 0 ? startPoint : order[i - 1].BoundingBox.Center; + var ci = order[i].BoundingBox.Center; + var cj = order[j].BoundingBox.Center; + var nextJ = j + 1 < order.Count ? order[j + 1].BoundingBox.Center : (Vector?)null; + + var oldDist = prevI.DistanceTo(ci); + var newDist = prevI.DistanceTo(cj); + + if (nextJ.HasValue) + { + oldDist += cj.DistanceTo(nextJ.Value); + newDist += ci.DistanceTo(nextJ.Value); + } + + return newDist - oldDist; + } + + private static void Reverse(List list, int start, int end) + { + while (start < end) + { + (list[start], list[end]) = (list[end], list[start]); + start++; + end--; + } + } + } +} +``` + +- [ ] **Step 4: Create PlateHelper for shared exit point logic** + +```csharp +using OpenNest.Geometry; + +namespace OpenNest.Engine.Sequencing +{ + internal static class PlateHelper + { + public static Vector GetExitPoint(Plate plate) + { + var w = plate.Size.Width; + var l = plate.Size.Length; + + return plate.Quadrant switch + { + 1 => new Vector(w, l), + 2 => new Vector(0, l), + 3 => new Vector(0, 0), + 4 => new Vector(w, 0), + _ => new Vector(w, l) + }; + } + } +} +``` + +- [ ] **Step 5: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter LeastCodeSequencerTests -v n` +Expected: 3 tests PASS + +- [ ] **Step 6: Commit** + +```bash +git add OpenNest.Engine/Sequencing/ OpenNest.Engine.Tests/Sequencing/ +git commit -m "feat: add LeastCodeSequencer with nearest-neighbor and 2-opt" +``` + +--- + +### Task 10: AdvancedSequencer + +**Files:** +- Create: `OpenNest.Engine/Sequencing/AdvancedSequencer.cs` +- Test: `OpenNest.Engine.Tests/Sequencing/AdvancedSequencerTests.cs` + +- [ ] **Step 1: Write the tests** + +```csharp +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.Sequencing; + +public class AdvancedSequencerTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y); + + [Fact] + public void GroupsIntoRows_NoAlternate() + { + // Two rows of parts with clear Y separation + // Q1 plate 100x100: exit point (100, 100), so leftToRight starts true + var plate = new Plate(100, 100); + var row1a = MakePartAt(10, 10); + var row1b = MakePartAt(30, 10); + var row2a = MakePartAt(10, 50); + var row2b = MakePartAt(30, 50); + plate.Parts.Add(row1a); + plate.Parts.Add(row1b); + plate.Parts.Add(row2a); + plate.Parts.Add(row2b); + + var parameters = new SequenceParameters + { + Method = SequenceMethod.Advanced, + MinDistanceBetweenRowsColumns = 5.0, + AlternateRowsColumns = false + }; + var sequencer = new AdvancedSequencer(parameters); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + // No alternation: both rows left-to-right (X ascending) + Assert.Same(row1a, result[0].Part); + Assert.Same(row1b, result[1].Part); + Assert.Same(row2a, result[2].Part); + Assert.Same(row2b, result[3].Part); + } + + [Fact] + public void SerpentineAlternatesDirection() + { + // Q1 plate 100x100: exit point (100, 100), so leftToRight starts true + var plate = new Plate(100, 100); + var r1Left = MakePartAt(10, 10); + var r1Right = MakePartAt(30, 10); + var r2Left = MakePartAt(10, 50); + var r2Right = MakePartAt(30, 50); + plate.Parts.Add(r1Left); + plate.Parts.Add(r1Right); + plate.Parts.Add(r2Left); + plate.Parts.Add(r2Right); + + var parameters = new SequenceParameters + { + Method = SequenceMethod.Advanced, + MinDistanceBetweenRowsColumns = 5.0, + AlternateRowsColumns = true + }; + var sequencer = new AdvancedSequencer(parameters); + var result = sequencer.Sequence(plate.Parts.ToList(), plate); + + // Row 1: left-to-right (X ascending) + Assert.Same(r1Left, result[0].Part); + Assert.Same(r1Right, result[1].Part); + // Row 2: right-to-left (X descending, alternated) + Assert.Same(r2Right, result[2].Part); + Assert.Same(r2Left, result[3].Part); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter AdvancedSequencerTests -v n` +Expected: FAIL + +- [ ] **Step 3: Implement AdvancedSequencer** + +```csharp +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Geometry; + +namespace OpenNest.Engine.Sequencing +{ + public class AdvancedSequencer : IPartSequencer + { + private readonly SequenceParameters _parameters; + + public AdvancedSequencer(SequenceParameters parameters) + { + _parameters = parameters; + } + + public List Sequence(IReadOnlyList parts, Plate plate) + { + if (parts.Count == 0) + return new List(); + + var rows = GroupIntoRows(parts); + var exitPoint = PlateHelper.GetExitPoint(plate); + + // Sort rows by Y (bottom to top for Q1) + rows.Sort((a, b) => a[0].BoundingBox.Center.Y.CompareTo(b[0].BoundingBox.Center.Y)); + + var result = new List(); + var leftToRight = exitPoint.X > plate.Size.Width * 0.5; + + for (var r = 0; r < rows.Count; r++) + { + var row = rows[r]; + + if (leftToRight) + row.Sort((a, b) => a.BoundingBox.Center.X.CompareTo(b.BoundingBox.Center.X)); + else + row.Sort((a, b) => b.BoundingBox.Center.X.CompareTo(a.BoundingBox.Center.X)); + + foreach (var part in row) + result.Add(new SequencedPart { Part = part }); + + if (_parameters.AlternateRowsColumns) + leftToRight = !leftToRight; + } + + return result; + } + + private List> GroupIntoRows(IReadOnlyList parts) + { + var sorted = parts.OrderBy(p => p.BoundingBox.Center.Y).ToList(); + var rows = new List>(); + var currentRow = new List { sorted[0] }; + + for (var i = 1; i < sorted.Count; i++) + { + var prevY = sorted[i - 1].BoundingBox.Center.Y; + var currY = sorted[i].BoundingBox.Center.Y; + + if (currY - prevY > _parameters.MinDistanceBetweenRowsColumns) + { + rows.Add(currentRow); + currentRow = new List(); + } + + currentRow.Add(sorted[i]); + } + + rows.Add(currentRow); + return rows; + } + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter AdvancedSequencerTests -v n` +Expected: 2 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Engine/Sequencing/AdvancedSequencer.cs OpenNest.Engine.Tests/Sequencing/AdvancedSequencerTests.cs +git commit -m "feat: add AdvancedSequencer with row grouping and serpentine" +``` + +--- + +### Task 11: PartSequencerFactory + +**Files:** +- Create: `OpenNest.Engine/Sequencing/PartSequencerFactory.cs` +- Test: `OpenNest.Engine.Tests/Sequencing/PartSequencerFactoryTests.cs` + +- [ ] **Step 1: Write the tests** + +```csharp +using System; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine.Sequencing; +using Xunit; + +namespace OpenNest.Engine.Tests.Sequencing; + +public class PartSequencerFactoryTests +{ + [Theory] + [InlineData(SequenceMethod.RightSide, typeof(RightSideSequencer))] + [InlineData(SequenceMethod.LeftSide, typeof(LeftSideSequencer))] + [InlineData(SequenceMethod.BottomSide, typeof(BottomSideSequencer))] + [InlineData(SequenceMethod.EdgeStart, typeof(EdgeStartSequencer))] + [InlineData(SequenceMethod.LeastCode, typeof(LeastCodeSequencer))] + [InlineData(SequenceMethod.Advanced, typeof(AdvancedSequencer))] + public void Create_ReturnsCorrectType(SequenceMethod method, Type expectedType) + { + var parameters = new SequenceParameters { Method = method }; + var sequencer = PartSequencerFactory.Create(parameters); + Assert.IsType(expectedType, sequencer); + } + + [Fact] + public void Create_RightSideAlt_Throws() + { + var parameters = new SequenceParameters { Method = SequenceMethod.RightSideAlt }; + Assert.Throws(() => PartSequencerFactory.Create(parameters)); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter PartSequencerFactoryTests -v n` +Expected: FAIL + +- [ ] **Step 3: Implement PartSequencerFactory** + +```csharp +using System; +using OpenNest.CNC.CuttingStrategy; + +namespace OpenNest.Engine.Sequencing +{ + public static class PartSequencerFactory + { + public static IPartSequencer Create(SequenceParameters parameters) + { + return parameters.Method switch + { + SequenceMethod.RightSide => new RightSideSequencer(), + SequenceMethod.LeftSide => new LeftSideSequencer(), + SequenceMethod.BottomSide => new BottomSideSequencer(), + SequenceMethod.EdgeStart => new EdgeStartSequencer(), + SequenceMethod.LeastCode => new LeastCodeSequencer(), + SequenceMethod.Advanced => new AdvancedSequencer(parameters), + _ => throw new NotSupportedException($"SequenceMethod {parameters.Method} is not supported") + }; + } + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter PartSequencerFactoryTests -v n` +Expected: 7 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Engine/Sequencing/PartSequencerFactory.cs OpenNest.Engine.Tests/Sequencing/PartSequencerFactoryTests.cs +git commit -m "feat: add PartSequencerFactory" +``` + +--- + +## Chunk 3: Rapid Planning and Orchestrator + +### Task 12: IRapidPlanner, RapidPath, and SafeHeightRapidPlanner + +**Files:** +- Create: `OpenNest.Engine/RapidPlanning/IRapidPlanner.cs` +- Create: `OpenNest.Engine/RapidPlanning/RapidPath.cs` +- Create: `OpenNest.Engine/RapidPlanning/SafeHeightRapidPlanner.cs` +- Test: `OpenNest.Engine.Tests/RapidPlanning/SafeHeightRapidPlannerTests.cs` + +- [ ] **Step 1: Write the test** + +```csharp +using System.Collections.Generic; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.RapidPlanning; + +public class SafeHeightRapidPlannerTests +{ + [Fact] + public void AlwaysReturnsHeadUp() + { + var planner = new SafeHeightRapidPlanner(); + var from = new Vector(10, 10); + var to = new Vector(50, 50); + var cutAreas = new List(); + + var result = planner.Plan(from, to, cutAreas); + + Assert.True(result.HeadUp); + Assert.Empty(result.Waypoints); + } + + [Fact] + public void ReturnsHeadUp_EvenWithCutAreas() + { + var planner = new SafeHeightRapidPlanner(); + var from = new Vector(0, 0); + var to = new Vector(10, 10); + + // Add a cut area (doesn't matter — always head up) + var shape = new Shape(); + shape.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20))); + var cutAreas = new List { shape }; + + var result = planner.Plan(from, to, cutAreas); + + Assert.True(result.HeadUp); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter SafeHeightRapidPlannerTests -v n` +Expected: FAIL + +- [ ] **Step 3: Create IRapidPlanner.cs** + +```csharp +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest.Engine.RapidPlanning +{ + public interface IRapidPlanner + { + RapidPath Plan(Vector from, Vector to, IReadOnlyList cutAreas); + } +} +``` + +- [ ] **Step 4: Create RapidPath.cs** + +```csharp +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest.Engine.RapidPlanning +{ + public readonly struct RapidPath + { + public bool HeadUp { get; init; } + public List Waypoints { get; init; } + } +} +``` + +- [ ] **Step 5: Create SafeHeightRapidPlanner.cs** + +```csharp +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest.Engine.RapidPlanning +{ + public class SafeHeightRapidPlanner : IRapidPlanner + { + public RapidPath Plan(Vector from, Vector to, IReadOnlyList cutAreas) + { + return new RapidPath + { + HeadUp = true, + Waypoints = new List() + }; + } + } +} +``` + +- [ ] **Step 6: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter SafeHeightRapidPlannerTests -v n` +Expected: 2 tests PASS + +- [ ] **Step 7: Commit** + +```bash +git add OpenNest.Engine/RapidPlanning/ OpenNest.Engine.Tests/RapidPlanning/ +git commit -m "feat: add IRapidPlanner, RapidPath, and SafeHeightRapidPlanner" +``` + +--- + +### Task 13: DirectRapidPlanner + +**Files:** +- Create: `OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs` +- Test: `OpenNest.Engine.Tests/RapidPlanning/DirectRapidPlannerTests.cs` + +- [ ] **Step 1: Write the tests** + +```csharp +using System.Collections.Generic; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests.RapidPlanning; + +public class DirectRapidPlannerTests +{ + [Fact] + public void NoCutAreas_ReturnsHeadDown() + { + var planner = new DirectRapidPlanner(); + var result = planner.Plan(new Vector(0, 0), new Vector(10, 10), new List()); + + Assert.False(result.HeadUp); + Assert.Empty(result.Waypoints); + } + + [Fact] + public void ClearPath_ReturnsHeadDown() + { + var planner = new DirectRapidPlanner(); + + // Cut area is off to the side — path doesn't cross it + var cutArea = new Shape(); + cutArea.Entities.Add(new Line(new Vector(50, 0), new Vector(50, 10))); + cutArea.Entities.Add(new Line(new Vector(50, 10), new Vector(60, 10))); + cutArea.Entities.Add(new Line(new Vector(60, 10), new Vector(60, 0))); + cutArea.Entities.Add(new Line(new Vector(60, 0), new Vector(50, 0))); + + var result = planner.Plan( + new Vector(0, 0), new Vector(10, 10), + new List { cutArea }); + + Assert.False(result.HeadUp); + } + + [Fact] + public void BlockedPath_ReturnsHeadUp() + { + var planner = new DirectRapidPlanner(); + + // Cut area directly between from and to + var cutArea = new Shape(); + cutArea.Entities.Add(new Line(new Vector(5, 0), new Vector(5, 20))); + cutArea.Entities.Add(new Line(new Vector(5, 20), new Vector(6, 20))); + cutArea.Entities.Add(new Line(new Vector(6, 20), new Vector(6, 0))); + cutArea.Entities.Add(new Line(new Vector(6, 0), new Vector(5, 0))); + + var result = planner.Plan( + new Vector(0, 10), new Vector(10, 10), + new List { cutArea }); + + Assert.True(result.HeadUp); + Assert.Empty(result.Waypoints); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter DirectRapidPlannerTests -v n` +Expected: FAIL + +- [ ] **Step 3: Implement DirectRapidPlanner** + +Uses `Shape.Intersects(Line)` — a public method on `Shape` that delegates to `Intersect.Intersects(Line, Shape, out pts)`. + +```csharp +using System.Collections.Generic; +using OpenNest.Geometry; + +namespace OpenNest.Engine.RapidPlanning +{ + public class DirectRapidPlanner : IRapidPlanner + { + public RapidPath Plan(Vector from, Vector to, IReadOnlyList cutAreas) + { + var travelLine = new Line(from, to); + + foreach (var cutArea in cutAreas) + { + if (cutArea.Intersects(travelLine)) + { + return new RapidPath + { + HeadUp = true, + Waypoints = new List() + }; + } + } + + return new RapidPath + { + HeadUp = false, + Waypoints = new List() + }; + } + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter DirectRapidPlannerTests -v n` +Expected: 3 tests PASS + +- [ ] **Step 5: Commit** + +```bash +git add OpenNest.Engine/RapidPlanning/DirectRapidPlanner.cs OpenNest.Engine.Tests/RapidPlanning/DirectRapidPlannerTests.cs +git commit -m "feat: add DirectRapidPlanner with line-shape intersection check" +``` + +--- + +### Task 14: PlateResult and ProcessedPart + +**Files:** +- Create: `OpenNest.Engine/PlateResult.cs` + +- [ ] **Step 1: Create PlateResult.cs** + +```csharp +using System.Collections.Generic; +using OpenNest.CNC; +using OpenNest.Engine.RapidPlanning; + +namespace OpenNest.Engine +{ + public class PlateResult + { + public List Parts { get; init; } + } + + public readonly struct ProcessedPart + { + public Part Part { get; init; } + public Program ProcessedProgram { get; init; } + public RapidPath RapidPath { get; init; } + } +} +``` + +- [ ] **Step 2: Build to verify** + +Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj` +Expected: Build succeeded + +- [ ] **Step 3: Commit** + +```bash +git add OpenNest.Engine/PlateResult.cs +git commit -m "feat: add PlateResult and ProcessedPart" +``` + +--- + +### Task 15: PlateProcessor orchestrator + +**Files:** +- Create: `OpenNest.Engine/PlateProcessor.cs` +- Test: `OpenNest.Engine.Tests/PlateProcessorTests.cs` + +- [ ] **Step 1: Write the tests** + +```csharp +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; +using Xunit; + +namespace OpenNest.Engine.Tests; + +public class PlateProcessorTests +{ + private static Part MakePartAt(double x, double y) => TestHelpers.MakePartAt(x, y, size: 2); + + [Fact] + public void Process_ReturnsAllParts() + { + var plate = new Plate(60, 120); + plate.Parts.Add(MakePartAt(10, 10)); + plate.Parts.Add(MakePartAt(30, 30)); + plate.Parts.Add(MakePartAt(50, 50)); + + var processor = new PlateProcessor + { + Sequencer = new RightSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Equal(3, result.Parts.Count); + } + + [Fact] + public void Process_PreservesSequenceOrder() + { + var plate = new Plate(60, 120); + var left = MakePartAt(5, 10); + var right = MakePartAt(50, 10); + plate.Parts.Add(left); + plate.Parts.Add(right); + + var processor = new PlateProcessor + { + Sequencer = new RightSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + // RightSide sorts X descending — right part first + Assert.Same(right, result.Parts[0].Part); + Assert.Same(left, result.Parts[1].Part); + } + + [Fact] + public void Process_SkipsCuttingStrategy_WhenManualLeadIns() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + part.HasManualLeadIns = true; + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + CuttingStrategy = new ContourCuttingStrategy + { + Parameters = new CuttingParameters() + }, + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + // Part program should be passed through unchanged + Assert.Same(part.Program, result.Parts[0].ProcessedProgram); + } + + [Fact] + public void Process_DoesNotMutatePart() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + var originalProgram = part.Program; + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + // Part.Program should be untouched + Assert.Same(originalProgram, part.Program); + } + + [Fact] + public void Process_NoCuttingStrategy_PassesProgramThrough() + { + var plate = new Plate(60, 120); + var part = MakePartAt(10, 10); + plate.Parts.Add(part); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + // No CuttingStrategy set + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Same(part.Program, result.Parts[0].ProcessedProgram); + } + + [Fact] + public void Process_EmptyPlate_ReturnsEmptyResult() + { + var plate = new Plate(60, 120); + + var processor = new PlateProcessor + { + Sequencer = new LeftSideSequencer(), + RapidPlanner = new SafeHeightRapidPlanner() + }; + + var result = processor.Process(plate); + + Assert.Empty(result.Parts); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test OpenNest.Engine.Tests --filter PlateProcessorTests -v n` +Expected: FAIL — `PlateProcessor` class doesn't exist + +- [ ] **Step 3: Implement PlateProcessor** + +```csharp +using System.Collections.Generic; +using System.Linq; +using OpenNest.CNC; +using OpenNest.CNC.CuttingStrategy; +using OpenNest.Engine.RapidPlanning; +using OpenNest.Engine.Sequencing; +using OpenNest.Geometry; + +namespace OpenNest.Engine +{ + public class PlateProcessor + { + public IPartSequencer Sequencer { get; set; } + public ContourCuttingStrategy CuttingStrategy { get; set; } + public IRapidPlanner RapidPlanner { get; set; } + + public PlateResult Process(Plate plate) + { + var ordered = Sequencer.Sequence(plate.Parts.ToList(), plate); + + var results = new List(); + var cutAreas = new List(); + var currentPoint = PlateHelper.GetExitPoint(plate); + + foreach (var sequenced in ordered) + { + var part = sequenced.Part; + var localApproach = ToPartLocal(currentPoint, part); + + CuttingResult cutResult; + if (!part.HasManualLeadIns && CuttingStrategy != null) + { + cutResult = CuttingStrategy.Apply(part.Program, localApproach); + } + else + { + cutResult = new CuttingResult + { + Program = part.Program, + LastCutPoint = GetProgramEndPoint(part.Program) + }; + } + + var piercePoint = ToPlateSpace(GetProgramStartPoint(cutResult.Program), part); + var rapid = RapidPlanner.Plan(currentPoint, piercePoint, cutAreas); + + results.Add(new ProcessedPart + { + Part = part, + ProcessedProgram = cutResult.Program, + RapidPath = rapid + }); + + var perimeter = GetPartPerimeter(part); + if (perimeter != null) + cutAreas.Add(perimeter); + + currentPoint = ToPlateSpace(cutResult.LastCutPoint, part); + } + + return new PlateResult { Parts = results }; + } + + private static Vector ToPartLocal(Vector platePoint, Part part) + { + return platePoint - part.Location; + } + + private static Vector ToPlateSpace(Vector localPoint, Part part) + { + return localPoint + part.Location; + } + + private static Vector GetProgramStartPoint(Program program) + { + var firstMove = program.Codes.OfType().FirstOrDefault(); + return firstMove?.EndPoint ?? new Vector(); + } + + private static Vector GetProgramEndPoint(Program program) + { + var lastMove = program.Codes.OfType().LastOrDefault(); + return lastMove?.EndPoint ?? new Vector(); + } + + private static Shape GetPartPerimeter(Part part) + { + var entities = part.Program.ToGeometry(); + if (entities == null || entities.Count == 0) + return null; + + var profile = new ShapeProfile(entities); + if (profile.Perimeter == null) + return null; + + var perimeter = profile.Perimeter; + perimeter.Offset(part.Location); + return perimeter; + } + } +} +``` + +Note: `PlateHelper.GetExitPoint` is already defined in Task 8. If `PlateHelper` was created in the `Sequencing` namespace, make it `internal` and accessible via `using OpenNest.Engine.Sequencing`. Alternatively, move it to the `OpenNest.Engine` namespace root. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test OpenNest.Engine.Tests --filter PlateProcessorTests -v n` +Expected: 6 tests PASS + +- [ ] **Step 5: Run all tests** + +Run: `dotnet test OpenNest.Engine.Tests -v n` +Expected: All tests PASS + +- [ ] **Step 6: Commit** + +```bash +git add OpenNest.Engine/PlateProcessor.cs OpenNest.Engine.Tests/PlateProcessorTests.cs +git commit -m "feat: add PlateProcessor orchestrator" +``` + +--- + +## Chunk 4: Final verification + +### Task 16: Full build and test run + +- [ ] **Step 1: Build entire solution** + +Run: `dotnet build OpenNest.sln` +Expected: Build succeeded with no errors + +- [ ] **Step 2: Run all tests** + +Run: `dotnet test OpenNest.Engine.Tests -v n` +Expected: All tests pass + +- [ ] **Step 3: Commit any remaining changes** + +If any files were missed, stage and commit them.