Skip to main content

oxihuman_morph/
brow_control.rs

1//! Eyebrow control system for inner/outer raise, furrow, and arch.
2
3// ---------------------------------------------------------------------------
4// Types
5// ---------------------------------------------------------------------------
6
7/// Which side of the face the brow operation applies to.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum BrowSide {
11    Left,
12    Right,
13    Both,
14}
15
16/// Configuration for brow movement ranges and dynamics.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct BrowConfig {
20    /// Maximum inner raise (0..1).
21    pub max_inner_raise: f32,
22    /// Maximum outer raise (0..1).
23    pub max_outer_raise: f32,
24    /// Maximum lower amount (0..1).
25    pub max_lower: f32,
26    /// Maximum furrow/scrunch amount (0..1).
27    pub max_furrow: f32,
28    /// Maximum arch amount (0..1).
29    pub max_arch: f32,
30    /// Smoothing factor for transitions (higher = snappier).
31    pub smoothing: f32,
32}
33
34/// Runtime state for both eyebrows.
35#[allow(dead_code)]
36#[derive(Clone, Debug)]
37pub struct BrowState {
38    /// Left inner brow raise (0..1).
39    pub inner_raise_left: f32,
40    /// Right inner brow raise (0..1).
41    pub inner_raise_right: f32,
42    /// Left outer brow raise (0..1).
43    pub outer_raise_left: f32,
44    /// Right outer brow raise (0..1).
45    pub outer_raise_right: f32,
46    /// Left brow lower amount (0..1).
47    pub lower_left: f32,
48    /// Right brow lower amount (0..1).
49    pub lower_right: f32,
50    /// Furrow / scrunch intensity (0..1, bilateral).
51    pub furrow: f32,
52    /// Arch intensity (0..1, per side).
53    pub arch_left: f32,
54    /// Arch intensity right side.
55    pub arch_right: f32,
56    /// Target inner raise left (for smooth interpolation).
57    pub target_inner_left: f32,
58    /// Target inner raise right.
59    pub target_inner_right: f32,
60}
61
62/// Type alias for morph-weight output pairs.
63#[allow(dead_code)]
64pub type BrowMorphWeights = Vec<(String, f32)>;
65
66// ---------------------------------------------------------------------------
67// Construction
68// ---------------------------------------------------------------------------
69
70/// Return a sensible default brow configuration.
71#[allow(dead_code)]
72pub fn default_brow_config() -> BrowConfig {
73    BrowConfig {
74        max_inner_raise: 1.0,
75        max_outer_raise: 1.0,
76        max_lower: 1.0,
77        max_furrow: 1.0,
78        max_arch: 1.0,
79        smoothing: 10.0,
80    }
81}
82
83/// Create a new brow state at rest (all zeros).
84#[allow(dead_code)]
85pub fn new_brow_state() -> BrowState {
86    BrowState {
87        inner_raise_left: 0.0,
88        inner_raise_right: 0.0,
89        outer_raise_left: 0.0,
90        outer_raise_right: 0.0,
91        lower_left: 0.0,
92        lower_right: 0.0,
93        furrow: 0.0,
94        arch_left: 0.0,
95        arch_right: 0.0,
96        target_inner_left: 0.0,
97        target_inner_right: 0.0,
98    }
99}
100
101// ---------------------------------------------------------------------------
102// Setters
103// ---------------------------------------------------------------------------
104
105fn clamp01(v: f32) -> f32 {
106    v.clamp(0.0, 1.0)
107}
108
109/// Set inner or outer brow raise on the given side.
110/// `inner` selects inner raise when `true`, outer raise when `false`.
111#[allow(dead_code)]
112pub fn set_brow_raise(state: &mut BrowState, side: BrowSide, amount: f32, inner: bool) {
113    let v = clamp01(amount);
114    match (side, inner) {
115        (BrowSide::Left, true) => state.target_inner_left = v,
116        (BrowSide::Right, true) => state.target_inner_right = v,
117        (BrowSide::Both, true) => {
118            state.target_inner_left = v;
119            state.target_inner_right = v;
120        }
121        (BrowSide::Left, false) => state.outer_raise_left = v,
122        (BrowSide::Right, false) => state.outer_raise_right = v,
123        (BrowSide::Both, false) => {
124            state.outer_raise_left = v;
125            state.outer_raise_right = v;
126        }
127    }
128}
129
130/// Lower brows on the given side (0 = neutral, 1 = fully lowered).
131#[allow(dead_code)]
132pub fn set_brow_lower(state: &mut BrowState, side: BrowSide, amount: f32) {
133    let v = clamp01(amount);
134    match side {
135        BrowSide::Left => state.lower_left = v,
136        BrowSide::Right => state.lower_right = v,
137        BrowSide::Both => {
138            state.lower_left = v;
139            state.lower_right = v;
140        }
141    }
142}
143
144/// Set bilateral brow furrow / scrunch amount (0..1).
145#[allow(dead_code)]
146pub fn set_brow_furrow(state: &mut BrowState, amount: f32) {
147    state.furrow = clamp01(amount);
148}
149
150/// Set brow arch amount on the given side (0..1).
151#[allow(dead_code)]
152pub fn set_brow_arch(state: &mut BrowState, side: BrowSide, amount: f32) {
153    let v = clamp01(amount);
154    match side {
155        BrowSide::Left => state.arch_left = v,
156        BrowSide::Right => state.arch_right = v,
157        BrowSide::Both => {
158            state.arch_left = v;
159            state.arch_right = v;
160        }
161    }
162}
163
164// ---------------------------------------------------------------------------
165// Update / smoothing
166// ---------------------------------------------------------------------------
167
168/// Advance brow state toward targets using exponential smoothing.
169/// `dt` is the timestep in seconds.
170#[allow(dead_code)]
171pub fn update_brows(state: &mut BrowState, cfg: &BrowConfig, dt: f32) {
172    let t = (cfg.smoothing * dt).min(1.0);
173    state.inner_raise_left += (state.target_inner_left - state.inner_raise_left) * t;
174    state.inner_raise_right += (state.target_inner_right - state.inner_raise_right) * t;
175}
176
177// ---------------------------------------------------------------------------
178// Convenience accessors
179// ---------------------------------------------------------------------------
180
181/// Return the current left brow raise (average of inner + outer).
182#[allow(dead_code)]
183pub fn brow_raise_left(state: &BrowState) -> f32 {
184    (state.inner_raise_left + state.outer_raise_left) * 0.5
185}
186
187/// Return the current right brow raise (average of inner + outer).
188#[allow(dead_code)]
189pub fn brow_raise_right(state: &BrowState) -> f32 {
190    (state.inner_raise_right + state.outer_raise_right) * 0.5
191}
192
193/// Return the current furrow amount.
194#[allow(dead_code)]
195pub fn brow_furrow_amount(state: &BrowState) -> f32 {
196    state.furrow
197}
198
199// ---------------------------------------------------------------------------
200// Blending and morph-weight output
201// ---------------------------------------------------------------------------
202
203/// Linearly blend two brow states by `t` (0 = a, 1 = b).
204#[allow(dead_code)]
205pub fn blend_brow_states(a: &BrowState, b: &BrowState, t: f32) -> BrowState {
206    let t = clamp01(t);
207    let lerp = |x: f32, y: f32| x + (y - x) * t;
208    BrowState {
209        inner_raise_left: lerp(a.inner_raise_left, b.inner_raise_left),
210        inner_raise_right: lerp(a.inner_raise_right, b.inner_raise_right),
211        outer_raise_left: lerp(a.outer_raise_left, b.outer_raise_left),
212        outer_raise_right: lerp(a.outer_raise_right, b.outer_raise_right),
213        lower_left: lerp(a.lower_left, b.lower_left),
214        lower_right: lerp(a.lower_right, b.lower_right),
215        furrow: lerp(a.furrow, b.furrow),
216        arch_left: lerp(a.arch_left, b.arch_left),
217        arch_right: lerp(a.arch_right, b.arch_right),
218        target_inner_left: lerp(a.target_inner_left, b.target_inner_left),
219        target_inner_right: lerp(a.target_inner_right, b.target_inner_right),
220    }
221}
222
223/// Convert the current brow state into morph-target weights.
224#[allow(dead_code)]
225pub fn brow_to_morph_weights(state: &BrowState) -> BrowMorphWeights {
226    vec![
227        ("brow_inner_raise_left".to_string(), state.inner_raise_left),
228        (
229            "brow_inner_raise_right".to_string(),
230            state.inner_raise_right,
231        ),
232        ("brow_outer_raise_left".to_string(), state.outer_raise_left),
233        (
234            "brow_outer_raise_right".to_string(),
235            state.outer_raise_right,
236        ),
237        ("brow_lower_left".to_string(), state.lower_left),
238        ("brow_lower_right".to_string(), state.lower_right),
239        ("brow_furrow".to_string(), state.furrow),
240        ("brow_arch_left".to_string(), state.arch_left),
241        ("brow_arch_right".to_string(), state.arch_right),
242    ]
243}
244
245// ---------------------------------------------------------------------------
246// Reset
247// ---------------------------------------------------------------------------
248
249/// Reset all brow values to neutral.
250#[allow(dead_code)]
251pub fn reset_brows(state: &mut BrowState) {
252    *state = new_brow_state();
253}
254
255// ---------------------------------------------------------------------------
256// Emotion mapping
257// ---------------------------------------------------------------------------
258
259/// Map an emotion label to a preset brow configuration.
260/// Supported labels: "angry", "sad", "surprised", "happy", "fearful", "neutral".
261#[allow(dead_code)]
262pub fn emotion_to_brow(emotion: &str, cfg: &BrowConfig) -> BrowState {
263    let mut s = new_brow_state();
264    match emotion {
265        "angry" => {
266            set_brow_lower(&mut s, BrowSide::Both, 0.6 * cfg.max_lower);
267            set_brow_furrow(&mut s, 0.8 * cfg.max_furrow);
268        }
269        "sad" => {
270            set_brow_raise(&mut s, BrowSide::Both, 0.5 * cfg.max_inner_raise, true);
271            set_brow_lower(&mut s, BrowSide::Both, 0.2 * cfg.max_lower);
272        }
273        "surprised" => {
274            set_brow_raise(&mut s, BrowSide::Both, 0.9 * cfg.max_inner_raise, true);
275            set_brow_raise(&mut s, BrowSide::Both, 0.9 * cfg.max_outer_raise, false);
276            // Immediately apply targets
277            s.inner_raise_left = s.target_inner_left;
278            s.inner_raise_right = s.target_inner_right;
279        }
280        "happy" => {
281            set_brow_raise(&mut s, BrowSide::Both, 0.2 * cfg.max_outer_raise, false);
282            s.arch_left = 0.3 * cfg.max_arch;
283            s.arch_right = 0.3 * cfg.max_arch;
284        }
285        "fearful" => {
286            set_brow_raise(&mut s, BrowSide::Both, 0.7 * cfg.max_inner_raise, true);
287            set_brow_furrow(&mut s, 0.4 * cfg.max_furrow);
288            s.inner_raise_left = s.target_inner_left;
289            s.inner_raise_right = s.target_inner_right;
290        }
291        _ => {} // "neutral" or unknown → rest
292    }
293    s
294}
295
296// ---------------------------------------------------------------------------
297// Tests
298// ---------------------------------------------------------------------------
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    #[test]
305    fn test_default_brow_config_non_zero() {
306        let cfg = default_brow_config();
307        assert!(cfg.max_inner_raise > 0.0);
308        assert!(cfg.max_furrow > 0.0);
309        assert!(cfg.smoothing > 0.0);
310    }
311
312    #[test]
313    fn test_new_brow_state_zeroed() {
314        let s = new_brow_state();
315        assert_eq!(s.inner_raise_left, 0.0);
316        assert_eq!(s.furrow, 0.0);
317        assert_eq!(s.arch_right, 0.0);
318    }
319
320    #[test]
321    fn test_set_brow_raise_inner_left() {
322        let mut s = new_brow_state();
323        set_brow_raise(&mut s, BrowSide::Left, 0.8, true);
324        assert!((s.target_inner_left - 0.8).abs() < 1e-5);
325        assert_eq!(s.target_inner_right, 0.0);
326    }
327
328    #[test]
329    fn test_set_brow_raise_outer_both() {
330        let mut s = new_brow_state();
331        set_brow_raise(&mut s, BrowSide::Both, 0.6, false);
332        assert!((s.outer_raise_left - 0.6).abs() < 1e-5);
333        assert!((s.outer_raise_right - 0.6).abs() < 1e-5);
334    }
335
336    #[test]
337    fn test_set_brow_raise_clamped() {
338        let mut s = new_brow_state();
339        set_brow_raise(&mut s, BrowSide::Both, 2.0, true);
340        assert!((s.target_inner_left - 1.0).abs() < 1e-5);
341    }
342
343    #[test]
344    fn test_set_brow_lower() {
345        let mut s = new_brow_state();
346        set_brow_lower(&mut s, BrowSide::Right, 0.5);
347        assert!((s.lower_right - 0.5).abs() < 1e-5);
348        assert_eq!(s.lower_left, 0.0);
349    }
350
351    #[test]
352    fn test_set_brow_furrow() {
353        let mut s = new_brow_state();
354        set_brow_furrow(&mut s, 0.7);
355        assert!((s.furrow - 0.7).abs() < 1e-5);
356    }
357
358    #[test]
359    fn test_set_brow_arch() {
360        let mut s = new_brow_state();
361        set_brow_arch(&mut s, BrowSide::Left, 0.4);
362        assert!((s.arch_left - 0.4).abs() < 1e-5);
363        assert_eq!(s.arch_right, 0.0);
364    }
365
366    #[test]
367    fn test_update_brows_smoothing() {
368        let cfg = default_brow_config();
369        let mut s = new_brow_state();
370        set_brow_raise(&mut s, BrowSide::Left, 1.0, true);
371        update_brows(&mut s, &cfg, 1.0);
372        assert!(s.inner_raise_left > 0.0);
373    }
374
375    #[test]
376    fn test_brow_raise_left_accessor() {
377        let mut s = new_brow_state();
378        s.inner_raise_left = 0.4;
379        s.outer_raise_left = 0.6;
380        let avg = brow_raise_left(&s);
381        assert!((avg - 0.5).abs() < 1e-5);
382    }
383
384    #[test]
385    fn test_brow_furrow_amount_accessor() {
386        let mut s = new_brow_state();
387        s.furrow = 0.3;
388        assert!((brow_furrow_amount(&s) - 0.3).abs() < 1e-5);
389    }
390
391    #[test]
392    fn test_blend_brow_states_midpoint() {
393        let mut a = new_brow_state();
394        let mut b = new_brow_state();
395        a.furrow = 0.0;
396        b.furrow = 1.0;
397        let blended = blend_brow_states(&a, &b, 0.5);
398        assert!((blended.furrow - 0.5).abs() < 1e-5);
399    }
400
401    #[test]
402    fn test_brow_to_morph_weights_count() {
403        let s = new_brow_state();
404        let w = brow_to_morph_weights(&s);
405        assert_eq!(w.len(), 9);
406    }
407
408    #[test]
409    fn test_brow_to_morph_weights_keys_present() {
410        let s = new_brow_state();
411        let w = brow_to_morph_weights(&s);
412        let keys: Vec<&str> = w.iter().map(|(k, _)| k.as_str()).collect();
413        assert!(keys.contains(&"brow_furrow"));
414        assert!(keys.contains(&"brow_arch_left"));
415    }
416
417    #[test]
418    fn test_reset_brows() {
419        let mut s = new_brow_state();
420        s.furrow = 0.9;
421        s.arch_left = 0.5;
422        reset_brows(&mut s);
423        assert_eq!(s.furrow, 0.0);
424        assert_eq!(s.arch_left, 0.0);
425    }
426
427    #[test]
428    fn test_emotion_angry_sets_lower_and_furrow() {
429        let cfg = default_brow_config();
430        let s = emotion_to_brow("angry", &cfg);
431        assert!(s.lower_left > 0.0);
432        assert!(s.furrow > 0.0);
433    }
434
435    #[test]
436    fn test_emotion_surprised_raises_brows() {
437        let cfg = default_brow_config();
438        let s = emotion_to_brow("surprised", &cfg);
439        assert!(s.inner_raise_left > 0.0);
440        assert!(s.outer_raise_right > 0.0);
441    }
442
443    #[test]
444    fn test_emotion_neutral_zeroed() {
445        let cfg = default_brow_config();
446        let s = emotion_to_brow("neutral", &cfg);
447        assert_eq!(s.furrow, 0.0);
448        assert_eq!(s.inner_raise_left, 0.0);
449    }
450}