1use crate::{Mesh, MeshError, MeshResult};
45use nalgebra::{DMatrix, DVector, Point3, Vector3};
46use std::collections::HashSet;
47
48#[derive(Debug, Clone)]
50pub struct MorphParams {
51 pub algorithm: MorphAlgorithm,
53
54 pub constraints: Vec<Constraint>,
56
57 pub region_mask: Option<HashSet<u32>>,
60
61 pub smoothness: f64,
64
65 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 pub fn rbf() -> Self {
85 Self {
86 algorithm: MorphAlgorithm::Rbf(RbfKernel::ThinPlateSpline),
87 ..Default::default()
88 }
89 }
90
91 pub fn rbf_gaussian() -> Self {
93 Self {
94 algorithm: MorphAlgorithm::Rbf(RbfKernel::Gaussian),
95 ..Default::default()
96 }
97 }
98
99 pub fn rbf_multiquadric() -> Self {
101 Self {
102 algorithm: MorphAlgorithm::Rbf(RbfKernel::Multiquadric),
103 ..Default::default()
104 }
105 }
106
107 pub fn ffd() -> Self {
109 Self {
110 algorithm: MorphAlgorithm::Ffd,
111 ..Default::default()
112 }
113 }
114
115 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 pub fn with_constraints(mut self, constraints: Vec<Constraint>) -> Self {
126 self.constraints = constraints;
127 self
128 }
129
130 pub fn with_region(mut self, vertex_indices: HashSet<u32>) -> Self {
132 self.region_mask = Some(vertex_indices);
133 self
134 }
135
136 pub fn with_smoothness(mut self, smoothness: f64) -> Self {
138 self.smoothness = smoothness;
139 self
140 }
141
142 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#[derive(Debug, Clone, Copy, PartialEq)]
151pub enum MorphAlgorithm {
152 Rbf(RbfKernel),
154 Ffd,
156}
157
158#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum RbfKernel {
161 ThinPlateSpline,
164
165 Gaussian,
168
169 Multiquadric,
172
173 InverseMultiquadric,
176}
177
178#[derive(Debug, Clone)]
180pub struct Constraint {
181 pub source: Point3<f64>,
183
184 pub target: Point3<f64>,
186
187 pub weight: f64,
189}
190
191impl Constraint {
192 pub fn point(source: Point3<f64>, target: Point3<f64>) -> Self {
194 Self {
195 source,
196 target,
197 weight: 1.0,
198 }
199 }
200
201 pub fn weighted(source: Point3<f64>, target: Point3<f64>, weight: f64) -> Self {
203 Self {
204 source,
205 target,
206 weight,
207 }
208 }
209
210 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 pub fn displacement_vector(&self) -> Vector3<f64> {
221 self.target - self.source
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct MorphResult {
228 pub mesh: Mesh,
230
231 pub vertices_modified: usize,
233
234 pub max_displacement: f64,
236
237 pub average_displacement: f64,
239
240 pub max_stretch: f64,
242
243 pub max_compression: f64,
245
246 pub volume_ratio: f64,
248}
249
250impl MorphResult {
251 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 pub fn has_significant_volume_change(&self, threshold: f64) -> bool {
260 (self.volume_ratio - 1.0).abs() > threshold
261 }
262}
263
264pub 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
300fn morph_rbf(mesh: &Mesh, params: &MorphParams, kernel: RbfKernel) -> MeshResult<MorphResult> {
302 let n = params.constraints.len();
303
304 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 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 matrix[(i, i)] += 1e-8;
321 }
322
323 if use_polynomial {
325 for i in 0..n {
326 let p = ¶ms.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 for i in n..matrix_size {
339 matrix[(i, i)] += 1e-10;
340 }
341 }
342
343 let svd = matrix.svd(true, true);
345
346 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 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 let original_volume = mesh.volume();
378
379 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 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 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 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 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 let (max_stretch, max_compression) = calculate_distortion_metrics(mesh, &morphed);
432
433 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
452fn 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
477fn 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 let (min, max) = mesh.bounds().ok_or(MeshError::EmptyMesh {
489 details: "Cannot get bounds of empty mesh".to_string(),
490 })?;
491
492 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 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 for constraint in ¶ms.constraints {
530 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 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 let disp = constraint.displacement_vector() * constraint.weight;
542 control_points[i][j][k] += disp;
543
544 let influence_radius = 1; 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 let original_volume = mesh.volume();
569
570 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 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 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 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 let (max_stretch, max_compression) = calculate_distortion_metrics(mesh, &morphed);
610
611 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#[allow(clippy::needless_range_loop)] fn 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
658fn 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
663fn 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 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
683fn 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]); mesh.faces.push([0, 1, 3]); mesh.faces.push([1, 2, 3]); mesh.faces.push([2, 0, 3]); mesh
729 }
730
731 fn create_test_cube() -> Mesh {
732 let mut mesh = Mesh::new();
733 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 mesh.faces.push([0, 2, 1]); mesh.faces.push([0, 3, 2]);
746 mesh.faces.push([4, 5, 6]); mesh.faces.push([4, 6, 7]);
748 mesh.faces.push([0, 1, 5]); mesh.faces.push([0, 5, 4]);
750 mesh.faces.push([2, 3, 7]); mesh.faces.push([2, 7, 6]);
752 mesh.faces.push([0, 4, 7]); mesh.faces.push([0, 7, 3]);
754 mesh.faces.push([1, 2, 6]); 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 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, ¶ms).unwrap();
773
774 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 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, ¶ms).unwrap();
796
797 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 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 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, ¶ms).unwrap();
831
832 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 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 let constraints = vec![
856 Constraint::point(Point3::new(5.0, 5.0, 5.0), Point3::new(5.0, 5.0, 5.0)), ];
858
859 let params = MorphParams::ffd().with_constraints(constraints);
860 let result = morph_mesh(&mesh, ¶ms).unwrap();
861
862 assert!(result.max_displacement < 2.0);
864 }
865
866 #[test]
867 fn test_ffd_scaling() {
868 let mesh = create_test_cube();
869
870 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, ¶ms).unwrap();
878
879 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 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 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, ¶ms).unwrap();
910
911 assert_eq!(result.vertices_modified, 4);
912
913 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, ¶ms),
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, ¶ms),
947 Err(MeshError::RepairFailed { .. })
948 ));
949 }
950
951 #[test]
952 fn test_distortion_metrics() {
953 let mesh = create_test_cube();
954
955 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, ¶ms).unwrap();
965
966 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 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, ¶ms).unwrap();
996
997 let v0_z = result.mesh.vertices[0].position.z;
999 let v1_z = result.mesh.vertices[1].position.z;
1000
1001 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 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 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, ¶ms).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 assert!((bernstein(2, 0, 0.5) - 0.25).abs() < 1e-10);
1042 assert!((bernstein(2, 1, 0.5) - 0.5).abs() < 1e-10);
1044 assert!((bernstein(2, 2, 0.5) - 0.25).abs() < 1e-10);
1046
1047 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}