mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-08 12:35:44 -05:00
The first fill of a new drawing was blamed on the best-fit pair search, but on the reported nest that search takes ~3.4 s. The time goes to FillLinear.FillGrid, run thousands of times by the Pairs, Row/Column and Linear strategies: pattern copy-distance slides and the grid overlap check. Every change keeps the output bit for bit (part poses compared on the benchmark nest; best-fit results unchanged). - ConvexDecomposition.Triangulate: unlink clipped vertices from a circular list instead of List.RemoveAt, and remember why a vertex failed the ear test (same neighbors, same blocking vertex still present) instead of retesting it after every ear. Compared bitwise against a frozen pre-change copy, including the blocker-clipped case. - Collision: skip triangles whose box misses the other triangulation's union box (exact: min/max and monotone subtraction), reuse each triangle's open vertices, and let the overlap-only path stop at its first region when there are no holes. Check still returns every region. - SpatialQuery.ExtractEntityVertices presizes its set; FillLinear extracts each entity list's vertices once per copy-distance search rather than once per pair. - StripeFiller converges its stripe angles and tiles its grids in parallel. Dedup claims, cached remnant fills, ranking and progress stay in candidate order, and grids are tiled only for configurations a claim can still win (GridDedup.Contains), so results and reports do not depend on the thread count. - LinearFillStrategy fills its angles in parallel through FillHelpers.BestOverAnglesInParallel; angle results, ranking and progress stay in order. Benchmark (one drawing, 60x120, 0.25 spacing, cleared plate, interleaved runs): FillItem Lattice 291/317 s -> 164/153 s, 132 parts both; PairFiller alone 48 s -> 38-42 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
644 lines
22 KiB
C#
644 lines
22 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 (angle, waste, count) = variant == 0
|
|
? ConvergeStripeAngle(pairParts, sheetSpan, spacing, axis, _context.Token, _context.MaxQuantity)
|
|
: ConvergeStripeAngleShrink(pairParts, 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;
|
|
}
|
|
|
|
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.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
|
|
)
|
|
{
|
|
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
|
|
)
|
|
{
|
|
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, maxParts);
|
|
}
|
|
|
|
private static (double Angle, double Waste, int Count) ConvergeFromAngle(
|
|
List<Part> 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, 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(
|
|
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.Length
|
|
: rotated.BoundingBox.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);
|
|
}
|