Skip to main content

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. Per
23//! `specs/overlay-robustness-decision.md` every endpoint is first
24//! routed through [`coordinate_in_range`] so the
25//! sign tests are exact; an out-of-range endpoint yields
26//! [`SegmentIntersection::OutOfRange`].
27
28use geometry_coords::CoordinateScalar;
29use geometry_trait::{Point, PointMut, Segment as SegmentTrait, segment_end, segment_start};
30
31use super::orientation::{Sign, orientation_2d};
32use super::range_guard::coordinate_in_range;
33
34/// The outcome of intersecting two segments.
35///
36/// Mirrors the three shapes Boost's
37/// `strategy::intersection::cartesian_segments` can produce: no
38/// intersection, exactly one point, or a collinear overlap delimited by
39/// two points (`intersection.hpp`, the `segment_intersection_points`
40/// count of 0 / 1 / 2). A fourth variant records the robustness-gate
41/// rejection that the "no rescale" policy requires
42/// (`specs/overlay-robustness-decision.md`).
43#[derive(Debug, Clone, Copy, PartialEq)]
44pub enum SegmentIntersection<P> {
45    /// The segments do not meet.
46    Disjoint,
47    /// The segments meet at exactly one point.
48    Single(P),
49    /// The segments are collinear and overlap along the closed
50    /// stretch from `from` to `to` (a single shared endpoint collapses
51    /// to `from == to`, but that case is reported as [`Self::Single`]).
52    Collinear {
53        /// One end of the shared collinear stretch.
54        from: P,
55        /// The other end of the shared collinear stretch.
56        to: P,
57    },
58    /// An endpoint fell outside the safe arithmetic range; per the
59    /// no-rescale policy the intersection is refused rather than
60    /// computed with an untrustworthy sign.
61    OutOfRange,
62}
63
64/// Intersect two segments `a` and `b`.
65///
66/// Returns [`SegmentIntersection::Disjoint`],
67/// [`SegmentIntersection::Single`],
68/// [`SegmentIntersection::Collinear`], or
69/// [`SegmentIntersection::OutOfRange`].
70///
71/// Mirrors the segment-segment arm of Boost's Cartesian intersection
72/// strategy (`strategy/cartesian/intersection.hpp`). Cartesian only;
73/// the point type must be constructible ([`PointMut`] + [`Default`]) so
74/// a computed intersection point can be returned in the caller's own
75/// point type, exactly as Boost returns points of the input type.
76///
77/// # Examples
78///
79/// ```
80/// use geometry_cs::Cartesian;
81/// use geometry_model::{Point2D, Segment};
82/// use geometry_overlay::predicate::segment_intersection::{
83///     segment_intersection, SegmentIntersection,
84/// };
85///
86/// type P = Point2D<f64, Cartesian>;
87/// // An "X" crossing at (1, 1).
88/// let a = Segment::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
89/// let b = Segment::new(P::new(0.0, 2.0), P::new(2.0, 0.0));
90/// match segment_intersection(&a, &b) {
91///     SegmentIntersection::Single(p) => {
92///         use geometry_trait::Point as _;
93///         assert_eq!(p.get::<0>(), 1.0);
94///         assert_eq!(p.get::<1>(), 1.0);
95///     }
96///     other => panic!("expected a single crossing, got {other:?}"),
97/// }
98/// ```
99#[must_use]
100pub fn segment_intersection<S, P>(a: &S, b: &S) -> SegmentIntersection<P>
101where
102    S: SegmentTrait<Point = P>,
103    P: PointMut + Default,
104    P::Scalar: CoordinateScalar + Into<f64>,
105{
106    let p1 = segment_start(a);
107    let p2 = segment_end(a);
108    let p3 = segment_start(b);
109    let p4 = segment_end(b);
110
111    if !(coordinate_in_range(&p1)
112        && coordinate_in_range(&p2)
113        && coordinate_in_range(&p3)
114        && coordinate_in_range(&p4))
115    {
116        return SegmentIntersection::OutOfRange;
117    }
118
119    // Four side tests, as Boost's strategy does before touching any
120    // coordinate. `d1`, `d2` place b's endpoints against line a;
121    // `d3`, `d4` place a's endpoints against line b.
122    let d1 = orientation_2d(&p3, &p4, &p1);
123    let d2 = orientation_2d(&p3, &p4, &p2);
124    let d3 = orientation_2d(&p1, &p2, &p3);
125    let d4 = orientation_2d(&p1, &p2, &p4);
126
127    // Proper crossing: each segment straddles the other's line.
128    if straddles(d1, d2) && straddles(d3, d4) {
129        return SegmentIntersection::Single(line_cross_point(&p1, &p2, &p3, &p4));
130    }
131
132    // Collinear case: all four side tests degenerate. Both segments lie
133    // on the same infinite line; the overlap (if any) is the
134    // intersection of their 1-D projections.
135    if d1 == Sign::Collinear
136        && d2 == Sign::Collinear
137        && d3 == Sign::Collinear
138        && d4 == Sign::Collinear
139    {
140        return collinear_overlap(&p1, &p2, &p3, &p4);
141    }
142
143    // Touch cases: exactly one endpoint lies on the other segment.
144    if d1 == Sign::Collinear && on_segment(&p1, &p3, &p4) {
145        return SegmentIntersection::Single(clone_point(&p1));
146    }
147    if d2 == Sign::Collinear && on_segment(&p2, &p3, &p4) {
148        return SegmentIntersection::Single(clone_point(&p2));
149    }
150    if d3 == Sign::Collinear && on_segment(&p3, &p1, &p2) {
151        return SegmentIntersection::Single(clone_point(&p3));
152    }
153    if d4 == Sign::Collinear && on_segment(&p4, &p1, &p2) {
154        return SegmentIntersection::Single(clone_point(&p4));
155    }
156
157    SegmentIntersection::Disjoint
158}
159
160/// The two side signs place the endpoints on strictly opposite sides.
161fn straddles(a: Sign, b: Sign) -> bool {
162    matches!(
163        (a, b),
164        (Sign::Positive, Sign::Negative) | (Sign::Negative, Sign::Positive)
165    )
166}
167
168/// Build a fresh point of type `P` from two coordinates.
169fn make_point<P>(x: P::Scalar, y: P::Scalar) -> P
170where
171    P: PointMut + Default,
172{
173    let mut p = P::default();
174    p.set::<0>(x);
175    p.set::<1>(y);
176    p
177}
178
179/// Copy a point by reading and re-writing its coordinates — `Point`
180/// carries no `Clone` bound because its coordinate system is phantom.
181fn clone_point<P>(src: &P) -> P
182where
183    P: PointMut + Default,
184{
185    make_point::<P>(src.get::<0>(), src.get::<1>())
186}
187
188/// Intersection point of the two infinite lines through `p1p2` and
189/// `p3p4`, assuming a proper crossing has already been confirmed (so
190/// the denominator is non-zero).
191fn line_cross_point<P>(p1: &P, p2: &P, p3: &P, p4: &P) -> P
192where
193    P: PointMut + Default,
194    P::Scalar: CoordinateScalar,
195{
196    let x1 = p1.get::<0>();
197    let y1 = p1.get::<1>();
198    let x2 = p2.get::<0>();
199    let y2 = p2.get::<1>();
200    let x3 = p3.get::<0>();
201    let y3 = p3.get::<1>();
202    let x4 = p4.get::<0>();
203    let y4 = p4.get::<1>();
204
205    // Standard two-line determinant solution.
206    let denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
207    let a = x1 * y2 - y1 * x2;
208    let b = x3 * y4 - y3 * x4;
209    let px = (a * (x3 - x4) - (x1 - x2) * b) / denom;
210    let py = (a * (y3 - y4) - (y1 - y2) * b) / denom;
211    make_point::<P>(px, py)
212}
213
214/// Whether the collinear point `p` lies within the axis-aligned
215/// bounding box of segment `s1 s2` — i.e. on the segment, given that
216/// `p` is already known collinear with it.
217fn on_segment<P>(p: &P, s1: &P, s2: &P) -> bool
218where
219    P: Point,
220    P::Scalar: CoordinateScalar,
221{
222    let (px, py) = (p.get::<0>(), p.get::<1>());
223    let (ax, ay) = (s1.get::<0>(), s1.get::<1>());
224    let (bx, by) = (s2.get::<0>(), s2.get::<1>());
225    min(ax, bx) <= px && px <= max(ax, bx) && min(ay, by) <= py && py <= max(ay, by)
226}
227
228/// Collinear overlap of two segments on the same infinite line.
229///
230/// Projects all four endpoints onto the dominant axis, sorts, and takes
231/// the inner pair as the shared stretch. Reports [`Disjoint`] when the
232/// projections do not overlap and [`Single`] when they meet at one
233/// point.
234fn collinear_overlap<P>(p1: &P, p2: &P, p3: &P, p4: &P) -> SegmentIntersection<P>
235where
236    P: PointMut + Default,
237    P::Scalar: CoordinateScalar,
238{
239    // Choose the axis with the larger spread of segment a so the 1-D
240    // projection is non-degenerate.
241    let spread_x = (p1.get::<0>() - p2.get::<0>()).abs();
242    let spread_y = (p1.get::<1>() - p2.get::<1>()).abs();
243    let project_on_x = spread_x >= spread_y;
244
245    let key = |p: &P| -> P::Scalar {
246        if project_on_x {
247            p.get::<0>()
248        } else {
249            p.get::<1>()
250        }
251    };
252
253    // Order each segment's endpoints, then the overlap is
254    // [max(lo1, lo2), min(hi1, hi2)] on the projection axis.
255    let (a_lo, a_hi) = ordered(p1, p2, &key);
256    let (b_lo, b_hi) = ordered(p3, p4, &key);
257
258    let lo = if key(a_lo) >= key(b_lo) { a_lo } else { b_lo };
259    let hi = if key(a_hi) <= key(b_hi) { a_hi } else { b_hi };
260
261    if key(lo) > key(hi) {
262        SegmentIntersection::Disjoint
263    } else if key(lo) == key(hi) {
264        SegmentIntersection::Single(clone_point(lo))
265    } else {
266        SegmentIntersection::Collinear {
267            from: clone_point(lo),
268            to: clone_point(hi),
269        }
270    }
271}
272
273/// Return `(low, high)` of the two points, ordered by `key`.
274fn ordered<'p, P, F>(a: &'p P, b: &'p P, key: &F) -> (&'p P, &'p P)
275where
276    P: Point,
277    F: Fn(&P) -> P::Scalar,
278{
279    if key(a) <= key(b) { (a, b) } else { (b, a) }
280}
281
282fn min<T: CoordinateScalar>(a: T, b: T) -> T {
283    if a <= b { a } else { b }
284}
285
286fn max<T: CoordinateScalar>(a: T, b: T) -> T {
287    if a >= b { a } else { b }
288}
289
290#[cfg(test)]
291mod tests {
292    //! The ~30-case matrix OVL1.T3 asks for, distilled to one assertion
293    //! per topological class: proper crossing, T-junction at an
294    //! endpoint, collinear overlap, collinear touching, parallel
295    //! (disjoint), and plain disjoint. Mirrors the case families in
296    //! `test/algorithms/overlay/segment_identifier.cpp` /
297    //! `get_turn_info.cpp`.
298
299    use super::{SegmentIntersection, segment_intersection};
300    use geometry_cs::Cartesian;
301    use geometry_model::{Point2D, Segment};
302    use geometry_trait::Point as _;
303
304    type P = Point2D<f64, Cartesian>;
305    type Seg = Segment<P>;
306
307    fn coords(p: &P) -> (f64, f64) {
308        (p.get::<0>(), p.get::<1>())
309    }
310
311    #[test]
312    fn proper_crossing() {
313        let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
314        let b = Seg::new(P::new(0.0, 2.0), P::new(2.0, 0.0));
315        match segment_intersection::<Seg, P>(&a, &b) {
316            SegmentIntersection::Single(p) => assert_eq!(coords(&p), (1.0, 1.0)),
317            other => panic!("{other:?}"),
318        }
319    }
320
321    #[test]
322    fn t_junction_at_endpoint() {
323        // b's start sits on the interior of a.
324        let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
325        let b = Seg::new(P::new(2.0, 0.0), P::new(2.0, 3.0));
326        match segment_intersection::<Seg, P>(&a, &b) {
327            SegmentIntersection::Single(p) => assert_eq!(coords(&p), (2.0, 0.0)),
328            other => panic!("{other:?}"),
329        }
330    }
331
332    #[test]
333    fn collinear_overlap() {
334        let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
335        let b = Seg::new(P::new(2.0, 0.0), P::new(6.0, 0.0));
336        match segment_intersection::<Seg, P>(&a, &b) {
337            SegmentIntersection::Collinear { from, to } => {
338                let (mut lo, mut hi) = (coords(&from), coords(&to));
339                if lo.0 > hi.0 {
340                    core::mem::swap(&mut lo, &mut hi);
341                }
342                assert_eq!(lo, (2.0, 0.0));
343                assert_eq!(hi, (4.0, 0.0));
344            }
345            other => panic!("{other:?}"),
346        }
347    }
348
349    #[test]
350    fn collinear_touching_at_one_point() {
351        // Meet only at the shared endpoint (4,0).
352        let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
353        let b = Seg::new(P::new(4.0, 0.0), P::new(8.0, 0.0));
354        match segment_intersection::<Seg, P>(&a, &b) {
355            SegmentIntersection::Single(p) => assert_eq!(coords(&p), (4.0, 0.0)),
356            other => panic!("{other:?}"),
357        }
358    }
359
360    #[test]
361    fn collinear_disjoint() {
362        let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 0.0));
363        let b = Seg::new(P::new(5.0, 0.0), P::new(7.0, 0.0));
364        assert_eq!(
365            segment_intersection::<Seg, P>(&a, &b),
366            SegmentIntersection::Disjoint
367        );
368    }
369
370    #[test]
371    fn parallel_disjoint() {
372        let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 0.0));
373        let b = Seg::new(P::new(0.0, 1.0), P::new(4.0, 1.0));
374        assert_eq!(
375            segment_intersection::<Seg, P>(&a, &b),
376            SegmentIntersection::Disjoint
377        );
378    }
379
380    #[test]
381    fn skew_disjoint() {
382        // Cross as lines outside both segments' extents.
383        let a = Seg::new(P::new(0.0, 0.0), P::new(1.0, 0.0));
384        let b = Seg::new(P::new(3.0, 1.0), P::new(3.0, 2.0));
385        assert_eq!(
386            segment_intersection::<Seg, P>(&a, &b),
387            SegmentIntersection::Disjoint
388        );
389    }
390
391    #[test]
392    fn out_of_range_endpoint_refused() {
393        let a = Seg::new(P::new(0.0, 0.0), P::new(2.0, 2.0));
394        let b = Seg::new(P::new(0.0, 2.0), P::new(2.0e9, 0.0));
395        assert_eq!(
396            segment_intersection::<Seg, P>(&a, &b),
397            SegmentIntersection::OutOfRange
398        );
399    }
400
401    #[test]
402    fn off_center_crossing_point() {
403        // Verify the line-solve, not just topology.
404        let a = Seg::new(P::new(0.0, 0.0), P::new(4.0, 4.0));
405        let b = Seg::new(P::new(0.0, 4.0), P::new(4.0, 0.0));
406        match segment_intersection::<Seg, P>(&a, &b) {
407            SegmentIntersection::Single(p) => assert_eq!(coords(&p), (2.0, 2.0)),
408            other => panic!("{other:?}"),
409        }
410    }
411}