use crate::skeleton::Skeleton;
pub type Quat = [f32; 4];
pub const IDENTITY_QUAT: Quat = [0.0, 0.0, 0.0, 1.0];
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Pose {
pub name: String,
pub rotations: std::collections::HashMap<String, Quat>,
}
impl Pose {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
rotations: std::collections::HashMap::new(),
}
}
pub fn rotation_for(&self, joint_name: &str) -> Quat {
self.rotations
.get(joint_name)
.copied()
.unwrap_or(IDENTITY_QUAT)
}
pub fn set_rotation(&mut self, joint_name: impl Into<String>, quat: Quat) {
self.rotations.insert(joint_name.into(), quat);
}
pub fn apply_to_skeleton(&self, skeleton: &Skeleton) -> Skeleton {
let mut new_skeleton = skeleton.clone();
for joint in &mut new_skeleton.joints {
joint.rotation = self.rotation_for(&joint.name);
}
new_skeleton
}
pub fn blend(a: &Pose, b: &Pose, t: f32) -> Pose {
let mut all_names: std::collections::HashSet<&str> = std::collections::HashSet::new();
for key in a.rotations.keys() {
all_names.insert(key.as_str());
}
for key in b.rotations.keys() {
all_names.insert(key.as_str());
}
let mut result = Pose::new(format!("blend({},{},{})", a.name, b.name, t));
for name in all_names {
let qa = a.rotation_for(name);
let qb = b.rotation_for(name);
let blended = quat_slerp(qa, qb, t);
result.set_rotation(name, blended);
}
result
}
}
pub struct PoseLibrary {
poses: Vec<Pose>,
}
impl Default for PoseLibrary {
fn default() -> Self {
Self::new()
}
}
impl PoseLibrary {
pub fn new() -> Self {
Self { poses: Vec::new() }
}
pub fn add(&mut self, pose: Pose) {
self.poses.push(pose);
}
pub fn get(&self, name: &str) -> Option<&Pose> {
let lower = name.to_lowercase();
self.poses.iter().find(|p| p.name.to_lowercase() == lower)
}
pub fn names(&self) -> Vec<&str> {
self.poses.iter().map(|p| p.name.as_str()).collect()
}
pub fn standard() -> Self {
let mut lib = PoseLibrary::new();
let mut t_pose = Pose::new("t-pose");
t_pose.set_rotation("left_shoulder", [0.0, 0.0, 0.707, 0.707]);
t_pose.set_rotation("right_shoulder", [0.0, 0.0, -0.707, 0.707]);
lib.add(t_pose);
let mut a_pose = Pose::new("a-pose");
a_pose.set_rotation("left_shoulder", [0.0, 0.0, 0.383, 0.924]);
a_pose.set_rotation("right_shoulder", [0.0, 0.0, -0.383, 0.924]);
lib.add(a_pose);
let standing = Pose::new("standing");
lib.add(standing);
let mut sitting = Pose::new("sitting");
sitting.set_rotation("left_upper_leg", [0.707, 0.0, 0.0, 0.707]);
sitting.set_rotation("right_upper_leg", [0.707, 0.0, 0.0, 0.707]);
sitting.set_rotation("left_lower_leg", [-0.707, 0.0, 0.0, 0.707]);
sitting.set_rotation("right_lower_leg", [-0.707, 0.0, 0.0, 0.707]);
lib.add(sitting);
lib
}
}
fn quat_slerp(a: Quat, b: Quat, t: f32) -> Quat {
let dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + a[3] * b[3];
let (b, dot) = if dot < 0.0 {
([-b[0], -b[1], -b[2], -b[3]], -dot)
} else {
(b, dot)
};
if dot > 0.9995 {
let r = [
a[0] + t * (b[0] - a[0]),
a[1] + t * (b[1] - a[1]),
a[2] + t * (b[2] - a[2]),
a[3] + t * (b[3] - a[3]),
];
quat_normalize(r)
} else {
let theta_0 = dot.acos();
let theta = theta_0 * t;
let sin_theta = theta.sin();
let sin_theta_0 = theta_0.sin();
let s0 = (theta_0 - theta).sin() / sin_theta_0;
let s1 = sin_theta / sin_theta_0;
[
s0 * a[0] + s1 * b[0],
s0 * a[1] + s1 * b[1],
s0 * a[2] + s1 * b[2],
s0 * a[3] + s1 * b[3],
]
}
}
fn quat_normalize(q: Quat) -> Quat {
let len = (q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]).sqrt();
if len < 1e-10 {
return IDENTITY_QUAT;
}
[q[0] / len, q[1] / len, q[2] / len, q[3] / len]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skeleton::Skeleton;
#[test]
fn standard_library_has_four_poses() {
let lib = PoseLibrary::standard();
assert_eq!(lib.names().len(), 4, "expected 4 standard poses");
}
#[test]
fn t_pose_has_shoulder_rotations() {
let lib = PoseLibrary::standard();
let t = lib.get("t-pose").expect("t-pose must exist");
assert!(
t.rotations.contains_key("left_shoulder"),
"t-pose must have left_shoulder rotation"
);
}
#[test]
fn rotation_for_unknown_returns_identity() {
let pose = Pose::new("test");
assert_eq!(pose.rotation_for("nonexistent"), IDENTITY_QUAT);
}
#[test]
fn blend_at_t0_equals_a() {
let mut a = Pose::new("a");
a.set_rotation("joint1", [0.0, 0.0, 0.707, 0.707]);
let mut b = Pose::new("b");
b.set_rotation("joint1", [0.0, 0.707, 0.0, 0.707]);
let blended = Pose::blend(&a, &b, 0.0);
let result = blended.rotation_for("joint1");
let expected = a.rotation_for("joint1");
for i in 0..4 {
assert!(
(result[i] - expected[i]).abs() < 1e-5,
"blend(t=0) component {} mismatch: {} vs {}",
i,
result[i],
expected[i]
);
}
}
#[test]
fn blend_at_t1_equals_b() {
let mut a = Pose::new("a");
a.set_rotation("joint1", [0.0, 0.0, 0.707, 0.707]);
let mut b = Pose::new("b");
b.set_rotation("joint1", [0.0, 0.707, 0.0, 0.707]);
let blended = Pose::blend(&a, &b, 1.0);
let result = blended.rotation_for("joint1");
let expected = b.rotation_for("joint1");
for i in 0..4 {
assert!(
(result[i] - expected[i]).abs() < 1e-5,
"blend(t=1) component {} mismatch: {} vs {}",
i,
result[i],
expected[i]
);
}
}
#[test]
fn apply_to_skeleton_overrides_rotations() {
let lib = PoseLibrary::standard();
let t_pose = lib.get("t-pose").expect("t-pose must exist");
let skeleton = Skeleton::human_body();
let posed = t_pose.apply_to_skeleton(&skeleton);
let left_shoulder = posed
.joints
.iter()
.find(|j| j.name == "left_shoulder")
.expect("left_shoulder must exist");
assert_ne!(
left_shoulder.rotation, IDENTITY_QUAT,
"left_shoulder should have non-identity rotation in t-pose"
);
}
#[test]
fn slerp_identity_to_identity_is_identity() {
let a = Pose::new("a"); let b = Pose::new("b");
let blended = Pose::blend(&a, &b, 0.5);
assert_eq!(blended.rotation_for("any_joint"), IDENTITY_QUAT);
}
}