Skip to main content

geometry_overlay/predicate/
orientation.rs

1//! OVL1.T1 — the orientation (side) predicate.
2//!
3//! Given three points `p`, `q`, `r`, decide whether `r` lies to the
4//! left of, to the right of, or on the directed line `p → q`. This is
5//! the signed area of the triangle `(p, q, r)`, reduced to its sign.
6//!
7//! Mirrors `boost::geometry::strategy::side::side_by_triangle`
8//! (`boost/geometry/strategy/cartesian/side_by_triangle.hpp`). Boost's
9//! `side_value` computes the same signed area
10//! `(qx - px)(ry - py) - (qy - py)(rx - px)`; its result sign is the
11//! side, with `+1` = left, `-1` = right, `0` = collinear — the
12//! convention the spherical side test spells out explicitly
13//! (`test/strategies/spherical_side.cpp:55-56`: `side == 1 ? 'L' :
14//! side == -1 ? 'R'`).
15//!
16//! # Robustness
17//!
18//! The sign is computed on the raw input coordinates (no rescale) by the
19//! adaptive expansion arithmetic in
20//! [`geometry_coords::precise_math::orient2d`]. This mirrors Boost's robust
21//! side strategy and produces the exact sign for finite `f32`/`f64` inputs.
22//! Boost's
23//! `side_by_triangle` additionally treats any coincident pair among the
24//! three points as collinear
25//! (`side_by_triangle.hpp:150-164`); this predicate does the same, and
26//! coincident means Boost's `math::equals` — a relative epsilon — not bitwise
27//! equality, because a zero-length base line has no well-defined side and a
28//! base line a few last bits long has none worth trusting.
29
30use geometry_coords::{CoordinateScalar, precise_math};
31use geometry_trait::Point;
32
33/// The three possible outcomes of the [`orientation_2d`] side test.
34///
35/// Mirrors the `+1 / 0 / -1` return of Boost's `side_by_triangle`
36/// (`boost/geometry/strategy/cartesian/side_by_triangle.hpp`), named
37/// so call sites read as topology rather than as integers.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
39pub enum Sign {
40    /// `r` lies to the **left** of the directed line `p → q`
41    /// (counter-clockwise turn). Boost's `+1`, the `'L'` case in
42    /// `test/strategies/spherical_side.cpp`.
43    Positive,
44    /// `r` lies to the **right** of the directed line `p → q`
45    /// (clockwise turn). Boost's `-1`, the `'R'` case.
46    Negative,
47    /// `p`, `q`, `r` are **collinear** (or two of them coincide).
48    /// Boost's `0`, the `'|'` case.
49    Collinear,
50}
51
52/// Sign of the signed area of the triangle `(p, q, r)` — i.e. which
53/// side of the directed line `p → q` the point `r` lies on.
54///
55/// Returns [`Sign::Positive`] for a left turn (counter-clockwise),
56/// [`Sign::Negative`] for a right turn (clockwise), and
57/// [`Sign::Collinear`] when the three points are collinear or any two
58/// coincide.
59///
60/// Mirrors `side_by_triangle::apply`
61/// (`boost/geometry/strategy/cartesian/side_by_triangle.hpp:144-147`),
62/// computing `(qx - px)(ry - py) - (qy - py)(rx - px)` and taking its
63/// sign. Cartesian only.
64///
65/// # Examples
66///
67/// ```
68/// use geometry_cs::Cartesian;
69/// use geometry_model::Point2D;
70/// use geometry_overlay::predicate::orientation::{orientation_2d, Sign};
71///
72/// type P = Point2D<f64, Cartesian>;
73/// let p = P::new(0.0, 0.0);
74/// let q = P::new(1.0, 0.0);
75///
76/// // A point above the x-axis is to the left of p → q.
77/// assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 1.0)), Sign::Positive);
78/// // Below is to the right.
79/// assert_eq!(orientation_2d(&p, &q, &P::new(0.5, -1.0)), Sign::Negative);
80/// // On the axis is collinear.
81/// assert_eq!(orientation_2d(&p, &q, &P::new(2.0, 0.0)), Sign::Collinear);
82/// ```
83#[must_use]
84pub fn orientation_2d<P>(p: &P, q: &P, r: &P) -> Sign
85where
86    P: Point,
87    P::Scalar: CoordinateScalar + Into<f64>,
88{
89    let px = p.get::<0>();
90    let py = p.get::<1>();
91    let qx = q.get::<0>();
92    let qy = q.get::<1>();
93    let rx = r.get::<0>();
94    let ry = r.get::<1>();
95
96    // C++: `side_by_triangle` opens by calling the three points collinear if
97    // any two of them are `equals_point_point` — which is `math::equals` per
98    // coordinate, a *relative* epsilon (`side_by_triangle.hpp:150-164`). Two
99    // points a few last bits apart at a large coordinate are the same point to
100    // Boost, and the determinant below never gets to disagree.
101    let coincident = |ax: P::Scalar, ay: P::Scalar, bx: P::Scalar, by: P::Scalar| {
102        ax.tolerant_eq(bx) && ay.tolerant_eq(by)
103    };
104    if coincident(px, py, qx, qy) || coincident(px, py, rx, ry) || coincident(qx, qy, rx, ry) {
105        return Sign::Collinear;
106    }
107
108    // Signed area of (p, q, r). Boost's `side_by_triangle::side_value`
109    // computes the identical determinant
110    // (`side_by_triangle.hpp` `side_value`).
111    let area = precise_math::orient2d(
112        [px.into(), py.into()],
113        [qx.into(), qy.into()],
114        [rx.into(), ry.into()],
115    );
116
117    if area > 0.0 {
118        Sign::Positive
119    } else if area < 0.0 {
120        Sign::Negative
121    } else {
122        Sign::Collinear
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    //! Reproduces the left / right / collinear convention asserted in
129    //! `test/strategies/spherical_side.cpp:55-56` (`1 = 'L'`,
130    //! `-1 = 'R'`, else collinear), on the Cartesian predicate.
131
132    use super::{Sign, orientation_2d};
133    use geometry_cs::Cartesian;
134    use geometry_model::Point2D;
135
136    type P = Point2D<f64, Cartesian>;
137
138    #[test]
139    fn left_right_collinear_unit_segment() {
140        let p = P::new(0.0, 0.0);
141        let q = P::new(1.0, 0.0);
142        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 1.0)), Sign::Positive);
143        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, -1.0)), Sign::Negative);
144        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 0.0)), Sign::Collinear);
145    }
146
147    #[test]
148    fn sign_flips_with_base_direction() {
149        // Reversing the directed base line flips left ↔ right — the
150        // signed area negates. `side_by_triangle` has the same
151        // antisymmetry.
152        let a = P::new(0.0, 0.0);
153        let b = P::new(4.0, 4.0);
154        let c = P::new(4.0, 0.0);
155        assert_eq!(orientation_2d(&a, &b, &c), Sign::Negative);
156        assert_eq!(orientation_2d(&b, &a, &c), Sign::Positive);
157    }
158
159    #[test]
160    fn coincident_points_are_collinear() {
161        // Boost returns 0 whenever two of the three points coincide
162        // (`side_by_triangle.hpp:159-164`) — a zero-length base line
163        // has no side.
164        let p = P::new(2.0, 3.0);
165        let r = P::new(9.0, 9.0);
166        assert_eq!(orientation_2d(&p, &p, &r), Sign::Collinear);
167        assert_eq!(orientation_2d(&p, &r, &p), Sign::Collinear);
168        assert_eq!(orientation_2d(&r, &p, &p), Sign::Collinear);
169    }
170
171    #[test]
172    fn diagonal_line_sides() {
173        // Line y = x, direction (0,0) → (2,2).
174        let p = P::new(0.0, 0.0);
175        let q = P::new(2.0, 2.0);
176        // (0,2) is above the line → left.
177        assert_eq!(orientation_2d(&p, &q, &P::new(0.0, 2.0)), Sign::Positive);
178        // (2,0) is below the line → right.
179        assert_eq!(orientation_2d(&p, &q, &P::new(2.0, 0.0)), Sign::Negative);
180        // (5,5) is on the line → collinear.
181        assert_eq!(orientation_2d(&p, &q, &P::new(5.0, 5.0)), Sign::Collinear);
182    }
183}