Skip to main content

oxiphysics_geometry/
triangle_mesh.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Triangle mesh shape with adjacency queries, vertex normals, Laplacian,
5//! geodesic distance, Loop subdivision, and watertight checks.
6
7use crate::shape::{RayHit, Shape};
8use oxiphysics_core::Aabb;
9use oxiphysics_core::math::{Mat3, Real, Vec3};
10use std::collections::{BinaryHeap, HashMap, HashSet};
11
12/// A triangle mesh defined by vertices and index triples.
13#[derive(Debug, Clone)]
14pub struct TriangleMesh {
15    /// Vertex positions.
16    pub vertices: Vec<Vec3>,
17    /// Triangle indices (groups of three).
18    pub indices: Vec<[usize; 3]>,
19}
20
21impl TriangleMesh {
22    /// Create a new triangle mesh.
23    pub fn new(vertices: Vec<Vec3>, indices: Vec<[usize; 3]>) -> Self {
24        Self { vertices, indices }
25    }
26
27    /// Surface area: sum of triangle areas.
28    #[allow(dead_code)]
29    pub fn surface_area(&self) -> Real {
30        let mut total = 0.0;
31        for tri in &self.indices {
32            let a = self.vertices[tri[0]];
33            let b = self.vertices[tri[1]];
34            let c = self.vertices[tri[2]];
35            let ab = b - a;
36            let ac = c - a;
37            total += ab.cross(&ac).norm() * 0.5;
38        }
39        total
40    }
41
42    /// Volume via signed tetrahedral decomposition (divergence theorem).
43    #[allow(dead_code)]
44    pub fn volume_explicit(&self) -> Real {
45        let mut vol = 0.0;
46        for tri in &self.indices {
47            let a = self.vertices[tri[0]];
48            let b = self.vertices[tri[1]];
49            let c = self.vertices[tri[2]];
50            vol += a.dot(&b.cross(&c)) / 6.0;
51        }
52        vol.abs()
53    }
54
55    /// Center of mass via weighted tetrahedral centroids.
56    #[allow(dead_code)]
57    pub fn center_of_mass_explicit(&self) -> [f64; 3] {
58        if self.indices.is_empty() {
59            return [0.0, 0.0, 0.0];
60        }
61        let mut weighted = Vec3::zeros();
62        let mut total_vol = 0.0;
63        for tri in &self.indices {
64            let a = self.vertices[tri[0]];
65            let b = self.vertices[tri[1]];
66            let c = self.vertices[tri[2]];
67            let tet_vol = a.dot(&b.cross(&c)) / 6.0;
68            let tet_center = (a + b + c) * 0.25;
69            weighted += tet_center * tet_vol;
70            total_vol += tet_vol;
71        }
72        if total_vol.abs() > 1e-12 {
73            let com = weighted / total_vol;
74            [com.x, com.y, com.z]
75        } else {
76            [0.0, 0.0, 0.0]
77        }
78    }
79
80    /// Compute per-face normals (unit vectors).
81    #[allow(dead_code)]
82    pub fn compute_normals(&self) -> Vec<[f64; 3]> {
83        self.indices
84            .iter()
85            .map(|tri| {
86                let a = self.vertices[tri[0]];
87                let b = self.vertices[tri[1]];
88                let c = self.vertices[tri[2]];
89                let ab = b - a;
90                let ac = c - a;
91                let n = ab.cross(&ac);
92                let len = n.norm();
93                if len < 1e-12 {
94                    [0.0, 1.0, 0.0]
95                } else {
96                    [n.x / len, n.y / len, n.z / len]
97                }
98            })
99            .collect()
100    }
101
102    /// Ray cast returning (t, face_index, normal) via Moller-Trumbore for all triangles.
103    #[allow(dead_code)]
104    pub fn ray_cast_full(
105        &self,
106        origin: [f64; 3],
107        direction: [f64; 3],
108        max_toi: f64,
109    ) -> Option<(f64, usize, [f64; 3])> {
110        let o = Vec3::new(origin[0], origin[1], origin[2]);
111        let d = Vec3::new(direction[0], direction[1], direction[2]);
112        let mut best: Option<(f64, usize, [f64; 3])> = None;
113
114        for (face_idx, tri) in self.indices.iter().enumerate() {
115            let v0 = self.vertices[tri[0]];
116            let v1 = self.vertices[tri[1]];
117            let v2 = self.vertices[tri[2]];
118
119            if let Some(hit) = ray_triangle(&o, &d, &v0, &v1, &v2)
120                && hit.toi >= 0.0
121                && hit.toi <= max_toi
122                && best.as_ref().is_none_or(|b| hit.toi < b.0)
123            {
124                let n = [hit.normal.x, hit.normal.y, hit.normal.z];
125                best = Some((hit.toi, face_idx, n));
126            }
127        }
128        best
129    }
130
131    /// Check whether the mesh is watertight (every edge is shared by exactly two triangles).
132    #[allow(dead_code)]
133    pub fn is_watertight(&self) -> bool {
134        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
135
136        for tri in &self.indices {
137            let verts = [tri[0], tri[1], tri[2]];
138            for i in 0..3 {
139                let a = verts[i];
140                let b = verts[(i + 1) % 3];
141                let key = if a < b { (a, b) } else { (b, a) };
142                *edge_count.entry(key).or_insert(0) += 1;
143            }
144        }
145
146        edge_count.values().all(|&count| count == 2)
147    }
148
149    // ------------------------------------------------------------------
150    // New: mesh adjacency queries
151    // ------------------------------------------------------------------
152
153    /// Build vertex-to-face adjacency: for each vertex index, the list of
154    /// triangle indices that reference it.
155    #[allow(dead_code)]
156    pub fn vertex_to_faces(&self) -> Vec<Vec<usize>> {
157        let mut v2f = vec![Vec::new(); self.vertices.len()];
158        for (fi, tri) in self.indices.iter().enumerate() {
159            for &vi in tri {
160                v2f[vi].push(fi);
161            }
162        }
163        v2f
164    }
165
166    /// Build face-to-face adjacency via shared edges.
167    /// Returns a vec of the same length as `self.indices`. Each entry contains
168    /// the indices of adjacent faces (those sharing at least one edge).
169    #[allow(dead_code)]
170    pub fn face_adjacency(&self) -> Vec<Vec<usize>> {
171        // edge -> list of face indices
172        let mut edge_faces: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
173        for (fi, tri) in self.indices.iter().enumerate() {
174            for k in 0..3 {
175                let a = tri[k];
176                let b = tri[(k + 1) % 3];
177                let key = if a < b { (a, b) } else { (b, a) };
178                edge_faces.entry(key).or_default().push(fi);
179            }
180        }
181        let mut adj = vec![Vec::new(); self.indices.len()];
182        for faces in edge_faces.values() {
183            for i in 0..faces.len() {
184                for j in (i + 1)..faces.len() {
185                    adj[faces[i]].push(faces[j]);
186                    adj[faces[j]].push(faces[i]);
187                }
188            }
189        }
190        // Deduplicate
191        for v in &mut adj {
192            v.sort_unstable();
193            v.dedup();
194        }
195        adj
196    }
197
198    /// Return the set of unique edges as sorted (min,max) vertex index pairs.
199    #[allow(dead_code)]
200    pub fn unique_edges(&self) -> Vec<(usize, usize)> {
201        let mut set: HashSet<(usize, usize)> = HashSet::new();
202        for tri in &self.indices {
203            for k in 0..3 {
204                let a = tri[k];
205                let b = tri[(k + 1) % 3];
206                let key = if a < b { (a, b) } else { (b, a) };
207                set.insert(key);
208            }
209        }
210        let mut edges: Vec<(usize, usize)> = set.into_iter().collect();
211        edges.sort_unstable();
212        edges
213    }
214
215    /// Build vertex-to-vertex adjacency (1-ring neighbours).
216    #[allow(dead_code)]
217    pub fn vertex_neighbors(&self) -> Vec<Vec<usize>> {
218        let mut nbrs = vec![HashSet::<usize>::new(); self.vertices.len()];
219        for tri in &self.indices {
220            for k in 0..3 {
221                let a = tri[k];
222                let b = tri[(k + 1) % 3];
223                nbrs[a].insert(b);
224                nbrs[b].insert(a);
225            }
226        }
227        nbrs.into_iter()
228            .map(|s| {
229                let mut v: Vec<usize> = s.into_iter().collect();
230                v.sort_unstable();
231                v
232            })
233            .collect()
234    }
235
236    // ------------------------------------------------------------------
237    // New: edge collapse
238    // ------------------------------------------------------------------
239
240    /// Collapse an edge between vertices `v0` and `v1`, merging them to their
241    /// midpoint. Triangles that degenerate (both endpoints are `v0`/`v1`) are
242    /// removed.
243    ///
244    /// Returns `true` if the edge was found and collapsed.
245    #[allow(dead_code)]
246    pub fn edge_collapse(&mut self, v0: usize, v1: usize) -> bool {
247        // Verify edge exists
248        let edge_found = self
249            .indices
250            .iter()
251            .any(|tri| tri.contains(&v0) && tri.contains(&v1));
252        if !edge_found {
253            return false;
254        }
255
256        // Midpoint
257        let mid = (self.vertices[v0] + self.vertices[v1]) * 0.5;
258        self.vertices[v0] = mid;
259
260        // Replace all references to v1 with v0
261        for tri in &mut self.indices {
262            for idx in tri.iter_mut() {
263                if *idx == v1 {
264                    *idx = v0;
265                }
266            }
267        }
268
269        // Remove degenerate triangles (two or more identical indices)
270        self.indices
271            .retain(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
272
273        true
274    }
275
276    // ------------------------------------------------------------------
277    // New: vertex normal computation (angle-weighted)
278    // ------------------------------------------------------------------
279
280    /// Compute per-vertex normals by accumulating face normals weighted by the
281    /// interior angle at each vertex.
282    #[allow(dead_code)]
283    pub fn compute_vertex_normals(&self) -> Vec<[f64; 3]> {
284        let mut normals = vec![Vec3::zeros(); self.vertices.len()];
285        for tri in &self.indices {
286            let v0 = self.vertices[tri[0]];
287            let v1 = self.vertices[tri[1]];
288            let v2 = self.vertices[tri[2]];
289            let e01 = v1 - v0;
290            let e02 = v2 - v0;
291            let face_n = e01.cross(&e02);
292
293            let angle_at = |va: Vec3, vb: Vec3, vc: Vec3| -> f64 {
294                let ab = (vb - va).normalize();
295                let ac = (vc - va).normalize();
296                ab.dot(&ac).clamp(-1.0, 1.0).acos()
297            };
298
299            let a0 = angle_at(v0, v1, v2);
300            let a1 = angle_at(v1, v2, v0);
301            let a2 = angle_at(v2, v0, v1);
302
303            normals[tri[0]] += face_n * a0;
304            normals[tri[1]] += face_n * a1;
305            normals[tri[2]] += face_n * a2;
306        }
307        normals
308            .iter()
309            .map(|n| {
310                let len = n.norm();
311                if len < 1e-12 {
312                    [0.0, 1.0, 0.0]
313                } else {
314                    [n.x / len, n.y / len, n.z / len]
315                }
316            })
317            .collect()
318    }
319
320    // ------------------------------------------------------------------
321    // New: mesh Laplacian smoothing
322    // ------------------------------------------------------------------
323
324    /// Uniform Laplacian smoothing: move each vertex towards the average of
325    /// its 1-ring neighbours by `factor` (0..1). Boundary vertices are not
326    /// moved.
327    #[allow(dead_code)]
328    pub fn laplacian_smooth(&mut self, factor: f64, iterations: usize) {
329        for _ in 0..iterations {
330            let nbrs = self.vertex_neighbors();
331            let mut new_pos = self.vertices.clone();
332            for (vi, neighbours) in nbrs.iter().enumerate() {
333                if neighbours.is_empty() {
334                    continue;
335                }
336                let mut avg = Vec3::zeros();
337                for &ni in neighbours {
338                    avg += self.vertices[ni];
339                }
340                avg /= neighbours.len() as f64;
341                let diff = avg - self.vertices[vi];
342                new_pos[vi] = self.vertices[vi] + diff * factor;
343            }
344            self.vertices = new_pos;
345        }
346    }
347
348    // ------------------------------------------------------------------
349    // New: approximate geodesic distance (Dijkstra on mesh edges)
350    // ------------------------------------------------------------------
351
352    /// Compute approximate geodesic distances from `source` vertex to all
353    /// other vertices using Dijkstra on edge lengths. Returns a vec of
354    /// distances indexed by vertex, with `f64::INFINITY` for unreachable
355    /// vertices.
356    #[allow(dead_code)]
357    pub fn geodesic_distance(&self, source: usize) -> Vec<f64> {
358        let nbrs = self.vertex_neighbors();
359        let n = self.vertices.len();
360        let mut dist = vec![f64::INFINITY; n];
361        dist[source] = 0.0;
362
363        // Min-heap: (OrderedFloat(dist), vertex)
364        let mut heap = BinaryHeap::new();
365        heap.push(std::cmp::Reverse(OrdF64(0.0, source)));
366
367        while let Some(std::cmp::Reverse(OrdF64(d, u))) = heap.pop() {
368            if d > dist[u] {
369                continue;
370            }
371            for &v in &nbrs[u] {
372                let edge_len = (self.vertices[v] - self.vertices[u]).norm();
373                let new_d = d + edge_len;
374                if new_d < dist[v] {
375                    dist[v] = new_d;
376                    heap.push(std::cmp::Reverse(OrdF64(new_d, v)));
377                }
378            }
379        }
380        dist
381    }
382
383    // ------------------------------------------------------------------
384    // New: Loop subdivision
385    // ------------------------------------------------------------------
386
387    /// Perform one iteration of Loop subdivision.
388    ///
389    /// Each triangle is split into four by inserting edge midpoints (for
390    /// boundary edges) or Loop-weighted edge vertices (for interior edges).
391    /// Existing vertices are repositioned using the Loop weighting scheme.
392    #[allow(dead_code)]
393    pub fn loop_subdivide(&mut self) {
394        let n_verts = self.vertices.len();
395
396        // Build edge -> face count for boundary detection
397        let mut edge_count: HashMap<(usize, usize), 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 key = if a < b { (a, b) } else { (b, a) };
403                *edge_count.entry(key).or_insert(0) += 1;
404            }
405        }
406
407        // Edge midpoint map: canonical edge -> new vertex index
408        let mut edge_verts: HashMap<(usize, usize), usize> = HashMap::new();
409        let mut new_vertices = self.vertices.clone();
410
411        // Build edge -> adjacent triangle vertex sets for Loop weights
412        let mut edge_opposite: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
413        for tri in &self.indices {
414            for k in 0..3 {
415                let a = tri[k];
416                let b = tri[(k + 1) % 3];
417                let c = tri[(k + 2) % 3]; // opposite vertex
418                let key = if a < b { (a, b) } else { (b, a) };
419                edge_opposite.entry(key).or_default().push(c);
420            }
421        }
422
423        let get_or_create_edge_vert = |a: usize,
424                                       b: usize,
425                                       edge_verts: &mut HashMap<(usize, usize), usize>,
426                                       new_vertices: &mut Vec<Vec3>,
427                                       edge_count: &HashMap<(usize, usize), usize>,
428                                       edge_opposite: &HashMap<(usize, usize), Vec<usize>>,
429                                       orig_verts: &[Vec3]|
430         -> usize {
431            let key = if a < b { (a, b) } else { (b, a) };
432            if let Some(&idx) = edge_verts.get(&key) {
433                return idx;
434            }
435            let idx = new_vertices.len();
436            let count = edge_count.get(&key).copied().unwrap_or(1);
437            let pos = if count == 2 {
438                // Interior edge: 3/8*(a+b) + 1/8*(c+d)
439                let opposites = edge_opposite.get(&key).expect("key must exist");
440                if opposites.len() == 2 {
441                    let c = orig_verts[opposites[0]];
442                    let d = orig_verts[opposites[1]];
443                    (orig_verts[a] + orig_verts[b]) * (3.0 / 8.0) + (c + d) * (1.0 / 8.0)
444                } else {
445                    (orig_verts[a] + orig_verts[b]) * 0.5
446                }
447            } else {
448                // Boundary edge: simple midpoint
449                (orig_verts[a] + orig_verts[b]) * 0.5
450            };
451            new_vertices.push(pos);
452            edge_verts.insert(key, idx);
453            idx
454        };
455
456        let orig_verts = self.vertices.clone();
457
458        // Create edge vertices for each triangle
459        let mut new_indices = Vec::with_capacity(self.indices.len() * 4);
460        for tri in &self.indices {
461            let v0 = tri[0];
462            let v1 = tri[1];
463            let v2 = tri[2];
464            let m01 = get_or_create_edge_vert(
465                v0,
466                v1,
467                &mut edge_verts,
468                &mut new_vertices,
469                &edge_count,
470                &edge_opposite,
471                &orig_verts,
472            );
473            let m12 = get_or_create_edge_vert(
474                v1,
475                v2,
476                &mut edge_verts,
477                &mut new_vertices,
478                &edge_count,
479                &edge_opposite,
480                &orig_verts,
481            );
482            let m20 = get_or_create_edge_vert(
483                v2,
484                v0,
485                &mut edge_verts,
486                &mut new_vertices,
487                &edge_count,
488                &edge_opposite,
489                &orig_verts,
490            );
491            // Four sub-triangles
492            new_indices.push([v0, m01, m20]);
493            new_indices.push([v1, m12, m01]);
494            new_indices.push([v2, m20, m12]);
495            new_indices.push([m01, m12, m20]);
496        }
497
498        // Reposition original vertices using Loop weights
499        let nbrs = self.vertex_neighbors();
500        // Determine boundary vertices
501        let mut is_boundary = vec![false; n_verts];
502        for (&(a, b), &cnt) in &edge_count {
503            if cnt == 1 {
504                is_boundary[a] = true;
505                is_boundary[b] = true;
506            }
507        }
508        for vi in 0..n_verts {
509            if is_boundary[vi] {
510                // Boundary: keep position (simple approach)
511                // Could use 3/4 * v + 1/8 * (n1 + n2) for boundary
512                continue;
513            }
514            let n = nbrs[vi].len();
515            if n < 3 {
516                continue;
517            }
518            let beta = if n == 3 {
519                3.0 / 16.0
520            } else {
521                3.0 / (8.0 * n as f64)
522            };
523            let mut neighbour_sum = Vec3::zeros();
524            for &ni in &nbrs[vi] {
525                neighbour_sum += orig_verts[ni];
526            }
527            new_vertices[vi] = orig_verts[vi] * (1.0 - n as f64 * beta) + neighbour_sum * beta;
528        }
529
530        self.vertices = new_vertices;
531        self.indices = new_indices;
532    }
533
534    // ------------------------------------------------------------------
535    // New: boundary edges and Euler characteristic
536    // ------------------------------------------------------------------
537
538    /// Return boundary edges (edges shared by only one triangle).
539    #[allow(dead_code)]
540    pub fn boundary_edges(&self) -> Vec<(usize, usize)> {
541        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
542        for tri in &self.indices {
543            for k in 0..3 {
544                let a = tri[k];
545                let b = tri[(k + 1) % 3];
546                let key = if a < b { (a, b) } else { (b, a) };
547                *edge_count.entry(key).or_insert(0) += 1;
548            }
549        }
550        edge_count
551            .into_iter()
552            .filter(|&(_, c)| c == 1)
553            .map(|(e, _)| e)
554            .collect()
555    }
556
557    /// Compute the Euler characteristic: V - E + F.
558    #[allow(dead_code)]
559    pub fn euler_characteristic(&self) -> i64 {
560        let v = self.vertices.len() as i64;
561        let e = self.unique_edges().len() as i64;
562        let f = self.indices.len() as i64;
563        v - e + f
564    }
565
566    /// Count non-manifold edges (shared by more than 2 triangles).
567    #[allow(dead_code)]
568    pub fn non_manifold_edge_count(&self) -> usize {
569        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
570        for tri in &self.indices {
571            for k in 0..3 {
572                let a = tri[k];
573                let b = tri[(k + 1) % 3];
574                let key = if a < b { (a, b) } else { (b, a) };
575                *edge_count.entry(key).or_insert(0) += 1;
576            }
577        }
578        edge_count.values().filter(|&&c| c > 2).count()
579    }
580
581    // ------------------------------------------------------------------
582    // Cotangent Laplacian
583    // ------------------------------------------------------------------
584
585    /// Compute the cotangent Laplacian weights for each directed edge (i → j).
586    ///
587    /// Returns a `HashMap<(usize, usize), f64>` mapping `(i, j)` to the
588    /// symmetric cotangent weight `w_{ij} = (cot α + cot β) / 2`, where α
589    /// and β are the angles opposite to edge (i,j) in the two incident faces.
590    ///
591    /// This is the standard discretisation used in geometry processing for
592    /// Laplace-Beltrami operators on triangulated surfaces.
593    #[allow(dead_code)]
594    pub fn compute_laplacian_matrix(&self) -> HashMap<(usize, usize), f64> {
595        let mut weights: HashMap<(usize, usize), f64> = HashMap::new();
596
597        for tri in &self.indices {
598            let [i0, i1, i2] = *tri;
599            let p0 = self.vertices[i0];
600            let p1 = self.vertices[i1];
601            let p2 = self.vertices[i2];
602
603            // Cotangent of the angle at each vertex:
604            //   cot(angle at v0) is the angle between edges v0→v1 and v0→v2
605            let cot_at = |a: Vec3, b: Vec3, c: Vec3| -> f64 {
606                // angle at vertex a, between edges a→b and a→c
607                let ab = b - a;
608                let ac = c - a;
609                let cos_t = ab.dot(&ac);
610                let sin_t = ab.cross(&ac).norm();
611                if sin_t.abs() < 1e-12 {
612                    0.0
613                } else {
614                    cos_t / sin_t
615                }
616            };
617
618            let cot0 = cot_at(p0, p1, p2); // angle at i0, opposite edge i1-i2
619            let cot1 = cot_at(p1, p0, p2); // angle at i1, opposite edge i0-i2
620            let cot2 = cot_at(p2, p0, p1); // angle at i2, opposite edge i0-i1
621
622            // Edge (i1, i2): opposite angle at i0
623            *weights.entry((i1, i2)).or_insert(0.0) += 0.5 * cot0;
624            *weights.entry((i2, i1)).or_insert(0.0) += 0.5 * cot0;
625
626            // Edge (i0, i2): opposite angle at i1
627            *weights.entry((i0, i2)).or_insert(0.0) += 0.5 * cot1;
628            *weights.entry((i2, i0)).or_insert(0.0) += 0.5 * cot1;
629
630            // Edge (i0, i1): opposite angle at i2
631            *weights.entry((i0, i1)).or_insert(0.0) += 0.5 * cot2;
632            *weights.entry((i1, i0)).or_insert(0.0) += 0.5 * cot2;
633        }
634
635        weights
636    }
637
638    // ------------------------------------------------------------------
639    // Heat Kernel Signature
640    // ------------------------------------------------------------------
641
642    /// Compute the Heat Kernel Signature (HKS) descriptor for each vertex
643    /// at a set of time scales `t_values`.
644    ///
645    /// The HKS approximates the diagonal of the heat kernel K(x, x, t) using
646    /// the cotangent Laplacian's (normalized) diagonal weights.  The full
647    /// spectral decomposition is expensive, so this implementation uses a
648    /// fast approximation: for each vertex i and time t, it computes
649    ///
650    ///   HKS(i, t) = exp(-t * D\[i\])
651    ///
652    /// where `D[i]` is the sum of cotangent weights incident on vertex i
653    /// (the diagonal of the stiffness matrix, normalised by the vertex area).
654    ///
655    /// Returns a `Vec<Vec`f64`>` of shape `[n_vertices][t_values.len()]`.
656    #[allow(dead_code)]
657    pub fn compute_heat_kernel_signature(&self, t_values: &[f64]) -> Vec<Vec<f64>> {
658        let n = self.vertices.len();
659        if n == 0 || t_values.is_empty() {
660            return vec![vec![0.0; t_values.len()]; n];
661        }
662
663        // Cotangent weights
664        let weights = self.compute_laplacian_matrix();
665
666        // Vertex areas (1/3 of the sum of incident triangle areas)
667        let mut vertex_area = vec![0.0f64; n];
668        for tri in &self.indices {
669            let [i0, i1, i2] = *tri;
670            let a = self.vertices[i0];
671            let b = self.vertices[i1];
672            let c = self.vertices[i2];
673            let area = (b - a).cross(&(c - a)).norm() * 0.5;
674            let a_third = area / 3.0;
675            vertex_area[i0] += a_third;
676            vertex_area[i1] += a_third;
677            vertex_area[i2] += a_third;
678        }
679
680        // Row sum of cotangent weights (diagonal of the stiffness matrix)
681        let mut diag = vec![0.0f64; n];
682        for (&(i, _j), &w) in &weights {
683            diag[i] += w;
684        }
685
686        // Effective frequency per vertex: lambda_i = diag[i] / area[i]
687        let lambda: Vec<f64> = (0..n)
688            .map(|i| {
689                if vertex_area[i] > 1e-15 {
690                    diag[i] / vertex_area[i]
691                } else {
692                    0.0
693                }
694            })
695            .collect();
696
697        // HKS(i, t) = exp(-t * lambda_i)
698        (0..n)
699            .map(|i| t_values.iter().map(|&t| (-t * lambda[i]).exp()).collect())
700            .collect()
701    }
702
703    // ------------------------------------------------------------------
704    // Laplacian smoothing (cotangent weights)
705    // ------------------------------------------------------------------
706
707    /// Iterative Laplacian smoothing using cotangent weights.
708    ///
709    /// Each iteration moves vertex `i` towards the weighted average of its
710    /// neighbours:
711    ///
712    ///   v_i' = v_i + factor * Σ_j w_{ij} * (v_j - v_i) / Σ_j w_{ij}
713    ///
714    /// Boundary vertices (those on edges shared by only one triangle) are
715    /// kept fixed.
716    ///
717    /// `factor` – step size in \[0, 1\]; `iterations` – number of passes.
718    #[allow(dead_code)]
719    pub fn smooth_laplacian(&mut self, factor: f64, iterations: usize) {
720        if self.vertices.is_empty() || self.indices.is_empty() {
721            return;
722        }
723
724        // Identify boundary vertices
725        let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
726        for tri in &self.indices {
727            for k in 0..3 {
728                let a = tri[k];
729                let b = tri[(k + 1) % 3];
730                let key = if a < b { (a, b) } else { (b, a) };
731                *edge_count.entry(key).or_insert(0) += 1;
732            }
733        }
734        let mut is_boundary = vec![false; self.vertices.len()];
735        for (&(a, b), &c) in &edge_count {
736            if c == 1 {
737                is_boundary[a] = true;
738                is_boundary[b] = true;
739            }
740        }
741
742        for _ in 0..iterations {
743            let weights = self.compute_laplacian_matrix();
744            let mut new_vertices = self.vertices.clone();
745
746            for i in 0..self.vertices.len() {
747                if is_boundary[i] {
748                    continue;
749                }
750                let mut weight_sum = 0.0f64;
751                let mut delta = Vec3::zeros();
752
753                for tri in &self.indices {
754                    for k in 0..3 {
755                        if tri[k] == i {
756                            let j = tri[(k + 1) % 3];
757                            let w = *weights.get(&(i, j)).unwrap_or(&0.0);
758                            delta += (self.vertices[j] - self.vertices[i]) * w;
759                            weight_sum += w;
760                        }
761                    }
762                }
763
764                if weight_sum > 1e-15 {
765                    new_vertices[i] += delta * (factor / weight_sum);
766                }
767            }
768
769            self.vertices = new_vertices;
770        }
771    }
772}
773
774// -----------------------------------------------------------------------
775// Helper for Dijkstra heap ordering
776// -----------------------------------------------------------------------
777
778#[derive(Clone, Copy, PartialEq)]
779struct OrdF64(f64, usize);
780
781impl Eq for OrdF64 {}
782
783impl PartialOrd for OrdF64 {
784    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
785        Some(self.cmp(other))
786    }
787}
788
789impl Ord for OrdF64 {
790    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
791        self.0
792            .partial_cmp(&other.0)
793            .unwrap_or(std::cmp::Ordering::Equal)
794    }
795}
796
797// -----------------------------------------------------------------------
798// Shape trait implementation
799// -----------------------------------------------------------------------
800
801impl Shape for TriangleMesh {
802    fn bounding_box(&self) -> Aabb {
803        if self.vertices.is_empty() {
804            return Aabb::new(Vec3::zeros(), Vec3::zeros());
805        }
806        let mut min = self.vertices[0];
807        let mut max = self.vertices[0];
808        for v in &self.vertices[1..] {
809            min = min.inf(v);
810            max = max.sup(v);
811        }
812        Aabb::new(min, max)
813    }
814
815    fn support_point(&self, direction: &Vec3) -> Vec3 {
816        self.vertices
817            .iter()
818            .max_by(|a, b| {
819                a.dot(direction)
820                    .partial_cmp(&b.dot(direction))
821                    .unwrap_or(std::cmp::Ordering::Equal)
822            })
823            .copied()
824            .unwrap_or_else(Vec3::zeros)
825    }
826
827    fn volume(&self) -> Real {
828        let mut vol = 0.0;
829        for tri in &self.indices {
830            let a = self.vertices[tri[0]];
831            let b = self.vertices[tri[1]];
832            let c = self.vertices[tri[2]];
833            vol += a.dot(&b.cross(&c)) / 6.0;
834        }
835        vol.abs()
836    }
837
838    fn center_of_mass(&self) -> Vec3 {
839        if self.indices.is_empty() {
840            return Vec3::zeros();
841        }
842        let mut weighted = Vec3::zeros();
843        let mut total_vol = 0.0;
844        for tri in &self.indices {
845            let a = self.vertices[tri[0]];
846            let b = self.vertices[tri[1]];
847            let c = self.vertices[tri[2]];
848            let tet_vol = a.dot(&b.cross(&c)) / 6.0;
849            let tet_center = (a + b + c) * 0.25;
850            weighted += tet_center * tet_vol;
851            total_vol += tet_vol;
852        }
853        if total_vol.abs() > 1e-12 {
854            weighted / total_vol
855        } else {
856            Vec3::zeros()
857        }
858    }
859
860    fn inertia_tensor(&self, mass: Real) -> Mat3 {
861        let bb = self.bounding_box();
862        let size = bb.max - bb.min;
863        let k = mass / 12.0;
864        Mat3::new(
865            k * (size.y * size.y + size.z * size.z),
866            0.0,
867            0.0,
868            0.0,
869            k * (size.x * size.x + size.z * size.z),
870            0.0,
871            0.0,
872            0.0,
873            k * (size.x * size.x + size.y * size.y),
874        )
875    }
876
877    fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
878        let mut best: Option<RayHit> = None;
879        for tri in &self.indices {
880            let v0 = self.vertices[tri[0]];
881            let v1 = self.vertices[tri[1]];
882            let v2 = self.vertices[tri[2]];
883            if let Some(hit) = ray_triangle(ray_origin, ray_direction, &v0, &v1, &v2)
884                && hit.toi <= max_toi
885                && hit.toi >= 0.0
886                && best.as_ref().is_none_or(|b| hit.toi < b.toi)
887            {
888                best = Some(hit);
889            }
890        }
891        best
892    }
893}
894
895/// Moller-Trumbore ray-triangle intersection.
896fn ray_triangle(
897    origin: &Vec3,
898    direction: &Vec3,
899    v0: &Vec3,
900    v1: &Vec3,
901    v2: &Vec3,
902) -> Option<RayHit> {
903    let edge1 = v1 - v0;
904    let edge2 = v2 - v0;
905    let h = direction.cross(&edge2);
906    let a = edge1.dot(&h);
907
908    if a.abs() < 1e-10 {
909        return None;
910    }
911
912    let f = 1.0 / a;
913    let s = origin - v0;
914    let u = f * s.dot(&h);
915    if !(0.0..=1.0).contains(&u) {
916        return None;
917    }
918
919    let q = s.cross(&edge1);
920    let v = f * direction.dot(&q);
921    if v < 0.0 || u + v > 1.0 {
922        return None;
923    }
924
925    let t = f * edge2.dot(&q);
926    if t < 0.0 {
927        return None;
928    }
929
930    let point = origin + direction * t;
931    let normal = edge1.cross(&edge2).normalize();
932    Some(RayHit {
933        point,
934        normal,
935        toi: t,
936    })
937}
938
939/// Build a simple unit cube mesh (12 triangles, watertight).
940#[cfg(test)]
941fn unit_cube_mesh() -> TriangleMesh {
942    let v = vec![
943        Vec3::new(0.0, 0.0, 0.0), // 0
944        Vec3::new(1.0, 0.0, 0.0), // 1
945        Vec3::new(1.0, 1.0, 0.0), // 2
946        Vec3::new(0.0, 1.0, 0.0), // 3
947        Vec3::new(0.0, 0.0, 1.0), // 4
948        Vec3::new(1.0, 0.0, 1.0), // 5
949        Vec3::new(1.0, 1.0, 1.0), // 6
950        Vec3::new(0.0, 1.0, 1.0), // 7
951    ];
952    let idx = vec![
953        // -Z face (z=0)
954        [0, 2, 1],
955        [0, 3, 2],
956        // +Z face (z=1)
957        [4, 5, 6],
958        [4, 6, 7],
959        // -X face (x=0)
960        [0, 4, 7],
961        [0, 7, 3],
962        // +X face (x=1)
963        [1, 2, 6],
964        [1, 6, 5],
965        // -Y face (y=0)
966        [0, 1, 5],
967        [0, 5, 4],
968        // +Y face (y=1)
969        [3, 7, 6],
970        [3, 6, 2],
971    ];
972    TriangleMesh::new(v, idx)
973}
974
975/// Build a simple tetrahedron mesh (4 triangles, watertight).
976#[cfg(test)]
977fn tetrahedron_mesh() -> TriangleMesh {
978    let v = vec![
979        Vec3::new(1.0, 1.0, 1.0),
980        Vec3::new(-1.0, -1.0, 1.0),
981        Vec3::new(-1.0, 1.0, -1.0),
982        Vec3::new(1.0, -1.0, -1.0),
983    ];
984    let idx = vec![[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]];
985    TriangleMesh::new(v, idx)
986}
987
988#[cfg(test)]
989mod tests {
990    use super::*;
991    use crate::TriangleMesh;
992    use oxiphysics_core::math::Vec3;
993
994    #[test]
995    fn test_triangle_mesh_raycast() {
996        let mesh = TriangleMesh::new(
997            vec![
998                Vec3::new(-1.0, 0.0, -1.0),
999                Vec3::new(1.0, 0.0, -1.0),
1000                Vec3::new(0.0, 0.0, 1.0),
1001            ],
1002            vec![[0, 1, 2]],
1003        );
1004        let origin = Vec3::new(0.0, 5.0, 0.0);
1005        let dir = Vec3::new(0.0, -1.0, 0.0);
1006        let hit = mesh.ray_cast(&origin, &dir, 100.0).unwrap();
1007        assert!((hit.toi - 5.0).abs() < 1e-10);
1008    }
1009
1010    #[test]
1011    fn test_triangle_mesh_raycast_miss() {
1012        let mesh = TriangleMesh::new(
1013            vec![
1014                Vec3::new(-1.0, 0.0, -1.0),
1015                Vec3::new(1.0, 0.0, -1.0),
1016                Vec3::new(0.0, 0.0, 1.0),
1017            ],
1018            vec![[0, 1, 2]],
1019        );
1020        let origin = Vec3::new(10.0, 5.0, 0.0);
1021        let dir = Vec3::new(0.0, -1.0, 0.0);
1022        assert!(mesh.ray_cast(&origin, &dir, 100.0).is_none());
1023    }
1024
1025    #[test]
1026    fn test_triangle_mesh_cube_volume() {
1027        let mesh = unit_cube_mesh();
1028        let vol = mesh.volume();
1029        assert!((vol - 1.0).abs() < 1e-6, "expected volume=1, got {}", vol);
1030    }
1031
1032    #[test]
1033    fn test_triangle_mesh_cube_surface_area() {
1034        let mesh = unit_cube_mesh();
1035        let sa = mesh.surface_area();
1036        assert!((sa - 6.0).abs() < 1e-6, "expected SA=6, got {}", sa);
1037    }
1038
1039    #[test]
1040    fn test_triangle_mesh_cube_is_watertight() {
1041        let mesh = unit_cube_mesh();
1042        assert!(mesh.is_watertight(), "unit cube should be watertight");
1043    }
1044
1045    #[test]
1046    fn test_triangle_mesh_open_not_watertight() {
1047        let mesh = TriangleMesh::new(
1048            vec![
1049                Vec3::new(0.0, 0.0, 0.0),
1050                Vec3::new(1.0, 0.0, 0.0),
1051                Vec3::new(0.0, 1.0, 0.0),
1052            ],
1053            vec![[0, 1, 2]],
1054        );
1055        assert!(!mesh.is_watertight(), "open mesh should not be watertight");
1056    }
1057
1058    #[test]
1059    fn test_triangle_mesh_compute_normals() {
1060        let mesh = TriangleMesh::new(
1061            vec![
1062                Vec3::new(0.0, 0.0, 0.0),
1063                Vec3::new(1.0, 0.0, 0.0),
1064                Vec3::new(0.0, 1.0, 0.0),
1065            ],
1066            vec![[0, 1, 2]],
1067        );
1068        let normals = mesh.compute_normals();
1069        assert_eq!(normals.len(), 1);
1070        assert!((normals[0][2] - 1.0).abs() < 1e-10, "expected +Z normal");
1071    }
1072
1073    #[test]
1074    fn test_triangle_mesh_ray_cast_full() {
1075        let mesh = unit_cube_mesh();
1076        let (t, _face, n) = mesh
1077            .ray_cast_full([0.5, 5.0, 0.5], [0.0, -1.0, 0.0], 100.0)
1078            .expect("should hit cube");
1079        assert!((t - 4.0).abs() < 1e-6, "expected t=4, got {}", t);
1080        assert!(n[1].abs() > 0.9, "normal should be roughly Y");
1081    }
1082
1083    #[test]
1084    fn test_triangle_mesh_surface_area_triangle() {
1085        let mesh = TriangleMesh::new(
1086            vec![
1087                Vec3::new(0.0, 0.0, 0.0),
1088                Vec3::new(3.0, 0.0, 0.0),
1089                Vec3::new(0.0, 4.0, 0.0),
1090            ],
1091            vec![[0, 1, 2]],
1092        );
1093        let sa = mesh.surface_area();
1094        assert!((sa - 6.0).abs() < 1e-10, "expected SA=6, got {}", sa);
1095    }
1096
1097    // ------------------------------------------------------------------
1098    // New tests: adjacency queries
1099    // ------------------------------------------------------------------
1100
1101    #[test]
1102    fn test_vertex_to_faces() {
1103        let mesh = unit_cube_mesh();
1104        let v2f = mesh.vertex_to_faces();
1105        assert_eq!(v2f.len(), 8);
1106        // Each vertex of a cube is shared by 3 faces (each face = 2 tris,
1107        // each vertex appears in exactly 3 faces = 6 tris / 2, but actually
1108        // each vertex is used by 3*2=6? Let's just check non-empty).
1109        for faces in &v2f {
1110            assert!(
1111                !faces.is_empty(),
1112                "every vertex should have at least one face"
1113            );
1114        }
1115        // Vertex 0 appears in -Z, -X, -Y faces (6 triangles)
1116        // Indices: [0,2,1],[0,3,2],[0,4,7],[0,7,3],[0,1,5],[0,5,4]
1117        assert_eq!(
1118            v2f[0].len(),
1119            6,
1120            "vertex 0 in unit cube should appear in 6 triangles"
1121        );
1122    }
1123
1124    #[test]
1125    fn test_face_adjacency_single_triangle() {
1126        let mesh = TriangleMesh::new(
1127            vec![
1128                Vec3::new(0.0, 0.0, 0.0),
1129                Vec3::new(1.0, 0.0, 0.0),
1130                Vec3::new(0.0, 1.0, 0.0),
1131            ],
1132            vec![[0, 1, 2]],
1133        );
1134        let adj = mesh.face_adjacency();
1135        assert_eq!(adj.len(), 1);
1136        assert!(adj[0].is_empty(), "single triangle has no adjacent faces");
1137    }
1138
1139    #[test]
1140    fn test_face_adjacency_cube() {
1141        let mesh = unit_cube_mesh();
1142        let adj = mesh.face_adjacency();
1143        assert_eq!(adj.len(), 12);
1144        // Each triangle in a cube shares edges with other triangles
1145        for neighbors in &adj {
1146            assert!(
1147                !neighbors.is_empty(),
1148                "each cube triangle should have neighbors"
1149            );
1150        }
1151    }
1152
1153    #[test]
1154    fn test_unique_edges_single_triangle() {
1155        let mesh = TriangleMesh::new(
1156            vec![
1157                Vec3::new(0.0, 0.0, 0.0),
1158                Vec3::new(1.0, 0.0, 0.0),
1159                Vec3::new(0.0, 1.0, 0.0),
1160            ],
1161            vec![[0, 1, 2]],
1162        );
1163        let edges = mesh.unique_edges();
1164        assert_eq!(edges.len(), 3);
1165    }
1166
1167    #[test]
1168    fn test_unique_edges_cube() {
1169        let mesh = unit_cube_mesh();
1170        let edges = mesh.unique_edges();
1171        // A cube has 12 edges + 6 face diagonals = 18
1172        assert_eq!(edges.len(), 18);
1173    }
1174
1175    #[test]
1176    fn test_vertex_neighbors() {
1177        let mesh = TriangleMesh::new(
1178            vec![
1179                Vec3::new(0.0, 0.0, 0.0),
1180                Vec3::new(1.0, 0.0, 0.0),
1181                Vec3::new(0.5, 1.0, 0.0),
1182            ],
1183            vec![[0, 1, 2]],
1184        );
1185        let nbrs = mesh.vertex_neighbors();
1186        assert_eq!(nbrs.len(), 3);
1187        assert_eq!(nbrs[0].len(), 2); // connected to 1 and 2
1188        assert_eq!(nbrs[1].len(), 2);
1189        assert_eq!(nbrs[2].len(), 2);
1190    }
1191
1192    // ------------------------------------------------------------------
1193    // New tests: edge collapse
1194    // ------------------------------------------------------------------
1195
1196    #[test]
1197    fn test_edge_collapse_basic() {
1198        // Two triangles sharing edge (0,1)
1199        let mut mesh = TriangleMesh::new(
1200            vec![
1201                Vec3::new(0.0, 0.0, 0.0),
1202                Vec3::new(1.0, 0.0, 0.0),
1203                Vec3::new(0.5, 1.0, 0.0),
1204                Vec3::new(0.5, -1.0, 0.0),
1205            ],
1206            vec![[0, 1, 2], [0, 3, 1]],
1207        );
1208        let ok = mesh.edge_collapse(0, 1);
1209        assert!(ok, "edge collapse should succeed");
1210        // After collapse, vertex 0 should be at midpoint (0.5, 0, 0)
1211        assert!((mesh.vertices[0].x - 0.5).abs() < 1e-10);
1212        // Triangles that degenerate should be removed
1213        // Both triangles had vertices 0 and 1, which are now both 0,
1214        // so they become degenerate -> removed? No: [0,1,2] becomes [0,0,2] -> degenerate.
1215        // [0,3,1] becomes [0,3,0] -> degenerate.
1216        assert!(mesh.indices.is_empty(), "both triangles should degenerate");
1217    }
1218
1219    #[test]
1220    fn test_edge_collapse_nonexistent() {
1221        let mut mesh = TriangleMesh::new(
1222            vec![
1223                Vec3::new(0.0, 0.0, 0.0),
1224                Vec3::new(1.0, 0.0, 0.0),
1225                Vec3::new(0.5, 1.0, 0.0),
1226            ],
1227            vec![[0, 1, 2]],
1228        );
1229        // Edge (0, 100) doesn't exist
1230        assert!(!mesh.edge_collapse(0, 100));
1231    }
1232
1233    #[test]
1234    fn test_edge_collapse_preserves_other_faces() {
1235        // Three triangles, collapse edge shared by first two
1236        let mut mesh = TriangleMesh::new(
1237            vec![
1238                Vec3::new(0.0, 0.0, 0.0),  // 0
1239                Vec3::new(2.0, 0.0, 0.0),  // 1
1240                Vec3::new(1.0, 1.0, 0.0),  // 2
1241                Vec3::new(1.0, -1.0, 0.0), // 3
1242                Vec3::new(3.0, 1.0, 0.0),  // 4
1243            ],
1244            vec![[0, 1, 2], [0, 3, 1], [1, 4, 2]],
1245        );
1246        let ok = mesh.edge_collapse(0, 1);
1247        assert!(ok);
1248        // Triangle [1,4,2] references v1 which is now v0
1249        // It becomes [0,4,2] which is non-degenerate
1250        assert_eq!(
1251            mesh.indices.len(),
1252            1,
1253            "two degenerate tris removed, one survives"
1254        );
1255    }
1256
1257    // ------------------------------------------------------------------
1258    // New tests: vertex normals
1259    // ------------------------------------------------------------------
1260
1261    #[test]
1262    fn test_vertex_normals_flat_surface() {
1263        let mesh = TriangleMesh::new(
1264            vec![
1265                Vec3::new(0.0, 0.0, 0.0),
1266                Vec3::new(1.0, 0.0, 0.0),
1267                Vec3::new(1.0, 1.0, 0.0),
1268                Vec3::new(0.0, 1.0, 0.0),
1269            ],
1270            vec![[0, 1, 2], [0, 2, 3]],
1271        );
1272        let normals = mesh.compute_vertex_normals();
1273        assert_eq!(normals.len(), 4);
1274        for n in &normals {
1275            assert!(
1276                (n[2] - 1.0).abs() < 1e-6,
1277                "flat XY surface normal should be +Z"
1278            );
1279        }
1280    }
1281
1282    #[test]
1283    fn test_vertex_normals_unit_length() {
1284        let mesh = unit_cube_mesh();
1285        let normals = mesh.compute_vertex_normals();
1286        for n in &normals {
1287            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1288            assert!(
1289                (len - 1.0).abs() < 1e-6,
1290                "vertex normal should be unit length, got {}",
1291                len
1292            );
1293        }
1294    }
1295
1296    // ------------------------------------------------------------------
1297    // New tests: Laplacian smoothing
1298    // ------------------------------------------------------------------
1299
1300    #[test]
1301    fn test_laplacian_smooth_doesnt_crash() {
1302        let mut mesh = unit_cube_mesh();
1303        mesh.laplacian_smooth(0.5, 3);
1304        // After smoothing the cube should still have 8 vertices and 12 faces
1305        assert_eq!(mesh.vertices.len(), 8);
1306        assert_eq!(mesh.indices.len(), 12);
1307    }
1308
1309    #[test]
1310    fn test_laplacian_smooth_converges_to_center() {
1311        // A mesh with one interior vertex should move towards neighbors
1312        let mut mesh = TriangleMesh::new(
1313            vec![
1314                Vec3::new(0.0, 0.0, 0.0), // 0
1315                Vec3::new(2.0, 0.0, 0.0), // 1
1316                Vec3::new(1.0, 2.0, 0.0), // 2
1317                Vec3::new(1.0, 0.5, 0.0), // 3 (interior-ish vertex)
1318            ],
1319            vec![[0, 1, 3], [1, 2, 3], [2, 0, 3]],
1320        );
1321        let orig_v3 = mesh.vertices[3];
1322        mesh.laplacian_smooth(1.0, 1);
1323        // Vertex 3 should have moved towards the average of its neighbors (0,1,2)
1324        let avg =
1325            (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;
1326        let new_v3 = mesh.vertices[3];
1327        let d_new = (new_v3 - avg).norm();
1328        let d_old = (orig_v3 - avg).norm();
1329        assert!(
1330            d_new < d_old + 1e-10,
1331            "vertex should move towards neighbor average"
1332        );
1333    }
1334
1335    // ------------------------------------------------------------------
1336    // New tests: geodesic distance
1337    // ------------------------------------------------------------------
1338
1339    #[test]
1340    fn test_geodesic_distance_source() {
1341        let mesh = unit_cube_mesh();
1342        let dist = mesh.geodesic_distance(0);
1343        assert_eq!(dist[0], 0.0, "distance to self should be 0");
1344    }
1345
1346    #[test]
1347    fn test_geodesic_distance_adjacent() {
1348        let mesh = TriangleMesh::new(
1349            vec![
1350                Vec3::new(0.0, 0.0, 0.0),
1351                Vec3::new(1.0, 0.0, 0.0),
1352                Vec3::new(0.5, 1.0, 0.0),
1353            ],
1354            vec![[0, 1, 2]],
1355        );
1356        let dist = mesh.geodesic_distance(0);
1357        // Distance from 0 to 1 should be 1.0
1358        assert!(
1359            (dist[1] - 1.0).abs() < 1e-10,
1360            "expected dist=1, got {}",
1361            dist[1]
1362        );
1363    }
1364
1365    #[test]
1366    fn test_geodesic_distance_symmetry() {
1367        let mesh = unit_cube_mesh();
1368        let d0 = mesh.geodesic_distance(0);
1369        let d6 = mesh.geodesic_distance(6);
1370        // Distance 0->6 should equal distance 6->0
1371        assert!(
1372            (d0[6] - d6[0]).abs() < 1e-10,
1373            "geodesic distance should be symmetric: {} vs {}",
1374            d0[6],
1375            d6[0]
1376        );
1377    }
1378
1379    // ------------------------------------------------------------------
1380    // New tests: Loop subdivision
1381    // ------------------------------------------------------------------
1382
1383    #[test]
1384    fn test_loop_subdivide_increases_faces() {
1385        let mut mesh = TriangleMesh::new(
1386            vec![
1387                Vec3::new(0.0, 0.0, 0.0),
1388                Vec3::new(1.0, 0.0, 0.0),
1389                Vec3::new(0.5, 1.0, 0.0),
1390            ],
1391            vec![[0, 1, 2]],
1392        );
1393        mesh.loop_subdivide();
1394        assert_eq!(mesh.indices.len(), 4, "one triangle should become four");
1395    }
1396
1397    #[test]
1398    fn test_loop_subdivide_cube() {
1399        let mut mesh = unit_cube_mesh();
1400        assert_eq!(mesh.indices.len(), 12);
1401        mesh.loop_subdivide();
1402        assert_eq!(mesh.indices.len(), 48, "12 tris * 4 = 48");
1403        // Should still have consistent structure
1404        assert!(
1405            mesh.vertices.len() > 8,
1406            "should have more vertices after subdivision"
1407        );
1408    }
1409
1410    #[test]
1411    fn test_loop_subdivide_watertight_preserved() {
1412        let mut mesh = tetrahedron_mesh();
1413        assert!(mesh.is_watertight(), "tetrahedron should be watertight");
1414        mesh.loop_subdivide();
1415        assert!(
1416            mesh.is_watertight(),
1417            "subdivided tetrahedron should still be watertight"
1418        );
1419    }
1420
1421    // ------------------------------------------------------------------
1422    // New tests: boundary edges, Euler characteristic, non-manifold
1423    // ------------------------------------------------------------------
1424
1425    #[test]
1426    fn test_boundary_edges_closed_mesh() {
1427        let mesh = unit_cube_mesh();
1428        let be = mesh.boundary_edges();
1429        assert!(
1430            be.is_empty(),
1431            "watertight mesh should have no boundary edges"
1432        );
1433    }
1434
1435    #[test]
1436    fn test_boundary_edges_open_mesh() {
1437        let mesh = TriangleMesh::new(
1438            vec![
1439                Vec3::new(0.0, 0.0, 0.0),
1440                Vec3::new(1.0, 0.0, 0.0),
1441                Vec3::new(0.5, 1.0, 0.0),
1442            ],
1443            vec![[0, 1, 2]],
1444        );
1445        let be = mesh.boundary_edges();
1446        assert_eq!(be.len(), 3, "single triangle has 3 boundary edges");
1447    }
1448
1449    #[test]
1450    fn test_euler_characteristic_cube() {
1451        let mesh = unit_cube_mesh();
1452        let chi = mesh.euler_characteristic();
1453        assert_eq!(chi, 2, "closed cube Euler characteristic should be 2");
1454    }
1455
1456    #[test]
1457    fn test_euler_characteristic_tetrahedron() {
1458        let mesh = tetrahedron_mesh();
1459        let chi = mesh.euler_characteristic();
1460        assert_eq!(
1461            chi, 2,
1462            "closed tetrahedron Euler characteristic should be 2"
1463        );
1464    }
1465
1466    #[test]
1467    fn test_non_manifold_edge_count_clean_mesh() {
1468        let mesh = unit_cube_mesh();
1469        assert_eq!(mesh.non_manifold_edge_count(), 0);
1470    }
1471
1472    #[test]
1473    fn test_non_manifold_edge_count_with_extra_face() {
1474        // Add a third triangle sharing an edge -> non-manifold
1475        let mesh = TriangleMesh::new(
1476            vec![
1477                Vec3::new(0.0, 0.0, 0.0),
1478                Vec3::new(1.0, 0.0, 0.0),
1479                Vec3::new(0.5, 1.0, 0.0),
1480                Vec3::new(0.5, -1.0, 0.0),
1481                Vec3::new(0.5, 0.5, 1.0),
1482            ],
1483            vec![[0, 1, 2], [0, 3, 1], [0, 4, 1]], // edge (0,1) shared by 3 faces
1484        );
1485        assert_eq!(mesh.non_manifold_edge_count(), 1);
1486    }
1487}
1488
1489// ---------------------------------------------------------------------------
1490// Tests for cotangent Laplacian, HKS, and smooth_laplacian
1491// ---------------------------------------------------------------------------
1492
1493#[cfg(test)]
1494mod tests_laplacian_hks {
1495
1496    use crate::TriangleMesh;
1497    use oxiphysics_core::math::Vec3;
1498
1499    /// Flat quad made of two triangles: 4 vertices, 2 triangles.
1500    fn flat_quad() -> TriangleMesh {
1501        TriangleMesh::new(
1502            vec![
1503                Vec3::new(0.0, 0.0, 0.0),
1504                Vec3::new(1.0, 0.0, 0.0),
1505                Vec3::new(1.0, 1.0, 0.0),
1506                Vec3::new(0.0, 1.0, 0.0),
1507            ],
1508            vec![[0, 1, 2], [0, 2, 3]],
1509        )
1510    }
1511
1512    /// Single triangle.
1513    fn single_triangle() -> TriangleMesh {
1514        TriangleMesh::new(
1515            vec![
1516                Vec3::new(0.0, 0.0, 0.0),
1517                Vec3::new(1.0, 0.0, 0.0),
1518                Vec3::new(0.5, 1.0, 0.0),
1519            ],
1520            vec![[0, 1, 2]],
1521        )
1522    }
1523
1524    // ── compute_laplacian_matrix ─────────────────────────────────────────────
1525
1526    #[test]
1527    fn test_laplacian_matrix_nonempty() {
1528        let mesh = flat_quad();
1529        let w = mesh.compute_laplacian_matrix();
1530        assert!(!w.is_empty(), "Laplacian weights should be non-empty");
1531    }
1532
1533    #[test]
1534    fn test_laplacian_matrix_symmetric() {
1535        let mesh = flat_quad();
1536        let w = mesh.compute_laplacian_matrix();
1537        // For every (i,j) entry there should be a matching (j,i)
1538        for (&(i, j), &wij) in &w {
1539            let wji = w.get(&(j, i)).copied().unwrap_or(0.0);
1540            assert!(
1541                (wij - wji).abs() < 1e-10,
1542                "Laplacian not symmetric at ({i},{j}): {wij} vs {wji}"
1543            );
1544        }
1545    }
1546
1547    #[test]
1548    fn test_laplacian_matrix_single_triangle_all_edges_present() {
1549        let mesh = single_triangle();
1550        let w = mesh.compute_laplacian_matrix();
1551        // 3 undirected edges × 2 directions = 6 entries
1552        assert_eq!(
1553            w.len(),
1554            6,
1555            "single triangle should produce 6 directed entries"
1556        );
1557    }
1558
1559    #[test]
1560    fn test_laplacian_matrix_empty_mesh() {
1561        let mesh = TriangleMesh::new(vec![], vec![]);
1562        let w = mesh.compute_laplacian_matrix();
1563        assert!(w.is_empty());
1564    }
1565
1566    // ── compute_heat_kernel_signature ────────────────────────────────────────
1567
1568    #[test]
1569    fn test_hks_output_shape() {
1570        let mesh = flat_quad();
1571        let t_vals = vec![0.1, 1.0, 10.0];
1572        let hks = mesh.compute_heat_kernel_signature(&t_vals);
1573        assert_eq!(hks.len(), mesh.vertices.len(), "one HKS row per vertex");
1574        for row in &hks {
1575            assert_eq!(row.len(), t_vals.len(), "one entry per time scale");
1576        }
1577    }
1578
1579    #[test]
1580    fn test_hks_values_in_range() {
1581        // HKS = exp(-t * lambda) is always in (0, 1] for t > 0, lambda >= 0
1582        let mesh = flat_quad();
1583        let t_vals = vec![0.01, 0.1, 1.0, 10.0];
1584        let hks = mesh.compute_heat_kernel_signature(&t_vals);
1585        for row in &hks {
1586            for &val in row {
1587                assert!(
1588                    val > 0.0 && val <= 1.0 + 1e-12,
1589                    "HKS value out of range: {val}"
1590                );
1591            }
1592        }
1593    }
1594
1595    #[test]
1596    fn test_hks_decreasing_with_time() {
1597        // For any vertex, HKS(i, t1) >= HKS(i, t2) when t1 < t2
1598        let mesh = flat_quad();
1599        let t_vals = vec![0.1, 1.0, 10.0];
1600        let hks = mesh.compute_heat_kernel_signature(&t_vals);
1601        for (vi, row) in hks.iter().enumerate() {
1602            for k in 0..(row.len() - 1) {
1603                assert!(
1604                    row[k] >= row[k + 1] - 1e-12,
1605                    "HKS not decreasing for vertex {vi}: t={}, val={}; t={}, val={}",
1606                    t_vals[k],
1607                    row[k],
1608                    t_vals[k + 1],
1609                    row[k + 1]
1610                );
1611            }
1612        }
1613    }
1614
1615    #[test]
1616    fn test_hks_empty_mesh() {
1617        let mesh = TriangleMesh::new(vec![], vec![]);
1618        let hks = mesh.compute_heat_kernel_signature(&[1.0, 2.0]);
1619        assert!(hks.is_empty());
1620    }
1621
1622    // ── smooth_laplacian ────────────────────────────────────────────────────
1623
1624    #[test]
1625    fn test_smooth_laplacian_vertex_count_unchanged() {
1626        let mut mesh = flat_quad();
1627        let n_before = mesh.vertices.len();
1628        mesh.smooth_laplacian(0.5, 3);
1629        assert_eq!(
1630            mesh.vertices.len(),
1631            n_before,
1632            "smoothing must not add/remove vertices"
1633        );
1634    }
1635
1636    #[test]
1637    fn test_smooth_laplacian_triangle_count_unchanged() {
1638        let mut mesh = flat_quad();
1639        let n_before = mesh.indices.len();
1640        mesh.smooth_laplacian(0.5, 3);
1641        assert_eq!(
1642            mesh.indices.len(),
1643            n_before,
1644            "smoothing must not change connectivity"
1645        );
1646    }
1647
1648    #[test]
1649    fn test_smooth_laplacian_zero_iterations_no_change() {
1650        let mut mesh = flat_quad();
1651        let verts_before: Vec<Vec3> = mesh.vertices.clone();
1652        mesh.smooth_laplacian(0.5, 0);
1653        for (a, b) in verts_before.iter().zip(mesh.vertices.iter()) {
1654            assert!(
1655                (a - b).norm() < 1e-12,
1656                "zero iterations should not move vertices"
1657            );
1658        }
1659    }
1660
1661    #[test]
1662    fn test_smooth_laplacian_boundary_fixed() {
1663        // For a single triangle (all boundary) smoothing must not move any vertex
1664        let mut mesh = single_triangle();
1665        let verts_before: Vec<Vec3> = mesh.vertices.clone();
1666        mesh.smooth_laplacian(0.5, 5);
1667        for (a, b) in verts_before.iter().zip(mesh.vertices.iter()) {
1668            assert!(
1669                (a - b).norm() < 1e-12,
1670                "boundary vertices must not move, delta={}",
1671                (a - b).norm()
1672            );
1673        }
1674    }
1675}