From f126bfe2ba38ec59a846a4a0f25b1987e29cd2ca Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 11 Mar 2026 14:28:22 -0400 Subject: [PATCH] docs: add NFP-based autonesting design spec Co-Authored-By: Claude Opus 4.6 --- .../specs/2026-03-11-nfp-autonest-design.md | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-11-nfp-autonest-design.md diff --git a/docs/superpowers/specs/2026-03-11-nfp-autonest-design.md b/docs/superpowers/specs/2026-03-11-nfp-autonest-design.md new file mode 100644 index 0000000..4f7bc10 --- /dev/null +++ b/docs/superpowers/specs/2026-03-11-nfp-autonest-design.md @@ -0,0 +1,230 @@ +# NFP-Based Autonesting Design + +## Problem + +OpenNest's current nesting engine handles single-drawing plate fills well (FillLinear, FillBestFit, FillWithPairs), but cannot produce mixed-part layouts — placing multiple different drawings together on a plate with true geometry-aware interlocking. The existing `PackBottomLeft` supports mixed parts but operates on bounding-box rectangles only, wasting the concave space around irregular shapes. + +Commercial nesting software (e.g., PEP at $30k) produces mixed-part layouts but with mediocre results — significant gaps and poor material utilization. + +## Goal + +Build an NFP-based mixed-part autonester that: +- Places multiple different drawings on a plate with true geometry-aware collision avoidance +- Produces tighter layouts than bounding-box packing by allowing parts to interlock +- Uses simulated annealing to optimize part ordering and rotation +- Integrates cleanly alongside existing Fill/Pack methods in NestEngine +- Provides an abstracted optimizer interface for future GA/parallel upgrades + +## Architecture + +Three layers, built bottom-up: + +``` +┌──────────────────────────────────┐ +│ Simulated Annealing Optimizer │ Layer 3: searches orderings/rotations +│ (implements INestOptimizer) │ +├──────────────────────────────────┤ +│ NFP-Based Bottom-Left Fill │ Layer 2: places parts using feasible regions +│ (BLF placement engine) │ +├──────────────────────────────────┤ +│ NFP / IFP Computation + Cache │ Layer 1: geometric foundation +└──────────────────────────────────┘ +``` + +## Layer 1: No-Fit Polygon (NFP) Computation + +### What is an NFP? + +The No-Fit Polygon between a stationary polygon A and an orbiting polygon B defines all positions where B's reference point would cause A and B to overlap. If B's reference point is: +- **Inside** the NFP → overlap +- **On the boundary** → touching +- **Outside** → no overlap + +### Computation Method + +Use the **Minkowski sum** approach: NFP(A, B) = A ⊕ (-B), where -B is B reflected through its reference point. + +For convex polygons this is a simple merge-sort of edge vectors. For concave polygons, decompose into convex pieces first, compute partial NFPs, then take the union. + +### Input + +Polygons come from the existing conversion chain: + +``` +Drawing.Program + → ConvertProgram.ToGeometry() // recursively expands SubProgramCalls + → Helper.GetShapes() // chains connected entities into Shapes + → DefinedShape // separates perimeter from cutouts + → .Perimeter // outer boundary + → .ToPolygonWithTolerance(circumscribe) // polygon with arcs circumscribed +``` + +Only `DefinedShape.Perimeter` is used for NFP — cutouts do not affect part-to-part collision. Spacing is applied by inflating the perimeter polygon by half-spacing before NFP computation (consistent with existing `PartBoundary` approach). + +### Concave Polygon Handling + +Many real parts are concave. The approach: +1. Decompose concave polygon into convex sub-polygons (ear-clipping or Hertel-Mehlhorn) +2. Compute NFP for each convex pair combination +3. Union the partial NFPs into the final NFP + +### NFP Cache + +NFPs are keyed by `(DrawingA.Id, RotationA, DrawingB.Id, RotationB)` and computed once per unique combination. During SA optimization, the same NFPs are reused thousands of times. + +**Type:** `NfpCache` — dictionary-based lookup with lazy computation. + +### New Types + +| Type | Namespace | Purpose | +|------|-----------|---------| +| `NoFitPolygon` | `OpenNest.Geometry` | Static methods for NFP computation between two polygons | +| `InnerFitPolygon` | `OpenNest.Geometry` | Static methods for IFP (part inside plate boundary) | +| `NfpCache` | `OpenNest.Engine` | Caches computed NFPs keyed by drawing/rotation pairs | + +## Layer 2: Inner-Fit Polygon (IFP) + +The IFP answers "where can this part be placed inside the plate?" It is the NFP of the plate boundary with the part, but inverted — the feasible region is the interior. + +For a rectangular plate and a convex part, the IFP is a smaller rectangle (plate shrunk by part dimensions). For concave parts the IFP boundary follows the plate edges inset by the part's profile. + +### Feasible Region + +For placing part P(i) given already-placed parts P(1)...P(i-1): + +``` +FeasibleRegion = IFP(plate, P(i)) ∩ complement(NFP(P(1), P(i))) ∩ ... ∩ complement(NFP(P(i-1), P(i))) +``` + +The placement point is the bottom-left-most point on the feasible region boundary. + +## Layer 3: NFP-Based Bottom-Left Fill (BLF) + +### Algorithm + +``` +BLF(sequence, plate, nfpCache): + placedParts = [] + for each (drawing, rotation) in sequence: + polygon = getPerimeterPolygon(drawing, rotation) + ifp = InnerFitPolygon.Compute(plate, polygon) + nfps = [nfpCache.Get(placed, polygon) for placed in placedParts] + feasible = ifp minus union(nfps) + point = bottomLeftMost(feasible) + if point exists: + place part at point + placedParts.append(part) + return FillScore.Compute(placedParts, plate) +``` + +### Bottom-Left Point Selection + +"Bottom-left" means: minimize X first, then minimize Y (leftmost, then lowest). This matches the existing `PackBottomLeft.FindPointVertical` priority but operates on polygon feasible regions instead of discrete candidate points. + +### Replaces + +This replaces `PackBottomLeft` for mixed-part scenarios. The existing rectangle-based `PackBottomLeft` remains available for pure-rectangle use cases. + +## Layer 4: Simulated Annealing Optimizer + +### Interface + +```csharp +interface INestOptimizer +{ + NestResult Optimize(List items, Plate plate, NfpCache cache); +} +``` + +This abstraction allows swapping SA for GA (or other meta-heuristics) in the future without touching the NFP or BLF layers. + +### State Representation + +The SA state is a sequence of `(DrawingId, RotationAngle)` tuples — one entry per physical part to place (respecting quantities from NestItem). The sequence determines placement order for BLF. + +### Mutation Operators + +Each iteration, one mutation is selected randomly: + +| Operator | Description | +|----------|-------------| +| **Swap** | Exchange two parts' positions in the sequence | +| **Rotate** | Change one part's rotation to another candidate angle | +| **Segment reverse** | Reverse a contiguous subsequence | + +### Cooling Schedule + +- **Initial temperature:** Calibrated so ~80% of worse moves are accepted initially +- **Cooling rate:** Geometric (T = T * alpha, alpha ~0.995-0.999) +- **Termination:** Temperature below threshold OR no improvement for N consecutive iterations +- **Quality focus:** Since quality > speed, allow long runs (thousands of iterations) + +### Candidate Rotations + +Per drawing, candidate rotations are determined by existing logic: +- Hull-edge angles from `RotationAnalysis.FindHullEdgeAngles()` +- 0 degrees baseline +- Fixed-increment sweep (e.g., 5 degrees) for small work areas + +### Initial Solution + +Generate the initial sequence by sorting parts largest-area-first (matches existing `PackBottomLeft` heuristic). This gives SA a reasonable starting point rather than random. + +## Integration: NestEngine.AutoNest + +New public entry point: + +```csharp +public List AutoNest(List items, Plate plate) +``` + +### Flow + +1. Extract perimeter polygons via `DefinedShape` for each unique drawing +2. Inflate perimeters by half-spacing +3. Compute candidate rotations per drawing +4. Pre-compute `NfpCache` for all (drawing, rotation) pair combinations +5. Run `INestOptimizer.Optimize()` → best sequence +6. Final BLF placement with best solution → placed `Part` instances +7. Return parts + +### Coexistence with Existing Methods + +| Method | Use Case | Status | +|--------|----------|--------| +| `Fill(NestItem)` | Single-drawing plate fill (tiling) | Unchanged | +| `Pack(List)` | Rectangle bin packing | Unchanged | +| `AutoNest(List, Plate)` | Mixed-part geometry-aware nesting | **New** | + +## Future: Simplifying FillLinear with NFP + +Once NFP infrastructure is in place, `FillLinear.FindCopyDistance` can be replaced with NFP projections: +- Copy distance along any axis = extreme point of NFP projected onto that axis +- Eliminates directional edge filtering, line-to-line sliding, and special-case handling in `PartBoundary` +- `BestFitFinder` pair evaluation simplifies to walking NFP boundaries + +This refactor is deferred to a future phase to keep scope focused. + +## Future: Nesting Inside Cutouts + +`DefinedShape.Cutouts` are preserved but unused in Phase 1. Future optimization: for parts with large interior cutouts, compute IFP of the cutout boundary and attempt to place small parts inside. This requires: +- Minimum cutout size threshold +- Modified BLF that considers cutout interiors as additional placement regions + +## Future: Parallel Optimization (Threadripper) + +The `INestOptimizer` interface naturally supports parallelism: +- **SA parallelism:** Run multiple independent SA chains with different random seeds, take the best result +- **GA upgrade:** Population-based evaluation is embarrassingly parallel — evaluate N candidate orderings simultaneously on N threads +- NFP cache is read-only during optimization, so it's inherently thread-safe + +## Scoring + +Results are scored using existing `FillScore` (lexicographic: count → usable remnant area → density). No changes needed. + +## Project Location + +All new types go in existing projects: +- `NoFitPolygon`, `InnerFitPolygon` → `OpenNest.Core/Geometry/` +- `NfpCache`, BLF engine, SA optimizer, `INestOptimizer` → `OpenNest.Engine/` +- `AutoNest` method → `NestEngine.cs`