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