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 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 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 if level >= ValidationLevel::Paranoid {
58 if object_type.requires_manifold() {
59 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 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 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 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 for (v_idx, tri_indices) in vertex_to_triangles.iter().enumerate() {
193 if tri_indices.len() <= 1 {
194 continue;
195 }
196
197 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 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 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 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 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 edge_counts.values().filter(|&&c| c != 2).count()
368}
369
370fn 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 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 if level >= ValidationLevel::Paranoid {
398 if object_type.requires_manifold() {
399 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 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 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}