Files
OpenNest/docs/superpowers/plans/2026-03-16-remnant-finder.md
AJ Isaacs 5873bff48b 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>
2026-03-16 12:38:44 -04:00

29 KiB

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

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
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

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
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:

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)
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:

Debug.WriteLine($"[FillWithPairs] Best pair result: {bestScore.Count} parts, remnant={bestScore.UsableRemnantArea:F1}, density={bestScore.Density:P1}");

to:

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
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:

public double UsableRemnantArea { get; set; }
  • Step 2: Remove UsableRemnantArea from ReportProgress

In NestEngineBase.cs at line 232, remove:

UsableRemnantArea = workArea.Area() - totalPartArea,
  • Step 3: Remove remnant display from NestProgressForm

In NestProgressForm.cs at line 40, remove:

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
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):

// 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):

// 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:

public override string Description => "Multi-phase nesting (Linear, Pairs, RectBestFit, Remainder)";

to:

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
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:

case NestPhase.Remainder: return "Remainder";
  • Step 3: Remove Remainder case from FormatPhase

In NestProgressForm.cs, remove:

case NestPhase.Remainder: return "Filling remainder...";
  • Step 4: Build to verify

Run: dotnet build OpenNest.sln Expected: No errors

  • Step 5: Commit
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:

var placedBox = parts.Cast<IBoundable>().GetBoundingBox();
workArea = ComputeRemainderWithin(workArea, placedBox, Plate.PartSpacing);

to:

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
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
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:

var remnants = plate.GetRemnants();

to:

var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants();
  • Step 2: Update InspectionTools.GetPlateInfo

At line 31, change:

var remnants = plate.GetRemnants();

to:

var remnants = RemnantFinder.FromPlate(plate).FindRemnants();
  • Step 3: Build to verify

Run: dotnet build OpenNest.sln Expected: Build succeeds

  • Step 4: Commit
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:

public Box RemnantBox { get; set; }
  • Step 2: Remove RemnantBox assignment in StripNestEngine

In StripNestEngine.cs at line 301, remove:

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
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?):

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
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:

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):

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:

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
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:

progressForm.UpdateProgress(p);
SetTemporaryParts(p.BestParts);

Add after SetTemporaryParts:

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);):

activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;

At line 896 (after activeForm.PlateView.SetTemporaryParts(p.BestParts);):

activeForm.PlateView.ActiveWorkArea = p.ActiveWorkArea;

At line 956 (after activeForm.PlateView.SetTemporaryParts(p.BestParts);):

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:

ActiveWorkArea = null;

Similarly in each MainForm nesting method's completion path:

activeForm.PlateView.ActiveWorkArea = null;
  • Step 4: Build to verify

Run: dotnet build OpenNest.sln Expected: Build succeeds

  • Step 5: Commit
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:

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
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
git add -A
git commit -m "chore: final cleanup after remnant finder extraction"