Skip to main content

oxihuman_morph/
lip_purse_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Lip purse control — orbicularis oris contraction and lip protrusion.
6
7/// Configuration for lip purse.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct LipPurseConfig {
11    pub max_purse: f32,
12}
13
14/// Runtime state.
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct LipPurseState {
18    pub upper_purse: f32,
19    pub lower_purse: f32,
20    pub protrusion: f32,
21}
22
23#[allow(dead_code)]
24pub fn default_lip_purse_config() -> LipPurseConfig {
25    LipPurseConfig { max_purse: 1.0 }
26}
27
28#[allow(dead_code)]
29pub fn new_lip_purse_state() -> LipPurseState {
30    LipPurseState {
31        upper_purse: 0.0,
32        lower_purse: 0.0,
33        protrusion: 0.0,
34    }
35}
36
37#[allow(dead_code)]
38pub fn lpur_set_upper(state: &mut LipPurseState, cfg: &LipPurseConfig, v: f32) {
39    state.upper_purse = v.clamp(0.0, cfg.max_purse);
40}
41
42#[allow(dead_code)]
43pub fn lpur_set_lower(state: &mut LipPurseState, cfg: &LipPurseConfig, v: f32) {
44    state.lower_purse = v.clamp(0.0, cfg.max_purse);
45}
46
47#[allow(dead_code)]
48pub fn lpur_set_both(state: &mut LipPurseState, cfg: &LipPurseConfig, v: f32) {
49    let clamped = v.clamp(0.0, cfg.max_purse);
50    state.upper_purse = clamped;
51    state.lower_purse = clamped;
52}
53
54#[allow(dead_code)]
55pub fn lpur_set_protrusion(state: &mut LipPurseState, v: f32) {
56    state.protrusion = v.clamp(0.0, 1.0);
57}
58
59#[allow(dead_code)]
60pub fn lpur_reset(state: &mut LipPurseState) {
61    *state = new_lip_purse_state();
62}
63
64#[allow(dead_code)]
65pub fn lpur_is_neutral(state: &LipPurseState) -> bool {
66    state.upper_purse.abs() < 1e-6
67        && state.lower_purse.abs() < 1e-6
68        && state.protrusion.abs() < 1e-6
69}
70
71#[allow(dead_code)]
72pub fn lpur_intensity(state: &LipPurseState) -> f32 {
73    (state.upper_purse + state.lower_purse) * 0.5
74}
75
76#[allow(dead_code)]
77pub fn lpur_blend(a: &LipPurseState, b: &LipPurseState, t: f32) -> LipPurseState {
78    let t = t.clamp(0.0, 1.0);
79    LipPurseState {
80        upper_purse: a.upper_purse + (b.upper_purse - a.upper_purse) * t,
81        lower_purse: a.lower_purse + (b.lower_purse - a.lower_purse) * t,
82        protrusion: a.protrusion + (b.protrusion - a.protrusion) * t,
83    }
84}
85
86#[allow(dead_code)]
87pub fn lpur_to_weights(state: &LipPurseState) -> Vec<(String, f32)> {
88    vec![
89        ("lip_purse_upper".to_string(), state.upper_purse),
90        ("lip_purse_lower".to_string(), state.lower_purse),
91        ("lip_protrusion".to_string(), state.protrusion),
92    ]
93}
94
95#[allow(dead_code)]
96pub fn lpur_to_json(state: &LipPurseState) -> String {
97    format!(
98        r#"{{"upper_purse":{:.4},"lower_purse":{:.4},"protrusion":{:.4}}}"#,
99        state.upper_purse, state.lower_purse, state.protrusion
100    )
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn default_config() {
109        let cfg = default_lip_purse_config();
110        assert!((cfg.max_purse - 1.0).abs() < 1e-6);
111    }
112
113    #[test]
114    fn new_state_neutral() {
115        let s = new_lip_purse_state();
116        assert!(lpur_is_neutral(&s));
117    }
118
119    #[test]
120    fn set_upper_clamps() {
121        let cfg = default_lip_purse_config();
122        let mut s = new_lip_purse_state();
123        lpur_set_upper(&mut s, &cfg, 5.0);
124        assert!((s.upper_purse - 1.0).abs() < 1e-6);
125    }
126
127    #[test]
128    fn set_lower() {
129        let cfg = default_lip_purse_config();
130        let mut s = new_lip_purse_state();
131        lpur_set_lower(&mut s, &cfg, 0.4);
132        assert!((s.lower_purse - 0.4).abs() < 1e-6);
133    }
134
135    #[test]
136    fn set_both() {
137        let cfg = default_lip_purse_config();
138        let mut s = new_lip_purse_state();
139        lpur_set_both(&mut s, &cfg, 0.6);
140        assert!((lpur_intensity(&s) - 0.6).abs() < 1e-6);
141    }
142
143    #[test]
144    fn set_protrusion() {
145        let mut s = new_lip_purse_state();
146        lpur_set_protrusion(&mut s, 0.5);
147        assert!((s.protrusion - 0.5).abs() < 1e-6);
148    }
149
150    #[test]
151    fn reset_clears() {
152        let cfg = default_lip_purse_config();
153        let mut s = new_lip_purse_state();
154        lpur_set_both(&mut s, &cfg, 0.8);
155        lpur_reset(&mut s);
156        assert!(lpur_is_neutral(&s));
157    }
158
159    #[test]
160    fn blend_midpoint() {
161        let a = new_lip_purse_state();
162        let cfg = default_lip_purse_config();
163        let mut b = new_lip_purse_state();
164        lpur_set_both(&mut b, &cfg, 1.0);
165        let m = lpur_blend(&a, &b, 0.5);
166        assert!((m.upper_purse - 0.5).abs() < 1e-6);
167    }
168
169    #[test]
170    fn to_weights_count() {
171        let s = new_lip_purse_state();
172        assert_eq!(lpur_to_weights(&s).len(), 3);
173    }
174
175    #[test]
176    fn to_json_fields() {
177        let s = new_lip_purse_state();
178        let j = lpur_to_json(&s);
179        assert!(j.contains("upper_purse"));
180    }
181}