Skip to main content

geometry_strategy/
intersects.rs

1//! Per-CS strategy for the `intersects` set-relation algorithm.
2//!
3//! Mirrors `boost::geometry::intersects(g1, g2)` from
4//! `boost/geometry/algorithms/intersects.hpp` together with the
5//! per-pair dispatch tables it pulls in from
6//! `algorithms/detail/intersects/{interface,implementation}.hpp` and,
7//! transitively, the Cartesian segment-segment / point-on-segment
8//! kernels in `strategy/cartesian/`. Two geometries `intersects` iff
9//! they are *not* `disjoint` — Boost's interface header is literally
10//! `intersects(a, b) == !disjoint(a, b)`
11//! (`algorithms/detail/intersects/interface.hpp:64-78`). The Rust port
12//! flips that around: `intersects` is the primary kernel here and
13//! [`crate::disjoint`] is the negation, because every constructive
14//! per-pair test is naturally an "is there a shared point?" question.
15//!
16//! ## Coherence note
17//!
18//! Rust's trait system cannot prove a downstream type does not implement
19//! several geometry traits at once, so two open blankets on one strategy
20//! struct would collide (E0119). The port reproduces Boost's per-pair
21//! tag dispatch instead: one **per-ordered-pair strategy struct**
22//! ([`IxPointPoint`], [`IxLinestringPolygon`], …) carries a single
23//! concept-pair-bounded `IntersectsStrategy` impl — distinct `Self`, so
24//! no overlap — and the tag-keyed [`IntersectsPairStrategy`] picker
25//! routes `(A::Kind, B::Kind)` to the right struct. Because it keys on
26//! the tags, a concept-adapted foreign type resolves through the same
27//! path as the equivalent `geometry-model` value (see
28//! `specs/open-tag-dispatch/`). [`CartesianIntersects`] remains as a thin
29//! facade that routes through the picker, so `disjoint` / `is_simple` and
30//! the `Reversed` symmetry adapter keep resolving unchanged.
31//!
32//! ## Symmetry
33//!
34//! `intersects` is symmetric: `intersects(a, b) == intersects(b, a)`.
35//! Each pair appears in exactly one canonical direction here and the
36//! [`Reversed`] blanket at the bottom lifts every
37//! `IntersectsStrategy<A, B>` to an `IntersectsStrategy<B, A>` for
38//! free — mirroring `boost::geometry::reverse_dispatch` at
39//! `core/reverse_dispatch.hpp`.
40
41extern crate alloc;
42
43use geometry_coords::CoordinateScalar;
44use geometry_cs::{CartesianFamily, CoordinateSystem};
45use geometry_tag::{LinestringTag, PointTag, PolygonTag, SameAs, SegmentTag};
46use geometry_trait::{
47    Geometry, Linestring as LinestringTrait, Point as PointTrait, Polygon as PolygonTrait,
48    Ring as RingTrait, Segment as SegmentTrait, segment_end, segment_start,
49};
50
51use crate::within::{WithinPoly, WithinStrategy};
52
53/// A strategy for "do these two geometries share at least one point?".
54///
55/// Mirrors `boost::geometry::intersects(g1, g2)` from
56/// `boost/geometry/algorithms/intersects.hpp`. The Boost API takes the
57/// strategy implicitly through the algorithm's per-pair dispatch
58/// table; the Rust analogue collapses dispatch and strategy onto one
59/// trait keyed on the geometry pair.
60pub trait IntersectsStrategy<A, B> {
61    /// `true` iff `a` and `b` share at least one point.
62    fn intersects(&self, a: &A, b: &B) -> bool;
63}
64
65/// The Cartesian intersection facade — Boost's default for the Cartesian
66/// coordinate system.
67///
68/// Routes `(A, B)` through the tag-keyed [`IntersectsPairStrategy`] picker
69/// to the matching per-pair strategy struct. Kept as a single type so
70/// consumers that name it directly ([`crate::disjoint`],
71/// `is_simple`, [`Reversed`]) resolve unchanged; the per-pair *bodies*
72/// live on the [`IxPointPoint`] … structs below.
73#[derive(Debug, Default, Clone, Copy)]
74pub struct CartesianIntersects;
75
76impl<A, B> IntersectsStrategy<A, B> for CartesianIntersects
77where
78    A: Geometry,
79    B: Geometry,
80    A::Kind: IntersectsPairStrategy<B::Kind>,
81    <A::Kind as IntersectsPairStrategy<B::Kind>>::S: IntersectsStrategy<A, B>,
82{
83    #[inline]
84    fn intersects(&self, a: &A, b: &B) -> bool {
85        <<A::Kind as IntersectsPairStrategy<B::Kind>>::S as Default>::default().intersects(a, b)
86    }
87}
88
89/// `Reversed<S>` lifts an `IntersectsStrategy<A, B>` to an
90/// `IntersectsStrategy<B, A>` by swapping the arguments.
91///
92/// Mirrors `boost::geometry::reverse_dispatch`
93/// (`core/reverse_dispatch.hpp`) together with the partial
94/// specialisation that uses it in
95/// `algorithms/detail/intersects/interface.hpp`. Boost has one such
96/// specialisation per algorithm; in Rust the symmetry is expressed
97/// once with the blanket impl below — every concrete intersects pair
98/// gets the swap for free.
99///
100/// Distinct from [`crate::distance::Reversed`] only because Rust does
101/// not let a single newtype carry two unrelated blanket impls without
102/// running into orphan / coherence collisions; we deliberately keep
103/// the two algorithms' adapters in their own namespaces.
104#[derive(Debug, Default, Clone, Copy)]
105pub struct Reversed<S>(pub S);
106
107impl<A, B, S> IntersectsStrategy<B, A> for Reversed<S>
108where
109    S: IntersectsStrategy<A, B>,
110{
111    #[inline]
112    fn intersects(&self, b: &B, a: &A) -> bool {
113        self.0.intersects(a, b)
114    }
115}
116
117// ---- Per-pair strategy structs --------------------------------------
118//
119// Each struct carries a single concept-pair-bounded `IntersectsStrategy`
120// impl; distinct structs never overlap. Bodies are the existing kernels,
121// re-bound on the open concepts. The four `covered_by` cross-strategy
122// calls go through the open [`WithinPoly`] strategy (not the algorithm
123// free fn — that would be an upward crate dependency).
124
125/// Point × Point. See the [module docs](self).
126#[derive(Debug, Default, Clone, Copy)]
127pub struct IxPointPoint;
128/// Point × Segment. See the [module docs](self).
129#[derive(Debug, Default, Clone, Copy)]
130pub struct IxPointSegment;
131/// Segment × Segment. See the [module docs](self).
132#[derive(Debug, Default, Clone, Copy)]
133pub struct IxSegmentSegment;
134/// Linestring × Segment. See the [module docs](self).
135#[derive(Debug, Default, Clone, Copy)]
136pub struct IxLinestringSegment;
137/// Linestring × Linestring. See the [module docs](self).
138#[derive(Debug, Default, Clone, Copy)]
139pub struct IxLinestringLinestring;
140/// Point × Polygon. See the [module docs](self).
141#[derive(Debug, Default, Clone, Copy)]
142pub struct IxPointPolygon;
143/// Linestring × Polygon. See the [module docs](self).
144#[derive(Debug, Default, Clone, Copy)]
145pub struct IxLinestringPolygon;
146/// Polygon × Polygon. See the [module docs](self).
147#[derive(Debug, Default, Clone, Copy)]
148pub struct IxPolygonPolygon;
149/// Segment × Point (reverse of [`IxPointSegment`]). See the [module docs](self).
150#[derive(Debug, Default, Clone, Copy)]
151pub struct IxSegmentPoint;
152/// Segment × Linestring (reverse of [`IxLinestringSegment`]). See the
153/// [module docs](self).
154#[derive(Debug, Default, Clone, Copy)]
155pub struct IxSegmentLinestring;
156/// Polygon × Point (reverse of [`IxPointPolygon`]). See the [module docs](self).
157#[derive(Debug, Default, Clone, Copy)]
158pub struct IxPolygonPoint;
159/// Polygon × Linestring (reverse of [`IxLinestringPolygon`]). See the
160/// [module docs](self).
161#[derive(Debug, Default, Clone, Copy)]
162pub struct IxPolygonLinestring;
163
164// ---- Point × Point ---------------------------------------------------
165//
166// Two points "intersect" iff they are coordinate-wise equal. Mirrors
167// the pointlike/pointlike arm at
168// `algorithms/detail/disjoint/point_point.hpp:36-66`.
169
170impl<A, B> IntersectsStrategy<A, B> for IxPointPoint
171where
172    A: PointTrait,
173    B: PointTrait<Scalar = A::Scalar>,
174    <A::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
175{
176    #[inline]
177    fn intersects(&self, a: &A, b: &B) -> bool {
178        points_equal::<A, B>(a, b, A::DIM)
179    }
180}
181
182// ---- Point × Segment -------------------------------------------------
183//
184// A point lies on a segment iff it is collinear with the two
185// endpoints and inside the bounding box of those endpoints.
186
187impl<P, S> IntersectsStrategy<P, S> for IxPointSegment
188where
189    P: PointTrait,
190    S: SegmentTrait<Point = P>,
191    P: geometry_trait::PointMut + Default,
192    P::Scalar: CoordinateScalar,
193    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
194{
195    #[inline]
196    fn intersects(&self, p: &P, s: &S) -> bool {
197        point_on_segment(p, &segment_start(s), &segment_end(s))
198    }
199}
200
201// ---- Segment × Segment -----------------------------------------------
202
203impl<A, B, P> IntersectsStrategy<A, B> for IxSegmentSegment
204where
205    A: SegmentTrait<Point = P>,
206    B: SegmentTrait<Point = P>,
207    P: PointTrait + geometry_trait::PointMut + Default,
208    P::Scalar: CoordinateScalar,
209    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
210{
211    #[inline]
212    fn intersects(&self, a: &A, b: &B) -> bool {
213        segments_intersect(
214            &segment_start(a),
215            &segment_end(a),
216            &segment_start(b),
217            &segment_end(b),
218        )
219    }
220}
221
222// ---- Linestring × Segment --------------------------------------------
223
224impl<L, S, P> IntersectsStrategy<L, S> for IxLinestringSegment
225where
226    L: LinestringTrait<Point = P>,
227    S: SegmentTrait<Point = P>,
228    P: PointTrait + geometry_trait::PointMut + Default,
229    P::Scalar: CoordinateScalar,
230    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
231{
232    #[inline]
233    fn intersects(&self, ls: &L, s: &S) -> bool {
234        let s1 = segment_start(s);
235        let s2 = segment_end(s);
236        let mut it = ls.points();
237        let Some(mut prev) = it.next() else {
238            return false;
239        };
240        for curr in it {
241            if segments_intersect(prev, curr, &s1, &s2) {
242                return true;
243            }
244            prev = curr;
245        }
246        false
247    }
248}
249
250// ---- Linestring × Linestring -----------------------------------------
251
252impl<A, B, P> IntersectsStrategy<A, B> for IxLinestringLinestring
253where
254    A: LinestringTrait<Point = P>,
255    B: LinestringTrait<Point = P>,
256    P: PointTrait,
257    P::Scalar: CoordinateScalar,
258    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
259{
260    #[inline]
261    fn intersects(&self, a: &A, b: &B) -> bool {
262        let mut ia = a.points();
263        let Some(mut pa) = ia.next() else {
264            return false;
265        };
266        for qa in ia {
267            let mut ib = b.points();
268            let Some(mut pb) = ib.next() else {
269                return false;
270            };
271            for qb in ib {
272                if segments_intersect(pa, qa, pb, qb) {
273                    return true;
274                }
275                pb = qb;
276            }
277            pa = qa;
278        }
279        false
280    }
281}
282
283// ---- Point × Polygon -------------------------------------------------
284//
285// A point intersects a polygon iff it is covered_by (interior or
286// boundary). Goes through the open `WithinPoly` strategy.
287
288impl<P, G> IntersectsStrategy<P, G> for IxPointPolygon
289where
290    P: PointTrait,
291    G: PolygonTrait<Point = P>,
292    P::Scalar: CoordinateScalar,
293    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
294{
295    #[inline]
296    fn intersects(&self, p: &P, pg: &G) -> bool {
297        WithinPoly.covered_by(p, pg)
298    }
299}
300
301// ---- Linestring × Polygon --------------------------------------------
302
303impl<L, G, P> IntersectsStrategy<L, G> for IxLinestringPolygon
304where
305    L: LinestringTrait<Point = P>,
306    G: PolygonTrait<Point = P>,
307    P: PointTrait,
308    P::Scalar: CoordinateScalar,
309    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
310{
311    fn intersects(&self, ls: &L, pg: &G) -> bool {
312        // (a) Any vertex inside or on the polygon — fast path.
313        for v in ls.points() {
314            if WithinPoly.covered_by(v, pg) {
315                return true;
316            }
317        }
318        // (b) Any sub-segment crossing any ring sub-segment.
319        if linestring_crosses_ring(ls, pg.exterior()) {
320            return true;
321        }
322        for hole in pg.interiors() {
323            if linestring_crosses_ring(ls, hole) {
324                return true;
325            }
326        }
327        false
328    }
329}
330
331// ---- Polygon × Polygon -----------------------------------------------
332
333impl<A, B, P> IntersectsStrategy<A, B> for IxPolygonPolygon
334where
335    A: PolygonTrait<Point = P>,
336    B: PolygonTrait<Point = P>,
337    P: PointTrait,
338    P::Scalar: CoordinateScalar,
339    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
340{
341    fn intersects(&self, a: &A, b: &B) -> bool {
342        // Containment fast path: a vertex of A inside B, or vice versa.
343        if let Some(v) = a.exterior().points().next() {
344            if WithinPoly.covered_by(v, b) {
345                return true;
346            }
347        }
348        if let Some(v) = b.exterior().points().next() {
349            if WithinPoly.covered_by(v, a) {
350                return true;
351            }
352        }
353        // Any ring-edge crossing.
354        if rings_cross(a.exterior(), b.exterior()) {
355            return true;
356        }
357        for hole in a.interiors() {
358            if rings_cross(hole, b.exterior()) {
359                return true;
360            }
361        }
362        for hole in b.interiors() {
363            if rings_cross(a.exterior(), hole) {
364                return true;
365            }
366        }
367        false
368    }
369}
370
371// ---- Reverse pairs ---------------------------------------------------
372//
373// Each asymmetric pair gets its own struct that swaps and delegates to
374// the forward struct, so the per-pair picker can cover each ordered pair
375// directly.
376
377impl<S, P> IntersectsStrategy<S, P> for IxSegmentPoint
378where
379    S: SegmentTrait<Point = P>,
380    P: PointTrait + geometry_trait::PointMut + Default,
381    P::Scalar: CoordinateScalar,
382    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
383{
384    #[inline]
385    fn intersects(&self, s: &S, p: &P) -> bool {
386        IxPointSegment.intersects(p, s)
387    }
388}
389
390impl<S, L, P> IntersectsStrategy<S, L> for IxSegmentLinestring
391where
392    S: SegmentTrait<Point = P>,
393    L: LinestringTrait<Point = P>,
394    P: PointTrait + geometry_trait::PointMut + Default,
395    P::Scalar: CoordinateScalar,
396    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
397{
398    #[inline]
399    fn intersects(&self, s: &S, ls: &L) -> bool {
400        IxLinestringSegment.intersects(ls, s)
401    }
402}
403
404impl<G, P> IntersectsStrategy<G, P> for IxPolygonPoint
405where
406    G: PolygonTrait<Point = P>,
407    P: PointTrait,
408    P::Scalar: CoordinateScalar,
409    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
410{
411    #[inline]
412    fn intersects(&self, pg: &G, p: &P) -> bool {
413        IxPointPolygon.intersects(p, pg)
414    }
415}
416
417impl<G, L, P> IntersectsStrategy<G, L> for IxPolygonLinestring
418where
419    G: PolygonTrait<Point = P>,
420    L: LinestringTrait<Point = P>,
421    P: PointTrait,
422    P::Scalar: CoordinateScalar,
423    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
424{
425    #[inline]
426    fn intersects(&self, pg: &G, ls: &L) -> bool {
427        IxLinestringPolygon.intersects(ls, pg)
428    }
429}
430
431/// Type-level "which `IntersectsStrategy` struct does this ordered pair
432/// of geometry *kinds* use". A trait parameterised by the second tag
433/// `K2`, keyed on the first tag `Self` — disjoint on the pair, so no
434/// overlap. One entry per implemented ordered pair. The
435/// [`crate::intersects`] free function routes `(A::Kind, B::Kind)`
436/// through this trait.
437#[doc(hidden)]
438pub trait IntersectsPairStrategy<K2> {
439    /// The per-pair [`IntersectsStrategy`] struct this tag pair uses.
440    type S: Default;
441}
442
443impl IntersectsPairStrategy<PointTag> for PointTag {
444    type S = IxPointPoint;
445}
446impl IntersectsPairStrategy<SegmentTag> for PointTag {
447    type S = IxPointSegment;
448}
449impl IntersectsPairStrategy<SegmentTag> for SegmentTag {
450    type S = IxSegmentSegment;
451}
452impl IntersectsPairStrategy<SegmentTag> for LinestringTag {
453    type S = IxLinestringSegment;
454}
455impl IntersectsPairStrategy<LinestringTag> for LinestringTag {
456    type S = IxLinestringLinestring;
457}
458impl IntersectsPairStrategy<PolygonTag> for PointTag {
459    type S = IxPointPolygon;
460}
461impl IntersectsPairStrategy<PolygonTag> for LinestringTag {
462    type S = IxLinestringPolygon;
463}
464impl IntersectsPairStrategy<PolygonTag> for PolygonTag {
465    type S = IxPolygonPolygon;
466}
467impl IntersectsPairStrategy<PointTag> for SegmentTag {
468    type S = IxSegmentPoint;
469}
470impl IntersectsPairStrategy<LinestringTag> for SegmentTag {
471    type S = IxSegmentLinestring;
472}
473impl IntersectsPairStrategy<PointTag> for PolygonTag {
474    type S = IxPolygonPoint;
475}
476impl IntersectsPairStrategy<LinestringTag> for PolygonTag {
477    type S = IxPolygonLinestring;
478}
479
480// ---- Kernels ---------------------------------------------------------
481
482/// Coordinate-wise point equality across the first `dim` dimensions.
483#[inline]
484fn points_equal<A, B>(a: &A, b: &B, dim: usize) -> bool
485where
486    A: PointTrait,
487    B: PointTrait<Scalar = A::Scalar>,
488{
489    let mut i = 0;
490    while i < dim {
491        // const-generic indexed access — match on the small set we
492        // support (2 and 3 are the only dimensions used in practice).
493        let eq = match i {
494            0 => a.get::<0>() == b.get::<0>(),
495            1 => a.get::<1>() == b.get::<1>(),
496            2 => a.get::<2>() == b.get::<2>(),
497            _ => true,
498        };
499        if !eq {
500            return false;
501        }
502        i += 1;
503    }
504    true
505}
506
507/// Point-on-segment test in 2D. Mirrors the per-segment short-circuit
508/// in `cartesian_winding_base::apply` at
509/// `strategy/cartesian/point_in_poly_winding.hpp:91-131`: the point
510/// lies on `s1->s2` iff the side cross product is zero **and** the
511/// point's parameter along the segment lies in `[0, 1]`.
512fn point_on_segment<P>(p: &P, s1: &P, s2: &P) -> bool
513where
514    P: PointTrait,
515    P::Scalar: CoordinateScalar,
516{
517    let px = p.get::<0>();
518    let py = p.get::<1>();
519    let ax = s1.get::<0>();
520    let ay = s1.get::<1>();
521    let bx = s2.get::<0>();
522    let by = s2.get::<1>();
523    let cross = (bx - ax) * (py - ay) - (by - ay) * (px - ax);
524    if cross != P::Scalar::ZERO {
525        return false;
526    }
527    let (xlo, xhi) = if ax <= bx { (ax, bx) } else { (bx, ax) };
528    let (ylo, yhi) = if ay <= by { (ay, by) } else { (by, ay) };
529    xlo <= px && px <= xhi && ylo <= py && py <= yhi
530}
531
532/// 2D segment-segment intersection test. Returns `true` iff the two
533/// closed segments share at least one point — proper crossing,
534/// endpoint touch, or collinear overlap. Mirrors the boolean
535/// projection of `strategy/cartesian/intersection.hpp:139-260`.
536fn segments_intersect<P>(p1: &P, p2: &P, p3: &P, p4: &P) -> bool
537where
538    P: PointTrait,
539    P::Scalar: CoordinateScalar,
540{
541    let x1 = p1.get::<0>();
542    let y1 = p1.get::<1>();
543    let x2 = p2.get::<0>();
544    let y2 = p2.get::<1>();
545    let x3 = p3.get::<0>();
546    let y3 = p3.get::<1>();
547    let x4 = p4.get::<0>();
548    let y4 = p4.get::<1>();
549
550    let d1 = side_sign((x3, y3), (x4, y4), (x1, y1));
551    let d2 = side_sign((x3, y3), (x4, y4), (x2, y2));
552    let d3 = side_sign((x1, y1), (x2, y2), (x3, y3));
553    let d4 = side_sign((x1, y1), (x2, y2), (x4, y4));
554
555    // Proper crossing — the endpoints of each segment lie on
556    // opposite sides of the other segment's line.
557    if ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0)) {
558        return true;
559    }
560
561    // Endpoint-on-segment / collinear cases: a `side_sign` of zero
562    // means the third point lies on the other segment's line; we
563    // still have to check the bounding-box containment.
564    if d1 == 0 && point_on_segment(p1, p3, p4) {
565        return true;
566    }
567    if d2 == 0 && point_on_segment(p2, p3, p4) {
568        return true;
569    }
570    if d3 == 0 && point_on_segment(p3, p1, p2) {
571        return true;
572    }
573    if d4 == 0 && point_on_segment(p4, p1, p2) {
574        return true;
575    }
576    false
577}
578
579/// Sign of the side cross product `(b - a) × (c - a)`. `+1` left,
580/// `-1` right, `0` collinear. Mirrors `side_by_triangle::side_value`
581/// at `strategy/cartesian/side_by_triangle.hpp:178-200`.
582#[inline]
583fn side_sign<T: CoordinateScalar>(a: (T, T), b: (T, T), c: (T, T)) -> i32 {
584    let v = (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0);
585    if v > T::ZERO {
586        1
587    } else if v < T::ZERO {
588        -1
589    } else {
590        0
591    }
592}
593
594/// Does any sub-segment of `ls` cross any sub-segment of `r` (with
595/// `r`'s closing edge added explicitly if the ring is open)?
596fn linestring_crosses_ring<L, R, P>(ls: &L, r: &R) -> bool
597where
598    L: LinestringTrait<Point = P>,
599    R: RingTrait<Point = P>,
600    P: PointTrait,
601    P::Scalar: CoordinateScalar,
602{
603    let mut ils = ls.points();
604    let Some(mut pls) = ils.next() else {
605        return false;
606    };
607    for qls in ils {
608        if ring_edge_crosses_segment(r, pls, qls) {
609            return true;
610        }
611        pls = qls;
612    }
613    false
614}
615
616/// Does the segment `pls -> qls` cross any edge of the ring `r`?
617fn ring_edge_crosses_segment<R, P>(r: &R, pls: &P, qls: &P) -> bool
618where
619    R: RingTrait<Point = P>,
620    P: PointTrait,
621    P::Scalar: CoordinateScalar,
622{
623    let mut ir = r.points();
624    let Some(mut pr) = ir.next() else {
625        return false;
626    };
627    let first = pr;
628    for qr in ir {
629        if segments_intersect(pls, qls, pr, qr) {
630            return true;
631        }
632        pr = qr;
633    }
634    // Close an open ring explicitly. For a closed ring `pr == first`
635    // and the test is degenerate (zero-length edge) — `segments_intersect`
636    // returns `false` unless the linestring edge passes through the
637    // closing vertex, which is the desired behaviour.
638    segments_intersect(pls, qls, pr, first)
639}
640
641/// Does any edge of ring `a` cross any edge of ring `b`? Both rings
642/// are walked with an explicit closing edge appended for open rings;
643/// for closed rings the closing edge degenerates to zero length and
644/// contributes nothing.
645fn rings_cross<Ra, Rb, P>(a: &Ra, b: &Rb) -> bool
646where
647    Ra: RingTrait<Point = P>,
648    Rb: RingTrait<Point = P>,
649    P: PointTrait,
650    P::Scalar: CoordinateScalar,
651{
652    let edges_a = ring_edges(a);
653    let edges_b = ring_edges(b);
654    for (pa, qa) in &edges_a {
655        for (pb, qb) in &edges_b {
656            if segments_intersect(*pa, *qa, *pb, *qb) {
657                return true;
658            }
659        }
660    }
661    false
662}
663
664/// Materialise every edge of `r` as a `(prev, curr)` pair of point
665/// references. The closing edge is appended explicitly so callers do
666/// not need to special-case open vs. closed rings.
667fn ring_edges<R>(r: &R) -> alloc::vec::Vec<(&R::Point, &R::Point)>
668where
669    R: RingTrait,
670    R::Point: PointTrait,
671{
672    let pts: alloc::vec::Vec<&R::Point> = r.points().collect();
673    let mut out = alloc::vec::Vec::with_capacity(pts.len());
674    if pts.len() < 2 {
675        return out;
676    }
677    for w in pts.windows(2) {
678        out.push((w[0], w[1]));
679    }
680    out.push((*pts.last().unwrap(), *pts.first().unwrap()));
681    out
682}
683
684#[cfg(test)]
685mod tests {
686    //! Reference values from
687    //! `geometry/test/algorithms/intersects/intersects.cpp:38-79`.
688    //! Each test cites the C++ line it mirrors.
689
690    use super::{CartesianIntersects, IntersectsStrategy, Reversed};
691    use geometry_cs::Cartesian;
692    use geometry_model::{Point2D, Polygon, Segment, linestring, polygon};
693
694    type P = Point2D<f64, Cartesian>;
695
696    fn pt(x: f64, y: f64) -> P {
697        Point2D::new(x, y)
698    }
699
700    use geometry_model::Linestring;
701    type LS = Linestring<P>;
702
703    /// `intersects.cpp:38` — linestring crosses segment.
704    #[test]
705    fn ls_crosses_segment() {
706        let ls: LS = linestring![(1.0, 1.0), (3.0, 3.0), (2.0, 5.0)];
707        let s = Segment::new(pt(2.0, 0.0), pt(2.0, 6.0));
708        assert!(CartesianIntersects.intersects(&ls, &s));
709    }
710
711    /// `intersects.cpp:39` — linestring touches segment endpoint.
712    #[test]
713    fn ls_touches_segment_endpoint() {
714        let ls: LS = linestring![(1.0, 1.0), (3.0, 3.0)];
715        let s = Segment::new(pt(1.0, 0.0), pt(1.0, 1.0));
716        assert!(CartesianIntersects.intersects(&ls, &s));
717    }
718
719    /// `intersects.cpp:41` — linestring disjoint from segment.
720    #[test]
721    fn ls_disjoint_from_segment() {
722        let ls: LS = linestring![(1.0, 1.0), (3.0, 3.0)];
723        let s = Segment::new(pt(3.0, 0.0), pt(4.0, 1.0));
724        assert!(!CartesianIntersects.intersects(&ls, &s));
725    }
726
727    /// `intersects.cpp:50` — linestring crosses linestring.
728    #[test]
729    fn ls_crosses_ls() {
730        let a: LS = linestring![(0.0, 0.0), (2.0, 0.0), (3.0, 0.0)];
731        let b: LS = linestring![(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)];
732        assert!(CartesianIntersects.intersects(&a, &b));
733    }
734
735    /// `intersects.cpp:55` — collinear overlap.
736    #[test]
737    fn ls_overlap_collinear() {
738        let a: LS = linestring![(0.0, 0.0), (2.0, 0.0), (3.0, 0.0)];
739        let b: LS = linestring![(1.0, 0.0), (4.0, 0.0), (5.0, 0.0)];
740        assert!(CartesianIntersects.intersects(&a, &b));
741    }
742
743    /// `intersects.cpp:69` — linestring inside polygon.
744    #[test]
745    fn ls_inside_polygon() {
746        let ls: LS = linestring![(1.0, 1.0), (2.0, 2.0)];
747        let p: Polygon<P> = polygon![[
748            (0.0, 0.0),
749            (10.0, 0.0),
750            (10.0, 10.0),
751            (0.0, 10.0),
752            (0.0, 0.0)
753        ]];
754        assert!(CartesianIntersects.intersects(&ls, &p));
755    }
756
757    /// `intersects.cpp:71` — linestring outside polygon.
758    #[test]
759    fn ls_outside_polygon() {
760        let ls: LS = linestring![(11.0, 0.0), (12.0, 12.0)];
761        let p: Polygon<P> = polygon![[
762            (0.0, 0.0),
763            (10.0, 0.0),
764            (10.0, 10.0),
765            (0.0, 10.0),
766            (0.0, 0.0)
767        ]];
768        assert!(!CartesianIntersects.intersects(&ls, &p));
769    }
770
771    /// `Reversed<S>` swaps the arguments transparently.
772    #[test]
773    fn reversed_pair_compiles_and_agrees() {
774        let ls: LS = linestring![(1.0, 1.0), (2.0, 2.0)];
775        let p: Polygon<P> = polygon![[
776            (0.0, 0.0),
777            (10.0, 0.0),
778            (10.0, 10.0),
779            (0.0, 10.0),
780            (0.0, 0.0)
781        ]];
782        let forward = CartesianIntersects.intersects(&ls, &p);
783        let reversed = Reversed(CartesianIntersects).intersects(&p, &ls);
784        assert_eq!(forward, reversed);
785    }
786
787    // KC1.T2 witness: proves this strategy accepts read-only `Point`
788    // operands (that need not implement `PointMut`). If it compiles,
789    // the read-only bound is locked.
790    fn _accepts_readonly_point<A, B, S>(s: &S, a: &A, b: &B) -> bool
791    where
792        A: geometry_trait::Point,
793        B: geometry_trait::Point,
794        S: IntersectsStrategy<A, B>,
795    {
796        s.intersects(a, b)
797    }
798}