Skip to main content

oxihuman_morph/
body_segment_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Body-segment proportioning control (torso, limbs, head scale).
6
7use std::f32::consts::PI;
8
9/// Which body segment to address.
10#[allow(dead_code)]
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum BodySegment {
13    Head,
14    Torso,
15    UpperArm,
16    LowerArm,
17    UpperLeg,
18    LowerLeg,
19}
20
21/// Per-segment scale override.
22#[allow(dead_code)]
23#[derive(Clone, Debug)]
24pub struct SegmentScale {
25    pub segment: BodySegment,
26    /// Relative scale factor (1.0 = neutral).
27    pub scale: f32,
28    /// Blend weight toward this override (0..1).
29    pub weight: f32,
30}
31
32/// Collection of all segment overrides.
33#[allow(dead_code)]
34#[derive(Clone, Debug, Default)]
35pub struct BodySegmentState {
36    pub overrides: Vec<SegmentScale>,
37}
38
39/// Configuration bounds for segment scaling.
40#[allow(dead_code)]
41#[derive(Clone, Debug)]
42pub struct BodySegmentConfig {
43    pub min_scale: f32,
44    pub max_scale: f32,
45}
46
47impl Default for BodySegmentConfig {
48    fn default() -> Self {
49        Self {
50            min_scale: 0.5,
51            max_scale: 2.0,
52        }
53    }
54}
55
56#[allow(dead_code)]
57pub fn new_body_segment_state() -> BodySegmentState {
58    BodySegmentState::default()
59}
60
61#[allow(dead_code)]
62pub fn default_body_segment_config() -> BodySegmentConfig {
63    BodySegmentConfig::default()
64}
65
66#[allow(dead_code)]
67pub fn set_segment_scale(
68    state: &mut BodySegmentState,
69    cfg: &BodySegmentConfig,
70    segment: BodySegment,
71    scale: f32,
72    weight: f32,
73) {
74    let scale = scale.clamp(cfg.min_scale, cfg.max_scale);
75    let weight = weight.clamp(0.0, 1.0);
76    if let Some(entry) = state.overrides.iter_mut().find(|e| e.segment == segment) {
77        entry.scale = scale;
78        entry.weight = weight;
79    } else {
80        state.overrides.push(SegmentScale {
81            segment,
82            scale,
83            weight,
84        });
85    }
86}
87
88#[allow(dead_code)]
89pub fn get_segment_scale(state: &BodySegmentState, segment: BodySegment) -> f32 {
90    state
91        .overrides
92        .iter()
93        .find(|e| e.segment == segment)
94        .map(|e| e.scale)
95        .unwrap_or(1.0)
96}
97
98#[allow(dead_code)]
99pub fn reset_segment(state: &mut BodySegmentState, segment: BodySegment) {
100    state.overrides.retain(|e| e.segment != segment);
101}
102
103#[allow(dead_code)]
104pub fn reset_all_segments(state: &mut BodySegmentState) {
105    state.overrides.clear();
106}
107
108#[allow(dead_code)]
109pub fn blend_segment_states(
110    a: &BodySegmentState,
111    b: &BodySegmentState,
112    t: f32,
113) -> BodySegmentState {
114    let t = t.clamp(0.0, 1.0);
115    let all_segs = [
116        BodySegment::Head,
117        BodySegment::Torso,
118        BodySegment::UpperArm,
119        BodySegment::LowerArm,
120        BodySegment::UpperLeg,
121        BodySegment::LowerLeg,
122    ];
123    let overrides = all_segs
124        .iter()
125        .map(|&seg| {
126            let sa = a
127                .overrides
128                .iter()
129                .find(|e| e.segment == seg)
130                .map(|e| e.scale)
131                .unwrap_or(1.0);
132            let sb = b
133                .overrides
134                .iter()
135                .find(|e| e.segment == seg)
136                .map(|e| e.scale)
137                .unwrap_or(1.0);
138            let wa = a
139                .overrides
140                .iter()
141                .find(|e| e.segment == seg)
142                .map(|e| e.weight)
143                .unwrap_or(0.0);
144            let wb = b
145                .overrides
146                .iter()
147                .find(|e| e.segment == seg)
148                .map(|e| e.weight)
149                .unwrap_or(0.0);
150            SegmentScale {
151                segment: seg,
152                scale: sa + (sb - sa) * t,
153                weight: wa + (wb - wa) * t,
154            }
155        })
156        .collect();
157    BodySegmentState { overrides }
158}
159
160#[allow(dead_code)]
161pub fn segment_name(seg: BodySegment) -> &'static str {
162    match seg {
163        BodySegment::Head => "head",
164        BodySegment::Torso => "torso",
165        BodySegment::UpperArm => "upper_arm",
166        BodySegment::LowerArm => "lower_arm",
167        BodySegment::UpperLeg => "upper_leg",
168        BodySegment::LowerLeg => "lower_leg",
169    }
170}
171
172#[allow(dead_code)]
173pub fn total_limb_scale(state: &BodySegmentState) -> f32 {
174    let ua = get_segment_scale(state, BodySegment::UpperArm);
175    let la = get_segment_scale(state, BodySegment::LowerArm);
176    let ul = get_segment_scale(state, BodySegment::UpperLeg);
177    let ll = get_segment_scale(state, BodySegment::LowerLeg);
178    (ua + la + ul + ll) / 4.0
179}
180
181/// Sinusoidal rhythm factor useful for breath-driven scale animation.
182#[allow(dead_code)]
183pub fn rhythm_scale(base: f32, amplitude: f32, phase_rad: f32) -> f32 {
184    base + amplitude * phase_rad.sin()
185}
186
187/// Estimate limb length given scale, using a reference length in metres.
188#[allow(dead_code)]
189pub fn limb_length_m(reference_m: f32, scale: f32) -> f32 {
190    reference_m * scale
191}
192
193/// Angular contribution of a segment scale (heuristic, uses PI internally).
194#[allow(dead_code)]
195pub fn segment_angle_contribution(scale: f32) -> f32 {
196    (scale - 1.0) * PI * 0.1
197}
198
199#[allow(dead_code)]
200pub fn state_to_json(state: &BodySegmentState) -> String {
201    let entries: Vec<String> = state
202        .overrides
203        .iter()
204        .map(|e| {
205            format!(
206                "{{\"segment\":\"{}\",\"scale\":{:.4},\"weight\":{:.4}}}",
207                segment_name(e.segment),
208                e.scale,
209                e.weight
210            )
211        })
212        .collect();
213    format!("[{}]", entries.join(","))
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn default_state_is_empty() {
222        let s = new_body_segment_state();
223        assert!(s.overrides.is_empty());
224    }
225
226    #[test]
227    fn set_and_get_scale() {
228        let mut s = new_body_segment_state();
229        let cfg = default_body_segment_config();
230        set_segment_scale(&mut s, &cfg, BodySegment::Torso, 1.2, 1.0);
231        assert!((get_segment_scale(&s, BodySegment::Torso) - 1.2).abs() < 1e-5);
232    }
233
234    #[test]
235    fn unknown_segment_returns_neutral() {
236        let s = new_body_segment_state();
237        assert!((get_segment_scale(&s, BodySegment::Head) - 1.0).abs() < 1e-5);
238    }
239
240    #[test]
241    fn clamps_min_scale() {
242        let mut s = new_body_segment_state();
243        let cfg = default_body_segment_config();
244        set_segment_scale(&mut s, &cfg, BodySegment::Head, 0.1, 1.0);
245        assert!(get_segment_scale(&s, BodySegment::Head) >= cfg.min_scale);
246    }
247
248    #[test]
249    fn clamps_max_scale() {
250        let mut s = new_body_segment_state();
251        let cfg = default_body_segment_config();
252        set_segment_scale(&mut s, &cfg, BodySegment::LowerLeg, 5.0, 1.0);
253        assert!(get_segment_scale(&s, BodySegment::LowerLeg) <= cfg.max_scale);
254    }
255
256    #[test]
257    fn reset_segment_removes_entry() {
258        let mut s = new_body_segment_state();
259        let cfg = default_body_segment_config();
260        set_segment_scale(&mut s, &cfg, BodySegment::UpperArm, 1.5, 1.0);
261        reset_segment(&mut s, BodySegment::UpperArm);
262        assert!(s.overrides.is_empty());
263    }
264
265    #[test]
266    fn blend_midpoint() {
267        let mut a = new_body_segment_state();
268        let mut b = new_body_segment_state();
269        let cfg = default_body_segment_config();
270        set_segment_scale(&mut a, &cfg, BodySegment::Torso, 1.0, 1.0);
271        set_segment_scale(&mut b, &cfg, BodySegment::Torso, 2.0, 1.0);
272        let mid = blend_segment_states(&a, &b, 0.5);
273        let s = get_segment_scale(&mid, BodySegment::Torso);
274        assert!((s - 1.5).abs() < 1e-4);
275    }
276
277    #[test]
278    fn segment_name_all_variants() {
279        assert_eq!(segment_name(BodySegment::Head), "head");
280        assert_eq!(segment_name(BodySegment::LowerLeg), "lower_leg");
281    }
282
283    #[test]
284    fn rhythm_scale_at_zero_phase() {
285        let v = rhythm_scale(1.0, 0.1, 0.0);
286        assert!((v - 1.0).abs() < 1e-5);
287    }
288
289    #[test]
290    fn json_contains_torso() {
291        let mut s = new_body_segment_state();
292        let cfg = default_body_segment_config();
293        set_segment_scale(&mut s, &cfg, BodySegment::Torso, 1.1, 0.8);
294        let j = state_to_json(&s);
295        assert!(j.contains("torso"));
296    }
297}