docs: add plans for ML angle pruning, fill-exact, and helper decomposition
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1003
docs/superpowers/plans/2026-03-14-ml-angle-pruning.md
Normal file
1003
docs/superpowers/plans/2026-03-14-ml-angle-pruning.md
Normal file
File diff suppressed because it is too large
Load Diff
462
docs/superpowers/plans/2026-03-15-fill-exact.md
Normal file
462
docs/superpowers/plans/2026-03-15-fill-exact.md
Normal file
@@ -0,0 +1,462 @@
|
||||
# FillExact Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a `FillExact` method to `NestEngine` that binary-searches for the smallest work area sub-region that fits an exact quantity of parts, then integrate it into AutoNest.
|
||||
|
||||
**Architecture:** `FillExact` wraps the existing `Fill(NestItem, Box, IProgress, CancellationToken)` method. It calls Fill repeatedly with progressively smaller test boxes (binary search on one dimension, both orientations), picks the tightest fit, then re-runs the winner with progress reporting. Callers swap `Fill` for `FillExact` — no other engine changes needed.
|
||||
|
||||
**Tech Stack:** C# / .NET 8, OpenNest.Engine, OpenNest (WinForms), OpenNest.Console, OpenNest.Mcp
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-03-15-fill-exact-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Core Implementation
|
||||
|
||||
### Task 1: Add `BinarySearchFill` helper to NestEngine
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestEngine.cs` (add private method after the existing `Fill` overloads, around line 85)
|
||||
|
||||
- [ ] **Step 1: Add the BinarySearchFill method**
|
||||
|
||||
Add after the `Fill(NestItem, Box, IProgress, CancellationToken)` method (line 85):
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Binary-searches for the smallest sub-area (one dimension fixed) that fits
|
||||
/// exactly item.Quantity parts. Returns the best parts list and the dimension
|
||||
/// value that achieved it.
|
||||
/// </summary>
|
||||
private (List<Part> parts, double usedDim) BinarySearchFill(
|
||||
NestItem item, Box workArea, bool shrinkWidth,
|
||||
CancellationToken token)
|
||||
{
|
||||
var quantity = item.Quantity;
|
||||
var partBox = item.Drawing.Program.BoundingBox();
|
||||
var partArea = item.Drawing.Area;
|
||||
|
||||
// Fixed and variable dimensions.
|
||||
var fixedDim = shrinkWidth ? workArea.Length : workArea.Width;
|
||||
var highDim = shrinkWidth ? workArea.Width : workArea.Length;
|
||||
|
||||
// Estimate starting point: target area at 50% utilization.
|
||||
var targetArea = partArea * quantity / 0.5;
|
||||
var minPartDim = shrinkWidth
|
||||
? partBox.Width + Plate.PartSpacing
|
||||
: partBox.Length + Plate.PartSpacing;
|
||||
var estimatedDim = System.Math.Max(minPartDim, targetArea / fixedDim);
|
||||
|
||||
var low = estimatedDim;
|
||||
var high = highDim;
|
||||
|
||||
List<Part> bestParts = null;
|
||||
var bestDim = high;
|
||||
|
||||
for (var iter = 0; iter < 8; iter++)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (high - low < Plate.PartSpacing)
|
||||
break;
|
||||
|
||||
var mid = (low + high) / 2.0;
|
||||
|
||||
var testBox = shrinkWidth
|
||||
? new Box(workArea.X, workArea.Y, mid, workArea.Length)
|
||||
: new Box(workArea.X, workArea.Y, workArea.Width, mid);
|
||||
|
||||
var result = Fill(item, testBox, null, token);
|
||||
|
||||
if (result.Count >= quantity)
|
||||
{
|
||||
bestParts = result.Count > quantity
|
||||
? result.Take(quantity).ToList()
|
||||
: result;
|
||||
bestDim = mid;
|
||||
high = mid;
|
||||
}
|
||||
else
|
||||
{
|
||||
low = mid;
|
||||
}
|
||||
}
|
||||
|
||||
return (bestParts, bestDim);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestEngine.cs
|
||||
git commit -m "feat(engine): add BinarySearchFill helper for exact-quantity search"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Add `FillExact` public method to NestEngine
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Engine/NestEngine.cs` (add public method after the existing `Fill` overloads, before `BinarySearchFill`)
|
||||
|
||||
- [ ] **Step 1: Add the FillExact method**
|
||||
|
||||
Add between the `Fill(NestItem, Box, IProgress, CancellationToken)` method and `BinarySearchFill`:
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// Finds the smallest sub-area of workArea that fits exactly item.Quantity parts.
|
||||
/// Uses binary search on both orientations and picks the tightest fit.
|
||||
/// Falls through to standard Fill for unlimited (0) or single (1) quantities.
|
||||
/// </summary>
|
||||
public List<Part> FillExact(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
{
|
||||
// Early exits: unlimited or single quantity — no benefit from area search.
|
||||
if (item.Quantity <= 1)
|
||||
return Fill(item, workArea, progress, token);
|
||||
|
||||
// Full fill to establish upper bound.
|
||||
var fullResult = Fill(item, workArea, progress, token);
|
||||
|
||||
if (fullResult.Count <= item.Quantity)
|
||||
return fullResult;
|
||||
|
||||
// Binary search: try shrinking each dimension.
|
||||
var (lengthParts, lengthDim) = BinarySearchFill(item, workArea, shrinkWidth: false, token);
|
||||
var (widthParts, widthDim) = BinarySearchFill(item, workArea, shrinkWidth: true, token);
|
||||
|
||||
// Pick winner by smallest test box area. Tie-break: prefer shrink-length.
|
||||
List<Part> winner;
|
||||
Box winnerBox;
|
||||
|
||||
var lengthArea = lengthParts != null ? workArea.Width * lengthDim : double.MaxValue;
|
||||
var widthArea = widthParts != null ? widthDim * workArea.Length : double.MaxValue;
|
||||
|
||||
if (lengthParts != null && lengthArea <= widthArea)
|
||||
{
|
||||
winner = lengthParts;
|
||||
winnerBox = new Box(workArea.X, workArea.Y, workArea.Width, lengthDim);
|
||||
}
|
||||
else if (widthParts != null)
|
||||
{
|
||||
winner = widthParts;
|
||||
winnerBox = new Box(workArea.X, workArea.Y, widthDim, workArea.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Neither search found the exact quantity — return full fill truncated.
|
||||
return fullResult.Take(item.Quantity).ToList();
|
||||
}
|
||||
|
||||
// Re-run the winner with progress so PhaseResults/WinnerPhase are correct
|
||||
// and the progress form shows the final result.
|
||||
var finalResult = Fill(item, winnerBox, progress, token);
|
||||
|
||||
if (finalResult.Count >= item.Quantity)
|
||||
return finalResult.Count > item.Quantity
|
||||
? finalResult.Take(item.Quantity).ToList()
|
||||
: finalResult;
|
||||
|
||||
// Fallback: return the binary search result if the re-run produced fewer.
|
||||
return winner;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/NestEngine.cs
|
||||
git commit -m "feat(engine): add FillExact method for exact-quantity nesting"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Add Compactor class to Engine
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Engine/Compactor.cs`
|
||||
|
||||
- [ ] **Step 1: Create the Compactor class**
|
||||
|
||||
Create `OpenNest.Engine/Compactor.cs`:
|
||||
|
||||
```csharp
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a group of parts left and down to close gaps after placement.
|
||||
/// Uses the same directional-distance logic as PlateView.PushSelected
|
||||
/// but operates on Part objects directly.
|
||||
/// </summary>
|
||||
public static class Compactor
|
||||
{
|
||||
private const double ChordTolerance = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// Compacts movingParts toward the bottom-left of the plate work area.
|
||||
/// Everything already on the plate (excluding movingParts) is treated
|
||||
/// as stationary obstacles.
|
||||
/// </summary>
|
||||
public static void Compact(List<Part> movingParts, Plate plate)
|
||||
{
|
||||
if (movingParts == null || movingParts.Count == 0)
|
||||
return;
|
||||
|
||||
Push(movingParts, plate, PushDirection.Left);
|
||||
Push(movingParts, plate, PushDirection.Down);
|
||||
}
|
||||
|
||||
private static void Push(List<Part> movingParts, Plate plate, PushDirection direction)
|
||||
{
|
||||
var stationaryParts = plate.Parts
|
||||
.Where(p => !movingParts.Contains(p))
|
||||
.ToList();
|
||||
|
||||
var stationaryBoxes = new Box[stationaryParts.Count];
|
||||
|
||||
for (var i = 0; i < stationaryParts.Count; i++)
|
||||
stationaryBoxes[i] = stationaryParts[i].BoundingBox;
|
||||
|
||||
var stationaryLines = new List<Line>[stationaryParts.Count];
|
||||
var opposite = Helper.OppositeDirection(direction);
|
||||
var halfSpacing = plate.PartSpacing / 2;
|
||||
var isHorizontal = Helper.IsHorizontalDirection(direction);
|
||||
var workArea = plate.WorkArea();
|
||||
|
||||
foreach (var moving in movingParts)
|
||||
{
|
||||
var distance = double.MaxValue;
|
||||
var movingBox = moving.BoundingBox;
|
||||
|
||||
// Plate edge distance.
|
||||
var edgeDist = Helper.EdgeDistance(movingBox, workArea, direction);
|
||||
if (edgeDist > 0 && edgeDist < distance)
|
||||
distance = edgeDist;
|
||||
|
||||
List<Line> movingLines = null;
|
||||
|
||||
for (var i = 0; i < stationaryBoxes.Length; i++)
|
||||
{
|
||||
var gap = Helper.DirectionalGap(movingBox, stationaryBoxes[i], direction);
|
||||
if (gap < 0 || gap >= distance)
|
||||
continue;
|
||||
|
||||
var perpOverlap = isHorizontal
|
||||
? movingBox.IsHorizontalTo(stationaryBoxes[i], out _)
|
||||
: movingBox.IsVerticalTo(stationaryBoxes[i], out _);
|
||||
|
||||
if (!perpOverlap)
|
||||
continue;
|
||||
|
||||
movingLines ??= halfSpacing > 0
|
||||
? Helper.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance)
|
||||
: Helper.GetPartLines(moving, direction, ChordTolerance);
|
||||
|
||||
stationaryLines[i] ??= halfSpacing > 0
|
||||
? Helper.GetOffsetPartLines(stationaryParts[i], halfSpacing, opposite, ChordTolerance)
|
||||
: Helper.GetPartLines(stationaryParts[i], opposite, ChordTolerance);
|
||||
|
||||
var d = Helper.DirectionalDistance(movingLines, stationaryLines[i], direction);
|
||||
if (d < distance)
|
||||
distance = d;
|
||||
}
|
||||
|
||||
if (distance < double.MaxValue && distance > 0)
|
||||
{
|
||||
var offset = Helper.DirectionToOffset(direction, distance);
|
||||
moving.Offset(offset);
|
||||
|
||||
// Update this part's bounding box in the stationary set for
|
||||
// subsequent moving parts to collide against correctly.
|
||||
// (Parts already pushed become obstacles for the next part.)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.Engine/OpenNest.Engine.csproj --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Engine/Compactor.cs
|
||||
git commit -m "feat(engine): add Compactor for post-fill gravity compaction"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: Integration
|
||||
|
||||
### Task 4: Integrate FillExact and Compactor into AutoNest (MainForm)
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest/Forms/MainForm.cs` (RunAutoNest_Click, around lines 797-815)
|
||||
|
||||
- [ ] **Step 1: Replace Fill with FillExact and add Compactor call**
|
||||
|
||||
In `RunAutoNest_Click`, change the Fill call and the block after it (around lines 799-815). Replace:
|
||||
|
||||
```csharp
|
||||
var parts = await Task.Run(() =>
|
||||
engine.Fill(item, workArea, progress, token));
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```csharp
|
||||
var parts = await Task.Run(() =>
|
||||
engine.FillExact(item, workArea, progress, token));
|
||||
```
|
||||
|
||||
Then after `plate.Parts.AddRange(parts);` and before `ComputeRemainderStrip`, add the compaction call:
|
||||
|
||||
```csharp
|
||||
plate.Parts.AddRange(parts);
|
||||
Compactor.Compact(parts, plate);
|
||||
activeForm.PlateView.Invalidate();
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.sln --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest/Forms/MainForm.cs
|
||||
git commit -m "feat(ui): use FillExact + Compactor in AutoNest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Integrate FillExact and Compactor into Console app
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Console/Program.cs` (around lines 346-360)
|
||||
|
||||
- [ ] **Step 1: Replace Fill with FillExact and add Compactor call**
|
||||
|
||||
Change the Fill call (around line 352) from:
|
||||
|
||||
```csharp
|
||||
var parts = engine.Fill(item, workArea, null, CancellationToken.None);
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```csharp
|
||||
var parts = engine.FillExact(item, workArea, null, CancellationToken.None);
|
||||
```
|
||||
|
||||
Then after `plate.Parts.AddRange(parts);` add the compaction call:
|
||||
|
||||
```csharp
|
||||
plate.Parts.AddRange(parts);
|
||||
Compactor.Compact(parts, plate);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.Console/OpenNest.Console.csproj --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Console/Program.cs
|
||||
git commit -m "feat(console): use FillExact + Compactor in --autonest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Integrate FillExact and Compactor into MCP server
|
||||
|
||||
**Files:**
|
||||
- Modify: `OpenNest.Mcp/Tools/NestingTools.cs` (around lines 255-264)
|
||||
|
||||
- [ ] **Step 1: Replace Fill with FillExact and add Compactor call**
|
||||
|
||||
Change the Fill call (around line 256) from:
|
||||
|
||||
```csharp
|
||||
var parts = engine.Fill(item, workArea, null, CancellationToken.None);
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```csharp
|
||||
var parts = engine.FillExact(item, workArea, null, CancellationToken.None);
|
||||
```
|
||||
|
||||
Then after `plate.Parts.AddRange(parts);` add the compaction call:
|
||||
|
||||
```csharp
|
||||
plate.Parts.AddRange(parts);
|
||||
Compactor.Compact(parts, plate);
|
||||
item.Quantity = System.Math.Max(0, item.Quantity - parts.Count);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Build to verify compilation**
|
||||
|
||||
Run: `dotnet build OpenNest.Mcp/OpenNest.Mcp.csproj --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add OpenNest.Mcp/Tools/NestingTools.cs
|
||||
git commit -m "feat(mcp): use FillExact in autonest_plate for tighter packing"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Verification
|
||||
|
||||
### Task 7: End-to-end test via Console
|
||||
|
||||
- [ ] **Step 1: Run AutoNest with qty > 1 and verify tighter packing**
|
||||
|
||||
Run: `dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- --autonest --quantity 10 --no-save "C:\Users\AJ\Desktop\N0312-002.zip"`
|
||||
|
||||
Verify:
|
||||
- Completes without error
|
||||
- Parts placed count is reasonable (not 0, not wildly over-placed)
|
||||
- Utilization is reported
|
||||
|
||||
- [ ] **Step 2: Run with qty=1 to verify fallback path**
|
||||
|
||||
Run: `dotnet run --project OpenNest.Console/OpenNest.Console.csproj -- --autonest --no-save "C:\Users\AJ\Desktop\N0312-002.zip"`
|
||||
|
||||
Verify:
|
||||
- Completes quickly (qty=1 goes through Pack, no binary search)
|
||||
- Parts placed > 0
|
||||
|
||||
- [ ] **Step 3: Build full solution one final time**
|
||||
|
||||
Run: `dotnet build OpenNest.sln --nologo -v q`
|
||||
Expected: `Build succeeded. 0 Error(s)`
|
||||
350
docs/superpowers/plans/2026-03-15-helper-decomposition.md
Normal file
350
docs/superpowers/plans/2026-03-15-helper-decomposition.md
Normal file
@@ -0,0 +1,350 @@
|
||||
# Helper Class Decomposition
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Break the 1,464-line `Helper` catch-all class into focused, single-responsibility static classes.
|
||||
|
||||
**Architecture:** Extract six logical groups from `Helper` into dedicated classes. Each extraction creates a new file, moves methods, updates all call sites, and verifies with `dotnet build`. The original `Helper.cs` is deleted once empty. No behavioral changes — pure mechanical refactoring.
|
||||
|
||||
**Tech Stack:** .NET 8, C# 12
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
| New File | Namespace | Responsibility | Methods Moved |
|
||||
|----------|-----------|----------------|---------------|
|
||||
| `OpenNest.Core/Math/Rounding.cs` | `OpenNest.Math` | Factor-based rounding | `RoundDownToNearest`, `RoundUpToNearest`, `RoundToNearest` |
|
||||
| `OpenNest.Core/Geometry/GeometryOptimizer.cs` | `OpenNest.Geometry` | Merge collinear lines / coradial arcs | `Optimize(arcs)`, `Optimize(lines)`, `TryJoinLines`, `TryJoinArcs`, `GetCollinearLines`, `GetCoradialArs` |
|
||||
| `OpenNest.Core/Geometry/ShapeBuilder.cs` | `OpenNest.Geometry` | Chain entities into shapes | `GetShapes`, `GetConnected` |
|
||||
| `OpenNest.Core/Geometry/Intersect.cs` | `OpenNest.Geometry` | All intersection algorithms | 16 `Intersects` overloads |
|
||||
| `OpenNest.Core/PartGeometry.cs` | `OpenNest` | Convert Parts to line geometry | `GetPartLines` (×2), `GetOffsetPartLines` (×2), `GetDirectionalLines` |
|
||||
| `OpenNest.Core/Geometry/SpatialQuery.cs` | `OpenNest.Geometry` | Directional distance, ray casting, box queries | `RayEdgeDistance` (×2), `DirectionalDistance` (×3), `FlattenLines`, `OneWayDistance`, `OppositeDirection`, `IsHorizontalDirection`, `EdgeDistance`, `DirectionToOffset`, `DirectionalGap`, `ClosestDistance*` (×4), `GetLargestBox*` (×2) |
|
||||
|
||||
**Files modified (call-site updates):**
|
||||
|
||||
| File | Methods Referenced |
|
||||
|------|--------------------|
|
||||
| `OpenNest.Core/Plate.cs` | `RoundUpToNearest` → `Rounding.RoundUpToNearest` |
|
||||
| `OpenNest.IO/DxfImporter.cs` | `Optimize` → `GeometryOptimizer.Optimize` |
|
||||
| `OpenNest.Core/Geometry/Shape.cs` | `Optimize` → `GeometryOptimizer.Optimize`, `Intersects` → `Intersect.Intersects` |
|
||||
| `OpenNest.Core/Drawing.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Core/Timing.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Core/Converters/ConvertGeometry.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Core/Geometry/ShapeProfile.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Core/Geometry/Arc.cs` | `Intersects` → `Intersect.Intersects` |
|
||||
| `OpenNest.Core/Geometry/Circle.cs` | `Intersects` → `Intersect.Intersects` |
|
||||
| `OpenNest.Core/Geometry/Line.cs` | `Intersects` → `Intersect.Intersects` |
|
||||
| `OpenNest.Core/Geometry/Polygon.cs` | `Intersects` → `Intersect.Intersects` |
|
||||
| `OpenNest/LayoutPart.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest/Actions/ActionSetSequence.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest/Actions/ActionSelectArea.cs` | `GetLargestBox*` → `SpatialQuery.GetLargestBox*` |
|
||||
| `OpenNest/Actions/ActionClone.cs` | `GetLargestBox*` → `SpatialQuery.GetLargestBox*` |
|
||||
| `OpenNest.Gpu/PartBitmap.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Gpu/GpuPairEvaluator.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Engine/RotationAnalysis.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Engine/BestFit/BestFitFinder.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Engine/BestFit/PairEvaluator.cs` | `GetShapes` → `ShapeBuilder.GetShapes` |
|
||||
| `OpenNest.Engine/FillLinear.cs` | `DirectionalDistance`, `OppositeDirection` → `SpatialQuery.*` |
|
||||
| `OpenNest.Engine/Compactor.cs` | Multiple `Helper.*` → `SpatialQuery.*` + `PartGeometry.*` |
|
||||
| `OpenNest.Engine/BestFit/RotationSlideStrategy.cs` | Multiple `Helper.*` → `SpatialQuery.*` + `PartGeometry.*` |
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Rounding + GeometryOptimizer + ShapeBuilder
|
||||
|
||||
### Task 1: Extract Rounding to OpenNest.Math
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/Math/Rounding.cs`
|
||||
- Modify: `OpenNest.Core/Plate.cs:415-416`
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 14–45)
|
||||
|
||||
- [ ] **Step 1: Create `Rounding.cs`**
|
||||
|
||||
```csharp
|
||||
using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Math
|
||||
{
|
||||
public static class Rounding
|
||||
{
|
||||
public static double RoundDownToNearest(double num, double factor)
|
||||
{
|
||||
return factor.IsEqualTo(0) ? num : System.Math.Floor(num / factor) * factor;
|
||||
}
|
||||
|
||||
public static double RoundUpToNearest(double num, double factor)
|
||||
{
|
||||
return factor.IsEqualTo(0) ? num : System.Math.Ceiling(num / factor) * factor;
|
||||
}
|
||||
|
||||
public static double RoundToNearest(double num, double factor)
|
||||
{
|
||||
return factor.IsEqualTo(0) ? num : System.Math.Round(num / factor) * factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update call site in `Plate.cs`**
|
||||
|
||||
Replace `Helper.RoundUpToNearest` with `Rounding.RoundUpToNearest`. Add `using OpenNest.Math;` if not present.
|
||||
|
||||
- [ ] **Step 3: Remove three rounding methods from `Helper.cs`**
|
||||
|
||||
Delete lines 14–45 (the three methods and their XML doc comments).
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeded
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract Rounding from Helper to OpenNest.Math
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Extract GeometryOptimizer
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/Geometry/GeometryOptimizer.cs`
|
||||
- Modify: `OpenNest.IO/DxfImporter.cs:59-60`, `OpenNest.Core/Geometry/Shape.cs:162-163`
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 47–237)
|
||||
|
||||
- [ ] **Step 1: Create `GeometryOptimizer.cs`**
|
||||
|
||||
Move these 6 methods (preserving exact code):
|
||||
- `Optimize(IList<Arc>)`
|
||||
- `Optimize(IList<Line>)`
|
||||
- `TryJoinLines`
|
||||
- `TryJoinArcs`
|
||||
- `GetCollinearLines` (private extension method)
|
||||
- `GetCoradialArs` (private extension method)
|
||||
|
||||
Namespace: `OpenNest.Geometry`. Class: `public static class GeometryOptimizer`.
|
||||
|
||||
Required usings: `System`, `System.Collections.Generic`, `System.Threading.Tasks`, `OpenNest.Math`.
|
||||
|
||||
- [ ] **Step 2: Update call sites**
|
||||
|
||||
- `DxfImporter.cs`: `Helper.Optimize(...)` → `GeometryOptimizer.Optimize(...)`. Add `using OpenNest.Geometry;`.
|
||||
- `Shape.cs`: `Helper.Optimize(...)` → `GeometryOptimizer.Optimize(...)`. Already in `OpenNest.Geometry` namespace — no using needed.
|
||||
|
||||
- [ ] **Step 3: Remove methods from `Helper.cs`**
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract GeometryOptimizer from Helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Extract ShapeBuilder
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/Geometry/ShapeBuilder.cs`
|
||||
- Modify: 11 files (see call-site table above for `GetShapes` callers)
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 239–378)
|
||||
|
||||
- [ ] **Step 1: Create `ShapeBuilder.cs`**
|
||||
|
||||
Move these 2 methods:
|
||||
- `GetShapes(IEnumerable<Entity>)` — public
|
||||
- `GetConnected(Vector, IEnumerable<Entity>)` — internal
|
||||
|
||||
Namespace: `OpenNest.Geometry`. Class: `public static class ShapeBuilder`.
|
||||
|
||||
Required usings: `System.Collections.Generic`, `System.Diagnostics`, `OpenNest.Math`.
|
||||
|
||||
- [ ] **Step 2: Update all call sites**
|
||||
|
||||
Replace `Helper.GetShapes` → `ShapeBuilder.GetShapes` in every file. Add `using OpenNest.Geometry;` where the file isn't already in that namespace.
|
||||
|
||||
Files to update:
|
||||
- `OpenNest.Core/Drawing.cs`
|
||||
- `OpenNest.Core/Timing.cs`
|
||||
- `OpenNest.Core/Converters/ConvertGeometry.cs`
|
||||
- `OpenNest.Core/Geometry/ShapeProfile.cs` (already in namespace)
|
||||
- `OpenNest/LayoutPart.cs`
|
||||
- `OpenNest/Actions/ActionSetSequence.cs`
|
||||
- `OpenNest.Gpu/PartBitmap.cs`
|
||||
- `OpenNest.Gpu/GpuPairEvaluator.cs`
|
||||
- `OpenNest.Engine/RotationAnalysis.cs`
|
||||
- `OpenNest.Engine/BestFit/BestFitFinder.cs`
|
||||
- `OpenNest.Engine/BestFit/PairEvaluator.cs`
|
||||
|
||||
- [ ] **Step 3: Remove methods from `Helper.cs`**
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract ShapeBuilder from Helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: Intersect + PartGeometry
|
||||
|
||||
### Task 4: Extract Intersect
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/Geometry/Intersect.cs`
|
||||
- Modify: `Arc.cs`, `Circle.cs`, `Line.cs`, `Shape.cs`, `Polygon.cs` (all in `OpenNest.Core/Geometry/`)
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 380–742)
|
||||
|
||||
- [ ] **Step 1: Create `Intersect.cs`**
|
||||
|
||||
Move all 16 `Intersects` overloads. Namespace: `OpenNest.Geometry`. Class: `public static class Intersect`.
|
||||
|
||||
All methods keep their existing access modifiers (`internal` for most, none are `public`).
|
||||
|
||||
Required usings: `System.Collections.Generic`, `System.Linq`, `OpenNest.Math`.
|
||||
|
||||
- [ ] **Step 2: Update call sites in geometry types**
|
||||
|
||||
All callers are in the same namespace (`OpenNest.Geometry`) so no using changes needed. Replace `Helper.Intersects` → `Intersect.Intersects` in:
|
||||
- `Arc.cs` (10 calls)
|
||||
- `Circle.cs` (10 calls)
|
||||
- `Line.cs` (8 calls)
|
||||
- `Shape.cs` (12 calls, including the internal offset usage at line 537)
|
||||
- `Polygon.cs` (10 calls)
|
||||
|
||||
- [ ] **Step 3: Remove methods from `Helper.cs`**
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract Intersect from Helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Extract PartGeometry
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/PartGeometry.cs`
|
||||
- Modify: `OpenNest.Engine/Compactor.cs`, `OpenNest.Engine/BestFit/RotationSlideStrategy.cs`
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 744–858)
|
||||
|
||||
- [ ] **Step 1: Create `PartGeometry.cs`**
|
||||
|
||||
Move these 5 methods:
|
||||
- `GetPartLines(Part, double)` — public
|
||||
- `GetPartLines(Part, PushDirection, double)` — public
|
||||
- `GetOffsetPartLines(Part, double, double)` — public
|
||||
- `GetOffsetPartLines(Part, double, PushDirection, double)` — public
|
||||
- `GetDirectionalLines(Polygon, PushDirection)` — private
|
||||
|
||||
Namespace: `OpenNest`. Class: `public static class PartGeometry`.
|
||||
|
||||
Required usings: `System.Collections.Generic`, `System.Linq`, `OpenNest.Converters`, `OpenNest.Geometry`.
|
||||
|
||||
- [ ] **Step 2: Update call sites**
|
||||
|
||||
- `Compactor.cs`: `Helper.GetOffsetPartLines` / `Helper.GetPartLines` → `PartGeometry.*`
|
||||
- `RotationSlideStrategy.cs`: `Helper.GetOffsetPartLines` → `PartGeometry.GetOffsetPartLines`
|
||||
|
||||
- [ ] **Step 3: Remove methods from `Helper.cs`**
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract PartGeometry from Helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: SpatialQuery + Cleanup
|
||||
|
||||
### Task 6: Extract SpatialQuery
|
||||
|
||||
**Files:**
|
||||
- Create: `OpenNest.Core/Geometry/SpatialQuery.cs`
|
||||
- Modify: `Compactor.cs`, `FillLinear.cs`, `RotationSlideStrategy.cs`, `ActionClone.cs`, `ActionSelectArea.cs`
|
||||
- Delete from: `OpenNest.Core/Helper.cs` (lines 860–1462, all remaining methods)
|
||||
|
||||
- [ ] **Step 1: Create `SpatialQuery.cs`**
|
||||
|
||||
Move all remaining methods (14 total):
|
||||
- `RayEdgeDistance(Vector, Line, PushDirection)` — private
|
||||
- `RayEdgeDistance(double, double, double, double, double, double, PushDirection)` — private, `[AggressiveInlining]`
|
||||
- `DirectionalDistance(List<Line>, List<Line>, PushDirection)` — public
|
||||
- `DirectionalDistance(List<Line>, double, double, List<Line>, PushDirection)` — public
|
||||
- `DirectionalDistance((Vector,Vector)[], Vector, (Vector,Vector)[], Vector, PushDirection)` — public
|
||||
- `FlattenLines(List<Line>)` — public
|
||||
- `OneWayDistance(Vector, (Vector,Vector)[], Vector, PushDirection)` — public
|
||||
- `OppositeDirection(PushDirection)` — public
|
||||
- `IsHorizontalDirection(PushDirection)` — public
|
||||
- `EdgeDistance(Box, Box, PushDirection)` — public
|
||||
- `DirectionToOffset(PushDirection, double)` — public
|
||||
- `DirectionalGap(Box, Box, PushDirection)` — public
|
||||
- `ClosestDistanceLeft/Right/Up/Down` — public (4 methods)
|
||||
- `GetLargestBoxVertically/Horizontally` — public (2 methods)
|
||||
|
||||
Namespace: `OpenNest.Geometry`. Class: `public static class SpatialQuery`.
|
||||
|
||||
Required usings: `System`, `System.Collections.Generic`, `System.Linq`, `OpenNest.Math`.
|
||||
|
||||
- [ ] **Step 2: Update call sites**
|
||||
|
||||
Replace `Helper.*` → `SpatialQuery.*` and add `using OpenNest.Geometry;` where needed:
|
||||
- `OpenNest.Engine/Compactor.cs` — `OppositeDirection`, `IsHorizontalDirection`, `EdgeDistance`, `DirectionalGap`, `DirectionalDistance`, `DirectionToOffset`
|
||||
- `OpenNest.Engine/FillLinear.cs` — `DirectionalDistance`, `OppositeDirection`
|
||||
- `OpenNest.Engine/BestFit/RotationSlideStrategy.cs` — `FlattenLines`, `OppositeDirection`, `OneWayDistance`
|
||||
- `OpenNest/Actions/ActionClone.cs` — `GetLargestBoxVertically`, `GetLargestBoxHorizontally`
|
||||
- `OpenNest/Actions/ActionSelectArea.cs` — `GetLargestBoxHorizontally`, `GetLargestBoxVertically`
|
||||
|
||||
- [ ] **Step 3: Remove methods from `Helper.cs`**
|
||||
|
||||
At this point `Helper.cs` should be empty (just the class wrapper and usings).
|
||||
|
||||
- [ ] **Step 4: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```
|
||||
refactor: extract SpatialQuery from Helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Delete Helper.cs
|
||||
|
||||
**Files:**
|
||||
- Delete: `OpenNest.Core/Helper.cs`
|
||||
|
||||
- [ ] **Step 1: Delete the empty `Helper.cs` file**
|
||||
|
||||
- [ ] **Step 2: Build and verify**
|
||||
|
||||
Run: `dotnet build OpenNest.sln`
|
||||
Expected: Build succeeded with zero errors
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```
|
||||
refactor: remove empty Helper class
|
||||
```
|
||||
96
docs/superpowers/specs/2026-03-15-fill-exact-design.md
Normal file
96
docs/superpowers/specs/2026-03-15-fill-exact-design.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# FillExact — Exact-Quantity Fill with Binary Search
|
||||
|
||||
## Problem
|
||||
|
||||
The current `NestEngine.Fill` fills an entire work area and truncates to `item.Quantity` with `.Take(n)`. This wastes plate space — parts are spread across the full area, leaving no usable remainder strip for subsequent drawings in AutoNest.
|
||||
|
||||
## Solution
|
||||
|
||||
Add a `FillExact` method that binary-searches for the smallest sub-area of the work area that fits exactly the requested quantity. This packs parts tightly against one edge, maximizing the remainder strip available for the next drawing.
|
||||
|
||||
## Coordinate Conventions
|
||||
|
||||
`Box.Width` is the X-axis extent. `Box.Length` is the Y-axis extent. The box is anchored at `(Box.X, Box.Y)` (bottom-left corner).
|
||||
|
||||
- **Shrink width** means reducing `Box.Width` (X-axis), producing a narrower box anchored at the left edge. The remainder strip extends to the right.
|
||||
- **Shrink length** means reducing `Box.Length` (Y-axis), producing a shorter box anchored at the bottom edge. The remainder strip extends upward.
|
||||
|
||||
## Algorithm
|
||||
|
||||
1. **Early exits:**
|
||||
- Quantity is 0 (unlimited): delegate to `Fill` directly.
|
||||
- Quantity is 1: delegate to `Fill` directly (a single part placement doesn't benefit from area search).
|
||||
2. **Full fill** — Call `Fill(item, workArea, progress, token)` to establish the upper bound (max parts that fit). This call gets progress reporting so the user sees the phases running.
|
||||
3. **Already exact or under** — If `fullCount <= quantity`, return the full fill result. The plate can't fit more than requested anyway.
|
||||
4. **Estimate starting point** — Calculate an initial dimension estimate assuming 50% utilization: `estimatedDim = (partArea * quantity) / (0.5 * fixedDim)`, clamped to at least the part's bounding box dimension in that axis.
|
||||
5. **Binary search** (max 8 iterations, or until `high - low < partSpacing`) — Keep one dimension of the work area fixed and binary-search on the other:
|
||||
- `low = estimatedDim`, `high = workArea dimension`
|
||||
- Each iteration: create a test box, call `Fill(item, testBox, null, token)` (no progress — search iterations are silent), check count.
|
||||
- `count >= quantity` → record result, shrink: `high = mid`
|
||||
- `count < quantity` → expand: `low = mid`
|
||||
- Check cancellation token between iterations; if cancelled, return best found so far.
|
||||
6. **Try both orientations** — Run the binary search twice: once shrinking length (fixed width) and once shrinking width (fixed length).
|
||||
7. **Pick winner** — Compare by test box area (`testBox.Width * testBox.Length`). Return whichever orientation's result has a smaller test box area, leaving more remainder for subsequent drawings. Tie-break: prefer shrink-length (leaves horizontal remainder strip, generally more useful on wide plates).
|
||||
|
||||
## Method Signature
|
||||
|
||||
```csharp
|
||||
// NestEngine.cs
|
||||
public List<Part> FillExact(NestItem item, Box workArea,
|
||||
IProgress<NestProgress> progress, CancellationToken token)
|
||||
```
|
||||
|
||||
Returns exactly `item.Quantity` parts packed into the smallest sub-area of `workArea`, or fewer if they don't all fit.
|
||||
|
||||
## Internal Helper
|
||||
|
||||
```csharp
|
||||
private (List<Part> parts, double usedDim) BinarySearchFill(
|
||||
NestItem item, Box workArea, bool shrinkWidth,
|
||||
CancellationToken token)
|
||||
```
|
||||
|
||||
Performs the binary search for one orientation. Returns the parts and the dimension value at which the exact quantity was achieved. Progress is not passed to inner Fill calls — the search iterations run silently.
|
||||
|
||||
## Engine State
|
||||
|
||||
Each inner `Fill` call clears `PhaseResults`, `AngleResults`, and overwrites `WinnerPhase`. After the winning Fill call is identified, `FillExact` runs the winner one final time with `progress` so:
|
||||
- `PhaseResults` / `AngleResults` / `WinnerPhase` reflect the winning fill.
|
||||
- The progress form shows the final result.
|
||||
|
||||
## Integration
|
||||
|
||||
### AutoNest (MainForm.RunAutoNest_Click)
|
||||
|
||||
Replace `engine.Fill(item, workArea, progress, token)` with `engine.FillExact(item, workArea, progress, token)` for multi-quantity items. The tighter packing means `ComputeRemainderStrip` returns a larger box for subsequent drawings.
|
||||
|
||||
### Single-drawing Fill
|
||||
|
||||
`FillExact` works for single-drawing fills too. When `item.Quantity` is set, the caller gets a tight layout instead of parts scattered across the full plate.
|
||||
|
||||
### Fallback
|
||||
|
||||
When `item.Quantity` is 0 (unlimited), `FillExact` falls through to the standard `Fill` behavior — fill the entire work area.
|
||||
|
||||
## Performance Notes
|
||||
|
||||
The binary search converges in at most 8 iterations per orientation. Each iteration calls `Fill` internally, which runs the pairs/linear/best-fit phases. For a typical auto-nest scenario:
|
||||
|
||||
- Full fill: 1 call (with progress)
|
||||
- Shrink-length search: ~6-8 calls (silent)
|
||||
- Shrink-width search: ~6-8 calls (silent)
|
||||
- Final re-fill of winner: 1 call (with progress)
|
||||
- Total: ~15-19 Fill calls per drawing
|
||||
|
||||
The inner `Fill` calls for reduced work areas are faster than full-plate fills since the search space is smaller. The `BestFitCache` (used by the pairs phase) is keyed on the full plate size, so it stays warm across iterations — only the linear/rect phases re-run.
|
||||
|
||||
Early termination (`high - low < partSpacing`) typically cuts 1-3 iterations, bringing the total closer to 12-15 calls.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **Quantity 0 (unlimited):** Skip binary search, delegate to `Fill` directly.
|
||||
- **Quantity 1:** Skip binary search, delegate to `Fill` directly.
|
||||
- **Full fill already exact:** Return immediately without searching.
|
||||
- **Part doesn't fit at all:** Return empty list.
|
||||
- **Binary search can't hit exact count** (e.g., jumps from N-1 to N+2): Take the smallest test box where `count >= quantity` and truncate with `.Take(quantity)`.
|
||||
- **Cancellation:** Check token between iterations. Return best result found so far.
|
||||
Reference in New Issue
Block a user