Skip to main content

oxihuman_morph/
body_taper_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4#![allow(dead_code)]
5
6//! Body taper control: adjusts torso taper from shoulders to waist.
7
8use std::f32::consts::PI;
9
10/// Configuration for body taper morphing.
11#[allow(dead_code)]
12#[derive(Debug, Clone)]
13pub struct BodyTaperConfig {
14    pub min_taper: f32,
15    pub max_taper: f32,
16}
17
18/// Runtime state for body taper morph.
19#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct BodyTaperState {
22    pub shoulder_width: f32,
23    pub waist_width: f32,
24    pub hip_width: f32,
25}
26
27#[allow(dead_code)]
28pub fn default_body_taper_config() -> BodyTaperConfig {
29    BodyTaperConfig {
30        min_taper: 0.0,
31        max_taper: 1.0,
32    }
33}
34
35#[allow(dead_code)]
36pub fn new_body_taper_state() -> BodyTaperState {
37    BodyTaperState {
38        shoulder_width: 0.6,
39        waist_width: 0.4,
40        hip_width: 0.5,
41    }
42}
43
44#[allow(dead_code)]
45pub fn bt_set_shoulder(state: &mut BodyTaperState, cfg: &BodyTaperConfig, v: f32) {
46    state.shoulder_width = v.clamp(cfg.min_taper, cfg.max_taper);
47}
48
49#[allow(dead_code)]
50pub fn bt_set_waist(state: &mut BodyTaperState, cfg: &BodyTaperConfig, v: f32) {
51    state.waist_width = v.clamp(cfg.min_taper, cfg.max_taper);
52}
53
54#[allow(dead_code)]
55pub fn bt_set_hip(state: &mut BodyTaperState, cfg: &BodyTaperConfig, v: f32) {
56    state.hip_width = v.clamp(cfg.min_taper, cfg.max_taper);
57}
58
59#[allow(dead_code)]
60pub fn bt_reset(state: &mut BodyTaperState) {
61    *state = new_body_taper_state();
62}
63
64#[allow(dead_code)]
65pub fn bt_taper_ratio(state: &BodyTaperState) -> f32 {
66    if state.shoulder_width.abs() < 1e-9 {
67        return 0.0;
68    }
69    state.waist_width / state.shoulder_width
70}
71
72#[allow(dead_code)]
73pub fn bt_to_weights(state: &BodyTaperState) -> Vec<(String, f32)> {
74    vec![
75        ("body_shoulder_width".to_string(), state.shoulder_width),
76        ("body_waist_width".to_string(), state.waist_width),
77        ("body_hip_width".to_string(), state.hip_width),
78    ]
79}
80
81#[allow(dead_code)]
82pub fn bt_to_json(state: &BodyTaperState) -> String {
83    format!(
84        r#"{{"shoulder_width":{:.4},"waist_width":{:.4},"hip_width":{:.4}}}"#,
85        state.shoulder_width, state.waist_width, state.hip_width
86    )
87}
88
89#[allow(dead_code)]
90pub fn bt_blend(a: &BodyTaperState, b: &BodyTaperState, t: f32) -> BodyTaperState {
91    let t = t.clamp(0.0, 1.0);
92    BodyTaperState {
93        shoulder_width: a.shoulder_width + (b.shoulder_width - a.shoulder_width) * t,
94        waist_width: a.waist_width + (b.waist_width - a.waist_width) * t,
95        hip_width: a.hip_width + (b.hip_width - a.hip_width) * t,
96    }
97}
98
99/// Approximate silhouette area using trapezoid approximation.
100#[allow(dead_code)]
101pub fn bt_silhouette_area(state: &BodyTaperState, height: f32) -> f32 {
102    let _ = PI; // use PI to keep import
103    0.5 * (state.shoulder_width + state.hip_width) * height
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_default_config() {
112        let cfg = default_body_taper_config();
113        assert!(cfg.min_taper.abs() < 1e-6);
114        assert!((cfg.max_taper - 1.0).abs() < 1e-6);
115    }
116
117    #[test]
118    fn test_new_state() {
119        let s = new_body_taper_state();
120        assert!((s.shoulder_width - 0.6).abs() < 1e-6);
121        assert!((s.waist_width - 0.4).abs() < 1e-6);
122    }
123
124    #[test]
125    fn test_set_shoulder_clamps() {
126        let cfg = default_body_taper_config();
127        let mut s = new_body_taper_state();
128        bt_set_shoulder(&mut s, &cfg, 5.0);
129        assert!((s.shoulder_width - 1.0).abs() < 1e-6);
130    }
131
132    #[test]
133    fn test_set_waist() {
134        let cfg = default_body_taper_config();
135        let mut s = new_body_taper_state();
136        bt_set_waist(&mut s, &cfg, 0.3);
137        assert!((s.waist_width - 0.3).abs() < 1e-6);
138    }
139
140    #[test]
141    fn test_set_hip() {
142        let cfg = default_body_taper_config();
143        let mut s = new_body_taper_state();
144        bt_set_hip(&mut s, &cfg, 0.7);
145        assert!((s.hip_width - 0.7).abs() < 1e-6);
146    }
147
148    #[test]
149    fn test_reset() {
150        let cfg = default_body_taper_config();
151        let mut s = new_body_taper_state();
152        bt_set_shoulder(&mut s, &cfg, 0.9);
153        bt_reset(&mut s);
154        assert!((s.shoulder_width - 0.6).abs() < 1e-6);
155    }
156
157    #[test]
158    fn test_taper_ratio() {
159        let s = new_body_taper_state();
160        let r = bt_taper_ratio(&s);
161        assert!((r - 0.4 / 0.6).abs() < 1e-4);
162    }
163
164    #[test]
165    fn test_to_weights() {
166        let s = new_body_taper_state();
167        assert_eq!(bt_to_weights(&s).len(), 3);
168    }
169
170    #[test]
171    fn test_blend() {
172        let a = new_body_taper_state();
173        let mut b = new_body_taper_state();
174        b.shoulder_width = 1.0;
175        let mid = bt_blend(&a, &b, 0.5);
176        assert!((mid.shoulder_width - 0.8).abs() < 1e-6);
177    }
178
179    #[test]
180    fn test_silhouette_area() {
181        let s = new_body_taper_state();
182        let area = bt_silhouette_area(&s, 1.0);
183        assert!(area > 0.0);
184    }
185}