oxihuman_morph/
brow_tail_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum BrowTailSide {
11 Left,
12 Right,
13 Both,
14}
15
16#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct BrowTailState {
20 pub raise_left: f32,
22 pub raise_right: f32,
24 pub angle_left: f32,
26 pub angle_right: f32,
27}
28
29#[allow(dead_code)]
31#[derive(Clone, Debug)]
32pub struct BrowTailConfig {
33 pub max_raise: f32,
34 pub max_lower: f32,
35 pub max_angle_deg: f32,
36}
37
38impl Default for BrowTailConfig {
39 fn default() -> Self {
40 Self {
41 max_raise: 1.0,
42 max_lower: 1.0,
43 max_angle_deg: 30.0,
44 }
45 }
46}
47
48impl Default for BrowTailState {
49 fn default() -> Self {
50 Self {
51 raise_left: 0.0,
52 raise_right: 0.0,
53 angle_left: 0.0,
54 angle_right: 0.0,
55 }
56 }
57}
58
59#[allow(dead_code)]
60pub fn new_brow_tail_state() -> BrowTailState {
61 BrowTailState::default()
62}
63
64#[allow(dead_code)]
65pub fn default_brow_tail_config() -> BrowTailConfig {
66 BrowTailConfig::default()
67}
68
69#[allow(dead_code)]
70pub fn bt_set_raise(state: &mut BrowTailState, cfg: &BrowTailConfig, side: BrowTailSide, v: f32) {
71 let v = v.clamp(-cfg.max_lower, cfg.max_raise);
72 match side {
73 BrowTailSide::Left => state.raise_left = v,
74 BrowTailSide::Right => state.raise_right = v,
75 BrowTailSide::Both => {
76 state.raise_left = v;
77 state.raise_right = v;
78 }
79 }
80}
81
82#[allow(dead_code)]
83pub fn bt_set_angle(state: &mut BrowTailState, cfg: &BrowTailConfig, side: BrowTailSide, deg: f32) {
84 let d = deg.clamp(-cfg.max_angle_deg, cfg.max_angle_deg);
85 match side {
86 BrowTailSide::Left => state.angle_left = d,
87 BrowTailSide::Right => state.angle_right = d,
88 BrowTailSide::Both => {
89 state.angle_left = d;
90 state.angle_right = d;
91 }
92 }
93}
94
95#[allow(dead_code)]
96pub fn bt_reset(state: &mut BrowTailState) {
97 *state = BrowTailState::default();
98}
99
100#[allow(dead_code)]
101pub fn bt_symmetry(state: &BrowTailState) -> f32 {
102 1.0 - (state.raise_left - state.raise_right).abs().min(1.0)
103}
104
105#[allow(dead_code)]
106pub fn bt_blend(a: &BrowTailState, b: &BrowTailState, t: f32) -> BrowTailState {
107 let t = t.clamp(0.0, 1.0);
108 BrowTailState {
109 raise_left: a.raise_left + (b.raise_left - a.raise_left) * t,
110 raise_right: a.raise_right + (b.raise_right - a.raise_right) * t,
111 angle_left: a.angle_left + (b.angle_left - a.angle_left) * t,
112 angle_right: a.angle_right + (b.angle_right - a.angle_right) * t,
113 }
114}
115
116#[allow(dead_code)]
117pub fn bt_to_morph_weights(state: &BrowTailState) -> [f32; 4] {
118 [
119 state.raise_left,
120 state.raise_right,
121 state.angle_left / 30.0,
122 state.angle_right / 30.0,
123 ]
124}
125
126#[allow(dead_code)]
127pub fn bt_is_neutral(state: &BrowTailState) -> bool {
128 state.raise_left.abs() < 1e-4
129 && state.raise_right.abs() < 1e-4
130 && state.angle_left.abs() < 1e-4
131 && state.angle_right.abs() < 1e-4
132}
133
134#[allow(dead_code)]
135pub fn bt_to_json(state: &BrowTailState) -> String {
136 format!(
137 "{{\"raise_left\":{:.4},\"raise_right\":{:.4},\"angle_left\":{:.4},\"angle_right\":{:.4}}}",
138 state.raise_left, state.raise_right, state.angle_left, state.angle_right
139 )
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn default_is_neutral() {
148 assert!(bt_is_neutral(&new_brow_tail_state()));
149 }
150
151 #[test]
152 fn set_raise_clamps_max() {
153 let mut s = new_brow_tail_state();
154 let cfg = default_brow_tail_config();
155 bt_set_raise(&mut s, &cfg, BrowTailSide::Left, 2.0);
156 assert!(s.raise_left <= cfg.max_raise);
157 }
158
159 #[test]
160 fn set_raise_clamps_min() {
161 let mut s = new_brow_tail_state();
162 let cfg = default_brow_tail_config();
163 bt_set_raise(&mut s, &cfg, BrowTailSide::Right, -5.0);
164 assert!(s.raise_right >= -cfg.max_lower);
165 }
166
167 #[test]
168 fn both_side_sets_both() {
169 let mut s = new_brow_tail_state();
170 let cfg = default_brow_tail_config();
171 bt_set_raise(&mut s, &cfg, BrowTailSide::Both, 0.5);
172 assert!((s.raise_left - 0.5).abs() < 1e-5);
173 assert!((s.raise_right - 0.5).abs() < 1e-5);
174 }
175
176 #[test]
177 fn reset_clears_state() {
178 let mut s = new_brow_tail_state();
179 let cfg = default_brow_tail_config();
180 bt_set_raise(&mut s, &cfg, BrowTailSide::Both, 0.8);
181 bt_reset(&mut s);
182 assert!(bt_is_neutral(&s));
183 }
184
185 #[test]
186 fn blend_midpoint() {
187 let mut a = new_brow_tail_state();
188 let mut b = new_brow_tail_state();
189 let cfg = default_brow_tail_config();
190 bt_set_raise(&mut a, &cfg, BrowTailSide::Left, 0.0);
191 bt_set_raise(&mut b, &cfg, BrowTailSide::Left, 1.0);
192 let mid = bt_blend(&a, &b, 0.5);
193 assert!((mid.raise_left - 0.5).abs() < 1e-4);
194 }
195
196 #[test]
197 fn symmetry_one_when_equal() {
198 let s = new_brow_tail_state();
199 assert!((bt_symmetry(&s) - 1.0).abs() < 1e-5);
200 }
201
202 #[test]
203 fn morph_weights_len() {
204 let s = new_brow_tail_state();
205 assert_eq!(bt_to_morph_weights(&s).len(), 4);
206 }
207
208 #[test]
209 fn angle_clamp() {
210 let mut s = new_brow_tail_state();
211 let cfg = default_brow_tail_config();
212 bt_set_angle(&mut s, &cfg, BrowTailSide::Left, 999.0);
213 assert!(s.angle_left <= cfg.max_angle_deg);
214 }
215
216 #[test]
217 fn json_output_not_empty() {
218 let s = new_brow_tail_state();
219 assert!(!bt_to_json(&s).is_empty());
220 }
221}