diff --git a/OpenNest.Engine/Fill/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs index 9251b82..13f8fa3 100644 --- a/OpenNest.Engine/Fill/RemnantFiller.cs +++ b/OpenNest.Engine/Fill/RemnantFiller.cs @@ -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 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 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 TryFillInRemnants( NestItem item, int qty, diff --git a/OpenNest.Engine/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index c73a9fe..4fda406 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -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 active = new(); + public string Name => "Pairs"; public NestPhase Phase => NestPhase.Pairs; public int Order => 100; public List 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; + } } } }