Skip to main content

lib3mf_core/validation/
geometry.rs

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