perf(core): optimize geometry with edge pruning and vertex dedup
Vector implements IEquatable<Vector> with proper GetHashCode for HashSet usage. Polygon.FindCrossing uses bounding-box pruning to skip non-overlapping edge pairs. Helper.DirectionalDistance deduplicates vertices via HashSet, sorts edges for early-exit pruning, and adds a new array-based overload that avoids allocations. PartBoundary sorts directional edges and exposes GetEdges for zero-alloc access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -3,7 +3,7 @@ using OpenNest.Math;
|
||||
|
||||
namespace OpenNest.Geometry
|
||||
{
|
||||
public struct Vector
|
||||
public struct Vector : IEquatable<Vector>
|
||||
{
|
||||
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);
|
||||
|
||||
@@ -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<Vector>();
|
||||
for (int i = 0; i < movingLines.Count; i++)
|
||||
{
|
||||
var movingStart = movingLines[i].pt1;
|
||||
var movingEnd = movingLines[i].pt2;
|
||||
movingVertices.Add(movingLines[i].pt1);
|
||||
movingVertices.Add(movingLines[i].pt2);
|
||||
}
|
||||
|
||||
for (int j = 0; j < stationaryLines.Count; j++)
|
||||
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 = RayEdgeDistance(movingStart, stationaryLines[j], direction);
|
||||
if (d < minDist) minDist = d;
|
||||
|
||||
d = RayEdgeDistance(movingEnd, stationaryLines[j], direction);
|
||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Each stationary vertex → each moving edge (opposite direction)
|
||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||
var opposite = OppositeDirection(direction);
|
||||
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
var stationaryStart = stationaryLines[i].pt1;
|
||||
var stationaryEnd = stationaryLines[i].pt2;
|
||||
|
||||
for (int j = 0; j < movingLines.Count; j++)
|
||||
{
|
||||
var d = RayEdgeDistance(stationaryStart, movingLines[j], opposite);
|
||||
if (d < minDist) minDist = d;
|
||||
|
||||
d = RayEdgeDistance(stationaryEnd, movingLines[j], opposite);
|
||||
if (d < minDist) minDist = d;
|
||||
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;
|
||||
@@ -974,51 +990,53 @@ namespace OpenNest
|
||||
List<Line> 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<Vector>();
|
||||
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;
|
||||
movingVertices.Add(movingLines[i].pt1 + movingOffset);
|
||||
movingVertices.Add(movingLines[i].pt2 + movingOffset);
|
||||
}
|
||||
|
||||
for (int j = 0; j < stationaryLines.Count; j++)
|
||||
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 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);
|
||||
var d = OneWayDistance(mv, stationaryEdges, Vector.Zero, direction);
|
||||
if (d < minDist) minDist = d;
|
||||
}
|
||||
}
|
||||
|
||||
// Case 2: Each stationary vertex → each moving edge (opposite direction)
|
||||
// Case 2: Each stationary vertex -> each moving edge (opposite direction)
|
||||
var opposite = OppositeDirection(direction);
|
||||
|
||||
var stationaryVertices = new HashSet<Vector>();
|
||||
for (int i = 0; i < stationaryLines.Count; i++)
|
||||
{
|
||||
var sl = stationaryLines[i];
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
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;
|
||||
@@ -1041,6 +1059,105 @@ namespace OpenNest
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Computes the minimum directional distance using raw edge arrays and location offsets
|
||||
/// to avoid all intermediate object allocations.
|
||||
/// </summary>
|
||||
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<Vector>();
|
||||
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<Vector>();
|
||||
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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -152,5 +152,14 @@ namespace OpenNest
|
||||
default: return _leftEdges;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the pre-computed edge arrays for the given direction.
|
||||
/// These are in part-local coordinates (no translation applied).
|
||||
/// </summary>
|
||||
public (Vector start, Vector end)[] GetEdges(PushDirection direction)
|
||||
{
|
||||
return GetDirectionalEdges(direction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user