axiolid_refine/
smooth.rs

1//! Laplacian smoothing with an explicitly fixed boundary.
2//!
3//! Smoothing moves every vertex toward the average of its neighbours. That
4//! is unconditionally destructive at a boundary: an open edge has neighbours
5//! on one side only, so the average pulls it inward and the mesh shrinks
6//! away from its own border.
7//!
8//! This module therefore treats the boundary as data, not as a special case
9//! to be handled later. `fix_boundary` is on by default and a fixed vertex
10//! is left BIT-IDENTICAL, not merely close: a caller comparing a smoothed
11//! border against the original is asking an exactness question and deserves
12//! an exact answer.
13
14use std::collections::BTreeSet;
15
16use axiolid_core::{Point3, Scalar};
17use axiolid_mesh::{EdgeAdjacency, TriMesh};
18
19use crate::{RefineError, SmoothReport};
20
21/// How strongly each pass pulls a vertex toward its neighbourhood average.
22///
23/// A factor of 1 replaces the vertex with the average outright, which is
24/// stable for a single pass but oscillates across several on an irregular
25/// mesh. Values below 1 damp that.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct SmoothOptions {
28    /// Relaxation factor in `(0, 1]`.
29    pub factor: Scalar,
30    /// Number of passes.
31    pub passes: u32,
32    /// Whether boundary vertices stay exactly where they are.
33    pub fix_boundary: bool,
34}
35
36impl Default for SmoothOptions {
37    fn default() -> Self {
38        Self {
39            factor: 0.5,
40            passes: 1,
41            fix_boundary: true,
42        }
43    }
44}
45
46/// Laplacian-smooth a triangle mesh.
47///
48/// # Errors
49///
50/// Refuses a ragged index buffer, an out-of-range index, and a relaxation
51/// factor outside `(0, 1]`.
52pub fn smooth(
53    mesh: &TriMesh,
54    options: SmoothOptions,
55) -> Result<(TriMesh, SmoothReport), RefineError> {
56    if mesh.indices.len() % 3 != 0 {
57        return Err(RefineError::RaggedIndices(mesh.indices.len()));
58    }
59    let vertex_count = mesh.positions.len();
60    for (triangle, chunk) in mesh.indices.chunks_exact(3).enumerate() {
61        for &index in chunk {
62            if index as usize >= vertex_count {
63                return Err(RefineError::IndexOutOfRange(triangle, index));
64            }
65        }
66    }
67    if !(options.factor > 0.0 && options.factor <= 1.0) {
68        return Err(RefineError::InvalidTarget(options.factor));
69    }
70
71    // Adjacency is derived once: connectivity never changes here, only
72    // positions, so rebuilding it per pass would be wasted work. Boundary
73    // vertices are pinned, since moving them would shrink the surface.
74    let adjacency = EdgeAdjacency::build(mesh);
75    let boundary: BTreeSet<u32> = adjacency.boundary_vertices().into_iter().collect();
76    let neighbours = adjacency.vertex_neighbours();
77
78    let mut positions = mesh.positions.clone();
79    let mut moved = 0usize;
80    let mut max_movement: Scalar = 0.0;
81
82    for _ in 0..options.passes {
83        let source = positions.clone();
84        for (index, position) in positions.iter_mut().enumerate() {
85            let vertex = index as u32;
86            if options.fix_boundary && boundary.contains(&vertex) {
87                continue;
88            }
89            let adjacent = &neighbours[index];
90            if adjacent.is_empty() {
91                continue;
92            }
93            let mut sum = Point3::ZERO;
94            for &other in adjacent {
95                sum += source[other as usize];
96            }
97            let average = sum / (adjacent.len() as Scalar);
98            let target = source[index] + (average - source[index]) * options.factor;
99            let movement = (target - source[index]).length();
100            if movement > 0.0 {
101                moved += 1;
102                max_movement = max_movement.max(movement);
103            }
104            *position = target;
105        }
106    }
107
108    let attribute_fates = crate::carry_attributes(mesh);
109    let out = TriMesh {
110        positions,
111        indices: mesh.indices.clone(),
112        normals: None,
113        attributes: Vec::new(),
114    };
115    let report = SmoothReport {
116        vertices_moved: moved,
117        boundary_vertices: boundary.len(),
118        max_movement,
119        attribute_fates,
120    };
121    Ok((out, report))
122}