From e695e29355ee0dcbcf937f6bb0fe64f8e8f003d8 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Wed, 18 Mar 2026 20:24:33 -0400 Subject: [PATCH] =?UTF-8?q?Revert=20"refactor(compactor):=20deduplicate=20?= =?UTF-8?q?Push=20=E2=80=94=20PushDirection=20delegates=20to=20Vector=20ov?= =?UTF-8?q?erload"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9012a9fc1cc8345ee8b2578c1ed27fa220567326. --- OpenNest.Engine/Fill/Compactor.cs | 74 +++++++++++++++++++++++++++-- OpenNest.Engine/Fill/FillExtents.cs | 63 ++++++++++++++++++++---- OpenNest.Engine/Fill/FillLinear.cs | 53 ++++++++++++++++++++- OpenNest.Tests/CompactorTests.cs | 11 ++--- OpenNest/Forms/PatternTileForm.cs | 6 +-- 5 files changed, 181 insertions(+), 26 deletions(-) diff --git a/OpenNest.Engine/Fill/Compactor.cs b/OpenNest.Engine/Fill/Compactor.cs index 66af511..676f625 100644 --- a/OpenNest.Engine/Fill/Compactor.cs +++ b/OpenNest.Engine/Fill/Compactor.cs @@ -31,16 +31,16 @@ namespace OpenNest.Engine.Fill .Where(p => !movingParts.Contains(p)) .ToList(); - var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); - return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, direction); + return Push(movingParts, obstacleParts, plate.WorkArea(), plate.PartSpacing, angle); } /// /// Pushes movingParts along an arbitrary angle (radians, 0 = right, π/2 = up). /// public static double Push(List movingParts, List obstacleParts, - Box workArea, double partSpacing, Vector direction) + Box workArea, double partSpacing, double angle) { + var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); var opposite = -direction; var obstacleBoxes = new Box[obstacleParts.Count]; @@ -104,8 +104,72 @@ namespace OpenNest.Engine.Fill public static double Push(List movingParts, List obstacleParts, Box workArea, double partSpacing, PushDirection direction) { - var vector = SpatialQuery.DirectionToOffset(direction, 1.0); - return Push(movingParts, obstacleParts, workArea, partSpacing, vector); + var obstacleBoxes = new Box[obstacleParts.Count]; + var obstacleLines = new List[obstacleParts.Count]; + + for (var i = 0; i < obstacleParts.Count; i++) + obstacleBoxes[i] = obstacleParts[i].BoundingBox; + + var opposite = SpatialQuery.OppositeDirection(direction); + var halfSpacing = partSpacing / 2; + var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); + var distance = double.MaxValue; + + foreach (var moving in movingParts) + { + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); + if (edgeDist <= 0) + distance = 0; + else if (edgeDist < distance) + distance = edgeDist; + + var movingBox = moving.BoundingBox; + List movingLines = null; + + for (var i = 0; i < obstacleBoxes.Length; i++) + { + // Use the reverse-direction gap to check if the obstacle is entirely + // behind the moving part. The forward gap (gap < 0) is unreliable for + // irregular shapes whose bounding boxes overlap even when the actual + // geometry still has a valid contact in the push direction. + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + if (reverseGap > 0) + continue; + + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); + if (gap >= distance) + continue; + + var perpOverlap = isHorizontal + ? movingBox.IsHorizontalTo(obstacleBoxes[i], out _) + : movingBox.IsVerticalTo(obstacleBoxes[i], out _); + + if (!perpOverlap) + continue; + + movingLines ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(moving, halfSpacing, direction, ChordTolerance) + : PartGeometry.GetPartLines(moving, direction, ChordTolerance); + + obstacleLines[i] ??= halfSpacing > 0 + ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) + : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); + + var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); + if (d < distance) + distance = d; + } + } + + if (distance < double.MaxValue && distance > 0) + { + var offset = SpatialQuery.DirectionToOffset(direction, distance); + foreach (var moving in movingParts) + moving.Offset(offset); + return distance; + } + + return 0; } /// diff --git a/OpenNest.Engine/Fill/FillExtents.cs b/OpenNest.Engine/Fill/FillExtents.cs index e5ccd6f..40f7dee 100644 --- a/OpenNest.Engine/Fill/FillExtents.cs +++ b/OpenNest.Engine/Fill/FillExtents.cs @@ -1,10 +1,8 @@ -using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Threading; namespace OpenNest.Engine.Fill @@ -107,7 +105,7 @@ namespace OpenNest.Engine.Fill private List BuildColumn(Part part1, Part part2, Box pairBbox) { - var pairParts = new List { (Part)part1.Clone(), (Part)part2.Clone() }; + 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); @@ -128,11 +126,22 @@ namespace OpenNest.Engine.Fill boundary1, boundary2, pairHeight); if (copyDistance <= 0) - return pairParts; + return column; - var result = new List(pairParts); - result.AddRange(FillHelpers.Tile(pairParts, workArea, copyDistance, NestDirection.Vertical, allowPartial: false)); - return result; + var count = 1; + while (true) + { + var nextBottom = pairBbox.Bottom + copyDistance * count; + if (nextBottom + pairHeight > workArea.Top + Tolerance.Epsilon) + break; + + var offset = new Vector(0, copyDistance * count); + column.Add(part1.CloneAtOffset(offset)); + column.Add(part2.CloneAtOffset(offset)); + count++; + } + + return column; } private double FindVerticalCopyDistance( @@ -315,10 +324,48 @@ namespace OpenNest.Engine.Fill Debug.WriteLine($"[FillExtents] Column copy distance: {copyDistance:F2} (bbox width: {columnWidth:F2}, spacing: {partSpacing:F2})"); + // Build all columns. var result = new List(column); - result.AddRange(FillHelpers.Tile(column, workArea, copyDistance, NestDirection.Horizontal, allowPartial: true)); + + // 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); + var anyFit = false; + + foreach (var part in column) + { + var clone = part.CloneAtOffset(offset); + if (IsWithinWorkArea(clone)) + { + result.Add(clone); + anyFit = true; + } + } + + if (!anyFit) + break; + + colIndex++; + } return result; } + + private bool IsWithinWorkArea(Part part) + { + return 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; + } } } diff --git a/OpenNest.Engine/Fill/FillLinear.cs b/OpenNest.Engine/Fill/FillLinear.cs index 98e88ed..674bff3 100644 --- a/OpenNest.Engine/Fill/FillLinear.cs +++ b/OpenNest.Engine/Fill/FillLinear.cs @@ -1,4 +1,3 @@ -using OpenNest.Engine.Strategies; using OpenNest.Geometry; using OpenNest.Math; using System.Collections.Generic; @@ -250,7 +249,57 @@ namespace OpenNest.Engine.Fill private List TilePattern(Pattern basePattern, NestDirection direction, PartBoundary[] boundaries) { var copyDistance = FindPatternCopyDistance(basePattern, direction, boundaries); - return FillHelpers.Tile(basePattern.Parts, WorkArea, copyDistance, direction, allowPartial: true); + + 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; } /// diff --git a/OpenNest.Tests/CompactorTests.cs b/OpenNest.Tests/CompactorTests.cs index 8d3e2c5..a205d24 100644 --- a/OpenNest.Tests/CompactorTests.cs +++ b/OpenNest.Tests/CompactorTests.cs @@ -109,9 +109,8 @@ namespace OpenNest.Tests var moving = new List { part }; var obstacles = new List(); - // direction = left - var direction = new Vector(System.Math.Cos(System.Math.PI), System.Math.Sin(System.Math.PI)); - var distance = Compactor.Push(moving, obstacles, workArea, 0, direction); + // angle = π = push left + var distance = Compactor.Push(moving, obstacles, workArea, 0, System.Math.PI); Assert.True(distance > 0); Assert.True(part.BoundingBox.Left < 1); @@ -125,10 +124,8 @@ namespace OpenNest.Tests var moving = new List { part }; var obstacles = new List(); - // direction = down - var angle = 3 * System.Math.PI / 2; - var direction = new Vector(System.Math.Cos(angle), System.Math.Sin(angle)); - var distance = Compactor.Push(moving, obstacles, workArea, 0, direction); + // angle = 3π/2 = push down + var distance = Compactor.Push(moving, obstacles, workArea, 0, 3 * System.Math.PI / 2); Assert.True(distance > 0); Assert.True(part.BoundingBox.Bottom < 1); diff --git a/OpenNest/Forms/PatternTileForm.cs b/OpenNest/Forms/PatternTileForm.cs index 5d2a52d..acbde4d 100644 --- a/OpenNest/Forms/PatternTileForm.cs +++ b/OpenNest/Forms/PatternTileForm.cs @@ -213,14 +213,12 @@ namespace OpenNest.Forms if (System.Math.Sqrt(dx * dx + dy * dy) < 0.01) continue; - var direction = new Vector(dx, dy); - var len = System.Math.Sqrt(dx * dx + dy * dy); - if (len > 0) direction = new Vector(dx / len, dy / len); + var angle = System.Math.Atan2(dy, dx); var single = new List { part }; var obstacles = parts.Where(p => p != part).ToList(); totalMoved += Compactor.Push(single, obstacles, - syntheticWorkArea, spacing, direction); + syntheticWorkArea, spacing, angle); } if (totalMoved < 0.01)