diff --git a/OpenNest.Core/Geometry/Polygon.cs b/OpenNest.Core/Geometry/Polygon.cs index c03f209..e48bf24 100644 --- a/OpenNest.Core/Geometry/Polygon.cs +++ b/OpenNest.Core/Geometry/Polygon.cs @@ -493,13 +493,37 @@ namespace OpenNest.Geometry { var n = Vertices.Count - 1; + // Pre-calculate edge bounding boxes to speed up intersection checks. + var edgeBounds = new (double minX, double maxX, double minY, double maxY)[n]; for (var i = 0; i < n; i++) { + var v1 = Vertices[i]; + var v2 = Vertices[i + 1]; + edgeBounds[i] = ( + System.Math.Min(v1.X, v2.X) - Tolerance.Epsilon, + System.Math.Max(v1.X, v2.X) + Tolerance.Epsilon, + System.Math.Min(v1.Y, v2.Y) - Tolerance.Epsilon, + System.Math.Max(v1.Y, v2.Y) + Tolerance.Epsilon + ); + } + + for (var i = 0; i < n; i++) + { + var bi = edgeBounds[i]; for (var j = i + 2; j < n; j++) { if (i == 0 && j == n - 1) continue; + var bj = edgeBounds[j]; + + // Prune with bounding box check. + if (bi.maxX < bj.minX || bj.maxX < bi.minX || + bi.maxY < bj.minY || bj.maxY < bi.minY) + { + continue; + } + if (SegmentsIntersect(Vertices[i], Vertices[i + 1], Vertices[j], Vertices[j + 1], out pt)) { edgeI = i; diff --git a/OpenNest.Core/Geometry/Vector.cs b/OpenNest.Core/Geometry/Vector.cs index 64dae7f..22eccda 100644 --- a/OpenNest.Core/Geometry/Vector.cs +++ b/OpenNest.Core/Geometry/Vector.cs @@ -3,7 +3,7 @@ using OpenNest.Math; namespace OpenNest.Geometry { - public struct Vector + public struct Vector : IEquatable { public static readonly Vector Invalid = new Vector(double.NaN, double.NaN); public static readonly Vector Zero = new Vector(0, 0); @@ -17,6 +17,29 @@ namespace OpenNest.Geometry Y = y; } + public bool Equals(Vector other) + { + return X.IsEqualTo(other.X) && Y.IsEqualTo(other.Y); + } + + public override bool Equals(object obj) + { + return obj is Vector other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + // Use a simple but effective hash combine. + // We use a small epsilon-safe rounding if needed, but for uniqueness in HashSet + // during a single operation, raw bits or slightly rounded is usually fine. + // However, IsEqualTo uses Tolerance.Epsilon, so we should probably round to some precision. + // But typically for these geometric algorithms, exact matches (or very close) are what we want to prune. + return (X.GetHashCode() * 397) ^ Y.GetHashCode(); + } + } + public double DistanceTo(Vector pt) { var vx = pt.X - X; @@ -186,21 +209,6 @@ namespace OpenNest.Geometry return new Vector(X, Y); } - public override bool Equals(object obj) - { - if (!(obj is Vector)) - return false; - - var pt = (Vector)obj; - - return (X.IsEqualTo(pt.X)) && (Y.IsEqualTo(pt.Y)); - } - - public override int GetHashCode() - { - return base.GetHashCode(); - } - public override string ToString() { return string.Format("[Vector: X:{0}, Y:{1}]", X, Y); diff --git a/OpenNest.Core/Helper.cs b/OpenNest.Core/Helper.cs index c2c23af..ecea400 100644 --- a/OpenNest.Core/Helper.cs +++ b/OpenNest.Core/Helper.cs @@ -882,7 +882,7 @@ namespace OpenNest case PushDirection.Right: { var dy = p2y - p1y; - if (dy > -Tolerance.Epsilon && dy < Tolerance.Epsilon) + if (System.Math.Abs(dy) < Tolerance.Epsilon) return double.MaxValue; var t = (vy - p1y) / dy; @@ -891,6 +891,7 @@ namespace OpenNest 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; @@ -900,7 +901,7 @@ namespace OpenNest case PushDirection.Up: { var dx = p2x - p1x; - if (dx > -Tolerance.Epsilon && dx < Tolerance.Epsilon) + if (System.Math.Abs(dx) < Tolerance.Epsilon) return double.MaxValue; var t = (vx - p1x) / dx; @@ -909,6 +910,7 @@ namespace OpenNest 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; @@ -928,38 +930,52 @@ namespace OpenNest { var minDist = double.MaxValue; - // Case 1: Each moving vertex → each stationary edge + // Case 1: Each moving vertex -> each stationary edge + var movingVertices = new HashSet(); for (int i = 0; i < movingLines.Count; i++) { - var movingStart = movingLines[i].pt1; - var movingEnd = movingLines[i].pt2; - - for (int j = 0; j < stationaryLines.Count; j++) - { - var d = RayEdgeDistance(movingStart, stationaryLines[j], direction); - if (d < minDist) minDist = d; - - d = RayEdgeDistance(movingEnd, stationaryLines[j], direction); - if (d < minDist) minDist = d; - } + movingVertices.Add(movingLines[i].pt1); + movingVertices.Add(movingLines[i].pt2); } - // Case 2: Each stationary vertex → each moving edge (opposite direction) - var opposite = OppositeDirection(direction); + 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++) { - var stationaryStart = stationaryLines[i].pt1; - var stationaryEnd = stationaryLines[i].pt2; + stationaryVertices.Add(stationaryLines[i].pt1); + stationaryVertices.Add(stationaryLines[i].pt2); + } - for (int j = 0; j < movingLines.Count; j++) - { - var d = RayEdgeDistance(stationaryStart, movingLines[j], opposite); - if (d < minDist) minDist = d; + 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); - d = RayEdgeDistance(stationaryEnd, movingLines[j], opposite); - if (d < minDist) minDist = d; - } + 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; @@ -974,51 +990,53 @@ namespace OpenNest List stationaryLines, PushDirection direction) { var minDist = double.MaxValue; + var movingOffset = new Vector(movingDx, movingDy); - // Case 1: Each moving vertex → each stationary edge + // Case 1: Each moving vertex -> each stationary edge + var movingVertices = new HashSet(); for (int i = 0; i < movingLines.Count; i++) { - var ml = movingLines[i]; - var mx1 = ml.pt1.X + movingDx; - var my1 = ml.pt1.Y + movingDy; - var mx2 = ml.pt2.X + movingDx; - var my2 = ml.pt2.Y + movingDy; - - for (int j = 0; j < stationaryLines.Count; j++) - { - var se = stationaryLines[j]; - var d = RayEdgeDistance(mx1, my1, se.pt1.X, se.pt1.Y, se.pt2.X, se.pt2.Y, direction); - if (d < minDist) minDist = d; - - d = RayEdgeDistance(mx2, my2, se.pt1.X, se.pt1.Y, se.pt2.X, se.pt2.Y, direction); - if (d < minDist) minDist = d; - } + movingVertices.Add(movingLines[i].pt1 + movingOffset); + movingVertices.Add(movingLines[i].pt2 + movingOffset); } - // Case 2: Each stationary vertex → each moving edge (opposite direction) - var opposite = OppositeDirection(direction); + 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++) { - var sl = stationaryLines[i]; + stationaryVertices.Add(stationaryLines[i].pt1); + stationaryVertices.Add(stationaryLines[i].pt2); + } - for (int j = 0; j < movingLines.Count; j++) - { - var me = movingLines[j]; - var d = RayEdgeDistance( - sl.pt1.X, sl.pt1.Y, - me.pt1.X + movingDx, me.pt1.Y + movingDy, - me.pt2.X + movingDx, me.pt2.Y + movingDy, - opposite); - if (d < minDist) minDist = d; + 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); - d = RayEdgeDistance( - sl.pt2.X, sl.pt2.Y, - me.pt1.X + movingDx, me.pt1.Y + movingDy, - me.pt2.X + movingDx, me.pt2.Y + movingDy, - opposite); - if (d < minDist) minDist = d; - } + 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; @@ -1041,6 +1059,105 @@ namespace OpenNest 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; + } + + private 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) diff --git a/OpenNest.Engine/PartBoundary.cs b/OpenNest.Engine/PartBoundary.cs index 2545b93..44c14bc 100644 --- a/OpenNest.Engine/PartBoundary.cs +++ b/OpenNest.Engine/PartBoundary.cs @@ -93,10 +93,10 @@ namespace OpenNest } } - leftEdges = left.ToArray(); - rightEdges = right.ToArray(); - upEdges = up.ToArray(); - downEdges = down.ToArray(); + leftEdges = left.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray(); + rightEdges = right.OrderBy(e => System.Math.Min(e.Item1.Y, e.Item2.Y)).ToArray(); + upEdges = up.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray(); + downEdges = down.OrderBy(e => System.Math.Min(e.Item1.X, e.Item2.X)).ToArray(); } /// @@ -152,5 +152,14 @@ namespace OpenNest default: return _leftEdges; } } + + /// + /// Returns the pre-computed edge arrays for the given direction. + /// These are in part-local coordinates (no translation applied). + /// + public (Vector start, Vector end)[] GetEdges(PushDirection direction) + { + return GetDirectionalEdges(direction); + } } }