Skip to main content

geometry_algorithm/
is_convex.rs

1//! `is_convex(&g) -> bool`.
2//!
3//! Mirrors `boost::geometry::is_convex(g)` from
4//! `boost/geometry/algorithms/is_convex.hpp`. A ring is convex iff every
5//! consecutive cross-product `((b − a) × (c − b))` has the same sign
6//! (zero allowed — collinear interior vertices don't disqualify). A
7//! polygon is convex iff its outer ring is convex AND it has no interior
8//! rings.
9//!
10//! This is Cartesian-only: the cross-product predicate is the same in
11//! every Cartesian coordinate system, and spherical / geographic
12//! convexity is a different algorithm not yet in Boost, so no strategy
13//! layer is needed — Boost itself ships `is_convex` as a plain free
14//! function.
15
16use alloc::vec::Vec;
17
18use geometry_coords::CoordinateScalar;
19use geometry_model::{MultiPolygon, Polygon, Ring};
20use geometry_trait::{Point as PointTrait, Polygon as _, Ring as RingTrait};
21
22/// True iff `g` is convex.
23///
24/// Mirrors `boost::geometry::is_convex(g)` from
25/// `boost/geometry/algorithms/is_convex.hpp`.
26#[must_use]
27pub fn is_convex<G: IsConvex>(g: &G) -> bool {
28    g.is_convex()
29}
30
31/// Per-kind convexity dispatch. Implemented for [`Ring`], [`Polygon`],
32/// and [`MultiPolygon`].
33#[doc(hidden)]
34pub trait IsConvex {
35    /// True iff `self` is convex.
36    fn is_convex(&self) -> bool;
37}
38
39/// Convexity test for a ring: every consecutive cross-product shares one
40/// sign (zero permitted). A closing duplicate is dropped first and the
41/// walk indexes modularly, so the seam vertex's own turn is examined —
42/// kept, the duplicate makes both windows touching the seam degenerate
43/// and a reflex first vertex goes unnoticed.
44fn ring_is_convex<P: PointTrait, const CW: bool, const CL: bool>(ring: &Ring<P, CW, CL>) -> bool {
45    let mut pts: Vec<&P> = ring.points().collect();
46    if pts.len() >= 2 {
47        let (first, last) = (pts[0], pts[pts.len() - 1]);
48        if first.get::<0>() == last.get::<0>() && first.get::<1>() == last.get::<1>() {
49            pts.pop();
50        }
51    }
52    let len = pts.len();
53    if len < 3 {
54        return true;
55    }
56
57    let zero = P::Scalar::ZERO;
58    let mut sign: Option<bool> = None; // Some(true) = positive turn
59    for index in 0..len {
60        let prev = pts[index];
61        let curr = pts[(index + 1) % len];
62        let next = pts[(index + 2) % len];
63        let edge_in_x = curr.get::<0>() - prev.get::<0>();
64        let edge_in_y = curr.get::<1>() - prev.get::<1>();
65        let edge_out_x = next.get::<0>() - curr.get::<0>();
66        let edge_out_y = next.get::<1>() - curr.get::<1>();
67        let cross = edge_in_x * edge_out_y - edge_in_y * edge_out_x;
68        if cross == zero {
69            continue;
70        }
71        let positive = cross > zero;
72        match sign {
73            None => sign = Some(positive),
74            Some(previous) if previous != positive => return false,
75            _ => {}
76        }
77    }
78    true
79}
80
81impl<P: PointTrait, const CW: bool, const CL: bool> IsConvex for Ring<P, CW, CL> {
82    fn is_convex(&self) -> bool {
83        ring_is_convex(self)
84    }
85}
86
87impl<P: PointTrait, const CW: bool, const CL: bool> IsConvex for Polygon<P, CW, CL> {
88    fn is_convex(&self) -> bool {
89        self.interiors().count() == 0 && ring_is_convex(self.exterior())
90    }
91}
92
93impl<Pg: IsConvex + geometry_trait::Polygon> IsConvex for MultiPolygon<Pg> {
94    fn is_convex(&self) -> bool {
95        self.0.iter().all(IsConvex::is_convex)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    //! Reference behaviour from
102    //! `boost/geometry/test/algorithms/is_convex.cpp` — a triangle /
103    //! square is convex; a reflex polygon and any polygon with a hole
104    //! are not.
105
106    use super::is_convex;
107    use geometry_cs::Cartesian;
108    use geometry_model::{MultiPolygon, Point2D, Polygon, Ring, polygon};
109
110    type Pt = Point2D<f64, Cartesian>;
111
112    #[test]
113    fn triangle_is_convex() {
114        let pg: Polygon<Pt> = polygon![[(0., 0.), (4., 0.), (2., 3.), (0., 0.)]];
115        assert!(is_convex(&pg));
116    }
117
118    #[test]
119    fn square_is_convex() {
120        let pg: Polygon<Pt> = polygon![[(0., 0.), (4., 0.), (4., 4.), (0., 4.), (0., 0.)]];
121        assert!(is_convex(&pg));
122    }
123
124    #[test]
125    fn reflex_polygon_is_not_convex() {
126        let pg: Polygon<Pt> =
127            polygon![[(0., 0.), (4., 0.), (2., 1.), (4., 4.), (0., 4.), (0., 0.)]];
128        assert!(!is_convex(&pg));
129    }
130
131    #[test]
132    fn polygon_with_hole_is_not_convex() {
133        let pg: Polygon<Pt> = polygon![
134            [(0., 0.), (4., 0.), (4., 4.), (0., 4.), (0., 0.)],
135            [(1., 1.), (2., 1.), (2., 2.), (1., 2.), (1., 1.)],
136        ];
137        assert!(!is_convex(&pg));
138    }
139
140    /// A ring with fewer than 3 distinct vertices is trivially convex
141    /// (the `len < 3` guard).
142    #[test]
143    fn two_point_ring_is_trivially_convex() {
144        let r: Ring<Pt> = Ring::from_vec(alloc::vec![Pt::new(0., 0.), Pt::new(1., 0.)]);
145        assert!(is_convex(&r));
146    }
147
148    /// All vertices collinear: every cross-product is zero, no sign is
149    /// ever set, and the ring counts as convex (matches Boost, where
150    /// degenerate/collinear rings are not rejected as concave).
151    #[test]
152    fn collinear_ring_is_convex() {
153        let r: Ring<Pt> = Ring::from_vec(alloc::vec![
154            Pt::new(0., 0.),
155            Pt::new(1., 1.),
156            Pt::new(2., 2.)
157        ]);
158        assert!(is_convex(&r));
159    }
160
161    /// A convex (non-degenerate) `Ring` exercises the `Ring` impl's
162    /// positive path directly, not via `Polygon`.
163    #[test]
164    fn convex_ring_direct() {
165        let r: Ring<Pt> = Ring::from_vec(alloc::vec![
166            Pt::new(0., 0.),
167            Pt::new(4., 0.),
168            Pt::new(4., 4.),
169            Pt::new(0., 4.),
170        ]);
171        assert!(is_convex(&r));
172    }
173
174    /// `MultiPolygon` is convex iff *every* member is.
175    #[test]
176    fn multi_polygon_all_members_must_be_convex() {
177        let convex: Polygon<Pt> = polygon![[(0., 0.), (4., 0.), (2., 3.), (0., 0.)]];
178        let reflex: Polygon<Pt> =
179            polygon![[(0., 0.), (4., 0.), (2., 1.), (4., 4.), (0., 4.), (0., 0.)]];
180        let all_convex = MultiPolygon(alloc::vec![convex.clone(), convex.clone()]);
181        assert!(is_convex(&all_convex));
182        let mixed = MultiPolygon(alloc::vec![convex, reflex]);
183        assert!(!is_convex(&mixed));
184    }
185
186    /// The turn at the seam vertex counts: a polygon whose only reflex
187    /// vertex is its first (and closing) vertex is not convex.
188    #[test]
189    fn reflex_vertex_at_closed_ring_seam_is_not_convex() {
190        let pg: Polygon<Pt> =
191            polygon![[(2., 1.), (4., 4.), (0., 4.), (0., 0.), (4., 0.), (2., 1.)]];
192        assert!(!is_convex(&pg));
193        let r: Ring<Pt> = Ring::from_vec(alloc::vec![
194            Pt::new(2., 1.),
195            Pt::new(4., 4.),
196            Pt::new(0., 4.),
197            Pt::new(0., 0.),
198            Pt::new(4., 0.),
199            Pt::new(2., 1.),
200        ]);
201        assert!(!is_convex(&r));
202    }
203
204    /// Below two vertices there is no pair to compare for a closing
205    /// duplicate, so the seam-trimming step is skipped entirely. Both
206    /// degenerate rings still have to answer — convex, by the same
207    /// `len < 3` rule that covers the two-point ring — rather than
208    /// index into an empty sequence.
209    #[test]
210    fn rings_below_two_vertices_are_trivially_convex() {
211        let empty: Ring<Pt> = Ring::from_vec(alloc::vec![]);
212        assert!(is_convex(&empty));
213
214        let single: Ring<Pt> = Ring::from_vec(alloc::vec![Pt::new(3., 7.)]);
215        assert!(is_convex(&single));
216    }
217}