1use threecrate_core::{TriangleMesh, Result, Point3f, Vector3f, Error};
16use std::collections::HashSet;
17
18fn build_adjacency(mesh: &TriangleMesh) -> Vec<Vec<usize>> {
25 let n = mesh.vertices.len();
26 let mut adj: Vec<HashSet<usize>> = vec![HashSet::new(); n];
27 for face in &mesh.faces {
28 let [a, b, c] = *face;
29 adj[a].insert(b);
30 adj[a].insert(c);
31 adj[b].insert(a);
32 adj[b].insert(c);
33 adj[c].insert(a);
34 adj[c].insert(b);
35 }
36 adj.into_iter().map(|s| s.into_iter().collect()).collect()
37}
38
39fn laplacian_step(vertices: &[Point3f], adj: &[Vec<usize>], factor: f32) -> Vec<Point3f> {
43 vertices
44 .iter()
45 .enumerate()
46 .map(|(i, &v)| {
47 let nbrs = &adj[i];
48 if nbrs.is_empty() {
49 return v;
50 }
51 let sum = nbrs
52 .iter()
53 .fold(Vector3f::zeros(), |acc, &j| acc + vertices[j].coords);
54 let centroid = Point3f::from(sum / nbrs.len() as f32);
55 v + (centroid - v) * factor
56 })
57 .collect()
58}
59
60#[derive(Debug, Clone)]
66pub struct LaplacianSmoothingConfig {
67 pub iterations: usize,
69 pub lambda: f32,
72}
73
74impl Default for LaplacianSmoothingConfig {
75 fn default() -> Self {
76 Self { iterations: 10, lambda: 0.5 }
77 }
78}
79
80pub fn smooth_laplacian(
93 mesh: &TriangleMesh,
94 config: &LaplacianSmoothingConfig,
95) -> Result<TriangleMesh> {
96 if mesh.is_empty() {
97 return Err(Error::InvalidData("Mesh is empty".to_string()));
98 }
99 if config.lambda <= 0.0 || config.lambda > 1.0 {
100 return Err(Error::InvalidData("lambda must be in (0, 1]".to_string()));
101 }
102 if config.iterations == 0 {
103 return Ok(mesh.clone());
104 }
105
106 let adj = build_adjacency(mesh);
107 let mut verts = mesh.vertices.clone();
108 for _ in 0..config.iterations {
109 verts = laplacian_step(&verts, &adj, config.lambda);
110 }
111 Ok(TriangleMesh::from_vertices_and_faces(verts, mesh.faces.clone()))
112}
113
114#[derive(Debug, Clone)]
120pub struct TaubinSmoothingConfig {
121 pub iterations: usize,
123 pub lambda: f32,
125 pub mu: f32,
128}
129
130impl Default for TaubinSmoothingConfig {
131 fn default() -> Self {
132 Self { iterations: 10, lambda: 0.5, mu: -0.53 }
133 }
134}
135
136pub fn smooth_taubin(
149 mesh: &TriangleMesh,
150 config: &TaubinSmoothingConfig,
151) -> Result<TriangleMesh> {
152 if mesh.is_empty() {
153 return Err(Error::InvalidData("Mesh is empty".to_string()));
154 }
155 if config.lambda <= 0.0 || config.lambda >= 1.0 {
156 return Err(Error::InvalidData("lambda must be in (0, 1)".to_string()));
157 }
158 if config.mu >= 0.0 {
159 return Err(Error::InvalidData("mu must be negative".to_string()));
160 }
161 if config.iterations == 0 {
162 return Ok(mesh.clone());
163 }
164
165 let adj = build_adjacency(mesh);
166 let mut verts = mesh.vertices.clone();
167 for _ in 0..config.iterations {
168 verts = laplacian_step(&verts, &adj, config.lambda);
169 verts = laplacian_step(&verts, &adj, config.mu);
170 }
171 Ok(TriangleMesh::from_vertices_and_faces(verts, mesh.faces.clone()))
172}
173
174#[derive(Debug, Clone)]
180pub struct HcSmoothingConfig {
181 pub iterations: usize,
183 pub alpha: f32,
187 pub beta: f32,
190}
191
192impl Default for HcSmoothingConfig {
193 fn default() -> Self {
194 Self { iterations: 10, alpha: 0.0, beta: 0.5 }
195 }
196}
197
198pub fn smooth_hc(
212 mesh: &TriangleMesh,
213 config: &HcSmoothingConfig,
214) -> Result<TriangleMesh> {
215 if mesh.is_empty() {
216 return Err(Error::InvalidData("Mesh is empty".to_string()));
217 }
218 if !(0.0..=1.0).contains(&config.alpha) {
219 return Err(Error::InvalidData("alpha must be in [0, 1]".to_string()));
220 }
221 if !(0.0..=1.0).contains(&config.beta) {
222 return Err(Error::InvalidData("beta must be in [0, 1]".to_string()));
223 }
224 if config.iterations == 0 {
225 return Ok(mesh.clone());
226 }
227
228 let adj = build_adjacency(mesh);
229 let original: Vec<Point3f> = mesh.vertices.clone();
230 let mut q: Vec<Point3f> = original.clone();
231
232 for _ in 0..config.iterations {
233 let q_bar: Vec<Point3f> = q
235 .iter()
236 .enumerate()
237 .map(|(i, &v)| {
238 let nbrs = &adj[i];
239 if nbrs.is_empty() {
240 return v;
241 }
242 let sum = nbrs.iter().fold(Vector3f::zeros(), |acc, &j| acc + q[j].coords);
243 Point3f::from(sum / nbrs.len() as f32)
244 })
245 .collect();
246
247 let b: Vec<Vector3f> = (0..q.len())
250 .map(|i| {
251 let blend =
252 original[i].coords * config.alpha + q[i].coords * (1.0 - config.alpha);
253 q_bar[i].coords - blend
254 })
255 .collect();
256
257 q = (0..q.len())
260 .map(|i| {
261 let nbrs = &adj[i];
262 let avg_b = if nbrs.is_empty() {
263 Vector3f::zeros()
264 } else {
265 nbrs.iter().fold(Vector3f::zeros(), |acc, &j| acc + b[j])
266 / nbrs.len() as f32
267 };
268 let correction = b[i] * config.beta + avg_b * (1.0 - config.beta);
269 Point3f::from(q_bar[i].coords - correction)
270 })
271 .collect();
272 }
273
274 Ok(TriangleMesh::from_vertices_and_faces(q, mesh.faces.clone()))
275}
276
277#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn make_spike_mesh() -> TriangleMesh {
288 let verts = vec![
289 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), ];
299 let faces = vec![
300 [0, 1, 3], [1, 4, 3],
301 [1, 2, 4], [2, 5, 4],
302 [3, 4, 6], [4, 7, 6],
303 [4, 5, 7], [5, 8, 7],
304 ];
305 TriangleMesh::from_vertices_and_faces(verts, faces)
306 }
307
308 fn centroid_z(mesh: &TriangleMesh) -> f32 {
310 mesh.vertices.iter().map(|v| v.z).sum::<f32>() / mesh.vertices.len() as f32
311 }
312
313 #[test]
316 fn test_laplacian_preserves_topology() {
317 let mesh = make_spike_mesh();
318 let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
319 assert_eq!(result.face_count(), mesh.face_count());
320 assert_eq!(result.vertex_count(), mesh.vertex_count());
321 assert_eq!(result.faces, mesh.faces);
322 }
323
324 #[test]
325 fn test_taubin_preserves_topology() {
326 let mesh = make_spike_mesh();
327 let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
328 assert_eq!(result.faces, mesh.faces);
329 }
330
331 #[test]
332 fn test_hc_preserves_topology() {
333 let mesh = make_spike_mesh();
334 let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
335 assert_eq!(result.faces, mesh.faces);
336 }
337
338 #[test]
341 fn test_laplacian_reduces_spike() {
342 let mesh = make_spike_mesh();
343 let before_z = mesh.vertices[4].z; let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
345 let after_z = result.vertices[4].z;
346 assert!(
347 after_z < before_z,
348 "spike z should decrease: before={before_z}, after={after_z}"
349 );
350 }
351
352 #[test]
353 fn test_taubin_reduces_spike() {
354 let mesh = make_spike_mesh();
355 let before_z = mesh.vertices[4].z;
356 let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
357 let after_z = result.vertices[4].z;
358 assert!(after_z < before_z, "Taubin should reduce spike: {before_z} → {after_z}");
359 }
360
361 #[test]
362 fn test_hc_reduces_spike() {
363 let mesh = make_spike_mesh();
364 let before_z = mesh.vertices[4].z;
365 let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
366 let after_z = result.vertices[4].z;
367 assert!(after_z < before_z, "HC should reduce spike: {before_z} → {after_z}");
368 }
369
370 #[test]
373 fn test_taubin_less_shrinkage_than_laplacian() {
374 let verts = vec![
378 Point3f::new(0.0, 0.0, 0.0), Point3f::new(1.0, 0.0, 0.0),
379 Point3f::new(1.0, 1.0, 0.0), Point3f::new(0.0, 1.0, 0.0),
380 Point3f::new(0.0, 0.0, 1.0), Point3f::new(1.0, 0.0, 1.0),
381 Point3f::new(1.0, 1.0, 1.0), Point3f::new(0.0, 1.0, 1.0),
382 ];
383 let faces = vec![
384 [0,2,1],[0,3,2], [4,5,6],[4,6,7],
385 [0,1,5],[0,5,4], [3,7,6],[3,6,2],
386 [0,4,7],[0,7,3], [1,2,6],[1,6,5],
387 ];
388 let mesh = TriangleMesh::from_vertices_and_faces(verts, faces);
389
390 let avg_dist = |m: &TriangleMesh| {
392 let c: Vector3f = m.vertices.iter().fold(Vector3f::zeros(), |acc, v| acc + v.coords)
393 / m.vertices.len() as f32;
394 m.vertices.iter().map(|v| (v.coords - c).magnitude()).sum::<f32>()
395 / m.vertices.len() as f32
396 };
397
398 let original_spread = avg_dist(&mesh);
399
400 let lap = smooth_laplacian(
401 &mesh,
402 &LaplacianSmoothingConfig { iterations: 50, lambda: 0.5 },
403 ).unwrap();
404 let tau = smooth_taubin(
405 &mesh,
406 &TaubinSmoothingConfig { iterations: 50, lambda: 0.5, mu: -0.53 },
407 ).unwrap();
408
409 let lap_spread = avg_dist(&lap);
410 let tau_spread = avg_dist(&tau);
411
412 assert!(
414 tau_spread > lap_spread,
415 "Taubin should preserve spread better than Laplacian: \
416 original={original_spread:.4}, lap={lap_spread:.4}, tau={tau_spread:.4}"
417 );
418 }
419
420 #[test]
423 fn test_laplacian_zero_iterations() {
424 let mesh = make_spike_mesh();
425 let result = smooth_laplacian(
426 &mesh,
427 &LaplacianSmoothingConfig { iterations: 0, lambda: 0.5 },
428 )
429 .unwrap();
430 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
431 assert!((a - b).magnitude() < 1e-6);
432 }
433 }
434
435 #[test]
436 fn test_taubin_zero_iterations() {
437 let mesh = make_spike_mesh();
438 let result = smooth_taubin(
439 &mesh,
440 &TaubinSmoothingConfig { iterations: 0, lambda: 0.5, mu: -0.53 },
441 )
442 .unwrap();
443 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
444 assert!((a - b).magnitude() < 1e-6);
445 }
446 }
447
448 #[test]
449 fn test_hc_zero_iterations() {
450 let mesh = make_spike_mesh();
451 let result =
452 smooth_hc(&mesh, &HcSmoothingConfig { iterations: 0, alpha: 0.0, beta: 0.5 })
453 .unwrap();
454 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
455 assert!((a - b).magnitude() < 1e-6);
456 }
457 }
458
459 #[test]
462 fn test_laplacian_empty_mesh() {
463 let empty = TriangleMesh::new();
464 assert!(smooth_laplacian(&empty, &LaplacianSmoothingConfig::default()).is_err());
465 }
466
467 #[test]
468 fn test_laplacian_invalid_lambda() {
469 let mesh = make_spike_mesh();
470 assert!(smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 0.0 }).is_err());
471 assert!(smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 1.5 }).is_err());
472 }
473
474 #[test]
475 fn test_taubin_invalid_params() {
476 let mesh = make_spike_mesh();
477 assert!(smooth_taubin(&mesh, &TaubinSmoothingConfig { iterations: 1, lambda: 0.0, mu: -0.53 }).is_err());
479 assert!(smooth_taubin(&mesh, &TaubinSmoothingConfig { iterations: 1, lambda: 0.5, mu: 0.1 }).is_err());
481 }
482
483 #[test]
484 fn test_hc_invalid_params() {
485 let mesh = make_spike_mesh();
486 assert!(smooth_hc(&mesh, &HcSmoothingConfig { iterations: 1, alpha: -0.1, beta: 0.5 }).is_err());
487 assert!(smooth_hc(&mesh, &HcSmoothingConfig { iterations: 1, alpha: 0.5, beta: 1.5 }).is_err());
488 }
489
490 #[test]
491 fn test_taubin_empty_mesh() {
492 let empty = TriangleMesh::new();
493 assert!(smooth_taubin(&empty, &TaubinSmoothingConfig::default()).is_err());
494 }
495
496 #[test]
497 fn test_hc_empty_mesh() {
498 let empty = TriangleMesh::new();
499 assert!(smooth_hc(&empty, &HcSmoothingConfig::default()).is_err());
500 }
501
502 #[test]
505 fn test_laplacian_more_iterations_smoother() {
506 let mesh = make_spike_mesh();
507 let r1 = smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 1, lambda: 0.5 }).unwrap();
508 let r5 = smooth_laplacian(&mesh, &LaplacianSmoothingConfig { iterations: 5, lambda: 0.5 }).unwrap();
509 assert!(r5.vertices[4].z < r1.vertices[4].z);
511 }
512}