docs: add FillScore design spec and implementation plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user