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) {
7 for object in model.resources.iter_objects() {
8 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 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 if level >= ValidationLevel::Paranoid {
59 if object_type.requires_manifold() {
60 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 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 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 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 for (v_idx, tri_indices) in vertex_to_triangles.iter().enumerate() {
194 if tri_indices.len() <= 1 {
195 continue;
196 }
197
198 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 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 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 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 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 edge_counts.values().filter(|&&c| c != 2).count()
369}
370
371fn 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 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 if level >= ValidationLevel::Paranoid {
399 if object_type.requires_manifold() {
400 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 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 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}