1pub type P2 = [f64; 2];
8pub type P3 = [f64; 3];
9
10const MAX_POINTS: usize = 20_000;
13
14fn cross(a: P2, b: P2, c: P2) -> f64 {
15 (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
16}
17
18fn signed_area(pts: &[P2], ring: &[usize]) -> f64 {
20 let n = ring.len();
21 (0..n).map(|k| {
22 let (a, b) = (pts[ring[k]], pts[ring[(k + 1) % n]]);
23 a[0] * b[1] - b[0] * a[1]
24 }).sum()
25}
26
27fn segments_cross(a: P2, b: P2, c: P2, d: P2) -> bool {
29 let d1 = cross(a, b, c);
30 let d2 = cross(a, b, d);
31 let d3 = cross(c, d, a);
32 let d4 = cross(c, d, b);
33 ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
34}
35
36fn locally_inside(pts: &[P2], ring: &[usize], k: usize, q: P2) -> bool {
39 let n = ring.len();
40 let (a, b, c) = (pts[ring[(k + n - 1) % n]], pts[ring[k]], pts[ring[(k + 1) % n]]);
41 if cross(a, b, c) >= 0.0 {
42 cross(b, c, q) >= 0.0 && cross(a, b, q) >= 0.0
44 } else {
45 cross(b, c, q) >= 0.0 || cross(a, b, q) >= 0.0
47 }
48}
49
50fn bridge(pts: &[P2], ring: &mut Vec<usize>, hole: &[usize], others: &[Vec<usize>]) {
53 let (hk, &m) = hole.iter().enumerate().max_by(|a, b| pts[*a.1][0].total_cmp(&pts[*b.1][0])).expect("a hole has vertices");
54 let pm = pts[m];
55 let d2 = |p: P2| (p[0] - pm[0]).powi(2) + (p[1] - pm[1]).powi(2);
56 let mut order: Vec<usize> = (0..ring.len()).collect();
57 order.sort_by(|&a, &b| d2(pts[ring[a]]).total_cmp(&d2(pts[ring[b]])));
58 let blocked = |pp: P2| {
59 let edges = std::iter::once(ring.as_slice()).chain(std::iter::once(hole)).chain(others.iter().map(Vec::as_slice));
60 for r in edges {
61 let n = r.len();
62 for e in 0..n {
63 let (c, d) = (pts[r[e]], pts[r[(e + 1) % n]]);
64 if segments_cross(pm, pp, c, d) {
65 return true;
66 }
67 }
68 }
69 false
70 };
71 let pos = order.iter().copied().find(|&k| locally_inside(pts, ring, k, pm) && !blocked(pts[ring[k]])).unwrap_or(order[0]);
72 let p = ring[pos];
73 let mut spliced = Vec::with_capacity(ring.len() + hole.len() + 2);
74 spliced.extend_from_slice(&ring[..=pos]);
75 spliced.extend_from_slice(&hole[hk..]);
76 spliced.extend_from_slice(&hole[..=hk]);
77 spliced.push(p);
78 spliced.extend_from_slice(&ring[pos + 1..]);
79 *ring = spliced;
80}
81
82pub fn triangulate_2d(outer: &[P2], holes: &[&[P2]]) -> Vec<[usize; 3]> {
87 let mut pts: Vec<P2> = outer.to_vec();
88 let mut hole_rings = Vec::new();
89 for h in holes {
90 let start = pts.len();
91 pts.extend_from_slice(h);
92 if h.len() >= 3 {
93 hole_rings.push((start..pts.len()).collect::<Vec<usize>>());
94 }
95 }
96 if outer.len() < 3 || pts.iter().any(|p| !p[0].is_finite() || !p[1].is_finite()) {
97 return Vec::new();
98 }
99 let mut ring: Vec<usize> = (0..outer.len()).collect();
100 let flip = signed_area(&pts, &ring) < 0.0;
101 if flip {
102 ring.reverse();
103 }
104 let fan = pts.len() > MAX_POINTS;
105 let mut tris = if fan {
106 (1..ring.len() - 1).map(|k| [ring[0], ring[k], ring[k + 1]]).collect()
107 } else {
108 for h in &mut hole_rings {
109 if signed_area(&pts, h) > 0.0 {
110 h.reverse();
111 }
112 }
113 hole_rings.retain(|h| signed_area(&pts, h) != 0.0);
114 hole_rings.sort_by(|a, b| {
115 let mx = |r: &Vec<usize>| r.iter().map(|&i| pts[i][0]).fold(f64::MIN, f64::max);
116 mx(b).total_cmp(&mx(a))
117 });
118 for k in 0..hole_rings.len() {
119 let (done, rest) = hole_rings.split_at(k + 1);
120 bridge(&pts, &mut ring, &done[k], rest);
121 }
122 clip(&pts, ring)
123 };
124 if flip {
125 for t in &mut tris {
126 t.swap(1, 2);
127 }
128 }
129 tris
130}
131
132fn clip(pts: &[P2], mut v: Vec<usize>) -> Vec<[usize; 3]> {
134 let (mut lo, mut hi) = ([f64::MAX; 2], [f64::MIN; 2]);
135 for &i in &v {
136 for k in 0..2 {
137 lo[k] = lo[k].min(pts[i][k]);
138 hi[k] = hi[k].max(pts[i][k]);
139 }
140 }
141 let size = (hi[0] - lo[0]).max(hi[1] - lo[1]);
142 let eps = size * size * 1e-12;
143 let mut tris = Vec::with_capacity(v.len().saturating_sub(2));
144 let mut i = 0;
145 let mut misses = 0;
146 while v.len() > 3 {
147 let n = v.len();
148 i %= n;
149 let (ia, ib, ic) = (v[(i + n - 1) % n], v[i], v[(i + 1) % n]);
150 let (a, b, c) = (pts[ia], pts[ib], pts[ic]);
151 let cr = cross(a, b, c);
152 if cr.abs() <= eps {
153 v.remove(i);
155 misses = 0;
156 continue;
157 }
158 let ear = cr > 0.0
159 && !v.iter().any(|&j| {
160 let p = pts[j];
161 p != a && p != b && p != c && cross(a, b, p) >= 0.0 && cross(b, c, p) >= 0.0 && cross(c, a, p) >= 0.0
162 });
163 if ear || misses > n {
164 tris.push([ia, ib, ic]);
167 v.remove(i);
168 misses = 0;
169 } else {
170 i += 1;
171 misses += 1;
172 }
173 }
174 if v.len() == 3 && cross(pts[v[0]], pts[v[1]], pts[v[2]]).abs() > eps {
175 tris.push([v[0], v[1], v[2]]);
176 }
177 tris
178}
179
180pub fn newell(pts: &[P3]) -> P3 {
182 let mut n = [0.0; 3];
183 for k in 0..pts.len() {
184 let (a, b) = (pts[k], pts[(k + 1) % pts.len()]);
185 n[0] += (a[1] - b[1]) * (a[2] + b[2]);
186 n[1] += (a[2] - b[2]) * (a[0] + b[0]);
187 n[2] += (a[0] - b[0]) * (a[1] + b[1]);
188 }
189 n
190}
191
192pub fn triangulate_3d(outer: &[P3], holes: &[&[P3]]) -> Vec<[usize; 3]> {
196 if outer.len() == 3 {
197 return vec![[0, 1, 2]];
198 }
199 let n = newell(outer);
200 let k = (0..3).max_by(|&a, &b| n[a].abs().total_cmp(&n[b].abs())).unwrap_or(2);
201 let (u, w) = ((k + 1) % 3, (k + 2) % 3);
203 let flat = |l: &[P3]| l.iter().map(|p| [p[u], p[w]]).collect::<Vec<P2>>();
204 let o = flat(outer);
205 let hs: Vec<Vec<P2>> = holes.iter().map(|h| flat(h)).collect();
206 let hr: Vec<&[P2]> = hs.iter().map(Vec::as_slice).collect();
207 triangulate_2d(&o, &hr)
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213
214 fn all_pts(outer: &[P2], holes: &[&[P2]]) -> Vec<P2> {
215 let mut v = outer.to_vec();
216 for h in holes {
217 v.extend_from_slice(h);
218 }
219 v
220 }
221
222 fn area(pts: &[P2], tris: &[[usize; 3]]) -> f64 {
224 tris.iter().map(|t| cross(pts[t[0]], pts[t[1]], pts[t[2]]) / 2.0).sum()
225 }
226
227 fn check(outer: &[P2], holes: &[&[P2]], want_area: f64, max_tris: usize) {
230 let tris = triangulate_2d(outer, holes);
231 let pts = all_pts(outer, holes);
232 assert!(tris.len() <= max_tris, "{tris:?}");
233 assert!((area(&pts, &tris) - want_area).abs() < 1e-9, "area {} want {want_area}", area(&pts, &tris));
234 let sign = want_area.signum();
236 for t in &tris {
237 assert!(cross(pts[t[0]], pts[t[1]], pts[t[2]]) * sign > 0.0, "{t:?}");
238 }
239 }
240
241 #[test]
242 fn concave_l_shape() {
243 let l = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]];
244 check(&l, &[], 3.0, 4);
245 let mut cw = l;
247 cw.reverse();
248 check(&cw, &[], -3.0, 4);
249 }
250
251 #[test]
252 fn concave_comb_and_star() {
253 let comb = [[0.0, 0.0], [5.0, 0.0], [5.0, 3.0], [4.0, 3.0], [4.0, 1.0], [3.0, 1.0], [3.0, 3.0], [2.0, 3.0], [2.0, 1.0], [1.0, 1.0], [1.0, 3.0], [0.0, 3.0]];
255 check(&comb, &[], 5.0 + 3.0 * 2.0, 10);
256 let star: Vec<P2> = (0..10)
258 .map(|k| {
259 let r = if k % 2 == 0 { 2.0 } else { 0.8 };
260 let a = std::f64::consts::PI * 2.0 * k as f64 / 10.0;
261 [r * a.cos(), r * a.sin()]
262 })
263 .collect();
264 let tris = triangulate_2d(&star, &[]);
265 assert_eq!(tris.len(), 8);
266 let want = 10.0 * 0.5 * 2.0 * 0.8 * (std::f64::consts::PI / 5.0).sin();
267 assert!((area(&star, &tris) - want).abs() < 1e-9);
268 }
269
270 #[test]
271 fn squares_with_holes() {
272 let outer = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
273 let hole = [[1.0, 1.0], [2.0, 1.0], [2.0, 2.0], [1.0, 2.0]];
274 check(&outer, &[&hole], 15.0, 8);
276 let h2 = [[3.0, 3.5], [3.5, 3.5], [3.5, 3.0], [3.0, 3.0]];
278 check(&outer, &[&hole, &h2], 16.0 - 1.0 - 0.25, 14);
279 let u = [[0.0, 0.0], [6.0, 0.0], [6.0, 4.0], [4.0, 4.0], [4.0, 2.0], [2.0, 2.0], [2.0, 4.0], [0.0, 4.0]];
281 let h = [[0.5, 0.5], [5.5, 0.5], [5.5, 1.5], [0.5, 1.5]];
282 check(&u, &[&h], 24.0 - 4.0 - 5.0, 12);
283 }
284
285 #[test]
286 fn collinear_and_degenerate_input() {
287 let sq = [[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [2.0, 2.0], [1.0, 2.0], [0.0, 2.0], [0.0, 1.0]];
289 let tris = triangulate_2d(&sq, &[]);
290 assert!((area(&sq, &tris) - 4.0).abs() < 1e-9);
291 assert!(triangulate_2d(&[[0.0, 0.0], [1.0, 0.0], [2.0, 0.0], [3.0, 0.0]], &[]).is_empty());
293 assert!(triangulate_2d(&[[0.0, 0.0], [1.0, 0.0]], &[]).is_empty());
294 assert!(triangulate_2d(&[[0.0, 0.0], [f64::NAN, 0.0], [1.0, 1.0]], &[]).is_empty());
295 let bow = [[0.0, 0.0], [2.0, 2.0], [2.0, 0.0], [0.0, 2.0]];
297 assert!(triangulate_2d(&bow, &[]).len() <= 2);
298 let rep = [[0.0, 0.0], [0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [1.0, 1.0], [0.0, 1.0]];
300 let tris = triangulate_2d(&rep, &[]);
301 assert!((area(&rep, &tris) - 1.0).abs() < 1e-9);
302 }
303
304 #[test]
305 fn faces_in_3d_keep_their_winding() {
306 let l: Vec<P3> = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [1.0, 1.0], [1.0, 2.0], [0.0, 2.0]].iter().map(|p| [p[0], 5.0, p[1]]).collect();
308 let n = newell(&l);
309 assert!(n[1] < 0.0);
310 let tris = triangulate_3d(&l, &[]);
311 assert_eq!(tris.len(), 4);
312 for t in &tris {
313 let [a, b, c] = t.map(|i| l[i]);
314 let tn = [(b[1] - a[1]) * (c[2] - a[2]) - (b[2] - a[2]) * (c[1] - a[1]), (b[2] - a[2]) * (c[0] - a[0]) - (b[0] - a[0]) * (c[2] - a[2]), (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])];
315 assert!(tn[0] * n[0] + tn[1] * n[1] + tn[2] * n[2] > 0.0);
316 }
317 let tilt = |p: P2| [p[0], p[1], 0.5 * p[0]];
319 let outer: Vec<P3> = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]].map(tilt).to_vec();
320 let hole: Vec<P3> = [[1.0, 1.0], [1.0, 2.0], [2.0, 2.0], [2.0, 1.0]].map(tilt).to_vec();
321 assert_eq!(triangulate_3d(&outer, &[&hole]).len(), 8);
322 }
323
324 #[test]
325 fn huge_faces_fall_back_to_a_fan() {
326 let n = MAX_POINTS + 10;
327 let ring: Vec<P2> = (0..n).map(|k| {
328 let a = std::f64::consts::TAU * k as f64 / n as f64;
329 [a.cos(), a.sin()]
330 }).collect();
331 assert_eq!(triangulate_2d(&ring, &[]).len(), n - 2);
332 }
333}