5873bff48b
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>
144 lines
6.9 KiB
Markdown
144 lines
6.9 KiB
Markdown
# 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 boxes
|
|
- `DefaultNestEngine.TryRemainderImprovement()` / `TryStripRefill()` / `ClusterParts()` — clusters parts into rows/columns and refills the last incomplete cluster
|
|
- `FillScore.ComputeUsableRemnantArea()` — estimates remnant area from rightmost/topmost part edges for fill scoring
|
|
- `NestEngineBase.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:
|
|
|
|
1. Fill an area
|
|
2. Get all remnants
|
|
3. Pick a remnant, fill it
|
|
4. Get all remnants again (repeat)
|
|
|
|
## API
|
|
|
|
### `RemnantFinder` — `OpenNest.Engine`
|
|
|
|
```csharp
|
|
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)
|
|
|
|
1. Collect all unique X coordinates from obstacle left/right edges + work area left/right.
|
|
2. Collect all unique Y coordinates from obstacle bottom/top edges + work area bottom/top.
|
|
3. Form candidate rectangles from every adjacent `(x[i], x[i+1])` x `(y[j], y[j+1])` cell in the grid.
|
|
4. Filter out any candidate that overlaps any obstacle.
|
|
5. 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.
|
|
6. Filter by `minDimension` — both width and height must be >= the threshold.
|
|
7. 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.Remainder` reporting in both `Fill()` 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:
|
|
- `MinRemnantDimension` constant
|
|
- `UsableRemnantArea` property
|
|
- `ComputeUsableRemnantArea()` method
|
|
- Remnant area from the `CompareTo` ordering
|
|
|
|
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:
|
|
```csharp
|
|
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.
|