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; /// /// When true, only complete stripes are placed — no partial rows/columns. /// 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 Fill() { var bestFits = GetPairCandidates(); if (bestFits.Count == 0) return new List(); var workArea = _context.WorkArea; var drawing = _context.Item.Drawing; var strategyName = _primaryAxis == NestDirection.Horizontal ? "Row" : "Column"; var layouts = ComputeLayouts(bestFits, drawing); List 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(); } private static readonly NestDirection[] Axes = { NestDirection.Horizontal, NestDirection.Vertical }; /// Expanding () then shrinking convergence. private const int VariantsPerAxis = 2; private static int LayoutIndex(int candidate, int axis, int variant) => (candidate * Axes.Length + axis) * VariantsPerAxis + variant; /// /// A converged stripe angle and, when it holds a pattern, the rotated pattern and the grid /// it tiles before the order-dependent steps ( claim, cached remnant /// fill). /// private sealed class StripeLayout { public double Angle; public double Waste; public int Count; public Pattern Pattern; public bool GridReady; public List Grid; } /// /// Converges every candidate's stripe angle per axis and variant, then tiles the grids that /// will claim, each step in parallel. Every layout is a pure function of /// its candidate, axis and variant (fresh part clones, per-call /// 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. /// then claims, fills remnants and ranks in the original order. /// private StripeLayout[] ComputeLayouts(List 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(); 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 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 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; } /// /// 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. /// private List 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(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; } /// /// Tiles the rotated pattern into a stripe and the stripe into a grid. Pure: reads only the /// pattern and the context's fixed inputs. /// private List 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 GetPairCandidates() { List bestFits; if (_context.SharedState.TryGetValue("BestFits", out var cached)) bestFits = (List)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 FillRemnant(List 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 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 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; } /// /// 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. /// private static double OrientShortSideAlong(List 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; } /// /// 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 caps /// the estimate rows like . /// public static (double Angle, double Waste, int Count) ConvergeStripeAngle( List 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); } /// /// Tries fitting N+1 narrower pairs by shrinking the pair width. /// Complements ConvergeStripeAngle which only expands. /// public static (double Angle, double Waste, int Count) ConvergeStripeAngleShrink( List 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 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)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 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 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 parts) => FillHelpers.HasOverlappingParts(parts); }