fix(engine): move progress reporting from inner fills to ShrinkFiller

StripNestEngine was passing progress directly to DefaultNestEngine.Fill
inside the ShrinkFiller loop, causing every per-angle/per-strategy report
to update the UI with overlapping layouts in the same work area.

Now inner fills are silent (null progress) and ShrinkFiller reports its
own progress when the best layout improves. IterativeShrinkFiller tracks
placed parts across items and includes them in reports. The trial box is
reported before the fill starts so the work area border updates immediately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 12:43:35 -04:00
parent ca35945c13
commit 8bfc13d529
3 changed files with 66 additions and 7 deletions
+27 -4
View File
@@ -28,7 +28,9 @@ namespace OpenNest.Engine.Fill
Box workArea, Box workArea,
Func<NestItem, Box, List<Part>> fillFunc, Func<NestItem, Box, List<Part>> fillFunc,
double spacing, double spacing,
CancellationToken token = default) CancellationToken token = default,
IProgress<NestProgress> progress = null,
int plateNumber = 0)
{ {
if (items == null || items.Count == 0) if (items == null || items.Count == 0)
return new IterativeShrinkResult(); return new IterativeShrinkResult();
@@ -65,16 +67,37 @@ namespace OpenNest.Engine.Fill
var filler = new RemnantFiller(workArea, spacing); var filler = new RemnantFiller(workArea, spacing);
// Track parts placed by previous items so ShrinkFiller can
// include them in progress reports.
var placedSoFar = new List<Part>();
Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) => Func<NestItem, Box, List<Part>> shrinkWrapper = (ni, box) =>
{ {
var target = ni.Quantity > 0 ? ni.Quantity : 0; var target = ni.Quantity > 0 ? ni.Quantity : 0;
var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token, targetCount: target); var heightResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Height, token,
var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token, targetCount: target); 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);
var heightScore = FillScore.Compute(heightResult.Parts, box); var heightScore = FillScore.Compute(heightResult.Parts, box);
var widthScore = FillScore.Compute(widthResult.Parts, box); var widthScore = FillScore.Compute(widthResult.Parts, box);
return widthScore > heightScore ? widthResult.Parts : heightResult.Parts; var best = widthScore > heightScore ? widthResult.Parts : heightResult.Parts;
// 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<Part>(placedSoFar.Count + best.Count);
allParts.AddRange(placedSoFar);
allParts.AddRange(best);
NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber,
allParts, box, $"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); var placed = filler.FillItems(workItems, shrinkWrapper, token);
+34 -1
View File
@@ -29,7 +29,10 @@ namespace OpenNest.Engine.Fill
ShrinkAxis axis, ShrinkAxis axis,
CancellationToken token = default, CancellationToken token = default,
int maxIterations = 20, int maxIterations = 20,
int targetCount = 0) int targetCount = 0,
IProgress<NestProgress> progress = null,
int plateNumber = 0,
List<Part> placedParts = null)
{ {
// If a target count is specified, estimate a smaller starting box // If a target count is specified, estimate a smaller starting box
// to avoid an expensive full-area fill. // to avoid an expensive full-area fill.
@@ -60,6 +63,8 @@ namespace OpenNest.Engine.Fill
var bestParts = parts; var bestParts = parts;
var bestDim = MeasureDimension(parts, box, axis); var bestDim = MeasureDimension(parts, box, axis);
ReportShrinkProgress(progress, plateNumber, placedParts, bestParts, box, axis, bestDim);
for (var i = 0; i < maxIterations; i++) for (var i = 0; i < maxIterations; i++)
{ {
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
@@ -73,6 +78,10 @@ namespace OpenNest.Engine.Fill
? new Box(box.X, box.Y, trialDim, box.Length) ? new Box(box.X, box.Y, trialDim, box.Length)
: new Box(box.X, box.Y, box.Width, trialDim); : new Box(box.X, box.Y, box.Width, trialDim);
// Report the trial box before the fill so the UI updates the
// work area border immediately rather than after the fill completes.
ReportShrinkProgress(progress, plateNumber, placedParts, bestParts, trialBox, axis, trialDim);
var trialParts = fillFunc(item, trialBox); var trialParts = fillFunc(item, trialBox);
if (trialParts == null || trialParts.Count < shrinkTarget) if (trialParts == null || trialParts.Count < shrinkTarget)
@@ -80,11 +89,35 @@ namespace OpenNest.Engine.Fill
bestParts = trialParts; bestParts = trialParts;
bestDim = MeasureDimension(trialParts, box, axis); bestDim = MeasureDimension(trialParts, box, axis);
ReportShrinkProgress(progress, plateNumber, placedParts, bestParts, trialBox, axis, bestDim);
} }
return new ShrinkResult { Parts = bestParts, Dimension = bestDim }; return new ShrinkResult { Parts = bestParts, Dimension = bestDim };
} }
private static void ReportShrinkProgress(
IProgress<NestProgress> progress, int plateNumber,
List<Part> placedParts, List<Part> bestParts,
Box workArea, ShrinkAxis axis, double dim)
{
if (progress == null)
return;
var allParts = placedParts != null && placedParts.Count > 0
? new List<Part>(placedParts.Count + bestParts.Count)
: new List<Part>(bestParts.Count);
if (placedParts != null && placedParts.Count > 0)
allParts.AddRange(placedParts);
allParts.AddRange(bestParts);
var desc = $"Shrink {axis}: {bestParts.Count} parts, dim={dim:F1}";
NestEngineBase.ReportProgress(progress, NestPhase.Custom, plateNumber,
allParts, workArea, desc);
}
/// <summary> /// <summary>
/// Uses FillBestFit (fast rectangle packing) to estimate a starting box /// Uses FillBestFit (fast rectangle packing) to estimate a starting box
/// that fits roughly the target count. Scales the shrink axis proportionally /// that fits roughly the target count. Scales the shrink axis proportionally
+5 -2
View File
@@ -77,14 +77,17 @@ namespace OpenNest
// Phase 1: Iterative shrink-fill for multi-quantity items. // Phase 1: Iterative shrink-fill for multi-quantity items.
if (fillItems.Count > 0) if (fillItems.Count > 0)
{ {
// Inner fills are silent — ShrinkFiller manages progress reporting
// to avoid overlapping layouts from per-angle/per-strategy reports.
Func<NestItem, Box, List<Part>> fillFunc = (ni, b) => Func<NestItem, Box, List<Part>> fillFunc = (ni, b) =>
{ {
var inner = new DefaultNestEngine(Plate); var inner = new DefaultNestEngine(Plate);
return inner.Fill(ni, b, progress, token); return inner.Fill(ni, b, null, token);
}; };
var shrinkResult = IterativeShrinkFiller.Fill( var shrinkResult = IterativeShrinkFiller.Fill(
fillItems, workArea, fillFunc, Plate.PartSpacing, token); fillItems, workArea, fillFunc, Plate.PartSpacing, token,
progress, PlateNumber);
allParts.AddRange(shrinkResult.Parts); allParts.AddRange(shrinkResult.Parts);