From df283d15dc84a24322982571ec7c73fb643b69ad Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 8 Mar 2026 16:12:51 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20update=20remnant=20fill=20investigation?= =?UTF-8?q?=20=E2=80=94=20engine=20needs=20more=20rotation=20candidates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine gets 7 parts where manual nesting gets 9, all at the same rotation. The fix is trying more rotations across all fill strategies. Co-Authored-By: Claude Opus 4.6 --- .../2026-03-08-remnant-fill-investigation.md | 67 ++++++------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/docs/plans/2026-03-08-remnant-fill-investigation.md b/docs/plans/2026-03-08-remnant-fill-investigation.md index 56ab4cb..9af066b 100644 --- a/docs/plans/2026-03-08-remnant-fill-investigation.md +++ b/docs/plans/2026-03-08-remnant-fill-investigation.md @@ -1,66 +1,37 @@ -# Remnant Fill Optimization Investigation +# Remnant Fill Optimization — Try More Rotations ## Problem -When filling remnant strips on a partially-nested plate, `NestEngine.Fill(NestItem, Box)` produces fewer parts than the UI's Ctrl+F fill. On a test case (N0308-008.zip, 36x36 plate, "Converto 3 YRD DUMPERSTER HINGE PLATE #2" 5.89x3.39): +`NestEngine.Fill(NestItem, Box)` gets 7 parts in a narrow remnant strip where manual nesting gets 9 — all at the same rotation. The engine simply isn't trying the rotation angle that fits best. -- **MCP fill_remnants**: 7 parts in the right-side strip -- **UI Ctrl+F (ActionClone.Fill)**: 9 parts in the same strip +## Test Case -## Test Setup +Load `C:/Users/AJ/Desktop/N0308-008.zip` — 75 parts on a 36x36 plate. Remnant strip is at `(31.0, 0.8) 4.7x35.0`. -Load `C:/Users/AJ/Desktop/N0308-008.zip` — 75 parts on plate 0, 5 columns of 15. The remnant strip is at `(31.0, 0.8) 4.7x35.0`. - -Use the OpenNest MCP tools to reproduce: ``` load_nest("C:/Users/AJ/Desktop/N0308-008.zip") -get_plate_info(0) → should show 75 parts, remnant at (31.0, 0.8) 4.7x35.0 -fill_remnants(0, "Converto 3 YRD DUMPERSTER HINGE PLATE #2") → gets 7 parts +fill_remnants(0, "Converto 3 YRD DUMPERSTER HINGE PLATE #2") → gets 7, should get 9 ``` -## Root Cause Analysis +## Root Cause -The MCP's `fill_remnants` calls `NestEngine.Fill(NestItem, Box)` which: +In `NestEngine.Fill(NestItem, Box)` (`OpenNest.Engine/NestEngine.cs:32-105`), the rotation candidates are: -1. Tries `FillLinear` with best rotation + 90° (and angle sweep if strip is narrow) -2. Tries `FillRectangleBestFit` (mixes horizontal/vertical in bin packing) -3. Tries `FillWithPairs` (paired part combinations via `BestFitCache`) -4. Picks the best result +1. `bestRotation` from `RotationAnalysis.FindBestRotation(item)` +2. `bestRotation + 90°` +3. If the strip is narrow relative to the part, a sweep every 5° from 0° to 175° -The UI's `ActionClone.Fill()` (`OpenNest/Actions/ActionClone.cs:171-201`) does something different: +The narrow-strip sweep triggers when `workAreaShortSide < partLongestSide`. For this strip (4.7" wide, part is 5.89x3.39), the short side is 4.7 and the part's longest side is 5.89, so the sweep **should** trigger. But the winning rotation may still not be found because: -1. Gets `Helper.GetLargestBoxVertically(pt, bounds, boxes)` and `GetLargestBoxHorizontally(pt, bounds, boxes)` from the cursor position -2. Picks the largest area -3. Calls `NestEngine.Fill(groupParts, bestArea)` — note: passes `List` not `NestItem` +- The sweep only runs on the `bestRotation`-normalized part dimensions +- `FillLinear` may not be optimal for all angles — check if `FillRectangleBestFit` and `FillWithPairs` are also tried at each sweep angle -The `Fill(List, Box)` overload uses `RotationAnalysis.FindHullEdgeAngles(groupParts)` which may produce different/better rotation candidates than `Fill(NestItem, Box)` which uses `RotationAnalysis.FindBestRotation(item)`. +## Fix -## Key Differences to Investigate +Ensure all three fill strategies (FillLinear, FillRectangleBestFit, FillWithPairs) are tried across the full set of rotation candidates, not just FillLinear. -### 1. Rotation candidate generation -- `Fill(NestItem, Box)` uses `FindBestRotation` → best angle + 90° + optional sweep -- `Fill(List, Box)` uses `FindHullEdgeAngles` → edge angles from convex hull -- The hull edge angles may include rotation angles that pack better in the narrow strip +## Files -### 2. Region selection -- UI uses cursor position to find the largest obstacle-free rectangle via `GetLargestBoxVertically`/`GetLargestBoxHorizontally` -- MCP uses `Plate.GetRemnants()` which returns edge strips from global part boundaries -- The UI region may differ in exact bounds from the remnant strip - -### 3. Part grouping -- UI's `ActionClone` can pass multi-part groups to `Fill(List, Box)`, enabling pattern-based tiling -- MCP passes single `NestItem` to `Fill(NestItem, Box)` - -## Files to Read - -- `OpenNest.Engine/NestEngine.cs` — both `Fill` overloads, `FillWithPairs`, `FillPattern` -- `OpenNest/Actions/ActionClone.cs:171-201` — the UI fill path -- `OpenNest.Engine/BestFit/RotationAnalysis.cs` — `FindBestRotation` vs `FindHullEdgeAngles` -- `OpenNest.Engine/FillLinear.cs` — the linear tiling engine -- `OpenNest.Core/Helper.cs:1098+` — `GetLargestBoxVertically`/`GetLargestBoxHorizontally` - -## Possible Fixes - -1. **Use hull edge angles in the NestItem overload** — merge rotation candidates from both `FindBestRotation` and `FindHullEdgeAngles` -2. **Improve GetRemnants** — instead of global edge strips, scan per-column to find the actual free space shape -3. **Add a smarter fill_remnants** — have the MCP tool use `GetLargestBox*` helpers to find free rectangles from multiple scan points, similar to how the UI does it +- `OpenNest.Engine/NestEngine.cs:32-105` — `Fill(NestItem, Box)` rotation logic +- `OpenNest.Engine/BestFit/RotationAnalysis.cs` — `FindBestRotation` +- `OpenNest.Engine/FillLinear.cs` — linear tiling