1use axiolid_core::{Point3, Tolerance};
4use axiolid_mesh::TriMesh;
5use std::collections::{BTreeMap, BTreeSet};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
10#[non_exhaustive]
11pub enum DecimateTarget {
12 TriangleBudget(usize),
17 MaxDeviation(f64),
19}
20
21#[derive(Debug, Clone, PartialEq, Error)]
23#[non_exhaustive]
24pub enum DecimateError {
25 #[error("index buffer length {0} is not a multiple of 3")]
27 RaggedIndices(usize),
28 #[error("triangle {0} references vertex {1}, which is out of range")]
30 IndexOutOfRange(usize, u32),
31 #[error("deviation bound {0} is not a positive finite length")]
33 InvalidBound(f64),
34}
35
36#[derive(Debug, Clone, PartialEq)]
42#[non_exhaustive]
43pub struct DecimateReport {
44 pub input_triangles: usize,
46 pub output_triangles: usize,
48 pub collapses: usize,
50 pub rejected_unsafe: usize,
55 pub rejected_deviation: usize,
57 pub max_deviation: f64,
59}
60
61impl DecimateReport {
62 pub fn is_noop(&self) -> bool {
64 self.collapses == 0
65 }
66}
67
68pub fn decimate(
80 mesh: &TriMesh,
81 target: DecimateTarget,
82 tolerance: Tolerance,
83) -> Result<(TriMesh, DecimateReport), DecimateError> {
84 if mesh.indices.len() % 3 != 0 {
85 return Err(DecimateError::RaggedIndices(mesh.indices.len()));
86 }
87 let vertex_count = mesh.positions.len();
88 for (t, chunk) in mesh.indices.chunks_exact(3).enumerate() {
89 for &index in chunk {
90 if index as usize >= vertex_count {
91 return Err(DecimateError::IndexOutOfRange(t, index));
92 }
93 }
94 }
95
96 let bound = match target {
97 DecimateTarget::MaxDeviation(d) => {
98 if !d.is_finite() || d <= 0.0 {
99 return Err(DecimateError::InvalidBound(d));
100 }
101 d
102 }
103 DecimateTarget::TriangleBudget(_) => tolerance.linear(),
106 };
107
108 let budget = match target {
109 DecimateTarget::TriangleBudget(n) => n,
110 DecimateTarget::MaxDeviation(_) => 0,
111 };
112
113 run(mesh, budget, bound)
114}
115
116fn run(
121 mesh: &TriMesh,
122 budget: usize,
123 bound: f64,
124) -> Result<(TriMesh, DecimateReport), DecimateError> {
125 let input_triangles = mesh.indices.len() / 3;
126 let mut positions = mesh.positions.clone();
127 let mut triangles: Vec<[u32; 3]> = mesh
128 .indices
129 .chunks_exact(3)
130 .map(|c| [c[0], c[1], c[2]])
131 .collect();
132
133 let mut candidates: Vec<(u32, u32)> = unique_edges(&triangles).into_iter().collect();
137 candidates.sort_by(|a, b| {
138 let la = (positions[a.0 as usize] - positions[a.1 as usize]).length();
139 let lb = (positions[b.0 as usize] - positions[b.1 as usize]).length();
140 la.partial_cmp(&lb)
141 .unwrap_or(std::cmp::Ordering::Equal)
142 .then(a.cmp(b))
143 });
144
145 let mut report = DecimateReport {
146 input_triangles,
147 output_triangles: input_triangles,
148 collapses: 0,
149 rejected_unsafe: 0,
150 rejected_deviation: 0,
151 max_deviation: 0.0,
152 };
153 let mut moved = vec![0.0_f64; positions.len()];
154 let mut alive: Vec<bool> = vec![true; positions.len()];
155
156 for (u, v) in candidates {
157 if triangles.len() <= budget.max(4) && budget > 0 {
158 break;
159 }
160 if !alive[u as usize] || !alive[v as usize] {
161 continue;
162 }
163 let midpoint = (positions[u as usize] + positions[v as usize]) / 2.0;
164
165 let deviation = (midpoint - positions[u as usize])
169 .length()
170 .max((midpoint - positions[v as usize]).length())
171 + moved[u as usize].max(moved[v as usize]);
172 if deviation > bound {
173 report.rejected_deviation += 1;
174 continue;
175 }
176
177 match try_collapse(&triangles, &positions, u, v, midpoint) {
178 Some(next) => {
179 triangles = next;
180 positions[u as usize] = midpoint;
181 moved[u as usize] = deviation;
182 alive[v as usize] = false;
183 report.collapses += 1;
184 report.max_deviation = report.max_deviation.max(deviation);
185 }
186 None => report.rejected_unsafe += 1,
187 }
188 }
189
190 report.output_triangles = triangles.len();
191 Ok((compact(&triangles, &positions), report))
192}
193
194fn try_collapse(
209 triangles: &[[u32; 3]],
210 positions: &[Point3],
211 u: u32,
212 v: u32,
213 midpoint: Point3,
214) -> Option<Vec<[u32; 3]>> {
215 let nu = neighbours(triangles, u);
219 let nv = neighbours(triangles, v);
220 let shared = nu.intersection(&nv).count();
221 if shared != 2 {
222 return None;
223 }
224
225 let mut next = Vec::with_capacity(triangles.len());
226 for &t in triangles {
227 let touches_u = t.contains(&u);
228 let touches_v = t.contains(&v);
229 if touches_u && touches_v {
230 continue;
232 }
233 let mapped = t.map(|c| if c == v { u } else { c });
234 if touches_u || touches_v {
235 let before = normal(positions, t);
236 let after = normal_with(positions, mapped, u, midpoint);
237 if after.length_squared() == 0.0 || before.dot(after) <= 0.0 {
241 return None;
242 }
243 }
244 next.push(mapped);
245 }
246 Some(next)
247}
248
249fn neighbours(triangles: &[[u32; 3]], vertex: u32) -> BTreeSet<u32> {
251 let mut set = BTreeSet::new();
252 for t in triangles {
253 if t.contains(&vertex) {
254 for &c in t {
255 if c != vertex {
256 set.insert(c);
257 }
258 }
259 }
260 }
261 set
262}
263
264fn unique_edges(triangles: &[[u32; 3]]) -> BTreeSet<(u32, u32)> {
266 let mut set = BTreeSet::new();
267 for t in triangles {
268 for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
269 set.insert((a.min(b), a.max(b)));
270 }
271 }
272 set
273}
274
275fn normal(positions: &[Point3], t: [u32; 3]) -> axiolid_core::Vec3 {
277 let a = positions[t[0] as usize];
278 let b = positions[t[1] as usize];
279 let c = positions[t[2] as usize];
280 (b - a).cross(c - a)
281}
282
283fn normal_with(
285 positions: &[Point3],
286 t: [u32; 3],
287 moved_index: u32,
288 moved_to: Point3,
289) -> axiolid_core::Vec3 {
290 let at = |i: u32| {
291 if i == moved_index {
292 moved_to
293 } else {
294 positions[i as usize]
295 }
296 };
297 let (a, b, c) = (at(t[0]), at(t[1]), at(t[2]));
298 (b - a).cross(c - a)
299}
300
301fn compact(triangles: &[[u32; 3]], positions: &[Point3]) -> TriMesh {
307 let mut remap: BTreeMap<u32, u32> = BTreeMap::new();
308 let mut kept = Vec::new();
309 let mut indices = Vec::with_capacity(triangles.len() * 3);
310 for t in triangles {
311 for &corner in t {
312 let next = u32::try_from(kept.len()).unwrap_or(u32::MAX);
313 let slot = *remap.entry(corner).or_insert_with(|| {
314 kept.push(positions[corner as usize]);
315 next
316 });
317 indices.push(slot);
318 }
319 }
320 TriMesh::new(kept, indices)
321}