docs: add remnant finder implementation plan
16-task plan covering RemnantFinder class, FillScore simplification, remainder phase removal, caller updates, and PlateView ActiveWorkArea visualization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
998
docs/superpowers/plans/2026-03-16-remnant-finder.md
Normal file
998
docs/superpowers/plans/2026-03-16-remnant-finder.md
Normal file
@@ -0,0 +1,998 @@
|
||||
# Remnant Finder 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 remnant detection from the nesting engine into a standalone `RemnantFinder` class that finds all rectangular empty regions via edge projection, and visualize the active work area on the plate view.
|
||||
|
||||
**Architecture:** `RemnantFinder` is a mutable class in `OpenNest.Engine` that takes a work area + obstacle boxes and uses edge projection to find empty rectangles. The remainder phase is removed from `DefaultNestEngine`, making `Fill()` single-pass. `FillScore` drops remnant tracking. `PlateView` gains a dashed orange rectangle overlay for the active work area. `NestProgress` carries `ActiveWorkArea` so callers can show which region is currently being filled.
|
||||
|
||||
**Tech Stack:** .NET 8, C#, xUnit, WinForms (GDI+)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-16-remnant-finder-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: RemnantFinder Core
|
||||
|
||||
### Task 1: RemnantFinder — failing tests
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Tests/RemnantFinderTests.cs`
|
||||
|
||||
- [ ] **Step 1: Write failing tests for RemnantFinder**
|
||||
|
||||
```csharp
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Tests;
|
||||
|
||||
public class RemnantFinderTests
|
||||
{
|
||||
[Fact]
|
||||
public void EmptyPlate_ReturnsWholeWorkArea()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Single(remnants);
|
||||
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_InCorner_FindsLShapedRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 40, 40));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// Should find at least the right strip (60x100) and top strip (40x60)
|
||||
Assert.True(remnants.Count >= 2);
|
||||
|
||||
// Largest remnant should be the right strip
|
||||
var largest = remnants[0];
|
||||
Assert.Equal(60 * 100, largest.Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SingleObstacle_InCenter_FindsFourRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(30, 30, 40, 40));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// Should find remnants on all four sides
|
||||
Assert.True(remnants.Count >= 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MinDimension_FiltersSmallRemnants()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
// Obstacle leaves a 5-wide strip on the right
|
||||
finder.AddObstacle(new Box(0, 0, 95, 100));
|
||||
var all = finder.FindRemnants(0);
|
||||
var filtered = finder.FindRemnants(10);
|
||||
|
||||
Assert.True(all.Count > filtered.Count);
|
||||
foreach (var r in filtered)
|
||||
{
|
||||
Assert.True(r.Width >= 10);
|
||||
Assert.True(r.Length >= 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResultsSortedByAreaDescending()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
for (var i = 1; i < remnants.Count; i++)
|
||||
Assert.True(remnants[i - 1].Area() >= remnants[i].Area());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AddObstacle_UpdatesResults()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
var before = finder.FindRemnants();
|
||||
Assert.Single(before);
|
||||
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
var after = finder.FindRemnants();
|
||||
Assert.True(after.Count > 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearObstacles_ResetsToFullWorkArea()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 50, 50));
|
||||
finder.ClearObstacles();
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Single(remnants);
|
||||
Assert.Equal(100 * 100, remnants[0].Area(), 0.1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FullyCovered_ReturnsEmpty()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
finder.AddObstacle(new Box(0, 0, 100, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
Assert.Empty(remnants);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultipleObstacles_FindsGapBetween()
|
||||
{
|
||||
var finder = new RemnantFinder(new Box(0, 0, 100, 100));
|
||||
// Two obstacles with a 20-wide gap in the middle
|
||||
finder.AddObstacle(new Box(0, 0, 40, 100));
|
||||
finder.AddObstacle(new Box(60, 0, 40, 100));
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// Should find the 20x100 gap between the two obstacles
|
||||
var gap = remnants.FirstOrDefault(r =>
|
||||
r.Width >= 19.9 && r.Width <= 20.1 &&
|
||||
r.Length >= 99.9);
|
||||
Assert.NotNull(gap);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FromPlate_CreatesFinderWithPartsAsObstacles()
|
||||
{
|
||||
var plate = TestHelpers.MakePlate(60, 120,
|
||||
TestHelpers.MakePartAt(0, 0, 20));
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
var remnants = finder.FindRemnants();
|
||||
|
||||
// Should have remnants around the 20x20 part
|
||||
Assert.True(remnants.Count >= 1);
|
||||
// Largest remnant area should be less than full plate work area
|
||||
Assert.True(remnants[0].Area() < plate.WorkArea().Area());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they fail**
|
||||
|
||||
Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~RemnantFinderTests" -v minimal`
|
||||
Expected: FAIL — `RemnantFinder` class does not exist
|
||||
|
||||
- [ ] **Step 3: Commit failing tests**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Tests/RemnantFinderTests.cs
|
||||
git commit -m "test: add RemnantFinder tests (red)"
|
||||
```
|
||||
|
||||
### Task 2: RemnantFinder — implementation
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Engine/RemnantFinder.cs`
|
||||
|
||||
- [ ] **Step 1: Implement RemnantFinder**
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public class RemnantFinder
|
||||
{
|
||||
private readonly Box workArea;
|
||||
|
||||
public List<Box> Obstacles { get; } = new();
|
||||
|
||||
public RemnantFinder(Box workArea, List<Box> obstacles = null)
|
||||
{
|
||||
this.workArea = workArea;
|
||||
|
||||
if (obstacles != null)
|
||||
Obstacles.AddRange(obstacles);
|
||||
}
|
||||
|
||||
public void AddObstacle(Box obstacle) => Obstacles.Add(obstacle);
|
||||
|
||||
public void AddObstacles(IEnumerable<Box> obstacles) => Obstacles.AddRange(obstacles);
|
||||
|
||||
public void ClearObstacles() => Obstacles.Clear();
|
||||
|
||||
public List<Box> FindRemnants(double minDimension = 0)
|
||||
{
|
||||
// Step 1-2: Collect unique X and Y coordinates
|
||||
var xs = new SortedSet<double> { workArea.Left, workArea.Right };
|
||||
var ys = new SortedSet<double> { workArea.Bottom, workArea.Top };
|
||||
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
var clipped = ClipToWorkArea(obs);
|
||||
if (clipped.Width <= 0 || clipped.Length <= 0)
|
||||
continue;
|
||||
|
||||
xs.Add(clipped.Left);
|
||||
xs.Add(clipped.Right);
|
||||
ys.Add(clipped.Bottom);
|
||||
ys.Add(clipped.Top);
|
||||
}
|
||||
|
||||
var xList = xs.ToList();
|
||||
var yList = ys.ToList();
|
||||
|
||||
// Step 3-4: Build grid cells and mark empty ones
|
||||
var cols = xList.Count - 1;
|
||||
var rows = yList.Count - 1;
|
||||
|
||||
if (cols <= 0 || rows <= 0)
|
||||
return new List<Box>();
|
||||
|
||||
var empty = new bool[rows, cols];
|
||||
|
||||
for (var r = 0; r < rows; r++)
|
||||
{
|
||||
for (var c = 0; c < cols; c++)
|
||||
{
|
||||
var cell = new Box(xList[c], yList[r],
|
||||
xList[c + 1] - xList[c], yList[r + 1] - yList[r]);
|
||||
|
||||
empty[r, c] = !OverlapsAnyObstacle(cell);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Merge adjacent empty cells into larger rectangles
|
||||
var merged = MergeCells(empty, xList, yList, rows, cols);
|
||||
|
||||
// Step 6: Filter by minDimension
|
||||
var results = new List<Box>();
|
||||
|
||||
foreach (var box in merged)
|
||||
{
|
||||
if (box.Width >= minDimension && box.Length >= minDimension)
|
||||
results.Add(box);
|
||||
}
|
||||
|
||||
// Step 7: Sort by area descending
|
||||
results.Sort((a, b) => b.Area().CompareTo(a.Area()));
|
||||
return results;
|
||||
}
|
||||
|
||||
public static RemnantFinder FromPlate(Plate plate)
|
||||
{
|
||||
var obstacles = new List<Box>(plate.Parts.Count);
|
||||
|
||||
foreach (var part in plate.Parts)
|
||||
obstacles.Add(part.BoundingBox.Offset(plate.PartSpacing));
|
||||
|
||||
return new RemnantFinder(plate.WorkArea(), obstacles);
|
||||
}
|
||||
|
||||
private Box ClipToWorkArea(Box obs)
|
||||
{
|
||||
var left = System.Math.Max(obs.Left, workArea.Left);
|
||||
var bottom = System.Math.Max(obs.Bottom, workArea.Bottom);
|
||||
var right = System.Math.Min(obs.Right, workArea.Right);
|
||||
var top = System.Math.Min(obs.Top, workArea.Top);
|
||||
|
||||
if (right <= left || top <= bottom)
|
||||
return Box.Empty;
|
||||
|
||||
return new Box(left, bottom, right - left, top - bottom);
|
||||
}
|
||||
|
||||
private bool OverlapsAnyObstacle(Box cell)
|
||||
{
|
||||
foreach (var obs in Obstacles)
|
||||
{
|
||||
var clipped = ClipToWorkArea(obs);
|
||||
|
||||
if (clipped.Width <= 0 || clipped.Length <= 0)
|
||||
continue;
|
||||
|
||||
if (cell.Left < clipped.Right &&
|
||||
cell.Right > clipped.Left &&
|
||||
cell.Bottom < clipped.Top &&
|
||||
cell.Top > clipped.Bottom)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<Box> MergeCells(bool[,] empty, List<double> xList, List<double> yList, int rows, int cols)
|
||||
{
|
||||
var used = new bool[rows, cols];
|
||||
var results = new List<Box>();
|
||||
|
||||
for (var r = 0; r < rows; r++)
|
||||
{
|
||||
for (var c = 0; c < cols; c++)
|
||||
{
|
||||
if (!empty[r, c] || used[r, c])
|
||||
continue;
|
||||
|
||||
// Expand right as far as possible
|
||||
var maxC = c;
|
||||
while (maxC + 1 < cols && empty[r, maxC + 1] && !used[r, maxC + 1])
|
||||
maxC++;
|
||||
|
||||
// Expand down as far as possible
|
||||
var maxR = r;
|
||||
while (maxR + 1 < rows)
|
||||
{
|
||||
var rowOk = true;
|
||||
for (var cc = c; cc <= maxC; cc++)
|
||||
{
|
||||
if (!empty[maxR + 1, cc] || used[maxR + 1, cc])
|
||||
{
|
||||
rowOk = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rowOk) break;
|
||||
maxR++;
|
||||
}
|
||||
|
||||
// Mark cells as used
|
||||
for (var rr = r; rr <= maxR; rr++)
|
||||
for (var cc = c; cc <= maxC; cc++)
|
||||
used[rr, cc] = true;
|
||||
|
||||
var box = new Box(
|
||||
xList[c], yList[r],
|
||||
xList[maxC + 1] - xList[c],
|
||||
yList[maxR + 1] - yList[r]);
|
||||
|
||||
results.Add(box);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run tests to verify they pass**
|
||||
|
||||
Run: `dotnet test OpenNest.Tests --filter "FullyQualifiedName~RemnantFinderTests" -v minimal`
|
||||
Expected: All PASS
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/RemnantFinder.cs
|
||||
git commit -m "feat: add RemnantFinder with edge projection algorithm"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: FillScore Simplification and Remnant Cleanup
|
||||
|
||||
### Task 3: Simplify FillScore — remove remnant tracking
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/FillScore.cs`
|
||||
|
||||
- [ ] **Step 1: Remove remnant-related members from FillScore**
|
||||
|
||||
Remove `MinRemnantDimension`, `UsableRemnantArea`, `ComputeUsableRemnantArea()`. Simplify constructor and `Compute()`. Update `CompareTo` to compare count then density (no remnant area).
|
||||
|
||||
New `FillScore.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public readonly struct FillScore : System.IComparable<FillScore>
|
||||
{
|
||||
public int Count { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total part area / bounding box area of all placed parts.
|
||||
/// </summary>
|
||||
public double Density { get; }
|
||||
|
||||
public FillScore(int count, double density)
|
||||
{
|
||||
Count = count;
|
||||
Density = density;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes a fill score from placed parts and the work area they were placed in.
|
||||
/// </summary>
|
||||
public static FillScore Compute(List<Part> parts, Box workArea)
|
||||
{
|
||||
if (parts == null || parts.Count == 0)
|
||||
return default;
|
||||
|
||||
var totalPartArea = 0.0;
|
||||
var minX = double.MaxValue;
|
||||
var minY = double.MaxValue;
|
||||
var maxX = double.MinValue;
|
||||
var maxY = double.MinValue;
|
||||
|
||||
foreach (var part in parts)
|
||||
{
|
||||
totalPartArea += part.BaseDrawing.Area;
|
||||
var bb = part.BoundingBox;
|
||||
|
||||
if (bb.Left < minX) minX = bb.Left;
|
||||
if (bb.Bottom < minY) minY = bb.Bottom;
|
||||
if (bb.Right > maxX) maxX = bb.Right;
|
||||
if (bb.Top > maxY) maxY = bb.Top;
|
||||
}
|
||||
|
||||
var bboxArea = (maxX - minX) * (maxY - minY);
|
||||
var density = bboxArea > 0 ? totalPartArea / bboxArea : 0;
|
||||
|
||||
return new FillScore(parts.Count, density);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lexicographic comparison: count, then density.
|
||||
/// </summary>
|
||||
public int CompareTo(FillScore other)
|
||||
{
|
||||
var c = Count.CompareTo(other.Count);
|
||||
|
||||
if (c != 0)
|
||||
return c;
|
||||
|
||||
return Density.CompareTo(other.Density);
|
||||
}
|
||||
|
||||
public static bool operator >(FillScore a, FillScore b) => a.CompareTo(b) > 0;
|
||||
public static bool operator <(FillScore a, FillScore b) => a.CompareTo(b) < 0;
|
||||
public static bool operator >=(FillScore a, FillScore b) => a.CompareTo(b) >= 0;
|
||||
public static bool operator <=(FillScore a, FillScore b) => a.CompareTo(b) <= 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit (build will not pass yet — remaining UsableRemnantArea references fixed in Tasks 4-5)**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/FillScore.cs
|
||||
git commit -m "refactor: simplify FillScore to count + density, remove remnant tracking"
|
||||
```
|
||||
|
||||
### Task 4: Update DefaultNestEngine debug logging
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/DefaultNestEngine.cs:456-459`
|
||||
|
||||
- [ ] **Step 1: Update FillWithPairs debug log**
|
||||
|
||||
At line 456, change:
|
||||
```csharp
|
||||
Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}");
|
||||
```
|
||||
to:
|
||||
```csharp
|
||||
Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, density={bestScore.Density:P1}");
|
||||
```
|
||||
|
||||
Also update the file-based debug log at lines 457-459 — change `bestScore.UsableRemnantArea` references similarly. If the file log references `UsableRemnantArea`, remove that interpolation.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/DefaultNestEngine.cs
|
||||
git commit -m "fix: update FillWithPairs debug logging after FillScore simplification"
|
||||
```
|
||||
|
||||
### Task 5: Remove NestProgress.UsableRemnantArea and UI references
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestProgress.cs:44`
|
||||
- Modify: `OpenNest.Engine/NestEngineBase.cs:232`
|
||||
- Modify: `OpenNest\Forms\NestProgressForm.cs:40`
|
||||
|
||||
- [ ] **Step 1: Remove UsableRemnantArea from NestProgress**
|
||||
|
||||
In `NestProgress.cs`, remove line 44:
|
||||
```csharp
|
||||
public double UsableRemnantArea { get; set; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove UsableRemnantArea from ReportProgress**
|
||||
|
||||
In `NestEngineBase.cs` at line 232, remove:
|
||||
```csharp
|
||||
UsableRemnantArea = workArea.Area() - totalPartArea,
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Remove remnant display from NestProgressForm**
|
||||
|
||||
In `NestProgressForm.cs` at line 40, remove:
|
||||
```csharp
|
||||
remnantValue.Text = $"{progress.UsableRemnantArea:F1} sq in";
|
||||
```
|
||||
|
||||
Also remove the `remnantValue` label and its corresponding "Remnant:" label from the form's Designer file (or set them to display something else if desired). If simpler, just remove the line that sets the text — the label will remain but show its default empty text.
|
||||
|
||||
- [ ] **Step 4: Build to verify all UsableRemnantArea references are resolved**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeds — all `UsableRemnantArea` references are now removed
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestProgress.cs OpenNest.Engine/NestEngineBase.cs OpenNest/Forms/NestProgressForm.cs
|
||||
git commit -m "refactor: remove UsableRemnantArea from NestProgress and UI"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Remove Remainder Phase from Engine
|
||||
|
||||
### Task 6: Remove remainder phase from DefaultNestEngine
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/DefaultNestEngine.cs`
|
||||
|
||||
- [ ] **Step 1: Remove TryRemainderImprovement calls from Fill() overrides**
|
||||
|
||||
In the first `Fill()` override (line 31), remove lines 40-53 (the remainder improvement block after `FindBestFill`):
|
||||
```csharp
|
||||
// Remove this entire block:
|
||||
if (!token.IsCancellationRequested)
|
||||
{
|
||||
var remainderSw = Stopwatch.StartNew();
|
||||
var improved = TryRemainderImprovement(item, workArea, best);
|
||||
// ... through to the closing brace
|
||||
}
|
||||
```
|
||||
|
||||
In the second `Fill()` override (line 118), remove lines 165-174 (the remainder improvement block inside the `if (groupParts.Count == 1)` block):
|
||||
```csharp
|
||||
// Remove this entire block:
|
||||
var improved = TryRemainderImprovement(nestItem, workArea, best);
|
||||
if (IsBetterFill(improved, best, workArea))
|
||||
{
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove TryRemainderImprovement, TryStripRefill, ClusterParts methods**
|
||||
|
||||
Remove the three private methods (lines 563-694):
|
||||
- `TryRemainderImprovement`
|
||||
- `TryStripRefill`
|
||||
- `ClusterParts`
|
||||
|
||||
- [ ] **Step 3: Update Description property**
|
||||
|
||||
Change:
|
||||
```csharp
|
||||
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)";
|
||||
```
|
||||
to:
|
||||
```csharp
|
||||
public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit)";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build and run tests**
|
||||
|
||||
Run: `dotnet build OpenNest.sln && dotnet test OpenNest.Tests -v minimal`
|
||||
Expected: Build succeeds, tests pass
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/DefaultNestEngine.cs
|
||||
git commit -m "refactor: remove remainder phase from DefaultNestEngine"
|
||||
```
|
||||
|
||||
### Task 7: Remove NestPhase.Remainder and cleanup
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestProgress.cs:11`
|
||||
- Modify: `OpenNest.Engine/NestEngineBase.cs:314`
|
||||
- Modify: `OpenNest\Forms\NestProgressForm.cs:100`
|
||||
|
||||
- [ ] **Step 1: Remove Remainder from NestPhase enum**
|
||||
|
||||
In `NestProgress.cs`, remove `Remainder` from the enum.
|
||||
|
||||
- [ ] **Step 2: Remove Remainder case from FormatPhaseName**
|
||||
|
||||
In `NestEngineBase.cs`, remove:
|
||||
```csharp
|
||||
case NestPhase.Remainder: return "Remainder";
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Remove Remainder case from FormatPhase**
|
||||
|
||||
In `NestProgressForm.cs`, remove:
|
||||
```csharp
|
||||
case NestPhase.Remainder: return "Filling remainder...";
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: No errors
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestProgress.cs OpenNest.Engine/NestEngineBase.cs OpenNest/Forms/NestProgressForm.cs
|
||||
git commit -m "refactor: remove NestPhase.Remainder enum value and switch cases"
|
||||
```
|
||||
|
||||
### Task 8: Remove ComputeRemainderWithin and update Nest()
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestEngineBase.cs:92,120-133`
|
||||
|
||||
- [ ] **Step 1: Replace ComputeRemainderWithin usage in Nest()**
|
||||
|
||||
At line 91-92, change:
|
||||
```csharp
|
||||
var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
|
||||
workArea = ComputeRemainderWithin(workArea, placedBox, Plate.PartSpacing);
|
||||
```
|
||||
to:
|
||||
```csharp
|
||||
var placedObstacles = parts.Select(p => p.BoundingBox.Offset(Plate.PartSpacing)).ToList();
|
||||
var finder = new RemnantFinder(workArea, placedObstacles);
|
||||
var remnants = finder.FindRemnants();
|
||||
if (remnants.Count == 0)
|
||||
break;
|
||||
workArea = remnants[0]; // Largest remnant
|
||||
```
|
||||
|
||||
Note: This is a behavioral improvement — the old code used a single merged bounding box and picked one strip. The new code finds per-part obstacles and discovers all gaps, using the largest. This may produce different (better) results for non-rectangular layouts.
|
||||
|
||||
- [ ] **Step 2: Remove ComputeRemainderWithin method**
|
||||
|
||||
Delete lines 120-133 (the `ComputeRemainderWithin` static method).
|
||||
|
||||
- [ ] **Step 3: Build and run tests**
|
||||
|
||||
Run: `dotnet build OpenNest.sln && dotnet test OpenNest.Tests -v minimal`
|
||||
Expected: Build succeeds, tests pass
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestEngineBase.cs
|
||||
git commit -m "refactor: replace ComputeRemainderWithin with RemnantFinder in Nest()"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: Remove Old Remnant Code and Update Callers
|
||||
|
||||
### Task 9: Remove Plate.GetRemnants()
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Core/Plate.cs:477-557`
|
||||
|
||||
- [ ] **Step 1: Remove GetRemnants method**
|
||||
|
||||
Delete the `GetRemnants()` method (lines 477-557, the XML doc comment through the closing brace).
|
||||
|
||||
- [ ] **Step 2: Build to check for remaining references**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Errors in `NestingTools.cs` and `InspectionTools.cs` (fixed in next task)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Core/Plate.cs
|
||||
git commit -m "refactor: remove Plate.GetRemnants(), replaced by RemnantFinder"
|
||||
```
|
||||
|
||||
### Task 10: Update MCP callers
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Mcp/Tools/NestingTools.cs:105`
|
||||
- Modify: `OpenNest.Mcp/Tools/InspectionTools.cs:31`
|
||||
|
||||
- [ ] **Step 1: Update NestingTools.FillRemnants**
|
||||
|
||||
At line 105, change:
|
||||
```csharp
|
||||
var remnants = plate.GetRemnants();
|
||||
```
|
||||
to:
|
||||
```csharp
|
||||
var finder = RemnantFinder.FromPlate(plate);
|
||||
var remnants = finder.FindRemnants();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update InspectionTools.GetPlateInfo**
|
||||
|
||||
At line 31, change:
|
||||
```csharp
|
||||
var remnants = plate.GetRemnants();
|
||||
```
|
||||
to:
|
||||
```csharp
|
||||
var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Build to verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeds
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Mcp/Tools/NestingTools.cs OpenNest.Mcp/Tools/InspectionTools.cs
|
||||
git commit -m "refactor: update MCP tools to use RemnantFinder"
|
||||
```
|
||||
|
||||
### Task 11: Remove StripNestResult.RemnantBox
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/StripNestResult.cs:10`
|
||||
- Modify: `OpenNest.Engine/StripNestEngine.cs:301`
|
||||
|
||||
- [ ] **Step 1: Remove RemnantBox property from StripNestResult**
|
||||
|
||||
In `StripNestResult.cs`, remove line 10:
|
||||
```csharp
|
||||
public Box RemnantBox { get; set; }
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Remove RemnantBox assignment in StripNestEngine**
|
||||
|
||||
In `StripNestEngine.cs` at line 301, remove:
|
||||
```csharp
|
||||
result.RemnantBox = remnantBox;
|
||||
```
|
||||
|
||||
Also check if the local `remnantBox` variable is now unused — if so, remove its declaration and computation too.
|
||||
|
||||
- [ ] **Step 3: Build and run tests**
|
||||
|
||||
Run: `dotnet build OpenNest.sln && dotnet test OpenNest.Tests -v minimal`
|
||||
Expected: Build succeeds, all tests pass
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/StripNestResult.cs OpenNest.Engine/StripNestEngine.cs
|
||||
git commit -m "refactor: remove StripNestResult.RemnantBox"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 5: PlateView Active Work Area Visualization
|
||||
|
||||
### Task 12: Add ActiveWorkArea to NestProgress
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestProgress.cs`
|
||||
|
||||
- [ ] **Step 1: Add ActiveWorkArea property to NestProgress**
|
||||
|
||||
`Box` is a reference type (class), so use `Box` directly (not `Box?`):
|
||||
|
||||
```csharp
|
||||
public Box ActiveWorkArea { get; set; }
|
||||
```
|
||||
|
||||
`NestProgress.cs` already has `using OpenNest.Geometry;` via the `Box` usage in existing properties. If not, add it.
|
||||
|
||||
- [ ] **Step 2: Build to verify**
|
||||
|
||||
Run: `dotnet build OpenNest.Engine`
|
||||
Expected: Build succeeds
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestProgress.cs
|
||||
git commit -m "feat: add ActiveWorkArea property to NestProgress"
|
||||
```
|
||||
|
||||
### Task 13: Draw active work area on PlateView
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest\Controls\PlateView.cs`
|
||||
|
||||
- [ ] **Step 1: Add ActiveWorkArea property**
|
||||
|
||||
Add a field and property to `PlateView`:
|
||||
```csharp
|
||||
private Box activeWorkArea;
|
||||
|
||||
public Box ActiveWorkArea
|
||||
{
|
||||
get => activeWorkArea;
|
||||
set
|
||||
{
|
||||
activeWorkArea = value;
|
||||
Invalidate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add DrawActiveWorkArea method**
|
||||
|
||||
Add a private method to draw the dashed orange rectangle, using the same coordinate transform pattern as `DrawBox` (line 591-601):
|
||||
|
||||
```csharp
|
||||
private void DrawActiveWorkArea(Graphics g)
|
||||
{
|
||||
if (activeWorkArea == null)
|
||||
return;
|
||||
|
||||
var rect = new RectangleF
|
||||
{
|
||||
Location = PointWorldToGraph(activeWorkArea.Location),
|
||||
Width = LengthWorldToGui(activeWorkArea.Width),
|
||||
Height = LengthWorldToGui(activeWorkArea.Length)
|
||||
};
|
||||
rect.Y -= rect.Height;
|
||||
|
||||
using var pen = new Pen(Color.Orange, 2f)
|
||||
{
|
||||
DashStyle = DashStyle.Dash
|
||||
};
|
||||
g.DrawRectangle(pen, rect.X, rect.Y, rect.Width, rect.Height);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Call DrawActiveWorkArea in OnPaint**
|
||||
|
||||
In `OnPaint` (line 363-364), add the call after `DrawParts`:
|
||||
```csharp
|
||||
DrawPlate(e.Graphics);
|
||||
DrawParts(e.Graphics);
|
||||
DrawActiveWorkArea(e.Graphics);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeds
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest/Controls/PlateView.cs
|
||||
git commit -m "feat: draw active work area as dashed orange rectangle on PlateView"
|
||||
```
|
||||
|
||||
### Task 14: Wire ActiveWorkArea through progress callbacks
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest\Controls\PlateView.cs:828-829`
|
||||
- Modify: `OpenNest\Forms\MainForm.cs:760-761,895-896,955-956`
|
||||
|
||||
The `PlateView` and `MainForm` both have progress callbacks that already set `SetTemporaryParts`. Add `ActiveWorkArea` alongside those.
|
||||
|
||||
- [ ] **Step 1: Update PlateView.FillWithProgress callback**
|
||||
|
||||
At `PlateView.cs` line 828-829, the callback currently does:
|
||||
```csharp
|
||||
progressForm.UpdateProgress(p);
|
||||
SetTemporaryParts(p.BestParts);
|
||||
```
|
||||
|
||||
Add after `SetTemporaryParts`:
|
||||
```csharp
|
||||
ActiveWorkArea = p.ActiveWorkArea;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update MainForm progress callbacks**
|
||||
|
||||
There are three progress callback sites in `MainForm.cs`. At each one, after the `SetTemporaryParts` call, add:
|
||||
|
||||
At line 761 (after `activeForm.PlateView.SetTemporaryParts(p.BestParts);`):
|
||||
```csharp
|
||||
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
||||
```
|
||||
|
||||
At line 896 (after `activeForm.PlateView.SetTemporaryParts(p.BestParts);`):
|
||||
```csharp
|
||||
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
||||
```
|
||||
|
||||
At line 956 (after `activeForm.PlateView.SetTemporaryParts(p.BestParts);`):
|
||||
```csharp
|
||||
activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Clear ActiveWorkArea when nesting completes**
|
||||
|
||||
In each nesting method's completion/cleanup path, clear the work area overlay. In `PlateView.cs` after the fill task completes (near `progressForm.ShowCompleted()`), add:
|
||||
```csharp
|
||||
ActiveWorkArea = null;
|
||||
```
|
||||
|
||||
Similarly in each `MainForm` nesting method's completion path:
|
||||
```csharp
|
||||
activeForm.PlateView.ActiveWorkArea = null;
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Build to verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeds
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest/Controls/PlateView.cs OpenNest/Forms/MainForm.cs
|
||||
git commit -m "feat: wire ActiveWorkArea from NestProgress to PlateView"
|
||||
```
|
||||
|
||||
### Task 15: Set ActiveWorkArea in Nest() method
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestEngineBase.cs` (the `Nest()` method updated in Task 8)
|
||||
|
||||
- [ ] **Step 1: Report ActiveWorkArea in Nest() progress**
|
||||
|
||||
In the `Nest()` method, after picking the largest remnant as the next work area (Task 8's change), set `ActiveWorkArea` on the progress report. Find the `ReportProgress` call inside or near the fill loop and ensure the progress object carries the current `workArea`.
|
||||
|
||||
The simplest approach: pass the work area through `ReportProgress`. In `NestEngineBase.ReportProgress` (the static helper), add `ActiveWorkArea = workArea` to the `NestProgress` initializer:
|
||||
|
||||
In `ReportProgress`, add to the `new NestProgress { ... }` block:
|
||||
```csharp
|
||||
ActiveWorkArea = workArea,
|
||||
```
|
||||
|
||||
This ensures every progress report includes the current work area being filled.
|
||||
|
||||
- [ ] **Step 2: Build and run tests**
|
||||
|
||||
Run: `dotnet build OpenNest.sln && dotnet test OpenNest.Tests -v minimal`
|
||||
Expected: Build succeeds, all tests pass
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestEngineBase.cs
|
||||
git commit -m "feat: report ActiveWorkArea in NestProgress from ReportProgress"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 6: Final Verification
|
||||
|
||||
### Task 16: Full build and test
|
||||
|
||||
**Files:** None (verification only)
|
||||
|
||||
- [ ] **Step 1: Run full build**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: 0 errors, 0 warnings related to remnant code
|
||||
|
||||
- [ ] **Step 2: Run all tests**
|
||||
|
||||
Run: `dotnet test OpenNest.Tests -v minimal`
|
||||
Expected: All tests pass including new `RemnantFinderTests`
|
||||
|
||||
- [ ] **Step 3: Verify no stale references**
|
||||
|
||||
Run: `grep -rn "GetRemnants\|ComputeRemainderWithin\|TryRemainderImprovement\|MinRemnantDimension\|UsableRemnantArea" --include="*.cs" .`
|
||||
Expected: No matches in source files (only in docs/specs/plans)
|
||||
|
||||
- [ ] **Step 4: Final commit if any fixups needed**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: final cleanup after remnant finder extraction"
|
||||
```
|
||||
@@ -132,11 +132,11 @@ var remnants = finder.FindRemnants(minDimension);
|
||||
|
||||
Any caller that previously relied on `TryRemainderImprovement` getting called automatically inside `Fill()` will need to implement the iterative loop: fill -> find remnants -> fill remnant -> repeat.
|
||||
|
||||
## PlateView Remnant Visualization
|
||||
## PlateView Work Area Visualization
|
||||
|
||||
When a remnant is being filled (during the iterative workflow), the `PlateView` control should display the active remnant's outline as a dashed rectangle in a contrasting color (orange). The outline persists while that remnant is being filled and disappears when the next remnant starts or the fill completes.
|
||||
When an area is being filled (during the iterative workflow), the `PlateView` control displays the active work area's outline as a dashed orange rectangle. The outline persists while that area is being filled and disappears when the fill completes.
|
||||
|
||||
**Implementation:** Add a `Box? ActiveRemnant` property to `PlateView`. When set, the paint handler draws a dashed rectangle at that location. The caller sets it before filling a remnant and clears it (sets to `null`) when done or moving to the next remnant. The `NestProgress` class gets a new `Box? ActiveRemnant` property so the progress pipeline can carry the current remnant box from the engine caller to the UI.
|
||||
**Implementation:** Add a `Box ActiveWorkArea` property to `PlateView` (`Box` is a reference type, so `null` means no overlay). When set, the paint handler draws a dashed rectangle at that location. The `NestProgress` class gets a new `Box ActiveWorkArea` property so the progress pipeline carries the current work area from the engine to the UI. The existing progress callbacks in `PlateView.FillWithProgress` and `MainForm` set `PlateView.ActiveWorkArea` from the progress object, alongside the existing `SetTemporaryParts` calls. `NestEngineBase.ReportProgress` populates `ActiveWorkArea` from its `workArea` parameter.
|
||||
|
||||
## Future
|
||||
|
||||
|
||||
Reference in New Issue
Block a user