1use serde::{Deserialize, Serialize};
11
12use crate::delta_painter::MirrorAxis;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TargetInfo {
19 pub name: String,
21 pub vertex_count: usize,
23 pub affected_count: usize,
25 pub max_displacement: f64,
27 pub average_displacement: f64,
29 pub sparsity: f64,
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct SymmetryReport {
36 pub symmetric: bool,
38 pub max_asymmetry: f64,
40 pub asymmetric_vertices: Vec<usize>,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ValidationWarning {
47 pub kind: WarningKind,
49 pub message: String,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55pub enum WarningKind {
56 ExcessiveDisplacement,
58 SelfIntersectionRisk,
60 Asymmetry,
62 EmptyTarget,
64}
65
66#[inline]
69fn magnitude(v: [f64; 3]) -> f64 {
70 (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
71}
72
73#[inline]
74fn is_zero(v: &[f64; 3], threshold: f64) -> bool {
75 magnitude(*v) < threshold
76}
77
78const DEFAULT_ZERO_THRESHOLD: f64 = 1e-10;
80
81pub struct TargetValidator;
85
86impl TargetValidator {
87 pub fn validate(
91 deltas: &[[f64; 3]],
92 vertex_count: usize,
93 ) -> anyhow::Result<Vec<ValidationWarning>> {
94 if deltas.len() != vertex_count {
95 anyhow::bail!(
96 "deltas length {} != vertex_count {}",
97 deltas.len(),
98 vertex_count
99 );
100 }
101
102 let mut warnings = Vec::new();
103
104 let affected = deltas
106 .iter()
107 .filter(|d| !is_zero(d, DEFAULT_ZERO_THRESHOLD))
108 .count();
109 if affected == 0 {
110 warnings.push(ValidationWarning {
111 kind: WarningKind::EmptyTarget,
112 message: "Target has no non-zero deltas".to_owned(),
113 });
114 }
115
116 let max_disp = deltas.iter().map(|d| magnitude(*d)).fold(0.0_f64, f64::max);
118 if max_disp > 1.0 {
119 warnings.push(ValidationWarning {
120 kind: WarningKind::ExcessiveDisplacement,
121 message: format!(
122 "Maximum displacement {:.4} exceeds safe limit 1.0",
123 max_disp
124 ),
125 });
126 }
127
128 let mut axis_min = [f64::MAX; 3];
132 let mut axis_max = [f64::MIN; 3];
133 for d in deltas {
134 for i in 0..3 {
135 if d[i] < axis_min[i] {
136 axis_min[i] = d[i];
137 }
138 if d[i] > axis_max[i] {
139 axis_max[i] = d[i];
140 }
141 }
142 }
143 for i in 0..3 {
144 let range = axis_max[i] - axis_min[i];
145 if range > 1.0 {
146 let axis_name = match i {
147 0 => "X",
148 1 => "Y",
149 _ => "Z",
150 };
151 warnings.push(ValidationWarning {
152 kind: WarningKind::SelfIntersectionRisk,
153 message: format!(
154 "Delta range on {} axis is {:.4}, which may cause self-intersection",
155 axis_name, range
156 ),
157 });
158 }
159 }
160
161 Ok(warnings)
162 }
163
164 pub fn check_symmetry(
169 deltas: &[[f64; 3]],
170 positions: &[[f64; 3]],
171 tolerance: f64,
172 ) -> SymmetryReport {
173 let n = deltas.len().min(positions.len());
174 let tol = tolerance.max(1e-12);
175 let mut max_asym = 0.0_f64;
176 let mut asym_verts = Vec::new();
177
178 for i in 0..n {
179 let pos = positions[i];
180 if pos[0] < 0.0 {
182 continue;
183 }
184
185 let mirror_pos = [-pos[0], pos[1], pos[2]];
187 let mut best_j: Option<usize> = None;
188 let mut best_dsq = f64::MAX;
189 for (j, jpos) in positions[..n].iter().enumerate() {
190 let dp0 = jpos[0] - mirror_pos[0];
191 let dp1 = jpos[1] - mirror_pos[1];
192 let dp2 = jpos[2] - mirror_pos[2];
193 let dsq = dp0 * dp0 + dp1 * dp1 + dp2 * dp2;
194 if dsq < best_dsq {
195 best_dsq = dsq;
196 best_j = Some(j);
197 }
198 }
199
200 if let Some(j) = best_j {
201 if best_dsq.sqrt() > tol * 10.0 {
202 continue;
204 }
205 let expected = [-deltas[i][0], deltas[i][1], deltas[i][2]];
207 let diff = [
208 deltas[j][0] - expected[0],
209 deltas[j][1] - expected[1],
210 deltas[j][2] - expected[2],
211 ];
212 let asym = magnitude(diff);
213 if asym > max_asym {
214 max_asym = asym;
215 }
216 if asym > tol {
217 asym_verts.push(i);
218 }
219 }
220 }
221
222 SymmetryReport {
223 symmetric: asym_verts.is_empty(),
224 max_asymmetry: max_asym,
225 asymmetric_vertices: asym_verts,
226 }
227 }
228
229 pub fn check_magnitude(deltas: &[[f64; 3]], max_displacement: f64) -> Vec<usize> {
231 deltas
232 .iter()
233 .enumerate()
234 .filter_map(|(i, d)| {
235 if magnitude(*d) > max_displacement {
236 Some(i)
237 } else {
238 None
239 }
240 })
241 .collect()
242 }
243}
244
245pub struct TargetInspector;
249
250impl TargetInspector {
251 pub fn inspect(deltas: &[[f64; 3]]) -> TargetInfo {
253 Self::inspect_named(deltas, "")
254 }
255
256 pub fn inspect_named(deltas: &[[f64; 3]], name: &str) -> TargetInfo {
258 let vertex_count = deltas.len();
259 let mut affected_count = 0usize;
260 let mut max_disp = 0.0_f64;
261 let mut sum_disp = 0.0_f64;
262
263 for d in deltas {
264 let m = magnitude(*d);
265 if m > DEFAULT_ZERO_THRESHOLD {
266 affected_count += 1;
267 }
268 if m > max_disp {
269 max_disp = m;
270 }
271 sum_disp += m;
272 }
273
274 let average_displacement = if vertex_count > 0 {
275 sum_disp / vertex_count as f64
276 } else {
277 0.0
278 };
279
280 let sparsity = if vertex_count > 0 {
281 1.0 - (affected_count as f64 / vertex_count as f64)
282 } else {
283 1.0
284 };
285
286 TargetInfo {
287 name: name.to_owned(),
288 vertex_count,
289 affected_count,
290 max_displacement: max_disp,
291 average_displacement,
292 sparsity,
293 }
294 }
295
296 pub fn affected_vertices(deltas: &[[f64; 3]], threshold: f64) -> Vec<usize> {
298 deltas
299 .iter()
300 .enumerate()
301 .filter_map(|(i, d)| {
302 if magnitude(*d) > threshold {
303 Some(i)
304 } else {
305 None
306 }
307 })
308 .collect()
309 }
310
311 pub fn bounding_box(deltas: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
313 if deltas.is_empty() {
314 return ([0.0; 3], [0.0; 3]);
315 }
316 let mut mn = [f64::MAX; 3];
317 let mut mx = [f64::MIN; 3];
318 for d in deltas {
319 for i in 0..3 {
320 if d[i] < mn[i] {
321 mn[i] = d[i];
322 }
323 if d[i] > mx[i] {
324 mx[i] = d[i];
325 }
326 }
327 }
328 (mn, mx)
329 }
330
331 pub fn max_displacement(deltas: &[[f64; 3]]) -> f64 {
333 deltas.iter().map(|d| magnitude(*d)).fold(0.0_f64, f64::max)
334 }
335
336 pub fn rms_displacement(deltas: &[[f64; 3]]) -> f64 {
338 if deltas.is_empty() {
339 return 0.0;
340 }
341 let sum_sq: f64 = deltas
342 .iter()
343 .map(|d| d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
344 .sum();
345 (sum_sq / deltas.len() as f64).sqrt()
346 }
347}
348
349pub fn merge_targets(targets: &[(&str, &[[f64; 3]], f64)]) -> anyhow::Result<Vec<[f64; 3]>> {
356 if targets.is_empty() {
357 anyhow::bail!("no targets to merge");
358 }
359
360 let vertex_count = targets[0].1.len();
361 for (name, deltas, _) in targets.iter().skip(1) {
362 if deltas.len() != vertex_count {
363 anyhow::bail!(
364 "target '{}' has {} vertices, expected {}",
365 name,
366 deltas.len(),
367 vertex_count
368 );
369 }
370 }
371
372 let mut result = vec![[0.0_f64; 3]; vertex_count];
373 for (_name, deltas, weight) in targets {
374 let w = *weight;
375 for (i, d) in deltas.iter().enumerate() {
376 result[i][0] += d[0] * w;
377 result[i][1] += d[1] * w;
378 result[i][2] += d[2] * w;
379 }
380 }
381 Ok(result)
382}
383
384pub fn mirror_target(
390 deltas: &[[f64; 3]],
391 positions: &[[f64; 3]],
392 axis: MirrorAxis,
393 tolerance: f64,
394) -> anyhow::Result<Vec<[f64; 3]>> {
395 let n = deltas.len();
396 if positions.len() != n {
397 anyhow::bail!(
398 "deltas length {} != positions length {}",
399 n,
400 positions.len()
401 );
402 }
403 if tolerance <= 0.0 {
404 anyhow::bail!("tolerance must be positive, got {}", tolerance);
405 }
406
407 let ax = axis.idx();
408 let tol_sq = tolerance * tolerance;
409 let mut result = deltas.to_vec();
410
411 for i in 0..n {
412 let pos = positions[i];
413 if pos[ax] < 0.0 {
415 continue;
416 }
417
418 let mut mirror_pos = pos;
419 mirror_pos[ax] = -mirror_pos[ax];
420
421 let mut best_j: Option<usize> = None;
422 let mut best_dsq = f64::MAX;
423 for (j, jpos) in positions[..n].iter().enumerate() {
424 let dp0 = jpos[0] - mirror_pos[0];
425 let dp1 = jpos[1] - mirror_pos[1];
426 let dp2 = jpos[2] - mirror_pos[2];
427 let dsq = dp0 * dp0 + dp1 * dp1 + dp2 * dp2;
428 if dsq < best_dsq {
429 best_dsq = dsq;
430 best_j = Some(j);
431 }
432 }
433
434 if let Some(j) = best_j {
435 if best_dsq <= tol_sq {
436 let mut d = deltas[i];
437 d[ax] = -d[ax];
438 result[j] = d;
439 }
440 }
441 }
442 Ok(result)
443}
444
445pub fn subtract_targets(a: &[[f64; 3]], b: &[[f64; 3]]) -> anyhow::Result<Vec<[f64; 3]>> {
447 if a.len() != b.len() {
448 anyhow::bail!("target lengths differ: {} vs {}", a.len(), b.len());
449 }
450 Ok(a.iter()
451 .zip(b.iter())
452 .map(|(va, vb)| [va[0] - vb[0], va[1] - vb[1], va[2] - vb[2]])
453 .collect())
454}
455
456pub fn add_targets(a: &[[f64; 3]], b: &[[f64; 3]]) -> anyhow::Result<Vec<[f64; 3]>> {
458 if a.len() != b.len() {
459 anyhow::bail!("target lengths differ: {} vs {}", a.len(), b.len());
460 }
461 Ok(a.iter()
462 .zip(b.iter())
463 .map(|(va, vb)| [va[0] + vb[0], va[1] + vb[1], va[2] + vb[2]])
464 .collect())
465}
466
467pub fn scale_target(deltas: &[[f64; 3]], factor: f64) -> Vec<[f64; 3]> {
469 deltas
470 .iter()
471 .map(|d| [d[0] * factor, d[1] * factor, d[2] * factor])
472 .collect()
473}
474
475pub fn clamp_target(deltas: &[[f64; 3]], max_magnitude: f64) -> Vec<[f64; 3]> {
477 deltas
478 .iter()
479 .map(|d| {
480 let m = magnitude(*d);
481 if m > max_magnitude && m > 1e-15 {
482 let scale = max_magnitude / m;
483 [d[0] * scale, d[1] * scale, d[2] * scale]
484 } else {
485 *d
486 }
487 })
488 .collect()
489}
490
491pub fn sparsify_target(deltas: &[[f64; 3]], threshold: f64) -> Vec<[f64; 3]> {
493 deltas
494 .iter()
495 .map(|d| {
496 if magnitude(*d) < threshold {
497 [0.0; 3]
498 } else {
499 *d
500 }
501 })
502 .collect()
503}
504
505#[cfg(test)]
510mod tests {
511 use super::*;
512
513 #[test]
514 fn test_validate_empty_target() {
515 let deltas = vec![[0.0; 3]; 5];
516 let warnings = TargetValidator::validate(&deltas, 5).expect("validate ok");
517 assert!(warnings.iter().any(|w| w.kind == WarningKind::EmptyTarget));
518 }
519
520 #[test]
521 fn test_validate_excessive_displacement() {
522 let mut deltas = vec![[0.0; 3]; 5];
523 deltas[0] = [2.0, 0.0, 0.0]; let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
525 assert!(warnings
526 .iter()
527 .any(|w| w.kind == WarningKind::ExcessiveDisplacement));
528 }
529
530 #[test]
531 fn test_validate_length_mismatch() {
532 let deltas = vec![[0.0; 3]; 5];
533 assert!(TargetValidator::validate(&deltas, 10).is_err());
534 }
535
536 #[test]
537 fn test_validate_clean_target() {
538 let mut deltas = vec![[0.0; 3]; 5];
539 deltas[0] = [0.1, 0.0, 0.0];
540 let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
541 assert!(
543 warnings.is_empty(),
544 "expected no warnings, got {:?}",
545 warnings
546 );
547 }
548
549 #[test]
550 fn test_check_symmetry_symmetric() {
551 let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
553 let deltas = vec![[0.1, 0.2, 0.3], [-0.1, 0.2, 0.3]]; let report = TargetValidator::check_symmetry(&deltas, &positions, 0.01);
555 assert!(report.symmetric, "should be symmetric");
556 assert!(report.max_asymmetry < 0.01);
557 }
558
559 #[test]
560 fn test_check_symmetry_asymmetric() {
561 let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
562 let deltas = vec![[0.1, 0.2, 0.3], [0.5, 0.0, 0.0]]; let report = TargetValidator::check_symmetry(&deltas, &positions, 0.01);
564 assert!(!report.symmetric, "should be asymmetric");
565 }
566
567 #[test]
568 fn test_check_magnitude() {
569 let deltas = vec![[0.1, 0.0, 0.0], [2.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
570 let exceeding = TargetValidator::check_magnitude(&deltas, 1.0);
571 assert_eq!(exceeding, vec![1]);
572 }
573
574 #[test]
575 fn test_inspect_basic() {
576 let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.5, 0.0]];
577 let info = TargetInspector::inspect(&deltas);
578 assert_eq!(info.vertex_count, 3);
579 assert_eq!(info.affected_count, 2);
580 assert!((info.max_displacement - 1.0).abs() < 1e-10);
581 assert!(info.sparsity > 0.0 && info.sparsity < 1.0);
582 }
583
584 #[test]
585 fn test_inspect_empty() {
586 let deltas: Vec<[f64; 3]> = vec![];
587 let info = TargetInspector::inspect(&deltas);
588 assert_eq!(info.vertex_count, 0);
589 assert_eq!(info.affected_count, 0);
590 assert!((info.max_displacement).abs() < 1e-15);
591 }
592
593 #[test]
594 fn test_affected_vertices() {
595 let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.5, 0.0]];
596 let affected = TargetInspector::affected_vertices(&deltas, 0.01);
597 assert_eq!(affected, vec![0, 2]);
598 }
599
600 #[test]
601 fn test_bounding_box() {
602 let deltas = vec![[1.0, -2.0, 3.0], [-1.0, 4.0, 0.5]];
603 let (mn, mx) = TargetInspector::bounding_box(&deltas);
604 assert!((mn[0] - (-1.0)).abs() < 1e-15);
605 assert!((mx[0] - 1.0).abs() < 1e-15);
606 assert!((mn[1] - (-2.0)).abs() < 1e-15);
607 assert!((mx[1] - 4.0).abs() < 1e-15);
608 assert!((mn[2] - 0.5).abs() < 1e-15);
609 assert!((mx[2] - 3.0).abs() < 1e-15);
610 }
611
612 #[test]
613 fn test_bounding_box_empty() {
614 let (mn, mx) = TargetInspector::bounding_box(&[]);
615 assert_eq!(mn, [0.0; 3]);
616 assert_eq!(mx, [0.0; 3]);
617 }
618
619 #[test]
620 fn test_max_displacement() {
621 let deltas = vec![[1.0, 0.0, 0.0], [0.0, 3.0, 4.0]]; let max_d = TargetInspector::max_displacement(&deltas);
623 assert!((max_d - 5.0).abs() < 1e-10);
624 }
625
626 #[test]
627 fn test_rms_displacement() {
628 let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
629 let rms = TargetInspector::rms_displacement(&deltas);
630 assert!((rms - (0.5_f64).sqrt()).abs() < 1e-10);
632 }
633
634 #[test]
635 fn test_merge_targets_single() {
636 let d = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
637 let result = merge_targets(&[("a", &d, 1.0)]).expect("merge ok");
638 assert_eq!(result.len(), 2);
639 assert!((result[0][0] - 1.0).abs() < 1e-15);
640 }
641
642 #[test]
643 fn test_merge_targets_weighted() {
644 let a = [[1.0, 0.0, 0.0]];
645 let b = [[0.0, 2.0, 0.0]];
646 let result = merge_targets(&[("a", &a[..], 0.5), ("b", &b[..], 0.5)]).expect("ok");
647 assert!((result[0][0] - 0.5).abs() < 1e-15);
648 assert!((result[0][1] - 1.0).abs() < 1e-15);
649 }
650
651 #[test]
652 fn test_merge_targets_empty() {
653 assert!(merge_targets(&[]).is_err());
654 }
655
656 #[test]
657 fn test_merge_targets_length_mismatch() {
658 let a = [[1.0, 0.0, 0.0]];
659 let b = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
660 assert!(merge_targets(&[("a", &a[..], 1.0), ("b", &b[..], 1.0)]).is_err());
661 }
662
663 #[test]
664 fn test_mirror_target_x() {
665 let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
666 let deltas = vec![[0.5, 0.3, 0.1], [0.0, 0.0, 0.0]];
667 let mirrored = mirror_target(&deltas, &positions, MirrorAxis::X, 0.1).expect("mirror ok");
668 assert!((mirrored[1][0] - (-0.5)).abs() < 1e-10);
670 assert!((mirrored[1][1] - 0.3).abs() < 1e-10);
671 assert!((mirrored[1][2] - 0.1).abs() < 1e-10);
672 }
673
674 #[test]
675 fn test_mirror_target_length_mismatch() {
676 let d = vec![[0.0; 3]; 3];
677 let p = vec![[0.0; 3]; 2];
678 assert!(mirror_target(&d, &p, MirrorAxis::X, 0.1).is_err());
679 }
680
681 #[test]
682 fn test_subtract_targets() {
683 let a = vec![[1.0, 2.0, 3.0]];
684 let b = vec![[0.5, 0.5, 0.5]];
685 let result = subtract_targets(&a, &b).expect("ok");
686 assert!((result[0][0] - 0.5).abs() < 1e-15);
687 assert!((result[0][1] - 1.5).abs() < 1e-15);
688 assert!((result[0][2] - 2.5).abs() < 1e-15);
689 }
690
691 #[test]
692 fn test_add_targets() {
693 let a = vec![[1.0, 2.0, 3.0]];
694 let b = vec![[0.5, 0.5, 0.5]];
695 let result = add_targets(&a, &b).expect("ok");
696 assert!((result[0][0] - 1.5).abs() < 1e-15);
697 }
698
699 #[test]
700 fn test_scale_target() {
701 let d = vec![[1.0, 2.0, 3.0]];
702 let result = scale_target(&d, 2.0);
703 assert!((result[0][0] - 2.0).abs() < 1e-15);
704 assert!((result[0][1] - 4.0).abs() < 1e-15);
705 }
706
707 #[test]
708 fn test_clamp_target() {
709 let d = vec![[10.0, 0.0, 0.0], [0.1, 0.0, 0.0]];
710 let clamped = clamp_target(&d, 1.0);
711 assert!((magnitude(clamped[0]) - 1.0).abs() < 1e-10);
712 assert!((clamped[1][0] - 0.1).abs() < 1e-15); }
714
715 #[test]
716 fn test_sparsify_target() {
717 let d = vec![[1.0, 0.0, 0.0], [0.001, 0.0, 0.0], [0.0, 0.5, 0.0]];
718 let sparse = sparsify_target(&d, 0.01);
719 assert!((sparse[0][0] - 1.0).abs() < 1e-15);
720 assert_eq!(sparse[1], [0.0; 3]); assert!((sparse[2][1] - 0.5).abs() < 1e-15);
722 }
723
724 #[test]
725 fn test_inspect_named() {
726 let deltas = vec![[1.0, 0.0, 0.0]];
727 let info = TargetInspector::inspect_named(&deltas, "my_target");
728 assert_eq!(info.name, "my_target");
729 assert_eq!(info.vertex_count, 1);
730 assert_eq!(info.affected_count, 1);
731 }
732
733 #[test]
734 fn test_self_intersection_warning() {
735 let mut deltas = vec![[0.0; 3]; 5];
736 deltas[0] = [0.8, 0.0, 0.0];
737 deltas[1] = [-0.8, 0.0, 0.0]; let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
739 assert!(warnings
740 .iter()
741 .any(|w| w.kind == WarningKind::SelfIntersectionRisk));
742 }
743}