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}