Skip to content

Commit 2b01e40

Browse files
committed
fix: float2 edge-edge intersection robustness
Fixes a false positive result in the edge–edge intersection test for non-intersecting, nearly collinear edges (#384). This fix primarily addresses `float2` precision issues (when using `double2` the issue was not detected). These checks will be improved in the future with *robust-predicates* implementation.
1 parent 4c012ef commit 2b01e40

File tree

2 files changed

+63
-8
lines changed

2 files changed

+63
-8
lines changed

Runtime/Triangulator.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5689,19 +5689,27 @@ private static (T2, T) CalculateCircumCircle(int i, int j, int k, NativeArray<T2
56895689
var (pA, pB, pC) = (positions[i], positions[j], positions[k]);
56905690
return (utils.CircumCenter(pA, pB, pC), utils.Cast(CircumRadiusSq(pA, pB, pC)));
56915691
}
5692-
private static bool ccw(T2 a, T2 b, T2 c) => utils.greater(
5693-
utils.mul(utils.diff(utils.Y(c), utils.Y(a)), utils.diff(utils.X(b), utils.X(a))),
5694-
utils.mul(utils.diff(utils.Y(b), utils.Y(a)), utils.diff(utils.X(c), utils.X(a)))
5695-
);
5692+
private static int ccw(T2 a, T2 b, T2 c) =>
5693+
utils.greater(Orient2dFast(a, b, c), utils.EPSILON()) ? 1 :
5694+
utils.less(Orient2dFast(a, b, c), utils.neg(utils.EPSILON())) ? -1 :
5695+
0
5696+
;
56965697
/// <summary>
5697-
/// Returns <see langword="true"/> if edge (<paramref name="a0"/>, <paramref name="a1"/>) intersects
5698-
/// (<paramref name="b0"/>, <paramref name="b1"/>), <see langword="false"/> otherwise.
5698+
/// Returns <see langword="true"/> if edge (<paramref name="a"/>, <paramref name="b"/>) intersects
5699+
/// (<paramref name="c"/>, <paramref name="d"/>), <see langword="false"/> otherwise.
56995700
/// </summary>
57005701
/// <remarks>
57015702
/// This method will not catch intersecting collinear segments. See unit tests for more details.
57025703
/// Segments intersecting only at their endpoints may or may not return <see langword="true"/>, depending on their orientation.
57035704
/// </remarks>
5704-
internal static bool EdgeEdgeIntersection(T2 a0, T2 a1, T2 b0, T2 b1) => ccw(a0, a1, b0) != ccw(a0, a1, b1) && ccw(b0, b1, a0) != ccw(b0, b1, a1);
5705+
// NOTE:
5706+
// The commonly used edge–edge intersection check found in the literature
5707+
// may fail when using single-precision (float2) calculations:
5708+
// ccw(a, b, c) ≠ ccw(a, b, d) ∧ ccw(c, d, a) ≠ ccw(c, d, b)
5709+
// Since we do not care about intersecting–collinear cases in this check,
5710+
// the algorithm can be implemented using imul:
5711+
// ccw(a, b, c) · ccw(a, b, d) < 0 ∧ ccw(c, d, a) · ccw(c, d, b) < 0
5712+
internal static bool EdgeEdgeIntersection(T2 a, T2 b, T2 c, T2 d) => ccw(a, b, c) * ccw(a, b, d) < 0 && ccw(c, d, a) * ccw(c, d, b) < 0;
57055713
internal static bool IsConvexQuadrilateral(T2 a, T2 b, T2 c, T2 d) => true
57065714
&& utils.greater(utils.abs(Orient2dFast(a, c, b)), utils.EPSILON())
57075715
&& utils.greater(utils.abs(Orient2dFast(a, c, d)), utils.EPSILON())
@@ -5715,7 +5723,8 @@ private static TBig Orient2dFast(T2 a, T2 b, T2 c) => utils.diff(
57155723
);
57165724
internal static bool PointLineSegmentIntersection(T2 a, T2 b0, T2 b1) => true
57175725
&& utils.le(utils.abs(Orient2dFast(a, b0, b1)), utils.EPSILON())
5718-
&& math.all(utils.ge(a, utils.min(b0, b1)) & utils.le(a, utils.max(b0, b1)));
5726+
&& math.all(utils.ge(a, utils.min(b0, b1)) & utils.le(a, utils.max(b0, b1)))
5727+
;
57195728
}
57205729

