use std::collections::HashMap;
use crate::geometry::{cotan_edge_weight, edge_length, face_normal};
use crate::ids::{FaceId, HalfEdgeId, VertexId};
use crate::linalg::norm2;
use crate::linalg::{SparseSystem, conjugate_gradient};
use crate::storage::MeshStorage;
use crate::traversal::{FaceHalfEdges, VertexAdjacentFaces};
#[derive(Debug, Clone)]
pub struct FaceLocalFrame {
pub e1: [f64; 3],
pub e2: [f64; 3],
pub normal: [f64; 3],
}
#[derive(Debug, Clone)]
pub struct Singularity {
pub vertex: VertexId,
pub index: f64,
}
pub fn build_face_local_frames(mesh: &MeshStorage) -> HashMap<FaceId, FaceLocalFrame> {
let mut frames = HashMap::new();
for f in mesh.face_ids() {
let normal = match face_normal(mesh, f) {
Some(n) => n,
None => continue,
};
let mut best_dir = [1.0, 0.0, 0.0];
let mut best_len = f64::INFINITY;
let he_ids: Vec<HalfEdgeId> = FaceHalfEdges::new(mesh, f).collect();
for &he in &he_ids {
if let Some(l) = edge_length(mesh, he)
&& l < best_len
&& l > 1e-14
{
best_len = l;
let h = match mesh.get_halfedge(he) {
Some(h) => h,
None => continue,
};
let tip = h.vertex;
let origin = match h.twin.and_then(|t| mesh.get_halfedge(t)) {
Some(t) => t.vertex,
None => continue,
};
let p_tip = match mesh.get_vertex(tip) {
Some(v) => v.position,
None => continue,
};
let p_origin = match mesh.get_vertex(origin) {
Some(v) => v.position,
None => continue,
};
let dir = sub3(p_tip, p_origin);
let len = norm3(dir);
if len > 1e-14 {
best_dir = scale3(dir, 1.0 / len);
}
}
}
let dot_val = dot3(best_dir, normal);
let proj = sub3(best_dir, scale3(normal, dot_val));
let proj_len = norm3(proj);
let e1 = if proj_len > 1e-14 {
scale3(proj, 1.0 / proj_len)
} else {
let arb = if normal[0].abs() < 0.9 {
[1.0, 0.0, 0.0]
} else {
[0.0, 1.0, 0.0]
};
let p = sub3(arb, scale3(normal, dot3(arb, normal)));
let pl = norm3(p);
if pl > 1e-14 {
scale3(p, 1.0 / pl)
} else {
[1.0, 0.0, 0.0]
}
};
let e2 = cross3(normal, e1);
let e2_len = norm3(e2);
let e2 = if e2_len > 1e-14 {
scale3(e2, 1.0 / e2_len)
} else {
[0.0, 1.0, 0.0]
};
frames.insert(f, FaceLocalFrame { e1, e2, normal });
}
frames
}
pub fn compute_transport_angles(
mesh: &MeshStorage,
frames: &HashMap<FaceId, FaceLocalFrame>,
face_index: &HashMap<FaceId, usize>,
) -> HashMap<(usize, usize), f64> {
let mut angles = HashMap::new();
for he in mesh.halfedge_ids() {
let h = match mesh.get_halfedge(he) {
Some(h) => h,
None => continue,
};
let Some(twin_id) = h.twin else { continue };
let twin = match mesh.get_halfedge(twin_id) {
Some(t) => t,
None => continue,
};
let Some(fi) = h.face else { continue };
let Some(fj) = twin.face else { continue };
if fi == fj {
continue;
}
let Some(frame_i) = frames.get(&fi) else {
continue;
};
let Some(frame_j) = frames.get(&fj) else {
continue;
};
let Some(&idx_i) = face_index.get(&fi) else {
continue;
};
let Some(&idx_j) = face_index.get(&fj) else {
continue;
};
let tip = h.vertex;
let origin = twin.vertex;
let p_tip = match mesh.get_vertex(tip) {
Some(v) => v.position,
None => continue,
};
let p_origin = match mesh.get_vertex(origin) {
Some(v) => v.position,
None => continue,
};
let edge_dir = sub3(p_tip, p_origin);
let edge_len = norm3(edge_dir);
if edge_len < 1e-14 {
continue;
}
let edge_dir = scale3(edge_dir, 1.0 / edge_len);
let ni = frame_i.normal;
let nj = frame_j.normal;
let cos_alpha = dot3(ni, nj);
let sin_alpha = dot3(cross3(nj, ni), edge_dir);
let alpha = sin_alpha.atan2(cos_alpha);
let e1j_par = rodrigues_rotate(frame_j.e1, edge_dir, alpha);
let proj = sub3(e1j_par, scale3(ni, dot3(e1j_par, ni)));
let proj_len = norm3(proj);
if proj_len < 1e-14 {
continue;
}
let e1j_par = scale3(proj, 1.0 / proj_len);
let cos_delta = dot3(frame_i.e1, e1j_par);
let sin_delta = dot3(cross3(ni, e1j_par), frame_i.e1);
let delta = sin_delta.atan2(cos_delta);
let key = if idx_i < idx_j {
(idx_i, idx_j)
} else {
(idx_j, idx_i)
};
let sign = if idx_i < idx_j { 1.0 } else { -1.0 };
angles.insert(key, sign * delta);
}
angles
}
fn rodrigues_rotate(v: [f64; 3], k: [f64; 3], angle: f64) -> [f64; 3] {
let cos_a = angle.cos();
let sin_a = angle.sin();
let k_cross_v = cross3(k, v);
let k_dot_v = dot3(k, v);
add3(
add3(scale3(v, cos_a), scale3(k_cross_v, sin_a)),
scale3(k, k_dot_v * (1.0 - cos_a)),
)
}
fn build_covariant_laplacian_real(
mesh: &MeshStorage,
n_sym: usize,
_frames: &HashMap<FaceId, FaceLocalFrame>,
face_index: &HashMap<FaceId, usize>,
transport_angles: &HashMap<(usize, usize), f64>,
n_faces: usize,
) -> sprs::CsMat<f64> {
let dim = 2 * n_faces;
let mut sys = SparseSystem::new(dim);
for he in mesh.halfedge_ids() {
let h = match mesh.get_halfedge(he) {
Some(h) => h,
None => continue,
};
let Some(twin_id) = h.twin else { continue };
let twin = match mesh.get_halfedge(twin_id) {
Some(t) => t,
None => continue,
};
let Some(fi) = h.face else { continue };
let Some(fj) = twin.face else { continue };
if fi == fj {
continue;
}
let Some(&idx_i) = face_index.get(&fi) else {
continue;
};
let Some(&idx_j) = face_index.get(&fj) else {
continue;
};
if idx_i >= idx_j {
continue; }
let w = cotan_edge_weight(mesh, he).unwrap_or(0.5);
let w = w.max(0.0);
if w < 1e-14 {
continue;
}
let key = (idx_i, idx_j);
let delta = match transport_angles.get(&key) {
Some(&d) => d,
None => continue,
};
let phi = n_sym as f64 * delta;
let cos_phi = phi.cos();
let sin_phi = phi.sin();
let ri = 2 * idx_i;
let rj = 2 * idx_j;
sys.add(ri, rj, -w * cos_phi);
sys.add(ri + 1, rj + 1, -w * cos_phi);
sys.add(ri, rj + 1, w * sin_phi);
sys.add(ri + 1, rj, -w * sin_phi);
sys.add_diag(ri, w);
sys.add_diag(ri + 1, w);
sys.add_diag(rj, w);
sys.add_diag(rj + 1, w);
}
for (&f, &idx) in face_index {
let ri = 2 * idx;
let has_neighbors = mesh.halfedge_ids().any(|he| {
mesh.get_halfedge(he)
.and_then(|h| h.face)
.is_some_and(|fi| fi == f)
&& mesh
.get_halfedge(he)
.and_then(|h| h.twin)
.is_some_and(|twin| {
mesh.get_halfedge(twin)
.and_then(|t| t.face)
.is_some_and(|fj| fj != f)
})
});
if !has_neighbors {
sys.add_diag(ri, 1e-6);
sys.add_diag(ri + 1, 1e-6);
}
}
sys.finish()
}
fn smallest_eigenvector(mat: &sprs::CsMat<f64>, dim: usize, max_iter: usize, tol: f64) -> Vec<f64> {
let mut mat_reg = mat.clone();
crate::linalg::regularize_diagonal(&mut mat_reg, 1e-8);
let mut x = vec![0.0; dim];
let seed = 42u64;
let mut state = seed;
for v in x.iter_mut() {
state = state.wrapping_mul(6364136223846793005).wrapping_add(1);
let val = ((state >> 33) as f64) / (1u64 << 31) as f64 - 1.0;
*v = val;
}
let x_norm = norm2(&x);
if x_norm > 1e-14 {
for v in &mut x {
*v /= x_norm;
}
}
for _ in 0..max_iter {
let mut y = match conjugate_gradient(&mat_reg, &x, 500, tol * 0.01) {
Some(y) => y,
None => break,
};
let y_norm = norm2(&y);
if y_norm < 1e-14 {
break;
}
for v in &mut y {
*v /= y_norm;
}
let diff: f64 = x.iter().zip(y.iter()).map(|(a, b)| (a - b) * (a - b)).sum();
x = y;
if diff.sqrt() < tol {
break;
}
}
x
}
pub fn smoothest_nrosy(mesh: &MeshStorage, n_sym: usize) -> HashMap<FaceId, f64> {
if n_sym == 0 {
return HashMap::new();
}
let faces: Vec<FaceId> = mesh.face_ids().collect();
let n_faces = faces.len();
if n_faces == 0 {
return HashMap::new();
}
let face_index: HashMap<FaceId, usize> =
faces.iter().enumerate().map(|(i, &f)| (f, i)).collect();
let frames = build_face_local_frames(mesh);
let transport_angles = compute_transport_angles(mesh, &frames, &face_index);
let mat = build_covariant_laplacian_real(
mesh,
n_sym,
&frames,
&face_index,
&transport_angles,
n_faces,
);
let dim = 2 * n_faces;
let eigvec = smallest_eigenvector(&mat, dim, 100, 1e-8);
let mut result = HashMap::new();
for (&f, &idx) in &face_index {
let re = eigvec[2 * idx];
let im = eigvec[2 * idx + 1];
let theta = im.atan2(re) / n_sym as f64;
result.insert(f, theta);
}
result
}
pub fn smoothest_vector_field(mesh: &MeshStorage) -> HashMap<FaceId, f64> {
smoothest_nrosy(mesh, 1)
}
pub fn smoothest_cross_field(mesh: &MeshStorage) -> HashMap<FaceId, f64> {
smoothest_nrosy(mesh, 2)
}
pub fn smoothest_frame_field(mesh: &MeshStorage) -> HashMap<FaceId, f64> {
smoothest_nrosy(mesh, 4)
}
pub fn detect_singularities(
mesh: &MeshStorage,
n_sym: usize,
theta: &HashMap<FaceId, f64>,
) -> Vec<Singularity> {
let frames = build_face_local_frames(mesh);
let faces: Vec<FaceId> = mesh.face_ids().collect();
let face_index: HashMap<FaceId, usize> =
faces.iter().enumerate().map(|(i, &f)| (f, i)).collect();
let transport_angles = compute_transport_angles(mesh, &frames, &face_index);
let mut singularities = Vec::new();
for v in mesh.vertex_ids().collect::<Vec<_>>() {
let adj_faces: Vec<FaceId> = VertexAdjacentFaces::new(mesh, v).collect();
if adj_faces.len() < 3 {
continue;
}
let mut total_angle = 0.0;
for i in 0..adj_faces.len() {
let fi = adj_faces[i];
let fj = adj_faces[(i + 1) % adj_faces.len()];
let Some(&theta_i) = theta.get(&fi) else {
continue;
};
let Some(&theta_j) = theta.get(&fj) else {
continue;
};
let Some(&idx_i) = face_index.get(&fi) else {
continue;
};
let Some(&idx_j) = face_index.get(&fj) else {
continue;
};
let key = if idx_i < idx_j {
(idx_i, idx_j)
} else {
(idx_j, idx_i)
};
let delta = match transport_angles.get(&key) {
Some(&d) => {
if idx_i < idx_j {
d
} else {
-d
}
}
None => 0.0,
};
let diff = n_sym as f64 * (theta_j - theta_i) - n_sym as f64 * delta;
let wrapped = wrap_angle(diff);
total_angle += wrapped;
}
let index = total_angle / (2.0 * std::f64::consts::PI);
if index.abs() > 0.01 {
singularities.push(Singularity { vertex: v, index });
}
}
singularities
}
fn wrap_angle(a: f64) -> f64 {
let pi = std::f64::consts::PI;
let two_pi = 2.0 * pi;
let mut a = a.rem_euclid(two_pi);
if a > pi {
a -= two_pi;
}
a
}
fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn scale3(v: [f64; 3], s: f64) -> [f64; 3] {
[v[0] * s, v[1] * s, v[2] * s]
}
fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
[
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 norm3(v: [f64; 3]) -> f64 {
dot3(v, v).sqrt()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::build_grid;
use crate::storage::{MeshStorage, Vertex};
use crate::test_util::build_icosphere;
use crate::topology_ops::add_triangle;
#[test]
fn smoothest_vector_field_grid() {
let mesh = build_grid(1.0, 1.0, 3, 3);
let theta = smoothest_vector_field(&mesh);
assert!(!theta.is_empty(), "方向场不应为空");
for &t in theta.values() {
assert!(t.abs() <= std::f64::consts::PI + 0.1);
}
}
#[test]
fn smoothest_cross_field_icosphere() {
let mesh = build_icosphere(1);
let theta = smoothest_cross_field(&mesh);
assert!(!theta.is_empty());
for &t in theta.values() {
assert!(t.abs() <= std::f64::consts::PI + 0.1, "角度超出范围: {}", t);
}
}
#[test]
fn smoothest_frame_field_grid() {
let mesh = build_grid(1.0, 1.0, 2, 2);
let theta = smoothest_frame_field(&mesh);
assert!(!theta.is_empty());
}
#[test]
fn smoothest_field_empty() {
let mesh = MeshStorage::new();
let theta = smoothest_nrosy(&mesh, 1);
assert!(theta.is_empty());
}
#[test]
fn smoothest_field_single_triangle() {
let mut mesh = MeshStorage::new();
let v0 = mesh.add_vertex(Vertex::new([0.0, 0.0, 0.0]));
let v1 = mesh.add_vertex(Vertex::new([1.0, 0.0, 0.0]));
let v2 = mesh.add_vertex(Vertex::new([0.0, 1.0, 0.0]));
add_triangle(&mut mesh, v0, v1, v2).unwrap();
let theta = smoothest_nrosy(&mesh, 1);
assert_eq!(theta.len(), 1);
}
#[test]
fn rodrigues_rotation_90deg() {
let v = [1.0, 0.0, 0.0];
let axis = [0.0, 0.0, 1.0];
let rotated = rodrigues_rotate(v, axis, std::f64::consts::FRAC_PI_2);
assert!((rotated[0] - 0.0).abs() < 1e-10, "x = {}", rotated[0]);
assert!((rotated[1] - 1.0).abs() < 1e-10, "y = {}", rotated[1]);
assert!((rotated[2] - 0.0).abs() < 1e-10, "z = {}", rotated[2]);
}
#[test]
fn face_local_frame_orthogonal() {
let mesh = build_icosphere(1);
let frames = build_face_local_frames(&mesh);
for frame in frames.values() {
assert!(
dot3(frame.e1, frame.normal).abs() < 1e-10,
"e1 不在切平面内"
);
assert!(
dot3(frame.e2, frame.normal).abs() < 1e-10,
"e2 不在切平面内"
);
assert!(dot3(frame.e1, frame.e2).abs() < 1e-10, "e1 与 e2 不正交");
assert!((norm3(frame.e1) - 1.0).abs() < 1e-10);
assert!((norm3(frame.e2) - 1.0).abs() < 1e-10);
}
}
#[test]
fn wrap_angle_test() {
assert!((wrap_angle(0.0)).abs() < 1e-10);
assert!((wrap_angle(std::f64::consts::PI) - std::f64::consts::PI).abs() < 1e-10);
assert!((wrap_angle(3.0 * std::f64::consts::PI) - std::f64::consts::PI).abs() < 1e-10);
assert!((wrap_angle(-3.0 * std::f64::consts::PI) - std::f64::consts::PI).abs() < 1e-10);
}
}