oxihuman_morph/
thigh_girth_control.rs1#![allow(dead_code)]
3
4use std::f32::consts::PI;
7
8#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ThighGirthSide {
12 Left,
13 Right,
14}
15
16#[allow(dead_code)]
18#[derive(Debug, Clone, PartialEq)]
19pub struct ThighGirthConfig {
20 pub max_radius_m: f32,
21}
22
23impl Default for ThighGirthConfig {
24 fn default() -> Self {
25 Self { max_radius_m: 0.04 }
26 }
27}
28
29#[allow(dead_code)]
31#[derive(Debug, Clone, Default)]
32pub struct ThighGirthState {
33 pub left_girth: f32,
35 pub right_girth: f32,
36 pub left_medial: f32,
38 pub right_medial: f32,
39}
40
41#[allow(dead_code)]
42pub fn new_thigh_girth_state() -> ThighGirthState {
43 ThighGirthState::default()
44}
45
46#[allow(dead_code)]
47pub fn default_thigh_girth_config() -> ThighGirthConfig {
48 ThighGirthConfig::default()
49}
50
51#[allow(dead_code)]
52pub fn tg_set_girth(state: &mut ThighGirthState, side: ThighGirthSide, v: f32) {
53 let v = v.clamp(0.0, 1.0);
54 match side {
55 ThighGirthSide::Left => state.left_girth = v,
56 ThighGirthSide::Right => state.right_girth = v,
57 }
58}
59
60#[allow(dead_code)]
61pub fn tg_set_medial(state: &mut ThighGirthState, side: ThighGirthSide, v: f32) {
62 let v = v.clamp(0.0, 1.0);
63 match side {
64 ThighGirthSide::Left => state.left_medial = v,
65 ThighGirthSide::Right => state.right_medial = v,
66 }
67}
68
69#[allow(dead_code)]
70pub fn tg_set_both(state: &mut ThighGirthState, v: f32) {
71 let v = v.clamp(0.0, 1.0);
72 state.left_girth = v;
73 state.right_girth = v;
74}
75
76#[allow(dead_code)]
77pub fn tg_reset(state: &mut ThighGirthState) {
78 *state = ThighGirthState::default();
79}
80
81#[allow(dead_code)]
82pub fn tg_is_neutral(state: &ThighGirthState) -> bool {
83 state.left_girth < 1e-4 && state.right_girth < 1e-4
84}
85
86#[allow(dead_code)]
88pub fn tg_circumference(
89 state: &ThighGirthState,
90 side: ThighGirthSide,
91 cfg: &ThighGirthConfig,
92) -> f32 {
93 let g = match side {
94 ThighGirthSide::Left => state.left_girth,
95 ThighGirthSide::Right => state.right_girth,
96 };
97 2.0 * PI * (0.06 + g * cfg.max_radius_m)
98}
99
100#[allow(dead_code)]
101pub fn tg_symmetry(state: &ThighGirthState) -> f32 {
102 1.0 - (state.left_girth - state.right_girth).abs()
103}
104
105#[allow(dead_code)]
106pub fn tg_to_weights(state: &ThighGirthState, cfg: &ThighGirthConfig) -> [f32; 4] {
107 [
108 state.left_girth * cfg.max_radius_m,
109 state.right_girth * cfg.max_radius_m,
110 state.left_medial * cfg.max_radius_m * 0.6,
111 state.right_medial * cfg.max_radius_m * 0.6,
112 ]
113}
114
115#[allow(dead_code)]
116pub fn tg_blend(a: &ThighGirthState, b: &ThighGirthState, t: f32) -> ThighGirthState {
117 let t = t.clamp(0.0, 1.0);
118 let inv = 1.0 - t;
119 ThighGirthState {
120 left_girth: a.left_girth * inv + b.left_girth * t,
121 right_girth: a.right_girth * inv + b.right_girth * t,
122 left_medial: a.left_medial * inv + b.left_medial * t,
123 right_medial: a.right_medial * inv + b.right_medial * t,
124 }
125}
126
127#[allow(dead_code)]
128pub fn tg_to_json(state: &ThighGirthState) -> String {
129 format!(
130 "{{\"left_girth\":{:.4},\"right_girth\":{:.4}}}",
131 state.left_girth, state.right_girth
132 )
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn default_neutral() {
141 assert!(tg_is_neutral(&new_thigh_girth_state()));
142 }
143
144 #[test]
145 fn girth_clamps_high() {
146 let mut s = new_thigh_girth_state();
147 tg_set_girth(&mut s, ThighGirthSide::Left, 5.0);
148 assert!((s.left_girth - 1.0).abs() < 1e-6);
149 }
150
151 #[test]
152 fn girth_clamps_low() {
153 let mut s = new_thigh_girth_state();
154 tg_set_girth(&mut s, ThighGirthSide::Right, -1.0);
155 assert!(s.right_girth < 1e-6);
156 }
157
158 #[test]
159 fn reset_clears() {
160 let mut s = new_thigh_girth_state();
161 tg_set_both(&mut s, 0.9);
162 tg_reset(&mut s);
163 assert!(tg_is_neutral(&s));
164 }
165
166 #[test]
167 fn circumference_at_neutral_positive() {
168 let cfg = default_thigh_girth_config();
169 let s = new_thigh_girth_state();
170 assert!(tg_circumference(&s, ThighGirthSide::Left, &cfg) > 0.0);
172 }
173
174 #[test]
175 fn circumference_grows_with_girth() {
176 let cfg = default_thigh_girth_config();
177 let mut s = new_thigh_girth_state();
178 let base = tg_circumference(&s, ThighGirthSide::Left, &cfg);
179 tg_set_girth(&mut s, ThighGirthSide::Left, 1.0);
180 let grown = tg_circumference(&s, ThighGirthSide::Left, &cfg);
181 assert!(grown > base);
182 }
183
184 #[test]
185 fn symmetry_one_when_equal() {
186 let mut s = new_thigh_girth_state();
187 tg_set_both(&mut s, 0.5);
188 assert!((tg_symmetry(&s) - 1.0).abs() < 1e-6);
189 }
190
191 #[test]
192 fn weights_four_elements() {
193 let w = tg_to_weights(&new_thigh_girth_state(), &default_thigh_girth_config());
194 assert_eq!(w.len(), 4);
195 }
196
197 #[test]
198 fn blend_midpoint() {
199 let mut b = new_thigh_girth_state();
200 tg_set_both(&mut b, 1.0);
201 let r = tg_blend(&new_thigh_girth_state(), &b, 0.5);
202 assert!((r.left_girth - 0.5).abs() < 1e-5);
203 }
204
205 #[test]
206 fn json_has_keys() {
207 let j = tg_to_json(&new_thigh_girth_state());
208 assert!(j.contains("left_girth") && j.contains("right_girth"));
209 }
210}