oxihuman_morph/
body_volume_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::PI;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct BodyVolumeConfig {
13 pub min_scale: f32,
15 pub max_scale: f32,
17 pub exponent: f32,
19}
20
21impl Default for BodyVolumeConfig {
22 fn default() -> Self {
23 BodyVolumeConfig {
24 min_scale: 0.5,
25 max_scale: 2.0,
26 exponent: 1.0,
27 }
28 }
29}
30
31#[allow(dead_code)]
33#[derive(Debug, Clone)]
34pub struct BodyVolumeState {
35 volume: f32,
37 chest: f32,
39 abdomen: f32,
41 config: BodyVolumeConfig,
42}
43
44#[allow(dead_code)]
46#[derive(Debug, Clone)]
47pub struct BodyVolumeWeights {
48 pub overall: f32,
49 pub chest: f32,
50 pub abdomen: f32,
51 pub limbs: f32,
52}
53
54pub fn new_body_volume_state(config: BodyVolumeConfig) -> BodyVolumeState {
56 BodyVolumeState {
57 volume: 0.5,
58 chest: 0.5,
59 abdomen: 0.5,
60 config,
61 }
62}
63
64pub fn default_body_volume_config() -> BodyVolumeConfig {
66 BodyVolumeConfig::default()
67}
68
69pub fn bvc_set_volume(state: &mut BodyVolumeState, v: f32) {
71 state.volume = v.clamp(0.0, 1.0);
72}
73
74pub fn bvc_set_chest(state: &mut BodyVolumeState, v: f32) {
76 state.chest = v.clamp(0.0, 1.0);
77}
78
79pub fn bvc_set_abdomen(state: &mut BodyVolumeState, v: f32) {
81 state.abdomen = v.clamp(0.0, 1.0);
82}
83
84pub fn bvc_reset(state: &mut BodyVolumeState) {
86 state.volume = 0.5;
87 state.chest = 0.5;
88 state.abdomen = 0.5;
89}
90
91pub fn bvc_is_neutral(state: &BodyVolumeState) -> bool {
93 (state.volume - 0.5).abs() < 1e-5
94 && (state.chest - 0.5).abs() < 1e-5
95 && (state.abdomen - 0.5).abs() < 1e-5
96}
97
98pub fn bvc_to_weights(state: &BodyVolumeState) -> BodyVolumeWeights {
100 let scale = |x: f32| x.powf(state.config.exponent);
101 BodyVolumeWeights {
102 overall: scale(state.volume),
103 chest: scale(state.chest),
104 abdomen: scale(state.abdomen),
105 limbs: scale((state.chest + state.abdomen) * 0.5),
106 }
107}
108
109pub fn bvc_blend(a: &BodyVolumeState, b: &BodyVolumeState, t: f32) -> BodyVolumeState {
111 let t = t.clamp(0.0, 1.0);
112 BodyVolumeState {
113 volume: a.volume + (b.volume - a.volume) * t,
114 chest: a.chest + (b.chest - a.chest) * t,
115 abdomen: a.abdomen + (b.abdomen - a.abdomen) * t,
116 config: a.config.clone(),
117 }
118}
119
120pub fn bvc_estimated_volume(state: &BodyVolumeState) -> f32 {
124 let w = bvc_to_weights(state);
125 let r = w.overall * 0.5 + 0.5; (4.0 / 3.0) * PI * r * r * r
127}
128
129pub fn bvc_to_json(state: &BodyVolumeState) -> String {
131 format!(
132 r#"{{"volume":{:.4},"chest":{:.4},"abdomen":{:.4}}}"#,
133 state.volume, state.chest, state.abdomen
134 )
135}
136
137#[cfg(test)]
141mod tests {
142 use super::*;
143
144 fn make() -> BodyVolumeState {
145 new_body_volume_state(default_body_volume_config())
146 }
147
148 #[test]
149 fn neutral_on_creation() {
150 let s = make();
151 assert!(bvc_is_neutral(&s));
152 }
153
154 #[test]
155 fn set_volume_clamps_high() {
156 let mut s = make();
157 bvc_set_volume(&mut s, 5.0);
158 let w = bvc_to_weights(&s);
159 assert!((0.0..=1.0).contains(&w.overall));
160 }
161
162 #[test]
163 fn set_volume_clamps_low() {
164 let mut s = make();
165 bvc_set_volume(&mut s, -3.0);
166 let w = bvc_to_weights(&s);
167 assert!((0.0..=1.0).contains(&w.overall));
168 }
169
170 #[test]
171 fn reset_restores_neutral() {
172 let mut s = make();
173 bvc_set_volume(&mut s, 0.9);
174 bvc_set_chest(&mut s, 0.1);
175 bvc_reset(&mut s);
176 assert!(bvc_is_neutral(&s));
177 }
178
179 #[test]
180 fn weights_in_unit_range() {
181 let mut s = make();
182 bvc_set_volume(&mut s, 0.8);
183 bvc_set_chest(&mut s, 0.3);
184 bvc_set_abdomen(&mut s, 0.6);
185 let w = bvc_to_weights(&s);
186 assert!((0.0..=1.0).contains(&w.overall));
187 assert!((0.0..=1.0).contains(&w.chest));
188 assert!((0.0..=1.0).contains(&w.abdomen));
189 assert!((0.0..=1.0).contains(&w.limbs));
190 }
191
192 #[test]
193 fn blend_midpoint() {
194 let mut a = make();
195 let mut b = make();
196 bvc_set_volume(&mut a, 0.0);
197 bvc_set_volume(&mut b, 1.0);
198 let mid = bvc_blend(&a, &b, 0.5);
199 assert!((mid.volume - 0.5).abs() < 1e-5);
200 }
201
202 #[test]
203 fn blend_at_zero_is_a() {
204 let a = make();
205 let b = make();
206 let r = bvc_blend(&a, &b, 0.0);
207 assert!((r.volume - a.volume).abs() < 1e-5);
208 }
209
210 #[test]
211 fn estimated_volume_positive() {
212 let s = make();
213 assert!(bvc_estimated_volume(&s) > 0.0);
214 }
215
216 #[test]
217 fn json_contains_volume_key() {
218 let s = make();
219 assert!(bvc_to_json(&s).contains("volume"));
220 }
221
222 #[test]
223 fn exponent_changes_output() {
224 let mut cfg = default_body_volume_config();
225 cfg.exponent = 2.0;
226 let s = new_body_volume_state(cfg);
227 let w = bvc_to_weights(&s);
228 assert!((w.overall - 0.25).abs() < 1e-5);
230 }
231}