diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs
index d114f11..573e3cf 100644
--- a/OpenNest.Engine/StripNestEngine.cs
+++ b/OpenNest.Engine/StripNestEngine.cs
@@ -15,11 +15,10 @@ namespace OpenNest
public override string Name => "Strip";
- public override string Description => "Strip-based nesting for mixed-drawing layouts";
+ public override string Description => "Iterative shrink-fill nesting for mixed-drawing layouts";
///
/// Single-item fill delegates to DefaultNestEngine.
- /// The strip strategy adds value for multi-drawing nesting, not single-item fills.
///
public override List Fill(NestItem item, Box workArea,
IProgress progress, CancellationToken token)
@@ -49,66 +48,10 @@ namespace OpenNest
}
///
- /// Selects the item that consumes the most plate area (bounding box area x quantity).
- /// Returns the index into the items list.
- ///
- private static int SelectStripItemIndex(List items, Box workArea)
- {
- var bestIndex = 0;
- var bestArea = 0.0;
-
- for (var i = 0; i < items.Count; i++)
- {
- var bbox = items[i].Drawing.Program.BoundingBox();
- var qty = items[i].Quantity > 0
- ? items[i].Quantity
- : (int)(workArea.Area() / bbox.Area());
- var totalArea = bbox.Area() * qty;
-
- if (totalArea > bestArea)
- {
- bestArea = totalArea;
- bestIndex = i;
- }
- }
-
- return bestIndex;
- }
-
- ///
- /// Estimates the strip dimension (height for bottom, width for left) needed
- /// to fit the target quantity. Tries 0 deg and 90 deg rotations and picks the shorter.
- /// This is only an estimate for the shrink loop starting point — the actual fill
- /// uses DefaultNestEngine.Fill which tries many rotation angles internally.
- ///
- private static double EstimateStripDimension(NestItem item, double stripLength, double maxDimension)
- {
- var bbox = item.Drawing.Program.BoundingBox();
- var qty = item.Quantity > 0
- ? item.Quantity
- : System.Math.Max(1, (int)(stripLength * maxDimension / bbox.Area()));
-
- // At 0 deg: parts per row along strip length, strip dimension is bbox.Length
- var perRow0 = (int)(stripLength / bbox.Width);
- var rows0 = perRow0 > 0 ? (int)System.Math.Ceiling((double)qty / perRow0) : int.MaxValue;
- var dim0 = rows0 * bbox.Length;
-
- // At 90 deg: rotated bounding box (Width and Length swap)
- var perRow90 = (int)(stripLength / bbox.Length);
- var rows90 = perRow90 > 0 ? (int)System.Math.Ceiling((double)qty / perRow90) : int.MaxValue;
- var dim90 = rows90 * bbox.Width;
-
- var estimate = System.Math.Min(dim0, dim90);
-
- // Clamp to available dimension
- return System.Math.Min(estimate, maxDimension);
- }
-
- ///
- /// Multi-drawing strip nesting strategy.
- /// Picks the largest-area drawing for strip treatment, finds the tightest strip
- /// in both bottom and left orientations, fills remnants with remaining drawings,
- /// and returns the denser result.
+ /// Multi-drawing iterative shrink-fill strategy.
+ /// Each multi-quantity drawing gets shrink-filled into the tightest
+ /// sub-region using dual-direction selection. Singles and leftovers
+ /// are packed at the end.
///
public override List Nest(List items,
IProgress progress, CancellationToken token)
@@ -118,147 +61,72 @@ namespace OpenNest
var workArea = Plate.WorkArea();
- // Select which item gets the strip treatment.
- var stripIndex = SelectStripItemIndex(items, workArea);
- var stripItem = items[stripIndex];
- var remainderItems = items.Where((_, i) => i != stripIndex).ToList();
+ // Separate multi-quantity from singles.
+ var fillItems = items
+ .Where(i => i.Quantity != 1)
+ .OrderBy(i => i.Priority)
+ .ThenByDescending(i => i.Drawing.Area)
+ .ToList();
- // Try both orientations.
- var bottomResult = TryOrientation(StripDirection.Bottom, stripItem, remainderItems, workArea, progress, token);
- var leftResult = TryOrientation(StripDirection.Left, stripItem, remainderItems, workArea, progress, token);
+ var packItems = items
+ .Where(i => i.Quantity == 1)
+ .ToList();
- // Pick the better result.
- var winner = bottomResult.Score >= leftResult.Score
- ? bottomResult.Parts
- : leftResult.Parts;
+ var allParts = new List();
- // Deduct placed quantities from the original items.
+ // Phase 1: Iterative shrink-fill for multi-quantity items.
+ if (fillItems.Count > 0)
+ {
+ Func> fillFunc = (ni, b) =>
+ {
+ var inner = new DefaultNestEngine(Plate);
+ return inner.Fill(ni, b, progress, token);
+ };
+
+ var shrinkResult = IterativeShrinkFiller.Fill(
+ fillItems, workArea, fillFunc, Plate.PartSpacing, token);
+
+ allParts.AddRange(shrinkResult.Parts);
+
+ // Add unfilled items to pack list.
+ packItems.AddRange(shrinkResult.Leftovers);
+ }
+
+ // Phase 2: Pack singles + leftovers into remaining space.
+ packItems = packItems.Where(i => i.Quantity > 0).ToList();
+
+ if (packItems.Count > 0 && !token.IsCancellationRequested)
+ {
+ // Reconstruct remaining area from placed parts.
+ var packArea = workArea;
+ if (allParts.Count > 0)
+ {
+ var obstacles = allParts
+ .Select(p => p.BoundingBox.Offset(Plate.PartSpacing))
+ .ToList();
+ var finder = new RemnantFinder(workArea, obstacles);
+ var remnants = finder.FindRemnants();
+ packArea = remnants.Count > 0 ? remnants[0] : new Box(0, 0, 0, 0);
+ }
+
+ if (packArea.Width > 0 && packArea.Length > 0)
+ {
+ var packParts = PackArea(packArea, packItems, progress, token);
+ allParts.AddRange(packParts);
+ }
+ }
+
+ // Deduct placed quantities from original items.
foreach (var item in items)
{
if (item.Quantity <= 0)
continue;
- var placed = winner.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
+ var placed = allParts.Count(p => p.BaseDrawing.Name == item.Drawing.Name);
item.Quantity = System.Math.Max(0, item.Quantity - placed);
}
- return winner;
+ return allParts;
}
-
- private StripNestResult TryOrientation(StripDirection direction, NestItem stripItem,
- List remainderItems, Box workArea, IProgress progress, CancellationToken token)
- {
- var result = new StripNestResult { Direction = direction };
-
- if (token.IsCancellationRequested)
- return result;
-
- // Estimate initial strip dimension.
- var stripLength = direction == StripDirection.Bottom ? workArea.Width : workArea.Length;
- var maxDimension = direction == StripDirection.Bottom ? workArea.Length : workArea.Width;
- var estimatedDim = EstimateStripDimension(stripItem, stripLength, maxDimension);
-
- // Create the initial strip box.
- var stripBox = direction == StripDirection.Bottom
- ? new Box(workArea.X, workArea.Y, workArea.Width, estimatedDim)
- : new Box(workArea.X, workArea.Y, estimatedDim, workArea.Length);
-
- // Shrink to tightest strip.
- var shrinkAxis = direction == StripDirection.Bottom
- ? ShrinkAxis.Height : ShrinkAxis.Width;
-
- Func> stripFill = (ni, b) =>
- {
- var trialInner = new DefaultNestEngine(Plate);
- return trialInner.Fill(ni, b, progress, token);
- };
-
- var shrinkResult = ShrinkFiller.Shrink(stripFill,
- new NestItem { Drawing = stripItem.Drawing, Quantity = stripItem.Quantity },
- stripBox, Plate.PartSpacing, shrinkAxis, token);
-
- if (shrinkResult.Parts == null || shrinkResult.Parts.Count == 0)
- return result;
-
- var bestParts = shrinkResult.Parts;
- var bestDim = shrinkResult.Dimension;
-
- // Build remnant box with spacing gap.
- var spacing = Plate.PartSpacing;
- var remnantBox = direction == StripDirection.Bottom
- ? new Box(workArea.X, workArea.Y + bestDim + spacing,
- workArea.Width, workArea.Length - bestDim - spacing)
- : new Box(workArea.X + bestDim + spacing, workArea.Y,
- workArea.Width - bestDim - spacing, workArea.Length);
-
- // Collect all parts.
- var allParts = new List(bestParts);
-
- // If strip item was only partially placed, add leftovers to remainder.
- var placed = bestParts.Count;
- var leftover = stripItem.Quantity > 0 ? stripItem.Quantity - placed : 0;
- var effectiveRemainder = new List(remainderItems);
-
- if (leftover > 0)
- {
- effectiveRemainder.Add(new NestItem
- {
- Drawing = stripItem.Drawing,
- Quantity = leftover
- });
- }
-
- // Sort remainder by descending bounding box area x quantity.
- effectiveRemainder = effectiveRemainder
- .OrderByDescending(i =>
- {
- var bb = i.Drawing.Program.BoundingBox();
- return bb.Area() * (i.Quantity > 0 ? i.Quantity : 1);
- })
- .ToList();
-
- // Fill remnants
- if (remnantBox.Width > 0 && remnantBox.Length > 0)
- {
- var remnantProgress = progress != null
- ? new AccumulatingProgress(progress, allParts)
- : (IProgress)null;
-
- var remnantFiller = new RemnantFiller(workArea, spacing);
- remnantFiller.AddObstacles(allParts);
-
- Func> remnantFillFunc = (ni, b) =>
- ShrinkFill(ni, b, remnantProgress, token);
-
- var additional = remnantFiller.FillItems(effectiveRemainder,
- remnantFillFunc, token, remnantProgress);
-
- allParts.AddRange(additional);
- }
-
- result.Parts = allParts;
- result.StripBox = direction == StripDirection.Bottom
- ? new Box(workArea.X, workArea.Y, workArea.Width, bestDim)
- : new Box(workArea.X, workArea.Y, bestDim, workArea.Length);
- result.Score = FillScore.Compute(allParts, workArea);
-
- return result;
- }
-
- private List ShrinkFill(NestItem item, Box box,
- IProgress progress, CancellationToken token)
- {
- Func> fillFunc = (ni, b) =>
- {
- var inner = new DefaultNestEngine(Plate);
- return inner.Fill(ni, b, null, token);
- };
-
- var heightResult = ShrinkFiller.Shrink(fillFunc, item, box,
- Plate.PartSpacing, ShrinkAxis.Height, token);
-
- return heightResult.Parts;
- }
-
}
}