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>
6.9 KiB
Remnant Finder Design
Problem
Remnant detection is currently scattered across four places in the codebase, all using simple edge-strip heuristics that miss interior gaps and produce unreliable results:
Plate.GetRemnants()— finds strips along plate edges from global min/max of part bounding boxesDefaultNestEngine.TryRemainderImprovement()/TryStripRefill()/ClusterParts()— clusters parts into rows/columns and refills the last incomplete clusterFillScore.ComputeUsableRemnantArea()— estimates remnant area from rightmost/topmost part edges for fill scoringNestEngineBase.ComputeRemainderWithin()— picks the larger of one horizontal or vertical strip
These approaches only find single edge strips and cannot discover multiple or interior empty regions.
Solution
A standalone RemnantFinder class in OpenNest.Engine that uses edge projection to find all rectangular empty regions in a work area given a set of obstacle bounding boxes. This decouples remnant detection from the nesting engine and enables an iterative workflow:
- Fill an area
- Get all remnants
- Pick a remnant, fill it
- Get all remnants again (repeat)
API
RemnantFinder — OpenNest.Engine
public class RemnantFinder
{
// Constructor
public RemnantFinder(Box workArea, List<Box> obstacles = null);
// Mutable obstacle management
public List<Box> Obstacles { get; }
public void AddObstacle(Box obstacle);
public void AddObstacles(IEnumerable<Box> obstacles);
public void ClearObstacles();
// Core method
public List<Box> FindRemnants(double minDimension = 0);
// Convenience factory
public static RemnantFinder FromPlate(Plate plate);
}
FindRemnants Algorithm (Edge Projection)
- Collect all unique X coordinates from obstacle left/right edges + work area left/right.
- Collect all unique Y coordinates from obstacle bottom/top edges + work area bottom/top.
- Form candidate rectangles from every adjacent
(x[i], x[i+1])x(y[j], y[j+1])cell in the grid. - Filter out any candidate that overlaps any obstacle.
- Merge adjacent empty cells into larger rectangles — greedy row-first merge: scan cells left-to-right within each row and merge horizontally where cells share the same Y span, then merge vertically where resulting rectangles share the same X span and are adjacent in Y. This produces "good enough" large rectangles without requiring maximal rectangle decomposition.
- Filter by
minDimension— both width and height must be >= the threshold. - Return sorted by area descending.
FromPlate Factory
Extracts plate.WorkArea() as the work area and each part's bounding box offset by plate.PartSpacing as obstacles.
Scoping
The RemnantFinder operates on whatever work area it's given. When used within the strip nester or sub-region fills, pass the sub-region's work area and only the parts placed within it — not the full plate. This prevents remnants from spanning into unrelated layout regions.
Thread Safety
RemnantFinder is not thread-safe. Each thread/task should use its own instance. The FromPlate factory creates a snapshot of obstacles at construction time, so concurrent reads of the plate during construction should be avoided.
Removals
DefaultNestEngine
Remove the entire remainder phase:
TryRemainderImprovement()TryStripRefill()ClusterParts()NestPhase.Remainderreporting in bothFill()overrides
The engine's Fill() becomes single-pass. Iterative remnant filling is the caller's responsibility.
NestPhase.Remainder
Remove the Remainder value from the NestPhase enum. Clean up corresponding switch cases in:
NestEngineBase.FormatPhaseName()NestProgressForm.FormatPhase()
Plate
Remove GetRemnants() — fully replaced by RemnantFinder.FromPlate(plate).
FillScore
Remove remnant-related members:
MinRemnantDimensionconstantUsableRemnantAreapropertyComputeUsableRemnantArea()method- Remnant area from the
CompareToordering
Constructor simplifies from FillScore(int count, double usableRemnantArea, double density) to FillScore(int count, double density). The Compute factory method drops the ComputeUsableRemnantArea call accordingly.
NestProgress
Remove UsableRemnantArea property. Update NestEngineBase.ReportProgress() to stop computing/setting it. Update NestProgressForm to stop displaying it.
NestEngineBase
Replace ComputeRemainderWithin() with RemnantFinder in the Nest() method. The current Nest() fills an item, then calls ComputeRemainderWithin to get a single remainder box for the next item. Updated behavior: after filling, create a RemnantFinder with the current work area and all placed parts, call FindRemnants(), and use the largest remnant as the next work area. If no remnants exist, the fill loop stops.
StripNestResult
Remove RemnantBox property. The StripNestEngine.TryOrientation assignment to result.RemnantBox is removed — the value was stored but never read externally. The StripNestResult class itself is retained (it still carries Parts, StripBox, Score, Direction).
Caller Updates
NestingTools (MCP)
fill_remnants switches from plate.GetRemnants() to:
var finder = RemnantFinder.FromPlate(plate);
var remnants = finder.FindRemnants(minDimension);
InspectionTools (MCP)
get_plate_info switches from plate.GetRemnants() to RemnantFinder.FromPlate(plate).FindRemnants().
Debug Logging
DefaultNestEngine.FillWithPairs() logs bestScore.UsableRemnantArea — update to log only count and density after the FillScore simplification.
UI / Console callers
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 Work Area Visualization
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 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
The edge projection algorithm is embarrassingly parallel — each candidate rectangle's overlap check is independent. This makes it a natural fit for GPU acceleration via OpenNest.Gpu in the future.