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}