Skip to main content

map_tile_toolkit/
geom.rs

1//! Exact integer geometry predicates shared by clipping, ring orientation, and point-in-ring tests.
2//!
3//! Every predicate is exact across the full `i32` coordinate range and never panics (no floats, no
4//! division). A 2-D cross product of full-`i32` points can reach `2^65`, so exactness needs 128-bit
5//! arithmetic *somewhere*; following [`i_overlay`]/`i_float` — which widen `i32 → i64` and
6//! cross-multiply the edge *difference* vectors within a bounded coordinate domain — [`orient`]
7//! evaluates the cross in `i64` on the difference vectors and only widens to `i128` when a difference
8//! does not fit `i32` (the rare near-full-`i32`-span case). Ordinary tile-local geometry therefore
9//! never pays the 128-bit width, while the crate keeps its full-`i32` contract.
10//!
11//! [`i_overlay`]: https://crates.io/crates/i_overlay
12
13use core::cmp::Ordering;
14
15use geo_types::Coord;
16
17use crate::vertex::Vertex;
18
19/// Orientation of the turn `a → b → c`: the sign of the cross product `(b − a) × (c − a)`.
20///
21/// [`Ordering::Greater`] is a left turn (counter-clockwise), [`Ordering::Less`] a right turn
22/// (clockwise), and [`Ordering::Equal`] means the three points are collinear.
23#[inline]
24pub(crate) fn orient(a: Coord<i32>, b: Coord<i32>, c: Coord<i32>) -> Ordering {
25    // Edge vectors as `i64` — an `i32 − i32` difference always fits `i64`.
26    let ex1 = i64::from(b.x) - i64::from(a.x);
27    let ey1 = i64::from(b.y) - i64::from(a.y);
28    let ex2 = i64::from(c.x) - i64::from(a.x);
29    let ey2 = i64::from(c.y) - i64::from(a.y);
30    // Fast path: when every component fits `i32`, each product is `≤ (2^31 − 1)^2 < 2^62`, so the two
31    // `i64` products compare exactly with no risk of overflow. Otherwise widen the two products to
32    // `i128`. Comparing the products (rather than subtracting) avoids a possible `i64` overflow in the
33    // difference and needs no wider type on the fast path.
34    if fits_i32(ex1) && fits_i32(ey1) && fits_i32(ex2) && fits_i32(ey2) {
35        (ex1 * ey2).cmp(&(ey1 * ex2))
36    } else {
37        (i128::from(ex1) * i128::from(ey2)).cmp(&(i128::from(ey1) * i128::from(ex2)))
38    }
39}
40
41/// Whether an `i64` value fits back into an `i32`.
42#[inline]
43fn fits_i32(v: i64) -> bool {
44    i32::try_from(v).is_ok()
45}
46
47/// The smallest integer `≥ x`, where `x` is the point at which segment `a`–`b` crosses the horizontal
48/// line at height `y` (`a.y ≠ b.y`, `y` between them). Exact: `x = a.x + dx·t/dy` is evaluated as a
49/// rounded-up integer quotient in `i64` when the factors fit `i32`, widening to `i128` otherwise —
50/// the same fast path / fallback split as [`orient`]. The result lies between `a.x` and `b.x`.
51#[expect(
52    clippy::cast_possible_truncation,
53    reason = "the offset is dx·t/dy with |t| ≤ |dy|, so it fits i64"
54)]
55pub(crate) fn crossing_x_ceil(a: Coord<i32>, b: Coord<i32>, y: i32) -> i64 {
56    let dx = i64::from(b.x) - i64::from(a.x);
57    let mut dy = i64::from(b.y) - i64::from(a.y);
58    let mut t = i64::from(y) - i64::from(a.y);
59    // A positive divisor lets `div_euclid` floor; ceil(n/d) = −floor(−n/d).
60    if dy < 0 {
61        dy = -dy;
62        t = -t;
63    }
64    let offset = if fits_i32(dx) && fits_i32(t) {
65        -(-(dx * t)).div_euclid(dy)
66    } else {
67        (-(-(i128::from(dx) * i128::from(t))).div_euclid(i128::from(dy))) as i64
68    };
69    i64::from(a.x) + offset
70}
71
72/// Twice the signed area of `ring` (the shoelace sum `Σ xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ`), exact for any `i32`
73/// ring. Works whether or not the ring repeats its first vertex, and is `0` for a degenerate ring.
74///
75/// The sign is the ring's winding: **positive** turns counter-clockwise with `y` up — which is
76/// *clockwise on screen* with `y` down, i.e. an MVT exterior ring in tile coordinates — and
77/// **negative** the other way. The slicer preserves each input ring's winding (synthetic corners
78/// follow it), so a caller that needs a fixed convention (MVT: exterior positive, holes negative)
79/// normalizes once per feature before slicing, reversing any ring whose sign disagrees with its role.
80///
81/// Accumulated in `i128`: each term is a cross product of two `i32`-difference vectors (`< 2^65`), so
82/// the sum cannot overflow below `2^62` vertices.
83#[must_use]
84pub fn signed_area_2x<V: Vertex>(ring: &[V]) -> i128 {
85    let Some(v0) = ring.first().map(Vertex::position) else {
86        return 0;
87    };
88    // Measured about the first vertex, so the closing edge `v_{n-1} → v0` and the opening edge
89    // `v0 → v1` both contribute zero: iterating `windows(2)` gives the full doubled area whether or not
90    // the ring repeats its first vertex, and smaller magnitudes keep the terms well inside `i128`.
91    let mut area2: i128 = 0;
92    for w in ring.windows(2) {
93        let (p, q) = (w[0].position(), w[1].position());
94        let px = i128::from(p.x) - i128::from(v0.x);
95        let py = i128::from(p.y) - i128::from(v0.y);
96        let qx = i128::from(q.x) - i128::from(v0.x);
97        let qy = i128::from(q.y) - i128::from(v0.y);
98        area2 += px * qy - py * qx;
99    }
100    area2
101}
102
103/// The winding of a closed `ring` from the sign of [`signed_area_2x`]: [`Ordering::Greater`] for
104/// counter-clockwise (y up), [`Ordering::Less`] for clockwise, [`Ordering::Equal`] for a degenerate
105/// ring (zero area).
106pub(crate) fn ring_orientation<V: Vertex>(ring: &[V]) -> Ordering {
107    signed_area_2x(ring).cmp(&0)
108}
109
110/// Whether point `p` lies inside `ring` (a closed polygon ring), counting the boundary as **inside**.
111///
112/// Even-odd (crossing-number) ray cast along `+x`, evaluated with [`orient`] so there is no division
113/// and it is exact for all `i32` inputs. A half-open rule on each edge's `y` span (`[y_lo, y_hi)`)
114/// counts every crossing once, so a ray grazing a shared vertex is handled consistently. Points on an
115/// edge or vertex return `true`. The ring is treated cyclically; a repeated closing vertex is fine.
116#[cfg(test)]
117pub(crate) fn point_in_ring(p: Coord<i32>, ring: &[Coord<i32>]) -> bool {
118    let n = ring.len();
119    if n < 3 {
120        return false;
121    }
122    // Drop a repeated closing vertex so the cyclic walk visits each edge once.
123    let m = if ring[n - 1] == ring[0] { n - 1 } else { n };
124    if m < 3 {
125        return false;
126    }
127    point_in_distinct_ring(p, m, |i| ring[i])
128}
129
130/// [`point_in_ring`] over the `m ≥ 3` vertices `at(0..m)` of a ring with no repeated closing vertex.
131pub(crate) fn point_in_distinct_ring(
132    p: Coord<i32>,
133    m: usize,
134    at: impl Fn(usize) -> Coord<i32>,
135) -> bool {
136    let mut inside = false;
137    let mut prev = m - 1;
138    for i in 0..m {
139        let tail = at(prev);
140        let head = at(i);
141        // On-boundary check: collinear and within the edge's bounding box → treat as inside.
142        if orient(tail, head, p) == Ordering::Equal
143            && p.x >= tail.x.min(head.x)
144            && p.x <= tail.x.max(head.x)
145            && p.y >= tail.y.min(head.y)
146            && p.y <= tail.y.max(head.y)
147        {
148            return true;
149        }
150        // Does the edge straddle the horizontal ray at `p.y`? Half-open: exactly one endpoint is
151        // strictly above `p.y`.
152        if (tail.y > p.y) != (head.y > p.y) {
153            // The crossing is to the right of `p` iff `p` is on the correct side of the directed edge.
154            // For an upward edge a left turn (`Greater`) puts `p` left of it → the ray crosses to
155            // `p`'s right; a downward edge flips the sense.
156            let left = orient(tail, head, p) == Ordering::Greater;
157            if left == (head.y > tail.y) {
158                inside = !inside;
159            }
160        }
161        prev = i;
162    }
163    inside
164}
165
166#[cfg(test)]
167mod tests {
168    use geo_types::Coord;
169
170    use super::*;
171
172    fn c(x: i32, y: i32) -> Coord<i32> {
173        Coord { x, y }
174    }
175
176    #[test]
177    fn degenerate_ring_contains_nothing() {
178        // Fewer than three vertices, or only two distinct ones once the closing copy is dropped.
179        assert!(!point_in_ring(c(0, 0), &[c(0, 0), c(5, 5)]));
180        assert!(!point_in_ring(c(0, 0), &[c(0, 0), c(5, 5), c(0, 0)]));
181    }
182
183    #[test]
184    fn orient_basic_and_extremes() {
185        assert_eq!(orient(c(0, 0), c(1, 0), c(0, 1)), Ordering::Greater); // CCW / left
186        assert_eq!(orient(c(0, 0), c(1, 0), c(0, -1)), Ordering::Less); // CW / right
187        assert_eq!(orient(c(0, 0), c(2, 2), c(1, 1)), Ordering::Equal); // collinear
188        // Full-`i32`-span vectors take the `i128` fallback and must stay exact.
189        assert_eq!(
190            orient(
191                c(i32::MIN, i32::MIN),
192                c(i32::MAX, i32::MIN),
193                c(i32::MIN, i32::MAX)
194            ),
195            Ordering::Greater
196        );
197        assert_eq!(
198            orient(
199                c(i32::MIN, i32::MIN),
200                c(i32::MAX, i32::MAX),
201                c(i32::MIN + 1, i32::MIN + 1)
202            ),
203            Ordering::Equal
204        );
205    }
206
207    #[test]
208    fn crossing_x_ceil_is_exact() {
209        // x = 0 + 10·(1/3) = 3.33… → 4; reversed direction gives the same point.
210        assert_eq!(crossing_x_ceil(c(0, 0), c(10, 3), 1), 4);
211        assert_eq!(crossing_x_ceil(c(10, 3), c(0, 0), 1), 4);
212        // Exact integer crossings stay put, negative offsets round toward +∞.
213        assert_eq!(crossing_x_ceil(c(0, 0), c(10, 2), 1), 5);
214        assert_eq!(crossing_x_ceil(c(0, 0), c(-10, 3), 1), -3);
215        // Full-`i32` spans take the `i128` path.
216        let x = crossing_x_ceil(c(i32::MIN, i32::MIN), c(i32::MAX, i32::MAX), 0);
217        assert_eq!(x, 0);
218        // The anti-diagonal `x + y = −1` crosses `y = 1` exactly at `x = −2`; one unit to the right
219        // of an exact crossing must not round further.
220        let x = crossing_x_ceil(c(i32::MIN, i32::MAX), c(i32::MAX, i32::MIN), 1);
221        assert_eq!(x, -2);
222        let x = crossing_x_ceil(c(i32::MIN, i32::MAX), c(i32::MAX - 1, i32::MIN), 1);
223        assert_eq!(x, -2); // the slightly steeper line crosses just left of −2
224    }
225
226    #[test]
227    fn ring_orientation_ccw_cw() {
228        let ccw = [c(0, 0), c(4, 0), c(4, 4), c(0, 4), c(0, 0)];
229        let cw = [c(0, 0), c(0, 4), c(4, 4), c(4, 0), c(0, 0)];
230        assert_eq!(ring_orientation(&ccw), Ordering::Greater);
231        assert_eq!(ring_orientation(&cw), Ordering::Less);
232        assert_eq!(ring_orientation(&ccw[..2]), Ordering::Equal); // degenerate
233        // Open and closed rings agree, and the magnitude is twice the area.
234        assert_eq!(signed_area_2x(&ccw), 32);
235        assert_eq!(signed_area_2x(&ccw[..4]), 32);
236        assert_eq!(signed_area_2x(&cw), -32);
237        assert_eq!(signed_area_2x::<Coord<i32>>(&[]), 0);
238        // Full-`i32` extremes stay exact.
239        let big = [
240            c(i32::MIN, i32::MIN),
241            c(i32::MAX, i32::MIN),
242            c(i32::MAX, i32::MAX),
243        ];
244        let side = i128::from(i32::MAX) - i128::from(i32::MIN);
245        assert_eq!(signed_area_2x(&big), side * side);
246    }
247
248    #[test]
249    fn point_in_ring_square() {
250        let sq = [c(0, 0), c(10, 0), c(10, 10), c(0, 10), c(0, 0)];
251        assert!(point_in_ring(c(5, 5), &sq)); // interior
252        assert!(!point_in_ring(c(15, 5), &sq)); // exterior
253        assert!(point_in_ring(c(0, 5), &sq)); // on the left edge → boundary counts as inside
254        assert!(point_in_ring(c(10, 10), &sq)); // corner vertex
255        assert!(!point_in_ring(c(-1, 5), &sq));
256    }
257
258    #[test]
259    fn point_in_ring_concave() {
260        // A C-shape (concave): the notch on the right is outside.
261        let c_shape = [
262            c(0, 0),
263            c(10, 0),
264            c(10, 3),
265            c(3, 3),
266            c(3, 7),
267            c(10, 7),
268            c(10, 10),
269            c(0, 10),
270            c(0, 0),
271        ];
272        assert!(point_in_ring(c(1, 5), &c_shape)); // in the spine
273        assert!(!point_in_ring(c(6, 5), &c_shape)); // in the notch → outside
274        assert!(point_in_ring(c(6, 1), &c_shape)); // in the bottom arm
275    }
276}