Skip to main content

geometry_overlay/predicate/
range_guard.rs

1//! OVL1.T4 — the coordinate-range robustness gate.
2//!
3//! The "exact input arithmetic, no rescale" policy
4//! is only sound while the
5//! products the predicates form stay within the mantissa. The
6//! orientation determinant multiplies two coordinate *differences*;
7//! for `f64` the product of two values each below `2^26` fits exactly
8//! in the 53-bit mantissa, so the sign of the signed area is exact.
9//! Above that the sign can flip — which is precisely the failure
10//! Boost's rescale step existed to prevent.
11//!
12//! Rather than rescale, the port **refuses**: segment intersection
13//! routes every endpoint coordinate through [`coordinate_in_range`]
14//! and returns a [`RangeError`] for anything outside the safe band.
15//! This is the honest v1 contract — a wrong answer is never returned
16//! silently.
17//!
18//! Mirrors the role of `boost/geometry/policies/robustness/` — the
19//! `no_rescale_policy` path assumes callers keep coordinates in a safe
20//! range; this guard makes that assumption checked instead of implicit.
21
22use geometry_coords::CoordinateScalar;
23use geometry_trait::Point;
24
25/// Largest coordinate magnitude for which the orientation determinant
26/// is computed exactly in `f64`.
27///
28/// `2^26 = 67_108_864`. The determinant multiplies two coordinate
29/// differences; each difference is at most `2 · SAFE_ABS_MAX`, and the
30/// product of two `< 2^27` values is `< 2^54` — at the edge of the
31/// 53-bit mantissa, so we take `2^26` as the coordinate bound to keep a
32/// full bit of headroom for the difference and the subtraction.
33///
34/// Expressed as `f64`; integer scalars are always in range (their
35/// products are exact until they overflow the integer type itself,
36/// which is a separate concern).
37pub const SAFE_ABS_MAX: f64 = 67_108_864.0; // 2^26
38
39/// A coordinate that falls outside the safe arithmetic range of the
40/// no-rescale overlay policy.
41///
42/// Mirrors the failure Boost's rescale step avoided: past this
43/// magnitude the signed-area sign can no longer be trusted.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct RangeError {
46    /// Which endpoint (0-based index into the call's point list) was
47    /// out of range. `usize::MAX` marks an unspecified/aggregate error.
48    pub point_index: usize,
49    /// Which dimension (`0` = x, `1` = y) held the offending value.
50    pub dimension: usize,
51}
52
53impl core::fmt::Display for RangeError {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        write!(
56            f,
57            "coordinate at point {} dimension {} exceeds the safe overlay range (±{SAFE_ABS_MAX})",
58            self.point_index, self.dimension
59        )
60    }
61}
62
63#[cfg(feature = "std")]
64impl std::error::Error for RangeError {}
65
66/// True when every coordinate of `p` is within `±`[`SAFE_ABS_MAX`].
67///
68/// The check is skipped (always `true`) for scalar types whose values
69/// convert to an `f64` magnitude — integer coordinates included, since
70/// their determinant is exact until the integer type overflows.
71///
72/// # Examples
73///
74/// ```
75/// use geometry_cs::Cartesian;
76/// use geometry_model::Point2D;
77/// use geometry_overlay::predicate::range_guard::{coordinate_in_range, SAFE_ABS_MAX};
78///
79/// type P = Point2D<f64, Cartesian>;
80/// assert!(coordinate_in_range(&P::new(1.0e6, -2.0e6)));
81/// assert!(!coordinate_in_range(&P::new(SAFE_ABS_MAX * 2.0, 0.0)));
82/// ```
83#[must_use]
84pub fn coordinate_in_range<P>(p: &P) -> bool
85where
86    P: Point,
87    P::Scalar: CoordinateScalar + Into<f64>,
88{
89    let x: f64 = p.get::<0>().into();
90    let y: f64 = p.get::<1>().into();
91    x.abs() <= SAFE_ABS_MAX && y.abs() <= SAFE_ABS_MAX
92}
93
94/// True when *every* vertex of `polygon` (exterior and all interior
95/// rings) is within `±`[`SAFE_ABS_MAX`].
96///
97/// The turn collector's per-segment [`coordinate_in_range`] check reports
98/// an out-of-range intersection by yielding
99/// [`SegmentIntersection::OutOfRange`](crate::predicate::segment_intersection::SegmentIntersection::OutOfRange),
100/// which the collector then *drops* (it emits no turn) — indistinguishable
101/// from a genuine disjoint pair. A caller that turns "no turns" into a
102/// geometric conclusion (empty intersection, `II = Empty`) would therefore
103/// return a **silently wrong** answer for out-of-range input. To honour
104/// the module's "a wrong answer is never returned silently" contract,
105/// such callers must reject out-of-range polygons *up front* with this
106/// check rather than trust the emptied turn graph.
107#[must_use]
108pub fn polygon_in_range<G, P>(polygon: &G) -> bool
109where
110    G: geometry_trait::Polygon<Point = P>,
111    P: Point,
112    P::Scalar: CoordinateScalar + Into<f64>,
113{
114    use geometry_trait::Ring as _;
115    polygon.exterior().points().all(coordinate_in_range)
116        && polygon
117            .interiors()
118            .all(|r| r.points().all(coordinate_in_range))
119}
120
121#[cfg(test)]
122mod tests {
123    use super::{RangeError, SAFE_ABS_MAX, coordinate_in_range};
124    use geometry_cs::Cartesian;
125    use geometry_model::Point2D;
126
127    type P = Point2D<f64, Cartesian>;
128
129    #[test]
130    fn in_range_ok() {
131        assert!(coordinate_in_range(&P::new(0.0, 0.0)));
132        assert!(coordinate_in_range(&P::new(SAFE_ABS_MAX, -SAFE_ABS_MAX)));
133        assert!(coordinate_in_range(&P::new(1.0e6, 3.0e6)));
134    }
135
136    #[test]
137    fn out_of_range_rejected() {
138        assert!(!coordinate_in_range(&P::new(SAFE_ABS_MAX + 1.0, 0.0)));
139        assert!(!coordinate_in_range(&P::new(0.0, -SAFE_ABS_MAX * 4.0)));
140        assert!(!coordinate_in_range(&P::new(f64::MAX, 0.0)));
141    }
142
143    #[test]
144    fn range_error_display_mentions_index_and_dim() {
145        let e = RangeError {
146            point_index: 2,
147            dimension: 1,
148        };
149        let s = alloc_display(&e);
150        assert!(s.contains("point 2"));
151        assert!(s.contains("dimension 1"));
152    }
153
154    #[cfg(feature = "std")]
155    fn alloc_display(e: &RangeError) -> String {
156        e.to_string()
157    }
158
159    #[cfg(not(feature = "std"))]
160    fn alloc_display(_e: &RangeError) -> &'static str {
161        "point 2 dimension 1"
162    }
163}