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().is_multiple_of(3) {
85 return Err(DecimateError::RaggedIndices(mesh.indices.len()));
86 }
87 let vertex_count = mesh.positions.len();
88 for (t, chunk) in mesh.indices.as_chunks::<3>().0.iter().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 .as_chunks::<3>()
130 .0
131 .iter()
132 .map(|c| [c[0], c[1], c[2]])
133 .collect();
134
135 let mut candidates: Vec<(u32, u32)> = unique_edges(&triangles).into_iter().collect();
139 candidates.sort_by(|a, b| {
140 let la = (positions[a.0 as usize] - positions[a.1 as usize]).length();
141 let lb = (positions[b.0 as usize] - positions[b.1 as usize]).length();
142 la.partial_cmp(&lb)
143 .unwrap_or(std::cmp::Ordering::Equal)
144 .then(a.cmp(b))
145 });
146
147 let mut report = DecimateReport {
148 input_triangles,
149 output_triangles: input_triangles,
150 collapses: 0,
151 rejected_unsafe: 0,
152 rejected_deviation: 0,
153 max_deviation: 0.0,
154 };
155 let mut moved = vec![0.0_f64; positions.len()];
156 let mut alive: Vec<bool> = vec![true; positions.len()];
157
158 for (u, v) in candidates {
159 if triangles.len() <= budget.max(4) && budget > 0 {
160 break;
161 }
162 if !alive[u as usize] || !alive[v as usize] {
163 continue;
164 }
165 let midpoint = (positions[u as usize] + positions[v as usize]) / 2.0;
166
167 let deviation = (midpoint - positions[u as usize])
171 .length()
172 .max((midpoint - positions[v as usize]).length())
173 + moved[u as usize].max(moved[v as usize]);
174 if deviation > bound {
175 report.rejected_deviation += 1;
176 continue;
177 }
178
179 match try_collapse(&triangles, &positions, u, v, midpoint) {
180 Some(next) => {
181 triangles = next;
182 positions[u as usize] = midpoint;
183 moved[u as usize] = deviation;
184 alive[v as usize] = false;
185 report.collapses += 1;
186 report.max_deviation = report.max_deviation.max(deviation);
187 }
188 None => report.rejected_unsafe += 1,
189 }
190 }
191
192 report.output_triangles = triangles.len();
193 Ok((compact(&triangles, &positions), report))
194}
195
196fn try_collapse(
211 triangles: &[[u32; 3]],
212 positions: &[Point3],
213 u: u32,
214 v: u32,
215 midpoint: Point3,
216) -> Option<Vec<[u32; 3]>> {
217 let nu = neighbours(triangles, u);
221 let nv = neighbours(triangles, v);
222 let shared = nu.intersection(&nv).count();
223 if shared != 2 {
224 return None;
225 }
226
227 let mut next = Vec::with_capacity(triangles.len());
228 for &t in triangles {
229 let touches_u = t.contains(&u);
230 let touches_v = t.contains(&v);
231 if touches_u && touches_v {
232 continue;
234 }
235 let mapped = t.map(|c| if c == v { u } else { c });
236 if touches_u || touches_v {
237 let before = normal(positions, t);
238 let after = normal_with(positions, mapped, u, midpoint);
239 if after.length_squared() == 0.0 || before.dot(after) <= 0.0 {
243 return None;
244 }
245 }
246 next.push(mapped);
247 }
248 Some(next)
249}
250
251fn neighbours(triangles: &[[u32; 3]], vertex: u32) -> BTreeSet<u32> {
253 let mut set = BTreeSet::new();
254 for t in triangles {
255 if t.contains(&vertex) {
256 for &c in t {
257 if c != vertex {
258 set.insert(c);
259 }
260 }
261 }
262 }
263 set
264}
265
266fn unique_edges(triangles: &[[u32; 3]]) -> BTreeSet<(u32, u32)> {
268 let mut set = BTreeSet::new();
269 for t in triangles {
270 for (a, b) in [(t[0], t[1]), (t[1], t[2]), (t[2], t[0])] {
271 set.insert((a.min(b), a.max(b)));
272 }
273 }
274 set
275}
276
277fn normal(positions: &[Point3], t: [u32; 3]) -> axiolid_core::Vec3 {
279 let a = positions[t[0] as usize];
280 let b = positions[t[1] as usize];
281 let c = positions[t[2] as usize];
282 (b - a).cross(c - a)
283}
284
285fn normal_with(
287 positions: &[Point3],
288 t: [u32; 3],
289 moved_index: u32,
290 moved_to: Point3,
291) -> axiolid_core::Vec3 {
292 let at = |i: u32| {
293 if i == moved_index {
294 moved_to
295 } else {
296 positions[i as usize]
297 }
298 };
299 let (a, b, c) = (at(t[0]), at(t[1]), at(t[2]));
300 (b - a).cross(c - a)
301}
302
303fn compact(triangles: &[[u32; 3]], positions: &[Point3]) -> TriMesh {
309 let mut remap: BTreeMap<u32, u32> = BTreeMap::new();
310 let mut kept = Vec::new();
311 let mut indices = Vec::with_capacity(triangles.len() * 3);
312 for t in triangles {
313 for &corner in t {
314 let next = u32::try_from(kept.len()).unwrap_or(u32::MAX);
315 let slot = *remap.entry(corner).or_insert_with(|| {
316 kept.push(positions[corner as usize]);
317 next
318 });
319 indices.push(slot);
320 }
321 }
322 TriMesh::new(kept, indices)
323}