1use crate::{Point3f, TriangleMesh};
5use std::collections::{HashMap, VecDeque};
6
7#[derive(Debug, Clone, PartialEq)]
9pub struct MeshReport {
10 pub vertices: usize,
11 pub faces: usize,
12 pub unused_vertices: usize,
14 pub degenerate_faces: usize,
16 pub invalid_faces: usize,
19 pub edges: usize,
21 pub boundary_edges: usize,
23 pub non_manifold_edges: usize,
25 pub inconsistent_edges: usize,
28 pub holes: Vec<usize>,
31 pub components: Vec<usize>,
33 pub watertight: bool,
35 pub min: Point3f,
37 pub max: Point3f,
38 pub size: [f32; 3],
39 pub surface_area: f64,
41 pub volume: Option<f64>,
44 pub printable: bool,
46 pub problems: Vec<String>,
48}
49
50pub fn analyze_mesh(mesh: &TriangleMesh) -> MeshReport {
54 let n = mesh.vertices.len();
55 let valid: Vec<[usize; 3]> = mesh
56 .faces
57 .iter()
58 .copied()
59 .filter(|f| f.iter().all(|&v| v < n))
60 .collect();
61
62 let mut edge_uses: HashMap<(usize, usize), (u32, u32)> = HashMap::new();
65 let mut used = vec![false; n];
66 let mut degenerate = 0;
67 let mut area = 0.0f64;
68 let mut signed_volume = 0.0f64;
69 for f in &valid {
70 let [a, b, c] = f.map(|v| mesh.vertices[v].coords.cast::<f64>());
71 let cross = (b - a).cross(&(c - a));
72 let face_area = 0.5 * cross.norm();
73 if face_area <= f64::EPSILON * (b - a).norm().max((c - a).norm()).powi(2) {
74 degenerate += 1;
75 }
76 area += face_area;
77 signed_volume += a.dot(&b.cross(&c)) / 6.0;
78 f.iter().for_each(|&v| used[v] = true);
79 if f[0] == f[1] || f[1] == f[2] || f[0] == f[2] {
82 continue;
83 }
84 for (u, v) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] {
85 let entry = edge_uses.entry((u.min(v), u.max(v))).or_default();
86 if u < v {
87 entry.0 += 1;
88 } else {
89 entry.1 += 1;
90 }
91 }
92 }
93
94 let mut boundary = 0;
95 let mut non_manifold = 0;
96 let mut inconsistent = 0;
97 let mut next: HashMap<usize, Vec<usize>> = HashMap::new();
99 for (&(a, b), &(forward, backward)) in &edge_uses {
100 match forward + backward {
101 1 => {
102 boundary += 1;
103 let (from, to) = if forward == 1 { (a, b) } else { (b, a) };
104 next.entry(from).or_default().push(to);
105 }
106 2 if forward != 1 => inconsistent += 1,
107 2 => {}
108 _ => non_manifold += 1,
109 }
110 }
111
112 let mut holes = Vec::new();
114 let mut starts: Vec<usize> = next.keys().copied().collect();
115 starts.sort_unstable();
116 for start in starts {
117 while next.get(&start).is_some_and(|out| !out.is_empty()) {
118 let mut length = 0;
119 let mut at = start;
120 while let Some(to) = next.get_mut(&at).and_then(Vec::pop) {
121 length += 1;
122 at = to;
123 if at == start {
124 break;
125 }
126 }
127 holes.push(length);
128 }
129 }
130 holes.sort_unstable_by(|a, b| b.cmp(a));
131
132 let mut components: Vec<usize> = mesh_pieces(&valid).iter().map(Vec::len).collect();
133 components.sort_unstable_by(|a, b| b.cmp(a));
134
135 let (min, max) = bounds(mesh, &used);
136 let size = [max.x - min.x, max.y - min.y, max.z - min.z];
137
138 let watertight = !valid.is_empty() && boundary == 0 && non_manifold == 0;
139 let oriented = inconsistent == 0;
140 let volume = (watertight && oriented).then_some(signed_volume.abs());
141
142 let mut problems = Vec::new();
143 if valid.is_empty() {
144 problems.push("the mesh has no faces".to_string());
145 }
146 if !holes.is_empty() {
147 problems.push(format!(
148 "{} hole(s) in the surface ({} open edges)",
149 holes.len(),
150 boundary
151 ));
152 }
153 if non_manifold > 0 {
154 problems.push(format!(
155 "{non_manifold} edge(s) shared by more than two faces"
156 ));
157 }
158 if !oriented {
159 problems.push(format!(
160 "{inconsistent} edge(s) between faces that face opposite ways"
161 ));
162 } else if watertight && signed_volume < 0.0 {
163 problems.push("all faces point inward (the mesh is inside out)".to_string());
164 }
165 if mesh.faces.len() != valid.len() {
166 problems.push(format!(
167 "{} face(s) use vertex indices past the vertex list",
168 mesh.faces.len() - valid.len()
169 ));
170 }
171
172 MeshReport {
173 vertices: n,
174 faces: mesh.faces.len(),
175 unused_vertices: used.iter().filter(|&&u| !u).count(),
176 degenerate_faces: degenerate,
177 invalid_faces: mesh.faces.len() - valid.len(),
178 edges: edge_uses.len(),
179 boundary_edges: boundary,
180 non_manifold_edges: non_manifold,
181 inconsistent_edges: inconsistent,
182 holes,
183 components,
184 watertight,
185 min,
186 max,
187 size,
188 surface_area: area,
189 volume,
190 printable: problems.is_empty(),
191 problems,
192 }
193}
194
195pub fn mesh_pieces(faces: &[[usize; 3]]) -> Vec<Vec<usize>> {
199 let mut by_edge: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
200 for (i, f) in faces.iter().enumerate() {
201 for (a, b) in [(f[0], f[1]), (f[1], f[2]), (f[2], f[0])] {
202 by_edge.entry((a.min(b), a.max(b))).or_default().push(i);
203 }
204 }
205 let mut piece_of = vec![usize::MAX; faces.len()];
206 let mut out: Vec<Vec<usize>> = Vec::new();
207 for start in 0..faces.len() {
208 if piece_of[start] != usize::MAX {
209 continue;
210 }
211 let id = out.len();
212 let mut members = vec![start];
213 piece_of[start] = id;
214 let mut queue = VecDeque::from([start]);
215 while let Some(f) = queue.pop_front() {
216 let face = faces[f];
217 for (a, b) in [(face[0], face[1]), (face[1], face[2]), (face[2], face[0])] {
218 for &g in &by_edge[&(a.min(b), a.max(b))] {
219 if piece_of[g] == usize::MAX {
220 piece_of[g] = id;
221 members.push(g);
222 queue.push_back(g);
223 }
224 }
225 }
226 }
227 out.push(members);
228 }
229 out
230}
231
232fn bounds(mesh: &TriangleMesh, used: &[bool]) -> (Point3f, Point3f) {
234 let mut points = mesh
235 .vertices
236 .iter()
237 .zip(used)
238 .filter(|(p, &u)| u && p.coords.iter().all(|c| c.is_finite()))
239 .map(|(p, _)| *p);
240 let Some(first) = points.next() else {
241 return (Point3f::origin(), Point3f::origin());
242 };
243 points.fold((first, first), |(lo, hi), p| {
244 (
245 Point3f::new(lo.x.min(p.x), lo.y.min(p.y), lo.z.min(p.z)),
246 Point3f::new(hi.x.max(p.x), hi.y.max(p.y), hi.z.max(p.z)),
247 )
248 })
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 fn cube() -> TriangleMesh {
257 let v = (0..8)
258 .map(|i| Point3f::new((i & 1) as f32, ((i >> 1) & 1) as f32, ((i >> 2) & 1) as f32))
259 .collect();
260 let faces = vec![
261 [0, 2, 1],
262 [1, 2, 3], [4, 5, 6],
264 [5, 7, 6], [0, 1, 4],
266 [1, 5, 4], [2, 6, 3],
268 [3, 6, 7], [0, 4, 2],
270 [2, 4, 6], [1, 3, 5],
272 [3, 7, 5], ];
274 TriangleMesh::from_vertices_and_faces(v, faces)
275 }
276
277 #[test]
278 fn closed_cube_is_printable_with_exact_measurements() {
279 let r = analyze_mesh(&cube());
280 assert!(r.watertight && r.printable, "{:?}", r.problems);
281 assert_eq!(
282 (r.edges, r.boundary_edges, r.non_manifold_edges),
283 (18, 0, 0)
284 );
285 assert!(r.holes.is_empty());
286 assert_eq!(r.components, vec![12]);
287 assert!((r.surface_area - 6.0).abs() < 1e-9);
288 assert!((r.volume.unwrap() - 1.0).abs() < 1e-9);
289 assert_eq!(r.size, [1.0, 1.0, 1.0]);
290 }
291
292 #[test]
293 fn missing_faces_are_reported_as_one_hole() {
294 let mut mesh = cube();
295 mesh.faces.truncate(10); let r = analyze_mesh(&mesh);
297 assert!(!r.watertight && !r.printable);
298 assert_eq!(r.holes, vec![4]);
299 assert_eq!(r.volume, None);
300 assert!(r.problems[0].contains("1 hole"), "{:?}", r.problems);
301 }
302
303 #[test]
304 fn inside_out_and_flipped_faces_are_caught() {
305 let mut inside_out = cube();
306 for f in inside_out.faces.iter_mut() {
307 f.swap(1, 2);
308 }
309 let r = analyze_mesh(&inside_out);
310 assert!(r.watertight && !r.printable);
311 assert!(
312 (r.volume.unwrap() - 1.0).abs() < 1e-9,
313 "volume is still the size"
314 );
315 assert!(r.problems.iter().any(|p| p.contains("inside out")));
316
317 let mut one_flipped = cube();
318 one_flipped.faces[0].swap(1, 2);
319 let r = analyze_mesh(&one_flipped);
320 assert_eq!(r.inconsistent_edges, 3);
321 assert_eq!(r.volume, None);
322 assert!(!r.printable);
323 }
324
325 #[test]
326 fn non_manifold_pieces_degenerate_and_bad_indices_are_counted() {
327 let mut mesh = cube();
328 mesh.vertices.extend([
331 Point3f::new(0.5, -1.0, 0.0),
332 Point3f::new(5.0, 5.0, 5.0),
333 Point3f::new(6.0, 5.0, 5.0),
334 Point3f::new(5.0, 6.0, 5.0),
335 Point3f::new(9.0, 9.0, 9.0), ]);
337 mesh.faces
338 .extend([[0, 1, 8], [9, 10, 11], [9, 9, 10], [0, 1, 99]]);
339 let r = analyze_mesh(&mesh);
340 assert_eq!(r.non_manifold_edges, 1);
341 assert_eq!(r.components, vec![13, 2]);
342 assert_eq!(r.degenerate_faces, 1);
343 assert_eq!(r.invalid_faces, 1);
344 assert_eq!(r.unused_vertices, 1);
345 assert!(!r.watertight && !r.printable);
346 }
347
348 #[test]
349 fn empty_mesh_is_not_printable() {
350 let r = analyze_mesh(&TriangleMesh::new());
351 assert!(!r.printable && !r.watertight);
352 assert_eq!(r.volume, None);
353 assert_eq!(r.size, [0.0, 0.0, 0.0]);
354 }
355
356 #[test]
357 fn faces_touching_at_one_corner_are_separate_pieces() {
358 let v = vec![
360 Point3f::new(0.0, 0.0, 0.0),
361 Point3f::new(1.0, 0.0, 0.0),
362 Point3f::new(0.0, 1.0, 0.0),
363 Point3f::new(-1.0, 0.0, 0.0),
364 Point3f::new(0.0, -1.0, 0.0),
365 ];
366 let mesh = TriangleMesh::from_vertices_and_faces(v, vec![[0, 1, 2], [0, 3, 4]]);
367 assert_eq!(analyze_mesh(&mesh).components, vec![1, 1]);
368 assert_eq!(mesh_pieces(&mesh.faces), vec![vec![0], vec![1]]);
369 }
370}