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; - } - } }