Skip to main content

oxihuman_morph/
body_volume_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Body volume control — overall body mass / volume morph parameter.
6
7use std::f32::consts::PI;
8
9/// Configuration for body volume scaling.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct BodyVolumeConfig {
13    /// Minimum allowed volume scale (default 0.5).
14    pub min_scale: f32,
15    /// Maximum allowed volume scale (default 2.0).
16    pub max_scale: f32,
17    /// Exponent applied to the normalised parameter before output.
18    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/// Runtime state for body volume control.
32#[allow(dead_code)]
33#[derive(Debug, Clone)]
34pub struct BodyVolumeState {
35    /// Normalised volume parameter in `[0.0, 1.0]`.
36    volume: f32,
37    /// Separate chest contribution in `[0.0, 1.0]`.
38    chest: f32,
39    /// Separate abdomen contribution in `[0.0, 1.0]`.
40    abdomen: f32,
41    config: BodyVolumeConfig,
42}
43
44/// Morph weights produced by body volume evaluation.
45#[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
54/// Create a new [`BodyVolumeState`] with neutral settings.
55pub 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
64/// Return a default [`BodyVolumeConfig`].
65pub fn default_body_volume_config() -> BodyVolumeConfig {
66    BodyVolumeConfig::default()
67}
68
69/// Set the overall volume parameter (clamped to `[0.0, 1.0]`).
70pub fn bvc_set_volume(state: &mut BodyVolumeState, v: f32) {
71    state.volume = v.clamp(0.0, 1.0);
72}
73
74/// Set the chest-specific volume parameter.
75pub fn bvc_set_chest(state: &mut BodyVolumeState, v: f32) {
76    state.chest = v.clamp(0.0, 1.0);
77}
78
79/// Set the abdomen-specific volume parameter.
80pub fn bvc_set_abdomen(state: &mut BodyVolumeState, v: f32) {
81    state.abdomen = v.clamp(0.0, 1.0);
82}
83
84/// Reset all parameters to neutral (0.5).
85pub fn bvc_reset(state: &mut BodyVolumeState) {
86    state.volume = 0.5;
87    state.chest = 0.5;
88    state.abdomen = 0.5;
89}
90
91/// Return true if all parameters are at neutral.
92pub 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
98/// Evaluate the body volume morph weights from the current state.
99pub 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
109/// Blend between two states by `t ∈ [0.0, 1.0]`.
110pub 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
120/// Estimate a rough spherical volume from the state (arbitrary units).
121///
122/// Uses `(4/3)π r³` where `r` is derived from overall scale.
123pub fn bvc_estimated_volume(state: &BodyVolumeState) -> f32 {
124    let w = bvc_to_weights(state);
125    let r = w.overall * 0.5 + 0.5; // map [0,1] → [0.5, 1.0]
126    (4.0 / 3.0) * PI * r * r * r
127}
128
129/// Serialise to a simple JSON-like string.
130pub 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// ---------------------------------------------------------------------------
138// Tests
139// ---------------------------------------------------------------------------
140#[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        // 0.5^2 = 0.25
229        assert!((w.overall - 0.25).abs() < 1e-5);
230    }
231}