Skip to content

Commit da8ee86

Browse files
Use Rust implementation of FindIntersection
1 parent 3fe5f2b commit da8ee86

File tree

3 files changed

+121
-84
lines changed

3 files changed

+121
-84
lines changed

src/PolygonClipper/PolygonUtilities.cs

Lines changed: 102 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Six Labors Split License.
33

44
using System;
5+
using System.Diagnostics.CodeAnalysis;
56
using System.Runtime.CompilerServices;
67

78
namespace PolygonClipper;
@@ -20,54 +21,41 @@ public static double SignedArea(Vertex p0, Vertex p1, Vertex p2)
2021
=> ((p0.X - p2.X) * (p1.Y - p2.Y)) - ((p1.X - p2.X) * (p0.Y - p2.Y));
2122

2223
/// <summary>
23-
/// Finds the intersection between two intervals [u0, u1] and [v0, v1].
24+
/// Finds the intersection of two line segments, constraining results to their intersection bounding box.
2425
/// </summary>
25-
/// <param name="u0">The start of the first interval.</param>
26-
/// <param name="u1">The end of the first interval.</param>
27-
/// <param name="v0">The start of the second interval.</param>
28-
/// <param name="v1">The end of the second interval.</param>
29-
/// <param name="start">
30-
/// The start of the intersection interval, or the single intersection point if there is only one.
31-
/// </param>
32-
/// <param name="end">
33-
/// The end of the intersection interval. If the intersection is a single point, this value is undefined.
34-
/// </param>
26+
/// <param name="seg0">The first segment.</param>
27+
/// <param name="seg1">The second segment.</param>
28+
/// <param name="pi0">The first intersection point.</param>
29+
/// <param name="pi1">The second intersection point (if overlap occurs).</param>
3530
/// <returns>
36-
/// An <see cref="int"/> indicating the type of intersection:
31+
/// An <see cref="int"/> indicating the number of intersection points:
3732
/// - Returns 0 if there is no intersection.
38-
/// - Returns 1 if the intersection is a single point.
39-
/// - Returns 2 if the intersection is an interval.
33+
/// - Returns 1 if the segments intersect at a single point.
34+
/// - Returns 2 if the segments overlap.
4035
/// </returns>
41-
public static int FindIntersection(double u0, double u1, double v0, double v1, out double start, out double end)
36+
public static int FindIntersection(Segment seg0, Segment seg1, out Vertex pi0, out Vertex pi1)
4237
{
43-
start = 0;
44-
end = 0;
38+
pi0 = default;
39+
pi1 = default;
4540

46-
if ((u1 < v0) || (u0 > v1))
41+
if (!TryGetIntersectionBoundingBox(seg0.Source, seg0.Target, seg1.Source, seg1.Target, out Box2? bbox))
4742
{
48-
return 0; // No intersection
43+
return 0;
4944
}
5045

51-
if (u1 > v0)
52-
{
53-
if (u0 < v1)
54-
{
55-
// There is an overlapping range
56-
start = (u0 < v0) ? v0 : u0;
57-
end = (u1 > v1) ? v1 : u1;
58-
return 2; // Two endpoints defining the intersection range
59-
}
60-
61-
// u0 == v1
62-
start = u0;
46+
int interResult = FindIntersectionImpl(seg0, seg1, out pi0, out pi1);
6347

64-
return 1; // Single point intersection
48+
if (interResult == 1)
49+
{
50+
pi0 = ConstrainToBoundingBox(pi0, bbox.Value);
51+
}
52+
else if (interResult == 2)
53+
{
54+
pi0 = ConstrainToBoundingBox(pi0, bbox.Value);
55+
pi1 = ConstrainToBoundingBox(pi1, bbox.Value);
6556
}
6657

67-
// u1 == v0
68-
start = u1;
69-
70-
return 1; // Single point intersection
58+
return interResult;
7159
}
7260

7361
/// <summary>
@@ -88,60 +76,58 @@ public static int FindIntersection(double u0, double u1, double v0, double v1, o
8876
/// - Returns 1 if the segments intersect at a single point.
8977
/// - Returns 2 if the segments overlap.
9078
/// </returns>
91-
public static int FindIntersection(Segment seg0, Segment seg1, out Vertex pi0, out Vertex pi1)
79+
private static int FindIntersectionImpl(Segment seg0, Segment seg1, out Vertex pi0, out Vertex pi1)
9280
{
9381
pi0 = default;
9482
pi1 = default;
9583

96-
Vertex p0 = seg0.Source;
97-
Vertex p1 = seg1.Source;
84+
Vertex a1 = seg0.Source;
85+
Vertex a2 = seg1.Source;
9886

99-
Vertex va = seg0.Target - p0;
100-
Vertex vb = seg1.Target - p1;
87+
Vertex va = seg0.Target - a1;
88+
Vertex vb = seg1.Target - a2;
89+
Vertex e = a2 - a1;
10190

102-
const double sqrEpsilon = 0.0000001; // Threshold for comparing-point precision
103-
Vertex e = p1 - p0;
10491
double kross = Vertex.Cross(va, vb);
10592
double sqrKross = kross * kross;
10693
double sqrLenA = Vertex.Dot(va, va);
107-
// double sqrLen1 = Vertex.Dot(vb, vb);
10894

10995
if (sqrKross > 0)
11096
{
11197
// Lines of the segments are not parallel
11298
double s = Vertex.Cross(e, vb) / kross;
113-
if (s is < 0 or > 1)
99+
if (s < 0 || s > 1)
114100
{
115101
return 0;
116102
}
117103

118104
double t = Vertex.Cross(e, va) / kross;
119-
if (t is < 0 or > 1)
105+
if (t < 0 || t > 1)
120106
{
121107
return 0;
122108
}
123109

110+
// If s or t is exactly 0 or 1, the intersection is on an endpoint
124111
if (s == 0 || s == 1)
125112
{
126-
// on an endpoint of line segment a
127-
pi0 = p0 + (s * va);
113+
// On an endpoint of line segment a
114+
pi0 = MidPoint(a1, s, va);
128115
return 1;
129116
}
130117

131118
if (t == 0 || t == 1)
132119
{
133-
// on an endpoint of line segment b
134-
pi0 = p1 + (t * vb);
120+
// On an endpoint of line segment b
121+
pi0 = MidPoint(a2, t, vb);
135122
return 1;
136123
}
137124

138125
// Intersection of lines is a point on each segment
139-
pi0 = p0 + (s * va);
126+
pi0 = a1 + (s * va);
140127
return 1;
141128
}
142129

143-
// Lines of the segments are parallel
144-
// double sqrLenE = (e.X * e.X) + (e.Y * e.Y);
130+
// Lines are parallel; check if they are collinear
145131
kross = Vertex.Cross(e, va);
146132
sqrKross = kross * kross;
147133
if (sqrKross > 0)
@@ -150,44 +136,82 @@ public static int FindIntersection(Segment seg0, Segment seg1, out Vertex pi0, o
150136
return 0;
151137
}
152138

153-
// Lines of the segments are the same. Need to test for overlap of segments.
154-
double s0 = Vertex.Dot(va, e) / sqrLenA; // so = Dot (D0, E) * sqrLen0
155-
double s1 = s0 + (Vertex.Dot(va, vb) / sqrLenA); // s1 = s0 + Dot (D0, D1) * sqrLen0
156-
double smin = Math.Min(s0, s1);
157-
double smax = Math.Max(s0, s1);
158-
int imax = FindIntersection(0F, 1F, smin, smax, out double w0, out double w1);
139+
// Segments are collinear, check for overlap
140+
double sa = Vertex.Dot(va, e) / sqrLenA;
141+
double sb = sa + (Vertex.Dot(va, vb) / sqrLenA);
142+
double smin = Math.Min(sa, sb);
143+
double smax = Math.Max(sa, sb);
159144

160-
if (imax > 0)
145+
if (smin <= 1 && smax >= 0)
161146
{
162-
pi0 = new Vertex(p0.X + (w0 * va.X), p0.Y + (w0 * va.Y));
163-
SnapToSegmentEndpoint(ref pi0, seg0);
164-
SnapToSegmentEndpoint(ref pi0, seg1);
165-
if (imax > 1)
147+
if (smin == 1)
166148
{
167-
pi1 = new Vertex(p0.X + (w1 * va.X), p0.Y + (w1 * va.Y));
149+
pi0 = MidPoint(a1, smin, va);
150+
return 1;
168151
}
152+
153+
if (smax == 0)
154+
{
155+
pi0 = MidPoint(a1, smax, va);
156+
return 1;
157+
}
158+
159+
pi0 = MidPoint(a1, Math.Max(smin, 0), va);
160+
pi1 = MidPoint(a1, Math.Min(smax, 1), va);
161+
return 2;
169162
}
170163

171-
return imax;
164+
return 0;
172165
}
173166

174167
/// <summary>
175-
/// Snaps the point to the nearest endpoint of the segment if it is very close.
168+
/// Computes the bounding box of the intersection area of two line segments.
176169
/// </summary>
177-
/// <param name="point">The point to snap.</param>
178-
/// <param name="segment">The segment to check.</param>
179-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
180-
private static void SnapToSegmentEndpoint(ref Vertex point, Segment segment)
170+
/// <param name="a1">The first point of the first segment.</param>
171+
/// <param name="a2">The second point of the first segment.</param>
172+
/// <param name="b1">The first point of the second segment.</param>
173+
/// <param name="b2">The second point of the second segment.</param>
174+
/// <param name="result">The intersection bounding box if one exists, otherwise null.</param>
175+
/// <returns>
176+
/// <see langword="true"/> if the segments intersect; otherwise, <see langword="false"/>.
177+
/// </returns>
178+
private static bool TryGetIntersectionBoundingBox(Vertex a1, Vertex a2, Vertex b1, Vertex b2, [NotNullWhen(true)] out Box2? result)
181179
{
182-
const double threshold = 0.00000001;
180+
Vertex minA = Vertex.Min(a1, a2);
181+
Vertex maxA = Vertex.Max(a1, a2);
182+
Vertex minB = Vertex.Min(b1, b2);
183+
Vertex maxB = Vertex.Max(b1, b2);
183184

184-
if (Vertex.Distance(point, segment.Source) < threshold)
185-
{
186-
point = segment.Source;
187-
}
188-
else if (Vertex.Distance(point, segment.Target) < threshold)
185+
Vertex interMin = Vertex.Max(minA, minB);
186+
Vertex interMax = Vertex.Min(maxA, maxB);
187+
188+
if (interMin.X <= interMax.X && interMin.Y <= interMax.Y)
189189
{
190-
point = segment.Target;
190+
result = new Box2(interMin, interMax);
191+
return true;
191192
}
193+
194+
result = null;
195+
return false;
192196
}
197+
198+
/// <summary>
199+
/// Constrains a point to the given bounding box.
200+
/// </summary>
201+
/// <param name="p">The point to constrain.</param>
202+
/// <param name="bbox">The bounding box.</param>
203+
/// <returns>The constrained point.</returns>
204+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
205+
private static Vertex ConstrainToBoundingBox(Vertex p, Box2 bbox)
206+
=> Vertex.Min(Vertex.Max(p, bbox.Min), bbox.Max);
207+
208+
/// <summary>
209+
/// Computes the point at a given fractional distance along a directed line segment.
210+
/// </summary>
211+
/// <param name="p">The starting vertex of the segment.</param>
212+
/// <param name="s">The scalar factor representing the fractional distance along the segment.</param>
213+
/// <param name="d">The direction vector of the segment.</param>
214+
/// <returns>The interpolated vertex at the given fractional distance.</returns>
215+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
216+
public static Vertex MidPoint(Vertex p, double s, Vertex d) => p + (s * d);
193217
}

tests/PolygonClipper.Benchmarks/ClippingLibraryComparison.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ public PathsD Clipper2()
4444
ClipperD clipper2 = new();
4545
clipper2.AddSubject(this.subject2);
4646
clipper2.AddClip(this.clipping2);
47-
bool x = clipper2.Execute(ClipType.Union, FillRule.EvenOdd, solution);
48-
if (!x)
49-
{
50-
throw new InvalidOperationException("Failed to clip polygons.");
51-
}
47+
clipper2.Execute(ClipType.Union, FillRule.Positive, solution);
5248
return solution;
5349
}
5450

