Files
OpenNest/docs/superpowers/specs/2026-03-10-fill-score-design.md
2026-03-10 23:03:21 -04:00

90 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Fill Score Design
## Problem
The nesting engine compares fill results by raw part count, which causes it to reject denser pair patterns that would yield more parts after remainder filling.
**Concrete case:** Part 4980 A24 PT02 on a 60×120" plate:
- Wider pair (90°/270°): 5 rows × 9 = **45 parts** at grid stage, no room for remainder
- Tighter pair #1 (89.7°/269.7°): 4 rows × 9 = **36 parts** at grid stage, 15.7" remainder strip → **47 total possible**
The algorithm compares 45 vs 36 at the grid stage. Pattern #1 loses before its remainder strip is fully evaluated. Two contributing issues:
1. **Scoring:** `FillWithPairs` uses raw count (`count > best.Count`) with no tiebreaker for density or compactness
2. **Remainder rotation coverage:** `FillLinear.FillRemainingStrip` only tries rotations present in the seed pattern (89.7°/269.7°), missing the 0° rotation that fits 11 parts in the strip vs ~8 at the seed angles
## Design
### 1. FillScore Struct
A value type encapsulating fill quality with lexicographic comparison:
```
Priority 1: Part count (higher wins)
Priority 2: Usable remnant area (higher wins) — largest remnant with short side ≥ MinRemnantDimension
Priority 3: Density (higher wins) — sum of part areas / bounding box area of placed parts
```
**Location:** `OpenNest.Engine/FillScore.cs`
**Fields:**
- `int Count` — number of parts placed
- `double UsableRemnantArea` — area of the largest remnant whose short side ≥ threshold (0 if none)
- `double Density` — total part area / bounding box area of all placed parts
**Constants:**
- `MinRemnantDimension = 12.0` (inches) — minimum short side for a remnant to be considered usable
**Implements** `IComparable<FillScore>` with lexicographic ordering.
**Static factory:** `FillScore.Compute(List<Part> parts, Box workArea)` — computes all three metrics from a fill result. Remnant calculation uses the same edge-strip approach as `Plate.GetRemnants()` but against the work area box and placed part bounding boxes.
### 2. Replace Comparison Points
All six comparison locations switch from raw count to `FillScore`:
| Location | File | Current Logic |
|----------|------|---------------|
| `IsBetterFill` | NestEngine.cs:299 | Count, then bbox area tiebreaker |
| `FillWithPairs` inner loop | NestEngine.cs:226 | Count only |
| `TryStripRefill` | NestEngine.cs:424 | `stripParts.Count > lastCluster.Count` (keep as-is — threshold check, not quality comparison) |
| `FillRemainingStrip` | FillLinear.cs:436 | `h.Count > best.Count` (keep as-is — internal sub-fill, count is correct) |
| `FillPattern` | NestEngine.cs:492 | `IsBetterValidFill` (overlap + count) |
| `FindBestFill` | NestEngine.cs:95-118 | `IsBetterFill` (already covered) |
`IsBetterFill(candidate, current)` becomes a `FillScore` comparison. `IsBetterValidFill` keeps its overlap check, then delegates to score comparison.
`FillLinear.FillRemainingStrip` needs access to the work area (already available via `WorkArea` property) to compute scores.
### 3. Expanded Remainder Rotations
`FillLinear.FillRemainingStrip` currently only tries rotations from the seed pattern. Add 0° and 90° (cardinal orientations) to the rotation set:
```csharp
// Current: only seed rotations
foreach (var seedPart in seedPattern.Parts) { ... }
// New: also try 0° and 90°
var rotations = new List<double> { 0, Angle.HalfPI };
foreach (var seedPart in seedPattern.Parts)
if (!rotations.Any(r => r.IsEqualTo(seedPart.Rotation)))
rotations.Add(seedPart.Rotation);
```
This is the change that actually fixes the 45→47 case by allowing `FillRemainingStrip` to discover the 0° rotation for the remainder strip.
## What This Does NOT Change
- Part count remains the primary criterion — no trading parts for remnants
- `FillWithPairs` still evaluates top 50 candidates (no extra `TryRemainderImprovement` per candidate)
- `BestFitResult` ranking/filtering is unchanged
- No new UI or configuration (MinRemnantDimension is a constant for now)
## Expected Outcome
For the 4980 A24 PT02 case:
1. Pattern #1 grid produces 36 parts
2. Expanded remainder rotations try 0° in the 15.7" strip → 11 parts → **47 total**
3. Wider pair grid produces 45 parts with ~5" remainder → ~0 extra parts → **45 total**
4. FillScore comparison: 47 > 45 → pattern #1 wins