Skip to main content

axiolid_mesh/
split.rs

1//! Re-triangulating a triangle whose sides carry extra corners.
2//!
3//! Index-only: the corners are caller-chosen indices, and collinearity is
4//! decided by which side a corner lies on, never from coordinates. The caller
5//! guarantees that a corner listed on a side lies on that side, strictly
6//! between its ends and in order.
7
8/// Triangulate a proper triangle `t` whose side `i` (from `t[i]` to
9/// `t[i+1]`) carries the corners `sides[i]` in order, into proper triangles
10/// with the same winding, appended to `out` as index triples.
11///
12/// The triangle plus its side corners is a convex polygon whose points all
13/// lie on three lines. Collinearity is decided from which sides a point is
14/// on, not from coordinates: `t[i]` is on sides `i` and `i-1`, a side
15/// corner only on its own side, and three points are collinear exactly when
16/// they share a side. Ears are clipped while one exists whose three points
17/// are not collinear and whose removal leaves a polygon that is not
18/// collinear either; clipping such an ear from a convex polygon leaves a
19/// convex polygon, so the loop always finds the next one and ends at a
20/// proper triangle. `O(k^2)` for `k` points, and `k` is small. With no
21/// corner on any side, `t` itself is appended.
22///
23/// Returns `false`, leaving `out` as it was, when no ear exists. The side
24/// masks this function builds itself never leave the loop without one, so it
25/// never returns `false` for them: properness of `t` (three distinct,
26/// non-collinear corners) is the caller's precondition, and `false` reports
27/// that it was violated. A caller then declines or keeps `t` unsplit; no
28/// degenerate piece is ever emitted.
29#[must_use]
30pub fn split_triangle_at_side_corners(
31    t: [usize; 3],
32    sides: &[Vec<usize>; 3],
33    out: &mut Vec<usize>,
34) -> bool {
35    let start = out.len();
36    // (index, side-membership bitmask)
37    let mut ring: Vec<(usize, u8)> =
38        Vec::with_capacity(3 + sides.iter().map(Vec::len).sum::<usize>());
39    for i in 0..3 {
40        ring.push((t[i], (1 << i) | (1 << ((i + 2) % 3))));
41        ring.extend(sides[i].iter().map(|&c| (c, 1u8 << i)));
42    }
43    let collinear =
44        |masks: &mut dyn Iterator<Item = u8>| masks.fold(0b111u8, |acc, m| acc & m) != 0;
45    while ring.len() > 3 {
46        let n = ring.len();
47        // Clip the first corner whose removal leaves a ring that is not
48        // all on one line. That alone keeps every emitted ear proper: the
49        // ring is a triangle with corners on its sides, so the first such
50        // corner always joins two different lines (checked exhaustively
51        // for up to four corners per side in the tests below).
52        let ear = (0..n).find(|&k| {
53            !collinear(
54                &mut ring
55                    .iter()
56                    .enumerate()
57                    .filter(|&(j, _)| j != k)
58                    .map(|(_, p)| p.1),
59            )
60        });
61        let Some(k) = ear else {
62            out.truncate(start);
63            return false;
64        };
65        let (prev, next) = ((k + n - 1) % n, (k + 1) % n);
66        out.extend_from_slice(&[ring[prev].0, ring[k].0, ring[next].0]);
67        ring.remove(k);
68    }
69    out.extend_from_slice(&[ring[0].0, ring[1].0, ring[2].0]);
70    true
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    /// A right triangle with legs of 5 so integer corners fit on each side:
78    /// the corner of side `i` at step `j` of `m` is `t[i] + (t[i+1]-t[i])*j/(m+1)`.
79    const T: [[f64; 2]; 3] = [[0.0, 0.0], [5.0, 0.0], [0.0, 5.0]];
80
81    fn area2(p: [f64; 2], q: [f64; 2], r: [f64; 2]) -> f64 {
82        (q[0] - p[0]) * (r[1] - p[1]) - (r[0] - p[0]) * (q[1] - p[1])
83    }
84
85    /// Lay out the triangle and `counts[i]` corners on side `i`; returns the
86    /// point table (triangle corners first) and the side lists.
87    fn layout(counts: [usize; 3]) -> (Vec<[f64; 2]>, [Vec<usize>; 3]) {
88        let mut points = T.to_vec();
89        let mut sides: [Vec<usize>; 3] = Default::default();
90        for i in 0..3 {
91            let (a, b) = (T[i], T[(i + 1) % 3]);
92            for j in 1..=counts[i] {
93                let f = j as f64 / (counts[i] + 1) as f64;
94                sides[i].push(points.len());
95                points.push([a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f]);
96            }
97        }
98        (points, sides)
99    }
100
101    #[test]
102    fn a_triangle_without_corners_is_kept() {
103        let mut out = Vec::new();
104        assert!(split_triangle_at_side_corners(
105            [7, 8, 9],
106            &Default::default(),
107            &mut out
108        ));
109        assert_eq!(out, [7, 8, 9]);
110    }
111
112    /// Every mix of 0..=4 corners per side: only proper triangles with the
113    /// parent's winding, covering exactly its area, `k + 1` of them, using
114    /// every corner and no collinear piece, appended after existing output.
115    #[test]
116    fn every_mix_of_corners_gives_a_proper_cover_with_the_same_winding() {
117        for counts in (0..125).map(|n| [n % 5, n / 5 % 5, n / 25]) {
118            let (points, sides) = layout(counts);
119            let mut out = vec![99];
120            assert!(split_triangle_at_side_corners([0, 1, 2], &sides, &mut out));
121            assert_eq!(out[0], 99, "{counts:?}: existing output kept");
122            let pieces = &out[1..];
123            let corners: usize = counts.iter().sum();
124            assert_eq!(pieces.len(), 3 * (corners + 1), "{counts:?}");
125            let parent = area2(T[0], T[1], T[2]);
126            let mut total = 0.0;
127            let mut seen = vec![false; points.len()];
128            for piece in pieces.chunks(3) {
129                let area = area2(points[piece[0]], points[piece[1]], points[piece[2]]);
130                assert!(area * parent > 0.0, "{counts:?}: {piece:?} is not proper");
131                total += area;
132                piece.iter().for_each(|&v| seen[v] = true);
133            }
134            assert!((total - parent).abs() < 1e-9, "{counts:?}: area covered");
135            assert!(seen.iter().all(|&s| s), "{counts:?}: every corner used");
136        }
137    }
138
139    /// The output order is part of the contract.
140    #[test]
141    fn the_pieces_come_in_a_fixed_order() {
142        let (_, sides) = layout([1, 0, 0]);
143        let mut out = Vec::new();
144        assert!(split_triangle_at_side_corners([0, 1, 2], &sides, &mut out));
145        assert_eq!(out, [2, 0, 3, 3, 1, 2]);
146    }
147
148    /// A reversed triangle keeps its winding: the same call on mirrored
149    /// indices emits pieces of the same (negative) orientation.
150    #[test]
151    fn a_clockwise_triangle_stays_clockwise() {
152        let (points, sides) = layout([2, 1, 3]);
153        let mut out = Vec::new();
154        // Mirror: t = [0, 2, 1] with sides reversed accordingly.
155        let mirrored: [Vec<usize>; 3] = [
156            sides[2].iter().rev().copied().collect(),
157            sides[1].iter().rev().copied().collect(),
158            sides[0].iter().rev().copied().collect(),
159        ];
160        assert!(split_triangle_at_side_corners(
161            [0, 2, 1],
162            &mirrored,
163            &mut out
164        ));
165        assert!(out
166            .chunks(3)
167            .all(|p| area2(points[p[0]], points[p[1]], points[p[2]]) < 0.0));
168    }
169}