using System.Collections.Generic; using OpenNest.Geometry; using OpenNest.Math; namespace OpenNest { public class FillLinear { public FillLinear(Box workArea, double partSpacing) { PartSpacing = partSpacing; WorkArea = new Box(workArea.X, workArea.Y, workArea.Width, workArea.Length); } public Box WorkArea { get; } public double PartSpacing { get; } public double HalfSpacing => PartSpacing / 2; private static Vector MakeOffset(NestDirection direction, double distance) { return direction == NestDirection.Horizontal ? new Vector(distance, 0) : new Vector(0, distance); } private static PushDirection GetPushDirection(NestDirection direction) { return direction == NestDirection.Horizontal ? PushDirection.Left : PushDirection.Down; } private static double GetDimension(Box box, NestDirection direction) { return direction == NestDirection.Horizontal ? box.Width : box.Length; } private static double GetStart(Box box, NestDirection direction) { return direction == NestDirection.Horizontal ? box.Left : box.Bottom; } private double GetLimit(NestDirection direction) { return direction == NestDirection.Horizontal ? WorkArea.Right : WorkArea.Top; } private static NestDirection PerpendicularAxis(NestDirection direction) { return direction == NestDirection.Horizontal ? NestDirection.Vertical : NestDirection.Horizontal; } /// /// Computes the slide distance for the push algorithm, returning the /// geometry-aware copy distance along the given axis. /// private double ComputeCopyDistance(double bboxDim, double slideDistance) { if (slideDistance >= double.MaxValue || slideDistance < 0) return bboxDim + PartSpacing; // The geometry-aware slide can produce a copy distance smaller than // the part itself when inflated corner/arc vertices interact spuriously. // Clamp to bboxDim + PartSpacing to prevent bounding box overlap. return System.Math.Max(bboxDim - slideDistance, bboxDim + PartSpacing); } /// /// Finds the geometry-aware copy distance between two identical parts along an axis. /// Both parts are inflated by half-spacing for symmetric spacing. /// private double FindCopyDistance(Part partA, NestDirection direction, PartBoundary boundary) { var bboxDim = GetDimension(partA.BoundingBox, direction); var pushDir = GetPushDirection(direction); var opposite = Helper.OppositeDirection(pushDir); var locationB = partA.Location + MakeOffset(direction, bboxDim); var movingLines = boundary.GetLines(locationB, pushDir); var stationaryLines = boundary.GetLines(partA.Location, opposite); var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir); var copyDist = ComputeCopyDistance(bboxDim, slideDistance); //System.Diagnostics.Debug.WriteLine($"[FindCopyDistance] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} locA={partA.Location} locB={locationB} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}"); return copyDist; } /// /// Finds the geometry-aware copy distance between two identical patterns along an axis. /// Checks every pair of parts across adjacent patterns so that multi-part /// patterns (e.g. interlocking pairs) maintain spacing between ALL parts. /// Both sides are inflated by half-spacing for symmetric spacing. /// private double FindPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary[] boundaries) { if (patternA.Parts.Count <= 1) return FindSinglePartPatternCopyDistance(patternA, direction, boundaries[0]); var bboxDim = GetDimension(patternA.BoundingBox, direction); var pushDir = GetPushDirection(direction); var opposite = Helper.OppositeDirection(pushDir); // Compute a starting offset large enough that every part-pair in // patternB has its offset geometry beyond patternA's offset geometry. // max(aUpper_i - bLower_j) = max(aUpper) - min(bLower). var maxUpper = double.MinValue; var minLower = double.MaxValue; for (var i = 0; i < patternA.Parts.Count; i++) { var bb = patternA.Parts[i].BoundingBox; var upper = direction == NestDirection.Horizontal ? bb.Right : bb.Top; var lower = direction == NestDirection.Horizontal ? bb.Left : bb.Bottom; if (upper > maxUpper) maxUpper = upper; if (lower < minLower) minLower = lower; } var startOffset = System.Math.Max(bboxDim, maxUpper - minLower + PartSpacing + Tolerance.Epsilon); var offset = MakeOffset(direction, startOffset); // Pre-compute stationary lines for patternA parts. var stationaryCache = new List[patternA.Parts.Count]; for (var i = 0; i < patternA.Parts.Count; i++) stationaryCache[i] = boundaries[i].GetLines(patternA.Parts[i].Location, opposite); var maxCopyDistance = 0.0; for (var j = 0; j < patternA.Parts.Count; j++) { var locationB = patternA.Parts[j].Location + offset; var movingLines = boundaries[j].GetLines(locationB, pushDir); for (var i = 0; i < patternA.Parts.Count; i++) { var slideDistance = Helper.DirectionalDistance(movingLines, stationaryCache[i], pushDir); if (slideDistance >= double.MaxValue || slideDistance < 0) continue; var copyDist = startOffset - slideDistance; if (copyDist > maxCopyDistance) maxCopyDistance = copyDist; } } // Fallback: if no pair interacted (shouldn't happen for real parts), // use the simple bounding-box + spacing distance. if (maxCopyDistance <= 0) return bboxDim + PartSpacing; return maxCopyDistance; } /// /// Fast path for single-part patterns — no cross-part conflicts possible. /// private double FindSinglePartPatternCopyDistance(Pattern patternA, NestDirection direction, PartBoundary boundary) { var bboxDim = GetDimension(patternA.BoundingBox, direction); var pushDir = GetPushDirection(direction); var opposite = Helper.OppositeDirection(pushDir); var offset = MakeOffset(direction, bboxDim); var movingLines = GetOffsetPatternLines(patternA, offset, boundary, pushDir); var stationaryLines = GetPatternLines(patternA, boundary, opposite); var slideDistance = Helper.DirectionalDistance(movingLines, stationaryLines, pushDir); var copyDist = ComputeCopyDistance(bboxDim, slideDistance); //System.Diagnostics.Debug.WriteLine($"[FindSinglePartPatternCopyDist] dir={direction} bboxDim={bboxDim:F4} slide={slideDistance:F4} copyDist={copyDist:F4} spacing={PartSpacing:F4} patternParts={patternA.Parts.Count} movingEdges={movingLines.Count} stationaryEdges={stationaryLines.Count}"); return copyDist; } /// /// Gets offset boundary lines for all parts in a pattern using a shared boundary. /// private static List GetPatternLines(Pattern pattern, PartBoundary boundary, PushDirection direction) { var lines = new List(); foreach (var part in pattern.Parts) lines.AddRange(boundary.GetLines(part.Location, direction)); return lines; } /// /// Gets boundary lines for all parts in a pattern, with an additional /// location offset applied. Avoids cloning the pattern. /// private static List GetOffsetPatternLines(Pattern pattern, Vector offset, PartBoundary boundary, PushDirection direction) { var lines = new List(); foreach (var part in pattern.Parts) lines.AddRange(boundary.GetLines(part.Location + offset, direction)); return lines; } /// /// Creates boundaries for all parts in a pattern. Parts that share the same /// program geometry (same drawing and rotation) reuse the same boundary instance. /// private PartBoundary[] CreateBoundaries(Pattern pattern) { var boundaries = new PartBoundary[pattern.Parts.Count]; var cache = new List<(Drawing drawing, double rotation, PartBoundary boundary)>(); for (var i = 0; i < pattern.Parts.Count; i++) { var part = pattern.Parts[i]; PartBoundary found = null; foreach (var entry in cache) { if (entry.drawing == part.BaseDrawing && entry.rotation.IsEqualTo(part.Rotation)) { found = entry.boundary; break; } } if (found == null) { found = new PartBoundary(part, HalfSpacing); cache.Add((part.BaseDrawing, part.Rotation, found)); } boundaries[i] = found; } return boundaries; } /// /// Tiles a pattern along the given axis, returning the cloned parts /// (does not include the original pattern's parts). For multi-part /// patterns, also adds individual parts from the next incomplete copy /// that still fit within the work area. /// private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); if (copyDistance <= 0) return new List(); var dim = GetDimension(basePattern.BoundingBox, direction); var start = GetStart(basePattern.BoundingBox, direction); var limit = GetLimit(direction); var estimatedCopies = (int)((limit - start - dim) / copyDistance); var result = new List(estimatedCopies * basePattern.Parts.Count); var count = 1; while (true) { var nextPos = start + copyDistance * count; if (nextPos + dim > limit + Tolerance.Epsilon) break; var offset = MakeOffset(direction, copyDistance * count); foreach (var part in basePattern.Parts) result.Add(part.CloneAtOffset(offset)); count++; } // For multi-part patterns, try to place individual parts from the // next copy that didn't fit as a whole. This handles cases where // e.g. a 2-part pair only partially fits — one part may still be // within the work area even though the full pattern exceeds it. if (basePattern.Parts.Count > 1) { var offset = MakeOffset(direction, copyDistance * count); foreach (var basePart in basePattern.Parts) { var part = basePart.CloneAtOffset(offset); if (part.BoundingBox.Right <= WorkArea.Right + Tolerance.Epsilon && part.BoundingBox.Top <= WorkArea.Top + Tolerance.Epsilon && part.BoundingBox.Left >= WorkArea.Left - Tolerance.Epsilon && part.BoundingBox.Bottom >= WorkArea.Bottom - Tolerance.Epsilon) { result.Add(part); } } } return result; } /// /// Creates a seed pattern containing a single part positioned at the work area origin. /// Returns an empty pattern if the part does not fit. /// private Pattern MakeSeedPattern(Drawing drawing, double rotationAngle) { var pattern = new Pattern(); var template = new Part(drawing); if (!rotationAngle.IsEqualTo(0)) template.Rotate(rotationAngle); template.Offset(WorkArea.Location - template.BoundingBox.Location); if (template.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon || template.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon) return pattern; pattern.Parts.Add(template); pattern.UpdateBounds(); return pattern; } /// /// Recursively fills the work area. At depth 0, tiles the pattern along the /// primary axis, then recurses perpendicular. At depth 1, tiles and returns. /// After the grid is formed, fills the remaining strip with individual parts. /// private List FillRecursive(Pattern pattern, NestDirection direction, int depth) { var boundaries = CreateBoundaries(pattern); var result = new List(pattern.Parts); result.AddRange(TilePattern(pattern, direction, boundaries)); if (depth == 0 && result.Count > pattern.Parts.Count) { var rowPattern = new Pattern(); rowPattern.Parts.AddRange(result); rowPattern.UpdateBounds(); var perpAxis = PerpendicularAxis(direction); var gridResult = FillRecursive(rowPattern, perpAxis, depth + 1); //System.Diagnostics.Debug.WriteLine($"[FillRecursive] Grid: {gridResult.Count} parts, rowSize={rowPattern.Parts.Count}, dir={direction}"); // Fill the remaining strip (after the last full row/column) // with individual parts from the seed pattern. var remaining = FillRemainingStrip(gridResult, pattern, perpAxis, direction); //System.Diagnostics.Debug.WriteLine($"[FillRecursive] Remainder: {remaining.Count} parts"); if (remaining.Count > 0) gridResult.AddRange(remaining); // Try one fewer row/column — the larger remainder strip may // fit more parts than the extra row contained. var fewerResult = TryFewerRows(gridResult, rowPattern, pattern, perpAxis, direction); //System.Diagnostics.Debug.WriteLine($"[FillRecursive] TryFewerRows: {fewerResult?.Count ?? -1} vs grid+remainder={gridResult.Count}"); if (fewerResult != null && fewerResult.Count > gridResult.Count) return fewerResult; return gridResult; } if (depth == 0) { // Single part didn't tile along primary — still try perpendicular. return FillRecursive(pattern, PerpendicularAxis(direction), depth + 1); } return result; } /// /// Tries removing the last row/column from the grid and re-filling the /// larger remainder strip. Returns null if this doesn't improve the total. /// private List TryFewerRows( List fullResult, Pattern rowPattern, Pattern seedPattern, NestDirection tiledAxis, NestDirection primaryAxis) { var rowPartCount = rowPattern.Parts.Count; //System.Diagnostics.Debug.WriteLine($"[TryFewerRows] fullResult={fullResult.Count}, rowPartCount={rowPartCount}, tiledAxis={tiledAxis}"); // Need at least 2 rows for this to make sense (remove 1, keep 1+). if (fullResult.Count < rowPartCount * 2) { //System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Skipped: too few parts for 2 rows"); return null; } // Remove the last row's worth of parts. var fewerParts = new List(fullResult.Count - rowPartCount); for (var i = 0; i < fullResult.Count - rowPartCount; i++) fewerParts.Add(fullResult[i]); // Find the top/right edge of the kept parts for logging. var edge = double.MinValue; foreach (var part in fewerParts) { var e = tiledAxis == NestDirection.Vertical ? part.BoundingBox.Top : part.BoundingBox.Right; if (e > edge) edge = e; } //System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Kept {fewerParts.Count} parts, edge={edge:F2}, workArea={WorkArea}"); var remaining = FillRemainingStrip(fewerParts, seedPattern, tiledAxis, primaryAxis); //System.Diagnostics.Debug.WriteLine($"[TryFewerRows] Remainder fill: {remaining.Count} parts (need > {rowPartCount} to improve)"); if (remaining.Count <= rowPartCount) return null; fewerParts.AddRange(remaining); return fewerParts; } /// /// After tiling full rows/columns, fills the remaining strip with individual /// parts. The strip is the leftover space along the tiled axis between the /// last full row/column and the work area boundary. Each unique drawing and /// rotation from the seed pattern is tried in both directions. /// private List FillRemainingStrip( List placedParts, Pattern seedPattern, NestDirection tiledAxis, NestDirection primaryAxis) { // Find the furthest edge of placed parts along the tiled axis. var placedEdge = double.MinValue; foreach (var part in placedParts) { var edge = tiledAxis == NestDirection.Vertical ? part.BoundingBox.Top : part.BoundingBox.Right; if (edge > placedEdge) placedEdge = edge; } // Build the remaining strip with a spacing gap from the last tiled row. Box remainingStrip; if (tiledAxis == NestDirection.Vertical) { var bottom = placedEdge + PartSpacing; var height = WorkArea.Top - bottom; if (height <= Tolerance.Epsilon) return new List(); remainingStrip = new Box(WorkArea.X, bottom, WorkArea.Width, height); } else { var left = placedEdge + PartSpacing; var width = WorkArea.Right - left; if (width <= Tolerance.Epsilon) return new List(); remainingStrip = new Box(left, WorkArea.Y, width, WorkArea.Length); } // Build rotation set: always try cardinal orientations (0° and 90°), // plus any unique rotations from the seed pattern. var filler = new FillLinear(remainingStrip, PartSpacing); List best = null; var rotations = new List<(Drawing drawing, double rotation)>(); // Cardinal rotations for each unique drawing. var drawings = new List(); foreach (var seedPart in seedPattern.Parts) { var found = false; foreach (var d in drawings) { if (d == seedPart.BaseDrawing) { found = true; break; } } if (!found) drawings.Add(seedPart.BaseDrawing); } foreach (var drawing in drawings) { rotations.Add((drawing, 0)); rotations.Add((drawing, Angle.HalfPI)); } // Add seed pattern rotations that aren't already covered. foreach (var seedPart in seedPattern.Parts) { var skip = false; foreach (var (d, r) in rotations) { if (d == seedPart.BaseDrawing && r.IsEqualTo(seedPart.Rotation)) { skip = true; break; } } if (!skip) rotations.Add((seedPart.BaseDrawing, seedPart.Rotation)); } var bag = new System.Collections.Concurrent.ConcurrentBag>(); System.Threading.Tasks.Parallel.ForEach(rotations, entry => { var localFiller = new FillLinear(remainingStrip, PartSpacing); var h = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Horizontal); var v = localFiller.Fill(entry.drawing, entry.rotation, NestDirection.Vertical); if (h != null && h.Count > 0) bag.Add(h); if (v != null && v.Count > 0) bag.Add(v); }); foreach (var candidate in bag) { if (best == null || candidate.Count > best.Count) best = candidate; } return best ?? new List(); } /// /// Fills a single row of identical parts along one axis using geometry-aware spacing. /// public Pattern FillRow(Drawing drawing, double rotationAngle, NestDirection direction) { var seed = MakeSeedPattern(drawing, rotationAngle); if (seed.Parts.Count == 0) return seed; var template = seed.Parts[0]; var boundary = new PartBoundary(template, HalfSpacing); var copyDistance = FindCopyDistance(template, direction, boundary); if (copyDistance <= 0) return seed; var dim = GetDimension(template.BoundingBox, direction); var start = GetStart(template.BoundingBox, direction); var limit = GetLimit(direction); var count = 1; while (true) { var nextPos = start + copyDistance * count; if (nextPos + dim > limit + Tolerance.Epsilon) break; var clone = template.CloneAtOffset(MakeOffset(direction, copyDistance * count)); seed.Parts.Add(clone); count++; } seed.UpdateBounds(); return seed; } /// /// Fills the work area by tiling a pre-built pattern along both axes. /// public List Fill(Pattern pattern, NestDirection primaryAxis) { if (pattern.Parts.Count == 0) return new List(); var offset = WorkArea.Location - pattern.BoundingBox.Location; var basePattern = pattern.Clone(offset); if (basePattern.BoundingBox.Width > WorkArea.Width + Tolerance.Epsilon || basePattern.BoundingBox.Length > WorkArea.Length + Tolerance.Epsilon) return new List(); return FillRecursive(basePattern, primaryAxis, depth: 0); } /// /// Fills the work area by creating a seed part, then recursively tiling /// along the primary axis and then the perpendicular axis. /// public List Fill(Drawing drawing, double rotationAngle, NestDirection primaryAxis) { var seed = MakeSeedPattern(drawing, rotationAngle); if (seed.Parts.Count == 0) return new List(); return FillRecursive(seed, primaryAxis, depth: 0); } } }