use std::collections::{HashMap, HashSet};
use crate::geometry::cotan_edge_weight;
use crate::ids::VertexId;
use crate::linalg::{SparseSystem, conjugate_gradient, regularize_diagonal};
use crate::storage::MeshStorage;
use crate::traversal::{VertexRing, boundary_loops, is_boundary_vertex};
fn build_vertex_index(mesh: &MeshStorage) -> HashMap<VertexId, usize> {
mesh.vertex_ids().enumerate().map(|(i, v)| (v, i)).collect()
}
fn collect_boundary_vertices(mesh: &MeshStorage) -> Vec<VertexId> {
mesh.vertex_ids()
.filter(|&v| is_boundary_vertex(mesh, v))
.collect()
}
fn order_boundary_vertices(mesh: &MeshStorage) -> Vec<VertexId> {
let loops = boundary_loops(mesh);
if loops.is_empty() {
return Vec::new();
}
let longest = loops
.into_iter()
.max_by_key(|l| l.len())
.unwrap_or_default();
longest
.into_iter()
.filter_map(|he| mesh.get_halfedge(he).map(|h| h.vertex))
.collect()
}
fn build_full_cotan_laplacian(mesh: &MeshStorage) -> (SparseSystem, HashMap<VertexId, usize>) {
let v_idx = build_vertex_index(mesh);
let n = v_idx.len();
let mut sys = SparseSystem::new(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;
sys.add(i, j, -w);
diag += w;
}
}
sys.add_diag(i, diag);
}
(sys, v_idx)
}
fn build_full_uniform_laplacian(mesh: &MeshStorage) -> (SparseSystem, HashMap<VertexId, usize>) {
let v_idx = build_vertex_index(mesh);
let n = v_idx.len();
let mut sys = SparseSystem::new(n);
for (v, &i) in &v_idx {
let mut degree = 0;
for he in VertexRing::new(mesh, *v) {
let neighbor = mesh.get_halfedge(he).unwrap().vertex;
if let Some(&j) = v_idx.get(&neighbor) {
sys.add(i, j, -0.5);
degree += 1;
}
}
sys.add_diag(i, (degree as f64) / 2.0);
}
(sys, v_idx)
}
fn apply_dirichlet(
laplacian: SparseSystem,
n: usize,
fixed_uv: &HashMap<usize, [f64; 2]>,
) -> Option<(sprs::CsMat<f64>, Vec<f64>, Vec<f64>)> {
let lap = laplacian.finish();
let fixed_set: HashSet<usize> = fixed_uv.keys().copied().collect();
let mut rhs_u = vec![0.0; n];
let mut rhs_v = vec![0.0; n];
for (&idx, &uv) in fixed_uv {
rhs_u[idx] = uv[0];
rhs_v[idx] = uv[1];
}
for row in 0..n {
if fixed_set.contains(&row) {
continue;
}
if let Some(row_view) = lap.outer_view(row) {
for (col, &val) in row_view.iter() {
if fixed_set.contains(&col) {
let uv = fixed_uv[&col];
rhs_u[row] -= val * uv[0];
rhs_v[row] -= val * uv[1];
}
}
}
}
let mut new_sys = SparseSystem::new(n);
for row in 0..n {
if fixed_set.contains(&row) {
new_sys.add_diag(row, 1.0);
} else {
if let Some(row_view) = lap.outer_view(row) {
for (col, &val) in row_view.iter() {
if !fixed_set.contains(&col) {
new_sys.add(row, col, val);
}
}
if let Some(diag_val) = lap.get(row, row) {
new_sys.add_diag(row, *diag_val);
}
}
}
}
let mut a = new_sys.finish();
regularize_diagonal(&mut a, 1e-8);
Some((a, rhs_u, rhs_v))
}
fn solve_param_system(
a: &sprs::CsMat<f64>,
rhs_u: &[f64],
rhs_v: &[f64],
n: usize,
) -> Option<Vec<[f64; 2]>> {
let x_u = conjugate_gradient(a, rhs_u, n * 200, 1e-6)?;
let x_v = conjugate_gradient(a, rhs_v, n * 200, 1e-6)?;
Some(x_u.into_iter().zip(x_v).map(|(u, v)| [u, v]).collect())
}
pub fn tutte_embedding(mesh: &MeshStorage) -> Option<Vec<[f64; 2]>> {
let n = mesh.vertex_count();
if n == 0 || mesh.face_count() == 0 {
return None;
}
let (laplacian, v_idx) = build_full_uniform_laplacian(mesh);
let boundary_v = collect_boundary_vertices(mesh);
if boundary_v.is_empty() {
return None;
}
let ordered_boundary = order_boundary_vertices(mesh);
let bdy_len = ordered_boundary.len();
let mut fixed_uv = HashMap::new();
for (k, &v) in ordered_boundary.iter().enumerate() {
let angle = 2.0 * std::f64::consts::PI * (k as f64) / (bdy_len as f64);
if let Some(&idx) = v_idx.get(&v) {
fixed_uv.insert(idx, [angle.cos(), angle.sin()]);
}
}
let (a, rhs_u, rhs_v) = apply_dirichlet(laplacian, n, &fixed_uv)?;
solve_param_system(&a, &rhs_u, &rhs_v, n)
}
pub fn harmonic_parameterization(mesh: &MeshStorage) -> Option<Vec<[f64; 2]>> {
let n = mesh.vertex_count();
if n == 0 || mesh.face_count() == 0 {
return None;
}
let (laplacian, v_idx) = build_full_cotan_laplacian(mesh);
let boundary_v = collect_boundary_vertices(mesh);
if boundary_v.is_empty() {
return None;
}
let ordered_boundary = order_boundary_vertices(mesh);
let bdy_len = ordered_boundary.len();
let mut fixed_uv = HashMap::new();
for (k, &v) in ordered_boundary.iter().enumerate() {
let angle = 2.0 * std::f64::consts::PI * (k as f64) / (bdy_len as f64);
if let Some(&idx) = v_idx.get(&v) {
fixed_uv.insert(idx, [angle.cos(), angle.sin()]);
}
}
let (a, rhs_u, rhs_v) = apply_dirichlet(laplacian, n, &fixed_uv)?;
solve_param_system(&a, &rhs_u, &rhs_v, n)
}
pub fn lscm(mesh: &MeshStorage) -> Option<Vec<[f64; 2]>> {
let n = mesh.vertex_count();
if n < 2 || mesh.face_count() == 0 {
return None;
}
let (laplacian, _v_idx) = build_full_cotan_laplacian(mesh);
let mut fixed_uv = HashMap::new();
fixed_uv.insert(0, [0.0, 0.0]);
if n > 1 {
fixed_uv.insert(1, [1.0, 0.0]);
}
let (a, rhs_u, rhs_v) = apply_dirichlet(laplacian, n, &fixed_uv)?;
solve_param_system(&a, &rhs_u, &rhs_v, n)
}
pub fn mvc_parameterization(mesh: &MeshStorage) -> Option<Vec<[f64; 2]>> {
let n = mesh.vertex_count();
if n == 0 || mesh.face_count() == 0 {
return None;
}
let v_idx = build_vertex_index(mesh);
let boundary_v = collect_boundary_vertices(mesh);
if boundary_v.is_empty() {
return None;
}
let ordered_boundary = order_boundary_vertices(mesh);
let bdy_len = ordered_boundary.len();
let mut fixed_uv: HashMap<usize, [f64; 2]> = HashMap::new();
for (k, &v) in ordered_boundary.iter().enumerate() {
let angle = 2.0 * std::f64::consts::PI * (k as f64) / (bdy_len as f64);
if let Some(&idx) = v_idx.get(&v) {
fixed_uv.insert(idx, [angle.cos(), angle.sin()]);
}
}
let boundary_set: HashSet<usize> = fixed_uv.keys().copied().collect();
let mut sys = SparseSystem::new(n);
let mut rhs_u = vec![0.0; n];
let mut rhs_v = vec![0.0; n];
for (v, &i) in &v_idx {
if boundary_set.contains(&i) {
sys.add_diag(i, 1.0);
let uv = fixed_uv[&i];
rhs_u[i] = uv[0];
rhs_v[i] = uv[1];
continue;
}
let neighbors: Vec<(usize, [f64; 3])> = VertexRing::new(mesh, *v)
.filter_map(|he| {
let h = mesh.get_halfedge(he)?;
let n_vid = h.vertex;
let n_pos = mesh.get_vertex(n_vid)?.position;
let n_idx = *v_idx.get(&n_vid)?;
Some((n_idx, n_pos))
})
.collect();
let k = neighbors.len();
if k == 0 {
sys.add_diag(i, 1.0);
continue;
}
let p_v = mesh.get_vertex(*v)?.position;
let d: Vec<f64> = neighbors
.iter()
.map(|(_, pos)| {
let diff = [pos[0] - p_v[0], pos[1] - p_v[1], pos[2] - p_v[2]];
(diff[0] * diff[0] + diff[1] * diff[1] + diff[2] * diff[2]).sqrt()
})
.collect();
if d.iter().any(|x| *x < 1e-14) {
sys.add_diag(i, 1.0);
continue;
}
let u: Vec<[f64; 3]> = neighbors
.iter()
.zip(d.iter())
.map(|((_, pos), &di)| {
[
(pos[0] - p_v[0]) / di,
(pos[1] - p_v[1]) / di,
(pos[2] - p_v[2]) / di,
]
})
.collect();
let mut alpha = Vec::with_capacity(k);
for i in 0..k {
let j = (i + 1) % k;
let cos_a =
(u[i][0] * u[j][0] + u[i][1] * u[j][1] + u[i][2] * u[j][2]).clamp(-1.0, 1.0);
alpha.push(cos_a.acos());
}
let mut w = Vec::with_capacity(k);
for i in 0..k {
let prev = if i == 0 { k - 1 } else { i - 1 };
let tan_prev = (alpha[prev] / 2.0).tan();
let tan_cur = (alpha[i] / 2.0).tan();
w.push((tan_prev + tan_cur) / d[i]);
}
let total: f64 = w.iter().sum();
if total < 1e-14 {
for (j, _) in neighbors.iter() {
if boundary_set.contains(j) {
let uv = fixed_uv[j];
rhs_u[i] += uv[0] / (k as f64);
rhs_v[i] += uv[1] / (k as f64);
} else {
sys.add(i, *j, -1.0 / (k as f64));
}
}
sys.add_diag(i, 1.0);
continue;
}
for (j, _) in neighbors.iter().enumerate() {
let lambda = w[j] / total;
let n_idx = neighbors[j].0;
if boundary_set.contains(&n_idx) {
let uv = fixed_uv[&n_idx];
rhs_u[i] += lambda * uv[0];
rhs_v[i] += lambda * uv[1];
} else {
sys.add(i, n_idx, -lambda);
}
}
sys.add_diag(i, 1.0);
}
let mut a = sys.finish();
regularize_diagonal(&mut a, 1e-10);
solve_param_system(&a, &rhs_u, &rhs_v, n)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lscm_simple_quad() {
let mut mesh = MeshStorage::new();
let v0 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 0.0, 0.0]));
let v1 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 0.0, 0.2]));
let v2 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 1.0, 0.0]));
let v3 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 1.0, 0.3]));
crate::topology_ops::add_triangle(&mut mesh, v0, v1, v2).unwrap();
crate::topology_ops::add_triangle(&mut mesh, v0, v2, v3).unwrap();
let result = lscm(&mesh);
assert!(result.is_some(), "LSCM should succeed on a simple quad");
let uv = result.unwrap();
assert_eq!(uv.len(), 4);
assert!(
(uv[0][0] - 0.0).abs() < 0.1,
"v0 pinned to (0,0), got ({},{})",
uv[0][0],
uv[0][1]
);
assert!((uv[0][1] - 0.0).abs() < 0.1);
assert!(
(uv[1][0] - 1.0).abs() < 0.1,
"v1 pinned to (1,0), got ({},{})",
uv[1][0],
uv[1][1]
);
assert!((uv[1][1] - 0.0).abs() < 0.1);
}
#[test]
fn test_mvc_simple_quad() {
let mut mesh = MeshStorage::new();
let v0 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 0.0, 0.0]));
let v1 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 0.0, 0.2]));
let v2 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 1.0, 0.0]));
let v3 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 1.0, 0.3]));
crate::topology_ops::add_triangle(&mut mesh, v0, v1, v2).unwrap();
crate::topology_ops::add_triangle(&mut mesh, v0, v2, v3).unwrap();
let result = mvc_parameterization(&mesh);
assert!(result.is_some(), "MVC should succeed on a simple quad");
let uv = result.unwrap();
assert_eq!(uv.len(), 4);
for &p in &uv {
let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
assert!(
(r - 1.0).abs() < 1e-3,
"boundary point should be on unit circle, got r={}",
r
);
}
}
#[test]
fn test_mvc_no_flip_on_concave_mesh() {
let mut mesh = MeshStorage::new();
let v0 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 0.0, 0.0]));
let v1 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 1.0, 0.0]));
let v2 = mesh.add_vertex(crate::storage::Vertex::new([1.0, 2.0, 0.0]));
let v3 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 2.0, 0.0]));
let v4 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 1.0, 0.0]));
let v5 = mesh.add_vertex(crate::storage::Vertex::new([0.0, 0.0, 0.0]));
crate::topology_ops::add_triangle(&mut mesh, v0, v1, v4).unwrap();
crate::topology_ops::add_triangle(&mut mesh, v0, v4, v5).unwrap();
crate::topology_ops::add_triangle(&mut mesh, v1, v2, v3).unwrap();
crate::topology_ops::add_triangle(&mut mesh, v1, v3, v4).unwrap();
let result = mvc_parameterization(&mesh);
assert!(result.is_some(), "MVC should succeed on concave mesh");
let uv = result.unwrap();
for &p in &uv {
let r = (p[0] * p[0] + p[1] * p[1]).sqrt();
assert!(
(r - 1.0).abs() < 1e-3,
"boundary point should be on unit circle, got r={}",
r
);
}
}
#[test]
fn test_mvc_returns_none_on_empty() {
let mesh = MeshStorage::new();
assert!(mvc_parameterization(&mesh).is_none());
}
#[test]
fn test_mvc_returns_none_on_closed_mesh() {
let mesh = crate::test_util::build_icosphere(1);
assert!(mvc_parameterization(&mesh).is_none());
}
}