Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
9.7 KiB
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:
- Decompose concave polygon into convex sub-polygons (ear-clipping or Hertel-Mehlhorn)
- Compute NFP for each convex pair combination
- 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
interface INestOptimizer
{
NestResult Optimize(List<NestItem> 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:
public List<Part> AutoNest(List<NestItem> items, Plate plate)
Flow
- Extract perimeter polygons via
DefinedShapefor each unique drawing - Inflate perimeters by half-spacing
- Compute candidate rotations per drawing
- Pre-compute
NfpCachefor all (drawing, rotation) pair combinations - Run
INestOptimizer.Optimize()→ best sequence - Final BLF placement with best solution → placed
Partinstances - Return parts
Coexistence with Existing Methods
| Method | Use Case | Status |
|---|---|---|
Fill(NestItem) |
Single-drawing plate fill (tiling) | Unchanged |
Pack(List<NestItem>) |
Rectangle bin packing | Unchanged |
AutoNest(List<NestItem>, 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 BestFitFinderpair 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/AutoNestmethod →NestEngine.cs