- FillExtents.Fill reported progress internally which overwrote the UI's temporary parts even when a better result (e.g. Pairs with 70 parts) won the competition. Added final ReportProgress call in FindBestFill and Fill(groupParts) to ensure the UI always shows the actual winner. - FillExtents vertical copy distance clamp (Math.Max with pairHeight + spacing) prevented geometry-aware compaction from ever occurring, causing visible gaps between rows. Boundaries are already inflated by halfSpacing so the calculated distance is correct; only fall back to bounding-box distance on non-positive results. - PairFiller now sets RemainderPatterns on FillLinear so remainder strips get pair-based filling instead of only individual parts (+1 part in tight layouts). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
5.0 KiB
C#
133 lines
5.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Engine.BestFit;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest
|
|
{
|
|
/// <summary>
|
|
/// Fills a work area using interlocking part pairs from BestFitCache.
|
|
/// Extracted from DefaultNestEngine.FillWithPairs.
|
|
/// </summary>
|
|
public class PairFiller
|
|
{
|
|
private readonly Size plateSize;
|
|
private readonly double partSpacing;
|
|
|
|
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)
|
|
{
|
|
var 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 result = candidates[i];
|
|
var pairParts = result.BuildParts(item.Drawing);
|
|
var angles = result.HullAngles;
|
|
var engine = new FillLinear(workArea, partSpacing);
|
|
|
|
// Let the remainder strip try pair-based filling too.
|
|
var p0 = DefaultNestEngine.BuildRotatedPattern(pairParts, 0);
|
|
var p90 = DefaultNestEngine.BuildRotatedPattern(pairParts, Angle.HalfPI);
|
|
engine.RemainderPatterns = new List<Pattern> { p0, p90 };
|
|
|
|
var filled = DefaultNestEngine.FillPattern(engine, pairParts, angles, 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");
|
|
|
|
// Early exit: stop if we've tried enough candidates without improvement.
|
|
if (i >= 9 && sinceImproved >= 10)
|
|
{
|
|
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<BestFitResult> SelectPairCandidates(List<BestFitResult> bestFits, Box workArea)
|
|
{
|
|
var kept = bestFits.Where(r => r.Keep).ToList();
|
|
var top = kept.Take(50).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 >= 0.3)
|
|
.OrderByDescending(r => r.Utilization);
|
|
|
|
var existing = new HashSet<BestFitResult>(top);
|
|
|
|
foreach (var r in stripCandidates)
|
|
{
|
|
if (top.Count >= 100)
|
|
break;
|
|
|
|
if (existing.Add(r))
|
|
top.Add(r);
|
|
}
|
|
|
|
Debug.WriteLine($"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})");
|
|
}
|
|
|
|
return top;
|
|
}
|
|
}
|
|
}
|