Skip to main content

oxihuman_morph/
cheek_control.rs

1//! Cheek puff and hollow morph control for facial expressions.
2
3// ---------------------------------------------------------------------------
4// Types
5// ---------------------------------------------------------------------------
6
7/// Which side of the face to address.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum CheekSide {
11    Left,
12    Right,
13    Both,
14}
15
16/// Configuration for cheek dynamics and limits.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct CheekConfig {
20    /// Maximum puff intensity (0 = neutral, 1 = fully puffed).
21    pub max_puff: f32,
22    /// Maximum hollow intensity (0 = neutral, -1 = fully hollowed).
23    pub max_hollow: f32,
24    /// Maximum raise amount (0..1).
25    pub max_raise: f32,
26    /// Smoothing factor for transitions (higher = snappier).
27    pub smoothing: f32,
28    /// How much a smile automatically raises/puffs the cheeks (0..1).
29    pub smile_puff_factor: f32,
30}
31
32/// Runtime state of both cheeks.
33#[allow(dead_code)]
34#[derive(Clone, Debug)]
35pub struct CheekState {
36    /// Left cheek puff amount (0..1).
37    pub puff_left: f32,
38    /// Right cheek puff amount (0..1).
39    pub puff_right: f32,
40    /// Left cheek hollow amount (-1..0).
41    pub hollow_left: f32,
42    /// Right cheek hollow amount (-1..0).
43    pub hollow_right: f32,
44    /// Left cheek raise amount (0..1).
45    pub raise_left: f32,
46    /// Right cheek raise amount (0..1).
47    pub raise_right: f32,
48    /// Target puff left (for smooth interpolation).
49    pub target_puff_left: f32,
50    /// Target puff right (for smooth interpolation).
51    pub target_puff_right: f32,
52}
53
54/// Type alias for a list of morph-target weight pairs.
55#[allow(dead_code)]
56pub type CheekMorphWeights = Vec<(String, f32)>;
57
58// ---------------------------------------------------------------------------
59// Construction
60// ---------------------------------------------------------------------------
61
62/// Return a sensible default cheek configuration.
63#[allow(dead_code)]
64pub fn default_cheek_config() -> CheekConfig {
65    CheekConfig {
66        max_puff: 1.0,
67        max_hollow: -1.0,
68        max_raise: 1.0,
69        smoothing: 8.0,
70        smile_puff_factor: 0.3,
71    }
72}
73
74/// Create a new cheek state at rest (all zeros).
75#[allow(dead_code)]
76pub fn new_cheek_state() -> CheekState {
77    CheekState {
78        puff_left: 0.0,
79        puff_right: 0.0,
80        hollow_left: 0.0,
81        hollow_right: 0.0,
82        raise_left: 0.0,
83        raise_right: 0.0,
84        target_puff_left: 0.0,
85        target_puff_right: 0.0,
86    }
87}
88
89// ---------------------------------------------------------------------------
90// Setters
91// ---------------------------------------------------------------------------
92
93fn clamp01(v: f32) -> f32 {
94    v.clamp(0.0, 1.0)
95}
96
97fn clamp_neg1_0(v: f32) -> f32 {
98    v.clamp(-1.0, 0.0)
99}
100
101/// Set cheek puff amount (clamped 0..1) on the given side.
102#[allow(dead_code)]
103pub fn set_cheek_puff(state: &mut CheekState, side: CheekSide, amount: f32) {
104    let v = clamp01(amount);
105    match side {
106        CheekSide::Left => {
107            state.target_puff_left = v;
108        }
109        CheekSide::Right => {
110            state.target_puff_right = v;
111        }
112        CheekSide::Both => {
113            state.target_puff_left = v;
114            state.target_puff_right = v;
115        }
116    }
117}
118
119/// Set cheek hollow amount (clamped -1..0) on the given side.
120#[allow(dead_code)]
121pub fn set_cheek_hollow(state: &mut CheekState, side: CheekSide, amount: f32) {
122    let v = clamp_neg1_0(amount);
123    match side {
124        CheekSide::Left => state.hollow_left = v,
125        CheekSide::Right => state.hollow_right = v,
126        CheekSide::Both => {
127            state.hollow_left = v;
128            state.hollow_right = v;
129        }
130    }
131}
132
133/// Set cheek raise amount (clamped 0..1) on the given side.
134#[allow(dead_code)]
135pub fn set_cheek_raise(state: &mut CheekState, side: CheekSide, amount: f32) {
136    let v = clamp01(amount);
137    match side {
138        CheekSide::Left => state.raise_left = v,
139        CheekSide::Right => state.raise_right = v,
140        CheekSide::Both => {
141            state.raise_left = v;
142            state.raise_right = v;
143        }
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Update / smoothing
149// ---------------------------------------------------------------------------
150
151/// Advance cheek state toward targets using exponential smoothing.
152/// `dt` is the timestep in seconds.
153#[allow(dead_code)]
154pub fn update_cheeks(state: &mut CheekState, cfg: &CheekConfig, dt: f32) {
155    let t = (cfg.smoothing * dt).min(1.0);
156    state.puff_left += (state.target_puff_left - state.puff_left) * t;
157    state.puff_right += (state.target_puff_right - state.puff_right) * t;
158}
159
160// ---------------------------------------------------------------------------
161// Convenience accessors
162// ---------------------------------------------------------------------------
163
164/// Return the current left cheek puff value.
165#[allow(dead_code)]
166pub fn cheek_puff_left(state: &CheekState) -> f32 {
167    state.puff_left
168}
169
170/// Return the current right cheek puff value.
171#[allow(dead_code)]
172pub fn cheek_puff_right(state: &CheekState) -> f32 {
173    state.puff_right
174}
175
176/// Return the current left cheek hollow value.
177#[allow(dead_code)]
178pub fn cheek_hollow_left(state: &CheekState) -> f32 {
179    state.hollow_left
180}
181
182/// Return the current right cheek hollow value.
183#[allow(dead_code)]
184pub fn cheek_hollow_right(state: &CheekState) -> f32 {
185    state.hollow_right
186}
187
188// ---------------------------------------------------------------------------
189// Blending and morph-weight output
190// ---------------------------------------------------------------------------
191
192/// Linearly blend two cheek states by weight `t` (0 = a, 1 = b).
193#[allow(dead_code)]
194pub fn blend_cheek_states(a: &CheekState, b: &CheekState, t: f32) -> CheekState {
195    let t = clamp01(t);
196    let lerp = |x: f32, y: f32| x + (y - x) * t;
197    CheekState {
198        puff_left: lerp(a.puff_left, b.puff_left),
199        puff_right: lerp(a.puff_right, b.puff_right),
200        hollow_left: lerp(a.hollow_left, b.hollow_left),
201        hollow_right: lerp(a.hollow_right, b.hollow_right),
202        raise_left: lerp(a.raise_left, b.raise_left),
203        raise_right: lerp(a.raise_right, b.raise_right),
204        target_puff_left: lerp(a.target_puff_left, b.target_puff_left),
205        target_puff_right: lerp(a.target_puff_right, b.target_puff_right),
206    }
207}
208
209/// Convert the current cheek state into a list of morph-target weights.
210#[allow(dead_code)]
211pub fn cheek_to_morph_weights(state: &CheekState) -> CheekMorphWeights {
212    vec![
213        ("cheek_puff_left".to_string(), state.puff_left),
214        ("cheek_puff_right".to_string(), state.puff_right),
215        ("cheek_hollow_left".to_string(), -state.hollow_left),
216        ("cheek_hollow_right".to_string(), -state.hollow_right),
217        ("cheek_raise_left".to_string(), state.raise_left),
218        ("cheek_raise_right".to_string(), state.raise_right),
219    ]
220}
221
222// ---------------------------------------------------------------------------
223// Reset
224// ---------------------------------------------------------------------------
225
226/// Reset all cheek values to neutral.
227#[allow(dead_code)]
228pub fn reset_cheeks(state: &mut CheekState) {
229    *state = new_cheek_state();
230}
231
232// ---------------------------------------------------------------------------
233// Smile effect
234// ---------------------------------------------------------------------------
235
236/// Automatically puff and raise cheeks proportional to a smile intensity (0..1).
237#[allow(dead_code)]
238pub fn apply_smile_effect(state: &mut CheekState, cfg: &CheekConfig, smile: f32) {
239    let smile = clamp01(smile);
240    let puff = smile * cfg.smile_puff_factor;
241    let raise = smile * cfg.smile_puff_factor;
242    set_cheek_puff(state, CheekSide::Both, puff);
243    set_cheek_raise(state, CheekSide::Both, raise);
244    // Immediately apply (no separate update step needed for raise)
245    state.puff_left = state.target_puff_left;
246    state.puff_right = state.target_puff_right;
247}
248
249// ---------------------------------------------------------------------------
250// Tests
251// ---------------------------------------------------------------------------
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    #[test]
258    fn test_default_cheek_config_values() {
259        let cfg = default_cheek_config();
260        assert_eq!(cfg.max_puff, 1.0);
261        assert_eq!(cfg.max_hollow, -1.0);
262        assert!(cfg.smoothing > 0.0);
263    }
264
265    #[test]
266    fn test_new_cheek_state_zeroed() {
267        let s = new_cheek_state();
268        assert_eq!(s.puff_left, 0.0);
269        assert_eq!(s.puff_right, 0.0);
270        assert_eq!(s.hollow_left, 0.0);
271        assert_eq!(s.hollow_right, 0.0);
272    }
273
274    #[test]
275    fn test_set_cheek_puff_left() {
276        let mut s = new_cheek_state();
277        set_cheek_puff(&mut s, CheekSide::Left, 0.7);
278        assert!((s.target_puff_left - 0.7).abs() < 1e-5);
279        assert_eq!(s.target_puff_right, 0.0);
280    }
281
282    #[test]
283    fn test_set_cheek_puff_right() {
284        let mut s = new_cheek_state();
285        set_cheek_puff(&mut s, CheekSide::Right, 0.5);
286        assert!((s.target_puff_right - 0.5).abs() < 1e-5);
287        assert_eq!(s.target_puff_left, 0.0);
288    }
289
290    #[test]
291    fn test_set_cheek_puff_both() {
292        let mut s = new_cheek_state();
293        set_cheek_puff(&mut s, CheekSide::Both, 0.9);
294        assert!((s.target_puff_left - 0.9).abs() < 1e-5);
295        assert!((s.target_puff_right - 0.9).abs() < 1e-5);
296    }
297
298    #[test]
299    fn test_set_cheek_puff_clamped_above() {
300        let mut s = new_cheek_state();
301        set_cheek_puff(&mut s, CheekSide::Both, 2.0);
302        assert!((s.target_puff_left - 1.0).abs() < 1e-5);
303    }
304
305    #[test]
306    fn test_set_cheek_hollow_clamped() {
307        let mut s = new_cheek_state();
308        set_cheek_hollow(&mut s, CheekSide::Both, -0.5);
309        assert!((s.hollow_left - (-0.5)).abs() < 1e-5);
310        set_cheek_hollow(&mut s, CheekSide::Both, -2.0);
311        assert!((s.hollow_left - (-1.0)).abs() < 1e-5);
312    }
313
314    #[test]
315    fn test_set_cheek_raise() {
316        let mut s = new_cheek_state();
317        set_cheek_raise(&mut s, CheekSide::Left, 0.4);
318        assert!((s.raise_left - 0.4).abs() < 1e-5);
319        assert_eq!(s.raise_right, 0.0);
320    }
321
322    #[test]
323    fn test_update_cheeks_smoothing() {
324        let cfg = default_cheek_config();
325        let mut s = new_cheek_state();
326        set_cheek_puff(&mut s, CheekSide::Left, 1.0);
327        update_cheeks(&mut s, &cfg, 1.0);
328        assert!(s.puff_left > 0.0);
329        assert!(s.puff_left <= 1.0);
330    }
331
332    #[test]
333    fn test_cheek_puff_accessors() {
334        let mut s = new_cheek_state();
335        s.puff_left = 0.3;
336        s.puff_right = 0.6;
337        assert!((cheek_puff_left(&s) - 0.3).abs() < 1e-5);
338        assert!((cheek_puff_right(&s) - 0.6).abs() < 1e-5);
339    }
340
341    #[test]
342    fn test_cheek_hollow_accessors() {
343        let mut s = new_cheek_state();
344        s.hollow_left = -0.4;
345        s.hollow_right = -0.8;
346        assert!((cheek_hollow_left(&s) - (-0.4)).abs() < 1e-5);
347        assert!((cheek_hollow_right(&s) - (-0.8)).abs() < 1e-5);
348    }
349
350    #[test]
351    fn test_blend_cheek_states_midpoint() {
352        let mut a = new_cheek_state();
353        let mut b = new_cheek_state();
354        a.puff_left = 0.0;
355        b.puff_left = 1.0;
356        let blended = blend_cheek_states(&a, &b, 0.5);
357        assert!((blended.puff_left - 0.5).abs() < 1e-5);
358    }
359
360    #[test]
361    fn test_blend_cheek_states_at_zero() {
362        let a = new_cheek_state();
363        let b = new_cheek_state();
364        let blended = blend_cheek_states(&a, &b, 0.0);
365        assert_eq!(blended.puff_left, a.puff_left);
366    }
367
368    #[test]
369    fn test_cheek_to_morph_weights_keys() {
370        let s = new_cheek_state();
371        let weights = cheek_to_morph_weights(&s);
372        assert_eq!(weights.len(), 6);
373        let keys: Vec<&str> = weights.iter().map(|(k, _)| k.as_str()).collect();
374        assert!(keys.contains(&"cheek_puff_left"));
375        assert!(keys.contains(&"cheek_hollow_left"));
376        assert!(keys.contains(&"cheek_raise_right"));
377    }
378
379    #[test]
380    fn test_reset_cheeks() {
381        let mut s = new_cheek_state();
382        s.puff_left = 0.9;
383        s.hollow_right = -0.5;
384        reset_cheeks(&mut s);
385        assert_eq!(s.puff_left, 0.0);
386        assert_eq!(s.hollow_right, 0.0);
387    }
388
389    #[test]
390    fn test_apply_smile_effect_puffs_cheeks() {
391        let cfg = default_cheek_config();
392        let mut s = new_cheek_state();
393        apply_smile_effect(&mut s, &cfg, 1.0);
394        assert!(s.puff_left > 0.0);
395        assert!(s.puff_right > 0.0);
396    }
397
398    #[test]
399    fn test_apply_smile_effect_zero_smile() {
400        let cfg = default_cheek_config();
401        let mut s = new_cheek_state();
402        apply_smile_effect(&mut s, &cfg, 0.0);
403        assert_eq!(s.puff_left, 0.0);
404        assert_eq!(s.puff_right, 0.0);
405    }
406}