Skip to main content

ndslive_math/
polygon.rs

1// SPDX-License-Identifier: BSD-3-Clause
2//! Generic polygon container with orientation, mirroring the C++ `Polygon`.
3//!
4//! Port of `cpp/include/ndsmath/polygon.h` and the Python reference
5//! `python/src/ndslive/math/polygon.py`. The C++ class is a template over a
6//! vertex container; here it is specialized directly to a `Vec` of
7//! [`crate::Wgs84`] vertices. Orientation is computed on the raw `(lon, lat)`
8//! plane (no WGS84 normalization, no antimeridian handling).
9
10use crate::wgs84::Wgs84;
11
12/// Winding order of a polygon. Integer values match the C++ enum.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[repr(i8)]
15pub enum Orientation {
16    /// Clockwise winding (negative signed area).
17    Clockwise = -1,
18    /// Degenerate / unsupported polygon (zero area or unsupported type).
19    InvalidOrientation = 0,
20    /// Counter-clockwise winding (positive signed area).
21    CounterClockwise = 1,
22}
23
24/// Polygon topology. Integer values match the C++ enum.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26#[repr(u8)]
27pub enum PolygonType {
28    /// A simple polygon with an arbitrary number of vertices and no holes; the
29    /// last vertex connects back to the first.
30    SimplePolygon = 0,
31    /// A triangle strip.
32    TriangleStrip = 1,
33    /// A triangle fan.
34    TriangleFan = 2,
35    /// A set of independent triangles; three consecutive vertices form one.
36    TriangleList = 3,
37    /// Illegal polygon type, used to signal failure.
38    Unknown = 4,
39}
40
41/// A set of vertices with a topology type and orientation query.
42///
43/// Mirrors the C++ `Polygon<Vector>` template. The base [`Polygon::is_valid`]
44/// requires at least one vertex; [`crate::Wgs84Polygon`] wraps this and
45/// overrides validity to require at least three.
46#[derive(Debug, Clone, PartialEq)]
47pub struct Polygon {
48    polygon_type: PolygonType,
49    vertices: Vec<Wgs84>,
50}
51
52impl Polygon {
53    /// Construct an empty polygon of the given type.
54    pub fn new(polygon_type: PolygonType) -> Self {
55        Polygon {
56            polygon_type,
57            vertices: Vec::new(),
58        }
59    }
60
61    /// Construct a polygon of the given type with the supplied vertices.
62    pub fn with_vertices(polygon_type: PolygonType, vertices: Vec<Wgs84>) -> Self {
63        Polygon {
64            polygon_type,
65            vertices,
66        }
67    }
68
69    /// Append a single vertex.
70    pub fn add_vertex(&mut self, position: Wgs84) {
71        self.vertices.push(position);
72    }
73
74    /// Append a list of vertices, in order.
75    pub fn add_vertices(&mut self, vertices: &[Wgs84]) {
76        self.vertices.extend_from_slice(vertices);
77    }
78
79    /// Get a vertex by index (panics on out-of-bounds, like C++ `operator[]`).
80    pub fn get(&self, index: usize) -> Wgs84 {
81        self.vertices[index]
82    }
83
84    /// Set a vertex by index.
85    pub fn set(&mut self, index: usize, value: Wgs84) {
86        self.vertices[index] = value;
87    }
88
89    /// Number of vertices.
90    pub fn len(&self) -> usize {
91        self.vertices.len()
92    }
93
94    /// Whether the polygon has no vertices.
95    pub fn is_empty(&self) -> bool {
96        self.vertices.is_empty()
97    }
98
99    /// Get the polygon type.
100    pub fn polygon_type(&self) -> PolygonType {
101        self.polygon_type
102    }
103
104    /// Set the polygon type.
105    pub fn set_type(&mut self, polygon_type: PolygonType) {
106        self.polygon_type = polygon_type;
107    }
108
109    /// Whether this is a valid polygon.
110    ///
111    /// Base implementation: at least one vertex. [`crate::Wgs84Polygon`]
112    /// requires at least three.
113    pub fn is_valid(&self) -> bool {
114        !self.vertices.is_empty()
115    }
116
117    /// Get the (shared) slice of vertices.
118    pub fn vertices(&self) -> &[Wgs84] {
119        &self.vertices
120    }
121
122    /// Get the mutable `Vec` of vertices.
123    pub fn vertices_mut(&mut self) -> &mut Vec<Wgs84> {
124        &mut self.vertices
125    }
126
127    /// Compute the winding order via the signed shoelace formula.
128    ///
129    /// Only works for `SimplePolygon` and a single-triangle `TriangleList`
130    /// (exactly 3 vertices). All other types return `InvalidOrientation`
131    /// without computing area. Collinear vertices (zero area) also return
132    /// `InvalidOrientation`.
133    ///
134    /// Uses raw `(lon, lat)` doubles; no normalization.
135    pub fn orientation(&self) -> Orientation {
136        let is_supported = self.polygon_type == PolygonType::SimplePolygon
137            || (self.polygon_type == PolygonType::TriangleList && self.vertices.len() == 3);
138        if !is_supported {
139            return Orientation::InvalidOrientation;
140        }
141
142        let n = self.vertices.len();
143        let mut area = 0.0_f64;
144        for i in 0..n {
145            let j = if i + 1 == n { 0 } else { i + 1 };
146            area += self.vertices[i].lon * self.vertices[j].lat;
147            area -= self.vertices[i].lat * self.vertices[j].lon;
148        }
149
150        if area > 0.0 {
151            Orientation::CounterClockwise
152        } else if area < 0.0 {
153            Orientation::Clockwise
154        } else {
155            Orientation::InvalidOrientation
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn pts(coords: &[(f64, f64)]) -> Vec<Wgs84> {
165        coords.iter().map(|&(x, y)| Wgs84::new(x, y)).collect()
166    }
167
168    #[test]
169    fn base_is_valid_needs_one_vertex() {
170        let empty = Polygon::new(PolygonType::SimplePolygon);
171        assert!(!empty.is_valid());
172        assert!(empty.is_empty());
173        let mut one = Polygon::new(PolygonType::SimplePolygon);
174        one.add_vertex(Wgs84::new(0.0, 0.0));
175        assert!(one.is_valid());
176    }
177
178    #[test]
179    fn ccw_and_cw() {
180        let ccw = Polygon::with_vertices(
181            PolygonType::SimplePolygon,
182            pts(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]),
183        );
184        assert_eq!(ccw.orientation(), Orientation::CounterClockwise);
185
186        let cw = Polygon::with_vertices(
187            PolygonType::SimplePolygon,
188            pts(&[(0.0, 0.0), (0.0, 1.0), (1.0, 0.0)]),
189        );
190        assert_eq!(cw.orientation(), Orientation::Clockwise);
191    }
192
193    #[test]
194    fn collinear_is_invalid() {
195        let p = Polygon::with_vertices(
196            PolygonType::SimplePolygon,
197            pts(&[(0.0, 0.0), (1.0, 1.0), (2.0, 2.0)]),
198        );
199        assert_eq!(p.orientation(), Orientation::InvalidOrientation);
200    }
201
202    #[test]
203    fn unsupported_type_is_invalid() {
204        let strip = Polygon::with_vertices(
205            PolygonType::TriangleStrip,
206            pts(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]),
207        );
208        assert_eq!(strip.orientation(), Orientation::InvalidOrientation);
209
210        // A multi-triangle TRIANGLE_LIST (4 vertices) is also unsupported.
211        let multi = Polygon::with_vertices(
212            PolygonType::TriangleList,
213            pts(&[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]),
214        );
215        assert_eq!(multi.orientation(), Orientation::InvalidOrientation);
216    }
217
218    #[test]
219    fn single_triangle_list_allowed() {
220        let t = Polygon::with_vertices(
221            PolygonType::TriangleList,
222            pts(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]),
223        );
224        assert_eq!(t.orientation(), Orientation::CounterClockwise);
225    }
226
227    #[test]
228    fn get_set_and_accessors() {
229        let mut p = Polygon::new(PolygonType::Unknown);
230        p.add_vertices(&pts(&[(1.0, 2.0), (3.0, 4.0)]));
231        assert_eq!(p.len(), 2);
232        assert_eq!(p.polygon_type(), PolygonType::Unknown);
233        assert_eq!(p.get(0), Wgs84::new(1.0, 2.0));
234        p.set(0, Wgs84::new(5.0, 6.0));
235        assert_eq!(p.get(0), Wgs84::new(5.0, 6.0));
236        p.set_type(PolygonType::SimplePolygon);
237        assert_eq!(p.polygon_type(), PolygonType::SimplePolygon);
238        assert_eq!(p.vertices().len(), 2);
239        p.vertices_mut().push(Wgs84::new(7.0, 8.0));
240        assert_eq!(p.len(), 3);
241    }
242}