mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 02:41:06 -04:00
feat(engine): offer a box layout before best-fit pairs in Fill
Interactive Fill ran the best-fit pair phase first. The first pair computation for a drawing can take about a minute, and until it finished the progress dialog showed no layout, Accept stayed disabled and the status text did not change. The fill now packs the part boxes (RectBestFit) before the pair phase and reports that layout, so Accept is available at once. The layout is only a fallback: strategies still run in their usual order, and a completed fill picks the same winner as before. While the pairs are computed, the detail line reads "Computing best-fit pairs (may take a minute)...". Accept or Stop now stops the wait. The computation keeps running in the background and is cached for the next fill. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
de845a5818
commit
859d10b31d
10 files changed
+272
-16
No files matched your search
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
@@ -40,6 +41,38 @@ namespace OpenNest.Engine.BestFit
|
||||
return entry.Filtered.GetOrAdd(key, _ => FilterForSize(candidates, plateWidth, plateHeight));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Like <see cref="GetOrCompute(Drawing, double, double, double)"/>, but cancelling stops
|
||||
/// the wait, not the computation: it finishes in the background and caches its result.
|
||||
/// Callers must pass a drawing nothing else mutates, such as a canonical copy.
|
||||
/// </summary>
|
||||
public static List<BestFitResult> GetOrCompute(
|
||||
Drawing drawing,
|
||||
double plateWidth,
|
||||
double plateHeight,
|
||||
double spacing,
|
||||
CancellationToken token
|
||||
)
|
||||
{
|
||||
if (!token.CanBeCanceled)
|
||||
return GetOrCompute(drawing, plateWidth, plateHeight, spacing);
|
||||
|
||||
token.ThrowIfCancellationRequested();
|
||||
return Task.Run(() => GetOrCompute(drawing, plateWidth, plateHeight, spacing))
|
||||
.WaitAsync(token)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
}
|
||||
|
||||
/// <summary>True when <see cref="GetOrCompute(Drawing, double, double, double)"/> would return at once.</summary>
|
||||
public static bool IsCached(Drawing drawing, double plateWidth, double plateHeight, double spacing)
|
||||
{
|
||||
var source = CanonicalFrame.SourceOf(drawing);
|
||||
return _entries.TryGetValue(source, out var entry)
|
||||
&& entry.IsCurrentFor(source)
|
||||
&& entry.Filtered.ContainsKey((plateWidth, plateHeight, spacing));
|
||||
}
|
||||
|
||||
public static void ComputeForSizes(
|
||||
Drawing drawing,
|
||||
double spacing,
|
||||
|
||||
@@ -110,12 +110,26 @@ namespace OpenNest.Engine.Fill
|
||||
Action<List<Part>, string> reportProgress = null
|
||||
)
|
||||
{
|
||||
var bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing,
|
||||
plateSize.Length,
|
||||
plateSize.Width,
|
||||
partSpacing
|
||||
);
|
||||
// The first computation for a drawing can take a minute; say so rather than look stuck.
|
||||
if (!BestFitCache.IsCached(item.Drawing, plateSize.Length, plateSize.Width, partSpacing))
|
||||
reportProgress?.Invoke(null, "Computing best-fit pairs (may take a minute)...");
|
||||
|
||||
List<BestFitResult> bestFits;
|
||||
try
|
||||
{
|
||||
bestFits = BestFitCache.GetOrCompute(
|
||||
item.Drawing,
|
||||
plateSize.Length,
|
||||
plateSize.Width,
|
||||
partSpacing,
|
||||
token
|
||||
);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// The pairs finish computing in the background and are cached for the next fill.
|
||||
return new PairFillResult();
|
||||
}
|
||||
|
||||
var candidates = candidateSelector.Select(bestFits, workArea);
|
||||
Debug.WriteLine(
|
||||
|
||||
@@ -214,7 +214,8 @@ public class StripeFiller
|
||||
_context.Item.Drawing,
|
||||
_context.Plate.Size.Length,
|
||||
_context.Plate.Size.Width,
|
||||
_context.Plate.PartSpacing
|
||||
_context.Plate.PartSpacing,
|
||||
_context.Token
|
||||
);
|
||||
|
||||
return bestFits.Where(r => r.Keep).Take(MaxPairCandidates).ToList();
|
||||
|
||||
@@ -419,6 +419,8 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
var angles = BuildAngles(context.Item, classification, context.WorkArea);
|
||||
context.SharedState["AngleCandidates"] = angles;
|
||||
|
||||
ReportProvisional(context);
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var strategy in FillStrategyRegistry.Strategies)
|
||||
@@ -456,7 +458,12 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
context.WinnerPhase = strategy.Phase;
|
||||
}
|
||||
|
||||
if (improved && context.CurrentBest != null && context.CurrentBest.Count > 0)
|
||||
if (
|
||||
improved
|
||||
&& context.CurrentBest != null
|
||||
&& context.CurrentBest.Count > 0
|
||||
&& !context.ProvisionalLeads(context.CurrentBest)
|
||||
)
|
||||
{
|
||||
NestProgressReporter.Report(
|
||||
context.Progress,
|
||||
@@ -476,8 +483,46 @@ internal class DefaultPlateFiller : PlateFillerBase
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.WriteLine("[RunPipeline] Cancelled, returning current best");
|
||||
if (context.ProvisionalLeads(context.CurrentBest))
|
||||
{
|
||||
context.CurrentBest = context.Provisional;
|
||||
context.CurrentBestScore = FillScore.Compute(context.Provisional, context.WorkArea);
|
||||
context.WinnerPhase = NestPhase.RectBestFit;
|
||||
}
|
||||
}
|
||||
|
||||
RecordProductiveAngles(context.AngleResults);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shows the box-packing layout before best-fit pairs, whose first computation for a drawing
|
||||
/// can take a long time, so the user can accept a usable layout at once. Only offered when the
|
||||
/// RectBestFit strategy is active, so the pipeline still computes it and a completed fill
|
||||
/// returns exactly what it would without the preview.
|
||||
/// </summary>
|
||||
private void ReportProvisional(FillContext context)
|
||||
{
|
||||
if (context.Token.IsCancellationRequested)
|
||||
return;
|
||||
if (!FillStrategyRegistry.Strategies.Any(s => s is RectBestFitStrategy))
|
||||
return;
|
||||
|
||||
var parts = RectBestFitStrategy.GetOrCompute(context);
|
||||
if (parts == null || parts.Count == 0)
|
||||
return;
|
||||
|
||||
context.Provisional = parts;
|
||||
NestProgressReporter.Report(
|
||||
context.Progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = NestPhase.RectBestFit,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = context.ToOriginalFrame(parts),
|
||||
WorkArea = context.WorkArea,
|
||||
Description = $"Quick box layout: {parts.Count} parts",
|
||||
IsOverallBest = true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,17 @@ namespace OpenNest.Engine.Strategies
|
||||
|
||||
public List<Part> CurrentBest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A cheap layout shown before the slow strategies run, so it can be accepted early. It
|
||||
/// never becomes <see cref="CurrentBest"/> while strategies run, so they pick the same
|
||||
/// winner as without it; the pipeline returns it only when cancelled while it still leads.
|
||||
/// </summary>
|
||||
public List<Part> Provisional { get; set; }
|
||||
|
||||
/// <summary>True when <see cref="Provisional"/> beats <paramref name="parts"/>.</summary>
|
||||
internal bool ProvisionalLeads(List<Part> parts) =>
|
||||
Provisional != null && Policy.Comparer.IsBetter(Provisional, parts, WorkArea);
|
||||
|
||||
/// <summary>For progress reporting only; comparisons use Policy.Comparer.</summary>
|
||||
public FillScore CurrentBestScore { get; set; }
|
||||
public NestPhase WinnerPhase { get; set; }
|
||||
@@ -65,16 +76,18 @@ namespace OpenNest.Engine.Strategies
|
||||
WinnerPhase = ActivePhase;
|
||||
}
|
||||
|
||||
// The shown layout never regresses below the provisional one.
|
||||
var shown = ProvisionalLeads(CurrentBest) ? Provisional : CurrentBest;
|
||||
NestProgressReporter.Report(
|
||||
Progress,
|
||||
new ProgressReport
|
||||
{
|
||||
Phase = ActivePhase,
|
||||
PlateNumber = PlateNumber,
|
||||
Parts = ToOriginalFrame(isNewBest ? parts : CurrentBest),
|
||||
Parts = ToOriginalFrame(shown),
|
||||
WorkArea = WorkArea,
|
||||
Description = description,
|
||||
IsOverallBest = isNewBest,
|
||||
IsOverallBest = isNewBest && ReferenceEquals(shown, parts),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ namespace OpenNest.Engine.Strategies
|
||||
context.ReportProgress
|
||||
);
|
||||
|
||||
context.SharedState["BestFits"] = result.BestFits;
|
||||
if (result.BestFits != null)
|
||||
context.SharedState["BestFits"] = result.BestFits;
|
||||
|
||||
return result.Parts;
|
||||
}
|
||||
|
||||
@@ -5,18 +5,31 @@ namespace OpenNest.Engine.Strategies
|
||||
{
|
||||
public class RectBestFitStrategy : IFillStrategy
|
||||
{
|
||||
private const string ResultKey = "RectBestFit";
|
||||
|
||||
public string Name => "RectBestFit";
|
||||
public NestPhase Phase => NestPhase.RectBestFit;
|
||||
public int Order => 200;
|
||||
|
||||
public List<Part> Fill(FillContext context)
|
||||
public List<Part> Fill(FillContext context) => GetOrCompute(context);
|
||||
|
||||
/// <summary>
|
||||
/// Packs the item's boxes once per context. It is cheap, so the pipeline also uses it as
|
||||
/// the provisional layout shown before the best-fit pairs are computed.
|
||||
/// </summary>
|
||||
internal static List<Part> GetOrCompute(FillContext context)
|
||||
{
|
||||
if (context.SharedState.TryGetValue(ResultKey, out var cached))
|
||||
return (List<Part>)cached;
|
||||
|
||||
var binItem = BinConverter.ToItem(context.Item, context.Plate.PartSpacing);
|
||||
var bin = BinConverter.CreateBin(context.WorkArea, context.Plate.PartSpacing);
|
||||
|
||||
RectFill.FillBest(bin, binItem);
|
||||
|
||||
return BinConverter.ToParts(bin, new List<NestItem> { context.Item });
|
||||
var parts = BinConverter.ToParts(bin, new List<NestItem> { context.Item });
|
||||
context.SharedState[ResultKey] = parts;
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user