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    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    /// Volume via signed tetrahedral decomposition (divergence theorem).
42    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    /// Center of mass via weighted tetrahedral centroids.
54    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    /// Compute per-face normals (unit vectors).
78    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    /// Ray cast returning (t, face_index, normal) via Moller-Trumbore for all triangles.
99    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    /// Check whether the mesh is watertight (every edge is shared by exactly two triangles).
127    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    // ------------------------------------------------------------------
144    // New: mesh adjacency queries
145    // ------------------------------------------------------------------
146
147    /// Build vertex-to-face adjacency: for each vertex index, the list of
148    /// triangle indices that reference it.
149    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    /// Build face-to-face adjacency via shared edges.
160    /// Returns a vec of the same length as `self.indices`. Each entry contains
161    /// the indices of adjacent faces (those sharing at least one edge).
162    pub fn face_adjacency(&self) -> Vec<Vec<usize>> {
163        // edge -> list of face indices
164        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        // Deduplicate
183        for v in &mut adj {
184            v.sort_unstable();
185            v.dedup();
186        }
187        adj
188    }
189
190    /// Return the set of unique edges as sorted (min,max) vertex index pairs.
191    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    /// Build vertex-to-vertex adjacency (1-ring neighbours).
207    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    // ------------------------------------------------------------------
227    // New: edge collapse
228    // ------------------------------------------------------------------
229
230    /// Collapse an edge between vertices `v0` and `v1`, merging them to their
231    /// midpoint. Triangles that degenerate (both endpoints are `v0`/`v1`) are
232    /// removed.
233    ///
234    /// Returns `true` if the edge was found and collapsed.
235    pub fn edge_collapse(&mut self, v0: usize, v1: usize) -> bool {
236        // Verify edge exists
237        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        // Midpoint
246        let mid = (self.vertices[v0] + self.vertices[v1]) * 0.5;
247        self.vertices[v0] = mid;
248
249        // Replace all references to v1 with v0
250        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        // Remove degenerate triangles (two or more identical indices)
259        self.indices
260            .retain(|tri| tri[0] != tri[1] && tri[1] != tri[2] && tri[0] != tri[2]);
261
262        true
263    }
264
265    // ------------------------------------------------------------------
266    // New: vertex normal computation (angle-weighted)
267    // ------------------------------------------------------------------
268
269    /// Compute per-vertex normals by accumulating face normals weighted by the
270    /// interior angle at each vertex.
271    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    // ------------------------------------------------------------------
309    // New: mesh Laplacian smoothing
310    // ------------------------------------------------------------------
311
312    /// Uniform Laplacian smoothing: move each vertex towards the average of
313    /// its 1-ring neighbours by `factor` (0..1). Boundary vertices are not
314    /// moved.
315    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    // ------------------------------------------------------------------
336    // New: approximate geodesic distance (Dijkstra on mesh edges)
337    // ------------------------------------------------------------------
338
339    /// Compute approximate geodesic distances from `source` vertex to all
340    /// other vertices using Dijkstra on edge lengths. Returns a vec of
341    /// distances indexed by vertex, with `f64::INFINITY` for unreachable
342    /// vertices.
343    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        // Min-heap: (OrderedFloat(dist), vertex)
350        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    // ------------------------------------------------------------------
370    // New: Loop subdivision
371    // ------------------------------------------------------------------
372
373    /// Perform one iteration of Loop subdivision.
374    ///
375    /// Each triangle is split into four by inserting edge midpoints (for
376    /// boundary edges) or Loop-weighted edge vertices (for interior edges).
377    /// Existing vertices are repositioned using the Loop weighting scheme.
378    pub fn loop_subdivide(&mut self) {
379        let n_verts = self.vertices.len();
380
381        // Build edge -> face count for boundary detection
382        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        // Edge midpoint map: canonical edge -> new vertex index
393        let mut edge_verts: HashMap<(usize, usize), usize> = HashMap::new();
394        let mut new_vertices = self.vertices.clone();
395
396        // Build edge -> adjacent triangle vertex sets for Loop weights
397        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]; // opposite vertex
403                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                // Interior edge: 3/8*(a+b) + 1/8*(c+d)
424                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                // Boundary edge: simple midpoint
434                (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        // Create edge vertices for each triangle
444        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            // Four sub-triangles
477            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        // Reposition original vertices using Loop weights
484        let nbrs = self.vertex_neighbors();
485        // Determine boundary vertices
486        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                // Boundary: keep position (simple approach)
496                // Could use 3/4 * v + 1/8 * (n1 + n2) for boundary
497                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    // ------------------------------------------------------------------
520    // New: boundary edges and Euler characteristic
521    // ------------------------------------------------------------------
522
523    /// Return boundary edges (edges shared by only one triangle).
524    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    /// Compute the Euler characteristic: V - E + F.
542    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    /// Count non-manifold edges (shared by more than 2 triangles).
550    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    // ------------------------------------------------------------------
564    // Cotangent Laplacian
565    // ------------------------------------------------------------------
566
567    /// Compute the cotangent Laplacian weights for each directed edge (i → j).
568    ///
569    /// Returns a `HashMap<(usize, usize), f64>` mapping `(i, j)` to the
570    /// symmetric cotangent weight `w_{ij} = (cot α + cot β) / 2`, where α
571    /// and β are the angles opposite to edge (i,j) in the two incident faces.
572    ///
573    /// This is the standard discretisation used in geometry processing for
574    /// Laplace-Beltrami operators on triangulated surfaces.
575    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            // Cotangent of the angle at each vertex:
585            //   cot(angle at v0) is the angle between edges v0→v1 and v0→v2
586            let cot_at = |a: Vec3, b: Vec3, c: Vec3| -> f64 {
587                // angle at vertex a, between edges a→b and a→c
588                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); // angle at i0, opposite edge i1-i2
600            let cot1 = cot_at(p1, p0, p2); // angle at i1, opposite edge i0-i2
601            let cot2 = cot_at(p2, p0, p1); // angle at i2, opposite edge i0-i1
602
603            // Edge (i1, i2): opposite angle at i0
604            *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            // Edge (i0, i2): opposite angle at i1
608            *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            // Edge (i0, i1): opposite angle at i2
612            *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    // ------------------------------------------------------------------
620    // Heat Kernel Signature
621    // ------------------------------------------------------------------
622
623    /// Compute the Heat Kernel Signature (HKS) descriptor for each vertex
624    /// at a set of time scales `t_values`.
625    ///
626    /// The HKS approximates the diagonal of the heat kernel K(x, x, t) using
627    /// the cotangent Laplacian's (normalized) diagonal weights.  The full
628    /// spectral decomposition is expensive, so this implementation uses a
629    /// fast approximation: for each vertex i and time t, it computes
630    ///
631    ///   HKS(i, t) = exp(-t * D\[i\])
632    ///
633    /// where `D[i]` is the sum of cotangent weights incident on vertex i
634    /// (the diagonal of the stiffness matrix, normalised by the vertex area).
635    ///
636    /// Returns a `Vec<Vec`f64`>` of shape `[n_vertices][t_values.len()]`.
637    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        // Cotangent weights
644        let weights = self.compute_laplacian_matrix();
645
646        // Vertex areas (1/3 of the sum of incident triangle areas)
647        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        // Row sum of cotangent weights (diagonal of the stiffness matrix)
661        let mut diag = vec![0.0f64; n];
662        for (&(i, _j), &w) in &weights {
663            diag[i] += w;
664        }
665
666        // Effective frequency per vertex: lambda_i = diag[i] / area[i]
667        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        // HKS(i, t) = exp(-t * lambda_i)
678        (0..n)
679            .map(|i| t_values.iter().map(|&t| (-t * lambda[i]).exp()).collect())
680            .collect()
681    }
682
683    // ------------------------------------------------------------------
684    // Laplacian smoothing (cotangent weights)
685    // ------------------------------------------------------------------
686
687    /// Iterative Laplacian smoothing using cotangent weights.
688    ///
689    /// Each iteration moves vertex `i` towards the weighted average of its
690    /// neighbours:
691    ///
692    ///   v_i' = v_i + factor * Σ_j w_{ij} * (v_j - v_i) / Σ_j w_{ij}
693    ///
694    /// Boundary vertices (those on edges shared by only one triangle) are
695    /// kept fixed.
696    ///
697    /// `factor` – step size in \[0, 1\]; `iterations` – number of passes.
698    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        // Identify boundary vertices
704        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// -----------------------------------------------------------------------
754// Helper for Dijkstra heap ordering
755// -----------------------------------------------------------------------
756
757#[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
776// -----------------------------------------------------------------------
777// Shape trait implementation
778// -----------------------------------------------------------------------
779
780impl 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
874/// Moller-Trumbore ray-triangle intersection.
875fn 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/// Build a simple unit cube mesh (12 triangles, watertight).
919#[cfg(test)]
920fn unit_cube_mesh() -> TriangleMesh {
921    let v = vec![
922        Vec3::new(0.0, 0.0, 0.0), // 0
923        Vec3::new(1.0, 0.0, 0.0), // 1
924        Vec3::new(1.0, 1.0, 0.0), // 2
925        Vec3::new(0.0, 1.0, 0.0), // 3
926        Vec3::new(0.0, 0.0, 1.0), // 4
927        Vec3::new(1.0, 0.0, 1.0), // 5
928        Vec3::new(1.0, 1.0, 1.0), // 6
929        Vec3::new(0.0, 1.0, 1.0), // 7
930    ];
931    let idx = vec![
932        // -Z face (z=0)
933        [0, 2, 1],
934        [0, 3, 2],
935        // +Z face (z=1)
936        [4, 5, 6],
937        [4, 6, 7],
938        // -X face (x=0)
939        [0, 4, 7],
940        [0, 7, 3],
941        // +X face (x=1)
942        [1, 2, 6],
943        [1, 6, 5],
944        // -Y face (y=0)
945        [0, 1, 5],
946        [0, 5, 4],
947        // +Y face (y=1)
948        [3, 7, 6],
949        [3, 6, 2],
950    ];
951    TriangleMesh::new(v, idx)
952}
953
954/// Build a simple tetrahedron mesh (4 triangles, watertight).
955#[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    // ------------------------------------------------------------------
1077    // New tests: adjacency queries
1078    // ------------------------------------------------------------------
1079
1080    #[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        // Each vertex of a cube is shared by 3 faces (each face = 2 tris,
1086        // each vertex appears in exactly 3 faces = 6 tris / 2, but actually
1087        // each vertex is used by 3*2=6? Let's just check non-empty).
1088        for faces in &v2f {
1089            assert!(
1090                !faces.is_empty(),
1091                "every vertex should have at least one face"
1092            );
1093        }
1094        // Vertex 0 appears in -Z, -X, -Y faces (6 triangles)
1095        // Indices: [0,2,1],[0,3,2],[0,4,7],[0,7,3],[0,1,5],[0,5,4]
1096        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        // Each triangle in a cube shares edges with other triangles
1124        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        // A cube has 12 edges + 6 face diagonals = 18
1151        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); // connected to 1 and 2
1167        assert_eq!(nbrs[1].len(), 2);
1168        assert_eq!(nbrs[2].len(), 2);
1169    }
1170
1171    // ------------------------------------------------------------------
1172    // New tests: edge collapse
1173    // ------------------------------------------------------------------
1174
1175    #[test]
1176    fn test_edge_collapse_basic() {
1177        // Two triangles sharing edge (0,1)
1178        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        // After collapse, vertex 0 should be at midpoint (0.5, 0, 0)
1190        assert!((mesh.vertices[0].x - 0.5).abs() < 1e-10);
1191        // Triangles that degenerate should be removed
1192        // Both triangles had vertices 0 and 1, which are now both 0,
1193        // so they become degenerate -> removed? No: [0,1,2] becomes [0,0,2] -> degenerate.
1194        // [0,3,1] becomes [0,3,0] -> degenerate.
1195        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        // Edge (0, 100) doesn't exist
1209        assert!(!mesh.edge_collapse(0, 100));
1210    }
1211
1212    #[test]
1213    fn test_edge_collapse_preserves_other_faces() {
1214        // Three triangles, collapse edge shared by first two
1215        let mut mesh = TriangleMesh::new(
1216            vec![
1217                Vec3::new(0.0, 0.0, 0.0),  // 0
1218                Vec3::new(2.0, 0.0, 0.0),  // 1
1219                Vec3::new(1.0, 1.0, 0.0),  // 2
1220                Vec3::new(1.0, -1.0, 0.0), // 3
1221                Vec3::new(3.0, 1.0, 0.0),  // 4
1222            ],
1223            vec![[0, 1, 2], [0, 3, 1], [1, 4, 2]],
1224        );
1225        let ok = mesh.edge_collapse(0, 1);
1226        assert!(ok);
1227        // Triangle [1,4,2] references v1 which is now v0
1228        // It becomes [0,4,2] which is non-degenerate
1229        assert_eq!(
1230            mesh.indices.len(),
1231            1,
1232            "two degenerate tris removed, one survives"
1233        );
1234    }
1235
1236    // ------------------------------------------------------------------
1237    // New tests: vertex normals
1238    // ------------------------------------------------------------------
1239
1240    #[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    // ------------------------------------------------------------------
1276    // New tests: Laplacian smoothing
1277    // ------------------------------------------------------------------
1278
1279    #[test]
1280    fn test_laplacian_smooth_doesnt_crash() {
1281        let mut mesh = unit_cube_mesh();
1282        mesh.laplacian_smooth(0.5, 3);
1283        // After smoothing the cube should still have 8 vertices and 12 faces
1284        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        // A mesh with one interior vertex should move towards neighbors
1291        let mut mesh = TriangleMesh::new(
1292            vec![
1293                Vec3::new(0.0, 0.0, 0.0), // 0
1294                Vec3::new(2.0, 0.0, 0.0), // 1
1295                Vec3::new(1.0, 2.0, 0.0), // 2
1296                Vec3::new(1.0, 0.5, 0.0), // 3 (interior-ish vertex)
1297            ],
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        // Vertex 3 should have moved towards the average of its neighbors (0,1,2)
1303        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    // ------------------------------------------------------------------
1315    // New tests: geodesic distance
1316    // ------------------------------------------------------------------
1317
1318    #[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        // Distance from 0 to 1 should be 1.0
1337        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        // Distance 0->6 should equal distance 6->0
1350        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    // ------------------------------------------------------------------
1359    // New tests: Loop subdivision
1360    // ------------------------------------------------------------------
1361
1362    #[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        // Should still have consistent structure
1383        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    // ------------------------------------------------------------------
1401    // New tests: boundary edges, Euler characteristic, non-manifold
1402    // ------------------------------------------------------------------
1403
1404    #[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        // Add a third triangle sharing an edge -> non-manifold
1454        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]], // edge (0,1) shared by 3 faces
1463        );
1464        assert_eq!(mesh.non_manifold_edge_count(), 1);
1465    }
1466}
1467
1468// ---------------------------------------------------------------------------
1469// Tests for cotangent Laplacian, HKS, and smooth_laplacian
1470// ---------------------------------------------------------------------------
1471
1472#[cfg(test)]
1473mod tests_laplacian_hks {
1474
1475    use crate::TriangleMesh;
1476    use oxiphysics_core::math::Vec3;
1477
1478    /// Flat quad made of two triangles: 4 vertices, 2 triangles.
1479    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    /// Single triangle.
1492    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    // ── compute_laplacian_matrix ─────────────────────────────────────────────
1504
1505    #[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 every (i,j) entry there should be a matching (j,i)
1517        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        // 3 undirected edges × 2 directions = 6 entries
1531        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    // ── compute_heat_kernel_signature ────────────────────────────────────────
1546
1547    #[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        // HKS = exp(-t * lambda) is always in (0, 1] for t > 0, lambda >= 0
1561        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        // For any vertex, HKS(i, t1) >= HKS(i, t2) when t1 < t2
1577        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    // ── smooth_laplacian ────────────────────────────────────────────────────
1602
1603    #[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        // For a single triangle (all boundary) smoothing must not move any vertex
1643        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}