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
{
///
/// Fills a work area using interlocking part pairs from BestFitCache.
///
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;
///
/// The best-fit results computed during the last Fill call.
/// Available after Fill returns so callers can reuse without recomputing.
///
public List BestFits { get; private set; }
public PairFiller(Size plateSize, double partSpacing)
{
this.plateSize = plateSize;
this.partSpacing = partSpacing;
}
public List Fill(NestItem item, Box workArea,
int plateNumber = 0,
CancellationToken token = default,
IProgress 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}");
var targetCount = item.Quantity > 0 ? item.Quantity : 0;
var effectiveWorkArea = workArea;
List 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, effectiveWorkArea);
if (filled != null && filled.Count > 0)
{
var score = FillScore.Compute(filled, effectiveWorkArea);
if (best == null || score > bestScore)
{
best = filled;
bestScore = score;
sinceImproved = 0;
// If we exceeded the target, reduce the work area for
// subsequent candidates by trimming excess parts and
// measuring the tighter bounding box.
if (targetCount > 0 && filled.Count > targetCount)
{
var reduced = ReduceWorkArea(filled, targetCount, workArea);
if (reduced.Area() < effectiveWorkArea.Area())
{
effectiveWorkArea = reduced;
Debug.WriteLine($"[PairFiller] Reduced work area to {effectiveWorkArea.Width:F2}x{effectiveWorkArea.Length:F2} (trimmed to {targetCount + 1} parts)");
}
}
}
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();
}
///
/// Given parts that exceed targetCount, sorts by BoundingBox.Top descending,
/// removes parts from the top until exactly targetCount remain, then returns
/// the Top of the remaining parts as the new work area height to beat.
///
private static Box ReduceWorkArea(List parts, int targetCount, Box workArea)
{
if (parts.Count <= targetCount)
return workArea;
// Sort by Top descending — highest parts get trimmed first.
var sorted = parts
.OrderByDescending(p => p.BoundingBox.Top)
.ToList();
// Remove from the top until exactly targetCount remain.
var trimCount = sorted.Count - targetCount;
var remaining = sorted.Skip(trimCount).ToList();
var newTop = remaining.Max(p => p.BoundingBox.Top);
return new Box(workArea.X, workArea.Y,
workArea.Width,
System.Math.Min(newTop - workArea.Y, workArea.Length));
}
private List 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 { p0, p90 };
// Include the pair's rotating calipers optimal rotation angle
// alongside the hull edge angles for tiling.
var angles = new List(candidate.HullAngles);
var optAngle = -candidate.OptimalRotation;
if (!angles.Any(a => a.IsEqualTo(optAngle)))
angles.Add(optAngle);
var optAngle90 = Angle.NormalizeRad(optAngle + Angle.HalfPI);
if (!angles.Any(a => a.IsEqualTo(optAngle90)))
angles.Add(optAngle90);
return FillHelpers.FillPattern(engine, pairParts, angles, workArea);
}
private List SelectPairCandidates(List 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(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;
}
}
}