Skip to main content

oxihuman_morph/
rib_cage_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Rib cage control — width, depth, and flare morphs.
6
7use std::f32::consts::FRAC_PI_6;
8
9/// Rib cage configuration.
10#[derive(Debug, Clone, PartialEq)]
11#[allow(dead_code)]
12pub struct RibCageConfig {
13    pub width_min: f32,
14    pub width_max: f32,
15    pub depth_min: f32,
16    pub depth_max: f32,
17}
18
19impl Default for RibCageConfig {
20    fn default() -> Self {
21        Self {
22            width_min: -1.0,
23            width_max: 1.0,
24            depth_min: -1.0,
25            depth_max: 1.0,
26        }
27    }
28}
29
30/// Rib cage state.
31#[derive(Debug, Clone, PartialEq, Default)]
32#[allow(dead_code)]
33pub struct RibCageState {
34    pub width: f32,
35    pub depth: f32,
36    pub flare: f32,
37    pub barrel: f32,
38}
39
40/// Morph weight output.
41#[derive(Debug, Clone, PartialEq, Default)]
42#[allow(dead_code)]
43pub struct RibCageWeights {
44    pub wide: f32,
45    pub narrow: f32,
46    pub deep: f32,
47    pub shallow: f32,
48    pub flare_weight: f32,
49    pub barrel_weight: f32,
50}
51
52/// Create default config.
53#[allow(dead_code)]
54pub fn default_rib_cage_config() -> RibCageConfig {
55    RibCageConfig::default()
56}
57
58/// Create new state.
59#[allow(dead_code)]
60pub fn new_rib_cage_state() -> RibCageState {
61    RibCageState::default()
62}
63
64/// Set width, clamped.
65#[allow(dead_code)]
66pub fn rc_set_width(s: &mut RibCageState, cfg: &RibCageConfig, v: f32) {
67    s.width = v.clamp(cfg.width_min, cfg.width_max);
68}
69
70/// Set depth, clamped.
71#[allow(dead_code)]
72pub fn rc_set_depth(s: &mut RibCageState, cfg: &RibCageConfig, v: f32) {
73    s.depth = v.clamp(cfg.depth_min, cfg.depth_max);
74}
75
76/// Set flare (0..=1).
77#[allow(dead_code)]
78pub fn rc_set_flare(s: &mut RibCageState, v: f32) {
79    s.flare = v.clamp(0.0, 1.0);
80}
81
82/// Set barrel chest (0..=1).
83#[allow(dead_code)]
84pub fn rc_set_barrel(s: &mut RibCageState, v: f32) {
85    s.barrel = v.clamp(0.0, 1.0);
86}
87
88/// Reset state.
89#[allow(dead_code)]
90pub fn rc_reset(s: &mut RibCageState) {
91    *s = RibCageState::default();
92}
93
94/// Blend two states.
95#[allow(dead_code)]
96pub fn rc_blend(a: &RibCageState, b: &RibCageState, t: f32) -> RibCageState {
97    let t = t.clamp(0.0, 1.0);
98    RibCageState {
99        width: a.width + (b.width - a.width) * t,
100        depth: a.depth + (b.depth - a.depth) * t,
101        flare: a.flare + (b.flare - a.flare) * t,
102        barrel: a.barrel + (b.barrel - a.barrel) * t,
103    }
104}
105
106/// Convert state to weights.
107#[allow(dead_code)]
108pub fn rc_to_weights(s: &RibCageState) -> RibCageWeights {
109    RibCageWeights {
110        wide: s.width.max(0.0),
111        narrow: (-s.width).max(0.0),
112        deep: s.depth.max(0.0),
113        shallow: (-s.depth).max(0.0),
114        flare_weight: s.flare,
115        barrel_weight: s.barrel,
116    }
117}
118
119/// Approximate rib flare angle in radians using FRAC_PI_6 as the base.
120#[allow(dead_code)]
121pub fn rc_flare_angle_rad(s: &RibCageState) -> f32 {
122    s.flare * FRAC_PI_6
123}
124
125/// Export to JSON-like string.
126#[allow(dead_code)]
127pub fn rc_to_json(s: &RibCageState) -> String {
128    format!(
129        r#"{{"width":{:.4},"depth":{:.4},"flare":{:.4},"barrel":{:.4}}}"#,
130        s.width, s.depth, s.flare, s.barrel
131    )
132}
133
134/// Check if state is neutral.
135#[allow(dead_code)]
136pub fn rc_is_neutral(s: &RibCageState) -> bool {
137    [s.width, s.depth, s.flare, s.barrel]
138        .iter()
139        .all(|v| v.abs() < 1e-6)
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn default_is_neutral() {
148        assert!(rc_is_neutral(&new_rib_cage_state()));
149    }
150
151    #[test]
152    fn width_clamped_positive() {
153        let cfg = default_rib_cage_config();
154        let mut s = new_rib_cage_state();
155        rc_set_width(&mut s, &cfg, 3.0);
156        assert!((s.width - 1.0).abs() < 1e-6);
157    }
158
159    #[test]
160    fn depth_clamped_negative() {
161        let cfg = default_rib_cage_config();
162        let mut s = new_rib_cage_state();
163        rc_set_depth(&mut s, &cfg, -3.0);
164        assert!((s.depth + 1.0).abs() < 1e-6);
165    }
166
167    #[test]
168    fn flare_clamped() {
169        let mut s = new_rib_cage_state();
170        rc_set_flare(&mut s, 2.0);
171        assert!((s.flare - 1.0).abs() < 1e-6);
172    }
173
174    #[test]
175    fn reset_works() {
176        let cfg = default_rib_cage_config();
177        let mut s = new_rib_cage_state();
178        rc_set_width(&mut s, &cfg, 0.5);
179        rc_reset(&mut s);
180        assert!(rc_is_neutral(&s));
181    }
182
183    #[test]
184    fn blend_midpoint() {
185        let a = RibCageState::default();
186        let b = RibCageState {
187            width: 1.0,
188            depth: 1.0,
189            flare: 1.0,
190            barrel: 1.0,
191        };
192        let m = rc_blend(&a, &b, 0.5);
193        assert!((m.width - 0.5).abs() < 1e-5);
194    }
195
196    #[test]
197    fn weights_wide() {
198        let s = RibCageState {
199            width: 0.8,
200            depth: 0.0,
201            flare: 0.0,
202            barrel: 0.0,
203        };
204        let w = rc_to_weights(&s);
205        assert!(w.wide > 0.0 && w.narrow < 1e-6);
206    }
207
208    #[test]
209    fn flare_angle_full() {
210        let s = RibCageState {
211            width: 0.0,
212            depth: 0.0,
213            flare: 1.0,
214            barrel: 0.0,
215        };
216        let a = rc_flare_angle_rad(&s);
217        assert!((a - FRAC_PI_6).abs() < 1e-6);
218    }
219
220    #[test]
221    fn json_contains_barrel() {
222        let s = RibCageState {
223            width: 0.0,
224            depth: 0.0,
225            flare: 0.0,
226            barrel: 0.5,
227        };
228        assert!(rc_to_json(&s).contains("barrel"));
229    }
230
231    #[test]
232    fn slice_not_empty_check() {
233        let weights = [0.1f32, 0.2, 0.3];
234        assert!(!weights.is_empty());
235    }
236}