Files
OpenNest/OpenNest.Engine/NfpCache.cs
AJ Isaacs 3f3b07ef5d feat: add NFP-based mixed-part autonesting
Implement geometry-aware nesting using No-Fit Polygons and simulated
annealing optimization. Parts interlock based on true shape rather than
bounding boxes, producing tighter layouts for mixed-part scenarios.

New types in Core/Geometry:
- ConvexDecomposition: ear-clipping triangulation for concave polygons
- NoFitPolygon: Minkowski sum via convex decomposition + Clipper2 union
- InnerFitPolygon: feasible region computation for plate placement

New types in Engine:
- NfpCache: caches NFPs keyed by (drawingId, rotation) pairs
- BottomLeftFill: places parts using feasible regions from IFP - NFP union
- INestOptimizer: abstraction for future GA/parallel upgrades
- SimulatedAnnealing: optimizes part ordering and rotation

Integration:
- NestEngine.AutoNest(): new public entry point for mixed-part nesting
- MainForm.RunAutoNest_Click: uses AutoNest instead of Pack
- NestingTools.autonest_plate: new MCP tool for Claude Code integration
- Drawing.Id: auto-incrementing identifier for NFP cache keys
- Clipper2 NuGet added to OpenNest.Core for polygon boolean operations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 08:08:22 -04:00

139 lines
4.7 KiB
C#

using System;
using System.Collections.Generic;
using OpenNest.Geometry;
namespace OpenNest
{
/// <summary>
/// Caches computed No-Fit Polygons keyed by (DrawingA.Id, RotationA, DrawingB.Id, RotationB).
/// NFPs are computed on first access and stored for reuse during optimization.
/// Thread-safe for concurrent reads after pre-computation.
/// </summary>
public class NfpCache
{
private readonly Dictionary<NfpKey, Polygon> cache = new Dictionary<NfpKey, Polygon>();
private readonly Dictionary<int, Dictionary<double, Polygon>> polygonCache
= new Dictionary<int, Dictionary<double, Polygon>>();
/// <summary>
/// Registers a pre-computed polygon for a drawing at a specific rotation.
/// Call this during initialization before computing NFPs.
/// </summary>
public void RegisterPolygon(int drawingId, double rotation, Polygon polygon)
{
if (!polygonCache.TryGetValue(drawingId, out var rotations))
{
rotations = new Dictionary<double, Polygon>();
polygonCache[drawingId] = rotations;
}
rotations[rotation] = polygon;
}
/// <summary>
/// Gets the polygon for a drawing at a specific rotation.
/// </summary>
public Polygon GetPolygon(int drawingId, double rotation)
{
if (polygonCache.TryGetValue(drawingId, out var rotations))
{
if (rotations.TryGetValue(rotation, out var polygon))
return polygon;
}
return null;
}
/// <summary>
/// Gets or computes the NFP between two drawings at their respective rotations.
/// The NFP is computed from the stationary polygon (drawingA at rotationA) and
/// the orbiting polygon (drawingB at rotationB).
/// </summary>
public Polygon Get(int drawingIdA, double rotationA, int drawingIdB, double rotationB)
{
var key = new NfpKey(drawingIdA, rotationA, drawingIdB, rotationB);
if (cache.TryGetValue(key, out var nfp))
return nfp;
var polyA = GetPolygon(drawingIdA, rotationA);
var polyB = GetPolygon(drawingIdB, rotationB);
if (polyA == null || polyB == null)
return new Polygon();
nfp = NoFitPolygon.Compute(polyA, polyB);
cache[key] = nfp;
return nfp;
}
/// <summary>
/// Pre-computes all NFPs for every combination of registered polygons.
/// Call after all polygons are registered to front-load computation.
/// </summary>
public void PreComputeAll()
{
var entries = new List<(int drawingId, double rotation)>();
foreach (var kvp in polygonCache)
{
foreach (var rot in kvp.Value)
entries.Add((kvp.Key, rot.Key));
}
for (var i = 0; i < entries.Count; i++)
{
for (var j = 0; j < entries.Count; j++)
{
Get(entries[i].drawingId, entries[i].rotation,
entries[j].drawingId, entries[j].rotation);
}
}
}
/// <summary>
/// Number of cached NFP entries.
/// </summary>
public int Count => cache.Count;
private readonly struct NfpKey : IEquatable<NfpKey>
{
public readonly int DrawingIdA;
public readonly double RotationA;
public readonly int DrawingIdB;
public readonly double RotationB;
public NfpKey(int drawingIdA, double rotationA, int drawingIdB, double rotationB)
{
DrawingIdA = drawingIdA;
RotationA = rotationA;
DrawingIdB = drawingIdB;
RotationB = rotationB;
}
public bool Equals(NfpKey other)
{
return DrawingIdA == other.DrawingIdA
&& RotationA == other.RotationA
&& DrawingIdB == other.DrawingIdB
&& RotationB == other.RotationB;
}
public override bool Equals(object obj) => obj is NfpKey key && Equals(key);
public override int GetHashCode()
{
unchecked
{
var hash = 17;
hash = hash * 31 + DrawingIdA;
hash = hash * 31 + RotationA.GetHashCode();
hash = hash * 31 + DrawingIdB;
hash = hash * 31 + RotationB.GetHashCode();
return hash;
}
}
}
}
}