feat(irregular): treat clamp zones as static NFP obstacles

This commit is contained in:
aj committed 2026-10-13 18:15:49 -04:00
1 parent de9a087027
commit c374e02614
6 files changed
+340 -8

No files matched your search

@@ -0,0 +1,52 @@
#nullable enable
using Clipper2Lib;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.NestingEngines.Irregular;
/// <summary>
/// Static, clearance-inflated rectangles prepared once per stock per solve. Kept out of
/// the part catalog and its spacing cache: clamp clearance is bottom edge spacing, not
/// part spacing, and two stocks at the same part spacing may have different clamps.
/// </summary>
internal sealed class ClampObstacles
{
private readonly Orientation[] rectangles;
private readonly NoFitCache nfps = new(0);
public ClampObstacles(NestPlateStock stock)
{
rectangles = new Orientation[stock.ClampZones.Count];
var clearance = System.Math.Max(0, stock.EdgeSpacing.Bottom);
for (var i = 0; i < rectangles.Length; i++)
{
var box = stock.ClampZones[i].Bounds;
var left = box.Left - clearance;
var bottom = box.Bottom - clearance;
var right = box.Right + clearance;
var top = box.Top + clearance;
rectangles[i] = new Orientation
{
// Real part types are nonnegative; the private cache cannot alias them.
TypeIndex = -1,
Index = i,
Rotation = 0,
Tolerance = 0,
MinX = left,
MinY = bottom,
MaxX = right,
MaxY = top,
Outline = new PathD
{
new(left, bottom), new(right, bottom), new(right, top), new(left, top),
},
};
}
}
public int Count => rectangles.Length;
// No additional physical spacing. NoFitCache retains the established arc/grid safety
// margin and covered-hole certification for the moving orientation and rectangle.
public Nfp Get(int index, Orientation moving) => nfps.Get(rectangles[index], moving);
}
@@ -35,8 +35,8 @@ internal sealed record SheetFill(NestPlateStock Stock, IReadOnlyList<Placed> Par
///
/// For every (part type, orientation) still in play the packer keeps the exact set of legal
/// reference points: the inner-fit rectangle of the work area minus the no-fit polygons of
/// everything already placed. Each placement subtracts one translated NFP from each region,
/// so regions only shrink, and a region that empties is retired for the rest of the sheet.
/// static clamps and everything already placed. Each placement subtracts one translated NFP
/// from each region, so regions only shrink; an empty region is retired for the sheet.
///
/// Choice rule, applied over all types and orientations at once (not in a fixed order):
/// 1. Gap fill - if any part fits without pushing the packing front forward, place the
@@ -62,6 +62,7 @@ internal sealed class FrontierPacker
private readonly Box work;
private readonly WorkCounter counter;
private readonly BlockCatalog? blocks;
private readonly ClampObstacles? clamps;
public FrontierPacker(
IReadOnlyList<PartType> types,
@@ -71,10 +72,12 @@ internal sealed class FrontierPacker
PackAxis axis,
double beta,
WorkCounter counter,
BlockCatalog? blocks = null
BlockCatalog? blocks = null,
ClampObstacles? clamps = null
)
{
this.blocks = blocks;
this.clamps = stock.ClampZones.Count == 0 ? null : clamps ?? new ClampObstacles(stock);
this.counter = counter;
this.types = types;
this.nfps = nfps;
@@ -97,6 +100,16 @@ internal sealed class FrontierPacker
if (!stock.Fits(o.Width, o.Height))
return false;
var region = new Region(o, work, single);
if (clamps != null)
for (var i = 0; i < clamps.Count; i++)
{
token.ThrowIfCancellationRequested();
var nfp = clamps.Get(i, o);
token.ThrowIfCancellationRequested();
region.Subtract(nfp, 0, 0);
counter.Add(1);
token.ThrowIfCancellationRequested();
}
states.Add(region);
byOrientation[o] = region;
return true;
@@ -118,6 +118,7 @@ public sealed class IrregularNestingEngine : INestingEngine
private const int MaxTail = 3;
private readonly Dictionary<double, NoFitCache> caches = new();
private readonly Dictionary<NestPlateStock, ClampObstacles> clamps = new();
private readonly Dictionary<double, IReadOnlyDictionary<int, IReadOnlyList<PairPose>>> pairs = new();
private readonly Dictionary<double, BlockCatalog> blocks = new();
private readonly IReadOnlyDictionary<string, NestJobPart> parts = job.Parts.ToDictionary(p => p.Id, StringComparer.Ordinal);
@@ -198,6 +199,15 @@ public sealed class IrregularNestingEngine : INestingEngine
return cache;
}
private ClampObstacles? ClampsFor(NestPlateStock stock)
{
if (stock.ClampZones.Count == 0)
return null;
if (!clamps.TryGetValue(stock, out var found))
clamps[stock] = found = new ClampObstacles(stock);
return found;
}
public bool PairFits(PartType type)
{
if (type.Part.Quantity < 2 || type.Orientations.Count == 0)
@@ -265,7 +275,8 @@ public sealed class IrregularNestingEngine : INestingEngine
if (stock.Quantity is int available && used[stock.Id] >= available)
continue;
progress?.Report(new NestJobProgress(NestJobStage.EvaluatingCandidate, stock.Id, sheets.Count, 0, 0));
var packer = new FrontierPacker(types, CacheFor(stock), PairsFor(stock), stock, axis, beta, Work, BlocksFor(stock));
var packer = new FrontierPacker(types, CacheFor(stock), PairsFor(stock), stock, axis, beta, Work,
BlocksFor(stock), ClampsFor(stock));
var fill = packer.Fill(remaining, token);
sheetPacked?.Invoke();
if (fill.Parts.Count > 0)