use std::collections::{HashMap, HashSet};
use rayon::prelude::*;
use crate::ids::VertexId;
use crate::linalg::{
SparseSystem, build_cotan_laplacian, build_vertex_index, conjugate_gradient,
regularize_diagonal,
};
use crate::storage::MeshStorage;
use crate::traversal::{VertexRing, boundary_loops, is_boundary_vertex};
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_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)
.expect("halfedge exists in mesh")
.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 v_idx = build_vertex_index(mesh);
let laplacian = build_cotan_laplacian(mesh, &v_idx);
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 v_idx = build_vertex_index(mesh);
let laplacian = build_cotan_laplacian(mesh, &v_idx);
let ordered_boundary = order_boundary_vertices(mesh);
if ordered_boundary.len() < 2 {
return None;
}
let (pin_a, pin_b) = pick_farthest_pair(mesh, &ordered_boundary)?;
let mut fixed_uv = HashMap::new();
if let Some(&idx_a) = v_idx.get(&pin_a) {
fixed_uv.insert(idx_a, [0.0, 0.0]);
}
if let Some(&idx_b) = v_idx.get(&pin_b) {
fixed_uv.insert(idx_b, [1.0, 0.0]);
}
if fixed_uv.len() < 2 {
return None;
}
let (a, rhs_u, rhs_v) = apply_dirichlet(laplacian, n, &fixed_uv)?;
solve_param_system(&a, &rhs_u, &rhs_v, n)
}
fn pick_farthest_pair(mesh: &MeshStorage, verts: &[VertexId]) -> Option<(VertexId, VertexId)> {
if verts.len() < 2 {
return None;
}
let pos_of = |v: VertexId| -> Option<[f64; 3]> { mesh.get_vertex(v).map(|vd| vd.position) };
let p0 = verts[0];
let p0_pos = pos_of(p0)?;
let mut p1 = p0;
let mut best_dist_sq = -1.0f64;
for &v in verts {
if let Some(pos) = pos_of(v) {
let d = dist_sq(p0_pos, pos);
if d > best_dist_sq {
best_dist_sq = d;
p1 = v;
}
}
}
let p1_pos = pos_of(p1)?;
let mut p2 = p1;
let mut best_dist_sq = -1.0f64;
for &v in verts {
if let Some(pos) = pos_of(v) {
let d = dist_sq(p1_pos, pos);
if d > best_dist_sq {
best_dist_sq = d;
p2 = v;
}
}
}
if p1 == p2 {
return None;
}
Some((p1, p2))
}
#[inline]
fn dist_sq(a: [f64; 3], b: [f64; 3]) -> f64 {
let dx = b[0] - a[0];
let dy = b[1] - a[1];
let dz = b[2] - a[2];
dx * dx + dy * dy + dz * dz
}
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();
type MvcEntry = (usize, Vec<(usize, f64)>, f64, [f64; 2]);
let vert_entries: Vec<MvcEntry> = v_idx
.par_iter()
.filter_map(|(&v, &i)| {
if boundary_set.contains(&i) {
let uv = fixed_uv[&i];
return Some((i, Vec::new(), 1.0, uv));
}
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 {
return Some((i, Vec::new(), 1.0, [0.0, 0.0]));
}
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) {
return Some((i, Vec::new(), 1.0, [0.0, 0.0]));
}
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 idx in 0..k {
let j = (idx + 1) % k;
let cos_a = (u[idx][0] * u[j][0] + u[idx][1] * u[j][1] + u[idx][2] * u[j][2])
.clamp(-1.0, 1.0);
alpha.push(cos_a.acos());
}
let mut w = Vec::with_capacity(k);
for idx in 0..k {
let prev = if idx == 0 { k - 1 } else { idx - 1 };
let tan_prev = (alpha[prev] / 2.0).tan();
let tan_cur = (alpha[idx] / 2.0).tan();
w.push((tan_prev + tan_cur) / d[idx]);
}
let total: f64 = w.iter().sum();
let mut entries = Vec::new();
let mut rhs = [0.0; 2];
if total < 1e-14 {
for (j, _) in neighbors.iter() {
if boundary_set.contains(j) {
let uv = fixed_uv[j];
rhs[0] += uv[0] / (k as f64);
rhs[1] += uv[1] / (k as f64);
} else {
entries.push((*j, -1.0 / (k as f64)));
}
}
} else {
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[0] += lambda * uv[0];
rhs[1] += lambda * uv[1];
} else {
entries.push((n_idx, -lambda));
}
}
}
Some((i, entries, 1.0, rhs))
})
.collect();
let mut sys = SparseSystem::new(n);
let mut rhs_u = vec![0.0; n];
let mut rhs_v = vec![0.0; n];
for (i, entries, diag, rhs) in vert_entries {
for (col, val) in entries {
sys.add(i, col, val);
}
sys.add_diag(i, diag);
rhs_u[i] = rhs[0];
rhs_v[i] = rhs[1];
}
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);
for v in &uv {
assert!(v[0].is_finite(), "u 有限值, got {}", v[0]);
assert!(v[1].is_finite(), "v 有限值, got {}", v[1]);
}
let n_pinned_zero = uv
.iter()
.filter(|p| (p[0].abs() < 1e-6) && (p[1].abs() < 1e-6))
.count();
let n_pinned_one = uv
.iter()
.filter(|p| ((p[0] - 1.0).abs() < 1e-6) && (p[1].abs() < 1e-6))
.count();
assert_eq!(
n_pinned_zero, 1,
"应有 1 个顶点钉在 (0,0), 实际 {}",
n_pinned_zero
);
assert_eq!(
n_pinned_one, 1,
"应有 1 个顶点钉在 (1,0), 实际 {}",
n_pinned_one
);
}
#[test]
fn test_lscm_returns_none_on_closed_mesh() {
let mesh = crate::test_util::build_icosphere(1);
let result = lscm(&mesh);
assert!(
result.is_none(),
"闭合网格无边界,LSCM 应返回 None, 实际得到 Some"
);
}
#[test]
fn test_lscm_pinned_vertices_are_boundary() {
let mut mesh = MeshStorage::new();
let center = mesh.add_vertex(crate::storage::Vertex::new([0.5, 0.5, 0.0]));
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.0]));
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.0]));
crate::topology_ops::add_triangle(&mut mesh, center, v0, v1).unwrap();
crate::topology_ops::add_triangle(&mut mesh, center, v1, v2).unwrap();
crate::topology_ops::add_triangle(&mut mesh, center, v2, v3).unwrap();
crate::topology_ops::add_triangle(&mut mesh, center, v3, v0).unwrap();
assert!(!is_boundary_vertex(&mesh, center));
for &v in &[v0, v1, v2, v3] {
assert!(is_boundary_vertex(&mesh, v));
}
let result = lscm(&mesh);
assert!(result.is_some(), "LSCM 应成功");
let uv = result.unwrap();
assert_eq!(uv.len(), 5);
let v_idx = build_vertex_index(&mesh);
let center_idx = *v_idx.get(¢er).unwrap();
let center_uv = uv[center_idx];
let is_pinned_zero = (center_uv[0].abs() < 1e-6) && (center_uv[1].abs() < 1e-6);
let is_pinned_one = ((center_uv[0] - 1.0).abs() < 1e-6) && (center_uv[1].abs() < 1e-6);
assert!(
!is_pinned_zero && !is_pinned_one,
"内部顶点不应被钉住, center uv = {:?}",
center_uv
);
}
#[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());
}
fn build_flat_grid() -> MeshStorage {
let verts = vec![
[0.0, 0.0, 0.0],
[1.0, 0.0, 0.0],
[2.0, 0.0, 0.0],
[3.0, 0.0, 0.0],
[0.0, 1.0, 0.0],
[1.0, 1.0, 0.0],
[2.0, 1.0, 0.0],
[3.0, 1.0, 0.0],
[0.0, 2.0, 0.0],
[1.0, 2.0, 0.0],
[2.0, 2.0, 0.0],
[3.0, 2.0, 0.0],
[0.0, 3.0, 0.0],
[1.0, 3.0, 0.0],
[2.0, 3.0, 0.0],
[3.0, 3.0, 0.0],
];
let faces = vec![
[0u32, 1, 5],
[0, 5, 4],
[1, 2, 6],
[1, 6, 5],
[2, 3, 7],
[2, 7, 6],
[4, 5, 9],
[4, 9, 8],
[5, 6, 10],
[5, 10, 9],
[6, 7, 11],
[6, 11, 10],
[8, 9, 13],
[8, 13, 12],
[9, 10, 14],
[9, 14, 13],
[10, 11, 15],
[10, 15, 14],
];
crate::io::build_mesh_from_vertices_and_faces(&verts, &faces)
.expect("flat grid construction")
}
#[test]
fn test_tutte_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 = tutte_embedding(&mesh);
assert!(result.is_some(), "Tutte should succeed on a simple quad");
let uv = result.unwrap();
assert_eq!(uv.len(), 4);
for v in &uv {
assert!(v[0].is_finite(), "u should be finite, got {}", v[0]);
assert!(v[1].is_finite(), "v should be finite, got {}", v[1]);
}
for v in &uv {
let r = (v[0] * v[0] + v[1] * v[1]).sqrt();
assert!(
(r - 1.0).abs() < 0.1,
"boundary point should be near unit circle, got r={}",
r
);
}
}
#[test]
fn test_tutte_returns_none_on_empty() {
let mesh = MeshStorage::new();
assert!(tutte_embedding(&mesh).is_none());
}
#[test]
fn test_tutte_returns_none_on_closed_mesh() {
let mesh = crate::test_util::build_icosphere(1);
assert!(
tutte_embedding(&mesh).is_none(),
"closed mesh has no boundary, Tutte should return None"
);
}
#[test]
fn test_tutte_flat_grid_no_flip() {
let mesh = build_flat_grid();
let result = tutte_embedding(&mesh);
assert!(result.is_some(), "Tutte should succeed on flat grid");
let uv = result.unwrap();
assert_eq!(uv.len(), mesh.vertex_count());
for v in &uv {
assert!(v[0].is_finite() && v[1].is_finite(), "UV must be finite");
}
let v_idx = build_vertex_index(&mesh);
let mut signs: Vec<i32> = Vec::new();
for f in mesh.face_ids() {
let verts: Vec<_> = crate::traversal::FaceHalfEdges::new(&mesh, f)
.filter_map(|he| mesh.get_halfedge(he))
.map(|h| h.vertex)
.collect();
if verts.len() != 3 {
continue;
}
let i0 = *v_idx.get(&verts[0]).unwrap();
let i1 = *v_idx.get(&verts[1]).unwrap();
let i2 = *v_idx.get(&verts[2]).unwrap();
let cross_z = (uv[i1][0] - uv[i0][0]) * (uv[i2][1] - uv[i0][1])
- (uv[i1][1] - uv[i0][1]) * (uv[i2][0] - uv[i0][0]);
if cross_z.abs() > 1e-14 {
signs.push(if cross_z > 0.0 { 1 } else { -1 });
}
}
if signs.len() > 1 {
let first = signs[0];
let all_same = signs.iter().all(|&s| s == first);
assert!(
all_same,
"Tutte should produce no flipped triangles (all same sign), got signs: {:?}",
signs
);
}
}
#[test]
fn test_harmonic_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 = harmonic_parameterization(&mesh);
assert!(result.is_some(), "Harmonic should succeed on a simple quad");
let uv = result.unwrap();
assert_eq!(uv.len(), 4);
for v in &uv {
assert!(v[0].is_finite() && v[1].is_finite(), "UV must be finite");
}
for v in &uv {
let r = (v[0] * v[0] + v[1] * v[1]).sqrt();
assert!(
(r - 1.0).abs() < 0.1,
"boundary point should be near unit circle, got r={}",
r
);
}
}
#[test]
fn test_harmonic_returns_none_on_empty() {
let mesh = MeshStorage::new();
assert!(harmonic_parameterization(&mesh).is_none());
}
#[test]
fn test_harmonic_returns_none_on_closed_mesh() {
let mesh = crate::test_util::build_icosphere(1);
assert!(
harmonic_parameterization(&mesh).is_none(),
"closed mesh has no boundary, harmonic should return None"
);
}
fn build_pentagon_fan() -> MeshStorage {
let mut mesh = MeshStorage::new();
let center = mesh.add_vertex(crate::storage::Vertex::new([0.0, 0.0, 0.0]));
let mut boundary = Vec::new();
for k in 0..5 {
let angle = 2.0 * std::f64::consts::PI * (k as f64) / 5.0;
let v = mesh.add_vertex(crate::storage::Vertex::new([angle.cos(), angle.sin(), 0.0]));
boundary.push(v);
}
for k in 0..5 {
let next = (k + 1) % 5;
crate::topology_ops::add_triangle(&mut mesh, center, boundary[k], boundary[next])
.unwrap();
}
mesh
}
#[test]
fn test_harmonic_pentagon_fan() {
let mesh = build_pentagon_fan();
let result = harmonic_parameterization(&mesh);
assert!(result.is_some(), "Harmonic should succeed on pentagon fan");
let uv = result.unwrap();
assert_eq!(uv.len(), mesh.vertex_count());
for v in &uv {
assert!(v[0].is_finite() && v[1].is_finite(), "UV must be finite");
}
let v_idx = build_vertex_index(&mesh);
let mut signs: Vec<i32> = Vec::new();
for f in mesh.face_ids() {
let verts: Vec<_> = crate::traversal::FaceHalfEdges::new(&mesh, f)
.filter_map(|he| mesh.get_halfedge(he))
.map(|h| h.vertex)
.collect();
if verts.len() != 3 {
continue;
}
let i0 = *v_idx.get(&verts[0]).unwrap();
let i1 = *v_idx.get(&verts[1]).unwrap();
let i2 = *v_idx.get(&verts[2]).unwrap();
let cross_z = (uv[i1][0] - uv[i0][0]) * (uv[i2][1] - uv[i0][1])
- (uv[i1][1] - uv[i0][1]) * (uv[i2][0] - uv[i0][0]);
if cross_z.abs() > 1e-14 {
signs.push(if cross_z > 0.0 { 1 } else { -1 });
}
}
if signs.len() > 1 {
let first = signs[0];
assert!(
signs.iter().all(|&s| s == first),
"Harmonic on pentagon fan should have consistent orientation, signs: {:?}",
signs
);
}
}
#[test]
fn test_tutte_and_harmonic_differ() {
let mesh = build_flat_grid();
let tutte = tutte_embedding(&mesh).unwrap();
let Some(harmonic) = harmonic_parameterization(&mesh) else {
return;
};
assert_eq!(tutte.len(), harmonic.len());
let mut diff_count = 0;
for (t, h) in tutte.iter().zip(harmonic.iter()) {
let d = ((t[0] - h[0]).powi(2) + (t[1] - h[1]).powi(2)).sqrt();
if d > 1e-6 {
diff_count += 1;
}
}
assert!(
diff_count > 0,
"Tutte (uniform weights) and harmonic (cotan weights) should produce different interior UVs"
);
}
}