docs: add lead item rotation design spec for strip nesting

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:12:17 -04:00
parent e969260f3d
commit 2cb2808c79

View File

@@ -0,0 +1,51 @@
# Lead Item Rotation for Strip Nesting
## Problem
`StripNestEngine.Nest()` sorts multi-quantity items by priority then area descending, always placing the largest-area drawing first. This fixed ordering can produce suboptimal layouts — a different starting drawing may create a tighter shrink region that leaves more usable remnant space for subsequent items.
## Solution
Try multiple candidate orderings by promoting each of the top N largest drawings to the front of the fill list. Run the full pipeline for each ordering, score the results, and keep the best.
## Candidate Generation
- Take the multi-quantity fill items (already filtered from singles)
- Identify the top `MaxLeadCandidates` (default 3) unique drawings by `Drawing.Area`, deduplicated by `Drawing` reference equality
- If there is only one unique drawing, skip the multi-ordering loop entirely (no-op — only one possible ordering)
- For each candidate drawing, create a reordered copy of the fill list where that drawing's items move to the front, preserving the original relative order for the remaining items
- The default ordering (largest area first) is always the first candidate, so the feature never regresses
- Lead promotion intentionally overrides the existing priority-then-area sort — the purpose is to explore whether a different lead item produces a better overall layout regardless of the default priority ordering
## Scoring
Use `FillScore` semantics for cross-ordering comparison: total placed part count as the primary metric, plate utilization (`sum(part.BaseDrawing.Area) / plate.WorkArea().Area()`) as tiebreaker. This is consistent with how `FillScore` works elsewhere in the codebase (count > density). Keep the first (default) result unless a later candidate is strictly better, so ties preserve the default ordering.
## Execution
- Run each candidate ordering sequentially through the existing pipeline: `IterativeShrinkFiller` → compaction → packing
- No added parallelism — each run already uses `Parallel.Invoke` internally for shrink axes
- `IterativeShrinkFiller.Fill` is a static method that creates fresh internal state (`RemnantFiller`, `placedSoFar` list) on each call, so the same input item list can be passed to multiple runs without interference. Neither `IterativeShrinkFiller` nor `RemnantFiller` mutate `NestItem.Quantity`. Each run also produces independent `Part` instances (created by `DefaultNestEngine.Fill`), so compaction mutations on one run's parts don't affect another.
- Only the winning result gets applied to the quantity deduction at the end of `Nest()`
## Progress Reporting
- Each candidate run reports progress normally (user sees live updates during shrink iterations)
- Between candidates, report a status message like "Lead item 2/3: [drawing name]"
- Only the final winning result is reported with `isOverallBest: true` to avoid the UI flashing between intermediate results
## Early Exit
- If a candidate meets all requested quantities **and** plate utilization exceeds 50%, skip remaining candidates
- Unlimited-quantity items (`Quantity <= 0`) never satisfy the quantity condition, so all candidates are always tried
- Cancellation token is respected — if cancelled mid-run, return the best result across all completed candidates
- The 50% threshold is a constant (`MinEarlyExitUtilization`) that can be tuned if typical nesting utilization proves higher or lower
## Scope
Changes are confined to `StripNestEngine.Nest()`. No modifications to `IterativeShrinkFiller`, `ShrinkFiller`, `DefaultNestEngine`, fill strategies, or the UI.
## Files
- Modify: `OpenNest.Engine/StripNestEngine.cs`
- Add test: `OpenNest.Tests/StripNestEngineTests.cs` (verify multiple orderings are tried, early exit works)