use std::collections::HashMap;
use rayon::prelude::*;
use crate::geometry::face_area;
use crate::ids::{FaceId, HalfEdgeId, VertexId};
use crate::linalg::vec3::{self, Vec3};
use crate::linalg::{
SparseSystem, build_cotan_laplacian, build_vertex_index, conjugate_gradient,
regularize_diagonal,
};
use crate::storage::MeshStorage;
use crate::traversal::{FaceHalfEdges, VertexAdjacentFaces, VertexRing};
fn build_vertex_index_and_list(mesh: &MeshStorage) -> (Vec<VertexId>, HashMap<VertexId, usize>) {
let list: Vec<VertexId> = mesh.vertex_ids().collect();
let map = list.iter().enumerate().map(|(i, &v)| (v, i)).collect();
(list, map)
}
fn build_laplacian_and_mass(
mesh: &MeshStorage,
v_idx: &HashMap<VertexId, usize>,
) -> (SparseSystem, Vec<f64>) {
let n = v_idx.len();
let lap = build_cotan_laplacian(mesh, v_idx);
let mut mass = vec![0.0; n];
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])
.expect("halfedge exists in mesh")
.vertex;
let v1 = mesh
.get_halfedge(halfedges[1])
.expect("halfedge exists in mesh")
.vertex;
let v2 = mesh
.get_halfedge(halfedges[2])
.expect("halfedge exists in mesh")
.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> {
let n = v_idx.len();
let mut rhs = vec![0.0; n];
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])
.expect("halfedge exists in mesh")
.vertex;
let v1 = mesh
.get_halfedge(halfedges[1])
.expect("halfedge exists in mesh")
.vertex;
let v2 = mesh
.get_halfedge(halfedges[2])
.expect("halfedge exists in mesh")
.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).expect("vertex exists in mesh").position;
let p1 = mesh.get_vertex(v1).expect("vertex exists in mesh").position;
let p2 = mesh.get_vertex(v2).expect("vertex exists in mesh").position;
let area = vec3::triangle_area(p0, p1, p2);
if area < 1e-14 {
continue;
}
let n = vec3::triangle_normal(p0, p1, p2);
let n = vec3::normalize(n);
let e01 = vec3::sub(p1, p0);
let e12 = vec3::sub(p2, p1);
let e20 = vec3::sub(p0, p2);
let grad_phi0 = vec3::scale(vec3::cross(n, e12), 1.0 / (2.0 * area));
let grad_phi1 = vec3::scale(vec3::cross(n, e20), 1.0 / (2.0 * area));
let grad_phi2 = vec3::scale(vec3::cross(n, e01), 1.0 / (2.0 * area));
rhs[i0] += vec3::dot(grad, grad_phi0) * area;
rhs[i1] += vec3::dot(grad, grad_phi1) * area;
rhs[i2] += vec3::dot(grad, grad_phi2) * area;
}
rhs
}
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 he_ids: Vec<HalfEdgeId> = mesh.halfedge_ids().collect();
let (total_len, edge_count) = he_ids
.par_iter()
.filter_map(|&he| crate::geometry::edge_length(mesh, he))
.fold(|| (0.0f64, 0u64), |(sum, cnt), len| (sum + len, cnt + 1))
.reduce(|| (0.0f64, 0u64), |a, b| (a.0 + b.0, a.1 + b.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 grad_entries: Vec<(FaceId, [f64; 3])> = face_grad.into_iter().collect();
let face_grad_norm: HashMap<FaceId, [f64; 3]> = grad_entries
.par_iter()
.map(|(f, g)| {
let len = vec3::length(*g);
if len > 1e-10 {
(*f, vec3::scale(*g, -1.0 / len))
} else {
(*f, [0.0, 0.0, 0.0])
}
})
.collect();
let div_rhs = build_divergence_from_gradient(mesh, &v_idx, &face_grad_norm);
let mut poisson_lap = cot_lap.clone();
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 face_ids: Vec<FaceId> = mesh.face_ids().collect();
let results: Vec<(FaceId, [f64; 3])> = face_ids
.par_iter()
.filter_map(|&f| {
let halfedges: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, f).collect();
if halfedges.len() != 3 {
return None;
}
let v0 = mesh
.get_halfedge(halfedges[0])
.expect("halfedge exists in mesh")
.vertex;
let v1 = mesh
.get_halfedge(halfedges[1])
.expect("halfedge exists in mesh")
.vertex;
let v2 = mesh
.get_halfedge(halfedges[2])
.expect("halfedge exists in mesh")
.vertex;
let &i0 = v_idx.get(&v0)?;
let &i1 = v_idx.get(&v1)?;
let &i2 = v_idx.get(&v2)?;
let p0 = mesh.get_vertex(v0).expect("vertex exists in mesh").position;
let p1 = mesh.get_vertex(v1).expect("vertex exists in mesh").position;
let p2 = mesh.get_vertex(v2).expect("vertex exists in mesh").position;
let area = vec3::triangle_area(p0, p1, p2);
if area < 1e-14 {
return None;
}
let n = vec3::triangle_normal(p0, p1, p2);
let e0 = vec3::sub(p2, p1);
let e1 = vec3::sub(p0, p2);
let e2 = vec3::sub(p1, p0);
let sum = vec3::add(
vec3::add(vec3::scale(e0, u[i0]), vec3::scale(e1, u[i1])),
vec3::scale(e2, u[i2]),
);
let g = vec3::scale(vec3::cross(n, sum), 1.0 / (2.0 * area));
Some((f, g))
})
.collect();
let mut grad = HashMap::with_capacity(results.len());
for (f, g) in results {
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)
.expect("halfedge exists in mesh")
.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 (vid_list, v_idx) = build_vertex_index_and_list(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 = vid_list[u];
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 (vid_list, v_idx) = build_vertex_index_and_list(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 = vid_list[u];
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 (vid_list, v_idx) = build_vertex_index_and_list(mesh);
if n == 0 || !v_idx.contains_key(&source) {
return (Vec::new(), Vec::new());
}
let source_idx = *v_idx.get(&source).expect("source vertex must be in index");
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 = vid_list[u];
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 he_ids: Vec<HalfEdgeId> = mesh.halfedge_ids().collect();
let (total_len, edge_count) = he_ids
.par_iter()
.filter_map(|&he| crate::geometry::edge_length(mesh, he))
.fold(|| (0.0f64, 0u64), |(sum, cnt), len| (sum + len, cnt + 1))
.reduce(|| (0.0f64, 0u64), |a, b| (a.0 + b.0, a.1 + b.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 grad_entries: Vec<(FaceId, [f64; 3])> = face_grad.into_iter().collect();
let face_grad_norm: HashMap<FaceId, [f64; 3]> = grad_entries
.par_iter()
.map(|(f, g)| {
let len = vec3::length(*g);
if len > 1e-10 {
(*f, vec3::scale(*g, -1.0 / len))
} else {
(*f, [0.0, 0.0, 0.0])
}
})
.collect();
let div_rhs = build_divergence_from_gradient(mesh, &v_idx, &face_grad_norm);
let mut poisson_lap = cot_lap.clone();
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)
}
type Vec2 = [f64; 2];
#[derive(Clone, Debug)]
struct MmpWindow {
he: HalfEdgeId,
b0: f64,
b1: f64,
d0: f64,
d1: f64,
pseudo_src: Vec3,
from_face: FaceId,
}
#[derive(Clone, Debug)]
struct MmpEntry {
key: f64,
window: MmpWindow,
}
impl Eq for MmpEntry {}
impl PartialEq for MmpEntry {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl Ord for MmpEntry {
fn cmp(&self, other: &Self) -> Ordering {
other
.key
.partial_cmp(&self.key)
.unwrap_or(Ordering::Equal)
.then_with(|| other.key.to_bits().cmp(&self.key.to_bits()))
}
}
impl PartialOrd for MmpEntry {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn sub2(a: Vec2, b: Vec2) -> Vec2 {
[a[0] - b[0], a[1] - b[1]]
}
fn dot2(a: &Vec2, b: &Vec2) -> f64 {
a[0] * b[0] + a[1] * b[1]
}
fn cross2(a: Vec2, b: Vec2) -> f64 {
a[0] * b[1] - a[1] * b[0]
}
fn length2(a: Vec2) -> f64 {
dot2(&a, &a).sqrt()
}
fn compute_face_normal(mesh: &MeshStorage, face: FaceId) -> Vec3 {
let hes: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, face).collect();
if hes.len() < 3 {
return [0.0, 0.0, 1.0];
}
let p0 = halfedge_tip(mesh, hes[0])
.and_then(|v| mesh.get_vertex(v))
.map(|v| v.position);
let p1 = halfedge_tip(mesh, hes[1])
.and_then(|v| mesh.get_vertex(v))
.map(|v| v.position);
let p2 = halfedge_tip(mesh, hes[2])
.and_then(|v| mesh.get_vertex(v))
.map(|v| v.position);
match (p0, p1, p2) {
(Some(a), Some(b), Some(c)) => vec3::triangle_normal(a, b, c),
_ => [0.0, 0.0, 1.0],
}
}
fn unfold_pseudo_source(
pseudo_src: Vec3,
edge_start: Vec3,
edge_end: Vec3,
source_face_normal: Vec3,
target_face_normal: Vec3,
) -> Vec3 {
let edge_vec = vec3::sub(edge_end, edge_start);
let edge_len = vec3::length(edge_vec);
if edge_len < 1e-14 {
return pseudo_src;
}
let ex = vec3::scale(edge_vec, 1.0 / edge_len);
let ey1 = vec3::normalize(vec3::cross(source_face_normal, ex));
let ey2 = vec3::normalize(vec3::cross(target_face_normal, ex));
let v = vec3::sub(pseudo_src, edge_start);
let sx = vec3::dot(v, ex);
let sy = vec3::dot(v, ey1);
vec3::add(
vec3::add(edge_start, vec3::scale(ex, sx)),
vec3::scale(ey2, sy),
)
}
fn project_to_face_2d(edge_start: Vec3, edge_end: Vec3, opp: Vec3, point: Vec3) -> Vec2 {
let edge_vec = vec3::sub(edge_end, edge_start);
let edge_len = vec3::length(edge_vec);
if edge_len < 1e-14 {
return [0.0, 0.0];
}
let x_axis = vec3::scale(edge_vec, 1.0 / edge_len);
let face_n = vec3::normalize(vec3::cross(edge_vec, vec3::sub(opp, edge_start)));
let y_axis = vec3::cross(face_n, x_axis);
let v = vec3::sub(point, edge_start);
[vec3::dot(v, x_axis), vec3::dot(v, y_axis)]
}
fn ray_seg_intersect_2d(o: Vec2, d: Vec2, a: Vec2, b: Vec2) -> Option<f64> {
let seg = sub2(b, a);
let denom = cross2(d, seg);
if denom.abs() < 1e-14 {
return None; }
let diff = sub2(a, o);
let t = cross2(diff, seg) / denom;
let s = cross2(diff, d) / denom;
if t > 1e-10 && (-1e-10..=1.0 + 1e-10).contains(&s) {
Some(s.clamp(0.0, 1.0))
} else {
None
}
}
fn point_in_wedge_2d(s: Vec2, a: Vec2, b: Vec2, p: Vec2) -> bool {
let sa = sub2(a, s);
let sb = sub2(b, s);
let sp = sub2(p, s);
let cross_a = cross2(sa, sp);
let cross_b = cross2(sb, sp);
let wedge_sign = cross2(sa, sb);
if wedge_sign.abs() < 1e-14 {
return cross_a.abs() < 1e-10 && dot2(&sp, &sa) > 0.0;
}
if wedge_sign > 0.0 {
cross_a >= -1e-10 && cross_b <= 1e-10
} else {
cross_a <= 1e-10 && cross_b >= -1e-10
}
}
fn find_halfedge(mesh: &MeshStorage, from: VertexId, to: VertexId) -> Option<HalfEdgeId> {
for he in VertexRing::new(mesh, from) {
if let Some(h) = mesh.get_halfedge(he)
&& h.vertex == to
{
return Some(he);
}
}
None
}
fn halfedge_origin(mesh: &MeshStorage, he: HalfEdgeId) -> Option<VertexId> {
let h = mesh.get_halfedge(he)?;
let twin = h.twin?;
let twin_h = mesh.get_halfedge(twin)?;
Some(twin_h.vertex)
}
fn halfedge_tip(mesh: &MeshStorage, he: HalfEdgeId) -> Option<VertexId> {
Some(mesh.get_halfedge(he)?.vertex)
}
fn propagate_window(
mesh: &MeshStorage,
win: &MmpWindow,
v_idx: &HashMap<VertexId, usize>,
dist: &mut [f64],
) -> Vec<MmpWindow> {
let mut new_windows = Vec::new();
let Some(origin_v) = halfedge_origin(mesh, win.he) else {
return new_windows;
};
let Some(tip_v) = halfedge_tip(mesh, win.he) else {
return new_windows;
};
let origin_pos = match mesh.get_vertex(origin_v) {
Some(v) => v.position,
None => return new_windows,
};
let tip_pos = match mesh.get_vertex(tip_v) {
Some(v) => v.position,
None => return new_windows,
};
let h = match mesh.get_halfedge(win.he) {
Some(h) => h,
None => return new_windows,
};
let target_face = if h.face == Some(win.from_face) {
let twin = match h.twin {
Some(t) => t,
None => return new_windows, };
match mesh.get_halfedge(twin) {
Some(th) => match th.face {
Some(f) => f,
None => return new_windows, },
None => return new_windows,
}
} else {
match h.face {
Some(f) if f != win.from_face => f,
_ => return new_windows,
}
};
let face_hes: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, target_face).collect();
if face_hes.len() != 3 {
return new_windows;
}
let face_verts: Vec<VertexId> = face_hes
.iter()
.filter_map(|&he| mesh.get_halfedge(he).map(|h| h.vertex))
.collect();
if face_verts.len() != 3 {
return new_windows;
}
let opp_v = match face_verts.iter().find(|&&v| v != origin_v && v != tip_v) {
Some(&v) => v,
None => return new_windows,
};
let opp_pos = match mesh.get_vertex(opp_v) {
Some(v) => v.position,
None => return new_windows,
};
let from_face_normal = compute_face_normal(mesh, win.from_face);
let target_face_normal = compute_face_normal(mesh, target_face);
let pseudo_r = unfold_pseudo_source(
win.pseudo_src,
origin_pos,
tip_pos,
from_face_normal,
target_face_normal,
);
let d_opp = vec3::length(vec3::sub(opp_pos, pseudo_r));
if let Some(&oi) = v_idx.get(&opp_v)
&& d_opp < dist[oi]
{
dist[oi] = d_opp;
}
let d_origin = vec3::length(vec3::sub(origin_pos, win.pseudo_src));
let d_tip = vec3::length(vec3::sub(tip_pos, win.pseudo_src));
if let Some(&oi) = v_idx.get(&origin_v)
&& d_origin < dist[oi]
{
dist[oi] = d_origin;
}
if let Some(&ti) = v_idx.get(&tip_v)
&& d_tip < dist[ti]
{
dist[ti] = d_tip;
}
let s_2d = project_to_face_2d(origin_pos, tip_pos, opp_pos, pseudo_r);
let origin_2d = [0.0_f64, 0.0]; let tip_2d = [vec3::length(vec3::sub(tip_pos, origin_pos)), 0.0]; let opp_2d = project_to_face_2d(origin_pos, tip_pos, opp_pos, opp_pos);
let edge_len = vec3::length(vec3::sub(tip_pos, origin_pos));
if edge_len < 1e-14 {
return new_windows;
}
let w0_2d = [win.b0 * edge_len, 0.0_f64];
let w1_2d = [win.b1 * edge_len, 0.0_f64];
let sub_edges: [(VertexId, VertexId); 2] = [(origin_v, opp_v), (tip_v, opp_v)];
let sub_2d: [(Vec2, Vec2); 2] = [(origin_2d, opp_2d), (tip_2d, opp_2d)];
for idx in 0..2 {
let (v_a, v_b) = sub_edges[idx];
let (a_2d, b_2d) = sub_2d[idx];
let sub_len = length2(sub2(b_2d, a_2d));
if sub_len < 1e-14 {
continue;
}
let mut s_min = 1.0_f64;
let mut s_max = 0.0_f64;
if point_in_wedge_2d(s_2d, w0_2d, w1_2d, a_2d) {
s_min = s_min.min(0.0);
s_max = s_max.max(0.0);
}
if point_in_wedge_2d(s_2d, w0_2d, w1_2d, b_2d) {
s_min = s_min.min(1.0);
s_max = s_max.max(1.0);
}
if let Some(s_param) = ray_seg_intersect_2d(s_2d, sub2(w0_2d, s_2d), a_2d, b_2d) {
s_min = s_min.min(s_param);
s_max = s_max.max(s_param);
}
if let Some(s_param) = ray_seg_intersect_2d(s_2d, sub2(w1_2d, s_2d), a_2d, b_2d) {
s_min = s_min.min(s_param);
s_max = s_max.max(s_param);
}
if s_min >= s_max - 1e-10 {
continue; }
let s_lo = s_min.max(0.0);
let s_hi = s_max.min(1.0);
if s_lo >= s_hi - 1e-10 {
continue;
}
let va_pos = mesh.get_vertex(v_a).map(|v| v.position).unwrap_or([0.0; 3]);
let vb_pos = mesh.get_vertex(v_b).map(|v| v.position).unwrap_or([0.0; 3]);
let sub_he = match find_halfedge(mesh, v_a, v_b) {
Some(he) => he,
None => continue,
};
let sub_origin = halfedge_origin(mesh, sub_he);
let sub_tip = halfedge_tip(mesh, sub_he);
let (new_b0, new_b1) = match (sub_origin, sub_tip) {
(Some(so), Some(st)) if so == v_a && st == v_b => (s_lo, s_hi),
(Some(so), Some(st)) if so == v_b && st == v_a => (1.0 - s_hi, 1.0 - s_lo),
_ => continue,
};
let p0_3d = vec3::add(va_pos, vec3::scale(vec3::sub(vb_pos, va_pos), s_lo));
let p1_3d = vec3::add(va_pos, vec3::scale(vec3::sub(vb_pos, va_pos), s_hi));
let new_d0 = vec3::length(vec3::sub(p0_3d, pseudo_r));
let new_d1 = vec3::length(vec3::sub(p1_3d, pseudo_r));
if s_lo < 1e-8
&& let Some(&ai) = v_idx.get(&v_a)
&& new_d0 < dist[ai]
{
dist[ai] = new_d0;
}
if s_lo > 1.0 - 1e-8
&& let Some(&bi) = v_idx.get(&v_b)
&& new_d0 < dist[bi]
{
dist[bi] = new_d0;
}
if s_hi < 1e-8
&& let Some(&ai) = v_idx.get(&v_a)
&& new_d1 < dist[ai]
{
dist[ai] = new_d1;
}
if s_hi > 1.0 - 1e-8
&& let Some(&bi) = v_idx.get(&v_b)
&& new_d1 < dist[bi]
{
dist[bi] = new_d1;
}
let _ = idx;
new_windows.push(MmpWindow {
he: sub_he,
b0: new_b0,
b1: new_b1,
d0: new_d0,
d1: new_d1,
pseudo_src: pseudo_r,
from_face: target_face,
});
}
new_windows
}
pub fn mmp_geodesic(mesh: &MeshStorage, source: VertexId) -> Vec<f64> {
mmp_geodesic_impl(mesh, &[source])
}
pub fn mmp_multi_source_geodesic(mesh: &MeshStorage, sources: &[VertexId]) -> Vec<f64> {
mmp_geodesic_impl(mesh, sources)
}
fn mmp_geodesic_impl(mesh: &MeshStorage, sources: &[VertexId]) -> Vec<f64> {
let n = mesh.vertex_count();
if n == 0 {
return Vec::new();
}
if sources.is_empty() {
return vec![f64::INFINITY; n];
}
let v_idx = build_vertex_index(mesh);
let mut dist = vec![f64::INFINITY; n];
let mut heap = BinaryHeap::new();
for &source in sources {
let Some(&source_idx) = v_idx.get(&source) else {
continue;
};
let source_pos = match mesh.get_vertex(source) {
Some(v) => v.position,
None => continue,
};
dist[source_idx] = dist[source_idx].min(0.0);
for face in VertexAdjacentFaces::new(mesh, source) {
let face_hes: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, face).collect();
if face_hes.len() != 3 {
continue;
}
let mut opp_he = None;
for &he in &face_hes {
let origin = halfedge_origin(mesh, he);
let tip = halfedge_tip(mesh, he);
if let (Some(o), Some(t)) = (origin, tip)
&& o != source
&& t != source
{
opp_he = Some(he);
break;
}
}
let Some(opp_he) = opp_he else {
continue;
};
let Some(opp_origin) = halfedge_origin(mesh, opp_he) else {
continue;
};
let Some(opp_tip) = halfedge_tip(mesh, opp_he) else {
continue;
};
let opp_origin_pos = mesh
.get_vertex(opp_origin)
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let opp_tip_pos = mesh
.get_vertex(opp_tip)
.map(|v| v.position)
.unwrap_or([0.0; 3]);
let d0 = vec3::length(vec3::sub(opp_origin_pos, source_pos));
let d1 = vec3::length(vec3::sub(opp_tip_pos, source_pos));
if let Some(&oi) = v_idx.get(&opp_origin) {
dist[oi] = dist[oi].min(d0);
}
if let Some(&ti) = v_idx.get(&opp_tip) {
dist[ti] = dist[ti].min(d1);
}
let win = MmpWindow {
he: opp_he,
b0: 0.0,
b1: 1.0,
d0,
d1,
pseudo_src: source_pos,
from_face: face,
};
let key = d0.min(d1);
heap.push(MmpEntry { key, window: win });
}
}
let max_iter = mesh.face_count() * 100; let mut iter_count = 0;
while let Some(entry) = heap.pop() {
iter_count += 1;
if iter_count > max_iter {
break;
}
let win = &entry.window;
let Some(origin_v) = halfedge_origin(mesh, win.he) else {
continue;
};
let Some(tip_v) = halfedge_tip(mesh, win.he) else {
continue;
};
let Some(&oi) = v_idx.get(&origin_v) else {
continue;
};
let Some(&ti) = v_idx.get(&tip_v) else {
continue;
};
let min_dist = win.d0.min(win.d1);
let tol = 1e-8;
if min_dist > dist[oi] + tol && min_dist > dist[ti] + tol {
continue;
}
let new_wins = propagate_window(mesh, win, &v_idx, &mut dist);
for nw in new_wins {
let key = nw.d0.min(nw.d1);
heap.push(MmpEntry { key, window: nw });
}
}
dist
}
#[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
);
}
#[test]
fn mmp_self_distance_zero() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let dist = mmp_geodesic(&mesh, vertices[0]);
assert_eq!(dist.len(), mesh.vertex_count());
assert!(
dist[0].abs() < 1e-10,
"MMP source distance must be 0, got {}",
dist[0]
);
}
#[test]
fn mmp_le_dijkstra() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_mmp = mmp_geodesic(&mesh, vertices[0]);
let d_dijk = dijkstra_geodesic(&mesh, vertices[0]);
for i in 0..d_mmp.len() {
assert!(
d_mmp[i] <= d_dijk[i] + 1e-8,
"MMP[{}] = {} should be <= Dijkstra[{}] = {}",
i,
d_mmp[i],
i,
d_dijk[i]
);
}
}
#[test]
fn mmp_symmetric() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let v0 = vertices[0];
let v1 = vertices[vertices.len() / 2];
let d_forward = mmp_geodesic(&mesh, v0);
let d_backward = mmp_geodesic(&mesh, v1);
let v_idx = build_vertex_index(&mesh);
let i1 = *v_idx.get(&v1).unwrap();
let i0 = *v_idx.get(&v0).unwrap();
let tol = d_forward[i1].max(d_backward[i0]) * 0.05; assert!(
(d_forward[i1] - d_backward[i0]).abs() < tol.max(1e-8),
"MMP d(s→t) = {} should ≈ d(t→s) = {}",
d_forward[i1],
d_backward[i0]
);
}
#[test]
fn mmp_multi_source() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_single = mmp_geodesic(&mesh, vertices[0]);
let sources = vec![vertices[0], vertices[vertices.len() - 1]];
let d_multi = mmp_multi_source_geodesic(&mesh, &sources);
for i in 0..d_single.len() {
assert!(
d_multi[i] <= d_single[i] + 1e-10,
"MMP multi[{}] = {} should be <= single[{}] = {}",
i,
d_multi[i],
i,
d_single[i]
);
}
}
#[test]
fn mmp_multi_source_unified_matches_naive() {
let mesh = build_icosphere(2);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let sources = vec![
vertices[0],
vertices[vertices.len() / 2],
vertices[vertices.len() - 1],
];
let d_unified = mmp_multi_source_geodesic(&mesh, &sources);
let n = mesh.vertex_count();
let mut d_naive = vec![f64::INFINITY; n];
for &s in &sources {
let d = mmp_geodesic(&mesh, s);
for i in 0..n {
d_naive[i] = d_naive[i].min(d[i]);
}
}
for i in 0..n {
let diff = (d_unified[i] - d_naive[i]).abs();
let tol = 1e-9 * d_unified[i].abs().max(1.0);
assert!(
diff <= tol,
"顶点 {}: 统一传播={}, 逐源取最小={}, 差异={}",
i,
d_unified[i],
d_naive[i],
diff
);
}
}
#[test]
fn mmp_flat_grid() {
use crate::storage::{Face, HalfEdge, Vertex};
let mut mesh = MeshStorage::new();
let mut grid_v = Vec::new();
for iy in 0..3 {
for ix in 0..3 {
let x = ix as f64;
let y = iy as f64;
let v = mesh.add_vertex(Vertex::new([x, y, 0.0]));
grid_v.push(v);
}
}
let vid = |ix: usize, iy: usize| -> VertexId { grid_v[iy * 3 + ix] };
let mut all_he = Vec::new();
for iy in 0..2 {
for ix in 0..2 {
let v00 = vid(ix, iy);
let v10 = vid(ix + 1, iy);
let v01 = vid(ix, iy + 1);
let v11 = vid(ix + 1, iy + 1);
let h0 = mesh.add_halfedge(HalfEdge::new(v10));
let h1 = mesh.add_halfedge(HalfEdge::new(v11));
let h2 = mesh.add_halfedge(HalfEdge::new(v00));
let h3 = mesh.add_halfedge(HalfEdge::new(v11));
let h4 = mesh.add_halfedge(HalfEdge::new(v01));
let h5 = mesh.add_halfedge(HalfEdge::new(v00));
let f1 = mesh.add_face(Face::new());
let f2 = mesh.add_face(Face::new());
all_he.push((h0, h1, h2, f1, v00, v10, v11));
all_he.push((h3, h4, h5, f2, v00, v11, v01));
}
}
for (h0, h1, h2, f, _va, _vb, _vc) in &all_he {
for (he, next, prev) in [(*h0, *h1, *h2), (*h1, *h2, *h0), (*h2, *h0, *h1)] {
let h = mesh.get_halfedge_mut(he).unwrap();
h.next = Some(next);
h.prev = Some(prev);
h.face = Some(*f);
}
}
let he_list: Vec<(HalfEdgeId, VertexId, VertexId)> = all_he
.iter()
.flat_map(|(h0, h1, h2, _, va, vb, vc)| {
vec![(*h0, *va, *vb), (*h1, *vb, *vc), (*h2, *vc, *va)]
})
.collect();
for i in 0..he_list.len() {
let (he_i, src_i, dst_i) = he_list[i];
if mesh.get_halfedge(he_i).unwrap().twin.is_some() {
continue;
}
for (j, &(he_j, src_j, dst_j)) in he_list.iter().enumerate() {
if i == j {
continue;
}
if src_j == dst_i && dst_j == src_i {
mesh.get_halfedge_mut(he_i).unwrap().twin = Some(he_j);
mesh.get_halfedge_mut(he_j).unwrap().twin = Some(he_i);
break;
}
}
}
for (he, _src, _dst) in &he_list {
let h = mesh.get_halfedge(*he).unwrap();
if let Some(twin) = h.twin {
let origin = mesh.get_halfedge(twin).unwrap().vertex;
if mesh.get_vertex(origin).unwrap().halfedge.is_none() {
mesh.get_vertex_mut(origin).unwrap().halfedge = Some(*he);
}
}
}
for (h0, _h1, _h2, f, _, _, _) in &all_he {
if mesh.get_face(*f).unwrap().halfedge.is_none() {
mesh.get_face_mut(*f).unwrap().halfedge = Some(*h0);
}
}
let source = vid(0, 0);
let dist = mmp_geodesic(&mesh, source);
let v_idx = build_vertex_index(&mesh);
let v20 = vid(2, 0);
if let Some(&i20) = v_idx.get(&v20) {
let eucl = 2.0_f64;
assert!(
(dist[i20] - eucl).abs() < 0.1,
"MMP dist to (2,0) = {}, euclidean = {}",
dist[i20],
eucl
);
}
let v02 = vid(0, 2);
if let Some(&i02) = v_idx.get(&v02) {
let eucl = 2.0_f64;
assert!(
(dist[i02] - eucl).abs() < 0.1,
"MMP dist to (0,2) = {}, euclidean = {}",
dist[i02],
eucl
);
}
let v22 = vid(2, 2);
if let Some(&i22) = v_idx.get(&v22) {
let eucl = 2.0_f64 * 2.0_f64.sqrt();
assert!(
(dist[i22] - eucl).abs() < 0.2,
"MMP dist to (2,2) = {}, euclidean = {}",
dist[i22],
eucl
);
}
}
#[test]
fn dijkstra_empty_mesh_returns_empty() {
let mesh = MeshStorage::new();
let dist = dijkstra_geodesic(&mesh, VertexId::default());
assert!(dist.is_empty(), "空网格应返回空 Vec");
}
#[test]
fn mmp_empty_mesh_returns_empty() {
let mesh = MeshStorage::new();
let dist = mmp_geodesic(&mesh, VertexId::default());
assert!(dist.is_empty(), "空网格应返回空 Vec");
}
#[test]
fn dijkstra_multi_source_empty_sources_returns_infinity() {
let mesh = build_icosphere(0);
let dist = dijkstra_multi_source_geodesic(&mesh, &[]);
assert_eq!(dist.len(), mesh.vertex_count());
for d in &dist {
assert!(d.is_infinite(), "空源集应返回全 INFINITY");
}
}
#[test]
fn dijkstra_invalid_source_returns_infinity() {
let mesh = build_icosphere(0);
let dist = dijkstra_geodesic(&mesh, VertexId::default());
assert_eq!(dist.len(), mesh.vertex_count());
for d in &dist {
assert!(d.is_infinite(), "无效 source 应使所有距离为 INFINITY");
}
}
#[test]
fn mmp_invalid_source_returns_infinity() {
let mesh = build_icosphere(0);
let dist = mmp_geodesic(&mesh, VertexId::default());
assert_eq!(dist.len(), mesh.vertex_count());
for d in &dist {
assert!(d.is_infinite(), "无效 source 应使所有距离为 INFINITY");
}
}
#[test]
fn geodesic_distance_from_vertex_invalid_source_returns_none() {
let mesh = build_icosphere(0);
let result = geodesic_distance_from_vertex(&mesh, VertexId::default());
assert!(result.is_none(), "无效 source 应返回 None");
}
#[test]
fn multi_source_geodesic_empty_sources_returns_none() {
let mesh = build_icosphere(0);
let result = multi_source_geodesic(&mesh, &[]);
assert!(result.is_none(), "空源集应返回 None");
}
#[test]
fn dijkstra_with_parent_invalid_source_returns_empty() {
let mesh = build_icosphere(0);
let (dist, parent) = dijkstra_with_parent(&mesh, VertexId::default());
assert!(dist.is_empty(), "无效 source 应返回空距离 Vec");
assert!(parent.is_empty(), "无效 source 应返回空 parent Vec");
}
#[test]
fn dijkstra_shortest_path_invalid_target_returns_empty() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let path = dijkstra_shortest_path(&mesh, vertices[0], VertexId::default());
assert!(path.is_empty(), "无效 target 应返回空路径");
}
#[test]
fn dijkstra_distance_nonnegative() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let dist = dijkstra_geodesic(&mesh, vertices[0]);
for (i, d) in dist.iter().enumerate() {
assert!(*d >= 0.0, "顶点 {} 距离 {} 应非负", i, d);
}
}
#[test]
fn mmp_multi_source_single_source_matches_naive() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let d_multi = mmp_multi_source_geodesic(&mesh, &[vertices[0]]);
let d_single = mmp_geodesic(&mesh, vertices[0]);
assert_eq!(d_multi.len(), d_single.len());
for i in 0..d_multi.len() {
assert!(
(d_multi[i] - d_single[i]).abs() < 1e-12,
"单源多源应一致: multi[{}]={}, single[{}]={}",
i,
d_multi[i],
i,
d_single[i]
);
}
}
#[test]
fn mmp_distance_nonnegative() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let dist = mmp_geodesic(&mesh, vertices[0]);
for (i, d) in dist.iter().enumerate() {
assert!(*d >= 0.0, "MMP 顶点 {} 距离 {} 应非负", i, d);
}
}
#[test]
fn shortest_path_to_self_returns_self() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let source = vertices[0];
let dist = dijkstra_geodesic(&mesh, source);
let path = shortest_path(&mesh, &dist, source);
assert_eq!(path, vec![source], "到自身的最短路径应为 [source]");
}
#[test]
fn shortest_path_invalid_distance_returns_empty() {
let mesh = build_icosphere(0);
let vertices: Vec<VertexId> = mesh.vertex_ids().collect();
let path = shortest_path(&mesh, &[], vertices[0]);
assert!(path.is_empty(), "空距离场应返回空路径");
}
}