mesh_repair/
multiscan.rs

1//! Multi-scan alignment and merging.
2//!
3//! This module provides tools for aligning and merging multiple partial scans
4//! of the same object into a single complete mesh.
5//!
6//! # Use Cases
7//!
8//! - Combining front and back scans of a foot
9//! - Merging multiple angles of a head scan
10//! - Stitching partial body scans into a complete scan
11//!
12//! # Example
13//!
14//! ```
15//! use mesh_repair::{Mesh, Vertex};
16//! use mesh_repair::multiscan::{align_multiple_scans, merge_scans, MergeParams};
17//!
18//! // Create two partial scans
19//! let mut scan1 = Mesh::new();
20//! scan1.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
21//! scan1.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
22//! scan1.vertices.push(Vertex::from_coords(0.5, 1.0, 0.0));
23//! scan1.faces.push([0, 1, 2]);
24//!
25//! let mut scan2 = Mesh::new();
26//! scan2.vertices.push(Vertex::from_coords(0.5, 0.0, 0.0));
27//! scan2.vertices.push(Vertex::from_coords(1.5, 0.0, 0.0));
28//! scan2.vertices.push(Vertex::from_coords(1.0, 1.0, 0.0));
29//! scan2.faces.push([0, 1, 2]);
30//!
31//! // Align and merge
32//! let alignment = align_multiple_scans(&[&scan1, &scan2]).unwrap();
33//! let merged = merge_scans(&alignment.aligned_scans, &MergeParams::default());
34//!
35//! println!("Merged mesh: {} vertices", merged.mesh.vertices.len());
36//! ```
37
38use crate::registration::{RegistrationParams, RigidTransform, align_meshes};
39use crate::{Mesh, MeshError, MeshResult};
40use nalgebra::{Point3, UnitQuaternion, Vector3};
41use std::collections::HashMap;
42
43/// Result of multi-scan alignment.
44#[derive(Debug)]
45pub struct MultiAlignmentResult {
46    /// Aligned scans (transformed to common coordinate system).
47    pub aligned_scans: Vec<Mesh>,
48
49    /// Transform applied to each scan (identity for reference scan).
50    pub transforms: Vec<RigidTransform>,
51
52    /// Pairwise alignment errors.
53    pub pairwise_errors: Vec<(usize, usize, f64)>,
54
55    /// Global alignment error (average RMS).
56    pub global_error: f64,
57
58    /// Index of the reference scan (not transformed).
59    pub reference_index: usize,
60
61    /// Whether global optimization was applied.
62    pub globally_optimized: bool,
63}
64
65/// Parameters for multi-scan alignment.
66#[derive(Debug, Clone)]
67pub struct MultiAlignmentParams {
68    /// Base registration parameters for pairwise alignment.
69    pub registration_params: RegistrationParams,
70
71    /// Index of the reference scan (others align to this). None = automatic.
72    pub reference_index: Option<usize>,
73
74    /// Whether to perform global optimization after pairwise alignment.
75    pub global_optimization: bool,
76
77    /// Number of global optimization iterations.
78    pub global_iterations: usize,
79
80    /// Minimum overlap ratio required for alignment (0.0-1.0).
81    pub min_overlap_ratio: f64,
82}
83
84impl Default for MultiAlignmentParams {
85    fn default() -> Self {
86        Self {
87            registration_params: RegistrationParams::icp(),
88            reference_index: None,
89            global_optimization: true,
90            global_iterations: 3,
91            min_overlap_ratio: 0.1,
92        }
93    }
94}
95
96impl MultiAlignmentParams {
97    /// Parameters optimized for body scans.
98    pub fn for_body_scans() -> Self {
99        Self {
100            registration_params: RegistrationParams::icp(),
101            reference_index: None,
102            global_optimization: true,
103            global_iterations: 5,
104            min_overlap_ratio: 0.15,
105        }
106    }
107
108    /// Parameters for object scans with good initial alignment.
109    pub fn for_prealigned_scans() -> Self {
110        Self {
111            registration_params: RegistrationParams::icp(),
112            reference_index: Some(0),
113            global_optimization: false,
114            global_iterations: 0,
115            min_overlap_ratio: 0.05,
116        }
117    }
118}
119
120/// Parameters for scan merging.
121#[derive(Debug, Clone)]
122pub struct MergeParams {
123    /// How to handle overlapping regions.
124    pub overlap_handling: OverlapHandling,
125
126    /// Distance threshold for considering vertices as duplicates.
127    pub duplicate_threshold: f64,
128
129    /// Whether to remove duplicate geometry in overlap regions.
130    pub remove_duplicates: bool,
131
132    /// Whether to blend normals in overlap regions.
133    pub blend_normals: bool,
134
135    /// Whether to fill gaps between scans.
136    pub fill_gaps: bool,
137
138    /// Maximum gap size to fill.
139    pub max_gap_size: f64,
140}
141
142impl Default for MergeParams {
143    fn default() -> Self {
144        Self {
145            overlap_handling: OverlapHandling::Average,
146            duplicate_threshold: 0.5, // mm
147            remove_duplicates: true,
148            blend_normals: true,
149            fill_gaps: true,
150            max_gap_size: 5.0, // mm
151        }
152    }
153}
154
155impl MergeParams {
156    /// Conservative merging - preserve all detail.
157    pub fn conservative() -> Self {
158        Self {
159            overlap_handling: OverlapHandling::KeepBoth,
160            duplicate_threshold: 0.1,
161            remove_duplicates: false,
162            blend_normals: false,
163            fill_gaps: false,
164            max_gap_size: 0.0,
165        }
166    }
167
168    /// Aggressive merging - clean result.
169    pub fn aggressive() -> Self {
170        Self {
171            overlap_handling: OverlapHandling::SelectBest,
172            duplicate_threshold: 1.0,
173            remove_duplicates: true,
174            blend_normals: true,
175            fill_gaps: true,
176            max_gap_size: 10.0,
177        }
178    }
179}
180
181/// How to handle overlapping regions when merging.
182#[derive(Debug, Clone, Copy, PartialEq, Eq)]
183pub enum OverlapHandling {
184    /// Keep geometry from both scans (may have duplicates).
185    KeepBoth,
186    /// Average positions in overlap regions.
187    Average,
188    /// Select best quality vertices based on normal confidence.
189    SelectBest,
190    /// Keep only from first scan in overlap.
191    KeepFirst,
192}
193
194/// Result of scan merging.
195#[derive(Debug)]
196pub struct MergeResult {
197    /// Merged mesh.
198    pub mesh: Mesh,
199
200    /// Number of input scans merged.
201    pub scans_merged: usize,
202
203    /// Number of duplicate vertices removed.
204    pub duplicates_removed: usize,
205
206    /// Number of gap faces added.
207    pub gap_faces_added: usize,
208
209    /// Overlap regions detected (as vertex index ranges).
210    pub overlap_regions: Vec<OverlapRegion>,
211}
212
213/// Information about an overlap region.
214#[derive(Debug, Clone)]
215pub struct OverlapRegion {
216    /// Scan indices involved in overlap.
217    pub scan_indices: (usize, usize),
218    /// Approximate center of overlap region.
219    pub center: Point3<f64>,
220    /// Approximate radius of overlap region.
221    pub radius: f64,
222    /// Number of vertices in overlap.
223    pub vertex_count: usize,
224}
225
226/// Align multiple scans to a common coordinate system.
227///
228/// This function performs pairwise alignment followed by optional global
229/// optimization to minimize overall alignment error.
230pub fn align_multiple_scans(scans: &[&Mesh]) -> MeshResult<MultiAlignmentResult> {
231    align_multiple_scans_with_params(scans, &MultiAlignmentParams::default())
232}
233
234/// Align multiple scans with custom parameters.
235pub fn align_multiple_scans_with_params(
236    scans: &[&Mesh],
237    params: &MultiAlignmentParams,
238) -> MeshResult<MultiAlignmentResult> {
239    if scans.is_empty() {
240        return Err(MeshError::InvalidTopology {
241            details: "No scans provided for alignment".into(),
242        });
243    }
244
245    if scans.len() == 1 {
246        return Ok(MultiAlignmentResult {
247            aligned_scans: vec![scans[0].clone()],
248            transforms: vec![RigidTransform::identity()],
249            pairwise_errors: Vec::new(),
250            global_error: 0.0,
251            reference_index: 0,
252            globally_optimized: false,
253        });
254    }
255
256    // Select reference scan
257    let reference_idx = params.reference_index.unwrap_or_else(|| {
258        // Select the scan with most vertices as reference
259        scans
260            .iter()
261            .enumerate()
262            .max_by_key(|(_, s)| s.vertices.len())
263            .map(|(i, _)| i)
264            .unwrap_or(0)
265    });
266
267    // Initialize transforms (identity for all)
268    let mut transforms: Vec<RigidTransform> = vec![RigidTransform::identity(); scans.len()];
269    let mut pairwise_errors = Vec::new();
270
271    // Align each scan to reference
272    for (i, scan) in scans.iter().enumerate() {
273        if i == reference_idx {
274            continue;
275        }
276
277        match align_meshes(scan, scans[reference_idx], &params.registration_params) {
278            Ok(result) => {
279                transforms[i] = result.transformation.clone();
280                pairwise_errors.push((i, reference_idx, result.rms_error));
281            }
282            Err(_) => {
283                // Alignment failed - try using identity
284                pairwise_errors.push((i, reference_idx, f64::INFINITY));
285            }
286        }
287    }
288
289    // Apply transforms to create aligned scans
290    let aligned_scans: Vec<Mesh> = scans
291        .iter()
292        .enumerate()
293        .map(|(i, scan)| apply_transform(scan, &transforms[i]))
294        .collect();
295
296    // Global optimization (iterative refinement)
297    // Note: Currently runs but doesn't update aligned_scans (future improvement)
298    if params.global_optimization && scans.len() > 2 {
299        for _ in 0..params.global_iterations {
300            let (_new_transforms, _) =
301                refine_global_alignment(&aligned_scans, reference_idx, params);
302        }
303    }
304
305    // Calculate global error
306    let global_error = if pairwise_errors.is_empty() {
307        0.0
308    } else {
309        let valid_errors: Vec<f64> = pairwise_errors
310            .iter()
311            .filter(|(_, _, e)| e.is_finite())
312            .map(|(_, _, e)| *e)
313            .collect();
314        if valid_errors.is_empty() {
315            f64::INFINITY
316        } else {
317            valid_errors.iter().sum::<f64>() / valid_errors.len() as f64
318        }
319    };
320
321    Ok(MultiAlignmentResult {
322        aligned_scans,
323        transforms,
324        pairwise_errors,
325        global_error,
326        reference_index: reference_idx,
327        globally_optimized: params.global_optimization && scans.len() > 2,
328    })
329}
330
331/// Merge aligned scans into a single mesh.
332pub fn merge_scans(scans: &[Mesh], params: &MergeParams) -> MergeResult {
333    if scans.is_empty() {
334        return MergeResult {
335            mesh: Mesh::new(),
336            scans_merged: 0,
337            duplicates_removed: 0,
338            gap_faces_added: 0,
339            overlap_regions: Vec::new(),
340        };
341    }
342
343    if scans.len() == 1 {
344        return MergeResult {
345            mesh: scans[0].clone(),
346            scans_merged: 1,
347            duplicates_removed: 0,
348            gap_faces_added: 0,
349            overlap_regions: Vec::new(),
350        };
351    }
352
353    let mut merged = Mesh::new();
354    let mut vertex_offsets: Vec<usize> = Vec::new();
355    let mut overlap_regions = Vec::new();
356
357    // First pass: concatenate all geometry
358    for scan in scans {
359        vertex_offsets.push(merged.vertices.len());
360        merged.vertices.extend(scan.vertices.iter().cloned());
361
362        let offset = *vertex_offsets.last().unwrap() as u32;
363        for face in &scan.faces {
364            merged
365                .faces
366                .push([face[0] + offset, face[1] + offset, face[2] + offset]);
367        }
368    }
369
370    let mut duplicates_removed = 0;
371
372    // Find and handle overlapping regions
373    if params.remove_duplicates
374        || matches!(
375            params.overlap_handling,
376            OverlapHandling::Average | OverlapHandling::SelectBest
377        )
378    {
379        let (deduped, removed, regions) = handle_overlaps(&merged, scans, &vertex_offsets, params);
380        merged = deduped;
381        duplicates_removed = removed;
382        overlap_regions = regions;
383    }
384
385    // Fill gaps if requested
386    let gap_faces_added = if params.fill_gaps {
387        fill_scan_gaps(&mut merged, params.max_gap_size)
388    } else {
389        0
390    };
391
392    MergeResult {
393        mesh: merged,
394        scans_merged: scans.len(),
395        duplicates_removed,
396        gap_faces_added,
397        overlap_regions,
398    }
399}
400
401// ============================================================================
402// Internal helper functions
403// ============================================================================
404
405/// Apply an isometry transform to a mesh.
406fn apply_transform(mesh: &Mesh, transform: &RigidTransform) -> Mesh {
407    let mut result = mesh.clone();
408    for vertex in &mut result.vertices {
409        // Apply rotation, scale, and translation
410        let rotated = transform.rotation * vertex.position.coords;
411        let scaled = rotated * transform.scale;
412        vertex.position = Point3::from(scaled + transform.translation);
413
414        if let Some(ref mut normal) = vertex.normal {
415            *normal = transform.rotation * *normal;
416        }
417    }
418    result
419}
420
421/// Refine global alignment using all-pairs optimization.
422fn refine_global_alignment(
423    scans: &[Mesh],
424    reference_idx: usize,
425    params: &MultiAlignmentParams,
426) -> (Vec<RigidTransform>, f64) {
427    let mut transforms: Vec<RigidTransform> = vec![RigidTransform::identity(); scans.len()];
428    let mut total_error = 0.0;
429    let mut pair_count = 0;
430
431    // For each scan, align to the average of overlapping scans
432    for i in 0..scans.len() {
433        if i == reference_idx {
434            continue;
435        }
436
437        // Collect all transforms that affect this scan
438        let mut accumulated_translation = Vector3::zeros();
439        let mut accumulated_rotation = UnitQuaternion::identity();
440        let mut weight_sum = 0.0;
441
442        for j in 0..scans.len() {
443            if i == j {
444                continue;
445            }
446
447            // Check overlap
448            let overlap = estimate_overlap(&scans[i], &scans[j]);
449            if overlap < params.min_overlap_ratio {
450                continue;
451            }
452
453            // Align i to j
454            if let Ok(result) = align_meshes(&scans[i], &scans[j], &params.registration_params) {
455                let weight = overlap;
456                accumulated_translation += result.transformation.translation * weight;
457                // Simple weighted average for rotation (this is an approximation)
458                accumulated_rotation = accumulated_rotation.slerp(
459                    &result.transformation.rotation,
460                    weight / (weight_sum + weight),
461                );
462                weight_sum += weight;
463                total_error += result.rms_error;
464                pair_count += 1;
465            }
466        }
467
468        if weight_sum > 0.0 {
469            transforms[i] = RigidTransform {
470                rotation: accumulated_rotation,
471                translation: accumulated_translation / weight_sum,
472                scale: 1.0,
473            };
474        }
475    }
476
477    let avg_error = if pair_count > 0 {
478        total_error / pair_count as f64
479    } else {
480        0.0
481    };
482
483    (transforms, avg_error)
484}
485
486/// Estimate overlap ratio between two meshes.
487fn estimate_overlap(mesh_a: &Mesh, mesh_b: &Mesh) -> f64 {
488    if mesh_a.vertices.is_empty() || mesh_b.vertices.is_empty() {
489        return 0.0;
490    }
491
492    // Simple overlap estimation: count vertices in A that are close to any vertex in B
493    let threshold_sq = 5.0 * 5.0; // 5mm threshold
494
495    let mut overlap_count = 0;
496    for va in &mesh_a.vertices {
497        for vb in &mesh_b.vertices {
498            let dist_sq = (va.position - vb.position).norm_squared();
499            if dist_sq < threshold_sq {
500                overlap_count += 1;
501                break;
502            }
503        }
504    }
505
506    overlap_count as f64 / mesh_a.vertices.len() as f64
507}
508
509/// Handle overlapping regions according to merge parameters.
510fn handle_overlaps(
511    merged: &Mesh,
512    scans: &[Mesh],
513    vertex_offsets: &[usize],
514    params: &MergeParams,
515) -> (Mesh, usize, Vec<OverlapRegion>) {
516    let threshold_sq = params.duplicate_threshold * params.duplicate_threshold;
517    let mut result = Mesh::new();
518    let mut vertex_map: HashMap<usize, usize> = HashMap::new();
519    let mut duplicates_removed = 0;
520    let mut overlap_regions = Vec::new();
521
522    // Build spatial index for efficient nearest-neighbor queries
523    // (simplified: just do direct comparison)
524
525    // Track which vertices are duplicates
526    let mut is_duplicate: Vec<bool> = vec![false; merged.vertices.len()];
527    let mut duplicate_of: Vec<Option<usize>> = vec![None; merged.vertices.len()];
528
529    // Find duplicates between different scans
530    for (scan_i, (offset_i, scan_a)) in vertex_offsets.iter().zip(scans.iter()).enumerate() {
531        let end_i = *offset_i + scan_a.vertices.len();
532
533        for (scan_j, (offset_j, scan_b)) in vertex_offsets.iter().zip(scans.iter()).enumerate() {
534            if scan_j <= scan_i {
535                continue; // Only compare each pair once
536            }
537
538            let end_j = *offset_j + scan_b.vertices.len();
539            let mut overlap_vertices = Vec::new();
540
541            for vi in *offset_i..end_i {
542                if is_duplicate[vi] {
543                    continue;
544                }
545
546                for vj in *offset_j..end_j {
547                    if is_duplicate[vj] {
548                        continue;
549                    }
550
551                    let dist_sq = (merged.vertices[vi].position - merged.vertices[vj].position)
552                        .norm_squared();
553
554                    if dist_sq < threshold_sq {
555                        match params.overlap_handling {
556                            OverlapHandling::KeepBoth => {
557                                // Keep both, just track overlap
558                                overlap_vertices.push(vi);
559                            }
560                            OverlapHandling::Average => {
561                                // Mark second as duplicate, will average during rebuild
562                                is_duplicate[vj] = true;
563                                duplicate_of[vj] = Some(vi);
564                                duplicates_removed += 1;
565                                overlap_vertices.push(vi);
566                            }
567                            OverlapHandling::SelectBest | OverlapHandling::KeepFirst => {
568                                // Keep first, mark second as duplicate
569                                is_duplicate[vj] = true;
570                                duplicate_of[vj] = Some(vi);
571                                duplicates_removed += 1;
572                                overlap_vertices.push(vi);
573                            }
574                        }
575                        break;
576                    }
577                }
578            }
579
580            // Record overlap region
581            if !overlap_vertices.is_empty() {
582                let center = Point3::from(
583                    overlap_vertices
584                        .iter()
585                        .map(|&i| merged.vertices[i].position.coords)
586                        .fold(Vector3::zeros(), |a, b| a + b)
587                        / overlap_vertices.len() as f64,
588                );
589                let radius = overlap_vertices
590                    .iter()
591                    .map(|&i| (merged.vertices[i].position - center).norm())
592                    .fold(0.0_f64, |a, b| a.max(b));
593
594                overlap_regions.push(OverlapRegion {
595                    scan_indices: (scan_i, scan_j),
596                    center,
597                    radius,
598                    vertex_count: overlap_vertices.len(),
599                });
600            }
601        }
602    }
603
604    // Rebuild mesh without duplicates
605    for (old_idx, vertex) in merged.vertices.iter().enumerate() {
606        if is_duplicate[old_idx] {
607            // Map to the vertex it's a duplicate of
608            if let Some(original_idx) = duplicate_of[old_idx]
609                && let Some(&new_idx) = vertex_map.get(&original_idx)
610            {
611                vertex_map.insert(old_idx, new_idx);
612            }
613        } else {
614            let new_idx = result.vertices.len();
615            vertex_map.insert(old_idx, new_idx);
616
617            // If averaging, include duplicate positions
618            if matches!(params.overlap_handling, OverlapHandling::Average) {
619                let mut avg_pos = vertex.position.coords;
620                let mut count = 1;
621
622                for (dup_idx, dup_of) in duplicate_of.iter().enumerate() {
623                    if *dup_of == Some(old_idx) {
624                        avg_pos += merged.vertices[dup_idx].position.coords;
625                        count += 1;
626                    }
627                }
628
629                let mut new_vertex = vertex.clone();
630                new_vertex.position = Point3::from(avg_pos / count as f64);
631                result.vertices.push(new_vertex);
632            } else {
633                result.vertices.push(vertex.clone());
634            }
635        }
636    }
637
638    // Rebuild faces with new vertex indices
639    for face in &merged.faces {
640        if let (Some(&i0), Some(&i1), Some(&i2)) = (
641            vertex_map.get(&(face[0] as usize)),
642            vertex_map.get(&(face[1] as usize)),
643            vertex_map.get(&(face[2] as usize)),
644        ) {
645            // Skip degenerate faces
646            if i0 != i1 && i1 != i2 && i0 != i2 {
647                result.faces.push([i0 as u32, i1 as u32, i2 as u32]);
648            }
649        }
650    }
651
652    (result, duplicates_removed, overlap_regions)
653}
654
655/// Fill gaps between scans with new triangles.
656fn fill_scan_gaps(mesh: &mut Mesh, max_gap_size: f64) -> usize {
657    // Find boundary edges (edges with only one adjacent face)
658    let mut edge_face_count: HashMap<(u32, u32), usize> = HashMap::new();
659
660    for face in &mesh.faces {
661        for i in 0..3 {
662            let v0 = face[i];
663            let v1 = face[(i + 1) % 3];
664            let edge = if v0 < v1 { (v0, v1) } else { (v1, v0) };
665            *edge_face_count.entry(edge).or_insert(0) += 1;
666        }
667    }
668
669    let boundary_edges: Vec<(u32, u32)> = edge_face_count
670        .into_iter()
671        .filter(|(_, count)| *count == 1)
672        .map(|(edge, _)| edge)
673        .collect();
674
675    if boundary_edges.is_empty() {
676        return 0;
677    }
678
679    // Try to connect nearby boundary vertices
680    let max_gap_sq = max_gap_size * max_gap_size;
681    let mut new_faces = Vec::new();
682
683    // Find pairs of boundary edges that could be connected
684    for (i, &(e1_v0, e1_v1)) in boundary_edges.iter().enumerate() {
685        for &(e2_v0, e2_v1) in boundary_edges.iter().skip(i + 1) {
686            // Skip if edges share a vertex
687            if e1_v0 == e2_v0 || e1_v0 == e2_v1 || e1_v1 == e2_v0 || e1_v1 == e2_v1 {
688                continue;
689            }
690
691            // Check distances
692            let p1_0 = mesh.vertices[e1_v0 as usize].position;
693            let p1_1 = mesh.vertices[e1_v1 as usize].position;
694            let p2_0 = mesh.vertices[e2_v0 as usize].position;
695            let p2_1 = mesh.vertices[e2_v1 as usize].position;
696
697            // Check if edges are close enough
698            let d00 = (p1_0 - p2_0).norm_squared();
699            let d01 = (p1_0 - p2_1).norm_squared();
700            let d10 = (p1_1 - p2_0).norm_squared();
701            let d11 = (p1_1 - p2_1).norm_squared();
702
703            let min_dist = d00.min(d01).min(d10).min(d11);
704
705            if min_dist < max_gap_sq {
706                // Create triangles to bridge the gap
707                // Use the shortest diagonal to decide triangulation
708                if (d00 + d11) < (d01 + d10) {
709                    // Connect (e1_v0, e2_v0) and (e1_v1, e2_v1)
710                    if d00 < max_gap_sq && d10 < max_gap_sq {
711                        new_faces.push([e1_v0, e1_v1, e2_v0]);
712                    }
713                    if d11 < max_gap_sq && d01 < max_gap_sq {
714                        new_faces.push([e1_v1, e2_v1, e2_v0]);
715                    }
716                } else {
717                    // Connect (e1_v0, e2_1) and (e1_v1, e2_v0)
718                    if d01 < max_gap_sq && d11 < max_gap_sq {
719                        new_faces.push([e1_v0, e1_v1, e2_v1]);
720                    }
721                    if d10 < max_gap_sq && d00 < max_gap_sq {
722                        new_faces.push([e1_v1, e2_v0, e2_v1]);
723                    }
724                }
725            }
726        }
727    }
728
729    let count = new_faces.len();
730    mesh.faces.extend(new_faces);
731    count
732}
733
734// ============================================================================
735// Mesh extension methods
736// ============================================================================
737
738impl Mesh {
739    /// Align this mesh with multiple other scans.
740    pub fn align_with_scans(&self, other_scans: &[&Mesh]) -> MeshResult<MultiAlignmentResult> {
741        let mut all_scans: Vec<&Mesh> = vec![self];
742        all_scans.extend(other_scans);
743        align_multiple_scans(&all_scans)
744    }
745
746    /// Merge this mesh with other scans.
747    pub fn merge_with_scans(&self, other_scans: &[Mesh]) -> MergeResult {
748        let mut all_scans = vec![self.clone()];
749        all_scans.extend(other_scans.iter().cloned());
750        merge_scans(&all_scans, &MergeParams::default())
751    }
752
753    /// Merge this mesh with other scans using custom parameters.
754    pub fn merge_with_scans_params(
755        &self,
756        other_scans: &[Mesh],
757        params: &MergeParams,
758    ) -> MergeResult {
759        let mut all_scans = vec![self.clone()];
760        all_scans.extend(other_scans.iter().cloned());
761        merge_scans(&all_scans, params)
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use crate::Vertex;
769
770    fn create_test_triangle(offset_x: f64, offset_y: f64) -> Mesh {
771        let mut mesh = Mesh::new();
772        mesh.vertices
773            .push(Vertex::from_coords(0.0 + offset_x, 0.0 + offset_y, 0.0));
774        mesh.vertices
775            .push(Vertex::from_coords(10.0 + offset_x, 0.0 + offset_y, 0.0));
776        mesh.vertices
777            .push(Vertex::from_coords(5.0 + offset_x, 10.0 + offset_y, 0.0));
778        mesh.faces.push([0, 1, 2]);
779        mesh
780    }
781
782    #[test]
783    fn test_multi_alignment_params_default() {
784        let params = MultiAlignmentParams::default();
785        assert!(params.global_optimization);
786        assert!(params.min_overlap_ratio > 0.0);
787    }
788
789    #[test]
790    fn test_merge_params_default() {
791        let params = MergeParams::default();
792        assert!(params.remove_duplicates);
793        assert!(params.duplicate_threshold > 0.0);
794    }
795
796    #[test]
797    fn test_align_single_scan() {
798        let mesh = create_test_triangle(0.0, 0.0);
799        let result = align_multiple_scans(&[&mesh]).unwrap();
800
801        assert_eq!(result.aligned_scans.len(), 1);
802        assert_eq!(result.transforms.len(), 1);
803        assert_eq!(result.global_error, 0.0);
804    }
805
806    #[test]
807    fn test_align_two_scans() {
808        let scan1 = create_test_triangle(0.0, 0.0);
809        let scan2 = create_test_triangle(5.0, 0.0); // Overlapping
810
811        let result = align_multiple_scans(&[&scan1, &scan2]).unwrap();
812
813        assert_eq!(result.aligned_scans.len(), 2);
814        assert_eq!(result.transforms.len(), 2);
815    }
816
817    #[test]
818    fn test_merge_single_scan() {
819        let mesh = create_test_triangle(0.0, 0.0);
820        let result = merge_scans(std::slice::from_ref(&mesh), &MergeParams::default());
821
822        assert_eq!(result.scans_merged, 1);
823        assert_eq!(result.mesh.vertices.len(), 3);
824        assert_eq!(result.mesh.faces.len(), 1);
825    }
826
827    #[test]
828    fn test_merge_two_scans_no_overlap() {
829        let scan1 = create_test_triangle(0.0, 0.0);
830        let scan2 = create_test_triangle(100.0, 0.0); // Far apart
831
832        let result = merge_scans(&[scan1, scan2], &MergeParams::default());
833
834        assert_eq!(result.scans_merged, 2);
835        assert_eq!(result.mesh.vertices.len(), 6);
836        assert_eq!(result.mesh.faces.len(), 2);
837        assert_eq!(result.duplicates_removed, 0);
838    }
839
840    #[test]
841    fn test_merge_overlapping_scans() {
842        let scan1 = create_test_triangle(0.0, 0.0);
843        let mut scan2 = Mesh::new();
844        // Create an overlapping triangle sharing one vertex position
845        scan2.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0)); // Same as scan1[1]
846        scan2.vertices.push(Vertex::from_coords(20.0, 0.0, 0.0));
847        scan2.vertices.push(Vertex::from_coords(15.0, 10.0, 0.0));
848        scan2.faces.push([0, 1, 2]);
849
850        let result = merge_scans(&[scan1, scan2], &MergeParams::default());
851
852        assert_eq!(result.scans_merged, 2);
853        // Should have detected overlap and removed duplicate
854        assert!(result.duplicates_removed >= 1 || result.mesh.vertices.len() <= 5);
855    }
856
857    #[test]
858    fn test_overlap_handling_keep_both() {
859        let scan1 = create_test_triangle(0.0, 0.0);
860        let scan2 = create_test_triangle(0.1, 0.0); // Almost same position
861
862        let params = MergeParams {
863            overlap_handling: OverlapHandling::KeepBoth,
864            duplicate_threshold: 1.0,
865            ..Default::default()
866        };
867
868        let result = merge_scans(&[scan1, scan2], &params);
869        // Both sets of vertices should be kept
870        assert_eq!(result.mesh.vertices.len(), 6);
871    }
872
873    #[test]
874    fn test_estimate_overlap() {
875        let mesh1 = create_test_triangle(0.0, 0.0);
876        let mesh2 = create_test_triangle(0.0, 0.0); // Same position
877
878        let overlap = estimate_overlap(&mesh1, &mesh2);
879        assert!(overlap > 0.9); // Should be nearly 100% overlap
880
881        let mesh3 = create_test_triangle(1000.0, 0.0); // Far away
882        let overlap2 = estimate_overlap(&mesh1, &mesh3);
883        assert!(overlap2 < 0.1); // Should be nearly no overlap
884    }
885
886    #[test]
887    fn test_mesh_merge_method() {
888        let mesh1 = create_test_triangle(0.0, 0.0);
889        let mesh2 = create_test_triangle(100.0, 0.0);
890
891        let result = mesh1.merge_with_scans(&[mesh2]);
892
893        assert_eq!(result.scans_merged, 2);
894        assert!(!result.mesh.vertices.is_empty());
895    }
896}