- Evaluate pair candidates in parallel batches instead of sequentially - Add GridDedup to skip duplicate pattern/direction/workArea combos across PairFiller and StripeFiller strategies - Replace crude 30% remnant area estimate with L-shaped geometry calculation using actual grid extents and max utilization - Move FillStrategyRegistry.SetEnabled to outer evaluation loop to avoid repeated enable/disable per remnant fill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
474 lines
17 KiB
C#
474 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.BestFit;
|
|
using OpenNest.Engine.Strategies;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
using System.Diagnostics;
|
|
|
|
namespace OpenNest.Engine.Fill;
|
|
|
|
public class StripeFiller
|
|
{
|
|
private const int MaxPairCandidates = 5;
|
|
private const int MaxConvergenceIterations = 20;
|
|
private const int AngleSamples = 36;
|
|
|
|
private readonly FillContext _context;
|
|
private readonly NestDirection _primaryAxis;
|
|
private readonly IFillComparer _comparer;
|
|
private readonly GridDedup _dedup;
|
|
|
|
/// <summary>
|
|
/// When true, only complete stripes are placed — no partial rows/columns.
|
|
/// </summary>
|
|
public bool CompleteStripesOnly { get; set; }
|
|
|
|
/// <summary>
|
|
/// Factory to create the engine used for filling the remnant strip.
|
|
/// Defaults to NestEngineRegistry.Create (uses the user's selected engine).
|
|
/// </summary>
|
|
public Func<Plate, NestEngineBase> CreateRemnantEngine { get; set; }
|
|
= NestEngineRegistry.Create;
|
|
|
|
public StripeFiller(FillContext context, NestDirection primaryAxis)
|
|
{
|
|
_context = context;
|
|
_primaryAxis = primaryAxis;
|
|
_comparer = context.Policy?.Comparer ?? new DefaultFillComparer();
|
|
_dedup = GridDedup.GetOrCreate(context.SharedState);
|
|
}
|
|
|
|
public List<Part> Fill()
|
|
{
|
|
var bestFits = GetPairCandidates();
|
|
if (bestFits.Count == 0)
|
|
return new List<Part>();
|
|
|
|
var workArea = _context.WorkArea;
|
|
var spacing = _context.Plate.PartSpacing;
|
|
var drawing = _context.Item.Drawing;
|
|
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
|
|
|
List<Part> bestParts = null;
|
|
|
|
for (var i = 0; i < bestFits.Count; i++)
|
|
{
|
|
_context.Token.ThrowIfCancellationRequested();
|
|
|
|
var candidate = bestFits[i];
|
|
var pairParts = candidate.BuildParts(drawing);
|
|
|
|
foreach (var axis in new[] { NestDirection.Horizontal, NestDirection.Vertical })
|
|
{
|
|
var perpAxis = axis == NestDirection.Horizontal
|
|
? NestDirection.Vertical : NestDirection.Horizontal;
|
|
var sheetSpan = GetDimension(workArea, axis);
|
|
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
|
|
|
var expandResult = ConvergeStripeAngle(
|
|
pairParts, sheetSpan, spacing, axis, _context.Token);
|
|
var shrinkResult = ConvergeStripeAngleShrink(
|
|
pairParts, sheetSpan, spacing, axis, _context.Token);
|
|
|
|
foreach (var (angle, waste, count) in new[] { expandResult, shrinkResult })
|
|
{
|
|
if (count <= 0)
|
|
continue;
|
|
|
|
var result = BuildGrid(pairParts, angle, axis, perpAxis);
|
|
|
|
if (result == null || result.Count == 0)
|
|
continue;
|
|
|
|
Debug.WriteLine($"[StripeFiller] {strategyName} candidate {i} {dirLabel}: " +
|
|
$"angle={Angle.ToDegrees(angle):F1}°, N={count}, waste={waste:F2}, " +
|
|
$"grid={result.Count} parts");
|
|
|
|
if (_comparer.IsBetter(result, bestParts, workArea))
|
|
{
|
|
bestParts = result;
|
|
}
|
|
}
|
|
}
|
|
|
|
NestEngineBase.ReportProgress(_context.Progress, new ProgressReport
|
|
{
|
|
Phase = NestPhase.Custom,
|
|
PlateNumber = _context.PlateNumber,
|
|
Parts = bestParts,
|
|
WorkArea = workArea,
|
|
Description = $"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts",
|
|
});
|
|
}
|
|
|
|
return bestParts ?? new List<Part>();
|
|
}
|
|
|
|
private List<Part> BuildGrid(List<Part> pairParts, double angle,
|
|
NestDirection primaryAxis, NestDirection perpAxis)
|
|
{
|
|
var workArea = _context.WorkArea;
|
|
var spacing = _context.Plate.PartSpacing;
|
|
|
|
var rotatedPattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
|
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
|
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
|
|
|
if (!_dedup.TryAdd(rotatedPattern.BoundingBox, workArea, primaryAxis))
|
|
return null;
|
|
|
|
var stripeEngine = new FillLinear(stripeBox, spacing);
|
|
var stripeParts = stripeEngine.Fill(rotatedPattern, primaryAxis);
|
|
|
|
if (stripeParts == null || stripeParts.Count == 0)
|
|
return null;
|
|
|
|
var partsPerStripe = stripeParts.Count;
|
|
|
|
Debug.WriteLine($"[StripeFiller] Stripe: {partsPerStripe} parts, " +
|
|
$"box={stripeBox.Width:F2}x{stripeBox.Length:F2}");
|
|
|
|
var stripePattern = new Pattern();
|
|
stripePattern.Parts.AddRange(stripeParts);
|
|
stripePattern.UpdateBounds();
|
|
|
|
var gridEngine = new FillLinear(workArea, spacing);
|
|
var gridParts = gridEngine.Fill(stripePattern, perpAxis);
|
|
|
|
if (gridParts == null || gridParts.Count == 0)
|
|
return null;
|
|
|
|
if (CompleteStripesOnly)
|
|
{
|
|
var completeCount = gridParts.Count / partsPerStripe * partsPerStripe;
|
|
if (completeCount < gridParts.Count)
|
|
{
|
|
Debug.WriteLine($"[StripeFiller] CompleteOnly: {gridParts.Count} → {completeCount} " +
|
|
$"(dropped {gridParts.Count - completeCount} partial)");
|
|
gridParts = gridParts.GetRange(0, completeCount);
|
|
}
|
|
}
|
|
|
|
Debug.WriteLine($"[StripeFiller] Grid: {gridParts.Count} parts");
|
|
|
|
if (gridParts.Count == 0)
|
|
return null;
|
|
|
|
var allParts = new List<Part>(gridParts);
|
|
|
|
var remnantParts = FillRemnant(gridParts, primaryAxis);
|
|
if (remnantParts != null)
|
|
{
|
|
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
|
allParts.AddRange(remnantParts);
|
|
}
|
|
|
|
return allParts;
|
|
}
|
|
|
|
private List<BestFitResult> GetPairCandidates()
|
|
{
|
|
List<BestFitResult> bestFits;
|
|
|
|
if (_context.SharedState.TryGetValue("BestFits", out var cached))
|
|
bestFits = (List<BestFitResult>)cached;
|
|
else
|
|
bestFits = BestFitCache.GetOrCompute(
|
|
_context.Item.Drawing,
|
|
_context.Plate.Size.Length,
|
|
_context.Plate.Size.Width,
|
|
_context.Plate.PartSpacing);
|
|
|
|
return bestFits
|
|
.Where(r => r.Keep)
|
|
.Take(MaxPairCandidates)
|
|
.ToList();
|
|
}
|
|
|
|
private static Box MakeStripeBox(Box workArea, double perpDim, NestDirection primaryAxis)
|
|
{
|
|
return primaryAxis == NestDirection.Horizontal
|
|
? new Box(workArea.X, workArea.Y, workArea.Width, perpDim)
|
|
: new Box(workArea.X, workArea.Y, perpDim, workArea.Length);
|
|
}
|
|
|
|
private List<Part> FillRemnant(List<Part> gridParts, NestDirection primaryAxis)
|
|
{
|
|
var workArea = _context.WorkArea;
|
|
var spacing = _context.Plate.PartSpacing;
|
|
var drawing = _context.Item.Drawing;
|
|
|
|
var gridBox = gridParts.GetBoundingBox();
|
|
var minDim = System.Math.Min(
|
|
drawing.Program.BoundingBox().Width,
|
|
drawing.Program.BoundingBox().Length);
|
|
|
|
Box remnantBox;
|
|
|
|
if (primaryAxis == NestDirection.Horizontal)
|
|
{
|
|
var remnantY = gridBox.Top + spacing;
|
|
var remnantLength = workArea.Top - remnantY;
|
|
if (remnantLength < minDim)
|
|
return null;
|
|
remnantBox = new Box(workArea.X, remnantY, workArea.Width, remnantLength);
|
|
}
|
|
else
|
|
{
|
|
var remnantX = gridBox.Right + spacing;
|
|
var remnantWidth = workArea.Right - remnantX;
|
|
if (remnantWidth < minDim)
|
|
return null;
|
|
remnantBox = new Box(remnantX, workArea.Y, remnantWidth, workArea.Length);
|
|
}
|
|
|
|
Debug.WriteLine($"[StripeFiller] Remnant box: {remnantBox.Width:F2}x{remnantBox.Length:F2}");
|
|
|
|
var cachedResult = FillResultCache.Get(drawing, remnantBox, spacing);
|
|
if (cachedResult != null)
|
|
{
|
|
Debug.WriteLine($"[StripeFiller] Remnant CACHE HIT: {cachedResult.Count} parts");
|
|
return cachedResult;
|
|
}
|
|
|
|
FillStrategyRegistry.SetEnabled("Pairs", "RectBestFit", "Extents", "Linear");
|
|
try
|
|
{
|
|
var engine = CreateRemnantEngine(_context.Plate);
|
|
var item = new NestItem { Drawing = drawing };
|
|
var parts = engine.Fill(item, remnantBox, _context.Progress, _context.Token);
|
|
|
|
Debug.WriteLine($"[StripeFiller] Remnant engine ({engine.Name}): {parts?.Count ?? 0} parts, " +
|
|
$"winner={engine.WinnerPhase}");
|
|
|
|
if (parts != null && parts.Count > 0)
|
|
{
|
|
FillResultCache.Store(drawing, remnantBox, spacing, parts);
|
|
return parts;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
finally
|
|
{
|
|
FillStrategyRegistry.SetEnabled(null);
|
|
}
|
|
}
|
|
|
|
public static double FindAngleForTargetSpan(
|
|
List<Part> patternParts, double targetSpan, NestDirection axis)
|
|
{
|
|
var bestAngle = 0.0;
|
|
var bestDiff = double.MaxValue;
|
|
var samples = new (double angle, double span)[AngleSamples + 1];
|
|
|
|
for (var i = 0; i <= AngleSamples; i++)
|
|
{
|
|
var angle = i * Angle.HalfPI / AngleSamples;
|
|
var span = GetRotatedSpan(patternParts, angle, axis);
|
|
samples[i] = (angle, span);
|
|
|
|
var diff = System.Math.Abs(span - targetSpan);
|
|
if (diff < bestDiff)
|
|
{
|
|
bestDiff = diff;
|
|
bestAngle = angle;
|
|
}
|
|
}
|
|
|
|
if (bestDiff < Tolerance.Epsilon)
|
|
return bestAngle;
|
|
|
|
for (var i = 0; i < samples.Length - 1; i++)
|
|
{
|
|
var (a1, s1) = samples[i];
|
|
var (a2, s2) = samples[i + 1];
|
|
|
|
if ((s1 <= targetSpan && targetSpan <= s2) ||
|
|
(s2 <= targetSpan && targetSpan <= s1))
|
|
{
|
|
var result = BisectForTarget(patternParts, a1, a2, targetSpan, axis);
|
|
var resultSpan = GetRotatedSpan(patternParts, result, axis);
|
|
var resultDiff = System.Math.Abs(resultSpan - targetSpan);
|
|
|
|
if (resultDiff < bestDiff)
|
|
{
|
|
bestDiff = resultDiff;
|
|
bestAngle = result;
|
|
}
|
|
}
|
|
}
|
|
|
|
return bestAngle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the rotation angle that orients the pair with its short side
|
|
/// along the given axis. Returns 0 if already oriented, PI/2 if rotated.
|
|
/// </summary>
|
|
private static double OrientShortSideAlong(List<Part> patternParts, NestDirection axis)
|
|
{
|
|
var box = FillHelpers.BuildRotatedPattern(patternParts, 0).BoundingBox;
|
|
var span0 = GetDimension(box, axis);
|
|
var perpSpan0 = axis == NestDirection.Horizontal ? box.Length : box.Width;
|
|
|
|
if (span0 <= perpSpan0)
|
|
return 0;
|
|
|
|
return Angle.HalfPI;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Iteratively finds the rotation angle where N copies of the pattern
|
|
/// span the given dimension with minimal waste by expanding pair width.
|
|
/// Returns (angle, waste, pairCount).
|
|
/// </summary>
|
|
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
|
List<Part> patternParts, double sheetSpan, double spacing,
|
|
NestDirection axis, CancellationToken token = default)
|
|
{
|
|
var startAngle = OrientShortSideAlong(patternParts, axis);
|
|
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tries fitting N+1 narrower pairs by shrinking the pair width.
|
|
/// Complements ConvergeStripeAngle which only expands.
|
|
/// </summary>
|
|
public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink(
|
|
List<Part> patternParts, double sheetSpan, double spacing,
|
|
NestDirection axis, CancellationToken token = default)
|
|
{
|
|
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
|
var naturalPattern = FillHelpers.BuildRotatedPattern(patternParts, baseAngle);
|
|
var naturalSpan = GetDimension(naturalPattern.BoundingBox, axis);
|
|
|
|
if (naturalSpan + spacing <= 0)
|
|
return (0, double.MaxValue, 0);
|
|
|
|
var naturalN = (int)System.Math.Floor((sheetSpan + spacing) / (naturalSpan + spacing));
|
|
var targetN = naturalN + 1;
|
|
var targetSpan = (sheetSpan + spacing) / targetN - spacing;
|
|
|
|
if (targetSpan <= 0)
|
|
return (0, double.MaxValue, 0);
|
|
|
|
var startAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
|
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token);
|
|
}
|
|
|
|
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
|
List<Part> patternParts, double startAngle, double sheetSpan,
|
|
double spacing, NestDirection axis, CancellationToken token)
|
|
{
|
|
var bestWaste = double.MaxValue;
|
|
var bestAngle = startAngle;
|
|
var bestCount = 0;
|
|
var tolerance = sheetSpan * 0.001;
|
|
var currentAngle = startAngle;
|
|
|
|
for (var iteration = 0; iteration < MaxConvergenceIterations; iteration++)
|
|
{
|
|
token.ThrowIfCancellationRequested();
|
|
|
|
var rotated = FillHelpers.BuildRotatedPattern(patternParts, currentAngle);
|
|
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
|
var perpDim = axis == NestDirection.Horizontal
|
|
? rotated.BoundingBox.Length : rotated.BoundingBox.Width;
|
|
|
|
if (pairSpan + spacing <= 0)
|
|
break;
|
|
|
|
var stripeBox = axis == NestDirection.Horizontal
|
|
? new Box(0, 0, sheetSpan, perpDim)
|
|
: new Box(0, 0, perpDim, sheetSpan);
|
|
var engine = new FillLinear(stripeBox, spacing);
|
|
var filled = engine.Fill(rotated, axis);
|
|
var n = filled?.Count ?? 0;
|
|
|
|
if (n <= 0)
|
|
break;
|
|
|
|
var filledBox = ((IEnumerable<IBoundable>)filled).GetBoundingBox();
|
|
var remaining = sheetSpan - GetDimension(filledBox, axis);
|
|
|
|
Debug.WriteLine($"[Converge] iter={iteration}: angle={Angle.ToDegrees(currentAngle):F2}°, " +
|
|
$"pairSpan={pairSpan:F4}, perpDim={perpDim:F4}, N={n}, waste={remaining:F3}");
|
|
|
|
if (remaining < bestWaste)
|
|
{
|
|
bestWaste = remaining;
|
|
bestAngle = currentAngle;
|
|
bestCount = n;
|
|
}
|
|
|
|
if (remaining <= tolerance)
|
|
break;
|
|
|
|
var bboxN = (int)System.Math.Floor((sheetSpan + spacing) / (pairSpan + spacing));
|
|
if (bboxN <= 0) bboxN = 1;
|
|
var delta = remaining / bboxN;
|
|
var targetSpan = pairSpan + delta;
|
|
|
|
var prevAngle = currentAngle;
|
|
currentAngle = FindAngleForTargetSpan(patternParts, targetSpan, axis);
|
|
|
|
if (System.Math.Abs(currentAngle - prevAngle) < Tolerance.Epsilon)
|
|
break;
|
|
}
|
|
|
|
return (bestAngle, bestWaste, bestCount);
|
|
}
|
|
|
|
private static double BisectForTarget(
|
|
List<Part> patternParts, double lo, double hi,
|
|
double targetSpan, NestDirection axis)
|
|
{
|
|
var bestAngle = lo;
|
|
var bestDiff = double.MaxValue;
|
|
|
|
for (var i = 0; i < 30; i++)
|
|
{
|
|
var mid = (lo + hi) / 2;
|
|
var span = GetRotatedSpan(patternParts, mid, axis);
|
|
var diff = System.Math.Abs(span - targetSpan);
|
|
|
|
if (diff < bestDiff)
|
|
{
|
|
bestDiff = diff;
|
|
bestAngle = mid;
|
|
}
|
|
|
|
if (diff < Tolerance.Epsilon)
|
|
break;
|
|
|
|
var loSpan = GetRotatedSpan(patternParts, lo, axis);
|
|
if ((loSpan < targetSpan && span < targetSpan) ||
|
|
(loSpan > targetSpan && span > targetSpan))
|
|
lo = mid;
|
|
else
|
|
hi = mid;
|
|
}
|
|
|
|
return bestAngle;
|
|
}
|
|
|
|
private static double GetRotatedSpan(
|
|
List<Part> patternParts, double angle, NestDirection axis)
|
|
{
|
|
var rotated = FillHelpers.BuildRotatedPattern(patternParts, angle);
|
|
return axis == NestDirection.Horizontal
|
|
? rotated.BoundingBox.Width
|
|
: rotated.BoundingBox.Length;
|
|
}
|
|
|
|
private static double GetDimension(Box box, NestDirection axis)
|
|
{
|
|
return axis == NestDirection.Horizontal ? box.Width : box.Length;
|
|
}
|
|
}
|