use threecrate_core::{TriangleMesh, Result, Point3f, Vector3f, Error};
use std::collections::HashSet;
fn build_adjacency(mesh: &TriangleMesh) -> Vec<Vec<usize>> {
let n = mesh.vertices.len();
let mut adj: Vec<HashSet<usize>> = vec![HashSet::new(); n];
for face in &mesh.faces {
let [a, b, c] = *face;
adj[a].insert(b);
adj[a].insert(c);
adj[b].insert(a);
adj[b].insert(c);
adj[c].insert(a);
adj[c].insert(b);
}
adj.into_iter().map(|s| s.into_iter().collect()).collect()
}
fn laplacian_step(vertices: &[Point3f], adj: &[Vec<usize>], factor: f32) -> Vec<Point3f> {
vertices
.iter()
.enumerate()
.map(|(i, &v)| {
let nbrs = &adj[i];
if nbrs.is_empty() {
return v;
}
let sum = nbrs
.iter()
.fold(Vector3f::zeros(), |acc, &j| acc + vertices[j].coords);
let centroid = Point3f::from(sum / nbrs.len() as f32);
v + (centroid - v) * factor
})
.collect()
}
#[derive(Debug, Clone)]
pub struct LaplacianSmoothingConfig {
pub iterations: usize,
pub lambda: f32,
}
impl Default for LaplacianSmoothingConfig {
fn default() -> Self {
Self { iterations: 10, lambda: 0.5 }
}
}
pub fn smooth_laplacian(
mesh: &TriangleMesh,
config: &LaplacianSmoothingConfig,
) -> Result<TriangleMesh> {
if mesh.is_empty() {
return Err(Error::InvalidData("Mesh is empty".to_string()));
}
if config.lambda <= 0.0 || config.lambda > 1.0 {
return Err(Error::InvalidData("lambda must be in (0, 1]".to_string()));
}
if config.iterations == 0 {
return Ok(mesh.clone());
}
let adj = build_adjacency(mesh);
let mut verts = mesh.vertices.clone();
for _ in 0..config.iterations {
verts = laplacian_step(&verts, &adj, config.lambda);
}
Ok(TriangleMesh::from_vertices_and_faces(verts, mesh.faces.clone()))
}
#[derive(Debug, Clone)]
pub struct TaubinSmoothingConfig {
pub iterations: usize,
pub lambda: f32,
pub mu: f32,
}
impl Default for TaubinSmoothingConfig {
fn default() -> Self {
Self { iterations: 10, lambda: 0.5, mu: -0.53 }
}
}
pub fn smooth_taubin(
mesh: &TriangleMesh,
config: &TaubinSmoothingConfig,
) -> Result<TriangleMesh> {
if mesh.is_empty() {
return Err(Error::InvalidData("Mesh is empty".to_string()));
}
if config.lambda <= 0.0 || config.lambda >= 1.0 {
return Err(Error::InvalidData("lambda must be in (0, 1)".to_string()));
}
if config.mu >= 0.0 {
return Err(Error::InvalidData("mu must be negative".to_string()));
}
if config.iterations == 0 {
return Ok(mesh.clone());
}
let adj = build_adjacency(mesh);
let mut verts = mesh.vertices.clone();
for _ in 0..config.iterations {
verts = laplacian_step(&verts, &adj, config.lambda);
verts = laplacian_step(&verts, &adj, config.mu);
}
Ok(TriangleMesh::from_vertices_and_faces(verts, mesh.faces.clone()))
}
#[derive(Debug, Clone)]
pub struct HcSmoothingConfig {
pub iterations: usize,
pub alpha: f32,
pub beta: f32,
}
impl Default for HcSmoothingConfig {
fn default() -> Self {
Self { iterations: 10, alpha: 0.0, beta: 0.5 }
}
}
pub fn smooth_hc(
mesh: &TriangleMesh,
config: &HcSmoothingConfig,
) -> Result<TriangleMesh> {
if mesh.is_empty() {
return Err(Error::InvalidData("Mesh is empty".to_string()));
}
if !(0.0..=1.0).contains(&config.alpha) {
return Err(Error::InvalidData("alpha must be in [0, 1]".to_string()));
}
if !(0.0..=1.0).contains(&config.beta) {
return Err(Error::InvalidData("beta must be in [0, 1]".to_string()));
}
if config.iterations == 0 {
return Ok(mesh.clone());
}
let adj = build_adjacency(mesh);
let original: Vec<Point3f> = mesh.vertices.clone();
let mut q: Vec<Point3f> = original.clone();
for _ in 0..config.iterations {
let q_bar: Vec<Point3f> = q
.iter()
.enumerate()
.map(|(i, &v)| {
let nbrs = &adj[i];
if nbrs.is_empty() {
return v;
}
let sum = nbrs.iter().fold(Vector3f::zeros(), |acc, &j| acc + q[j].coords);
Point3f::from(sum / nbrs.len() as f32)
})
.collect();
let b: Vec<Vector3f> = (0..q.len())
.map(|i| {
let blend =
original[i].coords * config.alpha + q[i].coords * (1.0 - config.alpha);
q_bar[i].coords - blend
})
.collect();
q = (0..q.len())
.map(|i| {
let nbrs = &adj[i];
let avg_b = if nbrs.is_empty() {
Vector3f::zeros()
} else {
nbrs.iter().fold(Vector3f::zeros(), |acc, &j| acc + b[j])
/ nbrs.len() as f32
};
let correction = b[i] * config.beta + avg_b * (1.0 - config.beta);
Point3f::from(q_bar[i].coords - correction)
})
.collect();
}
Ok(TriangleMesh::from_vertices_and_faces(q, mesh.faces.clone()))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_spike_mesh() -> TriangleMesh {
let verts = vec![
Point3f::new(0.0, 0.0, 0.0), Point3f::new(1.0, 0.0, 0.0), Point3f::new(2.0, 0.0, 0.0), Point3f::new(0.0, 1.0, 0.0), Point3f::new(1.0, 1.0, 1.0), Point3f::new(2.0, 1.0, 0.0), Point3f::new(0.0, 2.0, 0.0), Point3f::new(1.0, 2.0, 0.0), Point3f::new(2.0, 2.0, 0.0), ];
let faces = vec![
[0, 1, 3], [1, 4, 3],
[1, 2, 4], [2, 5, 4],
[3, 4, 6], [4, 7, 6],
[4, 5, 7], [5, 8, 7],
];
TriangleMesh::from_vertices_and_faces(verts, faces)
}
fn centroid_z(mesh: &TriangleMesh) -> f32 {
mesh.vertices.iter().map(|v| v.z).sum::<f32>() / mesh.vertices.len() as f32
}
#[test]
fn test_laplacian_preserves_topology() {
let mesh = make_spike_mesh();
let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
assert_eq!(result.face_count(), mesh.face_count());
assert_eq!(result.vertex_count(), mesh.vertex_count());
assert_eq!(result.faces, mesh.faces);
}
#[test]
fn test_taubin_preserves_topology() {
let mesh = make_spike_mesh();
let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
assert_eq!(result.faces, mesh.faces);
}
#[test]
fn test_hc_preserves_topology() {
let mesh = make_spike_mesh();
let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
assert_eq!(result.faces, mesh.faces);
}
#[test]
fn test_laplacian_reduces_spike() {
let mesh = make_spike_mesh();
let before_z = mesh.vertices[4].z; let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
let after_z = result.vertices[4].z;
assert!(
after_z < before_z,
"spike z should decrease: before={before_z}, after={after_z}"
);
}
#[test]
fn test_taubin_reduces_spike() {
let mesh = make_spike_mesh();
let before_z = mesh.vertices[4].z;
let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
let after_z = result.vertices[4].z;
assert!(after_z < before_z, "Taubin should reduce spike: {before_z} → {after_z}");
}
#[test]
fn test_hc_reduces_spike() {
let mesh = make_spike_mesh();
let before_z = mesh.vertices[4].z;
let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
let after_z = result.vertices[4].z;
assert!(after_z < before_z, "HC should reduce spike: {before_z} → {after_z}");
}
#[test]
fn test_taubin_less_shrinkage_than_laplacian() {
let verts = vec![
Point3f::new(0.0, 0.0, 0.0), Point3f::new(1.0, 0.0, 0.0),
Point3f::new(1.0, 1.0, 0.0), Point3f::new(0.0, 1.0, 0.0),
Point3f::new(0.0, 0.0, 1.0), Point3f::new(1.0, 0.0, 1.0),
Point3f::new(1.0, 1.0, 1.0), Point3f::new(0.0, 1.0, 1.0),
];
let faces = vec![
[0,2,1],[0,3,2], [4,5,6],[4,6,7],
[0,1,5],[0,5,4], [3,7,6],[3,6,2],
[0,4,7],[0,7,3], [1,2,6],[1,6,5],
];
let mesh = TriangleMesh::from_vertices_and_faces(verts, faces);
let avg_dist = |m: &TriangleMesh| {
let c: Vector3f = m.vertices.iter().fold(Vector3f::zeros(), |acc, v| acc + v.coords)
/ m.vertices.len() as f32;
m.vertices.iter().map(|v| (v.coords - c).magnitude()).sum::<f32>()
/ m.vertices.len() as f32
};
let original_spread = avg_dist(&mesh);
let lap = smooth_laplacian(
&mesh,
&LaplacianSmoothingConfig { iterations: 50, lambda: 0.5 },
).unwrap();
let tau = smooth_taubin(
&mesh,
&TaubinSmoothingConfig { iterations: 50, lambda: 0.5, mu: -0.53 },
).unwrap();
let lap_spread = avg_dist(&lap);
let tau_spread = avg_dist(&tau);
assert!(
tau_spread > lap_spread,
"Taubin should preserve spread better than Laplacian: \
original={original_spread:.4}, lap={lap_spread:.4}, tau={tau_spread:.4}"
);
}
#[test]
fn test_laplacian_zero_iterations() {
let mesh = make_spike_mesh();
let result = smooth_laplacian(
&mesh,
&LaplacianSmoothingConfig { iterations: 0, lambda: 0.5 },
)
.unwrap();
for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
assert!((a - b).magnitude() < 1e-6);
}
}
#[test]
fn test_taubin_zero_iterations() {
let mesh = make_spike_mesh();
let result = smooth_taubin(
&mesh,
&TaubinSmoothingConfig { iterations: 0, lambda: 0.5, mu: -0.53 },
)
.unwrap();
for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
assert!((a - b).magnitude() < 1e-6);
}
}
#[test]
fn test_hc_zero_iterations() {
let mesh = make_spike_mesh();
let result =
smooth_hc(&mesh, &HcSmoothingConfig { iterations: 0, alpha: 0.0, beta: 0.5 })
.unwrap();
for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
assert!((a - b).magnitude() < 1e-6);
}
}
#[test]
fn test_laplacian_empty_mesh() {
let empty = TriangleMesh::new();
assert!(smooth_laplacian(&empty, &LaplacianSmoothingConfig::default()).is_err());
}
#[test]
fn test_laplacian_invalid_lambda() {
let mesh = make_spike_mesh();
assert!(smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 0.0 }).is_err());
assert!(smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 1.5 }).is_err());
}
#[test]
fn test_taubin_invalid_params() {
let mesh = make_spike_mesh();
assert!(smooth_taubin(&mesh, &TaubinSmoothingConfig { iterations: 1, lambda: 0.0, mu: -0.53 }).is_err());
assert!(smooth_taubin(&mesh, &TaubinSmoothingConfig { iterations: 1, lambda: 0.5, mu: 0.1 }).is_err());
}
#[test]
fn test_hc_invalid_params() {
let mesh = make_spike_mesh();
assert!(smooth_hc(&mesh, &HcSmoothingConfig { iterations: 1, alpha: -0.1, beta: 0.5 }).is_err());
assert!(smooth_hc(&mesh, &HcSmoothingConfig { iterations: 1, alpha: 0.5, beta: 1.5 }).is_err());
}
#[test]
fn test_taubin_empty_mesh() {
let empty = TriangleMesh::new();
assert!(smooth_taubin(&empty, &TaubinSmoothingConfig::default()).is_err());
}
#[test]
fn test_hc_empty_mesh() {
let empty = TriangleMesh::new();
assert!(smooth_hc(&empty, &HcSmoothingConfig::default()).is_err());
}
#[test]
fn test_laplacian_more_iterations_smoother() {
let mesh = make_spike_mesh();
let r1 = smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 0.5 }).unwrap();
let r5 = smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 5, lambda: 0.5 }).unwrap();
assert!(r5.vertices[4].z < r1.vertices[4].z);
}
}