use std::collections::HashMap;
use crate::geometry::{cotan_edge_weight, face_area};
use crate::ids::{FaceId, HalfEdgeId, VertexId};
use crate::linalg::{SparseSystem, conjugate_gradient, regularize_diagonal};
use crate::storage::MeshStorage;
use crate::traversal::{FaceHalfEdges, VertexRing};
fn build_vertex_index(mesh: &MeshStorage) -> HashMap<VertexId, usize> {
mesh.vertex_ids().enumerate().map(|(i, v)| (v, i)).collect()
}
fn build_laplacian_and_mass(
mesh: &MeshStorage,
v_idx: &HashMap<VertexId, usize>,
) -> (SparseSystem, Vec<f64>) {
let n = v_idx.len();
let mut lap = SparseSystem::new(n);
let mut mass = vec![0.0; n];
for (v, &i) in v_idx {
let mut diag = 0.0;
for he in VertexRing::new(mesh, *v) {
let neighbor = mesh.get_halfedge(he).unwrap().vertex;
if let Some(&j) = v_idx.get(&neighbor) {
let w = cotan_edge_weight(mesh, he).unwrap_or(0.0) / 2.0;
if w > 0.0 {
lap.add(i, j, -w);
diag += w;
}
}
}
lap.add_diag(i, diag);
}
for f in mesh.face_ids() {
let halfedges: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, f).collect();
if halfedges.len() != 3 {
continue;
}
let v0 = mesh.get_halfedge(halfedges[0]).unwrap().vertex;
let v1 = mesh.get_halfedge(halfedges[1]).unwrap().vertex;
let v2 = mesh.get_halfedge(halfedges[2]).unwrap().vertex;
let Some(&i0) = v_idx.get(&v0) else { continue };
let Some(&i1) = v_idx.get(&v1) else { continue };
let Some(&i2) = v_idx.get(&v2) else { continue };
let area = face_area(mesh, f).unwrap_or(0.0);
let a3 = area / 3.0;
mass[i0] += a3;
mass[i1] += a3;
mass[i2] += a3;
}
for m in mass.iter_mut() {
if *m < 1e-14 {
*m = 1e-14;
}
}
(lap, mass)
}
fn build_divergence_from_gradient(
mesh: &MeshStorage,
v_idx: &HashMap<VertexId, usize>,
face_gradient: &HashMap<FaceId, [f64; 3]>,
) -> (Vec<f64>, SparseSystem) {
let n = v_idx.len();
let mut rhs = vec![0.0; n];
let mut lap = SparseSystem::new(n);
for i in 0..n {
lap.add_diag(i, 0.0);
}
for f in mesh.face_ids() {
let Some(&grad) = face_gradient.get(&f) else {
continue;
};
let halfedges: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, f).collect();
if halfedges.len() != 3 {
continue;
}
let v0 = mesh.get_halfedge(halfedges[0]).unwrap().vertex;
let v1 = mesh.get_halfedge(halfedges[1]).unwrap().vertex;
let v2 = mesh.get_halfedge(halfedges[2]).unwrap().vertex;
let Some(&i0) = v_idx.get(&v0) else { continue };
let Some(&i1) = v_idx.get(&v1) else { continue };
let Some(&i2) = v_idx.get(&v2) else { continue };
let p0 = mesh.get_vertex(v0).unwrap().position;
let p1 = mesh.get_vertex(v1).unwrap().position;
let p2 = mesh.get_vertex(v2).unwrap().position;
let area = face_area_from_positions(p0, p1, p2);
if area < 1e-14 {
continue;
}
let n = face_normal_from_positions(p0, p1, p2);
let n = normalize(n);
let e01 = sub(p1, p0);
let e12 = sub(p2, p1);
let e20 = sub(p0, p2);
let grad_phi0 = scale(cross(n, e12), 1.0 / (2.0 * area));
let grad_phi1 = scale(cross(n, e20), 1.0 / (2.0 * area));
let grad_phi2 = scale(cross(n, e01), 1.0 / (2.0 * area));
rhs[i0] += dot(&grad, &grad_phi0) * area;
rhs[i1] += dot(&grad, &grad_phi1) * area;
rhs[i2] += dot(&grad, &grad_phi2) * area;
}
let _interior_all: Vec<usize> = (0..n).collect();
let (cot_lap, _) = build_laplacian_and_mass(mesh, v_idx);
(rhs, cot_lap)
}
type Vec3 = [f64; 3];
fn sub(a: Vec3, b: Vec3) -> Vec3 {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add(a: Vec3, b: Vec3) -> Vec3 {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale(a: Vec3, s: f64) -> Vec3 {
[a[0] * s, a[1] * s, a[2] * s]
}
fn dot(a: &Vec3, b: &Vec3) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross(a: Vec3, b: Vec3) -> Vec3 {
[
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
]
}
fn length(a: Vec3) -> f64 {
dot(&a, &a).sqrt()
}
fn normalize(a: Vec3) -> Vec3 {
let l = length(a);
if l < 1e-12 { a } else { scale(a, 1.0 / l) }
}
fn face_area_from_positions(a: Vec3, b: Vec3, c: Vec3) -> f64 {
let cross = cross(sub(b, a), sub(c, a));
0.5 * length(cross)
}
fn face_normal_from_positions(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
normalize(cross(sub(b, a), sub(c, a)))
}
pub fn geodesic_distance_from_vertex(mesh: &MeshStorage, source: VertexId) -> Option<Vec<f64>> {
let n = mesh.vertex_count();
if n == 0 {
return Some(Vec::new());
}
let v_idx = build_vertex_index(mesh);
let source_idx = *v_idx.get(&source)?;
let mut total_len = 0.0;
let mut edge_count = 0;
for he in mesh.halfedge_ids() {
if let Some(len) = crate::geometry::edge_length(mesh, he) {
total_len += len;
edge_count += 1;
}
}
let h_sq = if edge_count > 0 {
let h = total_len / (edge_count as f64);
h * h
} else {
1.0
};
let (lap, mass) = build_laplacian_and_mass(mesh, &v_idx);
let mut heat_sys = SparseSystem::new(n);
let cot_lap = lap.finish();
for (row_idx, row) in cot_lap.outer_iterator().enumerate() {
for (col_idx, &val) in row.iter() {
heat_sys.add(row_idx, col_idx, h_sq * val);
}
}
for (i, &m) in mass.iter().enumerate() {
heat_sys.add_diag(i, m);
}
let mut heat_a = heat_sys.finish();
let mut u0 = vec![0.0; n];
u0[source_idx] = 1.0 / mass[source_idx];
let mut heat_rhs = vec![0.0; n];
for i in 0..n {
heat_rhs[i] = mass[i] * u0[i];
}
regularize_diagonal(&mut heat_a, 1e-10);
let u = conjugate_gradient(&heat_a, &heat_rhs, n * 100, 1e-6)?;
let face_grad = compute_face_gradients(mesh, &v_idx, &u);
let mut face_grad_norm: HashMap<FaceId, [f64; 3]> = HashMap::new();
for (&f, &g) in &face_grad {
let len = length(g);
if len > 1e-10 {
face_grad_norm.insert(f, scale(g, -1.0 / len));
} else {
face_grad_norm.insert(f, [0.0, 0.0, 0.0]);
}
}
let (div_rhs, _cot_lap_sys) = build_divergence_from_gradient(mesh, &v_idx, &face_grad_norm);
let (_lap_re, _) = build_laplacian_and_mass(mesh, &v_idx);
let mut poisson_lap = _lap_re.finish();
regularize_diagonal(&mut poisson_lap, 1e-10);
let phi = conjugate_gradient(&poisson_lap, &div_rhs, n * 100, 1e-6)?;
let phi_source = phi[source_idx];
let distance: Vec<f64> = phi.iter().map(|&p| (p - phi_source).abs()).collect();
Some(distance)
}
fn compute_face_gradients(
mesh: &MeshStorage,
v_idx: &HashMap<VertexId, usize>,
u: &[f64],
) -> HashMap<FaceId, [f64; 3]> {
let mut grad = HashMap::new();
for f in mesh.face_ids() {
let halfedges: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, f).collect();
if halfedges.len() != 3 {
continue;
}
let v0 = mesh.get_halfedge(halfedges[0]).unwrap().vertex;
let v1 = mesh.get_halfedge(halfedges[1]).unwrap().vertex;
let v2 = mesh.get_halfedge(halfedges[2]).unwrap().vertex;
let Some(&i0) = v_idx.get(&v0) else { continue };
let Some(&i1) = v_idx.get(&v1) else { continue };
let Some(&i2) = v_idx.get(&v2) else { continue };
let p0 = mesh.get_vertex(v0).unwrap().position;
let p1 = mesh.get_vertex(v1).unwrap().position;
let p2 = mesh.get_vertex(v2).unwrap().position;
let area = face_area_from_positions(p0, p1, p2);
if area < 1e-14 {
continue;
}
let n = face_normal_from_positions(p0, p1, p2);
let e0 = sub(p2, p1);
let e1 = sub(p0, p2);
let e2 = sub(p1, p0);
let sum = add(add(scale(e0, u[i0]), scale(e1, u[i1])), scale(e2, u[i2]));
let g = scale(cross(n, sum), 1.0 / (2.0 * area));
grad.insert(f, g);
}
grad
}
pub fn shortest_path(mesh: &MeshStorage, distance: &[f64], target: VertexId) -> Vec<VertexId> {
let v_idx = build_vertex_index(mesh);
let Some(&target_idx) = v_idx.get(&target) else {
return vec![];
};
if target_idx >= distance.len() {
return vec![];
}
let mut path = vec![target];
let mut current = target;
let mut current_dist = distance[target_idx];
let max_steps = mesh.vertex_count() * 2;
for _ in 0..max_steps {
if current_dist < 1e-10 {
break;
}
let mut best_neighbor = None;
let mut best_dist = current_dist;
for he in VertexRing::new(mesh, current) {
let neighbor = mesh.get_halfedge(he).unwrap().vertex;
if let Some(&ni) = v_idx.get(&neighbor)
&& ni < distance.len()
{
let nd = distance[ni];
if nd < best_dist {
best_dist = nd;
best_neighbor = Some(neighbor);
}
}
}
match best_neighbor {
Some(v) => {
path.push(v);
current = v;
current_dist = best_dist;
}
None => break, }
}
path
}
use std::cmp::Ordering;
use std::collections::BinaryHeap;
#[derive(Clone, Copy, Debug, PartialEq)]
struct QueueEntry {
dist: f64,
vertex: usize, }
impl Eq for QueueEntry {}
impl Ord for QueueEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.dist
.partial_cmp(&self.dist)
.unwrap_or(Ordering::Equal)
.then_with(|| other.vertex.cmp(&self.vertex))
}
}
impl PartialOrd for QueueEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn dijkstra_geodesic(mesh: &MeshStorage, source: VertexId) -> Vec<f64> {
let n = mesh.vertex_count();
if n == 0 {
return Vec::new();
}
let v_idx = build_vertex_index(mesh);
let Some(&source_idx) = v_idx.get(&source) else {
return vec![f64::INFINITY; n];
};
let mut dist = vec![f64::INFINITY; n];
let mut visited = vec![false; n];
dist[source_idx] = 0.0;
let mut heap = BinaryHeap::new();
heap.push(QueueEntry {
dist: 0.0,
vertex: source_idx,
});
while let Some(QueueEntry { dist: d, vertex: u }) = heap.pop() {
if visited[u] {
continue;
}
visited[u] = true;
let u_vid = mesh.vertex_ids().nth(u).unwrap();
for he in VertexRing::new(mesh, u_vid) {
let Some(h) = mesh.get_halfedge(he) else {
continue;
};
let neighbor_vid = h.vertex;
let Some(&v) = v_idx.get(&neighbor_vid) else {
continue;
};
if visited[v] {
continue;
}
let Some(edge_len) = crate::geometry::edge_length(mesh, he) else {
continue;
};
let new_dist = d + edge_len;
if new_dist < dist[v] {
dist[v] = new_dist;
heap.push(QueueEntry {
dist: new_dist,
vertex: v,
});
}
}
}
dist
}
pub fn dijkstra_multi_source_geodesic(mesh: &MeshStorage, sources: &[VertexId]) -> Vec<f64> {
let n = mesh.vertex_count();
if n == 0 {
return Vec::new();
}
let v_idx = build_vertex_index(mesh);
let mut dist = vec![f64::INFINITY; n];
let mut visited = vec![false; n];
let mut heap = BinaryHeap::new();
for &s in sources {
if let Some(&si) = v_idx.get(&s) {
dist[si] = 0.0;
heap.push(QueueEntry {
dist: 0.0,
vertex: si,
});
}
}
while let Some(QueueEntry { dist: d, vertex: u }) = heap.pop() {
if visited[u] {
continue;
}
visited[u] = true;
let u_vid = mesh.vertex_ids().nth(u).unwrap();
for he in VertexRing::new(mesh, u_vid) {
let Some(h) = mesh.get_halfedge(he) else {
continue;
};
let neighbor_vid = h.vertex;
let Some(&v) = v_idx.get(&neighbor_vid) else {
continue;
};
if visited[v] {
continue;
}
let Some(edge_len) = crate::geometry::edge_length(mesh, he) else {
continue;
};
let new_dist = d + edge_len;
if new_dist < dist[v] {
dist[v] = new_dist;
heap.push(QueueEntry {
dist: new_dist,
vertex: v,
});
}
}
}
dist
}
pub fn dijkstra_with_parent(mesh: &MeshStorage, source: VertexId) -> (Vec<f64>, Vec<usize>) {
let n = mesh.vertex_count();
let v_idx = build_vertex_index(mesh);
if n == 0 || !v_idx.contains_key(&source) {
return (Vec::new(), Vec::new());
}
let source_idx = *v_idx.get(&source).unwrap();
let mut dist = vec![f64::INFINITY; n];
let mut parent = vec![usize::MAX; n];
let mut visited = vec![false; n];
dist[source_idx] = 0.0;
parent[source_idx] = source_idx;
let mut heap = BinaryHeap::new();
heap.push(QueueEntry {
dist: 0.0,
vertex: source_idx,
});
while let Some(QueueEntry { dist: d, vertex: u }) = heap.pop() {
if visited[u] {
continue;
}
visited[u] = true;
let u_vid = mesh.vertex_ids().nth(u).unwrap();
for he in VertexRing::new(mesh, u_vid) {
let Some(h) = mesh.get_halfedge(he) else {
continue;
};
let neighbor_vid = h.vertex;
let Some(&v) = v_idx.get(&neighbor_vid) else {
continue;
};
if visited[v] {
continue;
}
let Some(edge_len) = crate::geometry::edge_length(mesh, he) else {
continue;
};
let new_dist = d + edge_len;
if new_dist < dist[v] {
dist[v] = new_dist;
parent[v] = u;
heap.push(QueueEntry {
dist: new_dist,
vertex: v,
});
}
}
}
(dist, parent)
}
pub fn dijkstra_shortest_path(
mesh: &MeshStorage,
source: VertexId,
target: VertexId,
) -> Vec<VertexId> {
let n = mesh.vertex_count();
if n == 0 {
return Vec::new();
}
let v_idx = build_vertex_index(mesh);
let Some(&source_idx) = v_idx.get(&source) else {
return Vec::new();
};
let Some(&target_idx) = v_idx.get(&target) else {
return Vec::new();
};
if source == target {
return vec![source];
}
let (dist, parent) = dijkstra_with_parent(mesh, source);
if !dist[target_idx].is_finite() || parent[target_idx] == usize::MAX {
return Vec::new();
}
let mut idx_path = Vec::new();
let mut cur = target_idx;
while cur != source_idx && cur != usize::MAX {
idx_path.push(cur);
cur = parent[cur];
}
if cur == usize::MAX {
return Vec::new();
}
idx_path.push(source_idx);
idx_path.reverse();
let vid_by_idx: Vec<VertexId> = mesh.vertex_ids().collect();
idx_path.into_iter().map(|i| vid_by_idx[i]).collect()
}
pub fn multi_source_geodesic(mesh: &MeshStorage, sources: &[VertexId]) -> Option<Vec<f64>> {
let n = mesh.vertex_count();
if n == 0 || sources.is_empty() {
return None;
}
let v_idx = build_vertex_index(mesh);
let mut total_len = 0.0;
let mut edge_count = 0usize;
for he in mesh.halfedge_ids() {
if let Some(len) = crate::geometry::edge_length(mesh, he) {
total_len += len;
edge_count += 1;
}
}
let h_sq = if edge_count > 0 {
let h = total_len / (edge_count as f64);
h * h
} else {
1.0
};
let (lap, mass) = build_laplacian_and_mass(mesh, &v_idx);
let mut heat_sys = SparseSystem::new(n);
let cot_lap = lap.finish();
for (row_idx, row) in cot_lap.outer_iterator().enumerate() {
for (col_idx, &val) in row.iter() {
heat_sys.add(row_idx, col_idx, h_sq * val);
}
}
for (i, &m) in mass.iter().enumerate() {
heat_sys.add_diag(i, m);
}
let mut heat_a = heat_sys.finish();
let mut u0 = vec![0.0; n];
for &s in sources {
if let Some(&si) = v_idx.get(&s) {
u0[si] += 1.0 / mass[si].max(1e-14);
}
}
let mut heat_rhs = vec![0.0; n];
for i in 0..n {
heat_rhs[i] = mass[i] * u0[i];
}
regularize_diagonal(&mut heat_a, 1e-10);
let u = conjugate_gradient(&heat_a, &heat_rhs, n * 100, 1e-6)?;
let face_grad = compute_face_gradients(mesh, &v_idx, &u);
let mut face_grad_norm: HashMap<FaceId, [f64; 3]> = HashMap::new();
for (&f, &g) in &face_grad {
let len = length(g);
if len > 1e-10 {
face_grad_norm.insert(f, scale(g, -1.0 / len));
} else {
face_grad_norm.insert(f, [0.0, 0.0, 0.0]);
}
}
let (div_rhs, _) = build_divergence_from_gradient(mesh, &v_idx, &face_grad_norm);
let (_lap_re, _) = build_laplacian_and_mass(mesh, &v_idx);
let mut poisson_lap = _lap_re.finish();
regularize_diagonal(&mut poisson_lap, 1e-10);
let phi = conjugate_gradient(&poisson_lap, &div_rhs, n * 100, 1e-6)?;
let mut min_source_phi = f64::INFINITY;
for &s in sources {
if let Some(&si) = v_idx.get(&s)
&& phi[si] < min_source_phi
{
min_source_phi = phi[si];
}
}
if !min_source_phi.is_finite() {
min_source_phi = 0.0;
}
let mut distance: Vec<f64> = phi.iter().map(|&p| (p - min_source_phi).abs()).collect();
for &s in sources {
if let Some(&si) = v_idx.get(&s) {
distance[si] = 0.0;
}
}
Some(distance)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_util::build_icosphere;
#[test]
fn test_geodesic_self_distance() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let result = geodesic_distance_from_vertex(&mesh, vertices[0]);
assert!(result.is_some(), "Heat method should succeed on icosphere");
let dist = result.unwrap();
assert!(
dist[0] < 1e-6,
"Source vertex distance should be ~0, got {}",
dist[0]
);
let has_positive = dist.iter().enumerate().any(|(i, d)| i != 0 && *d > 0.0);
assert!(
has_positive,
"Some non-source vertices should have positive distance"
);
}
#[test]
fn test_geodesic_monotonicity() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let result = geodesic_distance_from_vertex(&mesh, vertices[0]);
assert!(result.is_some());
let dist = result.unwrap();
for (i, &v) in vertices.iter().enumerate().skip(1) {
let d_i = dist[i];
let has_closer = VertexRing::new(&mesh, v).any(|he| {
let neighbor = mesh.get_halfedge(he).unwrap().vertex;
let ni = vertices.iter().position(|&x| x == neighbor);
ni.is_some_and(|j| dist[j] < d_i)
});
if i < 10 {
let _ = has_closer; }
}
}
#[test]
fn test_dijkstra_self_distance_zero() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let dist = dijkstra_geodesic(&mesh, vertices[0]);
assert_eq!(dist.len(), mesh.vertex_count());
assert!(dist[0].abs() < 1e-12, "source distance must be 0");
}
#[test]
fn test_dijkstra_symmetric_on_icosphere() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d0 = dijkstra_geodesic(&mesh, vertices[0]);
let (antipode_idx, &max_d) = d0
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
assert!(max_d > 0.0, "max distance should be positive");
let d_back = dijkstra_geodesic(&mesh, vertices[antipode_idx]);
assert!(
(d_back[0] - max_d).abs() < 1e-10,
"antipode-to-source {} should equal source-to-antipode {}",
d_back[0],
max_d
);
}
#[test]
fn test_dijkstra_distance_upper_bounds_heat_method() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_dijk = dijkstra_geodesic(&mesh, vertices[0]);
let d_heat = geodesic_distance_from_vertex(&mesh, vertices[0]).unwrap();
assert!(d_dijk[0].abs() < 1e-10);
assert!(d_heat[0] < 1e-3);
let max_dijk = d_dijk.iter().cloned().fold(0.0_f64, f64::max);
let max_heat = d_heat.iter().cloned().fold(0.0_f64, f64::max);
assert!(
max_heat <= max_dijk * 3.0 && max_heat >= max_dijk * 0.3,
"max heat {} should be within 3x of max dijkstra {}",
max_heat,
max_dijk
);
}
#[test]
fn test_dijkstra_multi_source() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_single = dijkstra_geodesic(&mesh, vertices[0]);
let (_antipode_idx, _) = d_single
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
let sources = vec![vertices[0], vertices[d_single.len() - 1]];
let d_multi = dijkstra_multi_source_geodesic(&mesh, &sources);
for i in 0..d_single.len() {
assert!(
d_multi[i] <= d_single[i] + 1e-12,
"multi-source {} should be <= single-source {} at vertex {}",
d_multi[i],
d_single[i],
i
);
}
}
#[test]
fn test_dijkstra_shortest_path_self() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let path = dijkstra_shortest_path(&mesh, vertices[0], vertices[0]);
assert_eq!(path, vec![vertices[0]]);
}
#[test]
fn test_dijkstra_shortest_path_to_neighbor() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let neighbor = VertexRing::new(&mesh, vertices[0]).next().unwrap();
let neighbor_vid = mesh.get_halfedge(neighbor).unwrap().vertex;
let path = dijkstra_shortest_path(&mesh, vertices[0], neighbor_vid);
assert_eq!(path.len(), 2, "path to neighbor should have 2 vertices");
assert_eq!(path[0], vertices[0]);
assert_eq!(path[1], neighbor_vid);
}
#[test]
fn test_dijkstra_shortest_path_consistency_with_distance() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d = dijkstra_geodesic(&mesh, vertices[0]);
let (target_idx, &target_dist) = d
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
.unwrap();
let path = dijkstra_shortest_path(&mesh, vertices[0], vertices[target_idx]);
assert!(path.len() >= 2);
let mut path_len = 0.0;
for w in path.windows(2) {
let mut found = false;
for he in VertexRing::new(&mesh, w[0]) {
let tip = mesh.get_halfedge(he).unwrap().vertex;
if tip == w[1] {
path_len += crate::geometry::edge_length(&mesh, he).unwrap();
found = true;
break;
}
}
assert!(found, "path contains a non-edge jump");
}
assert!(
(path_len - target_dist).abs() < 1e-9,
"path length {} should equal dijkstra distance {}",
path_len,
target_dist
);
}
#[test]
fn test_multi_source_geodesic_sources_zero() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let sources = vec![vertices[0], vertices[vertices.len() / 2]];
let result = multi_source_geodesic(&mesh, &sources);
assert!(result.is_some());
let dist = result.unwrap();
let v_idx = build_vertex_index(&mesh);
for &s in &sources {
let si = *v_idx.get(&s).unwrap();
assert!(
dist[si] < 1e-12,
"source {} distance {} should be 0",
si,
dist[si]
);
}
}
#[test]
fn test_multi_source_geodesic_le_single_source() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_single = geodesic_distance_from_vertex(&mesh, vertices[0]).unwrap();
let sources = vec![vertices[0], vertices[vertices.len() - 1]];
let d_multi = multi_source_geodesic(&mesh, &sources).unwrap();
let max_single = d_single.iter().cloned().fold(0.0_f64, f64::max);
let max_multi = d_multi.iter().cloned().fold(0.0_f64, f64::max);
assert!(
max_multi <= max_single + 1e-6,
"multi max {} should be <= single max {}",
max_multi,
max_single
);
}
}