mesh_repair/
morph.rs

1//! Mesh morphing and deformation algorithms.
2//!
3//! This module provides tools for deforming meshes to fit target shapes,
4//! including Free-Form Deformation (FFD) with control lattices and
5//! Radial Basis Function (RBF) morphing.
6//!
7//! # Use Cases
8//!
9//! - Morphing a template shoe last to fit a customer's foot scan
10//! - Deforming a helmet liner to match head measurements
11//! - Creating parametric variations of product designs
12//!
13//! # Example
14//!
15//! ```
16//! use mesh_repair::{Mesh, Vertex};
17//! use mesh_repair::morph::{MorphParams, MorphResult, Constraint};
18//! use nalgebra::Point3;
19//!
20//! // Create a simple mesh
21//! let mut mesh = Mesh::new();
22//! mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
23//! mesh.vertices.push(Vertex::from_coords(1.0, 0.0, 0.0));
24//! mesh.vertices.push(Vertex::from_coords(0.5, 1.0, 0.0));
25//! mesh.vertices.push(Vertex::from_coords(0.5, 0.5, 1.0));
26//! mesh.faces.push([0, 1, 2]);
27//! mesh.faces.push([0, 2, 3]);
28//! mesh.faces.push([0, 3, 1]);
29//! mesh.faces.push([1, 3, 2]);
30//!
31//! // Define control point constraints
32//! let constraints = vec![
33//!     Constraint::point(Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 0.5)),
34//!     Constraint::point(Point3::new(1.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.5)),
35//! ];
36//!
37//! // Morph the mesh using RBF
38//! let params = MorphParams::rbf().with_constraints(constraints);
39//! let result = mesh_repair::morph::morph_mesh(&mesh, &params).unwrap();
40//!
41//! println!("Max displacement: {:.3} mm", result.max_displacement);
42//! ```
43
44use crate::{Mesh, MeshError, MeshResult};
45use nalgebra::{DMatrix, DVector, Point3, Vector3};
46use std::collections::HashSet;
47
48/// Parameters for mesh morphing operations.
49#[derive(Debug, Clone)]
50pub struct MorphParams {
51    /// The morphing algorithm to use.
52    pub algorithm: MorphAlgorithm,
53
54    /// Control point constraints (source → target mappings).
55    pub constraints: Vec<Constraint>,
56
57    /// Optional region mask - only vertices in this set will be deformed.
58    /// If None, all vertices are affected.
59    pub region_mask: Option<HashSet<u32>>,
60
61    /// Smoothness parameter for RBF (higher = smoother deformation).
62    /// Default: 1.0
63    pub smoothness: f64,
64
65    /// Number of control points per dimension for FFD lattice.
66    /// Default: (4, 4, 4)
67    pub ffd_resolution: (usize, usize, usize),
68}
69
70impl Default for MorphParams {
71    fn default() -> Self {
72        Self {
73            algorithm: MorphAlgorithm::Rbf(RbfKernel::ThinPlateSpline),
74            constraints: Vec::new(),
75            region_mask: None,
76            smoothness: 1.0,
77            ffd_resolution: (4, 4, 4),
78        }
79    }
80}
81
82impl MorphParams {
83    /// Create params for RBF morphing with thin-plate spline kernel.
84    pub fn rbf() -> Self {
85        Self {
86            algorithm: MorphAlgorithm::Rbf(RbfKernel::ThinPlateSpline),
87            ..Default::default()
88        }
89    }
90
91    /// Create params for RBF morphing with Gaussian kernel.
92    pub fn rbf_gaussian() -> Self {
93        Self {
94            algorithm: MorphAlgorithm::Rbf(RbfKernel::Gaussian),
95            ..Default::default()
96        }
97    }
98
99    /// Create params for RBF morphing with multiquadric kernel.
100    pub fn rbf_multiquadric() -> Self {
101        Self {
102            algorithm: MorphAlgorithm::Rbf(RbfKernel::Multiquadric),
103            ..Default::default()
104        }
105    }
106
107    /// Create params for Free-Form Deformation.
108    pub fn ffd() -> Self {
109        Self {
110            algorithm: MorphAlgorithm::Ffd,
111            ..Default::default()
112        }
113    }
114
115    /// Create params for FFD with custom lattice resolution.
116    pub fn ffd_with_resolution(nx: usize, ny: usize, nz: usize) -> Self {
117        Self {
118            algorithm: MorphAlgorithm::Ffd,
119            ffd_resolution: (nx, ny, nz),
120            ..Default::default()
121        }
122    }
123
124    /// Add control point constraints.
125    pub fn with_constraints(mut self, constraints: Vec<Constraint>) -> Self {
126        self.constraints = constraints;
127        self
128    }
129
130    /// Set the region mask for partial deformation.
131    pub fn with_region(mut self, vertex_indices: HashSet<u32>) -> Self {
132        self.region_mask = Some(vertex_indices);
133        self
134    }
135
136    /// Set the smoothness parameter (RBF only).
137    pub fn with_smoothness(mut self, smoothness: f64) -> Self {
138        self.smoothness = smoothness;
139        self
140    }
141
142    /// Set the FFD lattice resolution.
143    pub fn with_ffd_resolution(mut self, nx: usize, ny: usize, nz: usize) -> Self {
144        self.ffd_resolution = (nx, ny, nz);
145        self
146    }
147}
148
149/// The morphing algorithm to use.
150#[derive(Debug, Clone, Copy, PartialEq)]
151pub enum MorphAlgorithm {
152    /// Radial Basis Function morphing with the specified kernel.
153    Rbf(RbfKernel),
154    /// Free-Form Deformation with a control lattice.
155    Ffd,
156}
157
158/// Kernel function for RBF morphing.
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum RbfKernel {
161    /// Thin-plate spline: r² log(r)
162    /// Good for smooth, natural deformations.
163    ThinPlateSpline,
164
165    /// Gaussian: exp(-r²/σ²)
166    /// Provides local deformation with controllable support.
167    Gaussian,
168
169    /// Multiquadric: sqrt(r² + c²)
170    /// Good balance between local and global deformation.
171    Multiquadric,
172
173    /// Inverse multiquadric: 1/sqrt(r² + c²)
174    /// Strong local effect, diminishes quickly with distance.
175    InverseMultiquadric,
176}
177
178/// A control point constraint for morphing.
179#[derive(Debug, Clone)]
180pub struct Constraint {
181    /// Source position (where the point is in the original mesh).
182    pub source: Point3<f64>,
183
184    /// Target position (where the point should move to).
185    pub target: Point3<f64>,
186
187    /// Optional weight for this constraint (default 1.0).
188    pub weight: f64,
189}
190
191impl Constraint {
192    /// Create a point-to-point constraint.
193    pub fn point(source: Point3<f64>, target: Point3<f64>) -> Self {
194        Self {
195            source,
196            target,
197            weight: 1.0,
198        }
199    }
200
201    /// Create a weighted constraint.
202    pub fn weighted(source: Point3<f64>, target: Point3<f64>, weight: f64) -> Self {
203        Self {
204            source,
205            target,
206            weight,
207        }
208    }
209
210    /// Create a constraint from a displacement vector.
211    pub fn displacement(source: Point3<f64>, displacement: Vector3<f64>) -> Self {
212        Self {
213            source,
214            target: source + displacement,
215            weight: 1.0,
216        }
217    }
218
219    /// Get the displacement vector for this constraint.
220    pub fn displacement_vector(&self) -> Vector3<f64> {
221        self.target - self.source
222    }
223}
224
225/// Result of a morphing operation.
226#[derive(Debug, Clone)]
227pub struct MorphResult {
228    /// The morphed mesh.
229    pub mesh: Mesh,
230
231    /// Number of vertices that were modified.
232    pub vertices_modified: usize,
233
234    /// Maximum displacement of any vertex.
235    pub max_displacement: f64,
236
237    /// Average displacement across all modified vertices.
238    pub average_displacement: f64,
239
240    /// Maximum local stretch (ratio of deformed to original edge length).
241    pub max_stretch: f64,
242
243    /// Maximum local compression (ratio of original to deformed edge length).
244    pub max_compression: f64,
245
246    /// Volume change ratio (new_volume / old_volume).
247    pub volume_ratio: f64,
248}
249
250impl MorphResult {
251    /// Check if the deformation introduced significant distortion.
252    ///
253    /// Returns true if stretch or compression exceeds the threshold.
254    pub fn has_significant_distortion(&self, threshold: f64) -> bool {
255        self.max_stretch > 1.0 + threshold || self.max_compression > 1.0 + threshold
256    }
257
258    /// Check if the volume changed significantly.
259    pub fn has_significant_volume_change(&self, threshold: f64) -> bool {
260        (self.volume_ratio - 1.0).abs() > threshold
261    }
262}
263
264/// Morph a mesh according to the given parameters.
265///
266/// # Arguments
267///
268/// * `mesh` - The input mesh to morph
269/// * `params` - Morphing parameters including algorithm and constraints
270///
271/// # Returns
272///
273/// A `MorphResult` containing the morphed mesh and quality metrics.
274///
275/// # Errors
276///
277/// Returns an error if:
278/// - The mesh is empty
279/// - No constraints are provided
280/// - The constraint system is degenerate (e.g., all constraints at same point)
281pub fn morph_mesh(mesh: &Mesh, params: &MorphParams) -> MeshResult<MorphResult> {
282    if mesh.is_empty() {
283        return Err(MeshError::EmptyMesh {
284            details: "Cannot morph an empty mesh".to_string(),
285        });
286    }
287
288    if params.constraints.is_empty() {
289        return Err(MeshError::RepairFailed {
290            details: "No constraints provided for morphing".to_string(),
291        });
292    }
293
294    match params.algorithm {
295        MorphAlgorithm::Rbf(kernel) => morph_rbf(mesh, params, kernel),
296        MorphAlgorithm::Ffd => morph_ffd(mesh, params),
297    }
298}
299
300/// RBF morphing implementation.
301fn morph_rbf(mesh: &Mesh, params: &MorphParams, kernel: RbfKernel) -> MeshResult<MorphResult> {
302    let n = params.constraints.len();
303
304    // Build the RBF interpolation matrix
305    // For thin-plate splines, we add polynomial terms for affine transformation
306    let use_polynomial = matches!(kernel, RbfKernel::ThinPlateSpline);
307    let matrix_size = if use_polynomial { n + 4 } else { n };
308
309    let mut matrix = DMatrix::zeros(matrix_size, matrix_size);
310
311    // Fill the RBF part of the matrix
312    for i in 0..n {
313        for j in 0..n {
314            let r = (params.constraints[i].source - params.constraints[j].source).norm();
315            let value =
316                evaluate_kernel(kernel, r, params.smoothness) * params.constraints[i].weight;
317            matrix[(i, j)] = value;
318        }
319        // Add small regularization to diagonal for numerical stability
320        matrix[(i, i)] += 1e-8;
321    }
322
323    // Add polynomial terms for thin-plate spline
324    if use_polynomial {
325        for i in 0..n {
326            let p = &params.constraints[i].source;
327            matrix[(i, n)] = 1.0;
328            matrix[(i, n + 1)] = p.x;
329            matrix[(i, n + 2)] = p.y;
330            matrix[(i, n + 3)] = p.z;
331
332            matrix[(n, i)] = 1.0;
333            matrix[(n + 1, i)] = p.x;
334            matrix[(n + 2, i)] = p.y;
335            matrix[(n + 3, i)] = p.z;
336        }
337        // Small regularization for polynomial constraints
338        for i in n..matrix_size {
339            matrix[(i, i)] += 1e-10;
340        }
341    }
342
343    // Solve for coefficients for each coordinate - use SVD for better numerical stability
344    let svd = matrix.svd(true, true);
345
346    // Build target vectors
347    let mut dx = DVector::zeros(matrix_size);
348    let mut dy = DVector::zeros(matrix_size);
349    let mut dz = DVector::zeros(matrix_size);
350
351    for i in 0..n {
352        let d = params.constraints[i].displacement_vector();
353        dx[i] = d.x;
354        dy[i] = d.y;
355        dz[i] = d.z;
356    }
357
358    // Solve the systems using SVD pseudoinverse for better numerical stability
359    let epsilon = 1e-10;
360    let wx = svd
361        .solve(&dx, epsilon)
362        .map_err(|_| MeshError::RepairFailed {
363            details: "Failed to solve RBF system (degenerate constraint configuration)".to_string(),
364        })?;
365    let wy = svd
366        .solve(&dy, epsilon)
367        .map_err(|_| MeshError::RepairFailed {
368            details: "Failed to solve RBF system (degenerate constraint configuration)".to_string(),
369        })?;
370    let wz = svd
371        .solve(&dz, epsilon)
372        .map_err(|_| MeshError::RepairFailed {
373            details: "Failed to solve RBF system (degenerate constraint configuration)".to_string(),
374        })?;
375
376    // Calculate original volume for comparison
377    let original_volume = mesh.volume();
378
379    // Apply the deformation to each vertex
380    let mut morphed = mesh.clone();
381    let mut vertices_modified = 0;
382    let mut max_displacement = 0.0f64;
383    let mut total_displacement = 0.0f64;
384
385    for (idx, vertex) in morphed.vertices.iter_mut().enumerate() {
386        // Check region mask
387        if let Some(ref mask) = params.region_mask
388            && !mask.contains(&(idx as u32))
389        {
390            continue;
391        }
392
393        let p = vertex.position;
394
395        // Evaluate the RBF at this point
396        let mut disp_x = 0.0;
397        let mut disp_y = 0.0;
398        let mut disp_z = 0.0;
399
400        for i in 0..n {
401            let r = (p - params.constraints[i].source).norm();
402            let phi = evaluate_kernel(kernel, r, params.smoothness);
403            disp_x += wx[i] * phi;
404            disp_y += wy[i] * phi;
405            disp_z += wz[i] * phi;
406        }
407
408        // Add polynomial terms for thin-plate spline
409        if use_polynomial {
410            disp_x += wx[n] + wx[n + 1] * p.x + wx[n + 2] * p.y + wx[n + 3] * p.z;
411            disp_y += wy[n] + wy[n + 1] * p.x + wy[n + 2] * p.y + wy[n + 3] * p.z;
412            disp_z += wz[n] + wz[n + 1] * p.x + wz[n + 2] * p.y + wz[n + 3] * p.z;
413        }
414
415        // Apply displacement
416        vertex.position = Point3::new(p.x + disp_x, p.y + disp_y, p.z + disp_z);
417
418        let displacement = Vector3::new(disp_x, disp_y, disp_z).norm();
419        max_displacement = max_displacement.max(displacement);
420        total_displacement += displacement;
421        vertices_modified += 1;
422    }
423
424    let average_displacement = if vertices_modified > 0 {
425        total_displacement / vertices_modified as f64
426    } else {
427        0.0
428    };
429
430    // Calculate stretch/compression metrics
431    let (max_stretch, max_compression) = calculate_distortion_metrics(mesh, &morphed);
432
433    // Calculate new volume
434    let new_volume = morphed.volume();
435    let volume_ratio = if original_volume > 0.0 {
436        new_volume / original_volume
437    } else {
438        1.0
439    };
440
441    Ok(MorphResult {
442        mesh: morphed,
443        vertices_modified,
444        max_displacement,
445        average_displacement,
446        max_stretch,
447        max_compression,
448        volume_ratio,
449    })
450}
451
452/// Evaluate an RBF kernel at distance r.
453fn evaluate_kernel(kernel: RbfKernel, r: f64, smoothness: f64) -> f64 {
454    match kernel {
455        RbfKernel::ThinPlateSpline => {
456            if r < 1e-10 {
457                0.0
458            } else {
459                r * r * r.ln()
460            }
461        }
462        RbfKernel::Gaussian => {
463            let sigma = smoothness;
464            (-r * r / (sigma * sigma)).exp()
465        }
466        RbfKernel::Multiquadric => {
467            let c = smoothness;
468            (r * r + c * c).sqrt()
469        }
470        RbfKernel::InverseMultiquadric => {
471            let c = smoothness;
472            1.0 / (r * r + c * c).sqrt()
473        }
474    }
475}
476
477/// FFD morphing implementation.
478fn morph_ffd(mesh: &Mesh, params: &MorphParams) -> MeshResult<MorphResult> {
479    let (nx, ny, nz) = params.ffd_resolution;
480
481    if nx < 2 || ny < 2 || nz < 2 {
482        return Err(MeshError::RepairFailed {
483            details: "FFD resolution must be at least 2x2x2".to_string(),
484        });
485    }
486
487    // Get mesh bounds to define the lattice
488    let (min, max) = mesh.bounds().ok_or(MeshError::EmptyMesh {
489        details: "Cannot get bounds of empty mesh".to_string(),
490    })?;
491
492    // Add small padding to avoid boundary issues
493    let padding = 0.01;
494    let range = max - min;
495    let lattice_min = Point3::new(
496        min.x - padding * range.x,
497        min.y - padding * range.y,
498        min.z - padding * range.z,
499    );
500    let lattice_max = Point3::new(
501        max.x + padding * range.x,
502        max.y + padding * range.y,
503        max.z + padding * range.z,
504    );
505    let lattice_size = lattice_max - lattice_min;
506
507    // Initialize control points on a regular grid
508    let mut control_points: Vec<Vec<Vec<Point3<f64>>>> = Vec::with_capacity(nx);
509    for i in 0..nx {
510        let mut plane = Vec::with_capacity(ny);
511        for j in 0..ny {
512            let mut row = Vec::with_capacity(nz);
513            for k in 0..nz {
514                let u = i as f64 / (nx - 1) as f64;
515                let v = j as f64 / (ny - 1) as f64;
516                let w = k as f64 / (nz - 1) as f64;
517                row.push(Point3::new(
518                    lattice_min.x + u * lattice_size.x,
519                    lattice_min.y + v * lattice_size.y,
520                    lattice_min.z + w * lattice_size.z,
521                ));
522            }
523            plane.push(row);
524        }
525        control_points.push(plane);
526    }
527
528    // Apply constraints to nearest control points
529    for constraint in &params.constraints {
530        // Find lattice coordinates
531        let u = (constraint.source.x - lattice_min.x) / lattice_size.x;
532        let v = (constraint.source.y - lattice_min.y) / lattice_size.y;
533        let w = (constraint.source.z - lattice_min.z) / lattice_size.z;
534
535        // Get nearest control point indices
536        let i = ((u * (nx - 1) as f64).round() as usize).min(nx - 1);
537        let j = ((v * (ny - 1) as f64).round() as usize).min(ny - 1);
538        let k = ((w * (nz - 1) as f64).round() as usize).min(nz - 1);
539
540        // Displace the control point
541        let disp = constraint.displacement_vector() * constraint.weight;
542        control_points[i][j][k] += disp;
543
544        // Optionally influence nearby control points for smoother deformation
545        // (using a simple falloff)
546        let influence_radius = 1; // Influence neighboring cells
547        for di in 0..=influence_radius * 2 {
548            for dj in 0..=influence_radius * 2 {
549                for dk in 0..=influence_radius * 2 {
550                    let ni = (i + di).saturating_sub(influence_radius);
551                    let nj = (j + dj).saturating_sub(influence_radius);
552                    let nk = (k + dk).saturating_sub(influence_radius);
553
554                    if ni < nx && nj < ny && nk < nz && (ni != i || nj != j || nk != k) {
555                        let dist = ((ni as f64 - i as f64).powi(2)
556                            + (nj as f64 - j as f64).powi(2)
557                            + (nk as f64 - k as f64).powi(2))
558                        .sqrt();
559                        let falloff = (1.0 - dist / (influence_radius as f64 + 1.0)).max(0.0);
560                        control_points[ni][nj][nk] += disp * falloff * 0.5;
561                    }
562                }
563            }
564        }
565    }
566
567    // Calculate original volume
568    let original_volume = mesh.volume();
569
570    // Deform mesh vertices using trilinear interpolation
571    let mut morphed = mesh.clone();
572    let mut vertices_modified = 0;
573    let mut max_displacement = 0.0f64;
574    let mut total_displacement = 0.0f64;
575
576    for (idx, vertex) in morphed.vertices.iter_mut().enumerate() {
577        // Check region mask
578        if let Some(ref mask) = params.region_mask
579            && !mask.contains(&(idx as u32))
580        {
581            continue;
582        }
583
584        let p = vertex.position;
585
586        // Convert to lattice coordinates [0, 1]
587        let u = ((p.x - lattice_min.x) / lattice_size.x).clamp(0.0, 1.0);
588        let v = ((p.y - lattice_min.y) / lattice_size.y).clamp(0.0, 1.0);
589        let w = ((p.z - lattice_min.z) / lattice_size.z).clamp(0.0, 1.0);
590
591        // Evaluate the deformed position using Bernstein polynomials
592        let new_pos = evaluate_ffd(&control_points, nx, ny, nz, u, v, w);
593
594        let displacement = (new_pos - p).norm();
595        max_displacement = max_displacement.max(displacement);
596        total_displacement += displacement;
597        vertices_modified += 1;
598
599        vertex.position = new_pos;
600    }
601
602    let average_displacement = if vertices_modified > 0 {
603        total_displacement / vertices_modified as f64
604    } else {
605        0.0
606    };
607
608    // Calculate stretch/compression metrics
609    let (max_stretch, max_compression) = calculate_distortion_metrics(mesh, &morphed);
610
611    // Calculate new volume
612    let new_volume = morphed.volume();
613    let volume_ratio = if original_volume > 0.0 {
614        new_volume / original_volume
615    } else {
616        1.0
617    };
618
619    Ok(MorphResult {
620        mesh: morphed,
621        vertices_modified,
622        max_displacement,
623        average_displacement,
624        max_stretch,
625        max_compression,
626        volume_ratio,
627    })
628}
629
630/// Evaluate FFD at parameter coordinates (u, v, w) using Bernstein polynomials.
631#[allow(clippy::needless_range_loop)] // 3D lattice indexing is clearer with explicit indices
632fn evaluate_ffd(
633    control_points: &[Vec<Vec<Point3<f64>>>],
634    nx: usize,
635    ny: usize,
636    nz: usize,
637    u: f64,
638    v: f64,
639    w: f64,
640) -> Point3<f64> {
641    let mut result = Point3::new(0.0, 0.0, 0.0);
642
643    for i in 0..nx {
644        for j in 0..ny {
645            for k in 0..nz {
646                let bi = bernstein(nx - 1, i, u);
647                let bj = bernstein(ny - 1, j, v);
648                let bk = bernstein(nz - 1, k, w);
649                let weight = bi * bj * bk;
650                result += control_points[i][j][k].coords * weight;
651            }
652        }
653    }
654
655    result
656}
657
658/// Evaluate Bernstein polynomial B_{i,n}(t).
659fn bernstein(n: usize, i: usize, t: f64) -> f64 {
660    binomial(n, i) as f64 * t.powi(i as i32) * (1.0 - t).powi((n - i) as i32)
661}
662
663/// Calculate binomial coefficient C(n, k).
664fn binomial(n: usize, k: usize) -> usize {
665    if k > n {
666        return 0;
667    }
668    if k == 0 || k == n {
669        return 1;
670    }
671
672    // Use the smaller k to minimize iterations
673    let k = k.min(n - k);
674    let mut result = 1usize;
675
676    for i in 0..k {
677        result = result * (n - i) / (i + 1);
678    }
679
680    result
681}
682
683/// Calculate distortion metrics by comparing edge lengths.
684fn calculate_distortion_metrics(original: &Mesh, deformed: &Mesh) -> (f64, f64) {
685    let mut max_stretch = 1.0f64;
686    let mut max_compression = 1.0f64;
687
688    for face in &original.faces {
689        for i in 0..3 {
690            let j = (i + 1) % 3;
691            let v0_orig = original.vertices[face[i] as usize].position;
692            let v1_orig = original.vertices[face[j] as usize].position;
693            let v0_def = deformed.vertices[face[i] as usize].position;
694            let v1_def = deformed.vertices[face[j] as usize].position;
695
696            let orig_len = (v1_orig - v0_orig).norm();
697            let def_len = (v1_def - v0_def).norm();
698
699            if orig_len > 1e-10 {
700                let ratio = def_len / orig_len;
701                if ratio > 1.0 {
702                    max_stretch = max_stretch.max(ratio);
703                } else if ratio < 1.0 && ratio > 1e-10 {
704                    max_compression = max_compression.max(1.0 / ratio);
705                }
706            }
707        }
708    }
709
710    (max_stretch, max_compression)
711}
712
713#[cfg(test)]
714mod tests {
715    use super::*;
716    use crate::Vertex;
717
718    fn create_test_tetrahedron() -> Mesh {
719        let mut mesh = Mesh::new();
720        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
721        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
722        mesh.vertices.push(Vertex::from_coords(5.0, 8.66, 0.0));
723        mesh.vertices.push(Vertex::from_coords(5.0, 2.89, 8.16));
724        mesh.faces.push([0, 2, 1]); // Bottom
725        mesh.faces.push([0, 1, 3]); // Front
726        mesh.faces.push([1, 2, 3]); // Right
727        mesh.faces.push([2, 0, 3]); // Left
728        mesh
729    }
730
731    fn create_test_cube() -> Mesh {
732        let mut mesh = Mesh::new();
733        // 8 vertices of a unit cube
734        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 0.0));
735        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 0.0));
736        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 0.0));
737        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 0.0));
738        mesh.vertices.push(Vertex::from_coords(0.0, 0.0, 10.0));
739        mesh.vertices.push(Vertex::from_coords(10.0, 0.0, 10.0));
740        mesh.vertices.push(Vertex::from_coords(10.0, 10.0, 10.0));
741        mesh.vertices.push(Vertex::from_coords(0.0, 10.0, 10.0));
742
743        // 12 triangles (2 per face)
744        mesh.faces.push([0, 2, 1]); // Bottom
745        mesh.faces.push([0, 3, 2]);
746        mesh.faces.push([4, 5, 6]); // Top
747        mesh.faces.push([4, 6, 7]);
748        mesh.faces.push([0, 1, 5]); // Front
749        mesh.faces.push([0, 5, 4]);
750        mesh.faces.push([2, 3, 7]); // Back
751        mesh.faces.push([2, 7, 6]);
752        mesh.faces.push([0, 4, 7]); // Left
753        mesh.faces.push([0, 7, 3]);
754        mesh.faces.push([1, 2, 6]); // Right
755        mesh.faces.push([1, 6, 5]);
756        mesh
757    }
758
759    #[test]
760    fn test_rbf_identity_morph() {
761        let mesh = create_test_tetrahedron();
762
763        // Identity constraints (target == source) - need at least 3 well-separated points
764        let constraints = vec![
765            Constraint::point(Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 0.0)),
766            Constraint::point(Point3::new(10.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)),
767            Constraint::point(Point3::new(5.0, 8.66, 0.0), Point3::new(5.0, 8.66, 0.0)),
768            Constraint::point(Point3::new(5.0, 2.89, 8.16), Point3::new(5.0, 2.89, 8.16)),
769        ];
770
771        let params = MorphParams::rbf().with_constraints(constraints);
772        let result = morph_mesh(&mesh, &params).unwrap();
773
774        // With identity constraints, displacement should be minimal
775        assert!(
776            result.max_displacement < 0.1,
777            "Max displacement: {}",
778            result.max_displacement
779        );
780    }
781
782    #[test]
783    fn test_rbf_translation() {
784        let mesh = create_test_tetrahedron();
785
786        // Translate by (5, 0, 0) using constraints at all vertices
787        let constraints = vec![
788            Constraint::displacement(Point3::new(0.0, 0.0, 0.0), Vector3::new(5.0, 0.0, 0.0)),
789            Constraint::displacement(Point3::new(10.0, 0.0, 0.0), Vector3::new(5.0, 0.0, 0.0)),
790            Constraint::displacement(Point3::new(5.0, 8.66, 0.0), Vector3::new(5.0, 0.0, 0.0)),
791            Constraint::displacement(Point3::new(5.0, 2.89, 8.16), Vector3::new(5.0, 0.0, 0.0)),
792        ];
793
794        let params = MorphParams::rbf().with_constraints(constraints);
795        let result = morph_mesh(&mesh, &params).unwrap();
796
797        // All vertices should be displaced by approximately 5mm in X
798        for (i, vertex) in result.mesh.vertices.iter().enumerate() {
799            let orig = &mesh.vertices[i];
800            let dx = vertex.position.x - orig.position.x;
801            assert!(
802                (dx - 5.0).abs() < 0.1,
803                "Vertex {} X displacement: {}",
804                i,
805                dx
806            );
807        }
808
809        // Volume should be approximately preserved
810        assert!(
811            (result.volume_ratio - 1.0).abs() < 0.01,
812            "Volume ratio: {}",
813            result.volume_ratio
814        );
815    }
816
817    #[test]
818    fn test_rbf_local_deformation() {
819        let mesh = create_test_cube();
820
821        // Deform just one corner
822        let constraints = vec![Constraint::displacement(
823            Point3::new(0.0, 0.0, 0.0),
824            Vector3::new(0.0, 0.0, 5.0),
825        )];
826
827        let params = MorphParams::rbf_gaussian()
828            .with_constraints(constraints)
829            .with_smoothness(5.0);
830        let result = morph_mesh(&mesh, &params).unwrap();
831
832        // The corner at origin should be displaced most
833        let corner_disp = (result.mesh.vertices[0].position.z - mesh.vertices[0].position.z).abs();
834        assert!(
835            corner_disp > 4.0,
836            "Corner displacement should be significant: {}",
837            corner_disp
838        );
839
840        // Far corner should be displaced less
841        let far_corner_disp =
842            (result.mesh.vertices[6].position.z - mesh.vertices[6].position.z).abs();
843        assert!(
844            far_corner_disp < corner_disp,
845            "Far corner should be displaced less: {}",
846            far_corner_disp
847        );
848    }
849
850    #[test]
851    fn test_ffd_identity() {
852        let mesh = create_test_cube();
853
854        // No constraints = identity transformation
855        let constraints = vec![
856            Constraint::point(Point3::new(5.0, 5.0, 5.0), Point3::new(5.0, 5.0, 5.0)), // Center, no change
857        ];
858
859        let params = MorphParams::ffd().with_constraints(constraints);
860        let result = morph_mesh(&mesh, &params).unwrap();
861
862        // Displacement should be small
863        assert!(result.max_displacement < 2.0);
864    }
865
866    #[test]
867    fn test_ffd_scaling() {
868        let mesh = create_test_cube();
869
870        // Scale the mesh by moving control points outward
871        let constraints = vec![
872            Constraint::displacement(Point3::new(0.0, 0.0, 0.0), Vector3::new(-2.0, -2.0, -2.0)),
873            Constraint::displacement(Point3::new(10.0, 10.0, 10.0), Vector3::new(2.0, 2.0, 2.0)),
874        ];
875
876        let params = MorphParams::ffd_with_resolution(3, 3, 3).with_constraints(constraints);
877        let result = morph_mesh(&mesh, &params).unwrap();
878
879        // Volume should increase (scaled up)
880        assert!(
881            result.volume_ratio > 1.0,
882            "Volume should increase: {}",
883            result.volume_ratio
884        );
885    }
886
887    #[test]
888    fn test_region_mask() {
889        let mesh = create_test_cube();
890
891        // Only affect vertices 0-3 (bottom face)
892        let mut region: HashSet<u32> = HashSet::new();
893        region.insert(0);
894        region.insert(1);
895        region.insert(2);
896        region.insert(3);
897
898        // Need multiple well-separated constraints for numerical stability
899        let constraints = vec![
900            Constraint::displacement(Point3::new(0.0, 0.0, 0.0), Vector3::new(0.0, 0.0, -2.0)),
901            Constraint::displacement(Point3::new(10.0, 0.0, 0.0), Vector3::new(0.0, 0.0, -2.0)),
902            Constraint::displacement(Point3::new(10.0, 10.0, 0.0), Vector3::new(0.0, 0.0, -2.0)),
903            Constraint::displacement(Point3::new(0.0, 10.0, 0.0), Vector3::new(0.0, 0.0, -2.0)),
904        ];
905
906        let params = MorphParams::rbf()
907            .with_constraints(constraints)
908            .with_region(region);
909        let result = morph_mesh(&mesh, &params).unwrap();
910
911        assert_eq!(result.vertices_modified, 4);
912
913        // Top vertices should not be displaced
914        for i in 4..8 {
915            let orig = mesh.vertices[i].position;
916            let new = result.mesh.vertices[i].position;
917            assert!(
918                (orig - new).norm() < 1e-10,
919                "Top vertex {} should not move",
920                i
921            );
922        }
923    }
924
925    #[test]
926    fn test_empty_mesh_error() {
927        let mesh = Mesh::new();
928        let constraints = vec![Constraint::point(
929            Point3::new(0.0, 0.0, 0.0),
930            Point3::new(1.0, 0.0, 0.0),
931        )];
932        let params = MorphParams::rbf().with_constraints(constraints);
933
934        assert!(matches!(
935            morph_mesh(&mesh, &params),
936            Err(MeshError::EmptyMesh { .. })
937        ));
938    }
939
940    #[test]
941    fn test_no_constraints_error() {
942        let mesh = create_test_tetrahedron();
943        let params = MorphParams::rbf();
944
945        assert!(matches!(
946            morph_mesh(&mesh, &params),
947            Err(MeshError::RepairFailed { .. })
948        ));
949    }
950
951    #[test]
952    fn test_distortion_metrics() {
953        let mesh = create_test_cube();
954
955        // Create a significant stretch in X direction
956        let constraints = vec![
957            Constraint::displacement(Point3::new(10.0, 0.0, 0.0), Vector3::new(5.0, 0.0, 0.0)),
958            Constraint::displacement(Point3::new(10.0, 10.0, 0.0), Vector3::new(5.0, 0.0, 0.0)),
959            Constraint::displacement(Point3::new(10.0, 0.0, 10.0), Vector3::new(5.0, 0.0, 0.0)),
960            Constraint::displacement(Point3::new(10.0, 10.0, 10.0), Vector3::new(5.0, 0.0, 0.0)),
961        ];
962
963        let params = MorphParams::rbf().with_constraints(constraints);
964        let result = morph_mesh(&mesh, &params).unwrap();
965
966        // Should have stretch > 1
967        assert!(
968            result.max_stretch > 1.0,
969            "Should have stretch: {}",
970            result.max_stretch
971        );
972        assert!(result.has_significant_distortion(0.1));
973    }
974
975    #[test]
976    fn test_weighted_constraints() {
977        let mesh = create_test_tetrahedron();
978
979        // Multiple well-separated constraints for numerical stability
980        let constraints = vec![
981            Constraint::weighted(Point3::new(0.0, 0.0, 0.0), Point3::new(0.0, 0.0, 10.0), 2.0),
982            Constraint::weighted(
983                Point3::new(10.0, 0.0, 0.0),
984                Point3::new(10.0, 0.0, 5.0),
985                1.0,
986            ),
987            Constraint::weighted(
988                Point3::new(5.0, 8.66, 0.0),
989                Point3::new(5.0, 8.66, 3.0),
990                1.0,
991            ),
992        ];
993
994        let params = MorphParams::rbf().with_constraints(constraints);
995        let result = morph_mesh(&mesh, &params).unwrap();
996
997        // Higher weighted constraint should have more influence
998        let v0_z = result.mesh.vertices[0].position.z;
999        let v1_z = result.mesh.vertices[1].position.z;
1000
1001        // Vertex 0 has higher weight target at z=10, vertex 1 at z=5
1002        // Expect v0 to be displaced more than v1
1003        assert!(v0_z > v1_z, "v0_z={} should be > v1_z={}", v0_z, v1_z);
1004    }
1005
1006    #[test]
1007    fn test_different_kernels() {
1008        let mesh = create_test_cube();
1009        // Multiple well-separated constraints for numerical stability
1010        let constraints = vec![
1011            Constraint::displacement(Point3::new(5.0, 5.0, 10.0), Vector3::new(0.0, 0.0, 5.0)),
1012            Constraint::displacement(Point3::new(0.0, 0.0, 10.0), Vector3::new(0.0, 0.0, 3.0)),
1013            Constraint::displacement(Point3::new(10.0, 10.0, 10.0), Vector3::new(0.0, 0.0, 3.0)),
1014        ];
1015
1016        // Test all kernel types
1017        for kernel in [
1018            RbfKernel::ThinPlateSpline,
1019            RbfKernel::Gaussian,
1020            RbfKernel::Multiquadric,
1021            RbfKernel::InverseMultiquadric,
1022        ] {
1023            let params = MorphParams {
1024                algorithm: MorphAlgorithm::Rbf(kernel),
1025                constraints: constraints.clone(),
1026                smoothness: 5.0,
1027                ..Default::default()
1028            };
1029            let result = morph_mesh(&mesh, &params).unwrap();
1030            assert!(
1031                result.max_displacement > 0.0,
1032                "Kernel {:?} should produce displacement",
1033                kernel
1034            );
1035        }
1036    }
1037
1038    #[test]
1039    fn test_bernstein_basis() {
1040        // B_{0,2}(0.5) = (1-t)^2 = 0.25
1041        assert!((bernstein(2, 0, 0.5) - 0.25).abs() < 1e-10);
1042        // B_{1,2}(0.5) = 2t(1-t) = 0.5
1043        assert!((bernstein(2, 1, 0.5) - 0.5).abs() < 1e-10);
1044        // B_{2,2}(0.5) = t^2 = 0.25
1045        assert!((bernstein(2, 2, 0.5) - 0.25).abs() < 1e-10);
1046
1047        // Sum should be 1
1048        let sum: f64 = (0..=3).map(|i| bernstein(3, i, 0.3)).sum();
1049        assert!((sum - 1.0).abs() < 1e-10);
1050    }
1051
1052    #[test]
1053    fn test_binomial_coefficients() {
1054        assert_eq!(binomial(0, 0), 1);
1055        assert_eq!(binomial(4, 0), 1);
1056        assert_eq!(binomial(4, 4), 1);
1057        assert_eq!(binomial(4, 2), 6);
1058        assert_eq!(binomial(5, 2), 10);
1059        assert_eq!(binomial(10, 5), 252);
1060    }
1061}