fix: prevent RemnantFiller interleaving and PairFiller recursion

RemnantFiller: add placed parts as a single envelope obstacle instead
of individual bounding boxes to prevent the next drawing from filling
into inter-row gaps. Remove the topmost bounding-box part to create a
clean rectangular boundary.

PairsFillStrategy: guard against recursive invocation — remnant fills
within PairFiller create a new engine that runs the full pipeline,
which would invoke PairsFillStrategy again causing deep recursion.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 21:11:47 -04:00
parent 31a9e6dbad
commit ebb18d9b49
2 changed files with 69 additions and 9 deletions

View File

@@ -102,11 +102,21 @@ namespace OpenNest.Engine.Fill
if (placed == null)
continue;
// Remove the topmost bounding box part to create a clean
// rectangular obstacle boundary. Without this, gaps between
// individual bounding boxes cause the next drawing to fill
// into inter-row spaces, producing an interleaved layout.
if (placed.Count > 1)
RemoveTopmostPart(placed);
allParts.AddRange(placed);
localQty[item.Drawing.Name] = System.Math.Max(0, qty - placed.Count);
foreach (var p in placed)
finder.AddObstacle(p.BoundingBox.Offset(spacing));
// Add the envelope of all placed parts as a single obstacle
// rather than individual bounding boxes, preventing the
// remnant finder from seeing inter-part gaps.
var envelope = ComputeEnvelope(placed, spacing);
finder.AddObstacle(envelope);
return true;
}
@@ -114,6 +124,39 @@ namespace OpenNest.Engine.Fill
return false;
}
private static void RemoveTopmostPart(List<Part> parts)
{
var topIdx = 0;
for (var i = 1; i < parts.Count; i++)
{
if (parts[i].BoundingBox.Top > parts[topIdx].BoundingBox.Top)
topIdx = i;
}
parts.RemoveAt(topIdx);
}
private static Box ComputeEnvelope(List<Part> parts, double spacing)
{
var left = double.MaxValue;
var bottom = double.MaxValue;
var right = double.MinValue;
var top = double.MinValue;
foreach (var p in parts)
{
var bb = p.BoundingBox;
if (bb.Left < left) left = bb.Left;
if (bb.Bottom < bottom) bottom = bb.Bottom;
if (bb.Right > right) right = bb.Right;
if (bb.Top > top) top = bb.Top;
}
return new Box(left - spacing, bottom - spacing,
right - left + spacing * 2, top - bottom + spacing * 2);
}
private static List<Part> TryFillInRemnants(
NestItem item,
int qty,

View File

@@ -1,25 +1,42 @@
using OpenNest.Engine.Fill;
using System.Collections.Generic;
using System.Threading;
namespace OpenNest.Engine.Strategies
{
public class PairsFillStrategy : IFillStrategy
{
private static readonly AsyncLocal<bool> active = new();
public string Name => "Pairs";
public NestPhase Phase => NestPhase.Pairs;
public int Order => 100;
public List<Part> Fill(FillContext context)
{
var comparer = context.Policy?.Comparer;
var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress);
// Prevent recursive PairFiller — remnant fills within PairFiller
// create a new engine that runs the full pipeline, which would
// invoke PairsFillStrategy again, causing deep recursion.
if (active.Value)
return null;
context.SharedState["BestFits"] = result.BestFits;
active.Value = true;
try
{
var comparer = context.Policy?.Comparer;
var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress);
return result.Parts;
context.SharedState["BestFits"] = result.BestFits;
return result.Parts;
}
finally
{
active.Value = false;
}
}
}
}