mesh_repair/
scan.rs

1//! Scan processing and cleanup utilities.
2//!
3//! This module provides tools for processing raw 3D scan data, including
4//! noise reduction, artifact removal, and automated cleanup pipelines.
5//!
6//! # Use Cases
7//!
8//! - Cleaning up body scans from depth cameras
9//! - Processing object scans from structured light scanners
10//! - Preparing point cloud data for surface reconstruction
11//!
12//! # Example
13//!
14//! ```
15//! use mesh_repair::{Mesh, Vertex};
16//! use mesh_repair::scan::{ScanCleanupParams, cleanup_scan, DenoiseParams};
17//!
18//! // Create a mesh (typically loaded from a scanner)
19//! let mut mesh = Mesh::new();
20//! mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
21//! mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
22//! mesh.vertices.push(Vertex::from_coords(0.5, 1.0, 0.0));
23//! mesh.faces.push([0, 1, 2]);
24//!
25//! // Clean up the scan with default parameters
26//! let params = ScanCleanupParams::default();
27//! let result = cleanup_scan(&mesh, &params);
28//! ```
29
30use crate::{Mesh, Vertex};
31use nalgebra::{Point3, Vector3};
32use std::collections::{HashMap, HashSet};
33
34/// Parameters for scan cleanup operations.
35#[derive(Debug, Clone)]
36pub struct ScanCleanupParams {
37    /// Remove isolated vertices/faces.
38    pub remove_isolated: bool,
39
40    /// Minimum component size to keep (in faces).
41    pub min_component_size: usize,
42
43    /// Remove spike artifacts.
44    pub remove_spikes: bool,
45
46    /// Spike detection threshold (standard deviations from mean edge length).
47    pub spike_threshold: f64,
48
49    /// Fill small holes.
50    pub fill_small_holes: bool,
51
52    /// Maximum hole size to fill (in edges).
53    pub max_hole_size: usize,
54
55    /// Apply smoothing.
56    pub smooth: bool,
57
58    /// Smoothing iterations.
59    pub smooth_iterations: usize,
60
61    /// Smoothing strength (0-1).
62    pub smooth_strength: f64,
63
64    /// Preserve features during smoothing.
65    pub preserve_features: bool,
66
67    /// Feature preservation threshold (dihedral angle in radians).
68    pub feature_angle_threshold: f64,
69}
70
71impl Default for ScanCleanupParams {
72    fn default() -> Self {
73        Self {
74            remove_isolated: true,
75            min_component_size: 100,
76            remove_spikes: true,
77            spike_threshold: 3.0,
78            fill_small_holes: true,
79            max_hole_size: 50,
80            smooth: true,
81            smooth_iterations: 2,
82            smooth_strength: 0.5,
83            preserve_features: true,
84            feature_angle_threshold: 0.5, // ~30 degrees
85        }
86    }
87}
88
89impl ScanCleanupParams {
90    /// Parameters optimized for body scans.
91    pub fn for_body_scan() -> Self {
92        Self {
93            remove_isolated: true,
94            min_component_size: 500,
95            remove_spikes: true,
96            spike_threshold: 2.5,
97            fill_small_holes: true,
98            max_hole_size: 100,
99            smooth: true,
100            smooth_iterations: 3,
101            smooth_strength: 0.4,
102            preserve_features: true,
103            feature_angle_threshold: 0.7, // ~40 degrees
104        }
105    }
106
107    /// Parameters optimized for object scans.
108    pub fn for_object_scan() -> Self {
109        Self {
110            remove_isolated: true,
111            min_component_size: 50,
112            remove_spikes: true,
113            spike_threshold: 3.5,
114            fill_small_holes: true,
115            max_hole_size: 30,
116            smooth: true,
117            smooth_iterations: 2,
118            smooth_strength: 0.3,
119            preserve_features: true,
120            feature_angle_threshold: 0.4, // ~23 degrees, preserve sharper edges
121        }
122    }
123
124    /// Parameters for minimal cleanup (preserve detail).
125    pub fn minimal() -> Self {
126        Self {
127            remove_isolated: true,
128            min_component_size: 10,
129            remove_spikes: false,
130            spike_threshold: 4.0,
131            fill_small_holes: false,
132            max_hole_size: 10,
133            smooth: false,
134            smooth_iterations: 1,
135            smooth_strength: 0.2,
136            preserve_features: true,
137            feature_angle_threshold: 0.3,
138        }
139    }
140
141    /// Parameters for aggressive cleanup.
142    pub fn aggressive() -> Self {
143        Self {
144            remove_isolated: true,
145            min_component_size: 1000,
146            remove_spikes: true,
147            spike_threshold: 2.0,
148            fill_small_holes: true,
149            max_hole_size: 200,
150            smooth: true,
151            smooth_iterations: 5,
152            smooth_strength: 0.7,
153            preserve_features: false,
154            feature_angle_threshold: 0.5,
155        }
156    }
157}
158
159/// Result of scan cleanup operation.
160#[derive(Debug)]
161pub struct ScanCleanupResult {
162    /// Cleaned mesh.
163    pub mesh: Mesh,
164
165    /// Number of isolated components removed.
166    pub components_removed: usize,
167
168    /// Number of spike vertices removed.
169    pub spikes_removed: usize,
170
171    /// Number of holes filled.
172    pub holes_filled: usize,
173
174    /// Smoothing applied.
175    pub smoothing_applied: bool,
176}
177
178/// Clean up a scan mesh using the specified parameters.
179pub fn cleanup_scan(mesh: &Mesh, params: &ScanCleanupParams) -> ScanCleanupResult {
180    let mut result_mesh = mesh.clone();
181    let mut components_removed = 0;
182    let mut spikes_removed = 0;
183    let mut holes_filled = 0;
184
185    // Step 1: Remove isolated components
186    if params.remove_isolated {
187        components_removed = remove_small_components(&mut result_mesh, params.min_component_size);
188    }
189
190    // Step 2: Remove spike artifacts
191    if params.remove_spikes {
192        spikes_removed = remove_spikes(&mut result_mesh, params.spike_threshold);
193    }
194
195    // Step 3: Fill small holes
196    if params.fill_small_holes {
197        holes_filled = fill_small_holes(&mut result_mesh, params.max_hole_size);
198    }
199
200    // Step 4: Smooth
201    if params.smooth {
202        if params.preserve_features {
203            smooth_preserve_features(
204                &mut result_mesh,
205                params.smooth_iterations,
206                params.smooth_strength,
207                params.feature_angle_threshold,
208            );
209        } else {
210            smooth_laplacian(
211                &mut result_mesh,
212                params.smooth_iterations,
213                params.smooth_strength,
214            );
215        }
216    }
217
218    ScanCleanupResult {
219        mesh: result_mesh,
220        components_removed,
221        spikes_removed,
222        holes_filled,
223        smoothing_applied: params.smooth,
224    }
225}
226
227/// Parameters for mesh denoising.
228#[derive(Debug, Clone)]
229pub struct DenoiseParams {
230    /// Denoising method.
231    pub method: DenoiseMethod,
232
233    /// Number of iterations.
234    pub iterations: usize,
235
236    /// Strength of denoising (0-1).
237    pub strength: f64,
238
239    /// Preserve sharp features.
240    pub preserve_features: bool,
241
242    /// Feature angle threshold (radians).
243    pub feature_threshold: f64,
244}
245
246impl Default for DenoiseParams {
247    fn default() -> Self {
248        Self {
249            method: DenoiseMethod::Bilateral,
250            iterations: 3,
251            strength: 0.5,
252            preserve_features: true,
253            feature_threshold: 0.5,
254        }
255    }
256}
257
258/// Denoising method.
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
260pub enum DenoiseMethod {
261    /// Laplacian smoothing.
262    Laplacian,
263
264    /// Bilateral filtering (edge-preserving).
265    Bilateral,
266
267    /// Taubin smoothing (reduces shrinkage).
268    Taubin,
269
270    /// Mean curvature flow.
271    MeanCurvatureFlow,
272}
273
274/// Result of denoising operation.
275#[derive(Debug)]
276pub struct DenoiseResult {
277    /// Denoised mesh.
278    pub mesh: Mesh,
279
280    /// Average displacement per vertex.
281    pub average_displacement: f64,
282
283    /// Maximum displacement.
284    pub max_displacement: f64,
285
286    /// Number of iterations performed.
287    pub iterations_performed: usize,
288}
289
290/// Denoise a mesh using the specified parameters.
291pub fn denoise_mesh(mesh: &Mesh, params: &DenoiseParams) -> DenoiseResult {
292    let mut result = mesh.clone();
293    let mut total_displacement = 0.0;
294    let mut max_displacement = 0.0f64;
295
296    for _ in 0..params.iterations {
297        let displacements = match params.method {
298            DenoiseMethod::Laplacian => compute_laplacian_displacements(&result, params.strength),
299            DenoiseMethod::Bilateral => {
300                compute_bilateral_displacements(&result, params.strength, params.feature_threshold)
301            }
302            DenoiseMethod::Taubin => compute_taubin_displacements(&result, params.strength),
303            DenoiseMethod::MeanCurvatureFlow => compute_mcf_displacements(&result, params.strength),
304        };
305
306        // Apply displacements
307        for (i, disp) in displacements.iter().enumerate() {
308            if params.preserve_features && is_feature_vertex(&result, i, params.feature_threshold) {
309                continue;
310            }
311
312            let disp_mag = disp.norm();
313            total_displacement += disp_mag;
314            max_displacement = max_displacement.max(disp_mag);
315            result.vertices[i].position += disp;
316        }
317    }
318
319    let vertex_count = result.vertices.len().max(1);
320    DenoiseResult {
321        mesh: result,
322        average_displacement: total_displacement / (vertex_count * params.iterations) as f64,
323        max_displacement,
324        iterations_performed: params.iterations,
325    }
326}
327
328/// Parameters for advanced hole filling.
329#[derive(Debug, Clone)]
330pub struct HoleFillParams {
331    /// Filling strategy.
332    pub strategy: HoleFillStrategy,
333
334    /// Maximum hole size to fill (edges).
335    pub max_size: usize,
336
337    /// Smoothing iterations after filling.
338    pub smooth_iterations: usize,
339}
340
341impl Default for HoleFillParams {
342    fn default() -> Self {
343        Self {
344            strategy: HoleFillStrategy::Smooth,
345            max_size: 100,
346            smooth_iterations: 2,
347        }
348    }
349}
350
351/// Hole filling strategy.
352#[derive(Debug, Clone, Copy, PartialEq, Eq)]
353pub enum HoleFillStrategy {
354    /// Simple triangulation (flat fill).
355    Planar,
356
357    /// Smooth interpolation from boundary.
358    Smooth,
359
360    /// Curvature-based fill matching surrounding surface.
361    CurvatureBased,
362
363    /// Minimal area fill.
364    MinimalArea,
365}
366
367/// Result of advanced hole filling.
368#[derive(Debug)]
369pub struct HoleFillResult {
370    /// Mesh with holes filled.
371    pub mesh: Mesh,
372
373    /// Number of holes filled.
374    pub holes_filled: usize,
375
376    /// Number of faces added.
377    pub faces_added: usize,
378
379    /// Sizes of filled holes (in edges).
380    pub hole_sizes: Vec<usize>,
381}
382
383/// Fill holes using advanced strategies.
384pub fn fill_holes_advanced(mesh: &Mesh, params: &HoleFillParams) -> HoleFillResult {
385    let mut result = mesh.clone();
386    let holes = detect_boundary_loops(&result);
387
388    let mut holes_filled = 0;
389    let mut faces_added = 0;
390    let mut hole_sizes = Vec::new();
391
392    for hole in holes {
393        if hole.len() > params.max_size {
394            continue;
395        }
396
397        let added = match params.strategy {
398            HoleFillStrategy::Planar => fill_hole_planar(&mut result, &hole),
399            HoleFillStrategy::Smooth => {
400                fill_hole_smooth(&mut result, &hole, params.smooth_iterations)
401            }
402            HoleFillStrategy::CurvatureBased => fill_hole_curvature(&mut result, &hole),
403            HoleFillStrategy::MinimalArea => fill_hole_minimal_area(&mut result, &hole),
404        };
405
406        if added > 0 {
407            holes_filled += 1;
408            faces_added += added;
409            hole_sizes.push(hole.len());
410        }
411    }
412
413    HoleFillResult {
414        mesh: result,
415        holes_filled,
416        faces_added,
417        hole_sizes,
418    }
419}
420
421/// Statistical outlier removal parameters.
422#[derive(Debug, Clone)]
423pub struct OutlierRemovalParams {
424    /// Number of neighbors to consider.
425    pub k_neighbors: usize,
426
427    /// Standard deviation threshold.
428    pub std_dev_threshold: f64,
429}
430
431impl Default for OutlierRemovalParams {
432    fn default() -> Self {
433        Self {
434            k_neighbors: 10,
435            std_dev_threshold: 2.0,
436        }
437    }
438}
439
440/// Remove statistical outliers from mesh.
441pub fn remove_outliers(mesh: &Mesh, params: &OutlierRemovalParams) -> Mesh {
442    if mesh.vertices.len() < params.k_neighbors {
443        return mesh.clone();
444    }
445
446    // Compute average distance to k neighbors for each vertex
447    let mut avg_distances: Vec<f64> = Vec::with_capacity(mesh.vertices.len());
448
449    for (i, v) in mesh.vertices.iter().enumerate() {
450        let mut distances: Vec<f64> = mesh
451            .vertices
452            .iter()
453            .enumerate()
454            .filter(|(j, _)| *j != i)
455            .map(|(_, other)| (v.position - other.position).norm())
456            .collect();
457
458        distances.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
459
460        let k = params.k_neighbors.min(distances.len());
461        let avg: f64 = distances[..k].iter().sum::<f64>() / k as f64;
462        avg_distances.push(avg);
463    }
464
465    // Compute mean and std dev
466    let mean: f64 = avg_distances.iter().sum::<f64>() / avg_distances.len() as f64;
467    let variance: f64 = avg_distances
468        .iter()
469        .map(|d| (d - mean).powi(2))
470        .sum::<f64>()
471        / avg_distances.len() as f64;
472    let std_dev = variance.sqrt();
473
474    let threshold = mean + params.std_dev_threshold * std_dev;
475
476    // Mark vertices to keep
477    let keep: HashSet<u32> = avg_distances
478        .iter()
479        .enumerate()
480        .filter(|(_, d)| **d <= threshold)
481        .map(|(i, _)| i as u32)
482        .collect();
483
484    // Build new mesh with only kept vertices
485    let mut result = Mesh::new();
486    let mut vertex_map: HashMap<u32, u32> = HashMap::new();
487
488    for (i, v) in mesh.vertices.iter().enumerate() {
489        if keep.contains(&(i as u32)) {
490            vertex_map.insert(i as u32, result.vertices.len() as u32);
491            result.vertices.push(v.clone());
492        }
493    }
494
495    // Keep faces where all vertices are kept
496    for face in &mesh.faces {
497        if let (Some(&i0), Some(&i1), Some(&i2)) = (
498            vertex_map.get(&face[0]),
499            vertex_map.get(&face[1]),
500            vertex_map.get(&face[2]),
501        ) {
502            result.faces.push([i0, i1, i2]);
503        }
504    }
505
506    result
507}
508
509// ============================================================================
510// Internal helper functions
511// ============================================================================
512
513/// Remove small connected components.
514fn remove_small_components(mesh: &mut Mesh, min_size: usize) -> usize {
515    let components = find_connected_components(mesh);
516    let mut removed = 0;
517
518    // Find faces to keep
519    let mut faces_to_keep: HashSet<usize> = HashSet::new();
520    for component in &components {
521        if component.len() >= min_size {
522            faces_to_keep.extend(component.iter());
523        } else {
524            removed += 1;
525        }
526    }
527
528    if removed == 0 {
529        return 0;
530    }
531
532    // Rebuild mesh with only kept faces
533    let new_faces: Vec<[u32; 3]> = mesh
534        .faces
535        .iter()
536        .enumerate()
537        .filter(|(i, _)| faces_to_keep.contains(i))
538        .map(|(_, f)| *f)
539        .collect();
540
541    mesh.faces = new_faces;
542
543    // Remove unreferenced vertices
544    let mut referenced: HashSet<u32> = HashSet::new();
545    for face in &mesh.faces {
546        referenced.insert(face[0]);
547        referenced.insert(face[1]);
548        referenced.insert(face[2]);
549    }
550
551    let mut vertex_map: HashMap<u32, u32> = HashMap::new();
552    let mut new_vertices = Vec::new();
553
554    for (i, v) in mesh.vertices.iter().enumerate() {
555        if referenced.contains(&(i as u32)) {
556            vertex_map.insert(i as u32, new_vertices.len() as u32);
557            new_vertices.push(v.clone());
558        }
559    }
560
561    mesh.vertices = new_vertices;
562
563    for face in &mut mesh.faces {
564        face[0] = vertex_map[&face[0]];
565        face[1] = vertex_map[&face[1]];
566        face[2] = vertex_map[&face[2]];
567    }
568
569    removed
570}
571
572/// Find connected components (returns face indices per component).
573fn find_connected_components(mesh: &Mesh) -> Vec<Vec<usize>> {
574    let mut visited: HashSet<usize> = HashSet::new();
575    let mut components: Vec<Vec<usize>> = Vec::new();
576
577    // Build face adjacency
578    let mut edge_to_faces: HashMap<(u32, u32), Vec<usize>> = HashMap::new();
579    for (fi, face) in mesh.faces.iter().enumerate() {
580        for i in 0..3 {
581            let v0 = face[i];
582            let v1 = face[(i + 1) % 3];
583            let edge = if v0 < v1 { (v0, v1) } else { (v1, v0) };
584            edge_to_faces.entry(edge).or_default().push(fi);
585        }
586    }
587
588    for fi in 0..mesh.faces.len() {
589        if visited.contains(&fi) {
590            continue;
591        }
592
593        let mut component = Vec::new();
594        let mut stack = vec![fi];
595
596        while let Some(current) = stack.pop() {
597            if visited.contains(&current) {
598                continue;
599            }
600            visited.insert(current);
601            component.push(current);
602
603            let face = &mesh.faces[current];
604            for i in 0..3 {
605                let v0 = face[i];
606                let v1 = face[(i + 1) % 3];
607                let edge = if v0 < v1 { (v0, v1) } else { (v1, v0) };
608
609                if let Some(neighbors) = edge_to_faces.get(&edge) {
610                    for &neighbor in neighbors {
611                        if !visited.contains(&neighbor) {
612                            stack.push(neighbor);
613                        }
614                    }
615                }
616            }
617        }
618
619        components.push(component);
620    }
621
622    components
623}
624
625/// Remove spike artifacts (vertices with abnormal edge lengths).
626fn remove_spikes(mesh: &mut Mesh, threshold: f64) -> usize {
627    if mesh.faces.is_empty() {
628        return 0;
629    }
630
631    // Compute edge lengths
632    let mut edge_lengths: Vec<f64> = Vec::new();
633    for face in &mesh.faces {
634        for i in 0..3 {
635            let v0 = &mesh.vertices[face[i] as usize].position;
636            let v1 = &mesh.vertices[face[(i + 1) % 3] as usize].position;
637            edge_lengths.push((v1 - v0).norm());
638        }
639    }
640
641    if edge_lengths.is_empty() {
642        return 0;
643    }
644
645    // Compute statistics
646    let mean: f64 = edge_lengths.iter().sum::<f64>() / edge_lengths.len() as f64;
647    let variance: f64 =
648        edge_lengths.iter().map(|l| (l - mean).powi(2)).sum::<f64>() / edge_lengths.len() as f64;
649    let std_dev = variance.sqrt();
650
651    let max_length = mean + threshold * std_dev;
652
653    // Find spike vertices (connected to abnormally long edges)
654    let mut spike_vertices: HashSet<u32> = HashSet::new();
655    for face in &mesh.faces {
656        for i in 0..3 {
657            let v0 = &mesh.vertices[face[i] as usize].position;
658            let v1 = &mesh.vertices[face[(i + 1) % 3] as usize].position;
659            if (v1 - v0).norm() > max_length {
660                spike_vertices.insert(face[i]);
661                spike_vertices.insert(face[(i + 1) % 3]);
662            }
663        }
664    }
665
666    if spike_vertices.is_empty() {
667        return 0;
668    }
669
670    // Remove faces containing spike vertices
671    let original_face_count = mesh.faces.len();
672    mesh.faces.retain(|face| {
673        !spike_vertices.contains(&face[0])
674            && !spike_vertices.contains(&face[1])
675            && !spike_vertices.contains(&face[2])
676    });
677
678    original_face_count - mesh.faces.len()
679}
680
681/// Fill small holes using simple ear-clipping triangulation.
682fn fill_small_holes(mesh: &mut Mesh, max_size: usize) -> usize {
683    let holes = detect_boundary_loops(mesh);
684    let mut filled = 0;
685
686    for hole in holes {
687        if hole.len() <= max_size && hole.len() >= 3 {
688            fill_hole_planar(mesh, &hole);
689            filled += 1;
690        }
691    }
692
693    filled
694}
695
696/// Detect boundary loops (holes).
697fn detect_boundary_loops(mesh: &Mesh) -> Vec<Vec<u32>> {
698    // Find boundary edges (edges with only one adjacent face)
699    let mut edge_count: HashMap<(u32, u32), usize> = HashMap::new();
700
701    for face in &mesh.faces {
702        for i in 0..3 {
703            let v0 = face[i];
704            let v1 = face[(i + 1) % 3];
705            let edge = if v0 < v1 { (v0, v1) } else { (v1, v0) };
706            *edge_count.entry(edge).or_insert(0) += 1;
707        }
708    }
709
710    let boundary_edges: HashSet<(u32, u32)> = edge_count
711        .into_iter()
712        .filter(|(_, count)| *count == 1)
713        .map(|(edge, _)| edge)
714        .collect();
715
716    if boundary_edges.is_empty() {
717        return Vec::new();
718    }
719
720    // Build adjacency for boundary vertices
721    let mut boundary_adj: HashMap<u32, Vec<u32>> = HashMap::new();
722    for (v0, v1) in &boundary_edges {
723        boundary_adj.entry(*v0).or_default().push(*v1);
724        boundary_adj.entry(*v1).or_default().push(*v0);
725    }
726
727    // Trace loops
728    let mut visited_edges: HashSet<(u32, u32)> = HashSet::new();
729    let mut loops: Vec<Vec<u32>> = Vec::new();
730
731    for (&start_v, neighbors) in &boundary_adj {
732        for &next_v in neighbors {
733            let edge = if start_v < next_v {
734                (start_v, next_v)
735            } else {
736                (next_v, start_v)
737            };
738
739            if visited_edges.contains(&edge) {
740                continue;
741            }
742
743            let mut current_loop = vec![start_v];
744            let mut prev = start_v;
745            let mut current = next_v;
746
747            loop {
748                let edge = if prev < current {
749                    (prev, current)
750                } else {
751                    (current, prev)
752                };
753                visited_edges.insert(edge);
754                current_loop.push(current);
755
756                if current == start_v {
757                    current_loop.pop(); // Remove duplicate start
758                    break;
759                }
760
761                // Find next vertex
762                if let Some(adj) = boundary_adj.get(&current) {
763                    let next = adj.iter().find(|&&v| v != prev);
764                    if let Some(&n) = next {
765                        prev = current;
766                        current = n;
767                    } else {
768                        break;
769                    }
770                } else {
771                    break;
772                }
773            }
774
775            if current_loop.len() >= 3 {
776                loops.push(current_loop);
777            }
778        }
779    }
780
781    loops
782}
783
784/// Fill a hole with planar triangulation.
785fn fill_hole_planar(mesh: &mut Mesh, hole: &[u32]) -> usize {
786    if hole.len() < 3 {
787        return 0;
788    }
789
790    // Simple fan triangulation from first vertex
791    let first = hole[0];
792    let mut faces_added = 0;
793
794    for i in 1..hole.len() - 1 {
795        mesh.faces.push([first, hole[i], hole[i + 1]]);
796        faces_added += 1;
797    }
798
799    faces_added
800}
801
802/// Fill a hole with smooth interpolation.
803fn fill_hole_smooth(mesh: &mut Mesh, hole: &[u32], smooth_iterations: usize) -> usize {
804    let faces_added = fill_hole_planar(mesh, hole);
805
806    if faces_added > 0 && smooth_iterations > 0 {
807        // Get vertices that were affected (the hole boundary)
808        let affected: HashSet<u32> = hole.iter().copied().collect();
809
810        for _ in 0..smooth_iterations {
811            smooth_vertices_subset(mesh, &affected, 0.5);
812        }
813    }
814
815    faces_added
816}
817
818/// Fill a hole matching surrounding curvature.
819fn fill_hole_curvature(mesh: &mut Mesh, hole: &[u32]) -> usize {
820    // First do basic fill, then adjust based on surrounding curvature
821    let faces_added = fill_hole_planar(mesh, hole);
822
823    if faces_added == 0 || hole.is_empty() {
824        return 0;
825    }
826
827    // Compute centroid
828    let centroid = compute_hole_centroid(mesh, hole);
829
830    // Estimate surface normal at hole boundary
831    let _normal = estimate_boundary_normal(mesh, hole);
832
833    // Add a center vertex and re-triangulate if the hole is large enough
834    if hole.len() > 4 {
835        // Remove the simple fan triangulation
836        let start_face_idx = mesh.faces.len() - faces_added;
837        mesh.faces.truncate(start_face_idx);
838
839        // Add center vertex
840        let center_idx = mesh.vertices.len() as u32;
841        mesh.vertices.push(Vertex::new(centroid));
842
843        // Create fan from center
844        for i in 0..hole.len() {
845            let v0 = hole[i];
846            let v1 = hole[(i + 1) % hole.len()];
847            mesh.faces.push([center_idx, v0, v1]);
848        }
849
850        return hole.len();
851    }
852
853    faces_added
854}
855
856/// Fill a hole with minimal area triangulation.
857fn fill_hole_minimal_area(mesh: &mut Mesh, hole: &[u32]) -> usize {
858    // For small holes, simple fan is often minimal area
859    // For larger holes, we'd use dynamic programming
860    fill_hole_planar(mesh, hole)
861}
862
863/// Laplacian smoothing.
864fn smooth_laplacian(mesh: &mut Mesh, iterations: usize, strength: f64) {
865    for _ in 0..iterations {
866        let displacements = compute_laplacian_displacements(mesh, strength);
867        for (i, disp) in displacements.iter().enumerate() {
868            mesh.vertices[i].position += disp;
869        }
870    }
871}
872
873/// Feature-preserving smoothing.
874fn smooth_preserve_features(
875    mesh: &mut Mesh,
876    iterations: usize,
877    strength: f64,
878    feature_threshold: f64,
879) {
880    for _ in 0..iterations {
881        let displacements = compute_bilateral_displacements(mesh, strength, feature_threshold);
882        for (i, disp) in displacements.iter().enumerate() {
883            if !is_feature_vertex(mesh, i, feature_threshold) {
884                mesh.vertices[i].position += disp;
885            }
886        }
887    }
888}
889
890/// Smooth only a subset of vertices.
891fn smooth_vertices_subset(mesh: &mut Mesh, vertices: &HashSet<u32>, strength: f64) {
892    let adjacency = build_vertex_adjacency(mesh);
893    let mut displacements: HashMap<u32, Vector3<f64>> = HashMap::new();
894
895    for &vi in vertices {
896        if let Some(neighbors) = adjacency.get(&vi) {
897            if neighbors.is_empty() {
898                continue;
899            }
900
901            let current = mesh.vertices[vi as usize].position;
902            let centroid: Point3<f64> = Point3::from(
903                neighbors
904                    .iter()
905                    .map(|&ni| mesh.vertices[ni as usize].position.coords)
906                    .sum::<Vector3<f64>>()
907                    / neighbors.len() as f64,
908            );
909
910            displacements.insert(vi, (centroid - current) * strength);
911        }
912    }
913
914    for (vi, disp) in displacements {
915        mesh.vertices[vi as usize].position += disp;
916    }
917}
918
919/// Build vertex adjacency map.
920fn build_vertex_adjacency(mesh: &Mesh) -> HashMap<u32, Vec<u32>> {
921    let mut adjacency: HashMap<u32, Vec<u32>> = HashMap::new();
922
923    for face in &mesh.faces {
924        for i in 0..3 {
925            let v0 = face[i];
926            let v1 = face[(i + 1) % 3];
927            adjacency.entry(v0).or_default().push(v1);
928            adjacency.entry(v1).or_default().push(v0);
929        }
930    }
931
932    // Remove duplicates
933    for neighbors in adjacency.values_mut() {
934        neighbors.sort();
935        neighbors.dedup();
936    }
937
938    adjacency
939}
940
941/// Compute Laplacian displacements.
942fn compute_laplacian_displacements(mesh: &Mesh, strength: f64) -> Vec<Vector3<f64>> {
943    let adjacency = build_vertex_adjacency(mesh);
944    let mut displacements = vec![Vector3::zeros(); mesh.vertices.len()];
945
946    for (vi, v) in mesh.vertices.iter().enumerate() {
947        if let Some(neighbors) = adjacency.get(&(vi as u32)) {
948            if neighbors.is_empty() {
949                continue;
950            }
951
952            let centroid: Point3<f64> = Point3::from(
953                neighbors
954                    .iter()
955                    .map(|&ni| mesh.vertices[ni as usize].position.coords)
956                    .sum::<Vector3<f64>>()
957                    / neighbors.len() as f64,
958            );
959
960            displacements[vi] = (centroid - v.position) * strength;
961        }
962    }
963
964    displacements
965}
966
967/// Compute bilateral (edge-preserving) displacements.
968fn compute_bilateral_displacements(
969    mesh: &Mesh,
970    strength: f64,
971    feature_threshold: f64,
972) -> Vec<Vector3<f64>> {
973    let adjacency = build_vertex_adjacency(mesh);
974    let normals = compute_vertex_normals(mesh);
975    let mut displacements = vec![Vector3::zeros(); mesh.vertices.len()];
976
977    for (vi, v) in mesh.vertices.iter().enumerate() {
978        if let Some(neighbors) = adjacency.get(&(vi as u32)) {
979            if neighbors.is_empty() {
980                continue;
981            }
982
983            let normal = normals.get(vi).copied().unwrap_or(Vector3::z());
984            let mut weighted_sum = Vector3::zeros();
985            let mut weight_sum = 0.0;
986
987            for &ni in neighbors {
988                let neighbor_pos = mesh.vertices[ni as usize].position;
989                let diff = neighbor_pos - v.position;
990
991                // Bilateral weight based on distance and normal similarity
992                let spatial_weight = (-diff.norm_squared() / (2.0 * strength * strength)).exp();
993
994                let neighbor_normal = normals.get(ni as usize).copied().unwrap_or(Vector3::z());
995                let normal_sim = normal.dot(&neighbor_normal).max(0.0);
996                let range_weight = if normal_sim > feature_threshold.cos() {
997                    1.0
998                } else {
999                    0.1
1000                };
1001
1002                let weight = spatial_weight * range_weight;
1003                weighted_sum += diff * weight;
1004                weight_sum += weight;
1005            }
1006
1007            if weight_sum > 1e-10 {
1008                displacements[vi] = weighted_sum / weight_sum * strength;
1009            }
1010        }
1011    }
1012
1013    displacements
1014}
1015
1016/// Compute Taubin smoothing displacements (reduces shrinkage).
1017fn compute_taubin_displacements(mesh: &Mesh, strength: f64) -> Vec<Vector3<f64>> {
1018    // Taubin smoothing alternates positive and negative lambda
1019    let lambda = strength;
1020    let mu = -strength * 1.02; // Slightly larger to compensate
1021
1022    let laplacian = compute_laplacian_displacements(mesh, lambda);
1023
1024    // Apply first step
1025    let mut temp_mesh = mesh.clone();
1026    for (i, disp) in laplacian.iter().enumerate() {
1027        temp_mesh.vertices[i].position += disp;
1028    }
1029
1030    // Compute second step (negative)
1031    let mut inverse = compute_laplacian_displacements(&temp_mesh, mu);
1032
1033    // Combine
1034    for (i, disp) in inverse.iter_mut().enumerate() {
1035        *disp = laplacian[i] + *disp;
1036    }
1037
1038    inverse
1039}
1040
1041/// Compute mean curvature flow displacements.
1042fn compute_mcf_displacements(mesh: &Mesh, strength: f64) -> Vec<Vector3<f64>> {
1043    let adjacency = build_vertex_adjacency(mesh);
1044    let normals = compute_vertex_normals(mesh);
1045    let mut displacements = vec![Vector3::zeros(); mesh.vertices.len()];
1046
1047    for (vi, v) in mesh.vertices.iter().enumerate() {
1048        if let Some(neighbors) = adjacency.get(&(vi as u32)) {
1049            if neighbors.is_empty() {
1050                continue;
1051            }
1052
1053            let normal = normals.get(vi).copied().unwrap_or(Vector3::z());
1054
1055            // Compute approximate mean curvature
1056            let centroid: Point3<f64> = Point3::from(
1057                neighbors
1058                    .iter()
1059                    .map(|&ni| mesh.vertices[ni as usize].position.coords)
1060                    .sum::<Vector3<f64>>()
1061                    / neighbors.len() as f64,
1062            );
1063
1064            let laplacian = centroid - v.position;
1065            let mean_curvature = laplacian.dot(&normal);
1066
1067            // Move along normal by curvature
1068            displacements[vi] = normal * mean_curvature * strength;
1069        }
1070    }
1071
1072    displacements
1073}
1074
1075/// Compute vertex normals.
1076fn compute_vertex_normals(mesh: &Mesh) -> Vec<Vector3<f64>> {
1077    let mut normals = vec![Vector3::zeros(); mesh.vertices.len()];
1078
1079    for face in &mesh.faces {
1080        let v0 = mesh.vertices[face[0] as usize].position;
1081        let v1 = mesh.vertices[face[1] as usize].position;
1082        let v2 = mesh.vertices[face[2] as usize].position;
1083
1084        let edge1 = v1 - v0;
1085        let edge2 = v2 - v0;
1086        let face_normal = edge1.cross(&edge2);
1087
1088        normals[face[0] as usize] += face_normal;
1089        normals[face[1] as usize] += face_normal;
1090        normals[face[2] as usize] += face_normal;
1091    }
1092
1093    for normal in &mut normals {
1094        let len = normal.norm();
1095        if len > 1e-10 {
1096            *normal /= len;
1097        }
1098    }
1099
1100    normals
1101}
1102
1103/// Check if a vertex is on a feature edge.
1104fn is_feature_vertex(mesh: &Mesh, vertex_idx: usize, threshold: f64) -> bool {
1105    // Find faces containing this vertex
1106    let mut face_normals: Vec<Vector3<f64>> = Vec::new();
1107
1108    for face in &mesh.faces {
1109        if face[0] as usize == vertex_idx
1110            || face[1] as usize == vertex_idx
1111            || face[2] as usize == vertex_idx
1112        {
1113            let v0 = mesh.vertices[face[0] as usize].position;
1114            let v1 = mesh.vertices[face[1] as usize].position;
1115            let v2 = mesh.vertices[face[2] as usize].position;
1116
1117            let edge1 = v1 - v0;
1118            let edge2 = v2 - v0;
1119            let normal = edge1.cross(&edge2);
1120            let len = normal.norm();
1121            if len > 1e-10 {
1122                face_normals.push(normal / len);
1123            }
1124        }
1125    }
1126
1127    // Check if any pair of normals has angle > threshold
1128    for i in 0..face_normals.len() {
1129        for j in (i + 1)..face_normals.len() {
1130            let dot = face_normals[i].dot(&face_normals[j]).clamp(-1.0, 1.0);
1131            let angle = dot.acos();
1132            if angle > threshold {
1133                return true;
1134            }
1135        }
1136    }
1137
1138    false
1139}
1140
1141/// Compute centroid of hole boundary vertices.
1142fn compute_hole_centroid(mesh: &Mesh, hole: &[u32]) -> Point3<f64> {
1143    if hole.is_empty() {
1144        return Point3::origin();
1145    }
1146
1147    let sum: Vector3<f64> = hole
1148        .iter()
1149        .map(|&vi| mesh.vertices[vi as usize].position.coords)
1150        .sum();
1151
1152    Point3::from(sum / hole.len() as f64)
1153}
1154
1155/// Estimate normal at hole boundary.
1156fn estimate_boundary_normal(mesh: &Mesh, hole: &[u32]) -> Vector3<f64> {
1157    if hole.len() < 3 {
1158        return Vector3::z();
1159    }
1160
1161    // Use Newell's method for polygon normal
1162    let mut normal: Vector3<f64> = Vector3::zeros();
1163
1164    for i in 0..hole.len() {
1165        let current = mesh.vertices[hole[i] as usize].position;
1166        let next = mesh.vertices[hole[(i + 1) % hole.len()] as usize].position;
1167
1168        normal.x += (current.y - next.y) * (current.z + next.z);
1169        normal.y += (current.z - next.z) * (current.x + next.x);
1170        normal.z += (current.x - next.x) * (current.y + next.y);
1171    }
1172
1173    let len = normal.norm();
1174    if len > 1e-10 {
1175        normal / len
1176    } else {
1177        Vector3::z()
1178    }
1179}
1180
1181// ============================================================================
1182// Mesh extension methods
1183// ============================================================================
1184
1185impl Mesh {
1186    /// Clean up scan data with default parameters.
1187    pub fn cleanup_scan(&self) -> ScanCleanupResult {
1188        cleanup_scan(self, &ScanCleanupParams::default())
1189    }
1190
1191    /// Clean up scan data with custom parameters.
1192    pub fn cleanup_scan_with_params(&self, params: &ScanCleanupParams) -> ScanCleanupResult {
1193        cleanup_scan(self, params)
1194    }
1195
1196    /// Denoise the mesh.
1197    pub fn denoise(&self) -> DenoiseResult {
1198        denoise_mesh(self, &DenoiseParams::default())
1199    }
1200
1201    /// Denoise the mesh with custom parameters.
1202    pub fn denoise_with_params(&self, params: &DenoiseParams) -> DenoiseResult {
1203        denoise_mesh(self, params)
1204    }
1205
1206    /// Fill holes with advanced strategies.
1207    pub fn fill_holes_advanced(&self) -> HoleFillResult {
1208        fill_holes_advanced(self, &HoleFillParams::default())
1209    }
1210
1211    /// Fill holes with custom parameters.
1212    pub fn fill_holes_advanced_with_params(&self, params: &HoleFillParams) -> HoleFillResult {
1213        fill_holes_advanced(self, params)
1214    }
1215
1216    /// Remove statistical outliers.
1217    pub fn remove_outliers(&self) -> Mesh {
1218        remove_outliers(self, &OutlierRemovalParams::default())
1219    }
1220
1221    /// Remove statistical outliers with custom parameters.
1222    pub fn remove_outliers_with_params(&self, params: &OutlierRemovalParams) -> Mesh {
1223        remove_outliers(self, params)
1224    }
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229    use super::*;
1230
1231    fn create_test_mesh() -> Mesh {
1232        let mut mesh = Mesh::new();
1233        // Create a simple pyramid
1234        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
1235        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
1236        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
1237        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
1238        mesh.vertices.push(Vertex::from_coords(5.0, 5.0, 10.0));
1239
1240        mesh.faces.push([0, 1, 4]);
1241        mesh.faces.push([1, 2, 4]);
1242        mesh.faces.push([2, 3, 4]);
1243        mesh.faces.push([3, 0, 4]);
1244        mesh
1245    }
1246
1247    #[test]
1248    fn test_cleanup_params_default() {
1249        let params = ScanCleanupParams::default();
1250        assert!(params.remove_isolated);
1251        assert!(params.remove_spikes);
1252        assert!(params.fill_small_holes);
1253        assert!(params.smooth);
1254    }
1255
1256    #[test]
1257    fn test_cleanup_params_presets() {
1258        let body = ScanCleanupParams::for_body_scan();
1259        assert!(body.min_component_size > 100);
1260
1261        let object = ScanCleanupParams::for_object_scan();
1262        assert!(object.min_component_size < body.min_component_size);
1263
1264        let minimal = ScanCleanupParams::minimal();
1265        assert!(!minimal.remove_spikes);
1266
1267        let aggressive = ScanCleanupParams::aggressive();
1268        assert!(aggressive.smooth_iterations > 3);
1269    }
1270
1271    #[test]
1272    fn test_cleanup_scan() {
1273        let mesh = create_test_mesh();
1274        // Use custom params with low threshold since test mesh is small
1275        let mut params = ScanCleanupParams::minimal();
1276        params.min_component_size = 1; // Don't remove small components in test
1277        let result = cleanup_scan(&mesh, &params);
1278
1279        assert!(!result.mesh.vertices.is_empty());
1280        assert!(!result.mesh.faces.is_empty());
1281    }
1282
1283    #[test]
1284    fn test_denoise_params() {
1285        let params = DenoiseParams::default();
1286        assert_eq!(params.method, DenoiseMethod::Bilateral);
1287        assert!(params.preserve_features);
1288    }
1289
1290    #[test]
1291    fn test_denoise_mesh() {
1292        let mesh = create_test_mesh();
1293        let result = denoise_mesh(&mesh, &DenoiseParams::default());
1294
1295        assert!(!result.mesh.vertices.is_empty());
1296        assert_eq!(result.iterations_performed, 3);
1297    }
1298
1299    #[test]
1300    fn test_outlier_removal() {
1301        let mut mesh = create_test_mesh();
1302        // Add an outlier
1303        mesh.vertices.push(Vertex::from_coords(1000.0, 0.0, 0.0));
1304
1305        let params = OutlierRemovalParams {
1306            k_neighbors: 3,
1307            std_dev_threshold: 1.5,
1308        };
1309        let result = remove_outliers(&mesh, &params);
1310
1311        // Outlier should be removed (or at least not cause crash)
1312        assert!(result.vertices.len() <= mesh.vertices.len());
1313    }
1314
1315    #[test]
1316    fn test_mesh_cleanup_method() {
1317        let mesh = create_test_mesh();
1318        // Use custom params with low threshold since test mesh is small
1319        let mut params = ScanCleanupParams::minimal();
1320        params.min_component_size = 1;
1321        let result = mesh.cleanup_scan_with_params(&params);
1322
1323        assert!(!result.mesh.vertices.is_empty());
1324    }
1325
1326    #[test]
1327    fn test_mesh_denoise_method() {
1328        let mesh = create_test_mesh();
1329        let result = mesh.denoise();
1330
1331        assert!(!result.mesh.vertices.is_empty());
1332    }
1333
1334    #[test]
1335    fn test_vertex_adjacency() {
1336        let mesh = create_test_mesh();
1337        let adjacency = build_vertex_adjacency(&mesh);
1338
1339        // Apex vertex (4) should be connected to all base vertices
1340        assert!(adjacency.get(&4).map(|v| v.len() >= 4).unwrap_or(false));
1341    }
1342
1343    #[test]
1344    fn test_vertex_normals() {
1345        let mesh = create_test_mesh();
1346        let normals = compute_vertex_normals(&mesh);
1347
1348        assert_eq!(normals.len(), mesh.vertices.len());
1349        // All normals should be unit length
1350        for normal in &normals {
1351            let len = normal.norm();
1352            assert!(len > 0.9 && len < 1.1, "Normal length: {}", len);
1353        }
1354    }
1355}