1use crate::shape::{RayHit, Shape};
8use oxiphysics_core::Aabb;
9use oxiphysics_core::math::{Mat3, Real, Vec3};
10use std::collections::{BinaryHeap, HashMap, HashSet};
11
12#[derive(Debug, Clone)]
14pub struct TriangleMesh {
15 pub vertices: Vec<Vec3>,
17 pub indices: Vec<[usize; 3]>,
19}
20
21impl TriangleMesh {
22 pub fn new(vertices: Vec<Vec3>, indices: Vec<[usize; 3]>) -> Self {
24 Self { vertices, indices }
25 }
26
27 pub fn surface_area(&self) -> Real {
29 let mut total = 0.0;
30 for tri in &self.indices {
31 let a = self.vertices[tri[0]];
32 let b = self.vertices[tri[1]];
33 let c = self.vertices[tri[2]];
34 let ab = b - a;
35 let ac = c - a;
36 total += ab.cross(&ac).norm() * 0.5;
37 }
38 total
39 }
40
41 pub fn volume_explicit(&self) -> Real {
43 let mut vol = 0.0;
44 for tri in &self.indices {
45 let a = self.vertices[tri[0]];
46 let b = self.vertices[tri[1]];
47 let c = self.vertices[tri[2]];
48 vol += a.dot(&b.cross(&c)) / 6.0;
49 }
50 vol.abs()
51 }
52
53 pub fn center_of_mass_explicit(&self) -> [f64; 3] {
55 if self.indices.is_empty() {
56 return [0.0, 0.0, 0.0];
57 }
58 let mut weighted = Vec3::zeros();
59 let mut total_vol = 0.0;
60 for tri in &self.indices {
61 let a = self.vertices[tri[0]];
62 let b = self.vertices[tri[1]];
63 let c = self.vertices[tri[2]];
64 let tet_vol = a.dot(&b.cross(&c)) / 6.0;
65 let tet_center = (a + b + c) * 0.25;
66 weighted += tet_center * tet_vol;
67 total_vol += tet_vol;
68 }
69 if total_vol.abs() > 1e-12 {
70 let com = weighted / total_vol;
71 [com.x, com.y, com.z]
72 } else {
73 [0.0, 0.0, 0.0]
74 }
75 }
76
77 pub fn compute_normals(&self) -> Vec<[f64; 3]> {
79 self.indices
80 .iter()
81 .map(|tri| {
82 let a = self.vertices[tri[0]];
83 let b = self.vertices[tri[1]];
84 let c = self.vertices[tri[2]];
85 let ab = b - a;
86 let ac = c - a;
87 let n = ab.cross(&ac);
88 let len = n.norm();
89 if len < 1e-12 {
90 [0.0, 1.0, 0.0]
91 } else {
92 [n.x / len, n.y / len, n.z / len]
93 }
94 })
95 .collect()
96 }
97
98 pub fn ray_cast_full(
100 &self,
101 origin: [f64; 3],
102 direction: [f64; 3],
103 max_toi: f64,
104 ) -> Option<(f64, usize, [f64; 3])> {
105 let o = Vec3::new(origin[0], origin[1], origin[2]);
106 let d = Vec3::new(direction[0], direction[1], direction[2]);
107 let mut best: Option<(f64, usize, [f64; 3])> = None;
108
109 for (face_idx, tri) in self.indices.iter().enumerate() {
110 let v0 = self.vertices[tri[0]];
111 let v1 = self.vertices[tri[1]];
112 let v2 = self.vertices[tri[2]];
113
114 if let Some(hit) = ray_triangle(&o, &d, &v0, &v1, &v2)
115 && hit.toi >= 0.0
116 && hit.toi <= max_toi
117 && best.as_ref().is_none_or(|b| hit.toi < b.0)
118 {
119 let n = [hit.normal.x, hit.normal.y, hit.normal.z];
120 best = Some((hit.toi, face_idx, n));
121 }
122 }
123 best
124 }
125
126 pub fn is_watertight(&self) -> bool {
128 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
129
130 for tri in &self.indices {
131 let verts = [tri[0], tri[1], tri[2]];
132 for i in 0..3 {
133 let a = verts[i];
134 let b = verts[(i + 1) % 3];
135 let key = if a < b { (a, b) } else { (b, a) };
136 *edge_count.entry(key).or_insert(0) += 1;
137 }
138 }
139
140 edge_count.values().all(|&count| count == 2)
141 }
142
143 pub fn vertex_to_faces(&self) -> Vec<Vec<usize>> {
150 let mut v2f = vec![Vec::new(); self.vertices.len()];
151 for (fi, tri) in self.indices.iter().enumerate() {
152 for &vi in tri {
153 v2f[vi].push(fi);
154 }
155 }
156 v2f
157 }
158
159 pub fn face_adjacency(&self) -> Vec<Vec<usize>> {
163 let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
165 for (fi, tri) in self.indices.iter().enumerate() {
166 for k in 0..3 {
167 let a = tri[k];
168 let b = tri[(k + 1) % 3];
169 let key = if a < b { (a, b) } else { (b, a) };
170 edge_faces.entry(key).or_default().push(fi);
171 }
172 }
173 let mut adj = vec![Vec::new(); self.indices.len()];
174 for faces in edge_faces.values() {
175 for i in 0..faces.len() {
176 for j in (i + 1)..faces.len() {
177 adj[faces[i]].push(faces[j]);
178 adj[faces[j]].push(faces[i]);
179 }
180 }
181 }
182 for v in &mut adj {
184 v.sort_unstable();
185 v.dedup();
186 }
187 adj
188 }
189
190 pub fn unique_edges(&self) -> Vec<(usize, usize)> {
192 let mut set: HashSet<(usize, usize)> = HashSet::new();
193 for tri in &self.indices {
194 for k in 0..3 {
195 let a = tri[k];
196 let b = tri[(k + 1) % 3];
197 let key = if a < b { (a, b) } else { (b, a) };
198 set.insert(key);
199 }
200 }
201 let mut edges: Vec<(usize, usize)> = set.into_iter().collect();
202 edges.sort_unstable();
203 edges
204 }
205
206 pub fn vertex_neighbors(&self) -> Vec<Vec<usize>> {
208 let mut nbrs = vec![HashSet::<usize>::new(); self.vertices.len()];
209 for tri in &self.indices {
210 for k in 0..3 {
211 let a = tri[k];
212 let b = tri[(k + 1) % 3];
213 nbrs[a].insert(b);
214 nbrs[b].insert(a);
215 }
216 }
217 nbrs.into_iter()
218 .map(|s| {
219 let mut v: Vec<usize> = s.into_iter().collect();
220 v.sort_unstable();
221 v
222 })
223 .collect()
224 }
225
226 pub fn edge_collapse(&mut self, v0: usize, v1: usize) -> bool {
236 let edge_found = self
238 .indices
239 .iter()
240 .any(|tri| tri.contains(&v0) && tri.contains(&v1));
241 if !edge_found {
242 return false;
243 }
244
245 let mid = (self.vertices[v0] + self.vertices[v1]) * 0.5;
247 self.vertices[v0] = mid;
248
249 for tri in &mut self.indices {
251 for idx in tri.iter_mut() {
252 if *idx == v1 {
253 *idx = v0;
254 }
255 }
256 }
257
258 self.indices
260 .retain(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
261
262 true
263 }
264
265 pub fn compute_vertex_normals(&self) -> Vec<[f64; 3]> {
272 let mut normals = vec![Vec3::zeros(); self.vertices.len()];
273 for tri in &self.indices {
274 let v0 = self.vertices[tri[0]];
275 let v1 = self.vertices[tri[1]];
276 let v2 = self.vertices[tri[2]];
277 let e01 = v1 - v0;
278 let e02 = v2 - v0;
279 let face_n = e01.cross(&e02);
280
281 let angle_at = |va: Vec3, vb: Vec3, vc: Vec3| -> f64 {
282 let ab = (vb - va).normalize();
283 let ac = (vc - va).normalize();
284 ab.dot(&ac).clamp(-1.0, 1.0).acos()
285 };
286
287 let a0 = angle_at(v0, v1, v2);
288 let a1 = angle_at(v1, v2, v0);
289 let a2 = angle_at(v2, v0, v1);
290
291 normals[tri[0]] += face_n * a0;
292 normals[tri[1]] += face_n * a1;
293 normals[tri[2]] += face_n * a2;
294 }
295 normals
296 .iter()
297 .map(|n| {
298 let len = n.norm();
299 if len < 1e-12 {
300 [0.0, 1.0, 0.0]
301 } else {
302 [n.x / len, n.y / len, n.z / len]
303 }
304 })
305 .collect()
306 }
307
308 pub fn laplacian_smooth(&mut self, factor: f64, iterations: usize) {
316 for _ in 0..iterations {
317 let nbrs = self.vertex_neighbors();
318 let mut new_pos = self.vertices.clone();
319 for (vi, neighbours) in nbrs.iter().enumerate() {
320 if neighbours.is_empty() {
321 continue;
322 }
323 let mut avg = Vec3::zeros();
324 for &ni in neighbours {
325 avg += self.vertices[ni];
326 }
327 avg /= neighbours.len() as f64;
328 let diff = avg - self.vertices[vi];
329 new_pos[vi] = self.vertices[vi] + diff * factor;
330 }
331 self.vertices = new_pos;
332 }
333 }
334
335 pub fn geodesic_distance(&self, source: usize) -> Vec<f64> {
344 let nbrs = self.vertex_neighbors();
345 let n = self.vertices.len();
346 let mut dist = vec![f64::INFINITY; n];
347 dist[source] = 0.0;
348
349 let mut heap = BinaryHeap::new();
351 heap.push(std::cmp::Reverse(OrdF64(0.0, source)));
352
353 while let Some(std::cmp::Reverse(OrdF64(d, u))) = heap.pop() {
354 if d > dist[u] {
355 continue;
356 }
357 for &v in &nbrs[u] {
358 let edge_len = (self.vertices[v] - self.vertices[u]).norm();
359 let new_d = d + edge_len;
360 if new_d < dist[v] {
361 dist[v] = new_d;
362 heap.push(std::cmp::Reverse(OrdF64(new_d, v)));
363 }
364 }
365 }
366 dist
367 }
368
369 pub fn loop_subdivide(&mut self) {
379 let n_verts = self.vertices.len();
380
381 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
383 for tri in &self.indices {
384 for k in 0..3 {
385 let a = tri[k];
386 let b = tri[(k + 1) % 3];
387 let key = if a < b { (a, b) } else { (b, a) };
388 *edge_count.entry(key).or_insert(0) += 1;
389 }
390 }
391
392 let mut edge_verts: HashMap<(usize, usize), usize> = HashMap::new();
394 let mut new_vertices = self.vertices.clone();
395
396 let mut edge_opposite: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
398 for tri in &self.indices {
399 for k in 0..3 {
400 let a = tri[k];
401 let b = tri[(k + 1) % 3];
402 let c = tri[(k + 2) % 3]; let key = if a < b { (a, b) } else { (b, a) };
404 edge_opposite.entry(key).or_default().push(c);
405 }
406 }
407
408 let get_or_create_edge_vert = |a: usize,
409 b: usize,
410 edge_verts: &mut HashMap<(usize, usize), usize>,
411 new_vertices: &mut Vec<Vec3>,
412 edge_count: &HashMap<(usize, usize), usize>,
413 edge_opposite: &HashMap<(usize, usize), Vec<usize>>,
414 orig_verts: &[Vec3]|
415 -> usize {
416 let key = if a < b { (a, b) } else { (b, a) };
417 if let Some(&idx) = edge_verts.get(&key) {
418 return idx;
419 }
420 let idx = new_vertices.len();
421 let count = edge_count.get(&key).copied().unwrap_or(1);
422 let pos = if count == 2 {
423 let opposites = edge_opposite.get(&key).expect("key must exist");
425 if opposites.len() == 2 {
426 let c = orig_verts[opposites[0]];
427 let d = orig_verts[opposites[1]];
428 (orig_verts[a] + orig_verts[b]) * (3.0 / 8.0) + (c + d) * (1.0 / 8.0)
429 } else {
430 (orig_verts[a] + orig_verts[b]) * 0.5
431 }
432 } else {
433 (orig_verts[a] + orig_verts[b]) * 0.5
435 };
436 new_vertices.push(pos);
437 edge_verts.insert(key, idx);
438 idx
439 };
440
441 let orig_verts = self.vertices.clone();
442
443 let mut new_indices = Vec::with_capacity(self.indices.len() * 4);
445 for tri in &self.indices {
446 let v0 = tri[0];
447 let v1 = tri[1];
448 let v2 = tri[2];
449 let m01 = get_or_create_edge_vert(
450 v0,
451 v1,
452 &mut edge_verts,
453 &mut new_vertices,
454 &edge_count,
455 &edge_opposite,
456 &orig_verts,
457 );
458 let m12 = get_or_create_edge_vert(
459 v1,
460 v2,
461 &mut edge_verts,
462 &mut new_vertices,
463 &edge_count,
464 &edge_opposite,
465 &orig_verts,
466 );
467 let m20 = get_or_create_edge_vert(
468 v2,
469 v0,
470 &mut edge_verts,
471 &mut new_vertices,
472 &edge_count,
473 &edge_opposite,
474 &orig_verts,
475 );
476 new_indices.push([v0, m01, m20]);
478 new_indices.push([v1, m12, m01]);
479 new_indices.push([v2, m20, m12]);
480 new_indices.push([m01, m12, m20]);
481 }
482
483 let nbrs = self.vertex_neighbors();
485 let mut is_boundary = vec![false; n_verts];
487 for (&(a, b), &cnt) in &edge_count {
488 if cnt == 1 {
489 is_boundary[a] = true;
490 is_boundary[b] = true;
491 }
492 }
493 for vi in 0..n_verts {
494 if is_boundary[vi] {
495 continue;
498 }
499 let n = nbrs[vi].len();
500 if n < 3 {
501 continue;
502 }
503 let beta = if n == 3 {
504 3.0 / 16.0
505 } else {
506 3.0 / (8.0 * n as f64)
507 };
508 let mut neighbour_sum = Vec3::zeros();
509 for &ni in &nbrs[vi] {
510 neighbour_sum += orig_verts[ni];
511 }
512 new_vertices[vi] = orig_verts[vi] * (1.0 - n as f64 * beta) + neighbour_sum * beta;
513 }
514
515 self.vertices = new_vertices;
516 self.indices = new_indices;
517 }
518
519 pub fn boundary_edges(&self) -> Vec<(usize, usize)> {
525 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
526 for tri in &self.indices {
527 for k in 0..3 {
528 let a = tri[k];
529 let b = tri[(k + 1) % 3];
530 let key = if a < b { (a, b) } else { (b, a) };
531 *edge_count.entry(key).or_insert(0) += 1;
532 }
533 }
534 edge_count
535 .into_iter()
536 .filter(|&(_, c)| c == 1)
537 .map(|(e, _)| e)
538 .collect()
539 }
540
541 pub fn euler_characteristic(&self) -> i64 {
543 let v = self.vertices.len() as i64;
544 let e = self.unique_edges().len() as i64;
545 let f = self.indices.len() as i64;
546 v - e + f
547 }
548
549 pub fn non_manifold_edge_count(&self) -> usize {
551 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
552 for tri in &self.indices {
553 for k in 0..3 {
554 let a = tri[k];
555 let b = tri[(k + 1) % 3];
556 let key = if a < b { (a, b) } else { (b, a) };
557 *edge_count.entry(key).or_insert(0) += 1;
558 }
559 }
560 edge_count.values().filter(|&&c| c > 2).count()
561 }
562
563 pub fn compute_laplacian_matrix(&self) -> HashMap<(usize, usize), f64> {
576 let mut weights: HashMap<(usize, usize), f64> = HashMap::new();
577
578 for tri in &self.indices {
579 let [i0, i1, i2] = *tri;
580 let p0 = self.vertices[i0];
581 let p1 = self.vertices[i1];
582 let p2 = self.vertices[i2];
583
584 let cot_at = |a: Vec3, b: Vec3, c: Vec3| -> f64 {
587 let ab = b - a;
589 let ac = c - a;
590 let cos_t = ab.dot(&ac);
591 let sin_t = ab.cross(&ac).norm();
592 if sin_t.abs() < 1e-12 {
593 0.0
594 } else {
595 cos_t / sin_t
596 }
597 };
598
599 let cot0 = cot_at(p0, p1, p2); let cot1 = cot_at(p1, p0, p2); let cot2 = cot_at(p2, p0, p1); *weights.entry((i1, i2)).or_insert(0.0) += 0.5 * cot0;
605 *weights.entry((i2, i1)).or_insert(0.0) += 0.5 * cot0;
606
607 *weights.entry((i0, i2)).or_insert(0.0) += 0.5 * cot1;
609 *weights.entry((i2, i0)).or_insert(0.0) += 0.5 * cot1;
610
611 *weights.entry((i0, i1)).or_insert(0.0) += 0.5 * cot2;
613 *weights.entry((i1, i0)).or_insert(0.0) += 0.5 * cot2;
614 }
615
616 weights
617 }
618
619 pub fn compute_heat_kernel_signature(&self, t_values: &[f64]) -> Vec<Vec<f64>> {
638 let n = self.vertices.len();
639 if n == 0 || t_values.is_empty() {
640 return vec![vec![0.0; t_values.len()]; n];
641 }
642
643 let weights = self.compute_laplacian_matrix();
645
646 let mut vertex_area = vec![0.0f64; n];
648 for tri in &self.indices {
649 let [i0, i1, i2] = *tri;
650 let a = self.vertices[i0];
651 let b = self.vertices[i1];
652 let c = self.vertices[i2];
653 let area = (b - a).cross(&(c - a)).norm() * 0.5;
654 let a_third = area / 3.0;
655 vertex_area[i0] += a_third;
656 vertex_area[i1] += a_third;
657 vertex_area[i2] += a_third;
658 }
659
660 let mut diag = vec![0.0f64; n];
662 for (&(i, _j), &w) in &weights {
663 diag[i] += w;
664 }
665
666 let lambda: Vec<f64> = (0..n)
668 .map(|i| {
669 if vertex_area[i] > 1e-15 {
670 diag[i] / vertex_area[i]
671 } else {
672 0.0
673 }
674 })
675 .collect();
676
677 (0..n)
679 .map(|i| t_values.iter().map(|&t| (-t * lambda[i]).exp()).collect())
680 .collect()
681 }
682
683 pub fn smooth_laplacian(&mut self, factor: f64, iterations: usize) {
699 if self.vertices.is_empty() || self.indices.is_empty() {
700 return;
701 }
702
703 let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
705 for tri in &self.indices {
706 for k in 0..3 {
707 let a = tri[k];
708 let b = tri[(k + 1) % 3];
709 let key = if a < b { (a, b) } else { (b, a) };
710 *edge_count.entry(key).or_insert(0) += 1;
711 }
712 }
713 let mut is_boundary = vec![false; self.vertices.len()];
714 for (&(a, b), &c) in &edge_count {
715 if c == 1 {
716 is_boundary[a] = true;
717 is_boundary[b] = true;
718 }
719 }
720
721 for _ in 0..iterations {
722 let weights = self.compute_laplacian_matrix();
723 let mut new_vertices = self.vertices.clone();
724
725 for i in 0..self.vertices.len() {
726 if is_boundary[i] {
727 continue;
728 }
729 let mut weight_sum = 0.0f64;
730 let mut delta = Vec3::zeros();
731
732 for tri in &self.indices {
733 for k in 0..3 {
734 if tri[k] == i {
735 let j = tri[(k + 1) % 3];
736 let w = *weights.get(&(i, j)).unwrap_or(&0.0);
737 delta += (self.vertices[j] - self.vertices[i]) * w;
738 weight_sum += w;
739 }
740 }
741 }
742
743 if weight_sum > 1e-15 {
744 new_vertices[i] += delta * (factor / weight_sum);
745 }
746 }
747
748 self.vertices = new_vertices;
749 }
750 }
751}
752
753#[derive(Clone, Copy, PartialEq)]
758struct OrdF64(f64, usize);
759
760impl Eq for OrdF64 {}
761
762impl PartialOrd for OrdF64 {
763 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
764 Some(self.cmp(other))
765 }
766}
767
768impl Ord for OrdF64 {
769 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
770 self.0
771 .partial_cmp(&other.0)
772 .unwrap_or(std::cmp::Ordering::Equal)
773 }
774}
775
776impl Shape for TriangleMesh {
781 fn bounding_box(&self) -> Aabb {
782 if self.vertices.is_empty() {
783 return Aabb::new(Vec3::zeros(), Vec3::zeros());
784 }
785 let mut min = self.vertices[0];
786 let mut max = self.vertices[0];
787 for v in &self.vertices[1..] {
788 min = min.inf(v);
789 max = max.sup(v);
790 }
791 Aabb::new(min, max)
792 }
793
794 fn support_point(&self, direction: &Vec3) -> Vec3 {
795 self.vertices
796 .iter()
797 .max_by(|a, b| {
798 a.dot(direction)
799 .partial_cmp(&b.dot(direction))
800 .unwrap_or(std::cmp::Ordering::Equal)
801 })
802 .copied()
803 .unwrap_or_else(Vec3::zeros)
804 }
805
806 fn volume(&self) -> Real {
807 let mut vol = 0.0;
808 for tri in &self.indices {
809 let a = self.vertices[tri[0]];
810 let b = self.vertices[tri[1]];
811 let c = self.vertices[tri[2]];
812 vol += a.dot(&b.cross(&c)) / 6.0;
813 }
814 vol.abs()
815 }
816
817 fn center_of_mass(&self) -> Vec3 {
818 if self.indices.is_empty() {
819 return Vec3::zeros();
820 }
821 let mut weighted = Vec3::zeros();
822 let mut total_vol = 0.0;
823 for tri in &self.indices {
824 let a = self.vertices[tri[0]];
825 let b = self.vertices[tri[1]];
826 let c = self.vertices[tri[2]];
827 let tet_vol = a.dot(&b.cross(&c)) / 6.0;
828 let tet_center = (a + b + c) * 0.25;
829 weighted += tet_center * tet_vol;
830 total_vol += tet_vol;
831 }
832 if total_vol.abs() > 1e-12 {
833 weighted / total_vol
834 } else {
835 Vec3::zeros()
836 }
837 }
838
839 fn inertia_tensor(&self, mass: Real) -> Mat3 {
840 let bb = self.bounding_box();
841 let size = bb.max - bb.min;
842 let k = mass / 12.0;
843 Mat3::new(
844 k * (size.y * size.y + size.z * size.z),
845 0.0,
846 0.0,
847 0.0,
848 k * (size.x * size.x + size.z * size.z),
849 0.0,
850 0.0,
851 0.0,
852 k * (size.x * size.x + size.y * size.y),
853 )
854 }
855
856 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
857 let mut best: Option<RayHit> = None;
858 for tri in &self.indices {
859 let v0 = self.vertices[tri[0]];
860 let v1 = self.vertices[tri[1]];
861 let v2 = self.vertices[tri[2]];
862 if let Some(hit) = ray_triangle(ray_origin, ray_direction, &v0, &v1, &v2)
863 && hit.toi <= max_toi
864 && hit.toi >= 0.0
865 && best.as_ref().is_none_or(|b| hit.toi < b.toi)
866 {
867 best = Some(hit);
868 }
869 }
870 best
871 }
872}
873
874fn ray_triangle(
876 origin: &Vec3,
877 direction: &Vec3,
878 v0: &Vec3,
879 v1: &Vec3,
880 v2: &Vec3,
881) -> Option<RayHit> {
882 let edge1 = v1 - v0;
883 let edge2 = v2 - v0;
884 let h = direction.cross(&edge2);
885 let a = edge1.dot(&h);
886
887 if a.abs() < 1e-10 {
888 return None;
889 }
890
891 let f = 1.0 / a;
892 let s = origin - v0;
893 let u = f * s.dot(&h);
894 if !(0.0..=1.0).contains(&u) {
895 return None;
896 }
897
898 let q = s.cross(&edge1);
899 let v = f * direction.dot(&q);
900 if v < 0.0 || u + v > 1.0 {
901 return None;
902 }
903
904 let t = f * edge2.dot(&q);
905 if t < 0.0 {
906 return None;
907 }
908
909 let point = origin + direction * t;
910 let normal = edge1.cross(&edge2).normalize();
911 Some(RayHit {
912 point,
913 normal,
914 toi: t,
915 })
916}
917
918#[cfg(test)]
920fn unit_cube_mesh() -> TriangleMesh {
921 let v = vec![
922 Vec3::new(0.0, 0.0, 0.0), Vec3::new(1.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 0.0), Vec3::new(0.0, 1.0, 0.0), Vec3::new(0.0, 0.0, 1.0), Vec3::new(1.0, 0.0, 1.0), Vec3::new(1.0, 1.0, 1.0), Vec3::new(0.0, 1.0, 1.0), ];
931 let idx = vec![
932 [0, 2, 1],
934 [0, 3, 2],
935 [4, 5, 6],
937 [4, 6, 7],
938 [0, 4, 7],
940 [0, 7, 3],
941 [1, 2, 6],
943 [1, 6, 5],
944 [0, 1, 5],
946 [0, 5, 4],
947 [3, 7, 6],
949 [3, 6, 2],
950 ];
951 TriangleMesh::new(v, idx)
952}
953
954#[cfg(test)]
956fn tetrahedron_mesh() -> TriangleMesh {
957 let v = vec![
958 Vec3::new(1.0, 1.0, 1.0),
959 Vec3::new(-1.0, -1.0, 1.0),
960 Vec3::new(-1.0, 1.0, -1.0),
961 Vec3::new(1.0, -1.0, -1.0),
962 ];
963 let idx = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
964 TriangleMesh::new(v, idx)
965}
966
967#[cfg(test)]
968mod tests {
969 use super::*;
970 use crate::TriangleMesh;
971 use oxiphysics_core::math::Vec3;
972
973 #[test]
974 fn test_triangle_mesh_raycast() {
975 let mesh = TriangleMesh::new(
976 vec![
977 Vec3::new(-1.0, 0.0, -1.0),
978 Vec3::new(1.0, 0.0, -1.0),
979 Vec3::new(0.0, 0.0, 1.0),
980 ],
981 vec![[0, 1, 2]],
982 );
983 let origin = Vec3::new(0.0, 5.0, 0.0);
984 let dir = Vec3::new(0.0, -1.0, 0.0);
985 let hit = mesh.ray_cast(&origin, &dir, 100.0).unwrap();
986 assert!((hit.toi - 5.0).abs() < 1e-10);
987 }
988
989 #[test]
990 fn test_triangle_mesh_raycast_miss() {
991 let mesh = TriangleMesh::new(
992 vec![
993 Vec3::new(-1.0, 0.0, -1.0),
994 Vec3::new(1.0, 0.0, -1.0),
995 Vec3::new(0.0, 0.0, 1.0),
996 ],
997 vec![[0, 1, 2]],
998 );
999 let origin = Vec3::new(10.0, 5.0, 0.0);
1000 let dir = Vec3::new(0.0, -1.0, 0.0);
1001 assert!(mesh.ray_cast(&origin, &dir, 100.0).is_none());
1002 }
1003
1004 #[test]
1005 fn test_triangle_mesh_cube_volume() {
1006 let mesh = unit_cube_mesh();
1007 let vol = mesh.volume();
1008 assert!((vol - 1.0).abs() < 1e-6, "expected volume=1, got {}", vol);
1009 }
1010
1011 #[test]
1012 fn test_triangle_mesh_cube_surface_area() {
1013 let mesh = unit_cube_mesh();
1014 let sa = mesh.surface_area();
1015 assert!((sa - 6.0).abs() < 1e-6, "expected SA=6, got {}", sa);
1016 }
1017
1018 #[test]
1019 fn test_triangle_mesh_cube_is_watertight() {
1020 let mesh = unit_cube_mesh();
1021 assert!(mesh.is_watertight(), "unit cube should be watertight");
1022 }
1023
1024 #[test]
1025 fn test_triangle_mesh_open_not_watertight() {
1026 let mesh = TriangleMesh::new(
1027 vec![
1028 Vec3::new(0.0, 0.0, 0.0),
1029 Vec3::new(1.0, 0.0, 0.0),
1030 Vec3::new(0.0, 1.0, 0.0),
1031 ],
1032 vec![[0, 1, 2]],
1033 );
1034 assert!(!mesh.is_watertight(), "open mesh should not be watertight");
1035 }
1036
1037 #[test]
1038 fn test_triangle_mesh_compute_normals() {
1039 let mesh = TriangleMesh::new(
1040 vec![
1041 Vec3::new(0.0, 0.0, 0.0),
1042 Vec3::new(1.0, 0.0, 0.0),
1043 Vec3::new(0.0, 1.0, 0.0),
1044 ],
1045 vec![[0, 1, 2]],
1046 );
1047 let normals = mesh.compute_normals();
1048 assert_eq!(normals.len(), 1);
1049 assert!((normals[0][2] - 1.0).abs() < 1e-10, "expected +Z normal");
1050 }
1051
1052 #[test]
1053 fn test_triangle_mesh_ray_cast_full() {
1054 let mesh = unit_cube_mesh();
1055 let (t, _face, n) = mesh
1056 .ray_cast_full([0.5, 5.0, 0.5], [0.0, -1.0, 0.0], 100.0)
1057 .expect("should hit cube");
1058 assert!((t - 4.0).abs() < 1e-6, "expected t=4, got {}", t);
1059 assert!(n[1].abs() > 0.9, "normal should be roughly Y");
1060 }
1061
1062 #[test]
1063 fn test_triangle_mesh_surface_area_triangle() {
1064 let mesh = TriangleMesh::new(
1065 vec![
1066 Vec3::new(0.0, 0.0, 0.0),
1067 Vec3::new(3.0, 0.0, 0.0),
1068 Vec3::new(0.0, 4.0, 0.0),
1069 ],
1070 vec![[0, 1, 2]],
1071 );
1072 let sa = mesh.surface_area();
1073 assert!((sa - 6.0).abs() < 1e-10, "expected SA=6, got {}", sa);
1074 }
1075
1076 #[test]
1081 fn test_vertex_to_faces() {
1082 let mesh = unit_cube_mesh();
1083 let v2f = mesh.vertex_to_faces();
1084 assert_eq!(v2f.len(), 8);
1085 for faces in &v2f {
1089 assert!(
1090 !faces.is_empty(),
1091 "every vertex should have at least one face"
1092 );
1093 }
1094 assert_eq!(
1097 v2f[0].len(),
1098 6,
1099 "vertex 0 in unit cube should appear in 6 triangles"
1100 );
1101 }
1102
1103 #[test]
1104 fn test_face_adjacency_single_triangle() {
1105 let mesh = TriangleMesh::new(
1106 vec![
1107 Vec3::new(0.0, 0.0, 0.0),
1108 Vec3::new(1.0, 0.0, 0.0),
1109 Vec3::new(0.0, 1.0, 0.0),
1110 ],
1111 vec![[0, 1, 2]],
1112 );
1113 let adj = mesh.face_adjacency();
1114 assert_eq!(adj.len(), 1);
1115 assert!(adj[0].is_empty(), "single triangle has no adjacent faces");
1116 }
1117
1118 #[test]
1119 fn test_face_adjacency_cube() {
1120 let mesh = unit_cube_mesh();
1121 let adj = mesh.face_adjacency();
1122 assert_eq!(adj.len(), 12);
1123 for neighbors in &adj {
1125 assert!(
1126 !neighbors.is_empty(),
1127 "each cube triangle should have neighbors"
1128 );
1129 }
1130 }
1131
1132 #[test]
1133 fn test_unique_edges_single_triangle() {
1134 let mesh = TriangleMesh::new(
1135 vec![
1136 Vec3::new(0.0, 0.0, 0.0),
1137 Vec3::new(1.0, 0.0, 0.0),
1138 Vec3::new(0.0, 1.0, 0.0),
1139 ],
1140 vec![[0, 1, 2]],
1141 );
1142 let edges = mesh.unique_edges();
1143 assert_eq!(edges.len(), 3);
1144 }
1145
1146 #[test]
1147 fn test_unique_edges_cube() {
1148 let mesh = unit_cube_mesh();
1149 let edges = mesh.unique_edges();
1150 assert_eq!(edges.len(), 18);
1152 }
1153
1154 #[test]
1155 fn test_vertex_neighbors() {
1156 let mesh = TriangleMesh::new(
1157 vec![
1158 Vec3::new(0.0, 0.0, 0.0),
1159 Vec3::new(1.0, 0.0, 0.0),
1160 Vec3::new(0.5, 1.0, 0.0),
1161 ],
1162 vec![[0, 1, 2]],
1163 );
1164 let nbrs = mesh.vertex_neighbors();
1165 assert_eq!(nbrs.len(), 3);
1166 assert_eq!(nbrs[0].len(), 2); assert_eq!(nbrs[1].len(), 2);
1168 assert_eq!(nbrs[2].len(), 2);
1169 }
1170
1171 #[test]
1176 fn test_edge_collapse_basic() {
1177 let mut mesh = TriangleMesh::new(
1179 vec![
1180 Vec3::new(0.0, 0.0, 0.0),
1181 Vec3::new(1.0, 0.0, 0.0),
1182 Vec3::new(0.5, 1.0, 0.0),
1183 Vec3::new(0.5, -1.0, 0.0),
1184 ],
1185 vec![[0, 1, 2], [0, 3, 1]],
1186 );
1187 let ok = mesh.edge_collapse(0, 1);
1188 assert!(ok, "edge collapse should succeed");
1189 assert!((mesh.vertices[0].x - 0.5).abs() < 1e-10);
1191 assert!(mesh.indices.is_empty(), "both triangles should degenerate");
1196 }
1197
1198 #[test]
1199 fn test_edge_collapse_nonexistent() {
1200 let mut mesh = TriangleMesh::new(
1201 vec![
1202 Vec3::new(0.0, 0.0, 0.0),
1203 Vec3::new(1.0, 0.0, 0.0),
1204 Vec3::new(0.5, 1.0, 0.0),
1205 ],
1206 vec![[0, 1, 2]],
1207 );
1208 assert!(!mesh.edge_collapse(0, 100));
1210 }
1211
1212 #[test]
1213 fn test_edge_collapse_preserves_other_faces() {
1214 let mut mesh = TriangleMesh::new(
1216 vec![
1217 Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0), Vec3::new(1.0, 1.0, 0.0), Vec3::new(1.0, -1.0, 0.0), Vec3::new(3.0, 1.0, 0.0), ],
1223 vec![[0, 1, 2], [0, 3, 1], [1, 4, 2]],
1224 );
1225 let ok = mesh.edge_collapse(0, 1);
1226 assert!(ok);
1227 assert_eq!(
1230 mesh.indices.len(),
1231 1,
1232 "two degenerate tris removed, one survives"
1233 );
1234 }
1235
1236 #[test]
1241 fn test_vertex_normals_flat_surface() {
1242 let mesh = TriangleMesh::new(
1243 vec![
1244 Vec3::new(0.0, 0.0, 0.0),
1245 Vec3::new(1.0, 0.0, 0.0),
1246 Vec3::new(1.0, 1.0, 0.0),
1247 Vec3::new(0.0, 1.0, 0.0),
1248 ],
1249 vec![[0, 1, 2], [0, 2, 3]],
1250 );
1251 let normals = mesh.compute_vertex_normals();
1252 assert_eq!(normals.len(), 4);
1253 for n in &normals {
1254 assert!(
1255 (n[2] - 1.0).abs() < 1e-6,
1256 "flat XY surface normal should be +Z"
1257 );
1258 }
1259 }
1260
1261 #[test]
1262 fn test_vertex_normals_unit_length() {
1263 let mesh = unit_cube_mesh();
1264 let normals = mesh.compute_vertex_normals();
1265 for n in &normals {
1266 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1267 assert!(
1268 (len - 1.0).abs() < 1e-6,
1269 "vertex normal should be unit length, got {}",
1270 len
1271 );
1272 }
1273 }
1274
1275 #[test]
1280 fn test_laplacian_smooth_doesnt_crash() {
1281 let mut mesh = unit_cube_mesh();
1282 mesh.laplacian_smooth(0.5, 3);
1283 assert_eq!(mesh.vertices.len(), 8);
1285 assert_eq!(mesh.indices.len(), 12);
1286 }
1287
1288 #[test]
1289 fn test_laplacian_smooth_converges_to_center() {
1290 let mut mesh = TriangleMesh::new(
1292 vec![
1293 Vec3::new(0.0, 0.0, 0.0), Vec3::new(2.0, 0.0, 0.0), Vec3::new(1.0, 2.0, 0.0), Vec3::new(1.0, 0.5, 0.0), ],
1298 vec![[0, 1, 3], [1, 2, 3], [2, 0, 3]],
1299 );
1300 let orig_v3 = mesh.vertices[3];
1301 mesh.laplacian_smooth(1.0, 1);
1302 let avg =
1304 (Vec3::new(0.0, 0.0, 0.0) + Vec3::new(2.0, 0.0, 0.0) + Vec3::new(1.0, 2.0, 0.0)) / 3.0;
1305 let new_v3 = mesh.vertices[3];
1306 let d_new = (new_v3 - avg).norm();
1307 let d_old = (orig_v3 - avg).norm();
1308 assert!(
1309 d_new < d_old + 1e-10,
1310 "vertex should move towards neighbor average"
1311 );
1312 }
1313
1314 #[test]
1319 fn test_geodesic_distance_source() {
1320 let mesh = unit_cube_mesh();
1321 let dist = mesh.geodesic_distance(0);
1322 assert_eq!(dist[0], 0.0, "distance to self should be 0");
1323 }
1324
1325 #[test]
1326 fn test_geodesic_distance_adjacent() {
1327 let mesh = TriangleMesh::new(
1328 vec![
1329 Vec3::new(0.0, 0.0, 0.0),
1330 Vec3::new(1.0, 0.0, 0.0),
1331 Vec3::new(0.5, 1.0, 0.0),
1332 ],
1333 vec![[0, 1, 2]],
1334 );
1335 let dist = mesh.geodesic_distance(0);
1336 assert!(
1338 (dist[1] - 1.0).abs() < 1e-10,
1339 "expected dist=1, got {}",
1340 dist[1]
1341 );
1342 }
1343
1344 #[test]
1345 fn test_geodesic_distance_symmetry() {
1346 let mesh = unit_cube_mesh();
1347 let d0 = mesh.geodesic_distance(0);
1348 let d6 = mesh.geodesic_distance(6);
1349 assert!(
1351 (d0[6] - d6[0]).abs() < 1e-10,
1352 "geodesic distance should be symmetric: {} vs {}",
1353 d0[6],
1354 d6[0]
1355 );
1356 }
1357
1358 #[test]
1363 fn test_loop_subdivide_increases_faces() {
1364 let mut mesh = TriangleMesh::new(
1365 vec![
1366 Vec3::new(0.0, 0.0, 0.0),
1367 Vec3::new(1.0, 0.0, 0.0),
1368 Vec3::new(0.5, 1.0, 0.0),
1369 ],
1370 vec![[0, 1, 2]],
1371 );
1372 mesh.loop_subdivide();
1373 assert_eq!(mesh.indices.len(), 4, "one triangle should become four");
1374 }
1375
1376 #[test]
1377 fn test_loop_subdivide_cube() {
1378 let mut mesh = unit_cube_mesh();
1379 assert_eq!(mesh.indices.len(), 12);
1380 mesh.loop_subdivide();
1381 assert_eq!(mesh.indices.len(), 48, "12 tris * 4 = 48");
1382 assert!(
1384 mesh.vertices.len() > 8,
1385 "should have more vertices after subdivision"
1386 );
1387 }
1388
1389 #[test]
1390 fn test_loop_subdivide_watertight_preserved() {
1391 let mut mesh = tetrahedron_mesh();
1392 assert!(mesh.is_watertight(), "tetrahedron should be watertight");
1393 mesh.loop_subdivide();
1394 assert!(
1395 mesh.is_watertight(),
1396 "subdivided tetrahedron should still be watertight"
1397 );
1398 }
1399
1400 #[test]
1405 fn test_boundary_edges_closed_mesh() {
1406 let mesh = unit_cube_mesh();
1407 let be = mesh.boundary_edges();
1408 assert!(
1409 be.is_empty(),
1410 "watertight mesh should have no boundary edges"
1411 );
1412 }
1413
1414 #[test]
1415 fn test_boundary_edges_open_mesh() {
1416 let mesh = TriangleMesh::new(
1417 vec![
1418 Vec3::new(0.0, 0.0, 0.0),
1419 Vec3::new(1.0, 0.0, 0.0),
1420 Vec3::new(0.5, 1.0, 0.0),
1421 ],
1422 vec![[0, 1, 2]],
1423 );
1424 let be = mesh.boundary_edges();
1425 assert_eq!(be.len(), 3, "single triangle has 3 boundary edges");
1426 }
1427
1428 #[test]
1429 fn test_euler_characteristic_cube() {
1430 let mesh = unit_cube_mesh();
1431 let chi = mesh.euler_characteristic();
1432 assert_eq!(chi, 2, "closed cube Euler characteristic should be 2");
1433 }
1434
1435 #[test]
1436 fn test_euler_characteristic_tetrahedron() {
1437 let mesh = tetrahedron_mesh();
1438 let chi = mesh.euler_characteristic();
1439 assert_eq!(
1440 chi, 2,
1441 "closed tetrahedron Euler characteristic should be 2"
1442 );
1443 }
1444
1445 #[test]
1446 fn test_non_manifold_edge_count_clean_mesh() {
1447 let mesh = unit_cube_mesh();
1448 assert_eq!(mesh.non_manifold_edge_count(), 0);
1449 }
1450
1451 #[test]
1452 fn test_non_manifold_edge_count_with_extra_face() {
1453 let mesh = TriangleMesh::new(
1455 vec![
1456 Vec3::new(0.0, 0.0, 0.0),
1457 Vec3::new(1.0, 0.0, 0.0),
1458 Vec3::new(0.5, 1.0, 0.0),
1459 Vec3::new(0.5, -1.0, 0.0),
1460 Vec3::new(0.5, 0.5, 1.0),
1461 ],
1462 vec![[0, 1, 2], [0, 3, 1], [0, 4, 1]], );
1464 assert_eq!(mesh.non_manifold_edge_count(), 1);
1465 }
1466}
1467
1468#[cfg(test)]
1473mod tests_laplacian_hks {
1474
1475 use crate::TriangleMesh;
1476 use oxiphysics_core::math::Vec3;
1477
1478 fn flat_quad() -> TriangleMesh {
1480 TriangleMesh::new(
1481 vec![
1482 Vec3::new(0.0, 0.0, 0.0),
1483 Vec3::new(1.0, 0.0, 0.0),
1484 Vec3::new(1.0, 1.0, 0.0),
1485 Vec3::new(0.0, 1.0, 0.0),
1486 ],
1487 vec![[0, 1, 2], [0, 2, 3]],
1488 )
1489 }
1490
1491 fn single_triangle() -> TriangleMesh {
1493 TriangleMesh::new(
1494 vec![
1495 Vec3::new(0.0, 0.0, 0.0),
1496 Vec3::new(1.0, 0.0, 0.0),
1497 Vec3::new(0.5, 1.0, 0.0),
1498 ],
1499 vec![[0, 1, 2]],
1500 )
1501 }
1502
1503 #[test]
1506 fn test_laplacian_matrix_nonempty() {
1507 let mesh = flat_quad();
1508 let w = mesh.compute_laplacian_matrix();
1509 assert!(!w.is_empty(), "Laplacian weights should be non-empty");
1510 }
1511
1512 #[test]
1513 fn test_laplacian_matrix_symmetric() {
1514 let mesh = flat_quad();
1515 let w = mesh.compute_laplacian_matrix();
1516 for (&(i, j), &wij) in &w {
1518 let wji = w.get(&(j, i)).copied().unwrap_or(0.0);
1519 assert!(
1520 (wij - wji).abs() < 1e-10,
1521 "Laplacian not symmetric at ({i},{j}): {wij} vs {wji}"
1522 );
1523 }
1524 }
1525
1526 #[test]
1527 fn test_laplacian_matrix_single_triangle_all_edges_present() {
1528 let mesh = single_triangle();
1529 let w = mesh.compute_laplacian_matrix();
1530 assert_eq!(
1532 w.len(),
1533 6,
1534 "single triangle should produce 6 directed entries"
1535 );
1536 }
1537
1538 #[test]
1539 fn test_laplacian_matrix_empty_mesh() {
1540 let mesh = TriangleMesh::new(vec![], vec![]);
1541 let w = mesh.compute_laplacian_matrix();
1542 assert!(w.is_empty());
1543 }
1544
1545 #[test]
1548 fn test_hks_output_shape() {
1549 let mesh = flat_quad();
1550 let t_vals = vec![0.1, 1.0, 10.0];
1551 let hks = mesh.compute_heat_kernel_signature(&t_vals);
1552 assert_eq!(hks.len(), mesh.vertices.len(), "one HKS row per vertex");
1553 for row in &hks {
1554 assert_eq!(row.len(), t_vals.len(), "one entry per time scale");
1555 }
1556 }
1557
1558 #[test]
1559 fn test_hks_values_in_range() {
1560 let mesh = flat_quad();
1562 let t_vals = vec![0.01, 0.1, 1.0, 10.0];
1563 let hks = mesh.compute_heat_kernel_signature(&t_vals);
1564 for row in &hks {
1565 for &val in row {
1566 assert!(
1567 val > 0.0 && val <= 1.0 + 1e-12,
1568 "HKS value out of range: {val}"
1569 );
1570 }
1571 }
1572 }
1573
1574 #[test]
1575 fn test_hks_decreasing_with_time() {
1576 let mesh = flat_quad();
1578 let t_vals = vec![0.1, 1.0, 10.0];
1579 let hks = mesh.compute_heat_kernel_signature(&t_vals);
1580 for (vi, row) in hks.iter().enumerate() {
1581 for k in 0..(row.len() - 1) {
1582 assert!(
1583 row[k] >= row[k + 1] - 1e-12,
1584 "HKS not decreasing for vertex {vi}: t={}, val={}; t={}, val={}",
1585 t_vals[k],
1586 row[k],
1587 t_vals[k + 1],
1588 row[k + 1]
1589 );
1590 }
1591 }
1592 }
1593
1594 #[test]
1595 fn test_hks_empty_mesh() {
1596 let mesh = TriangleMesh::new(vec![], vec![]);
1597 let hks = mesh.compute_heat_kernel_signature(&[1.0, 2.0]);
1598 assert!(hks.is_empty());
1599 }
1600
1601 #[test]
1604 fn test_smooth_laplacian_vertex_count_unchanged() {
1605 let mut mesh = flat_quad();
1606 let n_before = mesh.vertices.len();
1607 mesh.smooth_laplacian(0.5, 3);
1608 assert_eq!(
1609 mesh.vertices.len(),
1610 n_before,
1611 "smoothing must not add/remove vertices"
1612 );
1613 }
1614
1615 #[test]
1616 fn test_smooth_laplacian_triangle_count_unchanged() {
1617 let mut mesh = flat_quad();
1618 let n_before = mesh.indices.len();
1619 mesh.smooth_laplacian(0.5, 3);
1620 assert_eq!(
1621 mesh.indices.len(),
1622 n_before,
1623 "smoothing must not change connectivity"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_smooth_laplacian_zero_iterations_no_change() {
1629 let mut mesh = flat_quad();
1630 let verts_before: Vec<Vec3> = mesh.vertices.clone();
1631 mesh.smooth_laplacian(0.5, 0);
1632 for (a, b) in verts_before.iter().zip(mesh.vertices.iter()) {
1633 assert!(
1634 (a - b).norm() < 1e-12,
1635 "zero iterations should not move vertices"
1636 );
1637 }
1638 }
1639
1640 #[test]
1641 fn test_smooth_laplacian_boundary_fixed() {
1642 let mut mesh = single_triangle();
1644 let verts_before: Vec<Vec3> = mesh.vertices.clone();
1645 mesh.smooth_laplacian(0.5, 5);
1646 for (a, b) in verts_before.iter().zip(mesh.vertices.iter()) {
1647 assert!(
1648 (a - b).norm() < 1e-12,
1649 "boundary vertices must not move, delta={}",
1650 (a - b).norm()
1651 );
1652 }
1653 }
1654}