#[allow(unused_imports)]
use crate::engine::MeshBuffers;
use oxihuman_core::parser::target::{Delta, TargetFile};
use std::collections::HashMap;
pub struct SymmetryMap {
pub pairs: Vec<(u32, u32)>,
pub center_verts: Vec<u32>,
}
impl SymmetryMap {
pub fn from_positions(positions: &[[f32; 3]], tolerance: f32) -> Self {
let tol = if tolerance <= 0.0 { 0.001 } else { tolerance };
let quantize = |v: f32| -> i64 { (v / tol).round() as i64 };
let mut left_map: HashMap<(i64, i64), (u32, f32)> = HashMap::new();
let mut center_verts: Vec<u32> = Vec::new();
for (i, pos) in positions.iter().enumerate() {
let x = pos[0];
let y = pos[1];
let z = pos[2];
if x < -tol {
let key = (quantize(y), quantize(z));
left_map.entry(key).or_insert((i as u32, x));
} else if x.abs() <= tol {
center_verts.push(i as u32);
}
}
let mut pairs: Vec<(u32, u32)> = Vec::new();
for (i, pos) in positions.iter().enumerate() {
let x = pos[0];
let y = pos[1];
let z = pos[2];
if x > tol {
let mirrored_x = -x;
let key = (quantize(y), quantize(z));
if let Some(&(left_vid, left_x)) = left_map.get(&key) {
let left_pos = positions[left_vid as usize];
let dist = ((mirrored_x - left_x).powi(2)
+ (y - left_pos[1]).powi(2)
+ (z - left_pos[2]).powi(2))
.sqrt();
if dist <= tol {
pairs.push((left_vid, i as u32));
}
}
}
}
SymmetryMap {
pairs,
center_verts,
}
}
pub fn pair_count(&self) -> usize {
self.pairs.len()
}
pub fn center_count(&self) -> usize {
self.center_verts.len()
}
}
pub fn mirror_target_deltas(target: &TargetFile) -> TargetFile {
TargetFile {
name: format!("{}-mirrored", target.name),
deltas: target
.deltas
.iter()
.map(|d| Delta {
vid: d.vid,
dx: -d.dx,
dy: d.dy,
dz: d.dz,
})
.collect(),
}
}
pub fn symmetrize_positions(positions: &[[f32; 3]], map: &SymmetryMap) -> Vec<[f32; 3]> {
let mut result = positions.to_vec();
for &(left_vid, right_vid) in &map.pairs {
let l = positions[left_vid as usize];
let r = positions[right_vid as usize];
let avg_yz = ((l[1] + r[1]) / 2.0, (l[2] + r[2]) / 2.0);
let avg_x = (l[0].abs() + r[0].abs()) / 2.0;
result[left_vid as usize] = [-avg_x, avg_yz.0, avg_yz.1];
result[right_vid as usize] = [avg_x, avg_yz.0, avg_yz.1];
}
result
}
pub fn mirror_target_name(name: &str) -> Option<String> {
let lower = name.to_lowercase();
if let Some(pos) = lower.find("-l-") {
let rest = &name[pos + 3..];
let prefix = &name[..pos];
return Some(format!("{}-r-{}", prefix, rest));
}
if lower.starts_with("l-") {
return Some(format!("r-{}", &name[2..]));
}
if lower.contains("lside") {
let replaced = replace_case_insensitive(name, "lside", "rside");
return Some(replaced);
}
if lower.contains("left") {
let replaced = replace_case_insensitive(name, "left", "right");
return Some(replaced);
}
None
}
fn replace_case_insensitive(s: &str, from: &str, to: &str) -> String {
let lower = s.to_lowercase();
if let Some(pos) = lower.find(from) {
let mut result = String::with_capacity(s.len() - from.len() + to.len());
result.push_str(&s[..pos]);
result.push_str(to);
result.push_str(&s[pos + from.len()..]);
result
} else {
s.to_string()
}
}
pub fn is_left_side(name: &str) -> bool {
let lower = name.to_lowercase();
lower.starts_with("l-")
|| lower.contains("-l-")
|| lower.contains("left")
|| lower.contains("lside")
}
pub fn is_right_side(name: &str) -> bool {
let lower = name.to_lowercase();
lower.starts_with("r-")
|| lower.contains("-r-")
|| lower.contains("right")
|| lower.contains("rside")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mirror_target_deltas_negates_dx() {
let target = TargetFile {
name: "test".to_string(),
deltas: vec![Delta {
vid: 42,
dx: 0.5,
dy: 0.1,
dz: -0.2,
}],
};
let mirrored = mirror_target_deltas(&target);
assert_eq!(mirrored.deltas.len(), 1);
assert!(
(mirrored.deltas[0].dx + 0.5).abs() < 1e-6,
"dx should be -0.5"
);
assert!((mirrored.deltas[0].dy - 0.1).abs() < 1e-6, "dy unchanged");
assert!((mirrored.deltas[0].dz + 0.2).abs() < 1e-6, "dz unchanged");
assert_eq!(mirrored.deltas[0].vid, 42);
}
#[test]
fn mirror_target_name_l_prefix() {
let result = mirror_target_name("l-arm-muscle.target");
assert_eq!(result, Some("r-arm-muscle.target".to_string()));
}
#[test]
fn mirror_target_name_left_word() {
let result = mirror_target_name("leftarm-size.target");
assert_eq!(result, Some("rightarm-size.target".to_string()));
}
#[test]
fn mirror_target_name_none_for_center() {
let result = mirror_target_name("head-age.target");
assert_eq!(result, None);
}
#[test]
fn is_left_side_true() {
assert!(is_left_side("l-forearm-size.target"));
}
#[test]
fn is_right_side_true() {
assert!(is_right_side("r-forearm-size.target"));
}
#[test]
fn symmetry_map_from_simple_pair() {
let positions: Vec<[f32; 3]> = vec![
[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 0.0], ];
let map = SymmetryMap::from_positions(&positions, 0.001);
assert_eq!(map.pair_count(), 1, "should find 1 pair");
assert_eq!(map.center_count(), 1, "should find 1 center vertex");
assert_eq!(map.pairs[0], (0, 1));
assert_eq!(map.center_verts[0], 2);
}
#[test]
fn symmetrize_positions_averages_pair() {
let positions: Vec<[f32; 3]> = vec![
[-1.2, 0.5, 0.3], [1.0, 0.5, 0.3], [0.0, 0.0, 0.0], ];
let _map = SymmetryMap::from_positions(&positions, 0.001);
let manual_map = SymmetryMap {
pairs: vec![(0, 1)],
center_verts: vec![2],
};
let sym = symmetrize_positions(&positions, &manual_map);
let avg_x = (1.2_f32 + 1.0_f32) / 2.0;
assert!(
(sym[0][0] + avg_x).abs() < 1e-5,
"left x should be -{}",
avg_x
);
assert!(
(sym[1][0] - avg_x).abs() < 1e-5,
"right x should be +{}",
avg_x
);
assert!((sym[0][1] - 0.5).abs() < 1e-5);
assert!((sym[1][1] - 0.5).abs() < 1e-5);
assert!((sym[2][0] - 0.0).abs() < 1e-5);
}
}