57215730
/// <summary>
@@ -6155,6 +6164,7 @@ internal interface IUtils<T, T2, TBig> where T : unmanaged where T2 : unmanaged
61556164
T2 max(T2 v, T2 w);
61566165
T2 min(T2 v, T2 w);
61576166
TBig mul(T a, T b);
6167+
TBig neg(TBig v);
61586168
T2 neg(T2 v);
61596169
T2 normalizesafe(T2 v);
61606170
#pragma warning restore IDE1006
@@ -6253,6 +6263,7 @@ static float pseudoAngle(float dx, float dy)
62536263
public readonly float2 max(float2 v, float2 w) => math.max(v, w);
62546264
public readonly float2 min(float2 v, float2 w) => math.min(v, w);
62556265
public readonly float mul(float a, float b) => a * b;
6266+
public readonly float neg(float v) => -v;
62566267
public readonly float2 neg(float2 v) => -v;
62576268
public readonly float2 normalizesafe(float2 v) => math.normalizesafe(v);
62586269
}
@@ -6350,6 +6361,7 @@ static double pseudoAngle(double dx, double dy)
63506361
public readonly double2 max(double2 v, double2 w) => math.max(v, w);
63516362
public readonly double2 min(double2 v, double2 w) => math.min(v, w);
63526363
public readonly double mul(double a, double b) => a * b;
6364+
public readonly double neg(double v) => -v;
63536365
public readonly double2 neg(double2 v) => -v;
63546366
public readonly double2 normalizesafe(double2 v) => math.normalizesafe(v);
63556367
}
@@ -6455,6 +6467,7 @@ static double pseudoAngle(int dx, int dy)
64556467
public readonly int2 max(int2 v, int2 w) => math.max(v, w);
64566468
public readonly int2 min(int2 v, int2 w) => math.min(v, w);
64576469
public readonly long mul(int a, int b) => (long)a * b;
6470+
public readonly long neg(long v) => -v;
64586471
public readonly int2 neg(int2 v) => -v;
64596472
public readonly int2 normalizesafe(int2 v) => throw new NotImplementedException();
64606473
}
@@ -6553,6 +6566,7 @@ static fp pseudoAngle(fp dx, fp dy)
65536566
public readonly fp2 max(fp2 v, fp2 w) => fpmath.max(v, w);
65546567
public readonly fp2 min(fp2 v, fp2 w) => fpmath.min(v, w);
65556568
public readonly fp mul(fp a, fp b) => a * b;
6569+
public readonly fp neg(fp v) => -v;
65566570
public readonly fp2 neg(fp2 v) => -v;
65576571
public readonly fp2 normalizesafe(fp2 v) => fpmath.normalizesafe(v);
65586572
}

Tests/GithubReportedIssuesTests.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,5 +190,46 @@ public void GithubIssue134Test(Vector3[] positions, int[] triangles)
190190

191191
TestUtils.Draw(mesh.vertices, mesh.triangles, Color.red, duration: 5f);
192192
}
193+
194+
private static readonly TestCaseData[] githubIssue384Data =
195+
{
196+
new(math.int4(0, 1, 2, 3)),
197+
new(math.int4(0, 1, 3, 2)),
198+
new(math.int4(1, 0, 2, 3)),
199+
new(math.int4(1, 0, 3, 2)),
200+
};
201+
202+
[Test, TestCaseSource(nameof(githubIssue384Data))]
203+
public void GithubIssue384(int4 perm)
204+
{
205+
float2[] p =
206+
{
207+
new(7.95096207f, 5.45096207f),
208+
new(8.50216675f, 6.40567636f),
209+
new(9.00266838f, 7.27257061f),
210+
new(9.45096207f, 8.04903793f),
211+
};
212+
using var positions = new NativeArray<float2>(new[] {
213+
p[perm[0]],
214+
p[perm[1]],
215+
p[perm[2]],
216+
p[perm[3]],
217+
new(p[0].x, p[3].y),
218+
new(p[3].x, p[0].y),
219+
}, Allocator.Persistent);
220+
using var constraints = new NativeArray<int>(new[] { 0, 1, 2, 3 }, Allocator.Persistent);
221+
using var t = new Triangulator<float2>(Allocator.Persistent)
222+
{
223+
Input = { Positions = positions, ConstraintEdges = constraints },
224+
Settings = { ValidateInput = true },
225+
};
226+
t.Run();
227+
228+
t.Draw();
229+
Debug.DrawLine(math.float3(p[0], 0), math.float3(p[1], 0), Color.blue, 5f);
230+
Debug.DrawLine(math.float3(p[2], 0), math.float3(p[3], 0), Color.blue, 5f);
231+
232+
Assert.That(t.Output.Status.Value, Is.EqualTo(Status.OK));
233+
}
193234
}
194235
}

0 commit comments

Comments
 (0)