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:
2026-03-14 22:51:44 -04:00
parent eddcc7602d
commit 6993d169e4
4 changed files with 238 additions and 80 deletions

View File

@@ -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;

View File

@@ -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);