Files
OpenNest/docs/superpowers/specs/2026-03-11-nfp-autonest-design.md
T
2026-03-11 14:28:22 -04:00

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:

  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

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

  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<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
  • 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, InnerFitPolygonOpenNest.Core/Geometry/
  • NfpCache, BLF engine, SA optimizer, INestOptimizerOpenNest.Engine/
  • AutoNest method → NestEngine.cs