From 8bfc13d529f5793051370f9e7a967edc624e0530 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Thu, 19 Mar 2026 12:43:35 -0400 Subject: [PATCH] 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) --- OpenNest.Engine/Fill/IterativeShrinkFiller.cs | 31 +++++++++++++--- OpenNest.Engine/Fill/ShrinkFiller.cs | 35 ++++++++++++++++++- OpenNest.Engine/StripNestEngine.cs | 7 ++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs index e34a5e7..37255ba 100644 --- a/OpenNest.Engine/Fill/IterativeShrinkFiller.cs +++ b/OpenNest.Engine/Fill/IterativeShrinkFiller.cs @@ -28,7 +28,9 @@ namespace OpenNest.Engine.Fill Box workArea, Func> fillFunc, double spacing, - CancellationToken token = default) + CancellationToken token = default, + IProgress progress = null, + int plateNumber = 0) { if (items == null || items.Count == 0) return new IterativeShrinkResult(); @@ -65,16 +67,37 @@ namespace OpenNest.Engine.Fill var filler = new RemnantFiller(workArea, spacing); + // Track parts placed by previous items so ShrinkFiller can + // include them in progress reports. + var placedSoFar = new List(); + 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); - var widthResult = ShrinkFiller.Shrink(fillFunc, ni, box, spacing, ShrinkAxis.Width, token, targetCount: target); + 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); var heightScore = FillScore.Compute(heightResult.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(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); diff --git a/OpenNest.Engine/Fill/ShrinkFiller.cs b/OpenNest.Engine/Fill/ShrinkFiller.cs index 84a86f2..f33a762 100644 --- a/OpenNest.Engine/Fill/ShrinkFiller.cs +++ b/OpenNest.Engine/Fill/ShrinkFiller.cs @@ -29,7 +29,10 @@ namespace OpenNest.Engine.Fill ShrinkAxis axis, CancellationToken token = default, int maxIterations = 20, - int targetCount = 0) + int targetCount = 0, + IProgress progress = null, + int plateNumber = 0, + List placedParts = null) { // If a target count is specified, estimate a smaller starting box // to avoid an expensive full-area fill. @@ -60,6 +63,8 @@ namespace OpenNest.Engine.Fill var bestParts = parts; var bestDim = MeasureDimension(parts, box, axis); + ReportShrinkProgress(progress, plateNumber, placedParts, bestParts, box, axis, bestDim); + for (var i = 0; i < maxIterations; i++) { 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, 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); if (trialParts == null || trialParts.Count < shrinkTarget) @@ -80,11 +89,35 @@ namespace OpenNest.Engine.Fill bestParts = trialParts; bestDim = MeasureDimension(trialParts, box, axis); + + ReportShrinkProgress(progress, plateNumber, placedParts, bestParts, trialBox, axis, bestDim); } return new ShrinkResult { Parts = bestParts, Dimension = bestDim }; } + private static void ReportShrinkProgress( + IProgress progress, int plateNumber, + List placedParts, List bestParts, + Box workArea, ShrinkAxis axis, double dim) + { + if (progress == null) + return; + + var allParts = placedParts != null && placedParts.Count > 0 + ? new List(placedParts.Count + bestParts.Count) + : new List(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); + } + /// /// Uses FillBestFit (fast rectangle packing) to estimate a starting box /// that fits roughly the target count. Scales the shrink axis proportionally diff --git a/OpenNest.Engine/StripNestEngine.cs b/OpenNest.Engine/StripNestEngine.cs index 573e3cf..675307c 100644 --- a/OpenNest.Engine/StripNestEngine.cs +++ b/OpenNest.Engine/StripNestEngine.cs @@ -77,14 +77,17 @@ 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. Func> fillFunc = (ni, b) => { var inner = new DefaultNestEngine(Plate); - return inner.Fill(ni, b, progress, token); + return inner.Fill(ni, b, null, token); }; var shrinkResult = IterativeShrinkFiller.Fill( - fillItems, workArea, fillFunc, Plate.PartSpacing, token); + fillItems, workArea, fillFunc, Plate.PartSpacing, token, + progress, PlateNumber); allParts.AddRange(shrinkResult.Parts);