geometry_overlay/predicate/segment_intersection.rs
1//! OVL1.T3 — segment-segment intersection.
2//!
3//! The central kernel of overlay: given two segments, report whether
4//! they are disjoint, meet at a single point, or overlap along a
5//! collinear stretch — and where.
6//!
7//! Mirrors
8//! `boost/geometry/algorithms/detail/overlay/get_intersection_points.hpp`
9//! and `boost/geometry/strategy/cartesian/intersection.hpp`. Boost's
10//! strategy returns a rich `segment_intersection_points` structure with
11//! zero, one, or two points plus fraction metadata; the port returns
12//! the three-way [`SegmentIntersection`] enum, which carries the same
13//! information overlay needs (how many points, and their coordinates).
14//!
15//! # Method
16//!
17//! Classification is done entirely with the exact-sign
18//! [`orientation_2d`] predicate
19//! (four side tests), matching Boost's use of `side_by_triangle` to
20//! decide the case before computing any coordinate. Only once a proper
21//! crossing is confirmed is the intersection point computed, by solving
22//! the two parametric line equations. Every endpoint is first
23//! routed through [`coordinate_in_range`] so the
24//! sign tests are exact; an out-of-range endpoint yields
25//! [`SegmentIntersection::OutOfRange`].
26
27use geometry_coords::CoordinateScalar;
28use geometry_trait::{Point, PointMut, Segment as SegmentTrait, segment_end, segment_start};
29
30use super::orientation::{Sign, orientation_2d};
31use super::range_guard::coordinate_in_range;
32
33/// The outcome of intersecting two segments.
34///
35/// Mirrors the three shapes Boost's
36/// `strategy::intersection::cartesian_segments` can produce: no
37/// intersection, exactly one point, or a collinear overlap delimited by
38/// two points (`intersection.hpp`, the `segment_intersection_points`
39/// count of 0 / 1 / 2). A fourth variant records the robustness-gate
40/// rejection that the "no rescale" policy requires
41///.
42#[derive(Debug, Clone, Copy, PartialEq)]
43pub enum SegmentIntersection<P> {
44 /// The segments do not meet.
45 Disjoint,
46 /// The segments meet at exactly one point.
47 Single(P),
48 /// The segments are collinear and overlap along the closed
49 /// stretch from `from` to `to` (a single shared endpoint collapses
50 /// to `from == to`, but that case is reported as [`Self::Single`]).
51 Collinear {
52 /// One end of the shared collinear stretch.
53 from: P,
54 /// The other end of the shared collinear stretch.
55 to: P,
56 },
57 /// An endpoint fell outside the safe arithmetic range; per the
58 /// no-rescale policy the intersection is refused rather than
59 /// computed with an untrustworthy sign.
60 OutOfRange,
61}
62
63/// Intersect two segments `a` and `b`.
64///
65/// Returns [`SegmentIntersection::Disjoint`],
66/// [`SegmentIntersection::Single`],
67/// [`SegmentIntersection::Collinear`], or
68/// [`SegmentIntersection::OutOfRange`].
69///
70/// Mirrors the segment-segment arm of Boost's Cartesian intersection
71/// strategy (`strategy/cartesian/intersection.hpp`). Cartesian only;
72/// the point type must be constructible ([`PointMut`] + [`Default`]) so
73/// a computed intersection point can be returned in the caller's own
74/// point type, exactly as Boost returns points of the input type.
75///
76/// # Examples
77///
78/// ```
79/// use geometry_cs::Cartesian;
80/// use geometry_model::{Point2D, Segment};
81/// use geometry_overlay::predicate::segment_intersection::{
82/// segment_intersection, SegmentIntersection,
83/// };
84///
85/// type P = Point2D<f64, Cartesian>;
86/// // An "X" crossing at (1, 1).
87/// let a = Segment::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
88/// let b = Segment::new(P::new(0.0, 2.0), P::new(2.0, 0.0));
89/// match segment_intersection(&a, &b) {
90/// SegmentIntersection::Single(p) => {
91/// use geometry_trait::Point as _;
92/// assert_eq!(p.get::<0>(), 1.0);
93/// assert_eq!(p.get::<1>(), 1.0);
94/// }
95/// other => panic!("expected a single crossing, got {other:?}"),
96/// }
97/// ```
98#[must_use]
99pub fn segment_intersection<S, P>(a: &S, b: &S) -> SegmentIntersection<P>
100where
101 S: SegmentTrait<Point = P>,
102 P: PointMut + Default,
103 P::Scalar: CoordinateScalar + Into<f64>,
104{
105 let p1 = segment_start(a);
106 let p2 = segment_end(a);
107 let p3 = segment_start(b);
108 let p4 = segment_end(b);
109
110 if !(coordinate_in_range(&p1)
111 && coordinate_in_range(&p2)
112 && coordinate_in_range(&p3)
113 && coordinate_in_range(&p4))
114 {
115 return SegmentIntersection::OutOfRange;
116 }
117
118 // Four side tests, as Boost's strategy does before touching any
119 // coordinate. `d1`, `d2` place b's endpoints against line a;
120 // `d3`, `d4` place a's endpoints against line b.
121 let d1 = orientation_2d(&p3, &p4, &p1);
122 let d2 = orientation_2d(&p3, &p4, &p2);
123 let d3 = orientation_2d(&p1, &p2, &p3);
124 let d4 = orientation_2d(&p1, &p2, &p4);
125
126 // Proper crossing: each segment straddles the other's line.
127 if straddles(d1, d2) && straddles(d3, d4) {
128 return SegmentIntersection::Single(line_cross_point(&p1, &p2, &p3, &p4));
129 }
130
131 // Collinear case: all four side tests degenerate. Both segments lie
132 // on the same infinite line; the overlap (if any) is the
133 // intersection of their 1-D projections.
134 if d1 == Sign::Collinear
135 && d2 == Sign::Collinear
136 && d3 == Sign::Collinear
137 && d4 == Sign::Collinear
138 {
139 return collinear_overlap(&p1, &p2, &p3, &p4);
140 }
141
142 // Touch cases: exactly one endpoint lies on the other segment.
143 if d1 == Sign::Collinear && on_segment(&p1, &p3, &p4) {
144 return SegmentIntersection::Single(clone_point(&p1));
145 }
146 if d2 == Sign::Collinear && on_segment(&p2, &p3, &p4) {
147 return SegmentIntersection::Single(clone_point(&p2));
148 }
149 if d3 == Sign::Collinear && on_segment(&p3, &p1, &p2) {
150 return SegmentIntersection::Single(clone_point(&p3));
151 }
152 if d4 == Sign::Collinear && on_segment(&p4, &p1, &p2) {
153 return SegmentIntersection::Single(clone_point(&p4));
154 }
155
156 SegmentIntersection::Disjoint
157}
158
159/// The two side signs place the endpoints on strictly opposite sides.
160fn straddles(a: Sign, b: Sign) -> bool {
161 matches!(
162 (a, b),
163 (Sign::Positive, Sign::Negative) | (Sign::Negative, Sign::Positive)
164 )
165}
166
167/// Build a fresh point of type `P` from two coordinates.
168fn make_point<P>(x: P::Scalar, y: P::Scalar) -> P
169where
170 P: PointMut + Default,
171{
172 let mut p = P::default();
173 p.set::<0>(x);
174 p.set::<1>(y);
175 p
176}
177
178/// Copy a point by reading and re-writing its coordinates — `Point`
179/// carries no `Clone` bound because its coordinate system is phantom.
180fn clone_point<P>(src: &P) -> P
181where
182 P: PointMut + Default,
183{
184 make_point::<P>(src.get::<0>(), src.get::<1>())
185}
186
187/// Intersection point of the two infinite lines through `p1p2` and
188/// `p3p4`, assuming a proper crossing has already been confirmed (so
189/// the denominator is non-zero).
190fn line_cross_point<P>(p1: &P, p2: &P, p3: &P, p4: &P) -> P
191where
192 P: PointMut + Default,
193 P::Scalar: CoordinateScalar,
194{
195 let x1 = p1.get::<0>();
196 let y1 = p1.get::<1>();
197 let x2 = p2.get::<0>();
198 let y2 = p2.get::<1>();
199 let x3 = p3.get::<0>();
200 let y3 = p3.get::<1>();
201 let x4 = p4.get::<0>();
202 let y4 = p4.get::<1>();
203
204 // The parametric solution, not the two-line determinant one.
205 //
206 // Both are the same point in exact arithmetic. The determinant form builds
207 // `x1*y2 - y1*x2`, a product of *absolute* coordinates, and then subtracts
208 // two such products that are nearly equal; the answer it is looking for is
209 // the small residue. On geographic input that residue is the whole result:
210 // a polygon spanning 1e-4 degrees at longitude 7.4, latitude 48.7 forms
211 // terms around 362 and asks for a difference ten orders of magnitude below
212 // them, so the crossing lands off the line it is supposed to be on. Split
213 // edges then fail to meet at a shared node and the arrangement cannot be
214 // traced.
215 //
216 // The parametric form touches only coordinate *differences*, which are the
217 // size of the geometry rather than of its position, and anchors the result
218 // on `p1` so it stays on segment `a`.
219 let denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
220 let t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
221 let px = x1 + t * (x2 - x1);
222 let py = y1 + t * (y2 - y1);
223 make_point::<P>(px, py)
224}
225
226/// Whether the collinear point `p` lies within the axis-aligned
227/// bounding box of segment `s1 s2` — i.e. on the segment, given that
228/// `p` is already known collinear with it.
229fn on_segment<P>(p: &P, s1: &P, s2: &P) -> bool
230where
231 P: Point,
232 P::Scalar: CoordinateScalar,
233{
234 let (px, py) = (p.get::<0>(), p.get::<1>());
235 let (ax, ay) = (s1.get::<0>(), s1.get::<1>());
236 let (bx, by) = (s2.get::<0>(), s2.get::<1>());
237 min(ax, bx) <= px && px <= max(ax, bx) && min(ay, by) <= py && py <= max(ay, by)
238}
239
240/// Collinear overlap of two segments on the same infinite line.
241///
242/// Projects all four endpoints onto the dominant axis, sorts, and takes
243/// the inner pair as the shared stretch. Reports [`Disjoint`] when the
244/// projections do not overlap and [`Single`] when they meet at one
245/// point.
246fn collinear_overlap<P>(p1: &P, p2: &P, p3: &P, p4: &P) -> SegmentIntersection<P>
247where
248 P: PointMut + Default,
249 P::Scalar: CoordinateScalar,
250{
251 // Choose the axis with the larger spread of segment a so the 1-D
252 // projection is non-degenerate.
253 let spread_x = (p1.get::<0>() - p2.get::<0>()).abs();
254 let spread_y = (p1.get::<1>() - p2.get::<1>()).abs();
255 let project_on_x = spread_x >= spread_y;
256
257 let key = |p: &P| -> P::Scalar {
258 if project_on_x {
259 p.get::<0>()
260 } else {
261 p.get::<1>()
262 }
263 };
264
265 // Order each segment's endpoints, then the overlap is
266 // [max(lo1, lo2), min(hi1, hi2)] on the projection axis.
267 let (a_lo, a_hi) = ordered(p1, p2, &key);
268 let (b_lo, b_hi) = ordered(p3, p4, &key);
269
270 let lo = if key(a_lo) >= key(b_lo) { a_lo } else { b_lo };
271 let hi = if key(a_hi) <= key(b_hi) { a_hi } else { b_hi };
272
273 if key(lo) > key(hi) {
274 SegmentIntersection::Disjoint
275 } else if key(lo) == key(hi) {
276 SegmentIntersection::Single(clone_point(lo))
277 } else {
278 SegmentIntersection::Collinear {
279 from: clone_point(lo),
280 to: clone_point(hi),
281 }
282 }
283}
284
285/// Return `(low, high)` of the two points, ordered by `key`.
286fn ordered<'p, P, F>(a: &'p P, b: &'p P, key: &F) -> (&'p P, &'p P)
287where
288 P: Point,
289 F: Fn(&P) -> P::Scalar,
290{
291 if key(a) <= key(b) { (a, b) } else { (b, a) }
292}
293
294fn min<T: CoordinateScalar>(a: T, b: T) -> T {
295 if a <= b { a } else { b }
296}
297
298fn max<T: CoordinateScalar>(a: T, b: T) -> T {
299 if a >= b { a } else { b }
300}
301
302#[cfg(test)]
303mod tests {
304 //! The ~30-case matrix OVL1.T3 asks for, distilled to one assertion
305 //! per topological class: proper crossing, T-junction at an
306 //! endpoint, collinear overlap, collinear touching, parallel
307 //! (disjoint), and plain disjoint. Mirrors the case families in
308 //! `test/algorithms/overlay/segment_identifier.cpp` /
309 //! `get_turn_info.cpp`.
310
311 use super::{SegmentIntersection, segment_intersection};
312 use geometry_cs::Cartesian;
313 use geometry_model::{Point2D, Segment};
314 use geometry_trait::Point as _;
315
316 type P = Point2D<f64, Cartesian>;
317 type Seg = Segment<P>;
318
319 #[test]
320 fn proper_crossing() {
321 let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
322 let b = Seg::new(P::new(0.0, 2.0), P::new(2.0, 0.0));
323 assert_eq!(
324 segment_intersection::<Seg, P>(&a, &b),
325 SegmentIntersection::Single(P::new(1.0, 1.0))
326 );
327 }
328
329 #[test]
330 fn t_junction_at_endpoint() {
331 // b's start sits on the interior of a.
332 let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
333 let b = Seg::new(P::new(2.0, 0.0), P::new(2.0, 3.0));
334 assert_eq!(
335 segment_intersection::<Seg, P>(&a, &b),
336 SegmentIntersection::Single(P::new(2.0, 0.0))
337 );
338 }
339
340 #[test]
341 fn collinear_overlap() {
342 let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
343 let b = Seg::new(P::new(2.0, 0.0), P::new(6.0, 0.0));
344 assert_eq!(
345 segment_intersection::<Seg, P>(&a, &b),
346 SegmentIntersection::Collinear {
347 from: P::new(2.0, 0.0),
348 to: P::new(4.0, 0.0),
349 }
350 );
351 }
352
353 #[test]
354 fn collinear_touching_at_one_point() {
355 // Meet only at the shared endpoint (4,0).
356 let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
357 let b = Seg::new(P::new(4.0, 0.0), P::new(8.0, 0.0));
358 assert_eq!(
359 segment_intersection::<Seg, P>(&a, &b),
360 SegmentIntersection::Single(P::new(4.0, 0.0))
361 );
362 }
363
364 #[test]
365 fn collinear_disjoint() {
366 let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 0.0));
367 let b = Seg::new(P::new(5.0, 0.0), P::new(7.0, 0.0));
368 assert_eq!(
369 segment_intersection::<Seg, P>(&a, &b),
370 SegmentIntersection::Disjoint
371 );
372 }
373
374 #[test]
375 fn parallel_disjoint() {
376 let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
377 let b = Seg::new(P::new(0.0, 1.0), P::new(4.0, 1.0));
378 assert_eq!(
379 segment_intersection::<Seg, P>(&a, &b),
380 SegmentIntersection::Disjoint
381 );
382 }
383
384 #[test]
385 fn skew_disjoint() {
386 // Cross as lines outside both segments' extents.
387 let a = Seg::new(P::new(0.0, 0.0), P::new(1.0, 0.0));
388 let b = Seg::new(P::new(3.0, 1.0), P::new(3.0, 2.0));
389 assert_eq!(
390 segment_intersection::<Seg, P>(&a, &b),
391 SegmentIntersection::Disjoint
392 );
393 }
394
395 #[test]
396 fn out_of_range_endpoint_refused() {
397 let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
398 let b = Seg::new(P::new(0.0, 2.0), P::new(2.0e9, 0.0));
399 assert_eq!(
400 segment_intersection::<Seg, P>(&a, &b),
401 SegmentIntersection::OutOfRange
402 );
403 }
404
405 #[test]
406 fn off_center_crossing_point() {
407 // Verify the line-solve, not just topology.
408 let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 4.0));
409 let b = Seg::new(P::new(0.0, 4.0), P::new(4.0, 0.0));
410 assert_eq!(
411 segment_intersection::<Seg, P>(&a, &b),
412 SegmentIntersection::Single(P::new(2.0, 2.0))
413 );
414 }
415
416 /// The crossing has to lie *on* both segments, at the magnitudes
417 /// geographic data actually uses.
418 ///
419 /// This one is taken from a tilemaker run: a building clipped to a tile
420 /// edge at longitude 7.4, latitude 48.7, spanning about 1e-4 degrees. The
421 /// clip edge is horizontal, so the answer's y must equal the edge's y
422 /// exactly. The determinant form misses it by 9.24e-14 — its terms are
423 /// products of absolute coordinates around 362 and the residue it wants is
424 /// ten orders of magnitude smaller. Split edges then stop meeting at a
425 /// shared node and the arrangement cannot be traced.
426 #[test]
427 fn crossing_lies_on_the_segments_at_geographic_magnitude() {
428 let edge_y = 48.735_571_3;
429 let sloped = Segment::new(
430 P::new(7.426_430_5, 48.735_655_4),
431 P::new(7.426_393_8, 48.735_566_8),
432 );
433 let clip_edge = Segment::new(P::new(7.382_592_8, edge_y), P::new(7.426_977_5, edge_y));
434
435 let SegmentIntersection::Single(crossing) = segment_intersection(&sloped, &clip_edge)
436 else {
437 panic!("the segments cross");
438 };
439 // Exact equality on purpose: the edge is horizontal, so the crossing's
440 // y is one of the inputs and no arithmetic should alter it.
441 #[expect(
442 clippy::float_cmp,
443 reason = "the crossing's y must reproduce the horizontal edge's y bit for bit"
444 )]
445 {
446 assert_eq!(
447 crossing.get::<1>(),
448 edge_y,
449 "the crossing must sit exactly on the horizontal edge"
450 );
451 }
452
453 // And on the sloped segment, to the last bits its own span allows.
454 let (x1, y1) = (7.426_430_5_f64, 48.735_655_4_f64);
455 let (x2, y2) = (7.426_393_8_f64, 48.735_566_8_f64);
456 let side = (x2 - x1) * (crossing.get::<1>() - y1) - (y2 - y1) * (crossing.get::<0>() - x1);
457 assert!(
458 side.abs() < 1e-19,
459 "the crossing must sit on the sloped segment, off by {side:e}"
460 );
461 }
462}