diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 4a09190..97b5dc3 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -19,11 +19,6 @@ namespace OpenNest.Engine.Fill public double HalfSpacing => PartSpacing / 2; - /// - /// Optional multi-part patterns (e.g. interlocking pairs) to try in remainder strips. - /// - public List RemainderPatterns { get; set; } - private static Vector MakeOffset(NestDirection direction, double distance) { return direction == NestDirection.Horizontal @@ -346,215 +341,9 @@ namespace OpenNest.Engine.Fill var gridResult = new List(rowPattern.Parts); gridResult.AddRange(TilePattern(rowPattern, perpAxis, rowBoundaries)); - // Step 3: Fill remaining strip - var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction); - if (remaining.Count > 0) - gridResult.AddRange(remaining); - - // Step 4: Try fewer rows optimization - var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction); - if (fewerResult != null && fewerResult.Count > gridResult.Count) - return fewerResult; - return gridResult; } - /// - /// Tries removing the last row/column from the grid and re-filling the - /// larger remainder strip. Returns null if this doesn't improve the total. - /// - private List TryFewerRows( - List fullResult, Pattern rowPattern, Pattern seedPattern, - NestDirection tiledAxis, NestDirection primaryAxis) - { - var rowPartCount = rowPattern.Parts.Count; - - if (fullResult.Count < rowPartCount * 2) - return null; - - var fewerParts = new List(fullResult.Count - rowPartCount); - - for (var i = 0; i < fullResult.Count - rowPartCount; i++) - fewerParts.Add(fullResult[i]); - - var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis); - - if (remaining.Count <= rowPartCount) - return null; - - fewerParts.AddRange(remaining); - return fewerParts; - } - - /// - /// After tiling full rows/columns, fills the remaining strip with individual - /// parts. The strip is the leftover space along the tiled axis between the - /// last full row/column and the work area boundary. Each unique drawing and - /// rotation from the seed pattern is tried in both directions. - /// - private List FillRemainingStrip( - List placedParts, Pattern seedPattern, - NestDirection tiledAxis, NestDirection primaryAxis) - { - var placedEdge = FindPlacedEdge(placedParts, tiledAxis); - var remainingStrip = BuildRemainingStrip(placedEdge, tiledAxis); - - if (remainingStrip == null) - return new List(); - - var rotations = BuildRotationSet(seedPattern); - var best = FindBestFill(rotations, remainingStrip); - - if (RemainderPatterns != null) - { - System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Strip: {remainingStrip.Width:F1}x{remainingStrip.Length:F1}, individual best={best?.Count ?? 0}, trying {RemainderPatterns.Count} patterns"); - - foreach (var pattern in RemainderPatterns) - { - var filler = new FillLinear(remainingStrip, PartSpacing); - var h = filler.Fill(pattern, NestDirection.Horizontal); - var v = filler.Fill(pattern, NestDirection.Vertical); - - System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Pattern ({pattern.Parts.Count} parts, bbox={pattern.BoundingBox.Width:F1}x{pattern.BoundingBox.Length:F1}): H={h?.Count ?? 0}, V={v?.Count ?? 0}"); - - if (h != null && h.Count > (best?.Count ?? 0)) - best = h; - if (v != null && v.Count > (best?.Count ?? 0)) - best = v; - } - - System.Diagnostics.Debug.WriteLine($"[FillRemainingStrip] Final best={best?.Count ?? 0}"); - } - - return best ?? new List(); - } - - private static double FindPlacedEdge(List placedParts, NestDirection tiledAxis) - { - var placedEdge = double.MinValue; - - foreach (var part in placedParts) - { - var edge = tiledAxis == NestDirection.Vertical - ? part.BoundingBox.Top - : part.BoundingBox.Right; - - if (edge > placedEdge) - placedEdge = edge; - } - - return placedEdge; - } - - private Box BuildRemainingStrip(double placedEdge, NestDirection tiledAxis) - { - if (tiledAxis == NestDirection.Vertical) - { - var bottom = placedEdge + PartSpacing; - var height = WorkArea.Top - bottom; - - if (height <= Tolerance.Epsilon) - return null; - - return new Box(WorkArea.X, bottom, WorkArea.Width, height); - } - else - { - var left = placedEdge + PartSpacing; - var width = WorkArea.Right - left; - - if (width <= Tolerance.Epsilon) - return null; - - return new Box(left, WorkArea.Y, width, WorkArea.Length); - } - } - - /// - /// Builds a set of (drawing, rotation) candidates: cardinal orientations - /// (0° and 90°) for each unique drawing, plus any seed pattern rotations - /// not already covered. - /// - private static List<(Drawing drawing, double rotation)> BuildRotationSet(Pattern seedPattern) - { - var rotations = new List<(Drawing drawing, double rotation)>(); - var drawings = new List(); - - foreach (var seedPart in seedPattern.Parts) - { - var found = false; - - foreach (var d in drawings) - { - if (d == seedPart.BaseDrawing) - { - found = true; - break; - } - } - - if (!found) - drawings.Add(seedPart.BaseDrawing); - } - - foreach (var drawing in drawings) - { - rotations.Add((drawing, 0)); - rotations.Add((drawing, Angle.HalfPI)); - } - - foreach (var seedPart in seedPattern.Parts) - { - var skip = false; - - foreach (var (d, r) in rotations) - { - if (d == seedPart.BaseDrawing && r.IsEqualTo(seedPart.Rotation)) - { - skip = true; - break; - } - } - - if (!skip) - rotations.Add((seedPart.BaseDrawing, seedPart.Rotation)); - } - - return rotations; - } - - /// - /// Tries all rotation candidates in both directions in parallel, returns the - /// fill with the most parts. - /// - private List FindBestFill(List<(Drawing drawing, double rotation)> rotations, Box strip) - { - var bag = new System.Collections.Concurrent.ConcurrentBag>(); - - Parallel.ForEach(rotations, entry => - { - var filler = new FillLinear(strip, PartSpacing); - var h = filler.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal); - var v = filler.Fill(entry.drawing, entry.rotation, NestDirection.Vertical); - - if (h != null && h.Count > 0) - bag.Add(h); - - if (v != null && v.Count > 0) - bag.Add(v); - }); - - List best = null; - - foreach (var candidate in bag) - { - if (best == null || candidate.Count > best.Count) - best = candidate; - } - - return best ?? new List(); - } - /// /// Fills a single row of identical parts along one axis using geometry-aware spacing. /// diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs index 37255ba..e6de8b2 100644 --- a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs +++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs @@ -2,6 +2,7 @@ using OpenNest.Geometry; using System; using System.Collections.Generic; using System.Threading; +using System.Threading.Tasks; namespace OpenNest.Engine.Fill { @@ -74,16 +75,33 @@ namespace OpenNest.Engine.Fill Func> shrinkWrapper = (ni, box) => { var target = ni.Quantity > 0 ? ni.Quantity : 0; - var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token, - targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar); - var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token, - targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar); + + // 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.Height, token, + targetCount: target, progress: progress, plateNumber: plateNumber, placedParts: placedSoFar), + () => widthResult = ShrinkFiller.Shrink(fillFunc, 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) { @@ -134,5 +152,171 @@ namespace OpenNest.Engine.Fill 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) + { + if (parts == null || parts.Count <= 1) + return; + + // Sort parts by Left edge for grouping. + parts.Sort((a, b) => a.BoundingBox.Left.CompareTo(b.BoundingBox.Left)); + + // Group parts into columns by X overlap. + var columns = new List>(); + var column = new List { parts[0] }; + var columnRight = parts[0].BoundingBox.Right; + + for (var i = 1; i < parts.Count; i++) + { + if (parts[i].BoundingBox.Left > columnRight + spacing / 2) + { + columns.Add(column); + column = new List { parts[i] }; + columnRight = parts[i].BoundingBox.Right; + } + else + { + column.Add(parts[i]); + if (parts[i].BoundingBox.Right > columnRight) + columnRight = parts[i].BoundingBox.Right; + } + } + columns.Add(column); + + if (columns.Count <= 1) + return; + + // Measure inter-column gap from original layout. + var gap = MinLeft(columns[1]) - MaxRight(columns[0]); + + // Sort columns by height ascending (shortest first). + columns.Sort((a, b) => MaxTop(a).CompareTo(MaxTop(b))); + + // Reposition columns left-to-right. + var x = parts[0].BoundingBox.Left; // parts already sorted by Left + + foreach (var col in columns) + { + var colLeft = MinLeft(col); + var dx = x - colLeft; + + if (System.Math.Abs(dx) > OpenNest.Math.Tolerance.Epsilon) + { + var offset = new Vector(dx, 0); + foreach (var part in col) + part.Offset(offset); + } + + x = MaxRight(col) + gap; + } + + // Rebuild the parts list in column order. + parts.Clear(); + foreach (var col in columns) + parts.AddRange(col); + } + + /// + /// 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) + { + if (parts == null || parts.Count <= 1) + return; + + // Sort parts by Bottom edge for grouping. + parts.Sort((a, b) => a.BoundingBox.Bottom.CompareTo(b.BoundingBox.Bottom)); + + // Group parts into rows by Y overlap. + var rows = new List>(); + var row = new List { parts[0] }; + var rowTop = parts[0].BoundingBox.Top; + + for (var i = 1; i < parts.Count; i++) + { + if (parts[i].BoundingBox.Bottom > rowTop + spacing / 2) + { + rows.Add(row); + row = new List { parts[i] }; + rowTop = parts[i].BoundingBox.Top; + } + else + { + row.Add(parts[i]); + if (parts[i].BoundingBox.Top > rowTop) + rowTop = parts[i].BoundingBox.Top; + } + } + rows.Add(row); + + if (rows.Count <= 1) + return; + + // Measure inter-row gap from original layout. + var gap = MinBottom(rows[1]) - MaxTop(rows[0]); + + // Sort rows by width ascending (narrowest first). + rows.Sort((a, b) => MaxRight(a).CompareTo(MaxRight(b))); + + // Reposition rows bottom-to-top. + var y = parts[0].BoundingBox.Bottom; // parts already sorted by Bottom + + foreach (var r in rows) + { + var rowBottom = MinBottom(r); + var dy = y - rowBottom; + + if (System.Math.Abs(dy) > OpenNest.Math.Tolerance.Epsilon) + { + var offset = new Vector(0, dy); + foreach (var part in r) + part.Offset(offset); + } + + y = MaxTop(r) + gap; + } + + // Rebuild the parts list in row order. + parts.Clear(); + foreach (var r in rows) + parts.AddRange(r); + } + + 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; + } } } diff --git a/OpenNest.Engine/Fill/PairFiller.cs b/OpenNest.Engine/Fill/PairFiller.cs index 33e6fc3..ba620b6 100644 --- a/OpenNest.Engine/Fill/PairFiller.cs +++ b/OpenNest.Engine/Fill/PairFiller.cs @@ -10,6 +10,12 @@ using System.Threading; namespace OpenNest.Engine.Fill { + public class PairFillResult + { + public List Parts { get; set; } = new List(); + public List BestFits { get; set; } + } + /// /// Fills a work area using interlocking part pairs from BestFitCache. /// @@ -24,36 +30,40 @@ namespace OpenNest.Engine.Fill private readonly Size plateSize; private readonly double partSpacing; - /// - /// The best-fit results computed during the last Fill call. - /// Available after Fill returns so callers can reuse without recomputing. - /// - public List BestFits { get; private set; } - public PairFiller(Size plateSize, double partSpacing) { this.plateSize = plateSize; this.partSpacing = partSpacing; } - public List Fill(NestItem item, Box workArea, + public PairFillResult Fill(NestItem item, Box workArea, int plateNumber = 0, CancellationToken token = default, IProgress progress = null) { - BestFits = BestFitCache.GetOrCompute( + var bestFits = BestFitCache.GetOrCompute( item.Drawing, plateSize.Length, plateSize.Width, partSpacing); - var candidates = SelectPairCandidates(BestFits, workArea); - Debug.WriteLine($"[PairFiller] Total: {BestFits.Count}, Kept: {BestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); + var candidates = SelectPairCandidates(bestFits, workArea); + Debug.WriteLine($"[PairFiller] Total: {bestFits.Count}, Kept: {bestFits.Count(r => r.Keep)}, Trying: {candidates.Count}"); Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}"); var targetCount = item.Quantity > 0 ? item.Quantity : 0; - var effectiveWorkArea = workArea; + var parts = EvaluateCandidates(candidates, item.Drawing, workArea, targetCount, + plateNumber, token, progress); + return new PairFillResult { Parts = parts, BestFits = bestFits }; + } + + private List EvaluateCandidates( + List candidates, Drawing drawing, + Box workArea, int targetCount, + int plateNumber, CancellationToken token, IProgress progress) + { List best = null; var bestScore = default(FillScore); var sinceImproved = 0; + var effectiveWorkArea = workArea; try { @@ -61,34 +71,15 @@ namespace OpenNest.Engine.Fill { token.ThrowIfCancellationRequested(); - var filled = EvaluateCandidate(candidates[i], item.Drawing, effectiveWorkArea); + var filled = EvaluateCandidate(candidates[i], drawing, effectiveWorkArea); + var score = FillScore.Compute(filled, effectiveWorkArea); - if (filled != null && filled.Count > 0) + if (score > bestScore) { - var score = FillScore.Compute(filled, effectiveWorkArea); - if (best == null || score > bestScore) - { - best = filled; - bestScore = score; - sinceImproved = 0; - - // If we exceeded the target, reduce the work area for - // subsequent candidates by trimming excess parts and - // measuring the tighter bounding box. - if (targetCount > 0 && filled.Count > targetCount) - { - var reduced = ReduceWorkArea(filled, targetCount, workArea); - if (reduced.Area() < effectiveWorkArea.Area()) - { - effectiveWorkArea = reduced; - Debug.WriteLine($"[PairFiller] Reduced work area to {effectiveWorkArea.Width:F2}x{effectiveWorkArea.Length:F2} (trimmed to {targetCount + 1} parts)"); - } - } - } - else - { - sinceImproved++; - } + best = filled; + bestScore = score; + sinceImproved = 0; + effectiveWorkArea = TryReduceWorkArea(filled, targetCount, workArea, effectiveWorkArea); } else { @@ -114,6 +105,19 @@ namespace OpenNest.Engine.Fill return best ?? new List(); } + private static Box TryReduceWorkArea(List parts, int targetCount, Box workArea, Box effectiveWorkArea) + { + if (targetCount <= 0 || parts.Count <= targetCount) + return effectiveWorkArea; + + var reduced = ReduceWorkArea(parts, targetCount, workArea); + if (reduced.Area() >= effectiveWorkArea.Area()) + return effectiveWorkArea; + + Debug.WriteLine($"[PairFiller] Reduced work area to {reduced.Width:F2}x{reduced.Length:F2} (trimmed to {targetCount + 1} parts)"); + return reduced; + } + /// /// Given parts that exceed targetCount, sorts by BoundingBox.Top descending, /// removes parts from the top until exactly targetCount remain, then returns @@ -124,12 +128,10 @@ namespace OpenNest.Engine.Fill if (parts.Count <= targetCount) return workArea; - // Sort by Top descending — highest parts get trimmed first. var sorted = parts .OrderByDescending(p => p.BoundingBox.Top) .ToList(); - // Remove from the top until exactly targetCount remain. var trimCount = sorted.Count - targetCount; var remaining = sorted.Skip(trimCount).ToList(); @@ -144,22 +146,23 @@ namespace OpenNest.Engine.Fill { var pairParts = candidate.BuildParts(drawing); var engine = new FillLinear(workArea, partSpacing); + var angles = BuildTilingAngles(candidate); + return FillHelpers.FillPattern(engine, pairParts, angles, workArea); + } - var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0); - var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI); - engine.RemainderPatterns = new List { p0, p90 }; - - // Include the pair's rotating calipers optimal rotation angle - // alongside the hull edge angles for tiling. + private static List BuildTilingAngles(BestFitResult candidate) + { var angles = new List(candidate.HullAngles); var optAngle = -candidate.OptimalRotation; + if (!angles.Any(a => a.IsEqualTo(optAngle))) angles.Add(optAngle); + var optAngle90 = Angle.NormalizeRad(optAngle + Angle.HalfPI); if (!angles.Any(a => a.IsEqualTo(optAngle90))) angles.Add(optAngle90); - return FillHelpers.FillPattern(engine, pairParts, angles, workArea); + return angles; } private List SelectPairCandidates(List bestFits, Box workArea) @@ -191,7 +194,41 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"); } + SortByEstimatedCount(top, workArea); + return top; } + + private void SortByEstimatedCount(List candidates, Box workArea) + { + var w = workArea.Width; + var l = workArea.Length; + + candidates.Sort((a, b) => + { + var aCount = EstimateTileCount(a, w, l); + var bCount = EstimateTileCount(b, w, l); + + if (aCount != bCount) + return bCount.CompareTo(aCount); + + return b.Utilization.CompareTo(a.Utilization); + }); + } + + private int EstimateTileCount(BestFitResult r, double areaW, double areaL) + { + var h = EstimateCount(r.BoundingWidth, r.BoundingHeight, areaW, areaL); + var v = EstimateCount(r.BoundingHeight, r.BoundingWidth, areaW, areaL); + return System.Math.Max(h, v); + } + + private int EstimateCount(double pairW, double pairH, double areaW, double areaL) + { + if (pairW <= 0 || pairH <= 0) return 0; + var cols = (int)((areaW + partSpacing) / (pairW + partSpacing)); + var rows = (int)((areaL + partSpacing) / (pairH + partSpacing)); + return cols * rows * 2; + } } } diff --git a/OpenNest.Engine/Fill/RemnantFiller.cs b/OpenNest.Engine/Fill/RemnantFiller.cs index 94d8b98..9251b82 100644 --- a/OpenNest.Engine/Fill/RemnantFiller.cs +++ b/OpenNest.Engine/Fill/RemnantFiller.cs @@ -37,76 +37,106 @@ namespace OpenNest.Engine.Fill return new List(); var allParts = new List(); - var madeProgress = true; // Track quantities locally — do not mutate the input NestItem objects. - var localQty = new Dictionary(); - foreach (var item in items) - localQty[item.Drawing.Name] = item.Quantity; + var localQty = BuildLocalQuantities(items); - while (madeProgress && !token.IsCancellationRequested) + while (!token.IsCancellationRequested) { - madeProgress = false; - - var minRemnantDim = double.MaxValue; - foreach (var item in items) - { - var qty = localQty[item.Drawing.Name]; - if (qty <= 0) - continue; - var bb = item.Drawing.Program.BoundingBox(); - var dim = System.Math.Min(bb.Width, bb.Length); - if (dim < minRemnantDim) - minRemnantDim = dim; - } - - if (minRemnantDim == double.MaxValue) + var minDim = FindMinItemDimension(items, localQty); + if (minDim == double.MaxValue) break; - var freeBoxes = finder.FindRemnants(minRemnantDim); - + var freeBoxes = finder.FindRemnants(minDim); if (freeBoxes.Count == 0) break; - foreach (var item in items) - { - if (token.IsCancellationRequested) - break; - - var qty = localQty[item.Drawing.Name]; - if (qty == 0) - continue; - - var itemBbox = item.Drawing.Program.BoundingBox(); - var minItemDim = System.Math.Min(itemBbox.Width, itemBbox.Length); - - foreach (var box in freeBoxes) - { - if (System.Math.Min(box.Width, box.Length) < minItemDim) - continue; - - var fillItem = new NestItem { Drawing = item.Drawing, Quantity = qty }; - var remnantParts = fillFunc(fillItem, box); - - if (remnantParts != null && remnantParts.Count > 0) - { - allParts.AddRange(remnantParts); - localQty[item.Drawing.Name] = System.Math.Max(0, qty - remnantParts.Count); - - foreach (var p in remnantParts) - finder.AddObstacle(p.BoundingBox.Offset(spacing)); - - madeProgress = true; - break; - } - } - - if (madeProgress) - break; - } + if (!TryFillOneItem(items, freeBoxes, localQty, fillFunc, allParts, token)) + break; } return allParts; } + + private static Dictionary BuildLocalQuantities(List items) + { + var localQty = new Dictionary(items.Count); + foreach (var item in items) + localQty[item.Drawing.Name] = item.Quantity; + return localQty; + } + + private static double FindMinItemDimension(List items, Dictionary localQty) + { + var minDim = double.MaxValue; + foreach (var item in items) + { + if (localQty[item.Drawing.Name] <= 0) + continue; + var bb = item.Drawing.Program.BoundingBox(); + var dim = System.Math.Min(bb.Width, bb.Length); + if (dim < minDim) + minDim = dim; + } + return minDim; + } + + private bool TryFillOneItem( + List items, + List freeBoxes, + Dictionary localQty, + Func> fillFunc, + List allParts, + CancellationToken token) + { + foreach (var item in items) + { + if (token.IsCancellationRequested) + return false; + + var qty = localQty[item.Drawing.Name]; + if (qty <= 0) + continue; + + var placed = TryFillInRemnants(item, qty, freeBoxes, fillFunc); + if (placed == null) + continue; + + 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)); + + return true; + } + + return false; + } + + private static List TryFillInRemnants( + NestItem item, + int qty, + List freeBoxes, + Func> fillFunc) + { + var itemBbox = item.Drawing.Program.BoundingBox(); + + foreach (var box in freeBoxes) + { + var fitsNormal = box.Width >= itemBbox.Width && box.Length >= itemBbox.Length; + var fitsRotated = box.Width >= itemBbox.Length && box.Length >= itemBbox.Width; + if (!fitsNormal && !fitsRotated) + continue; + + var fillItem = new NestItem { Drawing = item.Drawing, Quantity = qty }; + var parts = fillFunc(fillItem, box); + + if (parts != null && parts.Count > 0) + return parts; + } + + return null; + } } } diff --git a/OpenNest.Engine/Strategies/PairsFillStrategy.cs b/OpenNest.Engine/Strategies/PairsFillStrategy.cs index 118c732..862390e 100644 --- a/OpenNest.Engine/Strategies/PairsFillStrategy.cs +++ b/OpenNest.Engine/Strategies/PairsFillStrategy.cs @@ -15,9 +15,9 @@ namespace OpenNest.Engine.Strategies var result = filler.Fill(context.Item, context.WorkArea, context.PlateNumber, context.Token, context.Progress); - context.SharedState["BestFits"] = filler.BestFits; + context.SharedState["BestFits"] = result.BestFits; - return result; + return result.Parts; } } } diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 675307c..efe69e2 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -77,12 +77,12 @@ namespace OpenNest // Phase 1: Iterative shrink-fill for multi-quantity items. if (fillItems.Count > 0) { - // Inner fills are silent — ShrinkFiller manages progress reporting - // to avoid overlapping layouts from per-angle/per-strategy reports. + // Pass progress through so the UI shows intermediate results + // during the initial BestFitCache computation and fill phases. Func> fillFunc = (ni, b) => { var inner = new DefaultNestEngine(Plate); - return inner.Fill(ni, b, null, token); + return inner.Fill(ni, b, progress, token); }; var shrinkResult = IterativeShrinkFiller.Fill( @@ -91,6 +91,14 @@ namespace OpenNest allParts.AddRange(shrinkResult.Parts); + // Compact placed parts toward the origin to close gaps. + if (allParts.Count > 1) + { + var noObstacles = new List(); + Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Left); + Compactor.Push(allParts, noObstacles, workArea, Plate.PartSpacing, PushDirection.Down); + } + // Add unfilled items to pack list. packItems.AddRange(shrinkResult.Leftovers); } diff --git a/OpenNest.Tests/PairFillerTests.cs b/OpenNest.Tests/PairFillerTests.cs index 06c654d..d138e2d 100644 --- a/OpenNest.Tests/PairFillerTests.cs +++ b/OpenNest.Tests/PairFillerTests.cs @@ -24,11 +24,10 @@ public class PairFillerTests var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; var workArea = new Box(0, 0, 120, 60); - var parts = filler.Fill(item, workArea); + var result = filler.Fill(item, workArea); - Assert.NotNull(parts); - // Pair filling may or may not find interlocking pairs for rectangles, - // but should return a non-null list. + Assert.NotNull(result.Parts); + Assert.NotNull(result.BestFits); } [Fact] @@ -39,10 +38,10 @@ public class PairFillerTests var item = new NestItem { Drawing = MakeRectDrawing(20, 20) }; var workArea = new Box(0, 0, 10, 10); - var parts = filler.Fill(item, workArea); + var result = filler.Fill(item, workArea); - Assert.NotNull(parts); - Assert.Empty(parts); + Assert.NotNull(result.Parts); + Assert.Empty(result.Parts); } [Fact] @@ -56,9 +55,8 @@ public class PairFillerTests var item = new NestItem { Drawing = MakeRectDrawing(20, 10) }; var workArea = new Box(0, 0, 120, 60); - var parts = filler.Fill(item, workArea, token: cts.Token); + var result = filler.Fill(item, workArea, token: cts.Token); - // Should return empty or partial — not throw - Assert.NotNull(parts); + Assert.NotNull(result.Parts); } }