docs: update remnant fill investigation with root causes and fix details

Documents both issues found (narrow pair selection, incomplete pattern tiling)
and their fixes with before/after results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 18:33:10 -04:00
parent 435a08074b
commit 57c35450dd

View File

@@ -1,37 +1,86 @@
# Remnant Fill Optimization — Try More Rotations
# Remnant Fill Optimization — Investigation & Fix
## Problem
## Status: Both fixes done
`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.
## Problem 1 (FIXED): N0308-008 hinge plate remnant
## Test Case
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`.
`NestEngine.Fill(NestItem, Box)` got 7 parts in a 4.7x35.0 remnant strip where manual nesting gets 8 using a staggered brick pattern with alternating rotations.
### Test Case
```
load_nest("C:/Users/AJ/Desktop/N0308-008.zip")
fill_remnants(0, "Converto 3 YRD DUMPERSTER HINGE PLATE #2") → gets 7, should get 9
fill_remnants(0, "Converto 3 YRD DUMPERSTER HINGE PLATE #2") → was 7, now 9
```
Reference: `C:/Users/AJ/Desktop/N0308-008 - Copy.zip` (83 parts total, 8 in remnant).
## Root Cause
### Root Cause Found
- FillLinear rotation sweep works correctly — tested at 1° resolution, max is always 7
- The reference uses a **staggered pair pattern** (alternating 90°/270° rotations with horizontal offset)
- `FillWithPairs` generates ~2572 pair candidates but only tried top 50 sorted by minimum bounding area
- The winning pair ranked ~882nd — excluded by the `Take(50)` cutoff
- Top-50-by-area favors compact pairs for full-plate tiling, not narrow pairs suited for remnant strips
In `NestEngine.Fill(NestItem, Box)` (`OpenNest.Engine/NestEngine.cs:32-105`), the rotation candidates are:
### Fix Applied (in `OpenNest.Engine/NestEngine.cs`)
Added `SelectPairCandidates()` method:
1. Always includes standard top 50 pairs by area (no change for full-plate fills)
2. When work area is narrow (`shortSide < plateShortSide * 0.5`), includes **all** pairs whose shortest side fits the strip width
3. Updated `FillWithPairs()` to call `SelectPairCandidates()` instead of `Take(50)`
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°
### Results
- Remnant fill: 7 → **9 parts** (beats reference of 8, with partial pattern fill)
- Full-plate fill: 75 parts (unchanged, no regression)
- Remnant fill time: ~440ms
- Overlap check: PASS
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:
---
- 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
## Problem 2 (FIXED): N0308-017 PT02 remnant
## Fix
`N0308-017.zip` — 54 parts on a 144x72 plate. Two remnant areas:
- Remnant 0: `(119.57, 0.75) 24.13x70.95` — end-of-sheet strip
- Remnant 1: `(0.30, 66.15) 143.40x5.55` — bottom strip
Ensure all three fill strategies (FillLinear, FillRectangleBestFit, FillWithPairs) are tried across the full set of rotation candidates, not just FillLinear.
Drawing "4980 A24 PT02" has bbox 10.562x15.406. Engine filled 8 parts (2 cols × 4 rows) in remnant 0. Reference (`N0308-017 - Copy.zip`) has 10 parts using alternating 0°/180° rows.
## Files
### Investigation
1. Tested PT02 in remnant isolation → still 8 parts (not a multi-drawing ordering issue)
2. Brute-forced all 7224 pair candidates → max was 8 (no pair yields >8 with full-pattern-only tiling)
3. Tried finer offset resolution (0.05" step) across 0°/90°/180°/270° → still max 8
4. Analyzed reference nest (`N0308-017 - Copy.zip`): **64 PT02 parts on full plate, 10 in remnant area**
- `OpenNest.Engine/NestEngine.cs:32-105``Fill(NestItem, Box)` rotation logic
- `OpenNest.Engine/BestFit/RotationAnalysis.cs``FindBestRotation`
- `OpenNest.Engine/FillLinear.cs` — linear tiling
### Root Cause Found
The reference uses a 0°/170° staggered pair pattern that tiles in 5 rows × 2 columns:
- Rows at y: 0.75, 14.88, 28.40, 42.53, 56.06 (alternating 0° and 170°)
- Pattern copy distance: ~27.65" (pair tiling distance)
- 2 full pairs = 8 parts, top at ~58.56"
- Remaining height: 71.70 - 58.56 = ~13.14" — enough for 1 more row of 0° parts (height 15.41)
- **But `FillLinear.TilePattern` only placed complete pattern copies**, so the partial 3rd pair (just the 0° row) was never attempted
The pair candidate DID exist in the candidate set and was being tried. The issue was entirely in `FillLinear.TilePattern` — it tiled 2 complete pairs (8 parts) and stopped, even though 2 more individual parts from the next incomplete pair would still fit within the work area.
### Fix Applied (in `OpenNest.Engine/FillLinear.cs`)
Added **partial pattern fill** to `TilePattern()`:
- After tiling complete pattern copies, if the pattern has multiple parts, clone the next would-be copy
- Check each individual part's bounding box against the work area
- Add any that fit — guaranteed no overlaps by the copy distance computation
This is safe because:
- The copy distance ensures no overlaps between adjacent full copies → partial (subset) is also safe
- Parts within the same pattern copy don't overlap by construction
- Individual bounds checking catches parts that exceed the work area
### Results
- PT02 remnant fill: 8 → **10 parts** (matches reference)
- Hinge remnant fill: 8 → **9 parts** (bonus improvement from same fix)
- Full-plate fill: 75 parts (unchanged, no regression)
- All overlap checks: PASS
- PT02 fill time: ~32s (unchanged, dominated by pair candidate evaluation)
---
## Files Modified
- `OpenNest.Engine/NestEngine.cs` — Added `SelectPairCandidates()`, updated `FillWithPairs()`, rotation sweep (pre-existing change)
- `OpenNest.Engine/FillLinear.cs` — Added partial pattern fill to `TilePattern()`
## Temp Files to Clean Up
- `OpenNest.Test/` — temporary test console project (can be deleted or kept for debugging)