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
19//! the raw input coordinates (no rescale). It is exact only inside the
20//! safe-magnitude range; callers that need that guarantee route their
21//! inputs through [`range_guard`](super::range_guard) first. Boost's
22//! `side_by_triangle` additionally treats any coincident pair among the
23//! three points as collinear
24//! (`side_by_triangle.hpp:159-164`); this predicate does the same,
25//! because a zero-length base line has no well-defined side.
26
27use geometry_coords::CoordinateScalar;
28use geometry_trait::Point;
29
30/// The three possible outcomes of the [`orientation_2d`] side test.
31///
32/// Mirrors the `+1 / 0 / -1` return of Boost's `side_by_triangle`
33/// (`boost/geometry/strategy/cartesian/side_by_triangle.hpp`), named
34/// so call sites read as topology rather than as integers.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36pub enum Sign {
37    /// `r` lies to the **left** of the directed line `p → q`
38    /// (counter-clockwise turn). Boost's `+1`, the `'L'` case in
39    /// `test/strategies/spherical_side.cpp`.
40    Positive,
41    /// `r` lies to the **right** of the directed line `p → q`
42    /// (clockwise turn). Boost's `-1`, the `'R'` case.
43    Negative,
44    /// `p`, `q`, `r` are **collinear** (or two of them coincide).
45    /// Boost's `0`, the `'|'` case.
46    Collinear,
47}
48
49/// Sign of the signed area of the triangle `(p, q, r)` — i.e. which
50/// side of the directed line `p → q` the point `r` lies on.
51///
52/// Returns [`Sign::Positive`] for a left turn (counter-clockwise),
53/// [`Sign::Negative`] for a right turn (clockwise), and
54/// [`Sign::Collinear`] when the three points are collinear or any two
55/// coincide.
56///
57/// Mirrors `side_by_triangle::apply`
58/// (`boost/geometry/strategy/cartesian/side_by_triangle.hpp:144-147`),
59/// computing `(qx - px)(ry - py) - (qy - py)(rx - px)` and taking its
60/// sign. Cartesian only.
61///
62/// # Examples
63///
64/// ```
65/// use geometry_cs::Cartesian;
66/// use geometry_model::Point2D;
67/// use geometry_overlay::predicate::orientation::{orientation_2d, Sign};
68///
69/// type P = Point2D<f64, Cartesian>;
70/// let p = P::new(0.0, 0.0);
71/// let q = P::new(1.0, 0.0);
72///
73/// // A point above the x-axis is to the left of p → q.
74/// assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 1.0)), Sign::Positive);
75/// // Below is to the right.
76/// assert_eq!(orientation_2d(&p, &q, &P::new(0.5, -1.0)), Sign::Negative);
77/// // On the axis is collinear.
78/// assert_eq!(orientation_2d(&p, &q, &P::new(2.0, 0.0)), Sign::Collinear);
79/// ```
80#[must_use]
81pub fn orientation_2d<P>(p: &P, q: &P, r: &P) -> Sign
82where
83    P: Point,
84    P::Scalar: CoordinateScalar,
85{
86    let px = p.get::<0>();
87    let py = p.get::<1>();
88    let qx = q.get::<0>();
89    let qy = q.get::<1>();
90    let rx = r.get::<0>();
91    let ry = r.get::<1>();
92
93    // Signed area of (p, q, r). Boost's `side_by_triangle::side_value`
94    // computes the identical determinant
95    // (`side_by_triangle.hpp` `side_value`).
96    let area = (qx - px) * (ry - py) - (qy - py) * (rx - px);
97
98    if area > P::Scalar::ZERO {
99        Sign::Positive
100    } else if area < P::Scalar::ZERO {
101        Sign::Negative
102    } else {
103        Sign::Collinear
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    //! Reproduces the left / right / collinear convention asserted in
110    //! `test/strategies/spherical_side.cpp:55-56` (`1 = 'L'`,
111    //! `-1 = 'R'`, else collinear), on the Cartesian predicate.
112
113    use super::{Sign, orientation_2d};
114    use geometry_cs::Cartesian;
115    use geometry_model::Point2D;
116
117    type P = Point2D<f64, Cartesian>;
118
119    #[test]
120    fn left_right_collinear_unit_segment() {
121        let p = P::new(0.0, 0.0);
122        let q = P::new(1.0, 0.0);
123        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 1.0)), Sign::Positive);
124        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, -1.0)), Sign::Negative);
125        assert_eq!(orientation_2d(&p, &q, &P::new(0.5, 0.0)), Sign::Collinear);
126    }
127
128    #[test]
129    fn sign_flips_with_base_direction() {
130        // Reversing the directed base line flips left ↔ right — the
131        // signed area negates. `side_by_triangle` has the same
132        // antisymmetry.
133        let a = P::new(0.0, 0.0);
134        let b = P::new(4.0, 4.0);
135        let c = P::new(4.0, 0.0);
136        assert_eq!(orientation_2d(&a, &b, &c), Sign::Negative);
137        assert_eq!(orientation_2d(&b, &a, &c), Sign::Positive);
138    }
139
140    #[test]
141    fn coincident_points_are_collinear() {
142        // Boost returns 0 whenever two of the three points coincide
143        // (`side_by_triangle.hpp:159-164`) — a zero-length base line
144        // has no side.
145        let p = P::new(2.0, 3.0);
146        let r = P::new(9.0, 9.0);
147        assert_eq!(orientation_2d(&p, &p, &r), Sign::Collinear);
148        assert_eq!(orientation_2d(&p, &r, &p), Sign::Collinear);
149        assert_eq!(orientation_2d(&r, &p, &p), Sign::Collinear);
150    }
151
152    #[test]
153    fn diagonal_line_sides() {
154        // Line y = x, direction (0,0) → (2,2).
155        let p = P::new(0.0, 0.0);
156        let q = P::new(2.0, 2.0);
157        // (0,2) is above the line → left.
158        assert_eq!(orientation_2d(&p, &q, &P::new(0.0, 2.0)), Sign::Positive);
159        // (2,0) is below the line → right.
160        assert_eq!(orientation_2d(&p, &q, &P::new(2.0, 0.0)), Sign::Negative);
161        // (5,5) is on the line → collinear.
162        assert_eq!(orientation_2d(&p, &q, &P::new(5.0, 5.0)), Sign::Collinear);
163    }
164}