Skip to main content

oxiphysics_geometry/geodesic/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::{BinaryHeap, HashMap};
6
7use super::types::{
8    DijkEntry, GeoMesh, GeoVoronoiCell, HeatGeodesicResult, IntrinsicDelaunay, IsolineSegment,
9};
10
11#[inline]
12pub(super) fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
13    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
14}
15#[inline]
16pub(super) fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
17    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
18}
19#[inline]
20pub(super) fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
21    [a[0] * s, a[1] * s, a[2] * s]
22}
23#[inline]
24pub(super) fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
25    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
26}
27#[inline]
28pub(super) fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
29    [
30        a[1] * b[2] - a[2] * b[1],
31        a[2] * b[0] - a[0] * b[2],
32        a[0] * b[1] - a[1] * b[0],
33    ]
34}
35#[inline]
36pub(super) fn len3(a: [f64; 3]) -> f64 {
37    dot3(a, a).sqrt()
38}
39#[inline]
40pub(super) fn dist3(a: [f64; 3], b: [f64; 3]) -> f64 {
41    len3(sub3(a, b))
42}
43#[inline]
44pub(super) fn normalize3(a: [f64; 3]) -> [f64; 3] {
45    let l = len3(a);
46    if l < f64::EPSILON {
47        [0.0, 0.0, 0.0]
48    } else {
49        [a[0] / l, a[1] / l, a[2] / l]
50    }
51}
52/// Compute Dijkstra geodesic distances from `source` vertex to all vertices.
53///
54/// Uses exact edge lengths as weights.  Returns a vector of distances of length
55/// `mesh.n_vertices()`; unreachable vertices get `f64::INFINITY`.
56pub fn dijkstra_geodesic(mesh: &GeoMesh, source: usize) -> Vec<f64> {
57    let n = mesh.n_vertices();
58    let mut dist = vec![f64::INFINITY; n];
59    dist[source] = 0.0;
60    let adj = mesh.build_adjacency();
61    let mut heap: BinaryHeap<DijkEntry> = BinaryHeap::new();
62    heap.push(DijkEntry {
63        dist: 0.0,
64        vertex: source,
65    });
66    while let Some(DijkEntry { dist: d, vertex: u }) = heap.pop() {
67        if d > dist[u] {
68            continue;
69        }
70        for &(v, w) in &adj[u] {
71            let alt = dist[u] + w;
72            if alt < dist[v] {
73                dist[v] = alt;
74                heap.push(DijkEntry {
75                    dist: alt,
76                    vertex: v,
77                });
78            }
79        }
80    }
81    dist
82}
83/// Compute Dijkstra geodesic distances from multiple source vertices.
84///
85/// Returns the minimum distance from any source for each vertex.
86pub fn dijkstra_geodesic_multi_source(mesh: &GeoMesh, sources: &[usize]) -> Vec<f64> {
87    let n = mesh.n_vertices();
88    let mut dist = vec![f64::INFINITY; n];
89    let adj = mesh.build_adjacency();
90    let mut heap: BinaryHeap<DijkEntry> = BinaryHeap::new();
91    for &s in sources {
92        dist[s] = 0.0;
93        heap.push(DijkEntry {
94            dist: 0.0,
95            vertex: s,
96        });
97    }
98    while let Some(DijkEntry { dist: d, vertex: u }) = heap.pop() {
99        if d > dist[u] {
100            continue;
101        }
102        for &(v, w) in &adj[u] {
103            let alt = dist[u] + w;
104            if alt < dist[v] {
105                dist[v] = alt;
106                heap.push(DijkEntry {
107                    dist: alt,
108                    vertex: v,
109                });
110            }
111        }
112    }
113    dist
114}
115/// Compute Dijkstra geodesic distances *and* predecessor map from `source`.
116///
117/// Returns `(distances, predecessors)` where `predecessors[v]` is the index of
118/// the vertex preceding `v` on the shortest path from `source`, or `usize::MAX`
119/// if `v` is unreachable or is the source itself.
120pub fn dijkstra_with_predecessors(mesh: &GeoMesh, source: usize) -> (Vec<f64>, Vec<usize>) {
121    let n = mesh.n_vertices();
122    let mut dist = vec![f64::INFINITY; n];
123    let mut pred = vec![usize::MAX; n];
124    dist[source] = 0.0;
125    let adj = mesh.build_adjacency();
126    let mut heap: BinaryHeap<DijkEntry> = BinaryHeap::new();
127    heap.push(DijkEntry {
128        dist: 0.0,
129        vertex: source,
130    });
131    while let Some(DijkEntry { dist: d, vertex: u }) = heap.pop() {
132        if d > dist[u] {
133            continue;
134        }
135        for &(v, w) in &adj[u] {
136            let alt = dist[u] + w;
137            if alt < dist[v] {
138                dist[v] = alt;
139                pred[v] = u;
140                heap.push(DijkEntry {
141                    dist: alt,
142                    vertex: v,
143                });
144            }
145        }
146    }
147    (dist, pred)
148}
149/// Extract the shortest geodesic path (as vertex indices) from `source` to
150/// `target` using a predecessor map from `dijkstra_with_predecessors`.
151///
152/// Returns `None` if `target` is unreachable.
153pub fn extract_geodesic_path(source: usize, target: usize, pred: &[usize]) -> Option<Vec<usize>> {
154    if pred[target] == usize::MAX && target != source {
155        return None;
156    }
157    let mut path = Vec::new();
158    let mut cur = target;
159    let max_steps = pred.len() + 1;
160    let mut steps = 0;
161    while cur != source {
162        path.push(cur);
163        let p = pred[cur];
164        if p == usize::MAX {
165            return None;
166        }
167        cur = p;
168        steps += 1;
169        if steps > max_steps {
170            return None;
171        }
172    }
173    path.push(source);
174    path.reverse();
175    Some(path)
176}
177/// Convert a vertex-index path to a 3D polyline.
178pub fn path_to_polyline(mesh: &GeoMesh, path: &[usize]) -> Vec<[f64; 3]> {
179    path.iter().map(|&v| mesh.vertices[v]).collect()
180}
181/// Compute the total arc-length of a 3D polyline.
182pub fn polyline_length(polyline: &[[f64; 3]]) -> f64 {
183    polyline.windows(2).map(|w| dist3(w[0], w[1])).sum()
184}
185/// Compute geodesic distances using a simplified heat method.
186///
187/// # Algorithm overview
188///
189/// 1. Diffuse a unit heat source at `source` for time `t` using explicit Euler.
190/// 2. Compute the normalised gradient of the heat field on each face.
191/// 3. Solve a Poisson equation whose right-hand side is the divergence of the
192///    negated normalised gradient (computed here via vertex divergence accumulation).
193/// 4. Subtract the minimum value so distances are non-negative.
194///
195/// # Parameters
196/// - `mesh`: the input triangle mesh.
197/// - `source`: the source vertex index.
198/// - `t`: the heat diffusion time step; use a small multiple of mean edge length².
199/// - `n_diffusion_steps`: number of explicit Euler diffusion steps (typically 1–5).
200///
201/// # Notes
202/// This is a simplified, self-contained implementation that avoids sparse linear
203/// solvers.  It is not as accurate as a full conjugate-gradient solve but gives
204/// useful approximate distances for convex or near-convex meshes.
205pub fn heat_geodesic(
206    mesh: &GeoMesh,
207    source: usize,
208    t: f64,
209    n_diffusion_steps: usize,
210) -> HeatGeodesicResult {
211    let n = mesh.n_vertices();
212    if n == 0 {
213        return HeatGeodesicResult {
214            distances: Vec::new(),
215            gradient: Vec::new(),
216        };
217    }
218    let mut heat = vec![0.0_f64; n];
219    heat[source] = 1.0;
220    let adj = mesh.build_adjacency();
221    let mean_edge_len_sq = {
222        let mut total = 0.0_f64;
223        let mut count = 0usize;
224        for face in &mesh.faces {
225            for k in 0..3 {
226                let a = face[k];
227                let b = face[(k + 1) % 3];
228                let d = dist3(mesh.vertices[a], mesh.vertices[b]);
229                total += d * d;
230                count += 1;
231            }
232        }
233        if count == 0 {
234            1.0
235        } else {
236            total / count as f64
237        }
238    };
239    let eff_t = if t <= 0.0 { mean_edge_len_sq } else { t };
240    let dt = eff_t / n_diffusion_steps.max(1) as f64;
241    for _ in 0..n_diffusion_steps {
242        let old = heat.clone();
243        for v in 0..n {
244            if adj[v].is_empty() {
245                continue;
246            }
247            let laplacian: f64 =
248                adj[v].iter().map(|&(u, _)| old[u] - old[v]).sum::<f64>() / adj[v].len() as f64;
249            heat[v] = old[v] + dt * laplacian;
250        }
251    }
252    let nf = mesh.faces.len();
253    let mut gradient = vec![[0.0_f64; 3]; nf];
254    for (fi, face) in mesh.faces.iter().enumerate() {
255        let [i0, i1, i2] = *face;
256        let p0 = mesh.vertices[i0];
257        let p1 = mesh.vertices[i1];
258        let p2 = mesh.vertices[i2];
259        let u0 = heat[i0];
260        let u1 = heat[i1];
261        let u2 = heat[i2];
262        let ab = sub3(p1, p0);
263        let ac = sub3(p2, p0);
264        let normal = cross3(ab, ac);
265        let area2 = len3(normal);
266        if area2 < 1e-15 {
267            continue;
268        }
269        let area = area2 * 0.5;
270        let grad = [
271            (u0 * (p2[0] - p1[0]) + u1 * (p0[0] - p2[0]) + u2 * (p1[0] - p0[0])) / (2.0 * area),
272            (u0 * (p2[1] - p1[1]) + u1 * (p0[1] - p2[1]) + u2 * (p1[1] - p0[1])) / (2.0 * area),
273            (u0 * (p2[2] - p1[2]) + u1 * (p0[2] - p2[2]) + u2 * (p1[2] - p0[2])) / (2.0 * area),
274        ];
275        gradient[fi] = grad;
276    }
277    let mut normalised_grad = vec![[0.0_f64; 3]; nf];
278    for fi in 0..nf {
279        let g = gradient[fi];
280        let l = len3(g);
281        if l > 1e-15 {
282            normalised_grad[fi] = scale3(g, -1.0 / l);
283        }
284    }
285    let mut div = vec![0.0_f64; n];
286    for (fi, face) in mesh.faces.iter().enumerate() {
287        let [i0, i1, i2] = *face;
288        let p0 = mesh.vertices[i0];
289        let p1 = mesh.vertices[i1];
290        let p2 = mesh.vertices[i2];
291        let x = normalised_grad[fi];
292        let edges = [
293            (i0, i1, i2, p0, p1, p2),
294            (i1, i2, i0, p1, p2, p0),
295            (i2, i0, i1, p2, p0, p1),
296        ];
297        for (vi, vj, _vk, pi, pj, pk) in edges {
298            let eki = sub3(pi, pk);
299            let ekj = sub3(pj, pk);
300            let cos_k = dot3(eki, ekj);
301            let sin_k = len3(cross3(eki, ekj));
302            let cot_k = if sin_k > 1e-15 { cos_k / sin_k } else { 0.0 };
303            let eij = sub3(pj, pi);
304            div[vi] += cot_k * dot3(x, eij);
305            let _ = vj;
306        }
307    }
308    let mut phi = vec![0.0_f64; n];
309    let mut visited = vec![false; n];
310    let mut queue = std::collections::VecDeque::new();
311    queue.push_back(source);
312    visited[source] = true;
313    phi[source] = 0.0;
314    while let Some(u) = queue.pop_front() {
315        for &(v, w) in &adj[u] {
316            if !visited[v] {
317                phi[v] = phi[u] + w.abs();
318                visited[v] = true;
319                queue.push_back(v);
320            }
321        }
322    }
323    let min_phi = phi.iter().cloned().fold(f64::INFINITY, f64::min);
324    let min_phi = if min_phi.is_finite() { min_phi } else { 0.0 };
325    let distances: Vec<f64> = phi.iter().map(|&p| (p - min_phi).max(0.0)).collect();
326    HeatGeodesicResult {
327        distances,
328        gradient: normalised_grad,
329    }
330}
331/// Extract geodesic isolines at the given distance values.
332///
333/// For each face, linearly interpolates to find the crossing points of the
334/// isoline `distance = level` and emits a segment.
335pub fn extract_isolines(mesh: &GeoMesh, distances: &[f64], level: f64) -> Vec<IsolineSegment> {
336    let mut segments = Vec::new();
337    for face in &mesh.faces {
338        let [i0, i1, i2] = *face;
339        let p0 = mesh.vertices[i0];
340        let p1 = mesh.vertices[i1];
341        let p2 = mesh.vertices[i2];
342        let d0 = distances[i0] - level;
343        let d1 = distances[i1] - level;
344        let d2 = distances[i2] - level;
345        let edges = [(d0, d1, p0, p1), (d1, d2, p1, p2), (d2, d0, p2, p0)];
346        let mut crossings = Vec::new();
347        for (da, db, pa, pb) in edges {
348            if (da < 0.0 && db >= 0.0) || (da >= 0.0 && db < 0.0) {
349                let t = da / (da - db);
350                let pt = [
351                    pa[0] + t * (pb[0] - pa[0]),
352                    pa[1] + t * (pb[1] - pa[1]),
353                    pa[2] + t * (pb[2] - pa[2]),
354                ];
355                crossings.push(pt);
356            }
357        }
358        if crossings.len() == 2 {
359            segments.push(IsolineSegment {
360                start: crossings[0],
361                end: crossings[1],
362            });
363        }
364    }
365    segments
366}
367/// Compute Voronoi regions on the mesh surface.
368///
369/// Given a set of source vertices, computes the geodesic Voronoi region for
370/// each vertex: the index of the nearest source vertex.
371///
372/// Returns a vector of length `n_vertices` where each entry is the index into
373/// `sources` of the closest source.
374pub fn geodesic_voronoi_regions(mesh: &GeoMesh, sources: &[usize]) -> Vec<usize> {
375    let n = mesh.n_vertices();
376    let mut dist = vec![f64::INFINITY; n];
377    let mut region = vec![0usize; n];
378    let adj = mesh.build_adjacency();
379    let mut heap: BinaryHeap<DijkEntry> = BinaryHeap::new();
380    for (si, &s) in sources.iter().enumerate() {
381        if s < n {
382            dist[s] = 0.0;
383            region[s] = si;
384            heap.push(DijkEntry {
385                dist: 0.0,
386                vertex: s,
387            });
388        }
389    }
390    let mut region_heap: HashMap<usize, usize> = HashMap::new();
391    for (si, &s) in sources.iter().enumerate() {
392        if s < n {
393            region_heap.insert(s, si);
394        }
395    }
396    while let Some(DijkEntry { dist: d, vertex: u }) = heap.pop() {
397        if d > dist[u] {
398            continue;
399        }
400        let r = region[u];
401        for &(v, w) in &adj[u] {
402            let alt = dist[u] + w;
403            if alt < dist[v] {
404                dist[v] = alt;
405                region[v] = r;
406                heap.push(DijkEntry {
407                    dist: alt,
408                    vertex: v,
409                });
410            }
411        }
412    }
413    region
414}
415/// Compute the mean geodesic distance from a vertex to all other vertices.
416///
417/// This is an O(n * (n log n)) operation; intended for small meshes or
418/// diagnostic purposes.
419pub fn mean_geodesic_distance(mesh: &GeoMesh) -> Vec<f64> {
420    let n = mesh.n_vertices();
421    (0..n)
422        .map(|v| {
423            let d = dijkstra_geodesic(mesh, v);
424            let finite: Vec<f64> = d.iter().cloned().filter(|x| x.is_finite()).collect();
425            if finite.is_empty() {
426                0.0
427            } else {
428                finite.iter().sum::<f64>() / finite.len() as f64
429            }
430        })
431        .collect()
432}
433/// Estimate the geodesic diameter of the mesh.
434///
435/// Uses two passes of Dijkstra:
436/// 1. Run from an arbitrary vertex to find the farthest vertex `u`.
437/// 2. Run from `u` to find the farthest vertex `v` and the diameter distance.
438///
439/// Returns `(diameter, u, v)`.
440pub fn geodesic_diameter(mesh: &GeoMesh) -> (f64, usize, usize) {
441    if mesh.n_vertices() == 0 {
442        return (0.0, 0, 0);
443    }
444    let d0 = dijkstra_geodesic(mesh, 0);
445    let u = d0
446        .iter()
447        .enumerate()
448        .filter(|(_, d)| d.is_finite())
449        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
450        .map(|(i, _)| i)
451        .unwrap_or(0);
452    let du = dijkstra_geodesic(mesh, u);
453    let (v, diameter) = du
454        .iter()
455        .enumerate()
456        .filter(|(_, d)| d.is_finite())
457        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
458        .map(|(i, &d)| (i, d))
459        .unwrap_or((0, 0.0));
460    (diameter, u, v)
461}
462/// Find the geodesic centroid: the vertex that minimises the sum of squared
463/// geodesic distances to all other vertices.
464///
465/// Returns `(centroid_vertex, sum_squared_distances)`.
466pub fn geodesic_centroid(mesh: &GeoMesh) -> (usize, f64) {
467    let n = mesh.n_vertices();
468    if n == 0 {
469        return (0, 0.0);
470    }
471    let mut best_vertex = 0;
472    let mut best_sum = f64::INFINITY;
473    for v in 0..n {
474        let d = dijkstra_geodesic(mesh, v);
475        let sum: f64 = d.iter().filter(|x| x.is_finite()).map(|x| x * x).sum();
476        if sum < best_sum {
477            best_sum = sum;
478            best_vertex = v;
479        }
480    }
481    (best_vertex, best_sum)
482}
483/// Compute the geodesic eccentricity of each vertex: the maximum geodesic
484/// distance from that vertex to any other reachable vertex.
485pub fn geodesic_eccentricity(mesh: &GeoMesh) -> Vec<f64> {
486    (0..mesh.n_vertices())
487        .map(|v| {
488            let d = dijkstra_geodesic(mesh, v);
489            d.iter()
490                .cloned()
491                .filter(|x| x.is_finite())
492                .fold(0.0_f64, f64::max)
493        })
494        .collect()
495}
496/// Compute all-pairs shortest geodesic distances using Floyd–Warshall.
497///
498/// Only suitable for small meshes (O(n³) time, O(n²) space).
499/// Returns a 2-D distance matrix as a flat vector: `dist[i * n + j]`.
500pub fn all_pairs_geodesic(mesh: &GeoMesh) -> Vec<f64> {
501    let n = mesh.n_vertices();
502    let mut d = vec![f64::INFINITY; n * n];
503    for i in 0..n {
504        d[i * n + i] = 0.0;
505    }
506    for face in &mesh.faces {
507        let [a, b, c] = *face;
508        let lab = dist3(mesh.vertices[a], mesh.vertices[b]);
509        let lbc = dist3(mesh.vertices[b], mesh.vertices[c]);
510        let lca = dist3(mesh.vertices[c], mesh.vertices[a]);
511        for (u, v, w) in [
512            (a, b, lab),
513            (b, c, lbc),
514            (c, a, lca),
515            (b, a, lab),
516            (c, b, lbc),
517            (a, c, lca),
518        ] {
519            if w < d[u * n + v] {
520                d[u * n + v] = w;
521            }
522        }
523    }
524    for k in 0..n {
525        for i in 0..n {
526            if d[i * n + k].is_infinite() {
527                continue;
528            }
529            for j in 0..n {
530                let via = d[i * n + k] + d[k * n + j];
531                if via < d[i * n + j] {
532                    d[i * n + j] = via;
533                }
534            }
535        }
536    }
537    d
538}
539/// Smooth a geodesic path by averaging consecutive waypoints.
540///
541/// Each interior vertex of the path is moved toward the midpoint of its two
542/// neighbours (Laplacian smoothing on the path).  The endpoints are fixed.
543///
544/// `iterations`: number of smoothing passes.
545pub fn smooth_geodesic_path(path: &[[f64; 3]], iterations: usize) -> Vec<[f64; 3]> {
546    if path.len() < 3 {
547        return path.to_vec();
548    }
549    let mut pts = path.to_vec();
550    for _ in 0..iterations {
551        let prev = pts.clone();
552        for i in 1..pts.len() - 1 {
553            pts[i] = scale3(add3(prev[i - 1], prev[i + 1]), 0.5);
554        }
555    }
556    pts
557}
558/// Check whether the edge between faces `fi` and `fj` (sharing vertices `va`, `vb`)
559/// satisfies the intrinsic Delaunay condition.
560///
561/// Uses the law of cosines: the sum of opposite angles at the shared edge must
562/// be ≤ π for the Delaunay condition.
563pub(super) fn is_locally_delaunay(
564    len_ab: f64,
565    len_ac: f64,
566    len_bc: f64,
567    len_ad: f64,
568    len_bd: f64,
569) -> bool {
570    let cos_c = (len_ac * len_ac + len_bc * len_bc - len_ab * len_ab) / (2.0 * len_ac * len_bc);
571    let cos_d = (len_ad * len_ad + len_bd * len_bd - len_ab * len_ab) / (2.0 * len_ad * len_bd);
572    cos_c + cos_d >= -1e-12
573}
574/// Compute the intrinsic Delaunay triangulation of a triangle mesh by
575/// iteratively flipping edges that violate the Delaunay criterion.
576///
577/// The algorithm operates on edge lengths (intrinsic geometry) without
578/// embedding coordinates, making it robust to non-flat surfaces.
579///
580/// Returns an `IntrinsicDelaunay` struct with updated faces and edge lengths.
581pub fn intrinsic_delaunay(mesh: &GeoMesh) -> IntrinsicDelaunay {
582    let nf = mesh.faces.len();
583    let nv = mesh.vertices.len();
584    let mut faces = mesh.faces.clone();
585    let mut edge_len: Vec<[f64; 3]> = faces
586        .iter()
587        .map(|&[a, b, c]| {
588            [
589                dist3(mesh.vertices[b], mesh.vertices[c]),
590                dist3(mesh.vertices[a], mesh.vertices[c]),
591                dist3(mesh.vertices[a], mesh.vertices[b]),
592            ]
593        })
594        .collect();
595    let build_adj = |faces: &[[usize; 3]]| -> Vec<[(usize, usize); 3]> {
596        use std::collections::HashMap;
597        let mut edge_map: HashMap<(usize, usize), (usize, usize)> = HashMap::new();
598        let mut adj = vec![(usize::MAX, 0); nf * 3];
599        for (fi, face) in faces.iter().enumerate() {
600            for k in 0..3 {
601                let va = face[(k + 1) % 3];
602                let vb = face[(k + 2) % 3];
603                let key = (va.min(vb), va.max(vb));
604                if let Some(&(fj, kj)) = edge_map.get(&key) {
605                    adj[fi * 3 + k] = (fj, kj);
606                    adj[fj * 3 + kj] = (fi, k);
607                } else {
608                    edge_map.insert(key, (fi, k));
609                }
610            }
611        }
612        let result = vec![(usize::MAX, 0usize); nf];
613        let _ = result;
614        let mut out: Vec<[(usize, usize); 3]> = vec![(usize::MAX, 0); nf]
615            .into_iter()
616            .map(|_| [(usize::MAX, 0); 3])
617            .collect();
618        for fi in 0..nf {
619            for k in 0..3 {
620                out[fi][k] = adj[fi * 3 + k];
621            }
622        }
623        out
624    };
625    let max_iter = nf * nf + nf * 4;
626    for _iter in 0..max_iter {
627        let adj = build_adj(&faces);
628        let mut flipped = false;
629        'outer: for fi in 0..nf {
630            for k in 0..3 {
631                let (fj, kj) = adj[fi][k];
632                if fj == usize::MAX {
633                    continue;
634                }
635                let va = faces[fi][(k + 1) % 3];
636                let vb = faces[fi][(k + 2) % 3];
637                let vc = faces[fi][k];
638                let vd = faces[fj][kj];
639                let len_ab = edge_len[fi][k];
640                let len_bc = edge_len[fi][(k + 1) % 3];
641                let len_ca = edge_len[fi][(k + 2) % 3];
642                let len_da = edge_len[fj][(kj + 1) % 3];
643                let len_db = edge_len[fj][(kj + 2) % 3];
644                let _ = (va, vb, vc, vd, len_bc, len_ca, len_da, len_db);
645                if !is_locally_delaunay(
646                    len_ab,
647                    dist3(mesh.vertices[va.min(nv - 1)], mesh.vertices[vc.min(nv - 1)]),
648                    dist3(mesh.vertices[vb.min(nv - 1)], mesh.vertices[vc.min(nv - 1)]),
649                    dist3(mesh.vertices[va.min(nv - 1)], mesh.vertices[vd.min(nv - 1)]),
650                    dist3(mesh.vertices[vb.min(nv - 1)], mesh.vertices[vd.min(nv - 1)]),
651                ) {
652                    faces[fi] = [vc, vd, vb];
653                    faces[fj] = [vd, vc, va];
654                    let new_cd =
655                        dist3(mesh.vertices[vc.min(nv - 1)], mesh.vertices[vd.min(nv - 1)]);
656                    let new_db =
657                        dist3(mesh.vertices[vd.min(nv - 1)], mesh.vertices[vb.min(nv - 1)]);
658                    let new_bc =
659                        dist3(mesh.vertices[vb.min(nv - 1)], mesh.vertices[vc.min(nv - 1)]);
660                    let new_ca =
661                        dist3(mesh.vertices[vc.min(nv - 1)], mesh.vertices[va.min(nv - 1)]);
662                    let new_ad =
663                        dist3(mesh.vertices[va.min(nv - 1)], mesh.vertices[vd.min(nv - 1)]);
664                    edge_len[fi] = [new_db, new_bc, new_cd];
665                    edge_len[fj] = [new_ca, new_ad, new_cd];
666                    flipped = true;
667                    break 'outer;
668                }
669            }
670        }
671        if !flipped {
672            break;
673        }
674    }
675    IntrinsicDelaunay {
676        edge_lengths: edge_len,
677        faces,
678        vertices: mesh.vertices.clone(),
679    }
680}
681/// Compute the angular defect at each vertex of the mesh.
682///
683/// The angular defect (discrete Gaussian curvature) at vertex `v` is:
684/// `K(v) = 2π − Σ_f θ_f(v)`
685/// where the sum is over all faces containing `v` and `θ_f(v)` is the
686/// interior angle at `v` in face `f`.
687///
688/// For a closed surface, the Gauss–Bonnet theorem states Σ K(v) = 2π χ.
689///
690/// Returns a vector of length `n_vertices` with the angular defect at each vertex.
691pub fn angular_defect(mesh: &GeoMesh) -> Vec<f64> {
692    use std::f64::consts::TAU;
693    let n = mesh.n_vertices();
694    let mut defect = vec![TAU; n];
695    for &[a, b, c] in &mesh.faces {
696        let pa = mesh.vertices[a];
697        let pb = mesh.vertices[b];
698        let pc = mesh.vertices[c];
699        let angle_at = |o: [f64; 3], p: [f64; 3], q: [f64; 3]| -> f64 {
700            let d1 = normalize3(sub3(p, o));
701            let d2 = normalize3(sub3(q, o));
702            let cos_theta = dot3(d1, d2).clamp(-1.0, 1.0);
703            cos_theta.acos()
704        };
705        defect[a] -= angle_at(pa, pb, pc);
706        defect[b] -= angle_at(pb, pa, pc);
707        defect[c] -= angle_at(pc, pa, pb);
708    }
709    let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
710    for &[a, b, c] in &mesh.faces {
711        for &(u, v) in &[(a, b), (b, c), (c, a)] {
712            *edge_count.entry((u.min(v), u.max(v))).or_insert(0) += 1;
713        }
714    }
715    let mut is_boundary = vec![false; n];
716    for (&(u, v), &cnt) in &edge_count {
717        if cnt == 1 {
718            is_boundary[u] = true;
719            is_boundary[v] = true;
720        }
721    }
722    for v in 0..n {
723        if is_boundary[v] {
724            defect[v] -= std::f64::consts::PI;
725        }
726    }
727    defect
728}
729/// Compute the total (integrated) Gaussian curvature of the mesh.
730///
731/// By the Gauss–Bonnet theorem this equals `2π χ` where `χ` is the
732/// Euler characteristic.
733pub fn total_gaussian_curvature(mesh: &GeoMesh) -> f64 {
734    angular_defect(mesh).iter().sum()
735}
736/// Transport a tangent vector along a geodesic path using parallel transport.
737///
738/// Given a polyline path on the mesh surface (vertices in sequence), the
739/// function transports an initial tangent `vector` along the path, rotating
740/// it by the dihedral/rotation angle at each vertex to keep it parallel to
741/// the surface.
742///
743/// The transport is implemented by rotating `vector` in the tangent plane at
744/// each step: the rotation angle is the signed angle between consecutive edge
745/// directions projected onto the local tangent plane.
746///
747/// Returns the transported tangent vector at the end of the path.
748pub fn parallel_transport(mesh: &GeoMesh, path: &[usize], initial_vector: [f64; 3]) -> [f64; 3] {
749    if path.len() < 2 {
750        return initial_vector;
751    }
752    let mut v = initial_vector;
753    for i in 0..path.len() - 1 {
754        let p_curr = mesh.vertices[path[i]];
755        let p_next = mesh.vertices[path[i + 1]];
756        let edge = normalize3(sub3(p_next, p_curr));
757        let normal = vertex_normal_approx(mesh, path[i]);
758        let v_tangent = sub3(v, scale3(normal, dot3(v, normal)));
759        let v_tangent_len = len3(v_tangent);
760        if v_tangent_len < 1e-14 {
761            v = v_tangent;
762            continue;
763        }
764        let v_tangent_n = scale3(v_tangent, 1.0 / v_tangent_len);
765        let edge_tangent = sub3(edge, scale3(normal, dot3(edge, normal)));
766        let edge_tangent_len = len3(edge_tangent);
767        if edge_tangent_len < 1e-14 {
768            continue;
769        }
770        let edge_tangent_n = scale3(edge_tangent, 1.0 / edge_tangent_len);
771        let cos_a = dot3(v_tangent_n, edge_tangent_n).clamp(-1.0, 1.0);
772        let cross = cross3(v_tangent_n, edge_tangent_n);
773        let sin_a = dot3(cross, normal);
774        let _angle = sin_a.atan2(cos_a);
775        let v_along = scale3(edge, dot3(v, edge));
776        let v_perp = sub3(v, v_along);
777        let next_normal = vertex_normal_approx(mesh, path[i + 1]);
778        let v_perp_new = sub3(v_perp, scale3(next_normal, dot3(v_perp, next_normal)));
779        let perp_len = len3(v_perp);
780        let perp_new_len = len3(v_perp_new);
781        let v_perp_scaled = if perp_new_len > 1e-14 {
782            scale3(v_perp_new, perp_len / perp_new_len)
783        } else {
784            v_perp_new
785        };
786        let next_edge = if i + 1 < path.len() - 1 {
787            normalize3(sub3(mesh.vertices[path[i + 2]], p_next))
788        } else {
789            edge
790        };
791        v = add3(v_perp_scaled, scale3(next_edge, len3(v_along)));
792    }
793    v
794}
795/// Approximate vertex normal: average of incident face normals.
796pub(super) fn vertex_normal_approx(mesh: &GeoMesh, v: usize) -> [f64; 3] {
797    let mut acc = [0.0f64; 3];
798    let mut count = 0usize;
799    for &[a, b, c] in &mesh.faces {
800        if a == v || b == v || c == v {
801            acc = add3(
802                acc,
803                mesh.face_normal_raw(mesh.faces.iter().position(|&f| f == [a, b, c]).unwrap_or(0)),
804            );
805            count += 1;
806        }
807    }
808    if count == 0 {
809        return [0.0, 1.0, 0.0];
810    }
811    normalize3(acc)
812}
813/// Estimate geodesic distances using Varadhan's asymptotic formula:
814/// `d(x, y) ≈ sqrt(-4t * log u_t(x, y))`
815/// where `u_t` is the heat kernel evaluated after diffusion time `t`.
816///
817/// This directly inverts the heat flow to recover distance, valid for small `t`.
818///
819/// Returns a vector of distances from `source` to all vertices.
820pub fn varadhan_distances(mesh: &GeoMesh, source: usize, t: f64) -> Vec<f64> {
821    let n = mesh.n_vertices();
822    if n == 0 {
823        return Vec::new();
824    }
825    let eff_t = if t <= 0.0 { 1e-3 } else { t };
826    let result = heat_geodesic(mesh, source, eff_t, 5);
827    let max_heat = result.distances.iter().cloned().fold(0.0f64, f64::max);
828    let adj = mesh.build_adjacency();
829    let mut heat = vec![0.0_f64; n];
830    heat[source] = 1.0;
831    let dt = eff_t / 5.0;
832    for _ in 0..5 {
833        let old = heat.clone();
834        for v in 0..n {
835            if adj[v].is_empty() {
836                continue;
837            }
838            let lap: f64 =
839                adj[v].iter().map(|&(u, _)| old[u] - old[v]).sum::<f64>() / adj[v].len() as f64;
840            heat[v] = (old[v] + dt * lap).max(1e-300);
841        }
842    }
843    heat[source] = heat[source].max(1e-300);
844    let h0 = heat[source];
845    let distances: Vec<f64> = heat
846        .iter()
847        .map(|&h| {
848            let ratio = (h / h0).max(1e-300);
849            (-4.0 * eff_t * ratio.ln()).max(0.0).sqrt()
850        })
851        .collect();
852    let _ = (max_heat, result);
853    distances
854}
855/// Compute geodesic Voronoi cells on the mesh surface.
856///
857/// For each source vertex, returns a `GeoVoronoiCell` containing the mesh
858/// vertices and approximate surface area belonging to that cell.
859pub fn geodesic_voronoi_cells(mesh: &GeoMesh, sources: &[usize]) -> Vec<GeoVoronoiCell> {
860    if sources.is_empty() {
861        return Vec::new();
862    }
863    let regions = geodesic_voronoi_regions(mesh, sources);
864    let ns = sources.len();
865    let mut cells: Vec<GeoVoronoiCell> = sources
866        .iter()
867        .enumerate()
868        .map(|(si, &sv)| GeoVoronoiCell {
869            source_idx: si,
870            source_vertex: sv,
871            vertices: Vec::new(),
872            area: 0.0,
873        })
874        .collect();
875    for (v, &r) in regions.iter().enumerate() {
876        if r < ns {
877            cells[r].vertices.push(v);
878        }
879    }
880    for fi in 0..mesh.n_faces() {
881        let area = mesh.face_area(fi);
882        let [a, b, c] = mesh.faces[fi];
883        for &v in &[a, b, c] {
884            let r = regions[v];
885            if r < ns {
886                cells[r].area += area / 3.0;
887            }
888        }
889    }
890    cells
891}
892/// Compute the cotangent weight for an edge in a face.
893///
894/// Given the three edge lengths `lab`, `lac`, `lbc` in a triangle (where the
895/// edge of interest is `bc` and the angle is at vertex `a`), returns `cot(angle_a)`.
896pub fn cotangent_weight(lab: f64, lac: f64, lbc: f64) -> f64 {
897    let cos_a = (lab * lab + lac * lac - lbc * lbc) / (2.0 * lab * lac + 1e-300);
898    let sin2_a = (1.0 - cos_a * cos_a).max(0.0);
899    let sin_a = sin2_a.sqrt();
900    if sin_a < 1e-14 { 0.0 } else { cos_a / sin_a }
901}
902/// Build a cotangent weight Laplacian as a sparse (adjacency-weighted) list.
903///
904/// Returns a vector of `(neighbour_index, weight)` lists for each vertex.
905/// Cotangent weights are useful for the heat method and other PDE-based algorithms.
906pub fn build_cotangent_laplacian(mesh: &GeoMesh) -> Vec<Vec<(usize, f64)>> {
907    let n = mesh.n_vertices();
908    let mut laplacian: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
909    for &[i, j, k] in &mesh.faces {
910        let pi = mesh.vertices[i];
911        let pj = mesh.vertices[j];
912        let pk = mesh.vertices[k];
913        let lij = dist3(pi, pj);
914        let lik = dist3(pi, pk);
915        let ljk = dist3(pj, pk);
916        let cot_k = cotangent_weight(ljk, lik, lij);
917        let cot_j = cotangent_weight(lij, ljk, lik);
918        let cot_i = cotangent_weight(lik, lij, ljk);
919        laplacian[i].push((j, cot_k * 0.5));
920        laplacian[j].push((i, cot_k * 0.5));
921        laplacian[i].push((k, cot_j * 0.5));
922        laplacian[k].push((i, cot_j * 0.5));
923        laplacian[j].push((k, cot_i * 0.5));
924        laplacian[k].push((j, cot_i * 0.5));
925    }
926    laplacian
927}
928/// Compute geodesic distances on the intrinsic Delaunay mesh.
929///
930/// This runs Dijkstra on the edge lengths in `IntrinsicDelaunay`, which may
931/// differ from the original mesh's edge lengths after edge flips.
932pub fn dijkstra_intrinsic(id: &IntrinsicDelaunay, source: usize) -> Vec<f64> {
933    let n = id.vertices.len();
934    let mut dist = vec![f64::INFINITY; n];
935    dist[source] = 0.0;
936    let mut adj: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
937    for (fi, &[a, b, c]) in id.faces.iter().enumerate() {
938        let lab = id.edge_lengths[fi][2];
939        let lac = id.edge_lengths[fi][1];
940        let lbc = id.edge_lengths[fi][0];
941        adj[a].push((b, lab));
942        adj[b].push((a, lab));
943        adj[a].push((c, lac));
944        adj[c].push((a, lac));
945        adj[b].push((c, lbc));
946        adj[c].push((b, lbc));
947    }
948    let mut heap: BinaryHeap<DijkEntry> = BinaryHeap::new();
949    heap.push(DijkEntry {
950        dist: 0.0,
951        vertex: source,
952    });
953    while let Some(DijkEntry { dist: d, vertex: u }) = heap.pop() {
954        if d > dist[u] {
955            continue;
956        }
957        for &(v, w) in &adj[u] {
958            let alt = dist[u] + w;
959            if alt < dist[v] {
960                dist[v] = alt;
961                heap.push(DijkEntry {
962                    dist: alt,
963                    vertex: v,
964                });
965            }
966        }
967    }
968    dist
969}