mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 01:05:07 -05:00
One Irregular decode of a small job took seconds because each block in the catalog is built by the full Fill pipeline (PrivatePlateFill), and inside it StripeFiller's estimate rows spent about 85% of the CPU in FillLinear's perpendicular TilePattern. A stripe estimate is exactly one pattern tall, so the pattern's copy distance, an all-pairs slide search, always exceeded the room left and the copies were discarded after the search had run in full. TilePattern now passes FindPatternCopyDistance the largest distance that could still place a copy: for a whole copy, the room past the pattern; for a pattern that may tile partially, the most room past any one of its parts. The distance is the maximum over every pair, so the search stops as soon as it passes that bound and the tile returns empty, which is what the full search led to. The public CopyDistance is unchanged. StripeFiller's angle searches also reuse each rotated pattern's bounding box per angle instead of cloning and rotating the parts again on every probe. Output is identical: the layouts of every benchmark job that finishes match the previous build. Measured A/B on this machine, alternating builds per job (Benchmark --engines Irregular --parallel 1, benchmark ms): - N26-2A5: 15.5 s -> 8.8 s (12.6 s -> ~7.2 s in quieter runs) - 73 of the 80 bench jobs (the other 7 time out at 5 minutes in both builds): 1,990 s -> 1,578 s total (-21%), process CPU 6,486 s -> 3,842 s (-41%), median job 0.91x. N26-295 now finishes in 242 s with the same layout where it hit the 5-minute limit before. - Problem-nest corpus auto-nest and fill areas: all at baseline. A parallel herringbone pair build was tried and dropped: it saved 1.2 s on N26-2A5 but cost 10 s and 45 s more CPU on P241017-01. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
690 lines
24 KiB
C#
690 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Runtime.ExceptionServices;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using OpenNest.Engine;
|
|
using OpenNest.Engine.BestFit;
|
|
using OpenNest.Engine.Strategies;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
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; }
|
|
|
|
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 drawing = _context.Item.Drawing;
|
|
var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column";
|
|
|
|
var layouts = ComputeLayouts(bestFits, drawing);
|
|
|
|
List<Part> bestParts = null;
|
|
|
|
for (var i = 0; i < bestFits.Count; i++)
|
|
{
|
|
_context.Token.ThrowIfCancellationRequested();
|
|
|
|
for (var a = 0; a < Axes.Length; a++)
|
|
{
|
|
var axis = Axes[a];
|
|
var dirLabel = axis == NestDirection.Horizontal ? "Row" : "Col";
|
|
|
|
for (var v = 0; v < VariantsPerAxis; v++)
|
|
{
|
|
var layout = layouts[LayoutIndex(i, a, v)];
|
|
if (layout.Count <= 0)
|
|
continue;
|
|
|
|
var result = ClaimGrid(layout, axis);
|
|
|
|
if (result == null || result.Count == 0)
|
|
continue;
|
|
|
|
Debug.WriteLine(
|
|
$"[StripeFiller] {strategyName} candidate {i} {dirLabel}: "
|
|
+ $"angle={Angle.ToDegrees(layout.Angle):F1}°, N={layout.Count}, waste={layout.Waste:F2}, "
|
|
+ $"grid={result.Count} parts"
|
|
);
|
|
|
|
if (_comparer.IsBetter(result, bestParts, workArea))
|
|
{
|
|
bestParts = result;
|
|
}
|
|
}
|
|
}
|
|
|
|
_context.ReportProgress(
|
|
bestParts,
|
|
$"{strategyName}: {i + 1}/{bestFits.Count} pairs, best = {bestParts?.Count ?? 0} parts"
|
|
);
|
|
}
|
|
|
|
return bestParts ?? new List<Part>();
|
|
}
|
|
|
|
private static readonly NestDirection[] Axes = { NestDirection.Horizontal, NestDirection.Vertical };
|
|
|
|
/// <summary>Expanding (<see cref="ConvergeStripeAngle"/>) then shrinking convergence.</summary>
|
|
private const int VariantsPerAxis = 2;
|
|
|
|
private static int LayoutIndex(int candidate, int axis, int variant) =>
|
|
(candidate * Axes.Length + axis) * VariantsPerAxis + variant;
|
|
|
|
/// <summary>
|
|
/// A converged stripe angle and, when it holds a pattern, the rotated pattern and the grid
|
|
/// it tiles before the order-dependent steps (<see cref="GridDedup"/> claim, cached remnant
|
|
/// fill).
|
|
/// </summary>
|
|
private sealed class StripeLayout
|
|
{
|
|
public double Angle;
|
|
public double Waste;
|
|
public int Count;
|
|
public Pattern Pattern;
|
|
public bool GridReady;
|
|
public List<Part> Grid;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converges every candidate's stripe angle per axis and variant, then tiles the grids that
|
|
/// <see cref="Fill"/> will claim, each step in parallel. Every layout is a pure function of
|
|
/// its candidate, axis and variant (fresh part clones, per-call <see cref="FillLinear"/>
|
|
/// state), so the results do not depend on the thread count. A grid is tiled only when its
|
|
/// configuration is not yet claimed and no earlier layout shares it: claims are only ever
|
|
/// added, so exactly those are the grids the sequential loop would have tiled.
|
|
/// <see cref="Fill"/> then claims, fills remnants and ranks in the original order.
|
|
/// </summary>
|
|
private StripeLayout[] ComputeLayouts(List<BestFitResult> bestFits, Drawing drawing)
|
|
{
|
|
var layouts = new StripeLayout[bestFits.Count * Axes.Length * VariantsPerAxis];
|
|
|
|
RunParallel(layouts.Length, index =>
|
|
{
|
|
var variant = index % VariantsPerAxis;
|
|
var axis = Axes[index / VariantsPerAxis % Axes.Length];
|
|
var candidate = bestFits[index / VariantsPerAxis / Axes.Length];
|
|
layouts[index] = Converge(candidate.BuildParts(drawing), axis, variant);
|
|
});
|
|
|
|
var batch = new GridDedup();
|
|
var toTile = new List<int>();
|
|
for (var index = 0; index < layouts.Length; index++)
|
|
{
|
|
var layout = layouts[index];
|
|
if (layout.Count <= 0)
|
|
continue;
|
|
|
|
var axis = Axes[index / VariantsPerAxis % Axes.Length];
|
|
var box = layout.Pattern.BoundingBox;
|
|
if (!_dedup.Contains(box, _context.WorkArea, axis) && batch.TryAdd(box, _context.WorkArea, axis))
|
|
toTile.Add(index);
|
|
}
|
|
|
|
RunParallel(toTile.Count, k =>
|
|
{
|
|
var index = toTile[k];
|
|
var layout = layouts[index];
|
|
layout.Grid = BuildGrid(layout.Pattern, Axes[index / VariantsPerAxis % Axes.Length]);
|
|
layout.GridReady = true;
|
|
});
|
|
|
|
return layouts;
|
|
}
|
|
|
|
private void RunParallel(int count, Action<int> body)
|
|
{
|
|
try
|
|
{
|
|
Parallel.For(0, count, new ParallelOptions { CancellationToken = _context.Token }, body);
|
|
}
|
|
catch (AggregateException ex)
|
|
{
|
|
// Surface the failure itself, cancellation included, as the sequential loop did.
|
|
ExceptionDispatchInfo.Capture(ex.InnerExceptions[0]).Throw();
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private StripeLayout Converge(List<Part> pairParts, NestDirection axis, int variant)
|
|
{
|
|
var spacing = _context.Plate.PartSpacing;
|
|
var sheetSpan = GetDimension(_context.WorkArea, axis);
|
|
|
|
var bounds = new RotatedBounds(pairParts);
|
|
var (angle, waste, count) = variant == 0
|
|
? ConvergeStripeAngle(bounds, sheetSpan, spacing, axis, _context.Token, _context.MaxQuantity)
|
|
: ConvergeStripeAngleShrink(bounds, sheetSpan, spacing, axis, _context.Token, _context.MaxQuantity);
|
|
|
|
var layout = new StripeLayout { Angle = angle, Waste = waste, Count = count };
|
|
if (count > 0)
|
|
layout.Pattern = FillHelpers.BuildRotatedPattern(pairParts, angle);
|
|
|
|
return layout;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The order-dependent half of a stripe grid: claims its configuration and adds the cached
|
|
/// remnant fill. Null when the configuration was already evaluated or no grid fits.
|
|
/// </summary>
|
|
private List<Part> ClaimGrid(StripeLayout layout, NestDirection primaryAxis)
|
|
{
|
|
if (!_dedup.TryAdd(layout.Pattern.BoundingBox, _context.WorkArea, primaryAxis))
|
|
return null;
|
|
|
|
// ComputeLayouts tiled every grid a claim can succeed for; tile here should it not have.
|
|
var gridParts = layout.GridReady ? layout.Grid : BuildGrid(layout.Pattern, primaryAxis);
|
|
if (gridParts == null)
|
|
return null;
|
|
|
|
var allParts = new List<Part>(gridParts);
|
|
|
|
// A capped fill that already holds the quantity needs no remnant parts.
|
|
if (_context.MaxQuantity > 0 && gridParts.Count >= _context.MaxQuantity)
|
|
return allParts;
|
|
|
|
var remnantParts = FillRemnant(gridParts, primaryAxis);
|
|
if (remnantParts != null)
|
|
{
|
|
Debug.WriteLine($"[StripeFiller] Remnant: {remnantParts.Count} parts");
|
|
allParts.AddRange(remnantParts);
|
|
}
|
|
|
|
return allParts;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tiles the rotated pattern into a stripe and the stripe into a grid. Pure: reads only the
|
|
/// pattern and the context's fixed inputs.
|
|
/// </summary>
|
|
private List<Part> BuildGrid(Pattern rotatedPattern, NestDirection primaryAxis)
|
|
{
|
|
var perpAxis =
|
|
primaryAxis == NestDirection.Horizontal ? NestDirection.Vertical : NestDirection.Horizontal;
|
|
var workArea = _context.WorkArea;
|
|
var spacing = _context.Plate.PartSpacing;
|
|
|
|
var perpDim = GetDimension(rotatedPattern.BoundingBox, perpAxis);
|
|
var stripeBox = MakeStripeBox(workArea, perpDim, primaryAxis);
|
|
|
|
var stripeEngine = new FillLinear(stripeBox, spacing)
|
|
{
|
|
Label = "Stripe",
|
|
MaxParts = _context.MaxQuantity,
|
|
};
|
|
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)
|
|
{
|
|
Label = "Stripe-Grid",
|
|
MaxParts = _context.MaxQuantity,
|
|
};
|
|
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;
|
|
|
|
// Reject results where bounding boxes overlap — the angle convergence
|
|
// can produce slightly off-axis rotations where FillLinear's copy
|
|
// distance calculation doesn't fully account for the rotated geometry.
|
|
if (HasOverlappingParts(gridParts))
|
|
{
|
|
Debug.WriteLine($"[StripeFiller] Rejected grid: overlapping bounding boxes detected");
|
|
return null;
|
|
}
|
|
|
|
return gridParts;
|
|
}
|
|
|
|
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,
|
|
_context.Token
|
|
);
|
|
|
|
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.Length, perpDim)
|
|
: new Box(workArea.X, workArea.Y, perpDim, workArea.Width);
|
|
}
|
|
|
|
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.Length, 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.Width);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
var filler = new FillLinear(remnantBox, spacing) { Label = "Stripe-Remnant" };
|
|
List<Part> best = null;
|
|
|
|
foreach (var angle in new[] { 0.0, Angle.HalfPI })
|
|
{
|
|
_context.Token.ThrowIfCancellationRequested();
|
|
var result = FillHelpers.FillWithDirectionPreference(
|
|
dir => filler.Fill(drawing, angle, dir),
|
|
null,
|
|
_comparer,
|
|
remnantBox
|
|
);
|
|
|
|
if (result != null && result.Count > (best?.Count ?? 0))
|
|
best = result;
|
|
}
|
|
|
|
Debug.WriteLine($"[StripeFiller] Remnant linear: {best?.Count ?? 0} parts");
|
|
|
|
if (best != null && best.Count > 0)
|
|
{
|
|
FillResultCache.Store(drawing, remnantBox, spacing, best);
|
|
return best;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Bounding boxes of one pattern's parts rotated about their common center, by angle. A
|
|
/// box is a pure function of the parts and the angle, and the angle searches sample the
|
|
/// same angles over and over (every <see cref="FindAngleForTargetSpan"/> scans the same
|
|
/// grid), so each is built once. The parts must not change while it is in use.
|
|
/// </summary>
|
|
private sealed class RotatedBounds
|
|
{
|
|
private readonly Dictionary<double, Box> boxes = new();
|
|
|
|
public RotatedBounds(List<Part> parts) => Parts = parts;
|
|
|
|
public List<Part> Parts { get; }
|
|
|
|
public Box At(double angle)
|
|
{
|
|
if (!boxes.TryGetValue(angle, out var box))
|
|
boxes[angle] = box = FillHelpers.BuildRotatedPattern(Parts, angle).BoundingBox;
|
|
return box;
|
|
}
|
|
}
|
|
|
|
public static double FindAngleForTargetSpan(
|
|
List<Part> patternParts,
|
|
double targetSpan,
|
|
NestDirection axis
|
|
) => FindAngleForTargetSpan(new RotatedBounds(patternParts), targetSpan, axis);
|
|
|
|
private static double FindAngleForTargetSpan(
|
|
RotatedBounds 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(RotatedBounds patternParts, NestDirection axis)
|
|
{
|
|
var box = patternParts.At(0);
|
|
var span0 = GetDimension(box, axis);
|
|
var perpSpan0 = axis == NestDirection.Horizontal ? box.Width : box.Length;
|
|
|
|
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). A positive <paramref name="maxParts"/> caps
|
|
/// the estimate rows like <see cref="FillLinear.MaxParts"/>.
|
|
/// </summary>
|
|
public static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
|
List<Part> patternParts,
|
|
double sheetSpan,
|
|
double spacing,
|
|
NestDirection axis,
|
|
CancellationToken token = default,
|
|
int maxParts = 0
|
|
) => ConvergeStripeAngle(new RotatedBounds(patternParts), sheetSpan, spacing, axis, token, maxParts);
|
|
|
|
private static (double Angle, double Waste, int Count) ConvergeStripeAngle(
|
|
RotatedBounds patternParts,
|
|
double sheetSpan,
|
|
double spacing,
|
|
NestDirection axis,
|
|
CancellationToken token,
|
|
int maxParts
|
|
)
|
|
{
|
|
var startAngle = OrientShortSideAlong(patternParts, axis);
|
|
return ConvergeFromAngle(patternParts, startAngle, sheetSpan, spacing, axis, token, maxParts);
|
|
}
|
|
|
|
/// <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,
|
|
int maxParts = 0
|
|
) => ConvergeStripeAngleShrink(new RotatedBounds(patternParts), sheetSpan, spacing, axis, token, maxParts);
|
|
|
|
private static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink(
|
|
RotatedBounds patternParts,
|
|
double sheetSpan,
|
|
double spacing,
|
|
NestDirection axis,
|
|
CancellationToken token,
|
|
int maxParts
|
|
)
|
|
{
|
|
var baseAngle = OrientShortSideAlong(patternParts, axis);
|
|
var naturalSpan = GetDimension(patternParts.At(baseAngle), 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, maxParts);
|
|
}
|
|
|
|
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
|
RotatedBounds patternParts,
|
|
double startAngle,
|
|
double sheetSpan,
|
|
double spacing,
|
|
NestDirection axis,
|
|
CancellationToken token,
|
|
int maxParts
|
|
)
|
|
{
|
|
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.Parts, currentAngle);
|
|
var pairSpan = GetDimension(rotated.BoundingBox, axis);
|
|
var perpDim =
|
|
axis == NestDirection.Horizontal
|
|
? rotated.BoundingBox.Width
|
|
: rotated.BoundingBox.Length;
|
|
|
|
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)
|
|
{
|
|
Label = "Stripe-EstimateRow",
|
|
MaxParts = maxParts,
|
|
};
|
|
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(
|
|
RotatedBounds 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(RotatedBounds patternParts, double angle, NestDirection axis)
|
|
{
|
|
var box = patternParts.At(angle);
|
|
return axis == NestDirection.Horizontal
|
|
? box.Length
|
|
: box.Width;
|
|
}
|
|
|
|
private static double GetDimension(Box box, NestDirection axis)
|
|
{
|
|
return axis == NestDirection.Horizontal ? box.Length : box.Width;
|
|
}
|
|
|
|
private static bool HasOverlappingParts(List<Part> parts) =>
|
|
FillHelpers.HasOverlappingParts(parts);
|
|
}
|