Skip to main content

falcon_library/model3d/
earclip.rs

1//! Triangulating flat polygons, concave ones and ones with holes, by ear
2//! clipping: each hole is joined to the outer loop by a bridge to a vertex
3//! it can see, then ears (convex corners with no other vertex inside) are
4//! cut off one by one. A 3D polygon is first projected onto the axis plane
5//! it faces most. Used by the IFC reader for faceted faces.
6
7pub type P2 = [f64; 2];
8pub type P3 = [f64; 3];
9
10/// Above this many vertices a face is cut as a fan of its outer loop, so
11/// a huge face can't take quadratic time.
12const 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
18/// Twice the signed area: positive when counter-clockwise.
19fn 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
27/// Whether segments ab and cd cross at a point inside both.
28fn 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
36/// Whether the direction from ring vertex `k` towards `q` points into the
37/// polygon (the ring runs counter-clockwise).
38fn 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        // Convex corner: q lies left of both edges.
43        cross(b, c, q) >= 0.0 && cross(a, b, q) >= 0.0
44    } else {
45        // Reflex corner: q is not in the outside wedge.
46        cross(b, c, q) >= 0.0 || cross(a, b, q) >= 0.0
47    }
48}
49
50/// Splices `hole` (clockwise) into `ring` (counter-clockwise) through a
51/// bridge from the hole's rightmost vertex to a ring vertex it can see.
52fn 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
82/// Triangulates a flat polygon with holes. The result indexes the outer
83/// loop's points followed by each hole's in turn, and runs the same way
84/// round as the outer loop. Degenerate input gives fewer (or no)
85/// triangles, never a panic.
86pub 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
132/// Cuts ears off a counter-clockwise ring until three corners are left.
133fn 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            // Collinear or doubled back: the corner adds no area.
154            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            // No ear in a whole lap means the ring self-intersects; cutting
165            // the corner anyway still ends.
166            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
180/// The Newell normal of a 3D loop (not normalised).
181pub 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
192/// Triangulates a flat (or nearly flat) 3D polygon with holes, projected
193/// onto the axis plane it faces most; indices as in [`triangulate_2d`],
194/// running the same way round as the outer loop.
195pub 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    // The other two axes in cyclic order keep the loop's turning sense.
202    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    /// The triangles' total signed area (counter-clockwise positive).
223    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    /// Checks the area covered and the winding; at most `max_tris`
228    /// triangles (fewer when collinear corners are dropped).
229    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        // Every triangle turns the same way as the outer loop.
235        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        // Clockwise input gives clockwise triangles.
246        let mut cw = l;
247        cw.reverse();
248        check(&cw, &[], -3.0, 4);
249    }
250
251    #[test]
252    fn concave_comb_and_star() {
253        // A comb with three teeth.
254        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        // A five-pointed star: ten corners, half of them reflex.
257        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        // n + h + 2 * holes - 2 triangles.
275        check(&outer, &[&hole], 15.0, 8);
276        // Two holes, one of them given clockwise already.
277        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        // A hole in a concave outline.
280        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        // A square with extra points along its sides.
288        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        // A line has no area and no triangles.
292        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        // A bow tie still ends.
296        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        // Repeated points.
299        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        // An L in the XZ plane (a wall), wound so its normal is −Y.
307        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        // With a hole, in a tilted plane.
318        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}