use crate::params::ParamState;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ParamDiff {
pub d_height: f32,
pub d_weight: f32,
pub d_muscle: f32,
pub d_age: f32,
pub extra_changes: Vec<(String, f32, f32)>,
pub extra_added: Vec<(String, f32)>,
pub extra_removed: Vec<(String, f32)>,
}
impl ParamDiff {
pub fn compute(a: &ParamState, b: &ParamState) -> Self {
let d_height = b.height - a.height;
let d_weight = b.weight - a.weight;
let d_muscle = b.muscle - a.muscle;
let d_age = b.age - a.age;
let mut extra_changes = Vec::new();
let mut extra_added = Vec::new();
let mut extra_removed = Vec::new();
for (key, &bval) in &b.extra {
if let Some(&aval) = a.extra.get(key) {
if (bval - aval).abs() > 0.0 {
extra_changes.push((key.clone(), aval, bval));
}
} else {
extra_added.push((key.clone(), bval));
}
}
for (key, &aval) in &a.extra {
if !b.extra.contains_key(key) {
extra_removed.push((key.clone(), aval));
}
}
ParamDiff {
d_height,
d_weight,
d_muscle,
d_age,
extra_changes,
extra_added,
extra_removed,
}
}
pub fn is_zero(&self, tolerance: f32) -> bool {
self.d_height.abs() <= tolerance
&& self.d_weight.abs() <= tolerance
&& self.d_muscle.abs() <= tolerance
&& self.d_age.abs() <= tolerance
&& self
.extra_changes
.iter()
.all(|(_, a, b)| (b - a).abs() <= tolerance)
&& self.extra_added.is_empty()
&& self.extra_removed.is_empty()
}
pub fn magnitude(&self) -> f32 {
(self.d_height.powi(2) + self.d_weight.powi(2) + self.d_muscle.powi(2) + self.d_age.powi(2))
.sqrt()
}
pub fn apply(&self, a: &ParamState) -> ParamState {
let mut result = a.clone();
result.height += self.d_height;
result.weight += self.d_weight;
result.muscle += self.d_muscle;
result.age += self.d_age;
for (key, _old, new) in &self.extra_changes {
result.extra.insert(key.clone(), *new);
}
for (key, val) in &self.extra_added {
result.extra.insert(key.clone(), *val);
}
for (key, _) in &self.extra_removed {
result.extra.remove(key);
}
result
}
pub fn scaled(&self, factor: f32) -> ParamDiff {
ParamDiff {
d_height: self.d_height * factor,
d_weight: self.d_weight * factor,
d_muscle: self.d_muscle * factor,
d_age: self.d_age * factor,
extra_changes: self
.extra_changes
.iter()
.map(|(k, a, b)| {
let mid = a + (b - a) * factor;
(k.clone(), *a, mid)
})
.collect(),
extra_added: self
.extra_added
.iter()
.map(|(k, v)| (k.clone(), v * factor))
.collect(),
extra_removed: self.extra_removed.clone(),
}
}
pub fn describe(&self, threshold: f32) -> String {
let mut changes = Vec::new();
if self.d_height.abs() > threshold {
changes.push(format!("height {:+.3}", self.d_height));
}
if self.d_weight.abs() > threshold {
changes.push(format!("weight {:+.3}", self.d_weight));
}
if self.d_muscle.abs() > threshold {
changes.push(format!("muscle {:+.3}", self.d_muscle));
}
if self.d_age.abs() > threshold {
changes.push(format!("age {:+.3}", self.d_age));
}
if changes.is_empty() {
"no significant changes".into()
} else {
changes.join(", ")
}
}
}
#[derive(Debug, Clone)]
pub struct MeshDiffStats {
pub vertex_count: usize,
pub changed_count: usize,
pub max_displacement: f32,
pub avg_displacement: f32,
pub rms_displacement: f32,
pub max_vertex_idx: usize,
}
impl MeshDiffStats {
pub fn compute(a: &[[f32; 3]], b: &[[f32; 3]], threshold: f32) -> Self {
let n = a.len().min(b.len());
if n == 0 {
return MeshDiffStats {
vertex_count: 0,
changed_count: 0,
max_displacement: 0.0,
avg_displacement: 0.0,
rms_displacement: 0.0,
max_vertex_idx: 0,
};
}
let displacements: Vec<f32> = (0..n)
.map(|i| {
let dx = b[i][0] - a[i][0];
let dy = b[i][1] - a[i][1];
let dz = b[i][2] - a[i][2];
(dx * dx + dy * dy + dz * dz).sqrt()
})
.collect();
let changed_count = displacements.iter().filter(|&&d| d > threshold).count();
let max_displacement = displacements.iter().cloned().fold(0.0f32, f32::max);
let avg = displacements.iter().sum::<f32>() / n as f32;
let rms = (displacements.iter().map(|d| d * d).sum::<f32>() / n as f32).sqrt();
let max_idx = displacements
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
MeshDiffStats {
vertex_count: n,
changed_count,
max_displacement,
avg_displacement: avg,
rms_displacement: rms,
max_vertex_idx: max_idx,
}
}
pub fn summary(&self) -> String {
format!(
"vertices: {}, changed: {}, max_disp: {:.4}, avg_disp: {:.4}, rms_disp: {:.4}, max_vertex_idx: {}",
self.vertex_count,
self.changed_count,
self.max_displacement,
self.avg_displacement,
self.rms_displacement,
self.max_vertex_idx,
)
}
}
#[allow(dead_code)]
pub fn vertex_displacements(a: &[[f32; 3]], b: &[[f32; 3]]) -> Vec<f32> {
let n = a.len().min(b.len());
(0..n)
.map(|i| {
let dx = b[i][0] - a[i][0];
let dy = b[i][1] - a[i][1];
let dz = b[i][2] - a[i][2];
(dx * dx + dy * dy + dz * dz).sqrt()
})
.collect()
}
#[allow(dead_code)]
pub fn top_displaced_vertices(a: &[[f32; 3]], b: &[[f32; 3]], n: usize) -> Vec<(usize, f32)> {
let mut displacements: Vec<(usize, f32)> =
vertex_displacements(a, b).into_iter().enumerate().collect();
displacements.sort_by(|x, y| y.1.partial_cmp(&x.1).unwrap_or(std::cmp::Ordering::Equal));
displacements.truncate(n);
displacements
}
#[cfg(test)]
mod tests {
use super::*;
fn p(h: f32, w: f32, m: f32, a: f32) -> ParamState {
ParamState {
height: h,
weight: w,
muscle: m,
age: a,
extra: Default::default(),
}
}
#[test]
fn diff_compute_height() {
let diff = ParamDiff::compute(&p(0.3, 0.5, 0.5, 0.5), &p(0.7, 0.5, 0.5, 0.5));
assert!((diff.d_height - 0.4).abs() < 1e-5);
}
#[test]
fn diff_is_zero_for_identical() {
let a = p(0.5, 0.5, 0.5, 0.5);
let diff = ParamDiff::compute(&a, &a);
assert!(diff.is_zero(1e-6));
}
#[test]
fn diff_magnitude_correct() {
let diff = ParamDiff {
d_height: 3.0,
d_weight: 4.0,
d_muscle: 0.0,
d_age: 0.0,
extra_changes: vec![],
extra_added: vec![],
extra_removed: vec![],
};
assert!((diff.magnitude() - 5.0).abs() < 1e-5);
}
#[test]
fn diff_apply_roundtrip() {
let a = p(0.3, 0.5, 0.5, 0.5);
let b = p(0.7, 0.5, 0.5, 0.5);
let diff = ParamDiff::compute(&a, &b);
let result = diff.apply(&a);
assert!((result.height - b.height).abs() < 1e-5);
assert!((result.weight - b.weight).abs() < 1e-5);
assert!((result.muscle - b.muscle).abs() < 1e-5);
assert!((result.age - b.age).abs() < 1e-5);
}
#[test]
fn diff_scaled_halves() {
let a = p(0.2, 0.5, 0.5, 0.5);
let b = p(0.8, 0.5, 0.5, 0.5);
let diff = ParamDiff::compute(&a, &b);
let scaled = diff.scaled(0.5);
assert!((scaled.d_height - diff.d_height * 0.5).abs() < 1e-5);
}
#[test]
fn diff_describe_nonempty() {
let a = p(0.2, 0.5, 0.5, 0.5);
let b = p(0.8, 0.5, 0.5, 0.5);
let diff = ParamDiff::compute(&a, &b);
let desc = diff.describe(0.01);
assert!(!desc.is_empty());
assert_ne!(desc, "no significant changes");
}
#[test]
fn mesh_diff_zero_for_same() {
let positions: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let stats = MeshDiffStats::compute(&positions, &positions, 1e-6);
assert_eq!(stats.max_displacement, 0.0);
}
#[test]
fn mesh_diff_detects_change() {
let a: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let mut b = a.clone();
b[1] = [2.0, 0.0, 0.0]; let stats = MeshDiffStats::compute(&a, &b, 0.5);
assert_eq!(stats.changed_count, 1);
}
#[test]
fn vertex_displacements_length() {
let a: Vec<[f32; 3]> = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
let b: Vec<[f32; 3]> = vec![[0.1, 0.0, 0.0], [1.0, 0.1, 0.0], [0.0, 1.0, 0.1]];
let disps = vertex_displacements(&a, &b);
assert_eq!(disps.len(), a.len());
}
#[test]
fn top_displaced_sorted_desc() {
let a: Vec<[f32; 3]> = vec![
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
];
let b: Vec<[f32; 3]> = vec![
[1.0, 0.0, 0.0], [3.0, 0.0, 0.0], [2.0, 0.0, 0.0], [0.5, 0.0, 0.0], ];
let top = top_displaced_vertices(&a, &b, 3);
assert_eq!(top.len(), 3);
assert!(top[0].1 >= top[1].1);
assert!(top[1].1 >= top[2].1);
assert_eq!(top[0].0, 1);
}
}