use serde::{Deserialize, Serialize};
use crate::delta_painter::MirrorAxis;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TargetInfo {
pub name: String,
pub vertex_count: usize,
pub affected_count: usize,
pub max_displacement: f64,
pub average_displacement: f64,
pub sparsity: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymmetryReport {
pub symmetric: bool,
pub max_asymmetry: f64,
pub asymmetric_vertices: Vec<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub kind: WarningKind,
pub message: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum WarningKind {
ExcessiveDisplacement,
SelfIntersectionRisk,
Asymmetry,
EmptyTarget,
}
#[inline]
fn magnitude(v: [f64; 3]) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
#[inline]
fn is_zero(v: &[f64; 3], threshold: f64) -> bool {
magnitude(*v) < threshold
}
const DEFAULT_ZERO_THRESHOLD: f64 = 1e-10;
pub struct TargetValidator;
impl TargetValidator {
pub fn validate(
deltas: &[[f64; 3]],
vertex_count: usize,
) -> anyhow::Result<Vec<ValidationWarning>> {
if deltas.len() != vertex_count {
anyhow::bail!(
"deltas length {} != vertex_count {}",
deltas.len(),
vertex_count
);
}
let mut warnings = Vec::new();
let affected = deltas
.iter()
.filter(|d| !is_zero(d, DEFAULT_ZERO_THRESHOLD))
.count();
if affected == 0 {
warnings.push(ValidationWarning {
kind: WarningKind::EmptyTarget,
message: "Target has no non-zero deltas".to_owned(),
});
}
let max_disp = deltas.iter().map(|d| magnitude(*d)).fold(0.0_f64, f64::max);
if max_disp > 1.0 {
warnings.push(ValidationWarning {
kind: WarningKind::ExcessiveDisplacement,
message: format!(
"Maximum displacement {:.4} exceeds safe limit 1.0",
max_disp
),
});
}
let mut axis_min = [f64::MAX; 3];
let mut axis_max = [f64::MIN; 3];
for d in deltas {
for i in 0..3 {
if d[i] < axis_min[i] {
axis_min[i] = d[i];
}
if d[i] > axis_max[i] {
axis_max[i] = d[i];
}
}
}
for i in 0..3 {
let range = axis_max[i] - axis_min[i];
if range > 1.0 {
let axis_name = match i {
0 => "X",
1 => "Y",
_ => "Z",
};
warnings.push(ValidationWarning {
kind: WarningKind::SelfIntersectionRisk,
message: format!(
"Delta range on {} axis is {:.4}, which may cause self-intersection",
axis_name, range
),
});
}
}
Ok(warnings)
}
pub fn check_symmetry(
deltas: &[[f64; 3]],
positions: &[[f64; 3]],
tolerance: f64,
) -> SymmetryReport {
let n = deltas.len().min(positions.len());
let tol = tolerance.max(1e-12);
let mut max_asym = 0.0_f64;
let mut asym_verts = Vec::new();
for i in 0..n {
let pos = positions[i];
if pos[0] < 0.0 {
continue;
}
let mirror_pos = [-pos[0], pos[1], pos[2]];
let mut best_j: Option<usize> = None;
let mut best_dsq = f64::MAX;
for (j, jpos) in positions[..n].iter().enumerate() {
let dp0 = jpos[0] - mirror_pos[0];
let dp1 = jpos[1] - mirror_pos[1];
let dp2 = jpos[2] - mirror_pos[2];
let dsq = dp0 * dp0 + dp1 * dp1 + dp2 * dp2;
if dsq < best_dsq {
best_dsq = dsq;
best_j = Some(j);
}
}
if let Some(j) = best_j {
if best_dsq.sqrt() > tol * 10.0 {
continue;
}
let expected = [-deltas[i][0], deltas[i][1], deltas[i][2]];
let diff = [
deltas[j][0] - expected[0],
deltas[j][1] - expected[1],
deltas[j][2] - expected[2],
];
let asym = magnitude(diff);
if asym > max_asym {
max_asym = asym;
}
if asym > tol {
asym_verts.push(i);
}
}
}
SymmetryReport {
symmetric: asym_verts.is_empty(),
max_asymmetry: max_asym,
asymmetric_vertices: asym_verts,
}
}
pub fn check_magnitude(deltas: &[[f64; 3]], max_displacement: f64) -> Vec<usize> {
deltas
.iter()
.enumerate()
.filter_map(|(i, d)| {
if magnitude(*d) > max_displacement {
Some(i)
} else {
None
}
})
.collect()
}
}
pub struct TargetInspector;
impl TargetInspector {
pub fn inspect(deltas: &[[f64; 3]]) -> TargetInfo {
Self::inspect_named(deltas, "")
}
pub fn inspect_named(deltas: &[[f64; 3]], name: &str) -> TargetInfo {
let vertex_count = deltas.len();
let mut affected_count = 0usize;
let mut max_disp = 0.0_f64;
let mut sum_disp = 0.0_f64;
for d in deltas {
let m = magnitude(*d);
if m > DEFAULT_ZERO_THRESHOLD {
affected_count += 1;
}
if m > max_disp {
max_disp = m;
}
sum_disp += m;
}
let average_displacement = if vertex_count > 0 {
sum_disp / vertex_count as f64
} else {
0.0
};
let sparsity = if vertex_count > 0 {
1.0 - (affected_count as f64 / vertex_count as f64)
} else {
1.0
};
TargetInfo {
name: name.to_owned(),
vertex_count,
affected_count,
max_displacement: max_disp,
average_displacement,
sparsity,
}
}
pub fn affected_vertices(deltas: &[[f64; 3]], threshold: f64) -> Vec<usize> {
deltas
.iter()
.enumerate()
.filter_map(|(i, d)| {
if magnitude(*d) > threshold {
Some(i)
} else {
None
}
})
.collect()
}
pub fn bounding_box(deltas: &[[f64; 3]]) -> ([f64; 3], [f64; 3]) {
if deltas.is_empty() {
return ([0.0; 3], [0.0; 3]);
}
let mut mn = [f64::MAX; 3];
let mut mx = [f64::MIN; 3];
for d in deltas {
for i in 0..3 {
if d[i] < mn[i] {
mn[i] = d[i];
}
if d[i] > mx[i] {
mx[i] = d[i];
}
}
}
(mn, mx)
}
pub fn max_displacement(deltas: &[[f64; 3]]) -> f64 {
deltas.iter().map(|d| magnitude(*d)).fold(0.0_f64, f64::max)
}
pub fn rms_displacement(deltas: &[[f64; 3]]) -> f64 {
if deltas.is_empty() {
return 0.0;
}
let sum_sq: f64 = deltas
.iter()
.map(|d| d[0] * d[0] + d[1] * d[1] + d[2] * d[2])
.sum();
(sum_sq / deltas.len() as f64).sqrt()
}
}
pub fn merge_targets(targets: &[(&str, &[[f64; 3]], f64)]) -> anyhow::Result<Vec<[f64; 3]>> {
if targets.is_empty() {
anyhow::bail!("no targets to merge");
}
let vertex_count = targets[0].1.len();
for (name, deltas, _) in targets.iter().skip(1) {
if deltas.len() != vertex_count {
anyhow::bail!(
"target '{}' has {} vertices, expected {}",
name,
deltas.len(),
vertex_count
);
}
}
let mut result = vec![[0.0_f64; 3]; vertex_count];
for (_name, deltas, weight) in targets {
let w = *weight;
for (i, d) in deltas.iter().enumerate() {
result[i][0] += d[0] * w;
result[i][1] += d[1] * w;
result[i][2] += d[2] * w;
}
}
Ok(result)
}
pub fn mirror_target(
deltas: &[[f64; 3]],
positions: &[[f64; 3]],
axis: MirrorAxis,
tolerance: f64,
) -> anyhow::Result<Vec<[f64; 3]>> {
let n = deltas.len();
if positions.len() != n {
anyhow::bail!(
"deltas length {} != positions length {}",
n,
positions.len()
);
}
if tolerance <= 0.0 {
anyhow::bail!("tolerance must be positive, got {}", tolerance);
}
let ax = axis.idx();
let tol_sq = tolerance * tolerance;
let mut result = deltas.to_vec();
for i in 0..n {
let pos = positions[i];
if pos[ax] < 0.0 {
continue;
}
let mut mirror_pos = pos;
mirror_pos[ax] = -mirror_pos[ax];
let mut best_j: Option<usize> = None;
let mut best_dsq = f64::MAX;
for (j, jpos) in positions[..n].iter().enumerate() {
let dp0 = jpos[0] - mirror_pos[0];
let dp1 = jpos[1] - mirror_pos[1];
let dp2 = jpos[2] - mirror_pos[2];
let dsq = dp0 * dp0 + dp1 * dp1 + dp2 * dp2;
if dsq < best_dsq {
best_dsq = dsq;
best_j = Some(j);
}
}
if let Some(j) = best_j {
if best_dsq <= tol_sq {
let mut d = deltas[i];
d[ax] = -d[ax];
result[j] = d;
}
}
}
Ok(result)
}
pub fn subtract_targets(a: &[[f64; 3]], b: &[[f64; 3]]) -> anyhow::Result<Vec<[f64; 3]>> {
if a.len() != b.len() {
anyhow::bail!("target lengths differ: {} vs {}", a.len(), b.len());
}
Ok(a.iter()
.zip(b.iter())
.map(|(va, vb)| [va[0] - vb[0], va[1] - vb[1], va[2] - vb[2]])
.collect())
}
pub fn add_targets(a: &[[f64; 3]], b: &[[f64; 3]]) -> anyhow::Result<Vec<[f64; 3]>> {
if a.len() != b.len() {
anyhow::bail!("target lengths differ: {} vs {}", a.len(), b.len());
}
Ok(a.iter()
.zip(b.iter())
.map(|(va, vb)| [va[0] + vb[0], va[1] + vb[1], va[2] + vb[2]])
.collect())
}
pub fn scale_target(deltas: &[[f64; 3]], factor: f64) -> Vec<[f64; 3]> {
deltas
.iter()
.map(|d| [d[0] * factor, d[1] * factor, d[2] * factor])
.collect()
}
pub fn clamp_target(deltas: &[[f64; 3]], max_magnitude: f64) -> Vec<[f64; 3]> {
deltas
.iter()
.map(|d| {
let m = magnitude(*d);
if m > max_magnitude && m > 1e-15 {
let scale = max_magnitude / m;
[d[0] * scale, d[1] * scale, d[2] * scale]
} else {
*d
}
})
.collect()
}
pub fn sparsify_target(deltas: &[[f64; 3]], threshold: f64) -> Vec<[f64; 3]> {
deltas
.iter()
.map(|d| {
if magnitude(*d) < threshold {
[0.0; 3]
} else {
*d
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_empty_target() {
let deltas = vec![[0.0; 3]; 5];
let warnings = TargetValidator::validate(&deltas, 5).expect("validate ok");
assert!(warnings.iter().any(|w| w.kind == WarningKind::EmptyTarget));
}
#[test]
fn test_validate_excessive_displacement() {
let mut deltas = vec![[0.0; 3]; 5];
deltas[0] = [2.0, 0.0, 0.0]; let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
assert!(warnings
.iter()
.any(|w| w.kind == WarningKind::ExcessiveDisplacement));
}
#[test]
fn test_validate_length_mismatch() {
let deltas = vec![[0.0; 3]; 5];
assert!(TargetValidator::validate(&deltas, 10).is_err());
}
#[test]
fn test_validate_clean_target() {
let mut deltas = vec![[0.0; 3]; 5];
deltas[0] = [0.1, 0.0, 0.0];
let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
assert!(
warnings.is_empty(),
"expected no warnings, got {:?}",
warnings
);
}
#[test]
fn test_check_symmetry_symmetric() {
let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
let deltas = vec![[0.1, 0.2, 0.3], [-0.1, 0.2, 0.3]]; let report = TargetValidator::check_symmetry(&deltas, &positions, 0.01);
assert!(report.symmetric, "should be symmetric");
assert!(report.max_asymmetry < 0.01);
}
#[test]
fn test_check_symmetry_asymmetric() {
let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
let deltas = vec![[0.1, 0.2, 0.3], [0.5, 0.0, 0.0]]; let report = TargetValidator::check_symmetry(&deltas, &positions, 0.01);
assert!(!report.symmetric, "should be asymmetric");
}
#[test]
fn test_check_magnitude() {
let deltas = vec![[0.1, 0.0, 0.0], [2.0, 0.0, 0.0], [0.5, 0.0, 0.0]];
let exceeding = TargetValidator::check_magnitude(&deltas, 1.0);
assert_eq!(exceeding, vec![1]);
}
#[test]
fn test_inspect_basic() {
let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.5, 0.0]];
let info = TargetInspector::inspect(&deltas);
assert_eq!(info.vertex_count, 3);
assert_eq!(info.affected_count, 2);
assert!((info.max_displacement - 1.0).abs() < 1e-10);
assert!(info.sparsity > 0.0 && info.sparsity < 1.0);
}
#[test]
fn test_inspect_empty() {
let deltas: Vec<[f64; 3]> = vec![];
let info = TargetInspector::inspect(&deltas);
assert_eq!(info.vertex_count, 0);
assert_eq!(info.affected_count, 0);
assert!((info.max_displacement).abs() < 1e-15);
}
#[test]
fn test_affected_vertices() {
let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.5, 0.0]];
let affected = TargetInspector::affected_vertices(&deltas, 0.01);
assert_eq!(affected, vec![0, 2]);
}
#[test]
fn test_bounding_box() {
let deltas = vec![[1.0, -2.0, 3.0], [-1.0, 4.0, 0.5]];
let (mn, mx) = TargetInspector::bounding_box(&deltas);
assert!((mn[0] - (-1.0)).abs() < 1e-15);
assert!((mx[0] - 1.0).abs() < 1e-15);
assert!((mn[1] - (-2.0)).abs() < 1e-15);
assert!((mx[1] - 4.0).abs() < 1e-15);
assert!((mn[2] - 0.5).abs() < 1e-15);
assert!((mx[2] - 3.0).abs() < 1e-15);
}
#[test]
fn test_bounding_box_empty() {
let (mn, mx) = TargetInspector::bounding_box(&[]);
assert_eq!(mn, [0.0; 3]);
assert_eq!(mx, [0.0; 3]);
}
#[test]
fn test_max_displacement() {
let deltas = vec![[1.0, 0.0, 0.0], [0.0, 3.0, 4.0]]; let max_d = TargetInspector::max_displacement(&deltas);
assert!((max_d - 5.0).abs() < 1e-10);
}
#[test]
fn test_rms_displacement() {
let deltas = vec![[1.0, 0.0, 0.0], [0.0, 0.0, 0.0]];
let rms = TargetInspector::rms_displacement(&deltas);
assert!((rms - (0.5_f64).sqrt()).abs() < 1e-10);
}
#[test]
fn test_merge_targets_single() {
let d = vec![[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]];
let result = merge_targets(&[("a", &d, 1.0)]).expect("merge ok");
assert_eq!(result.len(), 2);
assert!((result[0][0] - 1.0).abs() < 1e-15);
}
#[test]
fn test_merge_targets_weighted() {
let a = [[1.0, 0.0, 0.0]];
let b = [[0.0, 2.0, 0.0]];
let result = merge_targets(&[("a", &a[..], 0.5), ("b", &b[..], 0.5)]).expect("ok");
assert!((result[0][0] - 0.5).abs() < 1e-15);
assert!((result[0][1] - 1.0).abs() < 1e-15);
}
#[test]
fn test_merge_targets_empty() {
assert!(merge_targets(&[]).is_err());
}
#[test]
fn test_merge_targets_length_mismatch() {
let a = [[1.0, 0.0, 0.0]];
let b = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]];
assert!(merge_targets(&[("a", &a[..], 1.0), ("b", &b[..], 1.0)]).is_err());
}
#[test]
fn test_mirror_target_x() {
let positions = vec![[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0]];
let deltas = vec![[0.5, 0.3, 0.1], [0.0, 0.0, 0.0]];
let mirrored = mirror_target(&deltas, &positions, MirrorAxis::X, 0.1).expect("mirror ok");
assert!((mirrored[1][0] - (-0.5)).abs() < 1e-10);
assert!((mirrored[1][1] - 0.3).abs() < 1e-10);
assert!((mirrored[1][2] - 0.1).abs() < 1e-10);
}
#[test]
fn test_mirror_target_length_mismatch() {
let d = vec![[0.0; 3]; 3];
let p = vec![[0.0; 3]; 2];
assert!(mirror_target(&d, &p, MirrorAxis::X, 0.1).is_err());
}
#[test]
fn test_subtract_targets() {
let a = vec![[1.0, 2.0, 3.0]];
let b = vec![[0.5, 0.5, 0.5]];
let result = subtract_targets(&a, &b).expect("ok");
assert!((result[0][0] - 0.5).abs() < 1e-15);
assert!((result[0][1] - 1.5).abs() < 1e-15);
assert!((result[0][2] - 2.5).abs() < 1e-15);
}
#[test]
fn test_add_targets() {
let a = vec![[1.0, 2.0, 3.0]];
let b = vec![[0.5, 0.5, 0.5]];
let result = add_targets(&a, &b).expect("ok");
assert!((result[0][0] - 1.5).abs() < 1e-15);
}
#[test]
fn test_scale_target() {
let d = vec![[1.0, 2.0, 3.0]];
let result = scale_target(&d, 2.0);
assert!((result[0][0] - 2.0).abs() < 1e-15);
assert!((result[0][1] - 4.0).abs() < 1e-15);
}
#[test]
fn test_clamp_target() {
let d = vec![[10.0, 0.0, 0.0], [0.1, 0.0, 0.0]];
let clamped = clamp_target(&d, 1.0);
assert!((magnitude(clamped[0]) - 1.0).abs() < 1e-10);
assert!((clamped[1][0] - 0.1).abs() < 1e-15); }
#[test]
fn test_sparsify_target() {
let d = vec![[1.0, 0.0, 0.0], [0.001, 0.0, 0.0], [0.0, 0.5, 0.0]];
let sparse = sparsify_target(&d, 0.01);
assert!((sparse[0][0] - 1.0).abs() < 1e-15);
assert_eq!(sparse[1], [0.0; 3]); assert!((sparse[2][1] - 0.5).abs() < 1e-15);
}
#[test]
fn test_inspect_named() {
let deltas = vec![[1.0, 0.0, 0.0]];
let info = TargetInspector::inspect_named(&deltas, "my_target");
assert_eq!(info.name, "my_target");
assert_eq!(info.vertex_count, 1);
assert_eq!(info.affected_count, 1);
}
#[test]
fn test_self_intersection_warning() {
let mut deltas = vec![[0.0; 3]; 5];
deltas[0] = [0.8, 0.0, 0.0];
deltas[1] = [-0.8, 0.0, 0.0]; let warnings = TargetValidator::validate(&deltas, 5).expect("ok");
assert!(warnings
.iter()
.any(|w| w.kind == WarningKind::SelfIntersectionRisk));
}
}