Skip to main content

oxihuman_morph/
thigh_girth_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Thigh girth control — medial/lateral thigh volume adjustments.
5
6use std::f32::consts::PI;
7
8/// Leg side.
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ThighGirthSide {
12    Left,
13    Right,
14}
15
16/// Config.
17#[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/// State.
30#[allow(dead_code)]
31#[derive(Debug, Clone, Default)]
32pub struct ThighGirthState {
33    /// Overall girth, 0..=1.
34    pub left_girth: f32,
35    pub right_girth: f32,
36    /// Medial emphasis, 0..=1.
37    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/// Circumference estimate in metres for one side.
87#[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        // Even at 0 girth there's a base radius
171        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}