use std::collections::HashSet;
use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
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);
}
}