1use crate::numerical;
2use crate::{RecognitionError, SurfaceHint, Vec3};
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
7pub struct Mesh {
9 pub vertices: Vec<Vec3>,
11 pub triangles: Vec<[u32; 3]>,
13 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub vertex_normals: Option<Vec<Vec3>>,
17 #[serde(default)]
18 pub source_metadata: Vec<SourceMetadata>,
20}
21
22impl Mesh {
23 pub fn new(vertices: Vec<Vec3>, triangles: Vec<[u32; 3]>) -> Self {
25 Self {
26 vertices,
27 triangles,
28 vertex_normals: None,
29 source_metadata: Vec::new(),
30 }
31 }
32
33 pub fn with_vertex_normals(mut self, normals: Vec<Vec3>) -> Self {
36 self.vertex_normals = Some(normals);
37 self
38 }
39 pub fn analyze(&self, options: &MeshAnalysisOptions) -> Result<AnalyzedMesh, RecognitionError> {
41 AnalyzedMesh::new(self, options)
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
48pub struct SourceMetadata {
49 pub version: u32,
51 pub triangle_indices: Vec<usize>,
53 pub hint: SurfaceHint,
55 pub source_face_id: Option<u64>,
57 pub source_face_name: Option<String>,
59 pub source_surface_id: Option<String>,
61 pub orientation: Option<i8>,
63 pub source_tolerance: Option<f64>,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
68#[serde(default)]
69pub struct MeshAnalysisOptions {
71 pub minimum_triangle_area: f64,
73 pub feature_angle: f64,
75}
76impl Default for MeshAnalysisOptions {
77 fn default() -> Self {
78 Self {
79 minimum_triangle_area: 0.0,
80 feature_angle: 30_f64.to_radians(),
81 }
82 }
83}
84
85#[derive(Clone, Debug, PartialEq)]
86pub struct TriangleData {
88 pub vertices: [usize; 3],
90 pub centroid: Vec3,
92 pub normal: Vec3,
94 pub area: f64,
96 pub neighbors: [Option<usize>; 3],
98 pub feature_edges: [bool; 3],
100}
101
102#[derive(Clone, Debug)]
103pub struct AnalyzedMesh {
105 pub vertices: Vec<Vec3>,
107 pub vertex_normals: Option<Vec<Vec3>>,
109 pub triangles: Vec<TriangleData>,
111 pub bbox_min: Vec3,
113 pub bbox_max: Vec3,
115 pub diagonal: f64,
117 pub vertex_area_weights: Vec<f64>,
119 pub source_metadata: Vec<SourceMetadata>,
121 pub degenerate_triangles: Vec<usize>,
123}
124
125impl AnalyzedMesh {
126 pub fn new(mesh: &Mesh, options: &MeshAnalysisOptions) -> Result<Self, RecognitionError> {
128 if !options.minimum_triangle_area.is_finite() || options.minimum_triangle_area < 0.0 {
129 return Err(RecognitionError::InvalidOptions(
130 "minimum_triangle_area must be finite and non-negative".into(),
131 ));
132 }
133 if !(0.0..=std::f64::consts::PI).contains(&options.feature_angle) {
134 return Err(RecognitionError::InvalidOptions(
135 "feature_angle must be finite and in [0, pi]".into(),
136 ));
137 }
138 if mesh.vertices.is_empty() {
139 return Err(RecognitionError::InvalidMesh("no vertices".into()));
140 }
141 if mesh.triangles.is_empty() {
142 return Err(RecognitionError::InvalidMesh("no triangles".into()));
143 }
144 if mesh.vertices.iter().any(|v| !v.is_finite()) {
145 return Err(RecognitionError::InvalidMesh(
146 "vertex coordinates must be finite".into(),
147 ));
148 }
149 let vertex_normals = match &mesh.vertex_normals {
150 None => None,
151 Some(normals) => {
152 if normals.len() != mesh.vertices.len() {
153 return Err(RecognitionError::InvalidMesh(format!(
154 "vertex-normal buffer has {} entries for {} vertices",
155 normals.len(),
156 mesh.vertices.len()
157 )));
158 }
159 let mut normalized = Vec::with_capacity(normals.len());
160 for (index, &normal) in normals.iter().enumerate() {
161 if !normal.is_finite() {
162 return Err(RecognitionError::InvalidMesh(format!(
163 "vertex normal {index} is not finite"
164 )));
165 }
166 normalized.push(normal.normalized().ok_or_else(|| {
167 RecognitionError::InvalidMesh(format!(
168 "vertex normal {index} has zero length"
169 ))
170 })?);
171 }
172 Some(normalized)
173 }
174 };
175 let mut bbox_min = mesh.vertices[0];
176 let mut bbox_max = mesh.vertices[0];
177 for p in &mesh.vertices[1..] {
178 bbox_min.x = bbox_min.x.min(p.x);
179 bbox_min.y = bbox_min.y.min(p.y);
180 bbox_min.z = bbox_min.z.min(p.z);
181 bbox_max.x = bbox_max.x.max(p.x);
182 bbox_max.y = bbox_max.y.max(p.y);
183 bbox_max.z = bbox_max.z.max(p.z);
184 }
185 let diagonal = (bbox_max - bbox_min).length();
186 if diagonal <= 0.0 {
187 return Err(RecognitionError::InvalidMesh(
188 "zero-size bounding box".into(),
189 ));
190 }
191 let mut triangles = Vec::with_capacity(mesh.triangles.len());
192 let mut degenerate = Vec::new();
193 let mut vertex_area_weights = vec![0.0; mesh.vertices.len()];
194 for (id, raw) in mesh.triangles.iter().enumerate() {
195 let vi = raw.map(|x| x as usize);
196 if vi.iter().any(|&x| x >= mesh.vertices.len()) {
197 return Err(RecognitionError::InvalidMesh(format!(
198 "triangle {id} has an out-of-range vertex"
199 )));
200 }
201 if vi[0] == vi[1] || vi[1] == vi[2] || vi[2] == vi[0] {
202 degenerate.push(id);
203 triangles.push(TriangleData {
204 vertices: vi,
205 centroid: Vec3::ZERO,
206 normal: Vec3::ZERO,
207 area: 0.0,
208 neighbors: [None; 3],
209 feature_edges: [true; 3],
210 });
211 continue;
212 }
213 let [a, b, c] = vi.map(|i| mesh.vertices[i]);
214 let ab = b - a;
215 let ac = c - a;
216 let bc = c - b;
217 let cross = ab.cross(ac);
218 let area = 0.5 * cross.length();
219 let edge_scale = ab.length().max(ac.length()).max(bc.length());
227 let coordinate_scale = [a, b, c]
228 .into_iter()
229 .map(|point| point.x.abs().max(point.y.abs()).max(point.z.abs()))
230 .fold(edge_scale, f64::max);
231 let subtraction_error = f64::EPSILON * coordinate_scale;
232 let machine_area_floor = numerical::mesh::MACHINE_AREA_FLOOR_MULTIPLIER
233 * (edge_scale * subtraction_error + subtraction_error * subtraction_error);
234 let area_floor = options.minimum_triangle_area.max(machine_area_floor);
235 if area <= area_floor {
236 degenerate.push(id);
237 triangles.push(TriangleData {
238 vertices: vi,
239 centroid: (a + b + c) / 3.0,
240 normal: Vec3::ZERO,
241 area: 0.0,
244 neighbors: [None; 3],
245 feature_edges: [true; 3],
246 });
247 continue;
248 }
249 let normal = cross / (2.0 * area);
250 for &v in &vi {
251 vertex_area_weights[v] += area / 3.0;
252 }
253 triangles.push(TriangleData {
254 vertices: vi,
255 centroid: (a + b + c) / 3.0,
256 normal,
257 area,
258 neighbors: [None; 3],
259 feature_edges: [true; 3],
260 });
261 }
262 if degenerate.len() == mesh.triangles.len() {
263 return Err(RecognitionError::DegenerateData(
264 "all triangles are degenerate".into(),
265 ));
266 }
267 let mut edges: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new();
268 for (tid, t) in triangles.iter().enumerate() {
269 if t.area <= 0.0 {
270 continue;
271 }
272 for edge in 0..3 {
273 let a = t.vertices[edge];
274 let b = t.vertices[(edge + 1) % 3];
275 edges
276 .entry(if a < b { (a, b) } else { (b, a) })
277 .or_default()
278 .push((tid, edge));
279 }
280 }
281 for incidents in edges.values() {
282 if incidents.len() == 2 {
283 let (ta, ea) = incidents[0];
284 let (tb, eb) = incidents[1];
285 triangles[ta].neighbors[ea] = Some(tb);
286 triangles[tb].neighbors[eb] = Some(ta);
287 let cosine = triangles[ta]
288 .normal
289 .dot(triangles[tb].normal)
290 .clamp(-1.0, 1.0);
291 let feature = cosine.acos() > options.feature_angle;
292 triangles[ta].feature_edges[ea] = feature;
293 triangles[tb].feature_edges[eb] = feature;
294 }
295 }
296 for metadata in &mesh.source_metadata {
297 if metadata.version != 1 {
298 return Err(RecognitionError::InvalidMesh(format!(
299 "unsupported metadata version {}",
300 metadata.version
301 )));
302 }
303 if metadata
304 .triangle_indices
305 .iter()
306 .any(|&t| t >= mesh.triangles.len())
307 {
308 return Err(RecognitionError::InvalidMesh(
309 "metadata references an out-of-range triangle".into(),
310 ));
311 }
312 if metadata
313 .orientation
314 .is_some_and(|sense| !matches!(sense, -1 | 1))
315 {
316 return Err(RecognitionError::InvalidMesh(
317 "metadata orientation must be -1 or +1".into(),
318 ));
319 }
320 if metadata
321 .source_tolerance
322 .is_some_and(|tolerance| !tolerance.is_finite() || tolerance <= 0.0)
323 {
324 return Err(RecognitionError::InvalidMesh(
325 "metadata source_tolerance must be finite and positive".into(),
326 ));
327 }
328 }
329 Ok(Self {
330 vertices: mesh.vertices.clone(),
331 vertex_normals,
332 triangles,
333 bbox_min,
334 bbox_max,
335 diagonal,
336 vertex_area_weights,
337 source_metadata: mesh.source_metadata.clone(),
338 degenerate_triangles: degenerate,
339 })
340 }
341 pub fn all_non_degenerate(&self) -> Vec<usize> {
343 self.triangles
344 .iter()
345 .enumerate()
346 .filter_map(|(i, t)| (t.area > 0.0).then_some(i))
347 .collect()
348 }
349 pub fn validate_selection(&self, ids: &[usize]) -> Result<(), RecognitionError> {
351 if ids.is_empty() {
352 return Err(RecognitionError::InvalidSelection(
353 "selection is empty".into(),
354 ));
355 }
356 if ids.iter().any(|&i| i >= self.triangles.len()) {
357 return Err(RecognitionError::InvalidSelection(
358 "triangle index out of range".into(),
359 ));
360 }
361 if ids.iter().all(|&i| self.triangles[i].area == 0.0) {
362 return Err(RecognitionError::DegenerateData(
363 "selection contains no usable triangles".into(),
364 ));
365 }
366 Ok(())
367 }
368 pub fn validate_vertex_selection(&self, ids: &[usize]) -> Result<(), RecognitionError> {
371 if ids.is_empty() {
372 return Err(RecognitionError::InvalidSelection(
373 "vertex selection is empty".into(),
374 ));
375 }
376 if ids.iter().any(|&i| i >= self.vertices.len()) {
377 return Err(RecognitionError::InvalidSelection(
378 "vertex index out of range".into(),
379 ));
380 }
381 let mut unique = ids.to_vec();
382 unique.sort_unstable();
383 if unique.windows(2).any(|pair| pair[0] == pair[1]) {
384 return Err(RecognitionError::InvalidSelection(
385 "vertex selection contains duplicate indices".into(),
386 ));
387 }
388 if ids.iter().any(|&i| self.vertex_area_weights[i] == 0.0) {
389 return Err(RecognitionError::DegenerateData(
390 "vertex selection contains a vertex with no usable incident geometry".into(),
391 ));
392 }
393 Ok(())
394 }
395 pub fn selected_vertex_weights(&self, ids: &[usize]) -> Vec<(usize, f64)> {
397 let mut weights = BTreeMap::<usize, f64>::new();
398 for &tid in ids {
399 let t = &self.triangles[tid];
400 if t.area > 0.0 {
401 for &v in &t.vertices {
402 *weights.entry(v).or_default() += t.area / 3.0;
403 }
404 }
405 }
406 weights.into_iter().collect()
407 }
408 pub fn selection_centroid(&self, ids: &[usize]) -> Vec3 {
412 let mut sum = Vec3::ZERO;
413 let mut area = 0.0;
414 for &i in ids {
415 let t = &self.triangles[i];
416 sum += t.centroid * t.area;
417 area += t.area;
418 }
419 if area > 0.0 {
420 sum / area
421 } else {
422 Vec3::ZERO
423 }
424 }
425 pub fn connected_components(&self, ids: &[usize], respect_features: bool) -> Vec<Vec<usize>> {
429 let mut allowed = vec![false; self.triangles.len()];
430 for &i in ids {
431 if i < allowed.len() {
432 allowed[i] = true;
433 }
434 }
435 let mut seen = vec![false; self.triangles.len()];
436 let mut result = Vec::new();
437 for &seed in ids {
438 if seen[seed] || self.triangles[seed].area == 0.0 {
439 continue;
440 }
441 let mut stack = vec![seed];
442 seen[seed] = true;
443 let mut part = Vec::new();
444 while let Some(i) = stack.pop() {
445 part.push(i);
446 for edge in 0..3 {
447 if respect_features && self.triangles[i].feature_edges[edge] {
448 continue;
449 }
450 if let Some(n) = self.triangles[i].neighbors[edge] {
451 if allowed[n] && !seen[n] {
452 seen[n] = true;
453 stack.push(n);
454 }
455 }
456 }
457 }
458 part.sort_unstable();
459 result.push(part);
460 }
461 result
462 }
463}
464
465