refactor: standardize fill strategy progress reporting via FillContext

Strategies and fillers previously called NestEngineBase.ReportProgress
directly, each constructing ProgressReport structs with phase, plate
number, and work area manually. Some strategies (RectBestFit) reported
nothing at all. This made progress updates inconsistent and flakey.

Add FillContext.ReportProgress(parts, description) as the single
standard method for intermediate progress. RunPipeline sets ActivePhase
before each strategy, and the context handles common fields. Lower-level
fillers (PairFiller, FillExtents, StripeFiller) now accept an
Action<List<Part>, string> callback instead of raw IProgress, removing
their coupling to NestEngineBase and ProgressReport.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-03 23:21:48 -04:00
parent 943c262ad2
commit ef15421915
9 changed files with 36 additions and 61 deletions

View File

@@ -295,6 +295,7 @@ namespace OpenNest
foreach (var strategy in FillStrategyRegistry.Strategies)
{
context.Token.ThrowIfCancellationRequested();
context.ActivePhase = strategy.Phase;
var sw = Stopwatch.StartNew();
var result = strategy.Fill(context);

View File

@@ -24,10 +24,8 @@ namespace OpenNest.Engine.Fill
}
public List<Part> Fill(Drawing drawing, double rotationAngle = 0,
int plateNumber = 0,
CancellationToken token = default,
IProgress<NestProgress> progress = null,
List<Engine.BestFit.BestFitResult> bestFits = null)
Action<List<Part>, string> reportProgress = null)
{
var pair = BuildPair(drawing, rotationAngle);
if (pair == null)
@@ -37,14 +35,7 @@ namespace OpenNest.Engine.Fill
if (column.Count == 0)
return new List<Part>();
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = column,
WorkArea = workArea,
Description = $"Extents: initial column {column.Count} parts",
});
reportProgress?.Invoke(column, $"Extents: initial column {column.Count} parts");
var adjusted = AdjustColumn(pair.Value, column, token);
@@ -56,25 +47,11 @@ namespace OpenNest.Engine.Fill
adjusted = column;
}
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = adjusted,
WorkArea = workArea,
Description = $"Extents: column {adjusted.Count} parts",
});
reportProgress?.Invoke(adjusted, $"Extents: column {adjusted.Count} parts");
var result = RepeatColumns(adjusted, token);
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Extents,
PlateNumber = plateNumber,
Parts = result,
WorkArea = workArea,
Description = $"Extents: {result.Count} parts total",
});
reportProgress?.Invoke(result, $"Extents: {result.Count} parts total");
return result;
}

View File

@@ -45,9 +45,8 @@ namespace OpenNest.Engine.Fill
}
public PairFillResult Fill(NestItem item, Box workArea,
int plateNumber = 0,
CancellationToken token = default,
IProgress<NestProgress> progress = null)
Action<List<Part>, string> reportProgress = null)
{
var bestFits = BestFitCache.GetOrCompute(
item.Drawing, plateSize.Length, plateSize.Width, partSpacing);
@@ -58,7 +57,7 @@ namespace OpenNest.Engine.Fill
var targetCount = item.Quantity > 0 ? item.Quantity : 0;
var parts = EvaluateCandidates(candidates, item.Drawing, workArea, targetCount,
plateNumber, token, progress);
token, reportProgress);
return new PairFillResult { Parts = parts, BestFits = bestFits };
}
@@ -66,7 +65,7 @@ namespace OpenNest.Engine.Fill
private List<Part> EvaluateCandidates(
List<BestFitResult> candidates, Drawing drawing,
Box workArea, int targetCount,
int plateNumber, CancellationToken token, IProgress<NestProgress> progress)
CancellationToken token, Action<List<Part>, string> reportProgress)
{
List<Part> best = null;
var sinceImproved = 0;
@@ -112,14 +111,8 @@ namespace OpenNest.Engine.Fill
sinceImproved++;
}
NestEngineBase.ReportProgress(progress, new ProgressReport
{
Phase = NestPhase.Pairs,
PlateNumber = plateNumber,
Parts = best,
WorkArea = workArea,
Description = $"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts",
});
reportProgress?.Invoke(best,
$"Pairs: {batchStart + j + 1}/{candidates.Count} candidates, best = {best?.Count ?? 0} parts");
}
if (batchEnd >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)

