Files
OpenNest/OpenNest.Engine/Fill/PairCandidateSelector.cs
T
ajandClaude Opus 5.5 f62f73533e feat(fill): tile pair grids into narrow leftover strips
Fill tiled 8" gussets on a 16.85 x 60 strip as seven pairs in a 10.35"
column (14) and left a strip too narrow for a gusset square on. Sliding
each pair along its hypotenuse so six span the height narrows the column
to 8.75" and leaves a strip that holds eight more turned 45 degrees (20),
as laid out by hand.

- The pair remnant filler tries strips down to the part's narrowest hull
  width and tiles single best-fit pairs as a grid there, taking the grid
  only when it places more than the rows of pairs.
- After the pair filler's early exit it also tries skipped candidates
  whose grid plus an optimistic leftover-strip estimate could still win,
  keeping one only when it places more parts.
- The optimistic remnant bound counts parts by true area, not bounding
  box, so interlocking parts no longer prune winning candidates.

The mixed Fill nest pin now places 34 instead of 33; the mixed Strip nest
keeps 35 with its L-shapes laid out differently. Both pass the layout check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 22:24:49 -05:00

135 lines
4.8 KiB
C#

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OpenNest.Engine.BestFit;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Engine.Fill
{
/// <summary>
/// Selects and ranks BestFitCache pair candidates for a work area, favoring
/// narrow "strip" candidates when the work area is much smaller than the plate.
/// </summary>
public class PairCandidateSelector
{
private const int MaxTopCandidates = 50;
private const int MaxStripCandidates = 100;
private const double MinStripUtilization = 0.3;
private readonly Size plateSize;
private readonly double partSpacing;
public PairCandidateSelector(Size plateSize, double partSpacing)
{
this.plateSize = plateSize;
this.partSpacing = partSpacing;
}
public List<BestFitResult> Select(List<BestFitResult> bestFits, Box workArea)
{
var kept = bestFits.Where(r => r.Keep).ToList();
var workShortSide = System.Math.Min(workArea.Width, workArea.Length);
var plateShortSide = System.Math.Min(plateSize.Width, plateSize.Length);
if (workShortSide < plateShortSide * 0.5)
{
// Strip mode: prioritize candidates that fit the narrow dimension.
var stripCandidates = kept.Where(r =>
r.ShortestSide <= workShortSide + Tolerance.Epsilon
&& r.Utilization >= MinStripUtilization
)
.ToList();
SortByEstimatedCount(stripCandidates, workArea);
var top = stripCandidates.Take(MaxStripCandidates).ToList();
Debug.WriteLine(
$"[PairFiller] Strip mode: {top.Count} candidates (shortSide <= {workShortSide:F1})"
);
return top;
}
var result = kept.Take(MaxTopCandidates).ToList();
SortByEstimatedCount(result, workArea);
return result;
}
private void SortByEstimatedCount(List<BestFitResult> candidates, Box workArea)
{
var w = workArea.Width;
var l = workArea.Length;
candidates.Sort(
(a, b) =>
{
var aCount = EstimateTileCount(a, w, l);
var bCount = EstimateTileCount(b, w, l);
if (aCount != bCount)
return bCount.CompareTo(aCount);
return b.Utilization.CompareTo(a.Utilization);
}
);
}
/// <summary>
/// An optimistic count for the candidate's grid plus its leftover strips: each strip at
/// least <paramref name="minWidth"/> wide (the narrowest the part fits turned) is packed
/// at <paramref name="maxUtilization"/> of parts of <paramref name="partArea"/>.
/// </summary>
public int EstimateWithRemnant(
BestFitResult r,
Box workArea,
double minWidth,
double maxUtilization,
double partArea
)
{
int Estimate(double pairW, double pairH)
{
if (pairW <= 0 || pairH <= 0 || partArea <= 0)
return 0;
var areaW = workArea.Width;
var areaL = workArea.Length;
var cols = (int)((areaW + partSpacing) / (pairW + partSpacing));
var rows = (int)((areaL + partSpacing) / (pairH + partSpacing));
if (cols == 0 || rows == 0)
return 0;
var leftW = areaW - cols * (pairW + partSpacing);
var leftL = areaL - rows * (pairH + partSpacing);
var strips = (leftW >= minWidth ? leftW * areaL : 0)
+ (leftL >= minWidth ? leftL * areaW : 0);
return cols * rows * 2 + (int)(strips * maxUtilization / partArea);
}
return System.Math.Max(
Estimate(r.BoundingWidth, r.BoundingHeight),
Estimate(r.BoundingHeight, r.BoundingWidth)
);
}
private int EstimateTileCount(BestFitResult r, double areaW, double areaL)
{
var h = EstimateCount(r.BoundingWidth, r.BoundingHeight, areaW, areaL);
var v = EstimateCount(r.BoundingHeight, r.BoundingWidth, areaW, areaL);
return System.Math.Max(h, v);
}
private int EstimateCount(double pairW, double pairH, double areaW, double areaL)
{
if (pairW <= 0 || pairH <= 0)
return 0;
var cols = (int)((areaW + partSpacing) / (pairW + partSpacing));
var rows = (int)((areaL + partSpacing) / (pairH + partSpacing));
return cols * rows * 2;
}
}
}