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:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user