Skip to main content

geometry_strategy/
within.rs

1//! Per-CS strategy for point-in-polygon containment (`within` /
2//! `covered_by`).
3//!
4//! Mirrors the pieces of Boost.Geometry that collaborate to make
5//! `boost::geometry::within(p, g)` / `boost::geometry::covered_by(p, g)`
6//! work for any (point, polygonal | box) pair in any coordinate system:
7//!
8//! * `boost/geometry/strategies/within.hpp` — the per-CS
9//!   `within`-strategy concept (apply/result two-phase),
10//! * `boost/geometry/strategies/covered_by.hpp` — same concept reused,
11//! * `boost/geometry/strategies/cartesian/point_in_poly_winding.hpp` —
12//!   `cartesian_winding`, the default Cartesian PIP, implementing the
13//!   classic winding-number algorithm with on-segment detection,
14//! * `boost/geometry/strategies/cartesian/point_in_box.hpp` —
15//!   `cartesian_point_in_box`, the per-corner strict / non-strict
16//!   comparisons used to fold "is the point inside this axis-aligned
17//!   box" into the dispatch.
18//!
19//! The Boost concept exposes a stateful three-step API — construct a
20//! `state_type`, call `apply(point, s1, s2, state)` for every segment,
21//! then call `result(state)` to read off `-1` / `0` / `+1` (outside /
22//! boundary / interior). The Rust analogue collapses that three-step
23//! shape into a single `within` / `covered_by` pair on
24//! [`WithinStrategy`] because the per-segment walk is identical for
25//! every CS — only the per-segment kernel changes.
26//!
27//! ## Coherence note
28//!
29//! Boost dispatches on the geometry's tag via partial template
30//! specialisation — `dispatch::within<Point, Ring, _, ring_tag>` and
31//! `dispatch::within<Point, Polygon, _, polygon_tag>` are mutually
32//! exclusive because the C++ side can prove tags distinct. Rust's
33//! trait system cannot prove a downstream type does not implement
34//! several geometry traits at once, so two open blankets on one strategy
35//! struct would collide (E0119). The port reproduces Boost's tag
36//! dispatch instead: one **per-kind strategy struct** ([`WithinBox`],
37//! [`WithinRing`], [`WithinPoly`]) carries a single concept-bounded
38//! `WithinStrategy` impl — distinct `Self`, so no overlap — and the
39//! tag-keyed [`WithinStrategyForKind`] picker routes `G::Kind` to the
40//! right struct. Because the picker keys on the tag, any concept-adapted
41//! foreign type resolves through the same path as the equivalent
42//! `geometry-model` value.
43//!
44//! [`crate::intersects`] reaches point-in-polygon containment through
45//! the open [`WithinPoly`] strategy directly (not the algorithm-layer
46//! `covered_by` free fn — that would be an upward crate dependency /
47//! cycle), so both crates share the one open kernel.
48//!
49//! ## Result-code convention
50//!
51//! Mirrors Boost's `cartesian_winding::result` at
52//! `strategy/cartesian/point_in_poly_winding.hpp:69-74`:
53//!
54//! | Boost code | Meaning            | `within` | `covered_by` |
55//! |-----------:|--------------------|---------:|-------------:|
56//! |       `-1` | outside            |  `false` |      `false` |
57//! |        `0` | on the boundary    |  `false` |       `true` |
58//! |       `+1` | strict interior    |   `true` |       `true` |
59//!
60//! ## Precision limit (Cartesian, `f64`)
61//!
62//! The winding kernel decides each point's side of a segment from the
63//! sign of a cross product of coordinate *differences*. For `f64` that
64//! sign is exact only while the operands stay within the mantissa: past
65//! roughly `±2^26` (`67_108_864`) the products no longer fit in 53 bits
66//! and the sign can flip, so a strict-interior / boundary / exterior
67//! classification may be wrong for coordinates beyond that magnitude.
68//! This is the same limit the overlay engine gates on with its
69//! `SAFE_ABS_MAX` range guard, and it matches Boost: the non-rescaled
70//! `cartesian_winding` shares the bound (Boost's rescaling only ever
71//! applied at the overlay/turn layer, not here). `within` does **not**
72//! reject out-of-range input — callers that work with coordinates beyond
73//! `±2^26` must scale down first.
74
75use geometry_coords::CoordinateScalar;
76use geometry_cs::{CartesianFamily, CoordinateSystem};
77use geometry_tag::{BoxTag, PolygonTag, RingTag, SameAs};
78use geometry_trait::{
79    Box as BoxTrait, Point as PointTrait, PointMut, Polygon as PolygonTrait, Ring as RingTrait,
80    corner, fold_dims, ordinate,
81};
82
83/// A strategy for point-in-geometry containment.
84///
85/// Mirrors the per-CS `within` strategy concept declared in
86/// `boost/geometry/strategies/within.hpp` and refined per coordinate
87/// system in `strategies/cartesian/point_in_poly_winding.hpp` /
88/// `strategies/spherical/point_in_poly_winding.hpp`. The Boost concept
89/// exposes a stateful `apply(point, s1, s2, state)` accumulator plus a
90/// final `result(state)` reduction; the Rust analogue collapses the
91/// two phases into a single `within` / `covered_by` pair keyed on the
92/// geometry type, because the per-segment walk shape is identical for
93/// every CS — only the per-segment kernel changes.
94pub trait WithinStrategy<P: PointTrait, G> {
95    /// `true` iff `p` lies in the strict interior of `g`.
96    ///
97    /// Mirrors `boost::geometry::within(p, g, strategy)` from
98    /// `boost/geometry/algorithms/within.hpp` resolved through
99    /// `cartesian_winding::result == 1` at
100    /// `strategy/cartesian/point_in_poly_winding.hpp:69-74`.
101    fn within(&self, p: &P, g: &G) -> bool;
102
103    /// `true` iff `p` lies in the strict interior **or** on the
104    /// boundary of `g`.
105    ///
106    /// Mirrors `boost::geometry::covered_by(p, g, strategy)` from
107    /// `boost/geometry/algorithms/covered_by.hpp` resolved through
108    /// `cartesian_winding::result >= 0` at the same lines.
109    fn covered_by(&self, p: &P, g: &G) -> bool;
110}
111
112// =====================================================================
113// Per-kind strategy structs + tag-keyed picker
114// =====================================================================
115//
116// Each struct carries the kernel for one kind, bound on the *open*
117// concept (`G: Box`/`Ring`/`Polygon`) so any adapted foreign type
118// resolves. Distinct `Self` per kind ⇒ no overlap.
119//
120// * Box     — `strategy::within::cartesian_point_in_box::apply`
121//             (`strategy/cartesian/point_in_box.hpp:55-93`).
122// * Ring    — `cartesian_winding_base::apply`
123//             (`strategy/cartesian/point_in_poly_winding.hpp:91-131`).
124// * Polygon — `detail::within::point_in_polygon::apply`
125//             (`algorithms/detail/within/point_in_geometry.hpp:200-244`):
126//             within the exterior and not covered_by any hole.
127
128/// Open point-in-box strategy. See the [module docs](self).
129#[derive(Debug, Default, Clone, Copy)]
130pub struct WithinBox;
131/// Open point-in-ring (winding number) strategy. See the [module docs](self).
132#[derive(Debug, Default, Clone, Copy)]
133pub struct WithinRing;
134/// Open point-in-polygon (winding number, hole-aware) strategy. See the
135/// [module docs](self).
136#[derive(Debug, Default, Clone, Copy)]
137pub struct WithinPoly;
138
139impl<P, G> WithinStrategy<P, G> for WithinBox
140where
141    G: BoxTrait<Point = P>,
142    P: PointMut,
143    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
144{
145    #[inline]
146    fn within(&self, p: &P, b: &G) -> bool {
147        fold_dims(true, p, |inside, p, d| {
148            inside && box_dimension_contains(p, b, d, true)
149        })
150    }
151
152    #[inline]
153    fn covered_by(&self, p: &P, b: &G) -> bool {
154        fold_dims(true, p, |inside, p, d| {
155            inside && box_dimension_contains(p, b, d, false)
156        })
157    }
158}
159
160/// Does `p`'s ordinate `d` lie inside the box's `[min, max]` on that
161/// axis — strictly (`within`) or inclusively (`covered_by`)? One arm per
162/// dimension up to `MAX_DIM`, the per-dimension loop of
163/// `strategy/cartesian/point_in_box.hpp:55-93`.
164#[inline]
165fn box_dimension_contains<P, G>(p: &P, b: &G, d: usize, strict: bool) -> bool
166where
167    G: BoxTrait<Point = P>,
168    P: PointMut,
169{
170    let (min, max) = match d {
171        0 => (
172            b.get_indexed::<{ corner::MIN }, 0>(),
173            b.get_indexed::<{ corner::MAX }, 0>(),
174        ),
175        1 => (
176            b.get_indexed::<{ corner::MIN }, 1>(),
177            b.get_indexed::<{ corner::MAX }, 1>(),
178        ),
179        2 => (
180            b.get_indexed::<{ corner::MIN }, 2>(),
181            b.get_indexed::<{ corner::MAX }, 2>(),
182        ),
183        3 => (
184            b.get_indexed::<{ corner::MIN }, 3>(),
185            b.get_indexed::<{ corner::MAX }, 3>(),
186        ),
187        _ => unreachable!("fold_dims caps at MAX_DIM"),
188    };
189    let value = ordinate(p, d);
190    if strict {
191        min < value && value < max
192    } else {
193        min <= value && value <= max
194    }
195}
196
197impl<P, G> WithinStrategy<P, G> for WithinRing
198where
199    G: RingTrait<Point = P>,
200    P: PointTrait,
201    P::Scalar: CoordinateScalar,
202    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
203{
204    #[inline]
205    fn within(&self, p: &P, r: &G) -> bool {
206        winding_result(p, r) == InOut::Interior
207    }
208
209    #[inline]
210    fn covered_by(&self, p: &P, r: &G) -> bool {
211        !matches!(winding_result(p, r), InOut::Exterior)
212    }
213}
214
215impl<P, G> WithinStrategy<P, G> for WithinPoly
216where
217    G: PolygonTrait<Point = P>,
218    P: PointTrait,
219    P::Scalar: CoordinateScalar,
220    <P::Cs as CoordinateSystem>::Family: SameAs<CartesianFamily>,
221{
222    #[inline]
223    fn within(&self, p: &P, pg: &G) -> bool {
224        if !WithinRing.within(p, pg.exterior()) {
225            return false;
226        }
227        for hole in pg.interiors() {
228            if WithinRing.covered_by(p, hole) {
229                return false;
230            }
231        }
232        true
233    }
234
235    #[inline]
236    fn covered_by(&self, p: &P, pg: &G) -> bool {
237        if !WithinRing.covered_by(p, pg.exterior()) {
238            return false;
239        }
240        for hole in pg.interiors() {
241            if WithinRing.within(p, hole) {
242                return false;
243            }
244        }
245        true
246    }
247}
248
249/// Type-level "which `WithinStrategy` struct does this geometry *kind*
250/// use". One impl per [`geometry_tag`] kind tag, keyed on the tag (never a
251/// concept blanket — that would overlap, E0119). The
252/// [`crate::within`]/[`crate::covered_by`] free functions route
253/// `G → G::Kind → S` through this trait.
254#[doc(hidden)]
255pub trait WithinStrategyForKind {
256    /// The per-kind [`WithinStrategy`] struct this tag is computed with.
257    type S: Default;
258}
259
260impl WithinStrategyForKind for BoxTag {
261    type S = WithinBox;
262}
263impl WithinStrategyForKind for RingTag {
264    type S = WithinRing;
265}
266impl WithinStrategyForKind for PolygonTag {
267    type S = WithinPoly;
268}
269
270// ---- Winding-number kernel ------------------------------------------
271
272/// Tri-state outcome of the winding-number walk.
273///
274/// Mirrors the integer return of `cartesian_winding::result` at
275/// `strategy/cartesian/point_in_poly_winding.hpp:69-74`:
276/// `-1` outside, `0` on the boundary, `+1` strict interior.
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
278enum InOut {
279    Exterior,
280    Boundary,
281    Interior,
282}
283
284/// Walk the segments of `r` accumulating the winding count and the
285/// "on a segment" flag, then collapse to an [`InOut`].
286///
287/// Mirrors `cartesian_winding_base::apply` together with `result` at
288/// `strategy/cartesian/point_in_poly_winding.hpp:91-131, 69-74`. The
289/// closing edge for an open ring is added explicitly here — matches
290/// Boost's `closed_clockwise_view` wrap done one layer up at
291/// `algorithms/detail/within/point_in_geometry.hpp`.
292fn winding_result<P, R>(p: &P, r: &R) -> InOut
293where
294    P: PointTrait,
295    P::Scalar: CoordinateScalar,
296    R: RingTrait<Point = P>,
297{
298    let mut count: i32 = 0;
299    let mut it = r.points();
300    let Some(mut prev) = it.next() else {
301        // Empty ring — outside by convention. Mirrors the
302        // `boost::size(ring) < minimum_ring_size` guard at
303        // `algorithms/detail/within/point_in_geometry.hpp:198`.
304        return InOut::Exterior;
305    };
306    let first = prev;
307    let mut has_segment = false;
308    for curr in it {
309        has_segment = true;
310        match apply_segment(p, prev, curr) {
311            Step::Touches => return InOut::Boundary,
312            Step::Count(c) => count += c,
313        }
314        prev = curr;
315    }
316    // Close an open coordinate sequence explicitly. A repeated closing
317    // vertex already contributed through the preceding real edge.
318    let repeats_first =
319        has_segment && prev.get::<0>() == first.get::<0>() && prev.get::<1>() == first.get::<1>();
320    if !repeats_first {
321        match apply_segment(p, prev, first) {
322            Step::Touches => return InOut::Boundary,
323            Step::Count(c) => count += c,
324        }
325    }
326    if count == 0 {
327        InOut::Exterior
328    } else {
329        InOut::Interior
330    }
331}
332
333/// Per-segment outcome of the winding kernel.
334#[derive(Debug, Clone, Copy)]
335enum Step {
336    /// The point lies on this segment; the walk can short-circuit.
337    Touches,
338    /// Contribution to the running winding count.
339    Count(i32),
340}
341
342/// Single-segment contribution of the winding number, plus the
343/// on-segment short-circuit.
344///
345/// Mirrors `cartesian_winding_base::apply` at
346/// `strategy/cartesian/point_in_poly_winding.hpp:91-131` together
347/// with its `check_segment` / `check_touch` / `calculate_count` /
348/// `side_equal` helpers (lines 139-217). The cartesian side strategy
349/// reduces to the cross-product sign
350/// `(s2.x - s1.x) * (p.y - s1.y) - (s2.y - s1.y) * (p.x - s1.x)`
351/// (`strategy/cartesian/side_by_triangle.hpp:178-200`).
352fn apply_segment<P>(p: &P, s1: &P, s2: &P) -> Step
353where
354    P: PointTrait,
355    P::Scalar: CoordinateScalar,
356{
357    let px = p.get::<0>();
358    let py = p.get::<1>();
359    let s1x = s1.get::<0>();
360    let s2x = s2.get::<0>();
361    let s1y = s1.get::<1>();
362    let s2y = s2.get::<1>();
363
364    let eq1 = s1x == px;
365    let eq2 = s2x == px;
366
367    // check_touch: vertical segment exactly on the point's x.
368    // Mirrors lines 154-184 of point_in_poly_winding.hpp.
369    if eq1 && eq2 {
370        let (lo, hi) = if s1y <= s2y { (s1y, s2y) } else { (s2y, s1y) };
371        if lo <= py && py <= hi {
372            return Step::Touches;
373        }
374        return Step::Count(0);
375    }
376
377    // An endpoint on the ray contributes a half count only when it is
378    // vertically below the query. This is the direct form of Boost's
379    // `side_equal * count > 0` reduction.
380    if eq1 {
381        if py == s1y {
382            return Step::Touches;
383        }
384        return if py < s1y {
385            Step::Count(0)
386        } else if s2x > px {
387            Step::Count(1)
388        } else {
389            Step::Count(-1)
390        };
391    }
392    if eq2 {
393        if py == s2y {
394            return Step::Touches;
395        }
396        return if py < s2y {
397            Step::Count(0)
398        } else if s1x > px {
399            Step::Count(-1)
400        } else {
401            Step::Count(1)
402        };
403    }
404
405    let count = if s1x < px && s2x > px {
406        2
407    } else if s2x < px && s1x > px {
408        -2
409    } else {
410        return Step::Count(0);
411    };
412
413    // Cartesian side: sign of (s2 - s1) × (p - s1). A zero side is a
414    // boundary touch; otherwise it contributes only when its sign agrees
415    // with the crossing direction.
416    let cross = (s2x - s1x) * (py - s1y) - (s2y - s1y) * (px - s1x);
417    if cross > P::Scalar::ZERO {
418        if count > 0 {
419            Step::Count(count)
420        } else {
421            Step::Count(0)
422        }
423    } else if cross < P::Scalar::ZERO {
424        if count < 0 {
425            Step::Count(count)
426        } else {
427            Step::Count(0)
428        }
429    } else {
430        Step::Touches
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    //! Reference values from `geometry/test/strategies/winding.cpp:19-73`
437    //! (the Cartesian section). Each test cites the C++ line(s) it
438    //! mirrors.
439
440    use super::{Step, WithinBox, WithinPoly, WithinRing, WithinStrategy, apply_segment};
441    use geometry_cs::Cartesian;
442    use geometry_model::{Box, Point2D, Polygon, Ring, polygon};
443    use geometry_trait::Point as _;
444
445    type P = Point2D<f64, Cartesian>;
446
447    fn pt(x: f64, y: f64) -> P {
448        Point2D::new(x, y)
449    }
450
451    #[allow(clippy::float_cmp)]
452    fn reference_step(p: &P, s1: &P, s2: &P) -> i32 {
453        let px = p.get::<0>();
454        let py = p.get::<1>();
455        let s1x = s1.get::<0>();
456        let s2x = s2.get::<0>();
457        let s1y = s1.get::<1>();
458        let s2y = s2.get::<1>();
459
460        let eq1 = s1x == px;
461        let eq2 = s2x == px;
462        if eq1 && eq2 {
463            let (lo, hi) = if s1y <= s2y { (s1y, s2y) } else { (s2y, s1y) };
464            return if lo <= py && py <= hi { i32::MIN } else { 0 };
465        }
466
467        let count = if eq1 {
468            if s2x > px { 1 } else { -1 }
469        } else if eq2 {
470            if s1x > px { -1 } else { 1 }
471        } else if s1x < px && s2x > px {
472            2
473        } else if s2x < px && s1x > px {
474            -2
475        } else {
476            0
477        };
478        if count == 0 {
479            return 0;
480        }
481
482        let side = if count == 1 || count == -1 {
483            let sey = if eq1 { s1y } else { s2y };
484            if py == sey {
485                0
486            } else if py < sey {
487                -count
488            } else {
489                count
490            }
491        } else {
492            let cross = (s2x - s1x) * (py - s1y) - (s2y - s1y) * (px - s1x);
493            if cross > 0.0 {
494                1
495            } else if cross < 0.0 {
496                -1
497            } else {
498                0
499            }
500        };
501        if side == 0 {
502            i32::MIN
503        } else if side * count > 0 {
504            count
505        } else {
506            0
507        }
508    }
509
510    fn step_code(step: Step) -> i32 {
511        match step {
512            Step::Touches => i32::MIN,
513            Step::Count(count) => count,
514        }
515    }
516
517    #[test]
518    fn segment_step_matches_the_reference_branch_matrix() {
519        let values = [-2.0, -1.0, 0.0, 1.0, 2.0];
520        for &px in &values {
521            for &py in &values {
522                for &s1x in &values {
523                    for &s1y in &values {
524                        for &s2x in &values {
525                            for &s2y in &values {
526                                let point = pt(px, py);
527                                let first = pt(s1x, s1y);
528                                let second = pt(s2x, s2y);
529                                assert_eq!(
530                                    step_code(apply_segment(&point, &first, &second)),
531                                    reference_step(&point, &first, &second),
532                                    "point=({px}, {py}), segment=({s1x}, {s1y})→({s2x}, {s2y})"
533                                );
534                            }
535                        }
536                    }
537                }
538            }
539        }
540    }
541
542    fn box_polygon() -> Polygon<P> {
543        polygon![[(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0), (0.0, 0.0)]]
544    }
545
546    /// `winding.cpp:30` — `b1` interior point.
547    #[test]
548    fn box_b1_inside() {
549        assert!(WithinPoly.within(&pt(1.0, 1.0), &box_polygon()));
550    }
551
552    /// `winding.cpp:31` — `b2` exterior point.
553    #[test]
554    fn box_b2_outside() {
555        assert!(!WithinPoly.within(&pt(3.0, 3.0), &box_polygon()));
556    }
557
558    /// `winding.cpp:34-37` — all four corners are "officially false".
559    #[test]
560    fn box_corners_are_not_within() {
561        let p = box_polygon();
562        for (x, y) in [(0.0, 0.0), (0.0, 2.0), (2.0, 2.0), (2.0, 0.0)] {
563            assert!(!WithinPoly.within(&pt(x, y), &p), "corner ({x},{y})");
564        }
565    }
566
567    /// `winding.cpp:40-43` — all four sides are "officially false".
568    #[test]
569    fn box_sides_are_not_within() {
570        let p = box_polygon();
571        for (x, y) in [(0.0, 1.0), (1.0, 2.0), (2.0, 1.0), (1.0, 0.0)] {
572            assert!(!WithinPoly.within(&pt(x, y), &p), "side ({x},{y})");
573        }
574    }
575
576    /// `winding.cpp:46-47` — triangle interior / exterior.
577    #[test]
578    fn triangle_interior_and_exterior() {
579        let t: Polygon<P> = polygon![[(0.0, 0.0), (0.0, 4.0), (6.0, 0.0), (0.0, 0.0)]];
580        assert!(WithinPoly.within(&pt(1.0, 1.0), &t));
581        assert!(!WithinPoly.within(&pt(3.0, 3.0), &t));
582    }
583
584    /// `winding.cpp:58-60` — polygon-with-hole semantics: inside the
585    /// outer-but-outside the hole is within; inside the hole is not.
586    #[test]
587    fn hole_semantics() {
588        let with_hole: Polygon<P> = polygon![
589            [(0.0, 0.0), (0.0, 3.0), (3.0, 3.0), (3.0, 0.0), (0.0, 0.0)],
590            [(1.0, 1.0), (2.0, 1.0), (2.0, 2.0), (1.0, 2.0), (1.0, 1.0)]
591        ];
592        // h1
593        assert!(WithinPoly.within(&pt(0.5, 0.5), &with_hole));
594        // h2a — inside the hole
595        assert!(!WithinPoly.within(&pt(1.5, 1.5), &with_hole));
596    }
597
598    /// `covered_by` inverts the boundary rule: corners and sides are
599    /// covered, but external points are not. Mirrors the Boost
600    /// `result >= 0` projection at
601    /// `strategy/cartesian/point_in_poly_winding.hpp:69-74`.
602    #[test]
603    fn covered_by_includes_boundary() {
604        let p = box_polygon();
605        assert!(WithinPoly.covered_by(&pt(0.0, 0.0), &p));
606        assert!(WithinPoly.covered_by(&pt(0.0, 1.0), &p));
607        assert!(WithinPoly.covered_by(&pt(1.0, 1.0), &p));
608        assert!(!WithinPoly.covered_by(&pt(3.0, 3.0), &p));
609    }
610
611    /// `Box`-as-geometry path: strict-vs-non-strict per-dimension.
612    /// Mirrors `cartesian_point_in_box` at
613    /// `strategy/cartesian/point_in_box.hpp:55-93`.
614    #[test]
615    fn box_geometry_strict_vs_non_strict() {
616        let b = Box::from_corners(pt(0.0, 0.0), pt(2.0, 2.0));
617        // strict interior
618        assert!(WithinBox.within(&pt(1.0, 1.0), &b));
619        // boundary: corner
620        assert!(!WithinBox.within(&pt(0.0, 0.0), &b));
621        assert!(WithinBox.covered_by(&pt(0.0, 0.0), &b));
622        // boundary: side
623        assert!(!WithinBox.within(&pt(0.0, 1.0), &b));
624        assert!(WithinBox.covered_by(&pt(0.0, 1.0), &b));
625        // outside
626        assert!(!WithinBox.within(&pt(3.0, 3.0), &b));
627        assert!(!WithinBox.covered_by(&pt(3.0, 3.0), &b));
628    }
629
630    /// Ring-only path — same kernel, no exterior/interior split.
631    #[test]
632    fn ring_within_smoke() {
633        let r: Ring<P> = Ring::from_vec(vec![
634            pt(0.0, 0.0),
635            pt(0.0, 2.0),
636            pt(2.0, 2.0),
637            pt(2.0, 0.0),
638            pt(0.0, 0.0),
639        ]);
640        assert!(WithinRing.within(&pt(1.0, 1.0), &r));
641        assert!(!WithinRing.within(&pt(0.0, 0.0), &r));
642        assert!(WithinRing.covered_by(&pt(0.0, 0.0), &r));
643    }
644
645    /// Open ring (no repeated closing vertex): the kernel must add
646    /// the implicit `last -> first` edge so containment still works.
647    #[test]
648    fn open_ring_closes_implicitly() {
649        let mut r = Ring::<P, true, false>::new();
650        r.push(pt(0.0, 0.0));
651        r.push(pt(0.0, 2.0));
652        r.push(pt(2.0, 2.0));
653        r.push(pt(2.0, 0.0));
654        assert!(WithinRing.within(&pt(1.0, 1.0), &r));
655        assert!(!WithinRing.within(&pt(3.0, 3.0), &r));
656    }
657
658    /// `point_in_box.hpp` loops over every dimension: a point above a
659    /// 3-D box is outside it even when its `x`/`y` fall inside.
660    #[test]
661    fn box_containment_reads_the_third_dimension() {
662        use geometry_model::Point3D;
663        type P3 = Point3D<f64, Cartesian>;
664        let b = Box::from_corners(P3::new(0.0, 0.0, 0.0), P3::new(2.0, 2.0, 2.0));
665        assert!(WithinBox.within(&P3::new(1.0, 1.0, 1.0), &b));
666        assert!(!WithinBox.within(&P3::new(1.0, 1.0, 10.0), &b));
667        assert!(!WithinBox.covered_by(&P3::new(1.0, 1.0, 10.0), &b));
668        assert!(WithinBox.covered_by(&P3::new(1.0, 1.0, 2.0), &b));
669        assert!(!WithinBox.within(&P3::new(1.0, 1.0, 2.0), &b));
670    }
671
672    /// A 4-D point built ordinate-wise, since `Point::new` stops at
673    /// three arguments.
674    fn p4(v: [f64; 4]) -> geometry_model::Point<f64, 4> {
675        use geometry_trait::set_ordinate;
676        let mut p = geometry_model::Point::<f64, 4>::default();
677        for (d, value) in v.into_iter().enumerate() {
678            set_ordinate(&mut p, d, value);
679        }
680        p
681    }
682
683    /// `fold_dims` runs to the point's own arity, so the last row of the
684    /// per-dimension lookup is only reached by a point of the largest
685    /// arity the table supports. A point inside on x, y and z and
686    /// outside on the fourth axis is the input that distinguishes a
687    /// present row from a missing one — and the strict/inclusive split
688    /// must hold on that axis exactly as it does on x.
689    #[test]
690    fn box_containment_reads_the_fourth_dimension() {
691        let b = Box::from_corners(p4([0.0; 4]), p4([2.0; 4]));
692
693        assert!(WithinBox.within(&p4([1.0; 4]), &b));
694        assert!(!WithinBox.within(&p4([1.0, 1.0, 1.0, 10.0]), &b));
695        assert!(!WithinBox.covered_by(&p4([1.0, 1.0, 1.0, 10.0]), &b));
696
697        // On the boundary of the fourth axis only: covered, not within.
698        assert!(WithinBox.covered_by(&p4([1.0, 1.0, 1.0, 2.0]), &b));
699        assert!(!WithinBox.within(&p4([1.0, 1.0, 1.0, 2.0]), &b));
700    }
701
702    /// Past the last row the lookup must fail loudly rather than fall
703    /// through to another axis, which would answer with a comparison
704    /// the caller never asked for.
705    #[test]
706    #[should_panic(expected = "fold_dims caps at MAX_DIM")]
707    fn box_dimension_contains_panics_past_max_dim() {
708        let b = Box::from_corners(p4([0.0; 4]), p4([2.0; 4]));
709        let _ = super::box_dimension_contains(&p4([1.0; 4]), &b, 4, true);
710    }
711}