View File

@@ -95,14 +95,8 @@ public class StripeFiller
}
}
NestEngineBase.ReportProgress(_context.Progress, new ProgressReport
{
Phase = NestPhase.Custom,
PlateNumber = _context.PlateNumber,
Parts = bestParts,
WorkArea = workArea,
Description = $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts",
});
_context.ReportProgress(bestParts,
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts");
}
return bestParts ?? new List<Part>();

View File

@@ -24,8 +24,8 @@ namespace OpenNest.Engine.Strategies
return FillHelpers.BestOverAngles(context, angles,
angle => filler.Fill(context.Item.Drawing, angle,
context.PlateNumber, context.Token, context.Progress),
NestPhase.Extents, "Extents");
context.Token, context.ReportProgress),
"Extents");
}
}
}

View File

@@ -23,9 +23,26 @@ namespace OpenNest.Engine.Strategies
/// <summary>For progress reporting only; comparisons use Policy.Comparer.</summary>
public FillScore CurrentBestScore { get; set; }
public NestPhase WinnerPhase { get; set; }
public NestPhase ActivePhase { get; set; }
public List<PhaseResult> PhaseResults { get; } = new();
public List<AngleResult> AngleResults { get; } = new();
public Dictionary<string, object> SharedState { get; } = new();
/// <summary>
/// Standard progress reporting for strategies and fillers. Reports intermediate
/// results using the current ActivePhase, PlateNumber, and WorkArea.
/// </summary>
public void ReportProgress(List<Part> parts, string description)
{
NestEngineBase.ReportProgress(Progress, new ProgressReport
{
Phase = ActivePhase,
PlateNumber = PlateNumber,
Parts = parts,
WorkArea = WorkArea,
Description = description,
});
}
}
}

View File

@@ -113,13 +113,12 @@ namespace OpenNest.Engine.Strategies
/// <summary>
/// Sweeps a list of angles, calling fillAtAngle for each, and returns
/// the best result according to the context's comparer. Handles
/// cancellation and progress reporting.
/// cancellation and progress reporting via context.ReportProgress.
/// </summary>
public static List<Part> BestOverAngles(
FillContext context,
IReadOnlyList<double> angles,
Func<double, List<Part>> fillAtAngle,
NestPhase phase,
string phaseLabel)
{
var workArea = context.WorkArea;
@@ -140,14 +139,8 @@ namespace OpenNest.Engine.Strategies
best = result;
}
NestEngineBase.ReportProgress(context.Progress, new ProgressReport
{
Phase = phase,
PlateNumber = context.PlateNumber,
Parts = best,
WorkArea = workArea,
Description = $"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts",
});
context.ReportProgress(best,
$"{phaseLabel}: {i + 1}/{angles.Count} angles, {angleDeg:F0}° best = {best?.Count ?? 0} parts");
}
return best ?? new List<Part>();

View File

@@ -40,7 +40,7 @@ namespace OpenNest.Engine.Strategies
return result;
},
NestPhase.Linear, "Linear");
"Linear");
}
}
}

View File

@@ -30,7 +30,7 @@ namespace OpenNest.Engine.Strategies
var dedup = GridDedup.GetOrCreate(context.SharedState);
var filler = new PairFiller(context.Plate, comparer, dedup);
var result = filler.Fill(context.Item, context.WorkArea,
context.PlateNumber, context.Token, context.Progress);
context.Token, context.ReportProgress);
context.SharedState["BestFits"] = result.BestFits;