1use std::collections::HashSet;
16use threecrate_core::{Error, Point3f, Result, TriangleMesh, Vector3f};
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 {
77 iterations: 10,
78 lambda: 0.5,
79 }
80 }
81}
82
83pub fn smooth_laplacian(
96 mesh: &TriangleMesh,
97 config: &LaplacianSmoothingConfig,
98) -> Result<TriangleMesh> {
99 if mesh.is_empty() {
100 return Err(Error::InvalidData("Mesh is empty".to_string()));
101 }
102 if config.lambda <= 0.0 || config.lambda > 1.0 {
103 return Err(Error::InvalidData("lambda must be in (0, 1]".to_string()));
104 }
105 if config.iterations == 0 {
106 return Ok(mesh.clone());
107 }
108
109 let adj = build_adjacency(mesh);
110 let mut verts = mesh.vertices.clone();
111 for _ in 0..config.iterations {
112 verts = laplacian_step(&verts, &adj, config.lambda);
113 }
114 Ok(TriangleMesh::from_vertices_and_faces(
115 verts,
116 mesh.faces.clone(),
117 ))
118}
119
120#[derive(Debug, Clone)]
126pub struct TaubinSmoothingConfig {
127 pub iterations: usize,
129 pub lambda: f32,
131 pub mu: f32,
134}
135
136impl Default for TaubinSmoothingConfig {
137 fn default() -> Self {
138 Self {
139 iterations: 10,
140 lambda: 0.5,
141 mu: -0.53,
142 }
143 }
144}
145
146pub fn smooth_taubin(mesh: &TriangleMesh, config: &TaubinSmoothingConfig) -> Result<TriangleMesh> {
159 if mesh.is_empty() {
160 return Err(Error::InvalidData("Mesh is empty".to_string()));
161 }
162 if config.lambda <= 0.0 || config.lambda >= 1.0 {
163 return Err(Error::InvalidData("lambda must be in (0, 1)".to_string()));
164 }
165 if config.mu >= 0.0 {
166 return Err(Error::InvalidData("mu must be negative".to_string()));
167 }
168 if config.iterations == 0 {
169 return Ok(mesh.clone());
170 }
171
172 let adj = build_adjacency(mesh);
173 let mut verts = mesh.vertices.clone();
174 for _ in 0..config.iterations {
175 verts = laplacian_step(&verts, &adj, config.lambda);
176 verts = laplacian_step(&verts, &adj, config.mu);
177 }
178 Ok(TriangleMesh::from_vertices_and_faces(
179 verts,
180 mesh.faces.clone(),
181 ))
182}
183
184#[derive(Debug, Clone)]
190pub struct HcSmoothingConfig {
191 pub iterations: usize,
193 pub alpha: f32,
197 pub beta: f32,
200}
201
202impl Default for HcSmoothingConfig {
203 fn default() -> Self {
204 Self {
205 iterations: 10,
206 alpha: 0.0,
207 beta: 0.5,
208 }
209 }
210}
211
212pub fn smooth_hc(mesh: &TriangleMesh, config: &HcSmoothingConfig) -> Result<TriangleMesh> {
226 if mesh.is_empty() {
227 return Err(Error::InvalidData("Mesh is empty".to_string()));
228 }
229 if !(0.0..=1.0).contains(&config.alpha) {
230 return Err(Error::InvalidData("alpha must be in [0, 1]".to_string()));
231 }
232 if !(0.0..=1.0).contains(&config.beta) {
233 return Err(Error::InvalidData("beta must be in [0, 1]".to_string()));
234 }
235 if config.iterations == 0 {
236 return Ok(mesh.clone());
237 }
238
239 let adj = build_adjacency(mesh);
240 let original: Vec<Point3f> = mesh.vertices.clone();
241 let mut q: Vec<Point3f> = original.clone();
242
243 for _ in 0..config.iterations {
244 let q_bar: Vec<Point3f> = q
246 .iter()
247 .enumerate()
248 .map(|(i, &v)| {
249 let nbrs = &adj[i];
250 if nbrs.is_empty() {
251 return v;
252 }
253 let sum = nbrs
254 .iter()
255 .fold(Vector3f::zeros(), |acc, &j| acc + q[j].coords);
256 Point3f::from(sum / nbrs.len() as f32)
257 })
258 .collect();
259
260 let b: Vec<Vector3f> = (0..q.len())
263 .map(|i| {
264 let blend = original[i].coords * config.alpha + q[i].coords * (1.0 - config.alpha);
265 q_bar[i].coords - blend
266 })
267 .collect();
268
269 q = (0..q.len())
272 .map(|i| {
273 let nbrs = &adj[i];
274 let avg_b = if nbrs.is_empty() {
275 Vector3f::zeros()
276 } else {
277 nbrs.iter().fold(Vector3f::zeros(), |acc, &j| acc + b[j]) / nbrs.len() as f32
278 };
279 let correction = b[i] * config.beta + avg_b * (1.0 - config.beta);
280 Point3f::from(q_bar[i].coords - correction)
281 })
282 .collect();
283 }
284
285 Ok(TriangleMesh::from_vertices_and_faces(q, mesh.faces.clone()))
286}
287
288#[cfg(test)]
293mod tests {
294 use super::*;
295
296 fn make_spike_mesh() -> TriangleMesh {
299 let verts = vec![
300 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), ];
310 let faces = vec![
311 [0, 1, 3],
312 [1, 4, 3],
313 [1, 2, 4],
314 [2, 5, 4],
315 [3, 4, 6],
316 [4, 7, 6],
317 [4, 5, 7],
318 [5, 8, 7],
319 ];
320 TriangleMesh::from_vertices_and_faces(verts, faces)
321 }
322
323 fn centroid_z(mesh: &TriangleMesh) -> f32 {
325 mesh.vertices.iter().map(|v| v.z).sum::<f32>() / mesh.vertices.len() as f32
326 }
327
328 #[test]
331 fn test_laplacian_preserves_topology() {
332 let mesh = make_spike_mesh();
333 let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
334 assert_eq!(result.face_count(), mesh.face_count());
335 assert_eq!(result.vertex_count(), mesh.vertex_count());
336 assert_eq!(result.faces, mesh.faces);
337 }
338
339 #[test]
340 fn test_taubin_preserves_topology() {
341 let mesh = make_spike_mesh();
342 let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
343 assert_eq!(result.faces, mesh.faces);
344 }
345
346 #[test]
347 fn test_hc_preserves_topology() {
348 let mesh = make_spike_mesh();
349 let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
350 assert_eq!(result.faces, mesh.faces);
351 }
352
353 #[test]
356 fn test_laplacian_reduces_spike() {
357 let mesh = make_spike_mesh();
358 let before_z = mesh.vertices[4].z; let result = smooth_laplacian(&mesh, &LaplacianSmoothingConfig::default()).unwrap();
360 let after_z = result.vertices[4].z;
361 assert!(
362 after_z < before_z,
363 "spike z should decrease: before={before_z}, after={after_z}"
364 );
365 }
366
367 #[test]
368 fn test_taubin_reduces_spike() {
369 let mesh = make_spike_mesh();
370 let before_z = mesh.vertices[4].z;
371 let result = smooth_taubin(&mesh, &TaubinSmoothingConfig::default()).unwrap();
372 let after_z = result.vertices[4].z;
373 assert!(
374 after_z < before_z,
375 "Taubin should reduce spike: {before_z} → {after_z}"
376 );
377 }
378
379 #[test]
380 fn test_hc_reduces_spike() {
381 let mesh = make_spike_mesh();
382 let before_z = mesh.vertices[4].z;
383 let result = smooth_hc(&mesh, &HcSmoothingConfig::default()).unwrap();
384 let after_z = result.vertices[4].z;
385 assert!(
386 after_z < before_z,
387 "HC should reduce spike: {before_z} → {after_z}"
388 );
389 }
390
391 #[test]
394 fn test_taubin_less_shrinkage_than_laplacian() {
395 let verts = vec![
399 Point3f::new(0.0, 0.0, 0.0),
400 Point3f::new(1.0, 0.0, 0.0),
401 Point3f::new(1.0, 1.0, 0.0),
402 Point3f::new(0.0, 1.0, 0.0),
403 Point3f::new(0.0, 0.0, 1.0),
404 Point3f::new(1.0, 0.0, 1.0),
405 Point3f::new(1.0, 1.0, 1.0),
406 Point3f::new(0.0, 1.0, 1.0),
407 ];
408 let faces = vec![
409 [0, 2, 1],
410 [0, 3, 2],
411 [4, 5, 6],
412 [4, 6, 7],
413 [0, 1, 5],
414 [0, 5, 4],
415 [3, 7, 6],
416 [3, 6, 2],
417 [0, 4, 7],
418 [0, 7, 3],
419 [1, 2, 6],
420 [1, 6, 5],
421 ];
422 let mesh = TriangleMesh::from_vertices_and_faces(verts, faces);
423
424 let avg_dist = |m: &TriangleMesh| {
426 let c: Vector3f = m
427 .vertices
428 .iter()
429 .fold(Vector3f::zeros(), |acc, v| acc + v.coords)
430 / m.vertices.len() as f32;
431 m.vertices
432 .iter()
433 .map(|v| (v.coords - c).magnitude())
434 .sum::<f32>()
435 / m.vertices.len() as f32
436 };
437
438 let original_spread = avg_dist(&mesh);
439
440 let lap = smooth_laplacian(
441 &mesh,
442 &LaplacianSmoothingConfig {
443 iterations: 50,
444 lambda: 0.5,
445 },
446 )
447 .unwrap();
448 let tau = smooth_taubin(
449 &mesh,
450 &TaubinSmoothingConfig {
451 iterations: 50,
452 lambda: 0.5,
453 mu: -0.53,
454 },
455 )
456 .unwrap();
457
458 let lap_spread = avg_dist(&lap);
459 let tau_spread = avg_dist(&tau);
460
461 assert!(
463 tau_spread > lap_spread,
464 "Taubin should preserve spread better than Laplacian: \
465 original={original_spread:.4}, lap={lap_spread:.4}, tau={tau_spread:.4}"
466 );
467 }
468
469 #[test]
472 fn test_laplacian_zero_iterations() {
473 let mesh = make_spike_mesh();
474 let result = smooth_laplacian(
475 &mesh,
476 &LaplacianSmoothingConfig {
477 iterations: 0,
478 lambda: 0.5,
479 },
480 )
481 .unwrap();
482 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
483 assert!((a - b).magnitude() < 1e-6);
484 }
485 }
486
487 #[test]
488 fn test_taubin_zero_iterations() {
489 let mesh = make_spike_mesh();
490 let result = smooth_taubin(
491 &mesh,
492 &TaubinSmoothingConfig {
493 iterations: 0,
494 lambda: 0.5,
495 mu: -0.53,
496 },
497 )
498 .unwrap();
499 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
500 assert!((a - b).magnitude() < 1e-6);
501 }
502 }
503
504 #[test]
505 fn test_hc_zero_iterations() {
506 let mesh = make_spike_mesh();
507 let result = smooth_hc(
508 &mesh,
509 &HcSmoothingConfig {
510 iterations: 0,
511 alpha: 0.0,
512 beta: 0.5,
513 },
514 )
515 .unwrap();
516 for (a, b) in mesh.vertices.iter().zip(result.vertices.iter()) {
517 assert!((a - b).magnitude() < 1e-6);
518 }
519 }
520
521 #[test]
524 fn test_laplacian_empty_mesh() {
525 let empty = TriangleMesh::new();
526 assert!(smooth_laplacian(&empty, &LaplacianSmoothingConfig::default()).is_err());
527 }
528
529 #[test]
530 fn test_laplacian_invalid_lambda() {
531 let mesh = make_spike_mesh();
532 assert!(smooth_laplacian(
533 &mesh,
534 &LaplacianSmoothingConfig {
535 iterations: 1,
536 lambda: 0.0
537 }
538 )
539 .is_err());
540 assert!(smooth_laplacian(
541 &mesh,
542 &LaplacianSmoothingConfig {
543 iterations: 1,
544 lambda: 1.5
545 }
546 )
547 .is_err());
548 }
549
550 #[test]
551 fn test_taubin_invalid_params() {
552 let mesh = make_spike_mesh();
553 assert!(smooth_taubin(
555 &mesh,
556 &TaubinSmoothingConfig {
557 iterations: 1,
558 lambda: 0.0,
559 mu: -0.53
560 }
561 )
562 .is_err());
563 assert!(smooth_taubin(
565 &mesh,
566 &TaubinSmoothingConfig {
567 iterations: 1,
568 lambda: 0.5,
569 mu: 0.1
570 }
571 )
572 .is_err());
573 }
574
575 #[test]
576 fn test_hc_invalid_params() {
577 let mesh = make_spike_mesh();
578 assert!(smooth_hc(
579 &mesh,
580 &HcSmoothingConfig {
581 iterations: 1,
582 alpha: -0.1,
583 beta: 0.5
584 }
585 )
586 .is_err());
587 assert!(smooth_hc(
588 &mesh,
589 &HcSmoothingConfig {
590 iterations: 1,
591 alpha: 0.5,
592 beta: 1.5
593 }
594 )
595 .is_err());
596 }
597
598 #[test]
599 fn test_taubin_empty_mesh() {
600 let empty = TriangleMesh::new();
601 assert!(smooth_taubin(&empty, &TaubinSmoothingConfig::default()).is_err());
602 }
603
604 #[test]
605 fn test_hc_empty_mesh() {
606 let empty = TriangleMesh::new();
607 assert!(smooth_hc(&empty, &HcSmoothingConfig::default()).is_err());
608 }
609
610 #[test]
613 fn test_laplacian_more_iterations_smoother() {
614 let mesh = make_spike_mesh();
615 let r1 = smooth_laplacian(
616 &mesh,
617 &LaplacianSmoothingConfig {
618 iterations: 1,
619 lambda: 0.5,
620 },
621 )
622 .unwrap();
623 let r5 = smooth_laplacian(
624 &mesh,
625 &LaplacianSmoothingConfig {
626 iterations: 5,
627 lambda: 0.5,
628 },
629 )
630 .unwrap();
631 assert!(r5.vertices[4].z < r1.vertices[4].z);
633 }
634}