use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Build {
Slim,
#[default]
Average,
Heavy,
Heroic,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Frame {
Masculine,
Feminine,
#[default]
Neutral,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Proportions {
pub head_h: f32,
pub head_r: f32,
pub shoulder_w: f32,
pub hip_w: f32,
pub waist: f32,
pub arm_r: f32,
pub leg_r: f32,
pub arm_len: f32,
pub leg_len: f32,
pub neck_len: f32,
pub hand_r: f32,
pub torso_lean: f32,
}
impl Proportions {
pub fn derive(height: f32, bulk: f32, build: Build, frame: Frame) -> Self {
let head_h = height / 7.4;
let (bw, ww) = match build {
Build::Slim => (0.85, 0.86),
Build::Average => (1.0, 1.0),
Build::Heavy => (1.22, 1.22),
Build::Heroic => (1.12, 0.94),
};
let (sh, hp) = match frame {
Frame::Masculine => (1.08, 0.94),
Frame::Feminine => (0.90, 1.06),
Frame::Neutral => (1.0, 1.0),
};
Self {
head_h,
head_r: head_h * 0.62,
shoulder_w: height * 0.118 * sh * (0.9 + 0.1 * bulk),
hip_w: height * 0.095 * hp,
waist: 0.82 * ww,
arm_r: height * 0.036 * bulk * bw,
leg_r: height * 0.052 * bulk * bw,
arm_len: height * 0.44,
leg_len: height * 0.47,
neck_len: height * 0.045,
hand_r: height * 0.040,
torso_lean: 0.008 * height,
}
}
pub fn baseline(height: f32, bulk: f32) -> Self {
Self::derive(height, bulk, Build::Average, Frame::Neutral)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canon_head_count_in_range() {
for (b, f) in [
(Build::Slim, Frame::Feminine),
(Build::Average, Frame::Neutral),
(Build::Heavy, Frame::Masculine),
(Build::Heroic, Frame::Masculine),
] {
let p = Proportions::derive(1.7, 1.0, b, f);
let heads = 1.7 / p.head_h;
assert!((7.2..=7.6).contains(&heads), "{heads} heads");
}
}
#[test]
fn frame_drives_shoulder_hip_ratio() {
let m = Proportions::derive(1.7, 1.0, Build::Average, Frame::Masculine);
let f = Proportions::derive(1.7, 1.0, Build::Average, Frame::Feminine);
assert!(m.shoulder_w / m.hip_w > f.shoulder_w / f.hip_w);
}
#[test]
fn build_scales_limb_radius_monotonically() {
let s = Proportions::derive(1.7, 1.0, Build::Slim, Frame::Neutral);
let a = Proportions::derive(1.7, 1.0, Build::Average, Frame::Neutral);
let h = Proportions::derive(1.7, 1.0, Build::Heavy, Frame::Neutral);
assert!(s.arm_r < a.arm_r && a.arm_r < h.arm_r);
assert!(s.leg_r < a.leg_r && a.leg_r < h.leg_r);
}
#[test]
fn baseline_matches_average_neutral_derive() {
let baseline = Proportions::baseline(1.7, 1.0);
let derived = Proportions::derive(1.7, 1.0, Build::Average, Frame::Neutral);
assert_eq!(baseline, derived);
}
}