use serde::{Deserialize, Serialize};
use crate::skeleton::{Bone, BoneId, Skeleton};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BoneScale {
pub bone_id: BoneId,
pub length_scale: f32,
pub mass_scale: f32,
pub width_scale: f32,
}
impl BoneScale {
#[must_use]
pub fn uniform(bone_id: BoneId, scale: f32) -> Self {
Self {
bone_id,
length_scale: scale,
mass_scale: scale * scale * scale, width_scale: scale,
}
}
#[must_use]
pub fn identity(bone_id: BoneId) -> Self {
Self {
bone_id,
length_scale: 1.0,
mass_scale: 1.0,
width_scale: 1.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Morphology {
pub name: String,
pub bone_scales: Vec<BoneScale>,
pub height_factor: f32,
pub mass_factor: f32,
}
impl Morphology {
#[must_use]
pub fn identity() -> Self {
Self {
name: "identity".into(),
bone_scales: Vec::new(),
height_factor: 1.0,
mass_factor: 1.0,
}
}
#[must_use]
pub fn average() -> Self {
Self {
name: "average".into(),
bone_scales: Vec::new(),
height_factor: 1.0,
mass_factor: 1.0,
}
}
#[must_use]
pub fn heavy() -> Self {
Self {
name: "heavy".into(),
bone_scales: Vec::new(),
height_factor: 0.95,
mass_factor: 1.4,
}
}
#[must_use]
pub fn lean() -> Self {
Self {
name: "lean".into(),
bone_scales: Vec::new(),
height_factor: 1.0,
mass_factor: 0.75,
}
}
#[must_use]
pub fn tall() -> Self {
Self {
name: "tall".into(),
bone_scales: Vec::new(),
height_factor: 1.15,
mass_factor: 1.15 * 1.15 * 1.15, }
}
#[must_use]
pub fn compact() -> Self {
Self {
name: "compact".into(),
bone_scales: Vec::new(),
height_factor: 0.85,
mass_factor: 1.1,
}
}
pub fn with_bone_scale(mut self, scale: BoneScale) -> Self {
self.bone_scales.push(scale);
self
}
#[must_use]
pub fn random(seed: u64, variance: f32) -> Self {
let variance = variance.clamp(0.0, 0.5);
let h = |s: u64| -> f32 {
let x = s
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
let bits = ((x >> 33) ^ x) as u32;
(bits as f32 / u32::MAX as f32) * 2.0 - 1.0 };
let height = 1.0 + h(seed) * variance;
let mass = height.powi(3) * (1.0 + h(seed.wrapping_add(1)) * variance * 0.5);
Self {
name: format!("random_{seed}"),
bone_scales: Vec::new(),
height_factor: height,
mass_factor: mass,
}
}
#[must_use]
fn bone_length_scale(&self, bone_id: BoneId) -> f32 {
self.bone_scales
.iter()
.find(|s| s.bone_id == bone_id)
.map_or(self.height_factor, |s| s.length_scale * self.height_factor)
}
#[must_use]
fn bone_mass_scale(&self, bone_id: BoneId) -> f32 {
self.bone_scales
.iter()
.find(|s| s.bone_id == bone_id)
.map_or(self.mass_factor, |s| s.mass_scale * self.mass_factor)
}
}
#[must_use]
pub fn apply_morphology(skeleton: &Skeleton, morphology: &Morphology) -> Skeleton {
let mut result = Skeleton::new(format!("{}_{}", skeleton.name, morphology.name));
for bone in skeleton.bones() {
let len_scale = morphology.bone_length_scale(bone.id);
let mass_scale = morphology.bone_mass_scale(bone.id);
let mut new_bone = Bone::new(
bone.id,
bone.name.clone(),
bone.length * len_scale,
bone.mass * mass_scale,
bone.parent,
);
let pos_scale = bone
.parent
.map_or(1.0, |pid| morphology.bone_length_scale(pid));
new_bone.local_position = bone.local_position * pos_scale;
new_bone.local_rotation = bone.local_rotation;
result.add_bone(new_bone);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use hisab::Vec3;
fn test_skeleton() -> Skeleton {
let mut skeleton = Skeleton::new("test");
skeleton.add_bone(Bone::new(BoneId(0), "root", 0.5, 10.0, None));
skeleton.add_bone(
Bone::new(BoneId(1), "spine", 0.4, 8.0, Some(BoneId(0)))
.with_position(Vec3::new(0.0, 0.5, 0.0)),
);
skeleton.add_bone(
Bone::new(BoneId(2), "arm", 0.6, 4.0, Some(BoneId(1)))
.with_position(Vec3::new(0.2, 0.3, 0.0)),
);
skeleton
}
#[test]
fn identity_preserves_skeleton() {
let orig = test_skeleton();
let result = apply_morphology(&orig, &Morphology::identity());
assert_eq!(result.bone_count(), orig.bone_count());
for (a, b) in orig.bones().iter().zip(result.bones().iter()) {
assert!((a.length - b.length).abs() < 1e-5);
assert!((a.mass - b.mass).abs() < 1e-5);
}
}
#[test]
fn tall_increases_lengths() {
let orig = test_skeleton();
let tall = apply_morphology(&orig, &Morphology::tall());
for (a, b) in orig.bones().iter().zip(tall.bones().iter()) {
assert!(b.length > a.length, "tall should increase bone lengths");
}
}
#[test]
fn heavy_increases_mass() {
let orig = test_skeleton();
let heavy = apply_morphology(&orig, &Morphology::heavy());
assert!(
heavy.total_mass() > orig.total_mass(),
"heavy should increase total mass"
);
}
#[test]
fn lean_decreases_mass() {
let orig = test_skeleton();
let lean = apply_morphology(&orig, &Morphology::lean());
assert!(
lean.total_mass() < orig.total_mass(),
"lean should decrease total mass"
);
}
#[test]
fn compact_shorter_bones() {
let orig = test_skeleton();
let compact = apply_morphology(&orig, &Morphology::compact());
for (a, b) in orig.bones().iter().zip(compact.bones().iter()) {
assert!(b.length < a.length, "compact should shorten bones");
}
}
#[test]
fn preserves_hierarchy() {
let orig = test_skeleton();
let result = apply_morphology(&orig, &Morphology::heavy());
for (a, b) in orig.bones().iter().zip(result.bones().iter()) {
assert_eq!(a.parent, b.parent);
assert_eq!(a.id, b.id);
}
}
#[test]
fn per_bone_override() {
let orig = test_skeleton();
let morph = Morphology::identity().with_bone_scale(BoneScale {
bone_id: BoneId(2),
length_scale: 2.0,
mass_scale: 8.0, width_scale: 2.0,
});
let result = apply_morphology(&orig, &morph);
assert!((result.bones()[2].length - 1.2).abs() < 0.01, "arm doubled");
assert!(
(result.bones()[0].length - 0.5).abs() < 0.01,
"root unchanged"
);
}
#[test]
fn random_produces_variation() {
let m1 = Morphology::random(42, 0.1);
let m2 = Morphology::random(99, 0.1);
assert!(
(m1.height_factor - m2.height_factor).abs() > 0.001,
"different seeds should produce different morphologies"
);
}
#[test]
fn random_bounded() {
for seed in 0..100 {
let m = Morphology::random(seed, 0.1);
assert!(
m.height_factor > 0.8 && m.height_factor < 1.2,
"height should be within ±20% at 10% variance: {}",
m.height_factor
);
}
}
#[test]
fn zero_variance_is_identity() {
let m = Morphology::random(42, 0.0);
assert!((m.height_factor - 1.0).abs() < 1e-5);
}
#[test]
fn presets_have_names() {
assert_eq!(Morphology::average().name, "average");
assert_eq!(Morphology::heavy().name, "heavy");
assert_eq!(Morphology::lean().name, "lean");
assert_eq!(Morphology::tall().name, "tall");
assert_eq!(Morphology::compact().name, "compact");
}
}