Extract magic numbers into named constants (MaxTopCandidates, EarlyExitMinTried, etc.), extract candidate evaluation into EvaluateCandidate method, and expose BestFits property so PairsFillStrategy can reuse without redundant BestFitCache call. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
146 lines
5.5 KiB
C#
146 lines
5.5 KiB
C#
using OpenNest.Engine.BestFit;
|
|
using OpenNest.Engine.Strategies;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
|
|
namespace OpenNest.Engine.Fill
|
|
{
|
|
/// <summary>
|
|
/// Fills a work area using interlocking part pairs from BestFitCache.
|
|
/// </summary>
|
|
public class PairFiller
|
|
{
|
|
private const int MaxTopCandidates = 50;
|
|
private const int MaxStripCandidates = 100;
|
|
private const double MinStripUtilization = 0.3;
|
|
private const int EarlyExitMinTried = 10;
|
|
private const int EarlyExitStaleLimit = 10;
|
|
|
|
private readonly Size plateSize;
|
|
private readonly double partSpacing;
|
|
|
|
/// <summary>
|
|
/// The best-fit results computed during the last Fill call.
|
|
/// Available after Fill returns so callers can reuse without recomputing.
|
|
/// </summary>
|
|
public List<BestFitResult> BestFits { get; private set; }
|
|
|
|
public PairFiller(Size plateSize, double partSpacing)
|
|
{
|
|
this.plateSize = plateSize;
|
|
this.partSpacing = partSpacing;
|
|
}
|
|
|
|
public List<Part> Fill(NestItem item, Box workArea,
|
|
int plateNumber = 0,
|
|
CancellationToken token = default,
|
|
IProgress<NestProgress> progress = null)
|
|
{
|
|
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}");
|
|
Debug.WriteLine($"[PairFiller] Plate: {plateSize.Length:F2}x{plateSize.Width:F2}, WorkArea: {workArea.Width:F2}x{workArea.Length:F2}");
|
|
|
|
List<Part> best = null;
|
|
var bestScore = default(FillScore);
|
|
var sinceImproved = 0;
|
|
|
|
try
|
|
{
|
|
for (var i = 0; i < candidates.Count; i++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var filled = EvaluateCandidate(candidates[i], item.Drawing, workArea);
|
|
|
|
if (filled != null && filled.Count > 0)
|
|
{
|
|
var score = FillScore.Compute(filled, workArea);
|
|
if (best == null || score > bestScore)
|
|
{
|
|
best = filled;
|
|
bestScore = score;
|
|
sinceImproved = 0;
|
|
}
|
|
else
|
|
{
|
|
sinceImproved++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sinceImproved++;
|
|
}
|
|
|
|
NestEngineBase.ReportProgress(progress, NestPhase.Pairs, plateNumber, best, workArea,
|
|
$"Pairs: {i + 1}/{candidates.Count} candidates, best = {bestScore.Count} parts");
|
|
|
|
if (i + 1 >= EarlyExitMinTried && sinceImproved >= EarlyExitStaleLimit)
|
|
{
|
|
Debug.WriteLine($"[PairFiller] Early exit at {i + 1}/{candidates.Count} — no improvement in last {sinceImproved} candidates");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
Debug.WriteLine("[PairFiller] Cancelled mid-phase, using results so far");
|
|
}
|
|
|
|
Debug.WriteLine($"[PairFiller] Best pair result: {bestScore.Count} parts, density={bestScore.Density:P1}");
|
|
return best ?? new List<Part>();
|
|
}
|
|
|
|
private List<Part> EvaluateCandidate(BestFitResult candidate, Drawing drawing, Box workArea)
|
|
{
|
|
var pairParts = candidate.BuildParts(drawing);
|
|
var engine = new FillLinear(workArea, partSpacing);
|
|
|
|
var p0 = FillHelpers.BuildRotatedPattern(pairParts, 0);
|
|
var p90 = FillHelpers.BuildRotatedPattern(pairParts, Angle.HalfPI);
|
|
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
|
|
|
|
return FillHelpers.FillPattern(engine, pairParts, candidate.HullAngles, workArea);
|
|
}
|
|
|
|
private List<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
|
{
|
|
var kept = bestFits.Where(r => r.Keep).ToList();
|
|
var top = kept.Take(MaxTopCandidates).ToList();
|
|
|
|
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
|
|
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
|
|
|
|
if (workShortSide < plateShortSide * 0.5)
|
|
{
|
|
var stripCandidates = bestFits
|
|
.Where(r => r.ShortestSide <= workShortSide + Tolerance.Epsilon
|
|
&& r.Utilization >= MinStripUtilization)
|
|
.OrderByDescending(r => r.Utilization);
|
|
|
|
var existing = new HashSet<BestFitResult>(top);
|
|
|
|
foreach (var r in stripCandidates)
|
|
{
|
|
if (top.Count >= MaxStripCandidates)
|
|
break;
|
|
|
|
if (existing.Add(r))
|
|
top.Add(r);
|
|
}
|
|
|
|
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
|
}
|
|
|
|
return top;
|
|
}
|
|
}
|
|
}
|