lib3mf_core/validation/
geometry.rs

1use crate::model::{DisplacementMesh, Geometry, Mesh, Model, ObjectType, ResourceId};
2use crate::validation::{ValidationLevel, ValidationReport};
3use std::collections::HashMap;
4
5pub fn validate_geometry(model: &Model, level: ValidationLevel, report: &mut ValidationReport) {
6    for object in model.resources.iter_objects() {
7        // Per spec: "The object type is ignored on objects that contain components"
8        // Component-containing objects skip type-specific mesh validation
9        match &object.geometry {
10            Geometry::Mesh(mesh) => {
11                validate_mesh(
12                    mesh,
13                    object.id,
14                    object.object_type,
15                    level,
16                    report,
17                    model.unit,
18                );
19            }
20            Geometry::DisplacementMesh(dmesh) => {
21                validate_displacement_mesh_geometry(
22                    dmesh,
23                    object.id,
24                    object.object_type,
25                    level,
26                    report,
27                    model.unit,
28                );
29            }
30            _ => {}
31        }
32    }
33}
34
35fn validate_mesh(
36    mesh: &Mesh,
37    oid: ResourceId,
38    object_type: ObjectType,
39    level: ValidationLevel,
40    report: &mut ValidationReport,
41    unit: crate::model::Unit,
42) {
43    // Basic checks for ALL object types (degenerate triangles)
44    for (i, tri) in mesh.triangles.iter().enumerate() {
45        if tri.v1 == tri.v2 || tri.v2 == tri.v3 || tri.v1 == tri.v3 {
46            report.add_warning(
47                4001,
48                format!(
49                    "Triangle {} in Object {} ({}) is degenerate (duplicate vertices)",
50                    i, oid.0, object_type
51                ),
52            );
53        }
54    }
55
56    // Type-specific validation at Paranoid level
57    if level >= ValidationLevel::Paranoid {
58        if object_type.requires_manifold() {
59            // Strict checks for Model and SolidSupport
60            check_manifoldness(mesh, oid, report);
61            check_vertex_manifoldness(mesh, oid, report);
62            check_islands(mesh, oid, report);
63            check_self_intersections(mesh, oid, report);
64            check_orientation(mesh, oid, report);
65            check_degenerate_faces(mesh, oid, report, unit);
66        } else {
67            // Relaxed checks for Support/Surface/Other - only basic geometry warnings
68            // These are informational, not errors
69            let manifold_issues = count_non_manifold_edges(mesh);
70            if manifold_issues > 0 {
71                report.add_info(
72                    4100,
73                    format!(
74                        "Object {} ({}) has {} non-manifold edges (allowed for this type)",
75                        oid.0, object_type, manifold_issues
76                    ),
77                );
78            }
79        }
80    }
81}
82
83fn check_self_intersections(mesh: &Mesh, oid: ResourceId, report: &mut ValidationReport) {
84    if mesh.triangles.len() < 2 {
85        return;
86    }
87
88    use crate::validation::bvh::{AABB, BvhNode};
89
90    let tri_indices: Vec<usize> = (0..mesh.triangles.len()).collect();
91    let bvh = BvhNode::build(mesh, tri_indices);
92
93    let mut intersections = Vec::new();
94
95    for i in 0..mesh.triangles.len() {
96        let tri_aabb = AABB::from_triangle(mesh, &mesh.triangles[i]);
97        let mut results = Vec::new();
98        bvh.find_intersections(mesh, i, &tri_aabb, &mut results);
99        for &j in &results {
100            intersections.push((i, j));
101        }
102    }
103
104    if !intersections.is_empty() {
105        report.add_warning(
106            4008,
107            format!(
108                "Object {} has {} self-intersecting triangle pairs",
109                oid.0,
110                intersections.len()
111            ),
112        );
113    }
114}
115
116fn check_islands(mesh: &Mesh, oid: ResourceId, report: &mut ValidationReport) {
117    if mesh.triangles.is_empty() {
118        return;
119    }
120
121    // 1. Adjacency list: tri -> neighbors
122    let mut edge_to_tris: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
123    for (i, tri) in mesh.triangles.iter().enumerate() {
124        let edges = [
125            sort_edge(tri.v1, tri.v2),
126            sort_edge(tri.v2, tri.v3),
127            sort_edge(tri.v3, tri.v1),
128        ];
129        for e in edges {
130            edge_to_tris.entry(e).or_default().push(i);
131        }
132    }
133
134    let mut visited = vec![false; mesh.triangles.len()];
135    let mut component_count = 0;
136
137    for start_idx in 0..mesh.triangles.len() {
138        if visited[start_idx] {
139            continue;
140        }
141
142        component_count += 1;
143        let mut stack = vec![start_idx];
144        visited[start_idx] = true;
145
146        while let Some(curr_idx) = stack.pop() {
147            let tri = &mesh.triangles[curr_idx];
148            let edges = [
149                sort_edge(tri.v1, tri.v2),
150                sort_edge(tri.v2, tri.v3),
151                sort_edge(tri.v3, tri.v1),
152            ];
153
154            for e in edges {
155                if let Some(neighbors) = edge_to_tris.get(&e) {
156                    for &neigh_idx in neighbors {
157                        if !visited[neigh_idx] {
158                            visited[neigh_idx] = true;
159                            stack.push(neigh_idx);
160                        }
161                    }
162                }
163            }
164        }
165    }
166
167    if component_count > 1 {
168        report.add_warning(
169            4007,
170            format!(
171                "Object {} contains {} disconnected components (islands)",
172                oid.0, component_count
173            ),
174        );
175    }
176}
177
178fn check_vertex_manifoldness(mesh: &Mesh, oid: ResourceId, report: &mut ValidationReport) {
179    if mesh.vertices.is_empty() || mesh.triangles.is_empty() {
180        return;
181    }
182
183    // 1. Group triangles by vertex
184    let mut vertex_to_triangles = vec![Vec::new(); mesh.vertices.len()];
185    for (i, tri) in mesh.triangles.iter().enumerate() {
186        vertex_to_triangles[tri.v1 as usize].push(i);
187        vertex_to_triangles[tri.v2 as usize].push(i);
188        vertex_to_triangles[tri.v3 as usize].push(i);
189    }
190
191    // 2. For each vertex, check connectivity of its triangles
192    for (v_idx, tri_indices) in vertex_to_triangles.iter().enumerate() {
193        if tri_indices.len() <= 1 {
194            continue;
195        }
196
197        // We want to see if all triangles sharing this vertex are reachable from each other
198        // through edges that ALSO share this vertex.
199        let mut visited = vec![false; tri_indices.len()];
200        let mut components = 0;
201
202        for start_idx in 0..tri_indices.len() {
203            if visited[start_idx] {
204                continue;
205            }
206
207            components += 1;
208            let mut stack = vec![start_idx];
209            visited[start_idx] = true;
210
211            while let Some(current_idx) = stack.pop() {
212                let current_tri_idx = tri_indices[current_idx];
213                let current_tri = &mesh.triangles[current_tri_idx];
214
215                // Find neighbor triangles in the local neighbor list that share an edge with current_tri
216                // AND that edge must contain the vertex v_idx.
217                for (other_idx, &other_tri_idx) in tri_indices.iter().enumerate() {
218                    if visited[other_idx] {
219                        continue;
220                    }
221
222                    let other_tri = &mesh.triangles[other_tri_idx];
223
224                    // Do they share an edge containing v_idx?
225                    // An edge is shared if they share 2 vertices.
226                    // Since both share v_idx, they just need to share ONE MORE vertex.
227                    let shared_verts = count_shared_vertices(current_tri, other_tri);
228                    if shared_verts >= 2 {
229                        visited[other_idx] = true;
230                        stack.push(other_idx);
231                    }
232                }
233            }
234        }
235
236        if components > 1 {
237            report.add_warning(
238                4006,
239                format!(
240                    "Object {} has non-manifold vertex {} (points to {} disjoint triangle groups)",
241                    oid.0, v_idx, components
242                ),
243            );
244        }
245    }
246}
247
248fn count_shared_vertices(t1: &crate::model::Triangle, t2: &crate::model::Triangle) -> usize {
249    let mut count = 0;
250    let v1 = [t1.v1, t1.v2, t1.v3];
251    let v2 = [t2.v1, t2.v2, t2.v3];
252    for &va in &v1 {
253        for &vb in &v2 {
254            if va == vb {
255                count += 1;
256            }
257        }
258    }
259    count
260}
261
262fn check_manifoldness(mesh: &Mesh, oid: ResourceId, report: &mut ValidationReport) {
263    let mut edge_counts = HashMap::new();
264
265    for tri in &mesh.triangles {
266        let edges = [
267            sort_edge(tri.v1, tri.v2),
268            sort_edge(tri.v2, tri.v3),
269            sort_edge(tri.v3, tri.v1),
270        ];
271
272        for edge in edges {
273            *edge_counts.entry(edge).or_insert(0) += 1;
274        }
275    }
276
277    for (edge, count) in edge_counts {
278        if count == 1 {
279            report.add_warning(
280                4002,
281                format!(
282                    "Object {} has boundary edge {:?} (not watertight)",
283                    oid.0, edge
284                ),
285            );
286        } else if count > 2 {
287            report.add_warning(
288                4003,
289                format!(
290                    "Object {} has non-manifold edge {:?} (shared by {} triangles)",
291                    oid.0, edge, count
292                ),
293            );
294        }
295    }
296}
297
298fn check_orientation(mesh: &Mesh, oid: ResourceId, report: &mut ValidationReport) {
299    // Count occurrences of directed edges.
300    // If any directed edge count > 1, then two faces have edges in same direction -> Orientation Mismatch.
301
302    let mut directed_edge_counts = HashMap::new();
303    for tri in &mesh.triangles {
304        let edges = [(tri.v1, tri.v2), (tri.v2, tri.v3), (tri.v3, tri.v1)];
305        for edge in edges {
306            *directed_edge_counts.entry(edge).or_insert(0) += 1;
307        }
308    }
309
310    for (edge, count) in directed_edge_counts {
311        if count > 1 {
312            report.add_warning(
313                4004,
314                format!(
315                    "Object {} has orientation mismatch or duplicate faces at edge {:?}",
316                    oid.0, edge
317                ),
318            );
319        }
320    }
321}
322
323fn check_degenerate_faces(
324    mesh: &Mesh,
325    oid: ResourceId,
326    report: &mut ValidationReport,
327    unit: crate::model::Unit,
328) {
329    // Determine epsilon based on unit.
330    // Base reference: 1e-6 mm^2 (which is 1e-12 m^2)
331    // Formula: threshold = 1e-12 / scale_factor^2
332    let scale = unit.scale_factor();
333    let epsilon = 1e-12 / (scale * scale);
334
335    for (i, tri) in mesh.triangles.iter().enumerate() {
336        if mesh.compute_triangle_area(tri) < epsilon {
337            report.add_warning(
338                4005,
339                format!(
340                    "Triangle {} in Object {} has zero/near-zero area (unit scaled)",
341                    i, oid.0
342                ),
343            );
344        }
345    }
346}
347
348fn sort_edge(v1: u32, v2: u32) -> (u32, u32) {
349    if v1 < v2 { (v1, v2) } else { (v2, v1) }
350}
351
352fn count_non_manifold_edges(mesh: &Mesh) -> usize {
353    let mut edge_counts: HashMap<(u32, u32), usize> = HashMap::new();
354
355    for tri in &mesh.triangles {
356        let edges = [
357            sort_edge(tri.v1, tri.v2),
358            sort_edge(tri.v2, tri.v3),
359            sort_edge(tri.v3, tri.v1),
360        ];
361        for e in edges {
362            *edge_counts.entry(e).or_insert(0) += 1;
363        }
364    }
365
366    // Non-manifold edges have count != 2
367    edge_counts.values().filter(|&&c| c != 2).count()
368}
369
370/// Validate DisplacementMesh geometry (basic geometric checks at Paranoid level).
371///
372/// This performs similar checks to regular Mesh validation but adapted for DisplacementMesh.
373/// Displacement-specific validation (normals, gradients, texture refs) is handled by
374/// displacement::validate_displacement().
375fn validate_displacement_mesh_geometry(
376    dmesh: &DisplacementMesh,
377    oid: ResourceId,
378    object_type: ObjectType,
379    level: ValidationLevel,
380    report: &mut ValidationReport,
381    unit: crate::model::Unit,
382) {
383    // Basic checks for ALL object types (degenerate triangles)
384    for (i, tri) in dmesh.triangles.iter().enumerate() {
385        if tri.v1 == tri.v2 || tri.v2 == tri.v3 || tri.v1 == tri.v3 {
386            report.add_warning(
387                4001,
388                format!(
389                    "Triangle {} in DisplacementMesh object {} ({}) is degenerate (duplicate vertices)",
390                    i, oid.0, object_type
391                ),
392            );
393        }
394    }
395
396    // Type-specific validation at Paranoid level
397    if level >= ValidationLevel::Paranoid {
398        if object_type.requires_manifold() {
399            // Strict checks for Model and SolidSupport
400            check_displacement_manifoldness(dmesh, oid, report);
401            check_displacement_vertex_manifoldness(dmesh, oid, report);
402            check_displacement_islands(dmesh, oid, report);
403            check_displacement_orientation(dmesh, oid, report);
404            check_displacement_degenerate_faces(dmesh, oid, report, unit);
405        } else {
406            // Relaxed checks for Support/Surface/Other - only basic geometry warnings
407            let manifold_issues = count_displacement_non_manifold_edges(dmesh);
408            if manifold_issues > 0 {
409                report.add_info(
410                    4100,
411                    format!(
412                        "DisplacementMesh object {} ({}) has {} non-manifold edges (allowed for this type)",
413                        oid.0, object_type, manifold_issues
414                    ),
415                );
416            }
417        }
418    }
419}
420
421fn check_displacement_manifoldness(
422    dmesh: &DisplacementMesh,
423    oid: ResourceId,
424    report: &mut ValidationReport,
425) {
426    let mut edge_counts = HashMap::new();
427
428    for tri in &dmesh.triangles {
429        let edges = [
430            sort_edge(tri.v1, tri.v2),
431            sort_edge(tri.v2, tri.v3),
432            sort_edge(tri.v3, tri.v1),
433        ];
434
435        for edge in edges {
436            *edge_counts.entry(edge).or_insert(0) += 1;
437        }
438    }
439
440    for (edge, count) in edge_counts {
441        if count == 1 {
442            report.add_warning(
443                4002,
444                format!(
445                    "DisplacementMesh object {} has boundary edge {:?} (not watertight)",
446                    oid.0, edge
447                ),
448            );
449        } else if count > 2 {
450            report.add_warning(
451                4003,
452                format!(
453                    "DisplacementMesh object {} has non-manifold edge {:?} (shared by {} triangles)",
454                    oid.0, edge, count
455                ),
456            );
457        }
458    }
459}
460
461fn check_displacement_vertex_manifoldness(
462    dmesh: &DisplacementMesh,
463    oid: ResourceId,
464    report: &mut ValidationReport,
465) {
466    if dmesh.vertices.is_empty() || dmesh.triangles.is_empty() {
467        return;
468    }
469
470    let mut vertex_to_triangles = vec![Vec::new(); dmesh.vertices.len()];
471    for (i, tri) in dmesh.triangles.iter().enumerate() {
472        vertex_to_triangles[tri.v1 as usize].push(i);
473        vertex_to_triangles[tri.v2 as usize].push(i);
474        vertex_to_triangles[tri.v3 as usize].push(i);
475    }
476
477    for (v_idx, tri_indices) in vertex_to_triangles.iter().enumerate() {
478        if tri_indices.len() <= 1 {
479            continue;
480        }
481
482        let mut visited = vec![false; tri_indices.len()];
483        let mut components = 0;
484
485        for start_idx in 0..tri_indices.len() {
486            if visited[start_idx] {
487                continue;
488            }
489
490            components += 1;
491            let mut stack = vec![start_idx];
492            visited[start_idx] = true;
493
494            while let Some(current_idx) = stack.pop() {
495                let current_tri_idx = tri_indices[current_idx];
496                let current_tri = &dmesh.triangles[current_tri_idx];
497
498                for (other_idx, &other_tri_idx) in tri_indices.iter().enumerate() {
499                    if visited[other_idx] {
500                        continue;
501                    }
502
503                    let other_tri = &dmesh.triangles[other_tri_idx];
504                    let shared_verts = count_displacement_shared_vertices(current_tri, other_tri);
505                    if shared_verts >= 2 {
506                        visited[other_idx] = true;
507                        stack.push(other_idx);
508                    }
509                }
510            }
511        }
512
513        if components > 1 {
514            report.add_warning(
515                4006,
516                format!(
517                    "DisplacementMesh object {} has non-manifold vertex {} (points to {} disjoint triangle groups)",
518                    oid.0, v_idx, components
519                ),
520            );
521        }
522    }
523}
524
525fn count_displacement_shared_vertices(
526    t1: &crate::model::DisplacementTriangle,
527    t2: &crate::model::DisplacementTriangle,
528) -> usize {
529    let mut count = 0;
530    let v1 = [t1.v1, t1.v2, t1.v3];
531    let v2 = [t2.v1, t2.v2, t2.v3];
532    for &va in &v1 {
533        for &vb in &v2 {
534            if va == vb {
535                count += 1;
536            }
537        }
538    }
539    count
540}
541
542fn check_displacement_islands(
543    dmesh: &DisplacementMesh,
544    oid: ResourceId,
545    report: &mut ValidationReport,
546) {
547    if dmesh.triangles.is_empty() {
548        return;
549    }
550
551    let mut edge_to_tris: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
552    for (i, tri) in dmesh.triangles.iter().enumerate() {
553        let edges = [
554            sort_edge(tri.v1, tri.v2),
555            sort_edge(tri.v2, tri.v3),
556            sort_edge(tri.v3, tri.v1),
557        ];
558        for e in edges {
559            edge_to_tris.entry(e).or_default().push(i);
560        }
561    }
562
563    let mut visited = vec![false; dmesh.triangles.len()];
564    let mut component_count = 0;
565
566    for start_idx in 0..dmesh.triangles.len() {
567        if visited[start_idx] {
568            continue;
569        }
570
571        component_count += 1;
572        let mut stack = vec![start_idx];
573        visited[start_idx] = true;
574
575        while let Some(curr_idx) = stack.pop() {
576            let tri = &dmesh.triangles[curr_idx];
577            let edges = [
578                sort_edge(tri.v1, tri.v2),
579                sort_edge(tri.v2, tri.v3),
580                sort_edge(tri.v3, tri.v1),
581            ];
582
583            for e in edges {
584                if let Some(neighbors) = edge_to_tris.get(&e) {
585                    for &neigh_idx in neighbors {
586                        if !visited[neigh_idx] {
587                            visited[neigh_idx] = true;
588                            stack.push(neigh_idx);
589                        }
590                    }
591                }
592            }
593        }
594    }
595
596    if component_count > 1 {
597        report.add_warning(
598            4007,
599            format!(
600                "DisplacementMesh object {} contains {} disconnected components (islands)",
601                oid.0, component_count
602            ),
603        );
604    }
605}
606
607fn check_displacement_orientation(
608    dmesh: &DisplacementMesh,
609    oid: ResourceId,
610    report: &mut ValidationReport,
611) {
612    let mut directed_edge_counts = HashMap::new();
613    for tri in &dmesh.triangles {
614        let edges = [(tri.v1, tri.v2), (tri.v2, tri.v3), (tri.v3, tri.v1)];
615        for edge in edges {
616            *directed_edge_counts.entry(edge).or_insert(0) += 1;
617        }
618    }
619
620    for (edge, count) in directed_edge_counts {
621        if count > 1 {
622            report.add_warning(
623                4004,
624                format!(
625                    "DisplacementMesh object {} has orientation mismatch or duplicate faces at edge {:?}",
626                    oid.0, edge
627                ),
628            );
629        }
630    }
631}
632
633fn check_displacement_degenerate_faces(
634    dmesh: &DisplacementMesh,
635    oid: ResourceId,
636    report: &mut ValidationReport,
637    unit: crate::model::Unit,
638) {
639    let scale = unit.scale_factor();
640    let epsilon = (1e-12 / (scale * scale)) as f32;
641
642    for (i, tri) in dmesh.triangles.iter().enumerate() {
643        // Compute triangle area using vertices
644        let v1 = &dmesh.vertices[tri.v1 as usize];
645        let v2 = &dmesh.vertices[tri.v2 as usize];
646        let v3 = &dmesh.vertices[tri.v3 as usize];
647
648        let edge1 = glam::Vec3::new(v2.x - v1.x, v2.y - v1.y, v2.z - v1.z);
649        let edge2 = glam::Vec3::new(v3.x - v1.x, v3.y - v1.y, v3.z - v1.z);
650        let cross = edge1.cross(edge2);
651        let area = cross.length() / 2.0;
652
653        if area < epsilon {
654            report.add_warning(
655                4005,
656                format!(
657                    "Triangle {} in DisplacementMesh object {} has zero/near-zero area (unit scaled)",
658                    i, oid.0
659                ),
660            );
661        }
662    }
663}
664
665fn count_displacement_non_manifold_edges(dmesh: &DisplacementMesh) -> usize {
666    let mut edge_counts: HashMap<(u32, u32), usize> = HashMap::new();
667
668    for tri in &dmesh.triangles {
669        let edges = [
670            sort_edge(tri.v1, tri.v2),
671            sort_edge(tri.v2, tri.v3),
672            sort_edge(tri.v3, tri.v1),
673        ];
674        for e in edges {
675            *edge_counts.entry(e).or_insert(0) += 1;
676        }
677    }
678
679    edge_counts.values().filter(|&&c| c != 2).count()
680}