Skip to main content

ndslive_math/
polygon_triangulation.rs

1// SPDX-License-Identifier: BSD-3-Clause
2//! Ear-clipping polygon triangulation.
3//!
4//! Port of the C++ `PolygonTriangulation`
5//! (`cpp/include/ndsmath/polygontriangulation.{h,cpp}`) and the Python
6//! reference `python/src/ndslive/math/triangulation.py`. The C++ implementation
7//! uses a pointer-linked ring of partition vertices; this port uses an
8//! index-linked `Vec` of [`PartitionVertex`] (`previous` / `next` integer
9//! indices) to avoid manual memory management while reproducing the same
10//! traversal.
11//!
12//! # Note
13//!
14//! The *set* of output triangles is deterministic, but the ordering of
15//! triangles and the rotation of each triangle's three vertices are
16//! implementation-specific (driven by the "most extruded ear" tie-break and
17//! floating-point `angle` comparisons). Triangulation output is therefore
18//! **not** part of the cross-language parity vectors; tests assert structure
19//! (type and vertex count) only.
20
21use crate::polygon::PolygonType;
22use crate::wgs84::Wgs84;
23use crate::wgs84_polygon::Wgs84Polygon;
24
25/// A node in the ear-clipping ring (index-linked replacement for pointers).
26#[derive(Debug, Clone, Copy)]
27struct PartitionVertex {
28    is_active: bool,
29    is_convex: bool,
30    is_ear: bool,
31    p: Wgs84,
32    angle: f64,
33    previous: usize,
34    next: usize,
35}
36
37/// Triangulates simple polygons by ear clipping (O(n^2)).
38#[derive(Debug, Default, Clone, Copy)]
39pub struct PolygonTriangulation;
40
41impl PolygonTriangulation {
42    /// Construct a new triangulator.
43    pub fn new() -> Self {
44        PolygonTriangulation
45    }
46
47    /// Triangulate a CCW simple polygon into a `TriangleList`.
48    ///
49    /// The input must be a `SimplePolygon` with at least 3 vertices, in
50    /// counter-clockwise order. Returns a [`Wgs84Polygon`] of type
51    /// `TriangleList` on success (`3 * (n - 2)` vertices), or a polygon of type
52    /// `Unknown` on failure (wrong type, too few vertices, or no ear found).
53    pub fn triangulate_by_ear_clipping(&self, polygon: &Wgs84Polygon) -> Wgs84Polygon {
54        let mut result = Wgs84Polygon::with_type(PolygonType::TriangleList);
55
56        if polygon.polygon_type() != PolygonType::SimplePolygon || polygon.vertices().len() < 3 {
57            return Wgs84Polygon::with_type(PolygonType::Unknown);
58        }
59
60        let num_vertices = polygon.vertices().len();
61
62        // Nothing to do for a single triangle.
63        if num_vertices == 3 {
64            result.add_vertices(polygon.vertices());
65            return result;
66        }
67
68        let mut vertices: Vec<PartitionVertex> = (0..num_vertices)
69            .map(|i| PartitionVertex {
70                is_active: true,
71                is_convex: false,
72                is_ear: false,
73                p: polygon.get(i),
74                angle: 0.0,
75                next: if i == num_vertices - 1 { 0 } else { i + 1 },
76                previous: if i == 0 { num_vertices - 1 } else { i - 1 },
77            })
78            .collect();
79
80        for i in 0..num_vertices {
81            Self::update_vertex(i, &mut vertices, num_vertices);
82        }
83
84        let mut ear = 0usize;
85        for i in 0..(num_vertices - 3) {
86            let mut ear_found = false;
87
88            // Find the most extruded ear (largest angle; first wins ties).
89            for j in 0..num_vertices {
90                if !vertices[j].is_active || !vertices[j].is_ear {
91                    continue;
92                }
93                if !ear_found {
94                    ear_found = true;
95                    ear = j;
96                } else if vertices[j].angle > vertices[ear].angle {
97                    ear = j;
98                }
99            }
100
101            if !ear_found {
102                return Wgs84Polygon::with_type(PolygonType::Unknown);
103            }
104
105            let ear_prev = vertices[ear].previous;
106            let ear_next = vertices[ear].next;
107            result.add_vertex(vertices[ear_prev].p);
108            result.add_vertex(vertices[ear].p);
109            result.add_vertex(vertices[ear_next].p);
110
111            vertices[ear].is_active = false;
112            vertices[ear_prev].next = ear_next;
113            vertices[ear_next].previous = ear_prev;
114
115            if i == num_vertices - 4 {
116                break;
117            }
118
119            Self::update_vertex(ear_prev, &mut vertices, num_vertices);
120            Self::update_vertex(ear_next, &mut vertices, num_vertices);
121        }
122
123        for i in 0..num_vertices {
124            if vertices[i].is_active {
125                let prev = vertices[i].previous;
126                let next = vertices[i].next;
127                result.add_vertex(vertices[prev].p);
128                result.add_vertex(vertices[i].p);
129                result.add_vertex(vertices[next].p);
130                break;
131            }
132        }
133
134        result
135    }
136
137    /// Vector-normalize `p`; `(0, 0)` if it has zero length.
138    ///
139    /// Mirrors the C++ `normalize`: the unit vector is wrapped back into a
140    /// [`Wgs84`] (which re-normalizes as a coordinate). Odd, but part of the
141    /// reference behavior.
142    fn normalize(p: Wgs84) -> Wgs84 {
143        let n = (p.lon * p.lon + p.lat * p.lat).sqrt();
144        if n != 0.0 {
145            Wgs84::new(p.lon / n, p.lat / n)
146        } else {
147            Wgs84::new(0.0, 0.0)
148        }
149    }
150
151    /// Whether the turn `p1 -> p2 -> p3` is convex (positive cross product).
152    fn is_convex(p1: Wgs84, p2: Wgs84, p3: Wgs84) -> bool {
153        (p3.lat - p1.lat) * (p2.lon - p1.lon) - (p3.lon - p1.lon) * (p2.lat - p1.lat) > 0.0
154    }
155
156    /// Whether point `p` lies inside triangle `(p1, p2, p3)`.
157    fn is_inside(p1: Wgs84, p2: Wgs84, p3: Wgs84, p: Wgs84) -> bool {
158        !Self::is_convex(p1, p, p2) && !Self::is_convex(p2, p, p3) && !Self::is_convex(p3, p, p1)
159    }
160
161    /// Recompute convexity, ear status, and angle of vertex `v_idx`.
162    fn update_vertex(v_idx: usize, vertices: &mut [PartitionVertex], num_vertices: usize) {
163        let prev_idx = vertices[v_idx].previous;
164        let next_idx = vertices[v_idx].next;
165        let v_p = vertices[v_idx].p;
166        let v1_p = vertices[prev_idx].p;
167        let v3_p = vertices[next_idx].p;
168
169        let is_convex = Self::is_convex(v1_p, v_p, v3_p);
170
171        // NOTE: the subtraction goes through Wgs84 `operator-` which
172        // re-normalizes, matching the reference exactly.
173        let vec1 = Self::normalize(v1_p - v_p);
174        let vec3 = Self::normalize(v3_p - v_p);
175
176        let angle = vec1.lon * vec3.lon + vec1.lat * vec3.lat;
177
178        let mut is_ear = false;
179        if is_convex {
180            is_ear = true;
181            for vert in vertices.iter().take(num_vertices) {
182                let ip = vert.p;
183                if ip.lon == v_p.lon && ip.lat == v_p.lat {
184                    continue;
185                }
186                if ip.lon == v1_p.lon && ip.lat == v1_p.lat {
187                    continue;
188                }
189                if ip.lon == v3_p.lon && ip.lat == v3_p.lat {
190                    continue;
191                }
192                if Self::is_inside(v1_p, v_p, v3_p, ip) {
193                    is_ear = false;
194                    break;
195                }
196            }
197        }
198
199        let v = &mut vertices[v_idx];
200        v.is_convex = is_convex;
201        v.angle = angle;
202        v.is_ear = is_ear;
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    fn pts(coords: &[(f64, f64)]) -> Vec<Wgs84> {
211        coords.iter().map(|&(x, y)| Wgs84::new(x, y)).collect()
212    }
213
214    #[test]
215    fn triangle_passthrough() {
216        let t = PolygonTriangulation::new();
217        let poly = Wgs84Polygon::from_vertices(pts(&[(0.0, 0.0), (4.0, 0.0), (0.0, 4.0)]));
218        let out = t.triangulate_by_ear_clipping(&poly);
219        assert_eq!(out.polygon_type(), PolygonType::TriangleList);
220        assert_eq!(out.vertices().len(), 3);
221    }
222
223    #[test]
224    fn convex_quad() {
225        let t = PolygonTriangulation;
226        let poly =
227            Wgs84Polygon::from_vertices(pts(&[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]));
228        let out = t.triangulate_by_ear_clipping(&poly);
229        assert_eq!(out.polygon_type(), PolygonType::TriangleList);
230        // 3 * (n - 2) == 3 * 2 == 6 for a quad.
231        assert_eq!(out.vertices().len(), 6);
232    }
233
234    #[test]
235    fn concave_with_reflex_vertex() {
236        let t = PolygonTriangulation::new();
237        // An arrow / "dart" shape: the vertex (2, 2) is reflex (CCW order).
238        let poly = Wgs84Polygon::from_vertices(pts(&[
239            (0.0, 0.0),
240            (4.0, 0.0),
241            (2.0, 2.0),
242            (4.0, 4.0),
243            (0.0, 4.0),
244        ]));
245        let out = t.triangulate_by_ear_clipping(&poly);
246        assert_eq!(out.polygon_type(), PolygonType::TriangleList);
247        // 3 * (5 - 2) == 9.
248        assert_eq!(out.vertices().len(), 9);
249    }
250
251    #[test]
252    fn too_few_vertices_is_unknown() {
253        let t = PolygonTriangulation::new();
254        let poly = Wgs84Polygon::from_vertices(pts(&[(0.0, 0.0), (1.0, 0.0)]));
255        let out = t.triangulate_by_ear_clipping(&poly);
256        assert_eq!(out.polygon_type(), PolygonType::Unknown);
257        assert!(out.is_empty());
258    }
259
260    #[test]
261    fn wrong_type_is_unknown() {
262        let t = PolygonTriangulation::new();
263        let poly = Wgs84Polygon::with_type_and_vertices(
264            PolygonType::TriangleList,
265            pts(&[(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)]),
266        );
267        let out = t.triangulate_by_ear_clipping(&poly);
268        assert_eq!(out.polygon_type(), PolygonType::Unknown);
269    }
270
271    #[test]
272    fn regular_pentagon_uses_angle_tiebreak() {
273        // A regular pentagon's ears have differing angles, so the "most
274        // extruded ear" tie-break (largest angle wins) is exercised.
275        let t = PolygonTriangulation::new();
276        let mut coords = Vec::new();
277        for i in 0..5 {
278            let a = 2.0 * std::f64::consts::PI * (i as f64) / 5.0;
279            coords.push((a.cos(), a.sin()));
280        }
281        let poly = Wgs84Polygon::from_vertices(pts(&coords));
282        let out = t.triangulate_by_ear_clipping(&poly);
283        assert_eq!(out.polygon_type(), PolygonType::TriangleList);
284        // 3 * (5 - 2) == 9.
285        assert_eq!(out.vertices().len(), 9);
286    }
287
288    #[test]
289    fn clockwise_polygon_finds_no_ear() {
290        // A clockwise (wrong-winding) quad has no convex ears, so ear clipping
291        // fails and returns an UNKNOWN polygon.
292        let t = PolygonTriangulation::new();
293        let poly =
294            Wgs84Polygon::from_vertices(pts(&[(0.0, 0.0), (0.0, 4.0), (4.0, 4.0), (4.0, 0.0)]));
295        let out = t.triangulate_by_ear_clipping(&poly);
296        assert_eq!(out.polygon_type(), PolygonType::Unknown);
297        assert!(out.is_empty());
298    }
299
300    #[test]
301    fn coincident_vertex_zero_length_edge() {
302        // A duplicated leading vertex creates a zero-length edge, exercising the
303        // `normalize` zero-length branch. Still triangulates into 3*(n-2) verts.
304        let t = PolygonTriangulation::new();
305        let poly =
306            Wgs84Polygon::from_vertices(pts(&[(0.0, 0.0), (0.0, 0.0), (4.0, 0.0), (0.0, 4.0)]));
307        let out = t.triangulate_by_ear_clipping(&poly);
308        assert_eq!(out.polygon_type(), PolygonType::TriangleList);
309        assert_eq!(out.vertices().len(), 6);
310    }
311}