From 13b01240b19410d9e257a9b3f05f9103ce046608 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Sun, 15 Mar 2026 17:46:14 -0400 Subject: [PATCH] refactor: extract SpatialQuery from Helper Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Core/Geometry/SpatialQuery.cs | 614 ++++++++++++++++++ OpenNest.Core/Helper.cs | 612 ----------------- .../BestFit/RotationSlideStrategy.cs | 12 +- OpenNest.Engine/Compactor.cs | 14 +- OpenNest.Engine/FillLinear.cs | 8 +- OpenNest/Actions/ActionClone.cs | 4 +- OpenNest/Actions/ActionSelectArea.cs | 4 +- 7 files changed, 635 insertions(+), 633 deletions(-) create mode 100644 OpenNest.Core/Geometry/SpatialQuery.cs diff --git a/OpenNest.Core/Geometry/SpatialQuery.cs b/OpenNest.Core/Geometry/SpatialQuery.cs new file mode 100644 index 0000000..dca54cf --- /dev/null +++ b/OpenNest.Core/Geometry/SpatialQuery.cs @@ -0,0 +1,614 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using OpenNest.Math; + +namespace OpenNest.Geometry +{ + public static class SpatialQuery + { + /// + /// Finds the distance from a vertex to a line segment along a push axis. + /// Returns double.MaxValue if the ray does not hit the segment. + /// + private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction) + { + return RayEdgeDistance( + vertex.X, vertex.Y, + edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y, + direction); + } + + [System.Runtime.CompilerServices.MethodImpl( + System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + private static double RayEdgeDistance( + double vx, double vy, + double p1x, double p1y, double p2x, double p2y, + PushDirection direction) + { + switch (direction) + { + case PushDirection.Left: + case PushDirection.Right: + { + var dy = p2y - p1y; + if (System.Math.Abs(dy) < Tolerance.Epsilon) + return double.MaxValue; + + var t = (vy - p1y) / dy; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var ix = p1x + t * (p2x - p1x); + var dist = direction == PushDirection.Left ? vx - ix : ix - vx; + + if (dist > Tolerance.Epsilon) return dist; + if (dist >= -Tolerance.Epsilon) return 0; + return double.MaxValue; + } + + case PushDirection.Down: + case PushDirection.Up: + { + var dx = p2x - p1x; + if (System.Math.Abs(dx) < Tolerance.Epsilon) + return double.MaxValue; + + var t = (vx - p1x) / dx; + if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) + return double.MaxValue; + + var iy = p1y + t * (p2y - p1y); + var dist = direction == PushDirection.Down ? vy - iy : iy - vy; + + if (dist > Tolerance.Epsilon) return dist; + if (dist >= -Tolerance.Epsilon) return 0; + return double.MaxValue; + } + + default: + return double.MaxValue; + } + } + + /// + /// Computes the minimum translation distance along a push direction before + /// any edge of movingLines contacts any edge of stationaryLines. + /// Returns double.MaxValue if no collision path exists. + /// + public static double DirectionalDistance(List movingLines, List stationaryLines, PushDirection direction) + { + var minDist = double.MaxValue; + + // Case 1: Each moving vertex -> each stationary edge + var movingVertices = new HashSet(); + for (int i = 0; i < movingLines.Count; i++) + { + movingVertices.Add(movingLines[i].pt1); + movingVertices.Add(movingLines[i].pt2); + } + + var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count]; + for (int i = 0; i < stationaryLines.Count; i++) + stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2); + + // Sort edges for pruning if not already sorted (usually they aren't here) + if (direction == PushDirection.Left || direction == PushDirection.Right) + stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + + foreach (var mv in movingVertices) + { + var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); + if (d < minDist) minDist = d; + } + + // Case 2: Each stationary vertex -> each moving edge (opposite direction) + var opposite = OppositeDirection(direction); + var stationaryVertices = new HashSet(); + for (int i = 0; i < stationaryLines.Count; i++) + { + stationaryVertices.Add(stationaryLines[i].pt1); + stationaryVertices.Add(stationaryLines[i].pt2); + } + + var movingEdges = new (Vector start, Vector end)[movingLines.Count]; + for (int i = 0; i < movingLines.Count; i++) + movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2); + + if (opposite == PushDirection.Left || opposite == PushDirection.Right) + movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + + foreach (var sv in stationaryVertices) + { + var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite); + if (d < minDist) minDist = d; + } + + return minDist; + } + + /// + /// Computes the minimum directional distance with the moving lines translated + /// by (movingDx, movingDy) without creating new Line objects. + /// + public static double DirectionalDistance( + List movingLines, double movingDx, double movingDy, + List stationaryLines, PushDirection direction) + { + var minDist = double.MaxValue; + var movingOffset = new Vector(movingDx, movingDy); + + // Case 1: Each moving vertex -> each stationary edge + var movingVertices = new HashSet(); + for (int i = 0; i < movingLines.Count; i++) + { + movingVertices.Add(movingLines[i].pt1 + movingOffset); + movingVertices.Add(movingLines[i].pt2 + movingOffset); + } + + var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count]; + for (int i = 0; i < stationaryLines.Count; i++) + stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2); + + if (direction == PushDirection.Left || direction == PushDirection.Right) + stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + + foreach (var mv in movingVertices) + { + var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); + if (d < minDist) minDist = d; + } + + // Case 2: Each stationary vertex -> each moving edge (opposite direction) + var opposite = OppositeDirection(direction); + var stationaryVertices = new HashSet(); + for (int i = 0; i < stationaryLines.Count; i++) + { + stationaryVertices.Add(stationaryLines[i].pt1); + stationaryVertices.Add(stationaryLines[i].pt2); + } + + var movingEdges = new (Vector start, Vector end)[movingLines.Count]; + for (int i = 0; i < movingLines.Count; i++) + movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2); + + if (opposite == PushDirection.Left || opposite == PushDirection.Right) + movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); + else + movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); + + foreach (var sv in stationaryVertices) + { + var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); + if (d < minDist) minDist = d; + } + + return minDist; + } + + /// + /// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer. + /// + public static double[] FlattenLines(List lines) + { + var result = new double[lines.Count * 4]; + for (int i = 0; i < lines.Count; i++) + { + var line = lines[i]; + result[i * 4] = line.pt1.X; + result[i * 4 + 1] = line.pt1.Y; + result[i * 4 + 2] = line.pt2.X; + result[i * 4 + 3] = line.pt2.Y; + } + return result; + } + + /// + /// Computes the minimum directional distance using raw edge arrays and location offsets + /// to avoid all intermediate object allocations. + /// + public static double DirectionalDistance( + (Vector start, Vector end)[] movingEdges, Vector movingOffset, + (Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset, + PushDirection direction) + { + var minDist = double.MaxValue; + + // Extract unique vertices from moving edges. + var movingVertices = new HashSet(); + for (var i = 0; i < movingEdges.Length; i++) + { + movingVertices.Add(movingEdges[i].start + movingOffset); + movingVertices.Add(movingEdges[i].end + movingOffset); + } + + // Case 1: Each moving vertex -> each stationary edge + foreach (var mv in movingVertices) + { + var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); + if (d < minDist) minDist = d; + } + + // Case 2: Each stationary vertex -> each moving edge (opposite direction) + var opposite = OppositeDirection(direction); + var stationaryVertices = new HashSet(); + for (var i = 0; i < stationaryEdges.Length; i++) + { + stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset); + stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset); + } + + foreach (var sv in stationaryVertices) + { + var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); + if (d < minDist) minDist = d; + } + + return minDist; + } + + public static double OneWayDistance( + Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, + PushDirection direction) + { + var minDist = double.MaxValue; + var vx = vertex.X; + var vy = vertex.Y; + + // Pruning: edges are sorted by their perpendicular min-coordinate in PartBoundary. + if (direction == PushDirection.Left || direction == PushDirection.Right) + { + for (var i = 0; i < edges.Length; i++) + { + var e1 = edges[i].start + edgeOffset; + var e2 = edges[i].end + edgeOffset; + + var minY = e1.Y < e2.Y ? e1.Y : e2.Y; + var maxY = e1.Y > e2.Y ? e1.Y : e2.Y; + + // Since edges are sorted by minY, if vy < minY, then vy < all subsequent minY. + if (vy < minY - Tolerance.Epsilon) + break; + + if (vy > maxY + Tolerance.Epsilon) + continue; + + var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); + if (d < minDist) minDist = d; + } + } + else // Up/Down + { + for (var i = 0; i < edges.Length; i++) + { + var e1 = edges[i].start + edgeOffset; + var e2 = edges[i].end + edgeOffset; + + var minX = e1.X < e2.X ? e1.X : e2.X; + var maxX = e1.X > e2.X ? e1.X : e2.X; + + // Since edges are sorted by minX, if vx < minX, then vx < all subsequent minX. + if (vx < minX - Tolerance.Epsilon) + break; + + if (vx > maxX + Tolerance.Epsilon) + continue; + + var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); + if (d < minDist) minDist = d; + } + } + + return minDist; + } + + public static PushDirection OppositeDirection(PushDirection direction) + { + switch (direction) + { + case PushDirection.Left: return PushDirection.Right; + case PushDirection.Right: return PushDirection.Left; + case PushDirection.Up: return PushDirection.Down; + case PushDirection.Down: return PushDirection.Up; + default: return direction; + } + } + + public static bool IsHorizontalDirection(PushDirection direction) + { + return direction is PushDirection.Left or PushDirection.Right; + } + + public static double EdgeDistance(Box box, Box boundary, PushDirection direction) + { + switch (direction) + { + case PushDirection.Left: return box.Left - boundary.Left; + case PushDirection.Right: return boundary.Right - box.Right; + case PushDirection.Up: return boundary.Top - box.Top; + case PushDirection.Down: return box.Bottom - boundary.Bottom; + default: return double.MaxValue; + } + } + + public static Vector DirectionToOffset(PushDirection direction, double distance) + { + switch (direction) + { + case PushDirection.Left: return new Vector(-distance, 0); + case PushDirection.Right: return new Vector(distance, 0); + case PushDirection.Up: return new Vector(0, distance); + case PushDirection.Down: return new Vector(0, -distance); + default: return new Vector(); + } + } + + public static double DirectionalGap(Box from, Box to, PushDirection direction) + { + switch (direction) + { + case PushDirection.Left: return from.Left - to.Right; + case PushDirection.Right: return to.Left - from.Right; + case PushDirection.Up: return to.Bottom - from.Top; + case PushDirection.Down: return from.Bottom - to.Top; + default: return double.MaxValue; + } + } + + public static double ClosestDistanceLeft(Box box, List boxes) + { + var closestDistance = double.MaxValue; + + for (int i = 0; i < boxes.Count; i++) + { + var compareBox = boxes[i]; + + RelativePosition pos; + + if (!box.IsHorizontalTo(compareBox, out pos)) + continue; + + if (pos != RelativePosition.Right) + continue; + + var distance = box.Left - compareBox.Right; + + if (distance < closestDistance) + closestDistance = distance; + } + + return closestDistance == double.MaxValue ? double.NaN : closestDistance; + } + + public static double ClosestDistanceRight(Box box, List boxes) + { + var closestDistance = double.MaxValue; + + for (int i = 0; i < boxes.Count; i++) + { + var compareBox = boxes[i]; + + RelativePosition pos; + + if (!box.IsHorizontalTo(compareBox, out pos)) + continue; + + if (pos != RelativePosition.Left) + continue; + + var distance = compareBox.Left - box.Right; + + if (distance < closestDistance) + closestDistance = distance; + } + + return closestDistance == double.MaxValue ? double.NaN : closestDistance; + } + + public static double ClosestDistanceUp(Box box, List boxes) + { + var closestDistance = double.MaxValue; + + for (int i = 0; i < boxes.Count; i++) + { + var compareBox = boxes[i]; + + RelativePosition pos; + + if (!box.IsVerticalTo(compareBox, out pos)) + continue; + + if (pos != RelativePosition.Bottom) + continue; + + var distance = compareBox.Bottom - box.Top; + + if (distance < closestDistance) + closestDistance = distance; + } + + return closestDistance == double.MaxValue ? double.NaN : closestDistance; + } + + public static double ClosestDistanceDown(Box box, List boxes) + { + var closestDistance = double.MaxValue; + + for (int i = 0; i < boxes.Count; i++) + { + var compareBox = boxes[i]; + + RelativePosition pos; + + if (!box.IsVerticalTo(compareBox, out pos)) + continue; + + if (pos != RelativePosition.Top) + continue; + + var distance = box.Bottom - compareBox.Top; + + if (distance < closestDistance) + closestDistance = distance; + } + + return closestDistance == double.MaxValue ? double.NaN : closestDistance; + } + + public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable boxes) + { + var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList(); + + #region Find Top/Bottom Limits + + var top = double.MaxValue; + var btm = double.MinValue; + + foreach (var box in verticalBoxes) + { + var boxBtm = box.Bottom; + var boxTop = box.Top; + + if (boxBtm > pt.Y && boxBtm < top) + top = boxBtm; + + else if (box.Top < pt.Y && boxTop > btm) + btm = boxTop; + } + + if (top == double.MaxValue) + { + if (bounds.Top > pt.Y) + top = bounds.Top; + else return Box.Empty; + } + + if (btm == double.MinValue) + { + if (bounds.Bottom < pt.Y) + btm = bounds.Bottom; + else return Box.Empty; + } + + #endregion + + var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList(); + + #region Find Left/Right Limits + + var lft = double.MinValue; + var rgt = double.MaxValue; + + foreach (var box in horizontalBoxes) + { + var boxLft = box.Left; + var boxRgt = box.Right; + + if (boxLft > pt.X && boxLft < rgt) + rgt = boxLft; + + else if (boxRgt < pt.X && boxRgt > lft) + lft = boxRgt; + } + + if (rgt == double.MaxValue) + { + if (bounds.Right > pt.X) + rgt = bounds.Right; + else return Box.Empty; + } + + if (lft == double.MinValue) + { + if (bounds.Left < pt.X) + lft = bounds.Left; + else return Box.Empty; + } + + #endregion + + return new Box(lft, btm, rgt - lft, top - btm); + } + + public static Box GetLargestBoxHorizontally(Vector pt, Box bounds, IEnumerable boxes) + { + var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList(); + + #region Find Left/Right Limits + + var lft = double.MinValue; + var rgt = double.MaxValue; + + foreach (var box in horizontalBoxes) + { + var boxLft = box.Left; + var boxRgt = box.Right; + + if (boxLft > pt.X && boxLft < rgt) + rgt = boxLft; + + else if (boxRgt < pt.X && boxRgt > lft) + lft = boxRgt; + } + + if (rgt == double.MaxValue) + { + if (bounds.Right > pt.X) + rgt = bounds.Right; + else return Box.Empty; + } + + if (lft == double.MinValue) + { + if (bounds.Left < pt.X) + lft = bounds.Left; + else return Box.Empty; + } + + #endregion + + var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList(); + + #region Find Top/Bottom Limits + + var top = double.MaxValue; + var btm = double.MinValue; + + foreach (var box in verticalBoxes) + { + var boxBtm = box.Bottom; + var boxTop = box.Top; + + if (boxBtm > pt.Y && boxBtm < top) + top = boxBtm; + + else if (box.Top < pt.Y && boxTop > btm) + btm = boxTop; + } + + if (top == double.MaxValue) + { + if (bounds.Top > pt.Y) + top = bounds.Top; + else return Box.Empty; + } + + if (btm == double.MinValue) + { + if (bounds.Bottom < pt.Y) + btm = bounds.Bottom; + else return Box.Empty; + } + + #endregion + + return new Box(lft, btm, rgt - lft, top - btm); + } + } +} diff --git a/OpenNest.Core/Helper.cs b/OpenNest.Core/Helper.cs index a41b50f..fd09a71 100644 --- a/OpenNest.Core/Helper.cs +++ b/OpenNest.Core/Helper.cs @@ -1,618 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading.Tasks; -using OpenNest.Converters; -using OpenNest.Geometry; -using OpenNest.Math; - namespace OpenNest { public static class Helper { - /// - /// Finds the distance from a vertex to a line segment along a push axis. - /// Returns double.MaxValue if the ray does not hit the segment. - /// - private static double RayEdgeDistance(Vector vertex, Line edge, PushDirection direction) - { - return RayEdgeDistance( - vertex.X, vertex.Y, - edge.pt1.X, edge.pt1.Y, edge.pt2.X, edge.pt2.Y, - direction); - } - - [System.Runtime.CompilerServices.MethodImpl( - System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] - private static double RayEdgeDistance( - double vx, double vy, - double p1x, double p1y, double p2x, double p2y, - PushDirection direction) - { - switch (direction) - { - case PushDirection.Left: - case PushDirection.Right: - { - var dy = p2y - p1y; - if (System.Math.Abs(dy) < Tolerance.Epsilon) - return double.MaxValue; - - var t = (vy - p1y) / dy; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var ix = p1x + t * (p2x - p1x); - var dist = direction == PushDirection.Left ? vx - ix : ix - vx; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; - return double.MaxValue; - } - - case PushDirection.Down: - case PushDirection.Up: - { - var dx = p2x - p1x; - if (System.Math.Abs(dx) < Tolerance.Epsilon) - return double.MaxValue; - - var t = (vx - p1x) / dx; - if (t < -Tolerance.Epsilon || t > 1.0 + Tolerance.Epsilon) - return double.MaxValue; - - var iy = p1y + t * (p2y - p1y); - var dist = direction == PushDirection.Down ? vy - iy : iy - vy; - - if (dist > Tolerance.Epsilon) return dist; - if (dist >= -Tolerance.Epsilon) return 0; - return double.MaxValue; - } - - default: - return double.MaxValue; - } - } - - /// - /// Computes the minimum translation distance along a push direction before - /// any edge of movingLines contacts any edge of stationaryLines. - /// Returns double.MaxValue if no collision path exists. - /// - public static double DirectionalDistance(List movingLines, List stationaryLines, PushDirection direction) - { - var minDist = double.MaxValue; - - // Case 1: Each moving vertex -> each stationary edge - var movingVertices = new HashSet(); - for (int i = 0; i < movingLines.Count; i++) - { - movingVertices.Add(movingLines[i].pt1); - movingVertices.Add(movingLines[i].pt2); - } - - var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count]; - for (int i = 0; i < stationaryLines.Count; i++) - stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2); - - // Sort edges for pruning if not already sorted (usually they aren't here) - if (direction == PushDirection.Left || direction == PushDirection.Right) - stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); - else - stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); - - foreach (var mv in movingVertices) - { - var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); - if (d < minDist) minDist = d; - } - - // Case 2: Each stationary vertex -> each moving edge (opposite direction) - var opposite = OppositeDirection(direction); - var stationaryVertices = new HashSet(); - for (int i = 0; i < stationaryLines.Count; i++) - { - stationaryVertices.Add(stationaryLines[i].pt1); - stationaryVertices.Add(stationaryLines[i].pt2); - } - - var movingEdges = new (Vector start, Vector end)[movingLines.Count]; - for (int i = 0; i < movingLines.Count; i++) - movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2); - - if (opposite == PushDirection.Left || opposite == PushDirection.Right) - movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); - else - movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); - - foreach (var sv in stationaryVertices) - { - var d = OneWayDistance(sv, movingEdges, Vector.Zero, opposite); - if (d < minDist) minDist = d; - } - - return minDist; - } - - /// - /// Computes the minimum directional distance with the moving lines translated - /// by (movingDx, movingDy) without creating new Line objects. - /// - public static double DirectionalDistance( - List movingLines, double movingDx, double movingDy, - List stationaryLines, PushDirection direction) - { - var minDist = double.MaxValue; - var movingOffset = new Vector(movingDx, movingDy); - - // Case 1: Each moving vertex -> each stationary edge - var movingVertices = new HashSet(); - for (int i = 0; i < movingLines.Count; i++) - { - movingVertices.Add(movingLines[i].pt1 + movingOffset); - movingVertices.Add(movingLines[i].pt2 + movingOffset); - } - - var stationaryEdges = new (Vector start, Vector end)[stationaryLines.Count]; - for (int i = 0; i < stationaryLines.Count; i++) - stationaryEdges[i] = (stationaryLines[i].pt1, stationaryLines[i].pt2); - - if (direction == PushDirection.Left || direction == PushDirection.Right) - stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); - else - stationaryEdges = stationaryEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); - - foreach (var mv in movingVertices) - { - var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction); - if (d < minDist) minDist = d; - } - - // Case 2: Each stationary vertex -> each moving edge (opposite direction) - var opposite = OppositeDirection(direction); - var stationaryVertices = new HashSet(); - for (int i = 0; i < stationaryLines.Count; i++) - { - stationaryVertices.Add(stationaryLines[i].pt1); - stationaryVertices.Add(stationaryLines[i].pt2); - } - - var movingEdges = new (Vector start, Vector end)[movingLines.Count]; - for (int i = 0; i < movingLines.Count; i++) - movingEdges[i] = (movingLines[i].pt1, movingLines[i].pt2); - - if (opposite == PushDirection.Left || opposite == PushDirection.Right) - movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.Y, e.end.Y)).ToArray(); - else - movingEdges = movingEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); - - foreach (var sv in stationaryVertices) - { - var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); - if (d < minDist) minDist = d; - } - - return minDist; - } - - /// - /// Packs line segments into a flat double array [x1,y1,x2,y2, ...] for GPU transfer. - /// - public static double[] FlattenLines(List lines) - { - var result = new double[lines.Count * 4]; - for (int i = 0; i < lines.Count; i++) - { - var line = lines[i]; - result[i * 4] = line.pt1.X; - result[i * 4 + 1] = line.pt1.Y; - result[i * 4 + 2] = line.pt2.X; - result[i * 4 + 3] = line.pt2.Y; - } - return result; - } - - /// - /// Computes the minimum directional distance using raw edge arrays and location offsets - /// to avoid all intermediate object allocations. - /// - public static double DirectionalDistance( - (Vector start, Vector end)[] movingEdges, Vector movingOffset, - (Vector start, Vector end)[] stationaryEdges, Vector stationaryOffset, - PushDirection direction) - { - var minDist = double.MaxValue; - - // Extract unique vertices from moving edges. - var movingVertices = new HashSet(); - for (var i = 0; i < movingEdges.Length; i++) - { - movingVertices.Add(movingEdges[i].start + movingOffset); - movingVertices.Add(movingEdges[i].end + movingOffset); - } - - // Case 1: Each moving vertex -> each stationary edge - foreach (var mv in movingVertices) - { - var d = OneWayDistance(mv, stationaryEdges, stationaryOffset, direction); - if (d < minDist) minDist = d; - } - - // Case 2: Each stationary vertex -> each moving edge (opposite direction) - var opposite = OppositeDirection(direction); - var stationaryVertices = new HashSet(); - for (var i = 0; i < stationaryEdges.Length; i++) - { - stationaryVertices.Add(stationaryEdges[i].start + stationaryOffset); - stationaryVertices.Add(stationaryEdges[i].end + stationaryOffset); - } - - foreach (var sv in stationaryVertices) - { - var d = OneWayDistance(sv, movingEdges, movingOffset, opposite); - if (d < minDist) minDist = d; - } - - return minDist; - } - - public static double OneWayDistance( - Vector vertex, (Vector start, Vector end)[] edges, Vector edgeOffset, - PushDirection direction) - { - var minDist = double.MaxValue; - var vx = vertex.X; - var vy = vertex.Y; - - // Pruning: edges are sorted by their perpendicular min-coordinate in PartBoundary. - if (direction == PushDirection.Left || direction == PushDirection.Right) - { - for (var i = 0; i < edges.Length; i++) - { - var e1 = edges[i].start + edgeOffset; - var e2 = edges[i].end + edgeOffset; - - var minY = e1.Y < e2.Y ? e1.Y : e2.Y; - var maxY = e1.Y > e2.Y ? e1.Y : e2.Y; - - // Since edges are sorted by minY, if vy < minY, then vy < all subsequent minY. - if (vy < minY - Tolerance.Epsilon) - break; - - if (vy > maxY + Tolerance.Epsilon) - continue; - - var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); - if (d < minDist) minDist = d; - } - } - else // Up/Down - { - for (var i = 0; i < edges.Length; i++) - { - var e1 = edges[i].start + edgeOffset; - var e2 = edges[i].end + edgeOffset; - - var minX = e1.X < e2.X ? e1.X : e2.X; - var maxX = e1.X > e2.X ? e1.X : e2.X; - - // Since edges are sorted by minX, if vx < minX, then vx < all subsequent minX. - if (vx < minX - Tolerance.Epsilon) - break; - - if (vx > maxX + Tolerance.Epsilon) - continue; - - var d = RayEdgeDistance(vx, vy, e1.X, e1.Y, e2.X, e2.Y, direction); - if (d < minDist) minDist = d; - } - } - - return minDist; - } - - public static PushDirection OppositeDirection(PushDirection direction) - { - switch (direction) - { - case PushDirection.Left: return PushDirection.Right; - case PushDirection.Right: return PushDirection.Left; - case PushDirection.Up: return PushDirection.Down; - case PushDirection.Down: return PushDirection.Up; - default: return direction; - } - } - - public static bool IsHorizontalDirection(PushDirection direction) - { - return direction is PushDirection.Left or PushDirection.Right; - } - - public static double EdgeDistance(Box box, Box boundary, PushDirection direction) - { - switch (direction) - { - case PushDirection.Left: return box.Left - boundary.Left; - case PushDirection.Right: return boundary.Right - box.Right; - case PushDirection.Up: return boundary.Top - box.Top; - case PushDirection.Down: return box.Bottom - boundary.Bottom; - default: return double.MaxValue; - } - } - - public static Vector DirectionToOffset(PushDirection direction, double distance) - { - switch (direction) - { - case PushDirection.Left: return new Vector(-distance, 0); - case PushDirection.Right: return new Vector(distance, 0); - case PushDirection.Up: return new Vector(0, distance); - case PushDirection.Down: return new Vector(0, -distance); - default: return new Vector(); - } - } - - public static double DirectionalGap(Box from, Box to, PushDirection direction) - { - switch (direction) - { - case PushDirection.Left: return from.Left - to.Right; - case PushDirection.Right: return to.Left - from.Right; - case PushDirection.Up: return to.Bottom - from.Top; - case PushDirection.Down: return from.Bottom - to.Top; - default: return double.MaxValue; - } - } - - public static double ClosestDistanceLeft(Box box, List boxes) - { - var closestDistance = double.MaxValue; - - for (int i = 0; i < boxes.Count; i++) - { - var compareBox = boxes[i]; - - RelativePosition pos; - - if (!box.IsHorizontalTo(compareBox, out pos)) - continue; - - if (pos != RelativePosition.Right) - continue; - - var distance = box.Left - compareBox.Right; - - if (distance < closestDistance) - closestDistance = distance; - } - - return closestDistance == double.MaxValue ? double.NaN : closestDistance; - } - - public static double ClosestDistanceRight(Box box, List boxes) - { - var closestDistance = double.MaxValue; - - for (int i = 0; i < boxes.Count; i++) - { - var compareBox = boxes[i]; - - RelativePosition pos; - - if (!box.IsHorizontalTo(compareBox, out pos)) - continue; - - if (pos != RelativePosition.Left) - continue; - - var distance = compareBox.Left - box.Right; - - if (distance < closestDistance) - closestDistance = distance; - } - - return closestDistance == double.MaxValue ? double.NaN : closestDistance; - } - - public static double ClosestDistanceUp(Box box, List boxes) - { - var closestDistance = double.MaxValue; - - for (int i = 0; i < boxes.Count; i++) - { - var compareBox = boxes[i]; - - RelativePosition pos; - - if (!box.IsVerticalTo(compareBox, out pos)) - continue; - - if (pos != RelativePosition.Bottom) - continue; - - var distance = compareBox.Bottom - box.Top; - - if (distance < closestDistance) - closestDistance = distance; - } - - return closestDistance == double.MaxValue ? double.NaN : closestDistance; - } - - public static double ClosestDistanceDown(Box box, List boxes) - { - var closestDistance = double.MaxValue; - - for (int i = 0; i < boxes.Count; i++) - { - var compareBox = boxes[i]; - - RelativePosition pos; - - if (!box.IsVerticalTo(compareBox, out pos)) - continue; - - if (pos != RelativePosition.Top) - continue; - - var distance = box.Bottom - compareBox.Top; - - if (distance < closestDistance) - closestDistance = distance; - } - - return closestDistance == double.MaxValue ? double.NaN : closestDistance; - } - - public static Box GetLargestBoxVertically(Vector pt, Box bounds, IEnumerable boxes) - { - var verticalBoxes = boxes.Where(b => !(b.Left > pt.X || b.Right < pt.X)).ToList(); - - #region Find Top/Bottom Limits - - var top = double.MaxValue; - var btm = double.MinValue; - - foreach (var box in verticalBoxes) - { - var boxBtm = box.Bottom; - var boxTop = box.Top; - - if (boxBtm > pt.Y && boxBtm < top) - top = boxBtm; - - else if (box.Top < pt.Y && boxTop > btm) - btm = boxTop; - } - - if (top == double.MaxValue) - { - if (bounds.Top > pt.Y) - top = bounds.Top; - else return Box.Empty; - } - - if (btm == double.MinValue) - { - if (bounds.Bottom < pt.Y) - btm = bounds.Bottom; - else return Box.Empty; - } - - #endregion - - var horizontalBoxes = boxes.Where(b => !(b.Bottom >= top || b.Top <= btm)).ToList(); - - #region Find Left/Right Limits - - var lft = double.MinValue; - var rgt = double.MaxValue; - - foreach (var box in horizontalBoxes) - { - var boxLft = box.Left; - var boxRgt = box.Right; - - if (boxLft > pt.X && boxLft < rgt) - rgt = boxLft; - - else if (boxRgt < pt.X && boxRgt > lft) - lft = boxRgt; - } - - if (rgt == double.MaxValue) - { - if (bounds.Right > pt.X) - rgt = bounds.Right; - else return Box.Empty; - } - - if (lft == double.MinValue) - { - if (bounds.Left < pt.X) - lft = bounds.Left; - else return Box.Empty; - } - - #endregion - - return new Box(lft, btm, rgt - lft, top - btm); - } - - public static Box GetLargestBoxHorizontally(Vector pt, Box bounds, IEnumerable boxes) - { - var horizontalBoxes = boxes.Where(b => !(b.Bottom > pt.Y || b.Top < pt.Y)).ToList(); - - #region Find Left/Right Limits - - var lft = double.MinValue; - var rgt = double.MaxValue; - - foreach (var box in horizontalBoxes) - { - var boxLft = box.Left; - var boxRgt = box.Right; - - if (boxLft > pt.X && boxLft < rgt) - rgt = boxLft; - - else if (boxRgt < pt.X && boxRgt > lft) - lft = boxRgt; - } - - if (rgt == double.MaxValue) - { - if (bounds.Right > pt.X) - rgt = bounds.Right; - else return Box.Empty; - } - - if (lft == double.MinValue) - { - if (bounds.Left < pt.X) - lft = bounds.Left; - else return Box.Empty; - } - - #endregion - - var verticalBoxes = boxes.Where(b => !(b.Left >= rgt || b.Right <= lft)).ToList(); - - #region Find Top/Bottom Limits - - var top = double.MaxValue; - var btm = double.MinValue; - - foreach (var box in verticalBoxes) - { - var boxBtm = box.Bottom; - var boxTop = box.Top; - - if (boxBtm > pt.Y && boxBtm < top) - top = boxBtm; - - else if (box.Top < pt.Y && boxTop > btm) - btm = boxTop; - } - - if (top == double.MaxValue) - { - if (bounds.Top > pt.Y) - top = bounds.Top; - else return Box.Empty; - } - - if (btm == double.MinValue) - { - if (bounds.Bottom < pt.Y) - btm = bounds.Bottom; - else return Box.Empty; - } - - #endregion - - return new Box(lft, btm, rgt - lft, top - btm); - } } } diff --git a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs index 26a6db4..9308ec3 100644 --- a/OpenNest.Engine/BestFit/RotationSlideStrategy.cs +++ b/OpenNest.Engine/BestFit/RotationSlideStrategy.cs @@ -128,8 +128,8 @@ namespace OpenNest.Engine.BestFit if (_slideComputer != null) { - var stationarySegments = Helper.FlattenLines(part1Lines); - var movingSegments = Helper.FlattenLines(part2TemplateLines); + var stationarySegments = SpatialQuery.FlattenLines(part1Lines); + var movingSegments = SpatialQuery.FlattenLines(part2TemplateLines); var offsets = new double[count * 2]; var directions = new int[count]; @@ -182,7 +182,7 @@ namespace OpenNest.Engine.BestFit sEdges = sEdges.OrderBy(e => System.Math.Min(e.start.X, e.end.X)).ToArray(); stationaryEdgesByDir[dir] = sEdges; - var opposite = Helper.OppositeDirection(dir); + var opposite = SpatialQuery.OppositeDirection(dir); var mEdges = new (Vector start, Vector end)[part2TemplateLines.Count]; for (var i = 0; i < part2TemplateLines.Count; i++) mEdges[i] = (part2TemplateLines[i].StartPoint, part2TemplateLines[i].EndPoint); @@ -204,21 +204,21 @@ namespace OpenNest.Engine.BestFit var sEdges = stationaryEdgesByDir[dir]; var mEdges = movingEdgesByDir[dir]; - var opposite = Helper.OppositeDirection(dir); + var opposite = SpatialQuery.OppositeDirection(dir); var minDist = double.MaxValue; // Case 1: Moving vertices -> Stationary edges foreach (var mv in movingVerticesArray) { - var d = Helper.OneWayDistance(mv + movingOffset, sEdges, Vector.Zero, dir); + var d = SpatialQuery.OneWayDistance(mv + movingOffset, sEdges, Vector.Zero, dir); if (d < minDist) minDist = d; } // Case 2: Stationary vertices -> Moving edges (translated) foreach (var sv in stationaryVerticesArray) { - var d = Helper.OneWayDistance(sv, mEdges, movingOffset, opposite); + var d = SpatialQuery.OneWayDistance(sv, mEdges, movingOffset, opposite); if (d < minDist) minDist = d; } diff --git a/OpenNest.Engine/Compactor.cs b/OpenNest.Engine/Compactor.cs index 364fb26..f7bc551 100644 --- a/OpenNest.Engine/Compactor.cs +++ b/OpenNest.Engine/Compactor.cs @@ -87,9 +87,9 @@ namespace OpenNest for (var i = 0; i < obstacleParts.Count; i++) obstacleBoxes[i] = obstacleParts[i].BoundingBox; - var opposite = Helper.OppositeDirection(direction); + var opposite = SpatialQuery.OppositeDirection(direction); var halfSpacing = plate.PartSpacing / 2; - var isHorizontal = Helper.IsHorizontalDirection(direction); + var isHorizontal = SpatialQuery.IsHorizontalDirection(direction); var workArea = plate.WorkArea(); var distance = double.MaxValue; @@ -98,7 +98,7 @@ namespace OpenNest foreach (var moving in movingParts) { - var edgeDist = Helper.EdgeDistance(moving.BoundingBox, workArea, direction); + var edgeDist = SpatialQuery.EdgeDistance(moving.BoundingBox, workArea, direction); if (edgeDist <= 0) distance = 0; else if (edgeDist < distance) @@ -113,11 +113,11 @@ namespace OpenNest // 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 = Helper.DirectionalGap(movingBox, obstacleBoxes[i], opposite); + var reverseGap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], opposite); if (reverseGap > 0) continue; - var gap = Helper.DirectionalGap(movingBox, obstacleBoxes[i], direction); + var gap = SpatialQuery.DirectionalGap(movingBox, obstacleBoxes[i], direction); if (gap >= distance) continue; @@ -136,7 +136,7 @@ namespace OpenNest ? PartGeometry.GetOffsetPartLines(obstacleParts[i], halfSpacing, opposite, ChordTolerance) : PartGeometry.GetPartLines(obstacleParts[i], opposite, ChordTolerance); - var d = Helper.DirectionalDistance(movingLines, obstacleLines[i], direction); + var d = SpatialQuery.DirectionalDistance(movingLines, obstacleLines[i], direction); if (d < distance) distance = d; } @@ -144,7 +144,7 @@ namespace OpenNest if (distance < double.MaxValue && distance > 0) { - var offset = Helper.DirectionToOffset(direction, distance); + var offset = SpatialQuery.DirectionToOffset(direction, distance); foreach (var moving in movingParts) moving.Offset(offset); return distance; diff --git a/OpenNest.Engine/FillLinear.cs b/OpenNest.Engine/FillLinear.cs index 09c0ead..92e6415 100644 --- a/OpenNest.Engine/FillLinear.cs +++ b/OpenNest.Engine/FillLinear.cs @@ -82,9 +82,9 @@ namespace OpenNest var locationBOffset = MakeOffset(direction, bboxDim); // Use the most efficient array-based overload to avoid all allocations. - var slideDistance = Helper.DirectionalDistance( + var slideDistance = SpatialQuery.DirectionalDistance( boundary.GetEdges(pushDir), partA.Location + locationBOffset, - boundary.GetEdges(Helper.OppositeDirection(pushDir)), partA.Location, + boundary.GetEdges(SpatialQuery.OppositeDirection(pushDir)), partA.Location, pushDir); return ComputeCopyDistance(bboxDim, slideDistance); @@ -103,7 +103,7 @@ namespace OpenNest var bboxDim = GetDimension(patternA.BoundingBox, direction); var pushDir = GetPushDirection(direction); - var opposite = Helper.OppositeDirection(pushDir); + var opposite = SpatialQuery.OppositeDirection(pushDir); // Compute a starting offset large enough that every part-pair in // patternB has its offset geometry beyond patternA's offset geometry. @@ -143,7 +143,7 @@ namespace OpenNest for (var i = 0; i < patternA.Parts.Count; i++) { - var slideDistance = Helper.DirectionalDistance( + var slideDistance = SpatialQuery.DirectionalDistance( movingEdges[j], locationB, stationaryEdges[i], patternA.Parts[i].Location, pushDir); diff --git a/OpenNest/Actions/ActionClone.cs b/OpenNest/Actions/ActionClone.cs index 9abb583..793c6eb 100644 --- a/OpenNest/Actions/ActionClone.cs +++ b/OpenNest/Actions/ActionClone.cs @@ -186,8 +186,8 @@ namespace OpenNest.Actions boxes.Add(part.BoundingBox.Offset(plate.PartSpacing)); var pt = plateView.CurrentPoint; - var vertical = Helper.GetLargestBoxVertically(pt, bounds, boxes); - var horizontal = Helper.GetLargestBoxHorizontally(pt, bounds, boxes); + var vertical = SpatialQuery.GetLargestBoxVertically(pt, bounds, boxes); + var horizontal = SpatialQuery.GetLargestBoxHorizontally(pt, bounds, boxes); var bestArea = vertical; if (horizontal.Area() > vertical.Area()) diff --git a/OpenNest/Actions/ActionSelectArea.cs b/OpenNest/Actions/ActionSelectArea.cs index d65c46b..77b3ff3 100644 --- a/OpenNest/Actions/ActionSelectArea.cs +++ b/OpenNest/Actions/ActionSelectArea.cs @@ -150,8 +150,8 @@ namespace OpenNest.Actions private void UpdateSelectedArea() { SelectedArea = altSelect - ? Helper.GetLargestBoxHorizontally(plateView.CurrentPoint, Bounds, boxes) - : Helper.GetLargestBoxVertically(plateView.CurrentPoint, Bounds, boxes); + ? SpatialQuery.GetLargestBoxHorizontally(plateView.CurrentPoint, Bounds, boxes) + : SpatialQuery.GetLargestBoxVertically(plateView.CurrentPoint, Bounds, boxes); plateView.Invalidate(); }