From d1d47b52233103154fd057e043db891773be002c Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:13:55 -0400 Subject: [PATCH] refactor(engine): simplify FillExtents logic using Compactor.Push Simplify geometry-aware positioning by replacing manual slide calculations with higher-level Compactor.Push utility. Extract pair creation into CreatePair helper, remove redundant UpdateBounds calls, and clean up column/horizontal repetition logic. --- OpenNest.Engine/Fill/FillExtents.cs | 262 ++++++++-------------------- 1 file changed, 69 insertions(+), 193 deletions(-) diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index 40f7dee..b331b87 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -3,6 +3,7 @@ using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Threading; namespace OpenNest.Engine.Fill @@ -13,13 +14,11 @@ namespace OpenNest.Engine.Fill private readonly Box workArea; private readonly double partSpacing; - private readonly double halfSpacing; public FillExtents(Box workArea, double partSpacing) { this.workArea = workArea; this.partSpacing = partSpacing; - halfSpacing = partSpacing / 2; } public List Fill(Drawing drawing, double rotationAngle = 0, @@ -27,18 +26,18 @@ namespace OpenNest.Engine.Fill CancellationToken token = default, IProgress progress = null) { - var pair = BuildPair(drawing, rotationAngle); - if (pair == null) + var initialPair = CreatePair(drawing, rotationAngle, rotationAngle + System.Math.PI); + if (initialPair == null) return new List(); - var column = BuildColumn(pair.Value.part1, pair.Value.part2, pair.Value.pairBbox); + var column = BuildColumn(initialPair.Value.part1, initialPair.Value.part2, initialPair.Value.pairBbox); if (column.Count == 0) return new List(); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, column, workArea, $"Extents: initial column {column.Count} parts"); - var adjusted = AdjustColumn(pair.Value, column, token); + var adjusted = AdjustColumn(initialPair.Value, column, token); NestEngineBase.ReportProgress(progress, NestPhase.Extents, plateNumber, adjusted, workArea, $"Extents: adjusted column {adjusted.Count} parts"); @@ -53,52 +52,33 @@ namespace OpenNest.Engine.Fill // --- Step 1: Pair Construction --- - private (Part part1, Part part2, Box pairBbox)? BuildPair(Drawing drawing, double rotationAngle) + private (Part part1, Part part2, Box pairBbox)? CreatePair( + Drawing drawing, double rotation1, double rotation2, double verticalShift2 = 0) { - var part1 = Part.CreateAtOrigin(drawing, rotationAngle); - var part2 = Part.CreateAtOrigin(drawing, rotationAngle + System.Math.PI); + var p1 = Part.CreateAtOrigin(drawing, rotation1); + var p2 = Part.CreateAtOrigin(drawing, rotation2); - // Check that each part fits in the work area individually. - if (part1.BoundingBox.Width > workArea.Width + Tolerance.Epsilon || - part1.BoundingBox.Length > workArea.Length + Tolerance.Epsilon) - return null; + // Initial positioning: p2 to the right of p1, with optional vertical shift. + p2.Offset(p1.BoundingBox.Width + partSpacing, verticalShift2); - // Slide part2 toward part1 from the right using geometry-aware distance. - var boundary1 = new PartBoundary(part1, halfSpacing); - var boundary2 = new PartBoundary(part2, halfSpacing); + // Compact p2 left toward p1 using geometry-aware distance. + Compactor.Push(new List { p2 }, new List { p1 }, workArea, partSpacing, PushDirection.Left); - // Position part2 to the right of part1 at bounding box width distance. - var startOffset = part1.BoundingBox.Width + part2.BoundingBox.Width + partSpacing; - part2.Offset(startOffset, 0); - part2.UpdateBounds(); + var pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - // Slide part2 left toward part1. - var movingLines = boundary2.GetLines(part2.Location, PushDirection.Left); - var stationaryLines = boundary1.GetLines(part1.Location, PushDirection.Right); - var dist = SpatialQuery.DirectionalDistance(movingLines, stationaryLines, PushDirection.Left); - - if (dist < double.MaxValue && dist > 0) - { - part2.Offset(-dist, 0); - part2.UpdateBounds(); - } - - // Re-anchor pair to work area origin. - var pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + // Re-anchor pair to work area origin (bottom-left). var anchor = new Vector(workArea.X - pairBbox.Left, workArea.Y - pairBbox.Bottom); - part1.Offset(anchor); - part2.Offset(anchor); - part1.UpdateBounds(); - part2.UpdateBounds(); + p1.Offset(anchor); + p2.Offset(anchor); - pairBbox = ((IEnumerable)new IBoundable[] { part1, part2 }).GetBoundingBox(); + pairBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); // Verify pair fits in work area. if (pairBbox.Width > workArea.Width + Tolerance.Epsilon || pairBbox.Length > workArea.Length + Tolerance.Epsilon) return null; - return (part1, part2, pairBbox); + return (p1, p2, pairBbox); } // --- Step 2: Build Column (tile vertically) --- @@ -107,193 +87,104 @@ namespace OpenNest.Engine.Fill { var column = new List { (Part)part1.Clone(), (Part)part2.Clone() }; - // Find geometry-aware copy distance for the pair vertically. - var boundary1 = new PartBoundary(part1, halfSpacing); - var boundary2 = new PartBoundary(part2, halfSpacing); - - // Compute vertical copy distance using bounding boxes as starting point, - // then slide down to find true geometry distance. - var pairHeight = pairBbox.Length; - var testOffset = new Vector(0, pairHeight); - - // Create test parts for slide distance measurement. - var testPart1 = part1.CloneAtOffset(testOffset); - var testPart2 = part2.CloneAtOffset(testOffset); - - // Find minimum distance from test pair sliding down toward original pair. - var copyDistance = FindVerticalCopyDistance( - part1, part2, testPart1, testPart2, - boundary1, boundary2, pairHeight); - + var copyDistance = ComputeVerticalCopyDistance(part1, part2, pairBbox); if (copyDistance <= 0) return column; - var count = 1; - while (true) - { - var nextBottom = pairBbox.Bottom + copyDistance * count; - if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) - break; + var pairHeight = pairBbox.Length; + var currentY = pairBbox.Bottom + copyDistance; - var offset = new Vector(0, copyDistance * count); + while (currentY + pairHeight <= workArea.Top + Tolerance.Epsilon) + { + var offset = new Vector(0, currentY - pairBbox.Bottom); column.Add(part1.CloneAtOffset(offset)); column.Add(part2.CloneAtOffset(offset)); - count++; + currentY += copyDistance; } return column; } - private double FindVerticalCopyDistance( - Part origPart1, Part origPart2, - Part testPart1, Part testPart2, - PartBoundary boundary1, PartBoundary boundary2, - double pairHeight) + private double ComputeVerticalCopyDistance(Part p1, Part p2, Box pairBbox) { - // Check all 4 combinations: test parts sliding down toward original parts. - var minSlide = double.MaxValue; + var pairHeight = pairBbox.Length; + // Start the test pair high enough so it doesn't overlap the original pair's bounding box initially. + var startOffset = pairHeight + partSpacing; + var testParts = new List { p1.CloneAtOffset(new Vector(0, startOffset)), p2.CloneAtOffset(new Vector(0, startOffset)) }; + var obstacles = new List { p1, p2 }; - // Test1 -> Orig1 - var d = SlideDistance(boundary1, testPart1.Location, boundary1, origPart1.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test1 -> Orig2 - d = SlideDistance(boundary1, testPart1.Location, boundary2, origPart2.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test2 -> Orig1 - d = SlideDistance(boundary2, testPart2.Location, boundary1, origPart1.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - // Test2 -> Orig2 - d = SlideDistance(boundary2, testPart2.Location, boundary2, origPart2.Location, PushDirection.Down); - if (d < minSlide) minSlide = d; - - if (minSlide >= double.MaxValue || minSlide < 0) - return pairHeight + partSpacing; - - // Match FillLinear.ComputeCopyDistance: copyDist = startOffset - slide, - // clamped so it never goes below pairHeight + partSpacing to prevent - // bounding-box overlap from spurious slide values. - var copyDist = pairHeight - minSlide; + // Use a large work area to prevent edge-clamping during distance measurement. + var largeWorkArea = new Box(workArea.X, workArea.Y - pairHeight, workArea.Width, workArea.Length + pairHeight * 3); + var slide = Compactor.Push(testParts, obstacles, largeWorkArea, partSpacing, PushDirection.Down); + // True copy distance = start - slide. Clamp to BB height + spacing to prevent BB overlap. + var copyDist = startOffset - slide; return System.Math.Max(copyDist, pairHeight + partSpacing); } - private static double SlideDistance( - PartBoundary movingBoundary, Vector movingLocation, - PartBoundary stationaryBoundary, Vector stationaryLocation, - PushDirection direction) - { - var opposite = SpatialQuery.OppositeDirection(direction); - var movingEdges = movingBoundary.GetEdges(direction); - var stationaryEdges = stationaryBoundary.GetEdges(opposite); - - return SpatialQuery.DirectionalDistance( - movingEdges, movingLocation, - stationaryEdges, stationaryLocation, - direction); - } - // --- Step 3: Iterative Adjustment --- private List AdjustColumn( - (Part part1, Part part2, Box pairBbox) pair, - List column, + (Part part1, Part part2, Box pairBbox) initialPair, + List initialColumn, CancellationToken token) { - var originalPairWidth = pair.pairBbox.Width; + var currentPair = initialPair; + var currentColumn = initialColumn; + var originalWidth = initialPair.pairBbox.Width; for (var iteration = 0; iteration < MaxIterations; iteration++) { if (token.IsCancellationRequested) break; - // Measure current gap. - var topEdge = double.MinValue; - foreach (var p in column) - if (p.BoundingBox.Top > topEdge) - topEdge = p.BoundingBox.Top; - - var gap = workArea.Top - topEdge; + var columnBbox = ((IEnumerable)currentColumn).GetBoundingBox(); + var gap = workArea.Top - columnBbox.Top; if (gap <= Tolerance.Epsilon) break; - var pairCount = column.Count / 2; - if (pairCount <= 0) - break; - + var pairCount = currentColumn.Count / 2; var adjustment = gap / pairCount; if (adjustment <= Tolerance.Epsilon) break; - // Try adjusting the pair and rebuilding the column. - var adjusted = TryAdjustPair(pair, adjustment, originalPairWidth); + // Try shifting p2 up or down relative to p1 to see if we can close the gap + // without making the pair wider than its initial horizontal footprint. + var adjusted = TryAdjustPair(currentPair, adjustment, originalWidth); if (adjusted == null) break; var newColumn = BuildColumn(adjusted.Value.part1, adjusted.Value.part2, adjusted.Value.pairBbox); - if (newColumn.Count == 0) - break; + if (newColumn.Count <= currentColumn.Count) + break; // No improvement in part count. - column = newColumn; - pair = adjusted.Value; + currentColumn = newColumn; + currentPair = adjusted.Value; } - return column; + return currentColumn; } private (Part part1, Part part2, Box pairBbox)? TryAdjustPair( (Part part1, Part part2, Box pairBbox) pair, - double adjustment, double originalPairWidth) + double adjustment, double maxWidth) { // Try shifting part2 up first. - var result = TryShiftDirection(pair, adjustment, originalPairWidth); + var result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, + (pair.part2.Location.Y - pair.part1.Location.Y) + adjustment); - if (result != null) + if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) return result; - // Up made the pair wider — try down instead. - return TryShiftDirection(pair, -adjustment, originalPairWidth); - } + // Up made it wider or didn't fit — try down instead. + result = CreatePair(pair.part1.BaseDrawing, pair.part1.Rotation, pair.part2.Rotation, + (pair.part2.Location.Y - pair.part1.Location.Y) - adjustment); - private (Part part1, Part part2, Box pairBbox)? TryShiftDirection( - (Part part1, Part part2, Box pairBbox) pair, - double verticalShift, double originalPairWidth) - { - // Clone parts so we don't mutate the originals. - var p1 = (Part)pair.part1.Clone(); - var p2 = (Part)pair.part2.Clone(); + if (result != null && result.Value.pairBbox.Width <= maxWidth + Tolerance.Epsilon) + return result; - // Separate: shift part2 right so bounding boxes don't touch. - p2.Offset(partSpacing, 0); - p2.UpdateBounds(); - - // Apply the vertical shift. - p2.Offset(0, verticalShift); - p2.UpdateBounds(); - - // Compact part2 left toward part1. - var moving = new List { p2 }; - var obstacles = new List { p1 }; - Compactor.Push(moving, obstacles, workArea, partSpacing, PushDirection.Left); - - // Check if the pair got wider. - var newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - - if (newBbox.Width > originalPairWidth + Tolerance.Epsilon) - return null; - - // Re-anchor to work area origin. - var anchor = new Vector(workArea.X - newBbox.Left, workArea.Y - newBbox.Bottom); - p1.Offset(anchor); - p2.Offset(anchor); - p1.UpdateBounds(); - p2.UpdateBounds(); - - newBbox = ((IEnumerable)new IBoundable[] { p1, p2 }).GetBoundingBox(); - return (p1, p2, newBbox); + return null; } // --- Step 4: Horizontal Repetition --- @@ -306,36 +197,21 @@ namespace OpenNest.Engine.Fill var columnBbox = ((IEnumerable)column).GetBoundingBox(); var columnWidth = columnBbox.Width; - // Create a test column shifted right by columnWidth + spacing. - var testOffset = columnWidth + partSpacing; - var testColumn = new List(column.Count); - foreach (var part in column) - testColumn.Add(part.CloneAtOffset(new Vector(testOffset, 0))); + // Create a test column shifted right and compact it left to find the true copy distance. + var startOffset = columnWidth + partSpacing; + var testColumn = column.Select(p => p.CloneAtOffset(new Vector(startOffset, 0))).ToList(); - // Compact the test column left against the original column. - var distanceMoved = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); - - // Derive the true copy distance from where the test column ended up. - var testBbox = ((IEnumerable)testColumn).GetBoundingBox(); - var copyDistance = testBbox.Left - columnBbox.Left; + var slide = Compactor.Push(testColumn, column, workArea, partSpacing, PushDirection.Left); + var copyDistance = startOffset - slide; if (copyDistance <= Tolerance.Epsilon) copyDistance = columnWidth + partSpacing; Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); - // Build all columns. var result = new List(column); + var colIndex = 1; - // Add the test column we already computed as column 2. - foreach (var part in testColumn) - { - if (IsWithinWorkArea(part)) - result.Add(part); - } - - // Tile additional columns at the copy distance. - var colIndex = 2; while (!token.IsCancellationRequested) { var offset = new Vector(copyDistance * colIndex, 0);