using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace OpenNest.Engine.Fill { /// /// Result returned by . /// public class IterativeShrinkResult { public List Parts { get; set; } = new List(); public List Leftovers { get; set; } = new List(); } /// /// Composes and with /// dual-direction shrink selection. Wraps the caller's fill function in a /// closure that tries both and /// , picks the better , /// and passes the wrapper to . /// public static class IterativeShrinkFiller { public static IterativeShrinkResult Fill( List items, Box workArea, Func> fillFunc, double spacing, CancellationToken token = default, IProgress progress = null, int plateNumber = 0, Func> widthFillFunc = null) { if (items == null || items.Count == 0) return new IterativeShrinkResult(); // RemnantFiller.FillItems skips items with Quantity == 0 (its localQty // check treats them as done). Convert unlimited items (Quantity <= 0) // to an estimated max capacity so they are actually processed. var workItems = new List(items.Count); foreach (var item in items) { if (item.Quantity <= 0) { var bbox = item.Drawing.Program.BoundingBox(); var estimatedMax = bbox.Area() > 0 ? (int)(workArea.Area() / bbox.Area()) * 2 : 1000; workItems.Add(new NestItem { Drawing = item.Drawing, Quantity = System.Math.Max(1, estimatedMax), Priority = item.Priority, StepAngle = item.StepAngle, RotationStart = item.RotationStart, RotationEnd = item.RotationEnd }); } else { workItems.Add(item); } } var filler = new RemnantFiller(workArea, spacing); // Track parts placed by previous items so ShrinkFiller can // include them in progress reports. var placedSoFar = new List(); var wFillFunc = widthFillFunc ?? fillFunc; Func> shrinkWrapper = (ni, box) => { var target = ni.Quantity > 0 ? ni.Quantity : 0; // Run height and width shrinks in parallel — they are independent // (same inputs, no shared mutable state). ShrinkResult heightResult = null; ShrinkResult widthResult = null; Parallel.Invoke( () => heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Length, token, targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar), () => widthResult = ShrinkFiller.Shrink(wFillFunc, ni, box, spacing, ShrinkAxis.Width, token, targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar) ); var heightScore = FillScore.Compute(heightResult.Parts, box); var widthScore = FillScore.Compute(widthResult.Parts, box); var best = widthScore > heightScore ? widthResult.Parts : heightResult.Parts; // Sort pair groups so shorter/narrower groups are closer to the origin, // creating a staircase profile that maximizes remnant area. // Height shrink → columns vary in height → sort columns. // Width shrink → rows vary in width → sort rows. if (widthScore > heightScore) SortRowsByWidth(best, spacing); else SortColumnsByHeight(best, spacing); // Report the winner as overall best so the UI shows it as settled. if (progress != null && best != null && best.Count > 0) { var allParts = new List(placedSoFar.Count + best.Count); allParts.AddRange(placedSoFar); allParts.AddRange(best); NestEngineBase.ReportProgress(progress, new ProgressReport { Phase = NestPhase.Custom, PlateNumber = plateNumber, Parts = allParts, WorkArea = box, Description = $"Shrink: {best.Count} parts placed", IsOverallBest = true, }); } // Accumulate for the next item's progress reports. placedSoFar.AddRange(best); return best; }; var placed = filler.FillItems(workItems, shrinkWrapper, token); // Build leftovers: compare placed count to original quantities. // RemnantFiller.FillItems does NOT mutate NestItem.Quantity. var leftovers = new List(); foreach (var item in items) { var placedCount = 0; foreach (var p in placed) { if (p.BaseDrawing.Name == item.Drawing.Name) placedCount++; } if (item.Quantity <= 0) continue; // unlimited items are always "satisfied" — no leftover var remaining = item.Quantity - placedCount; if (remaining > 0) { leftovers.Add(new NestItem { Drawing = item.Drawing, Quantity = remaining, Priority = item.Priority, StepAngle = item.StepAngle, RotationStart = item.RotationStart, RotationEnd = item.RotationEnd }); } } return new IterativeShrinkResult { Parts = placed, Leftovers = leftovers }; } /// /// Sorts pair columns by height (shortest first on the left) to create /// a staircase profile that maximizes usable remnant area. /// internal static void SortColumnsByHeight(List parts, double spacing) => SortStrips(parts, spacing, primaryEdge: b => b.Left, extentEdge: b => b.Right, sortMetric: MaxTop, stripMin: MinLeft, stripMax: MaxRight, makeOffset: d => new Vector(d, 0)); /// /// Sorts pair rows by width (narrowest first on the bottom) to create /// a staircase profile on the right side that maximizes usable remnant area. /// internal static void SortRowsByWidth(List parts, double spacing) => SortStrips(parts, spacing, primaryEdge: b => b.Bottom, extentEdge: b => b.Top, sortMetric: MaxRight, stripMin: MinBottom, stripMax: MaxTop, makeOffset: d => new Vector(0, d)); private static void SortStrips( List parts, double spacing, Func primaryEdge, Func extentEdge, Func, double> sortMetric, Func, double> stripMin, Func, double> stripMax, Func makeOffset) { if (parts == null || parts.Count <= 1) return; parts.Sort((a, b) => primaryEdge(a.BoundingBox).CompareTo(primaryEdge(b.BoundingBox))); var strips = new List>(); var strip = new List { parts[0] }; var stripExtent = extentEdge(parts[0].BoundingBox); for (var i = 1; i < parts.Count; i++) { if (primaryEdge(parts[i].BoundingBox) > stripExtent + spacing / 2) { strips.Add(strip); strip = new List { parts[i] }; stripExtent = extentEdge(parts[i].BoundingBox); } else { strip.Add(parts[i]); var extent = extentEdge(parts[i].BoundingBox); if (extent > stripExtent) stripExtent = extent; } } strips.Add(strip); if (strips.Count <= 1) return; var gap = stripMin(strips[1]) - stripMax(strips[0]); strips.Sort((a, b) => sortMetric(a).CompareTo(sortMetric(b))); var pos = primaryEdge(parts[0].BoundingBox); foreach (var s in strips) { var delta = pos - stripMin(s); if (System.Math.Abs(delta) > OpenNest.Math.Tolerance.Epsilon) { var offset = makeOffset(delta); foreach (var part in s) part.Offset(offset); } pos = stripMax(s) + gap; } parts.Clear(); foreach (var s in strips) parts.AddRange(s); } private static double MaxTop(List col) { var max = double.MinValue; foreach (var p in col) if (p.BoundingBox.Top > max) max = p.BoundingBox.Top; return max; } private static double MaxRight(List col) { var max = double.MinValue; foreach (var p in col) if (p.BoundingBox.Right > max) max = p.BoundingBox.Right; return max; } private static double MinLeft(List col) { var min = double.MaxValue; foreach (var p in col) if (p.BoundingBox.Left < min) min = p.BoundingBox.Left; return min; } private static double MinBottom(List row) { var min = double.MaxValue; foreach (var p in row) if (p.BoundingBox.Bottom < min) min = p.BoundingBox.Bottom; return min; } } }