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)
|
if (placed == null)
|
||||||
continue;
|
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);
|
allParts.AddRange(placed);
|
||||||
localQty[item.Drawing.Name] = System.Math.Max(0, qty - placed.Count);
|
localQty[item.Drawing.Name] = System.Math.Max(0, qty - placed.Count);
|
||||||
|
|
||||||
foreach (var p in placed)
|
// Add the envelope of all placed parts as a single obstacle
|
||||||
finder.AddObstacle(p.BoundingBox.Offset(spacing));
|
// rather than individual bounding boxes, preventing the
|
||||||
|
// remnant finder from seeing inter-part gaps.
|
||||||
|
var envelope = ComputeEnvelope(placed, spacing);
|
||||||
|
finder.AddObstacle(envelope);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -114,6 +124,39 @@ namespace OpenNest.Engine.Fill
|
|||||||
return false;
|
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(
|
private static List<Part> TryFillInRemnants(
|
||||||
NestItem item,
|
NestItem item,
|
||||||
int qty,
|
int qty,
|
||||||
|
|||||||
@@ -1,25 +1,42 @@
|
|||||||
using OpenNest.Engine.Fill;
|
using OpenNest.Engine.Fill;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace OpenNest.Engine.Strategies
|
namespace OpenNest.Engine.Strategies
|
||||||
{
|
{
|
||||||
public class PairsFillStrategy : IFillStrategy
|
public class PairsFillStrategy : IFillStrategy
|
||||||
{
|
{
|
||||||
|
private static readonly AsyncLocal<bool> active = new();
|
||||||
|
|
||||||
public string Name => "Pairs";
|
public string Name => "Pairs";
|
||||||
public NestPhase Phase => NestPhase.Pairs;
|
public NestPhase Phase => NestPhase.Pairs;
|
||||||
public int Order => 100;
|
public int Order => 100;
|
||||||
|
|
||||||
public List<Part> Fill(FillContext context)
|
public List<Part> Fill(FillContext context)
|
||||||
{
|
{
|
||||||
var comparer = context.Policy?.Comparer;
|
// Prevent recursive PairFiller — remnant fills within PairFiller
|
||||||
var dedup = GridDedup.GetOrCreate(context.SharedState);
|
// create a new engine that runs the full pipeline, which would
|
||||||
var filler = new PairFiller(context.Plate, comparer, dedup);
|
// invoke PairsFillStrategy again, causing deep recursion.
|
||||||
var result = filler.Fill(context.Item, context.WorkArea,
|
if (active.Value)
|
||||||
context.PlateNumber, context.Token, context.Progress);
|
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