1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_6;
8
9#[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#[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#[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#[allow(dead_code)]
54pub fn default_rib_cage_config() -> RibCageConfig {
55 RibCageConfig::default()
56}
57
58#[allow(dead_code)]
60pub fn new_rib_cage_state() -> RibCageState {
61 RibCageState::default()
62}
63
64#[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#[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#[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#[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#[allow(dead_code)]
90pub fn rc_reset(s: &mut RibCageState) {
91 *s = RibCageState::default();
92}
93
94#[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#[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#[allow(dead_code)]
121pub fn rc_flare_angle_rad(s: &RibCageState) -> f32 {
122 s.flare * FRAC_PI_6
123}
124
125#[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#[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}