tests/PolygonClipper.Tests/GenericTestCases.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,30 @@
99
using GeoJSON.Text.Geometry;
1010
using PolygonClipper.Tests.TestCases;
1111
using Xunit;
12-
1312
using GeoPolygon = GeoJSON.Text.Geometry.Polygon;
1413

1514
namespace PolygonClipper.Tests;
15+
1616
public class GenericTestCases
1717
{
1818
public static IEnumerable<object[]> GetTestCases()
1919
=> TestData.Generic.GetFileNames().Select(x => new object[] { x });
2020

21+
[Fact]
22+
public Polygon Profile()
23+
{
24+
// Use this test for profiling purposes. (Rider)
25+
FeatureCollection data = TestData.Generic.GetFeatureCollection("issue71.geojson");
26+
27+
IGeometryObject subjectGeometry = data.Features[0].Geometry;
28+
IGeometryObject clippingGeometry = data.Features[1].Geometry;
29+
30+
Polygon subject = ConvertToPolygon(subjectGeometry);
31+
Polygon clipping = ConvertToPolygon(clippingGeometry);
32+
33+
return PolygonClipper.Union(subject, clipping);
34+
}
35+
2136
[Theory]
2237
[MemberData(nameof(GetTestCases))]
2338
public void GenericTestCase(string testCaseFile)
@@ -71,6 +86,7 @@ private static Polygon ConvertToPolygon(IGeometryObject geometry)
7186
{
7287
contour.AddVertex(new Vertex(xy.Longitude, xy.Latitude));
7388
}
89+
7490
polygon.Push(contour);
7591
}
7692

@@ -89,6 +105,7 @@ private static Polygon ConvertToPolygon(IGeometryObject geometry)
89105
{
90106
contour.AddVertex(new Vertex(xy.Longitude, xy.Latitude));
91107
}
108+
92109
polygon.Push(contour);
93110
}
94111
}

0 commit comments

Comments
 (0)