oxihuman_morph/
lip_retract_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_6;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum LipSide {
13 Upper,
14 Lower,
15}
16
17#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct LipRetractConfig {
21 pub max_angle_rad: f32,
23}
24
25impl Default for LipRetractConfig {
26 fn default() -> Self {
27 LipRetractConfig {
28 max_angle_rad: FRAC_PI_6,
29 }
30 }
31}
32
33#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct LipRetractState {
37 upper: f32,
38 lower: f32,
39 corners: f32,
41 config: LipRetractConfig,
42}
43
44pub fn default_lip_retract_config() -> LipRetractConfig {
46 LipRetractConfig::default()
47}
48
49pub fn new_lip_retract_state(config: LipRetractConfig) -> LipRetractState {
51 LipRetractState {
52 upper: 0.0,
53 lower: 0.0,
54 corners: 0.0,
55 config,
56 }
57}
58
59pub fn lrc_set_retract(state: &mut LipRetractState, side: LipSide, v: f32) {
61 let v = v.clamp(0.0, 1.0);
62 match side {
63 LipSide::Upper => state.upper = v,
64 LipSide::Lower => state.lower = v,
65 }
66}
67
68pub fn lrc_set_both(state: &mut LipRetractState, v: f32) {
70 let v = v.clamp(0.0, 1.0);
71 state.upper = v;
72 state.lower = v;
73}
74
75pub fn lrc_set_corners(state: &mut LipRetractState, v: f32) {
77 state.corners = v.clamp(0.0, 1.0);
78}
79
80pub fn lrc_reset(state: &mut LipRetractState) {
82 state.upper = 0.0;
83 state.lower = 0.0;
84 state.corners = 0.0;
85}
86
87pub fn lrc_is_neutral(state: &LipRetractState) -> bool {
89 state.upper < 1e-5 && state.lower < 1e-5 && state.corners < 1e-5
90}
91
92pub fn lrc_average(state: &LipRetractState) -> f32 {
94 (state.upper + state.lower) * 0.5
95}
96
97pub fn lrc_angle_rad(state: &LipRetractState) -> f32 {
99 lrc_average(state) * state.config.max_angle_rad
100}
101
102pub fn lrc_to_weights(state: &LipRetractState) -> [f32; 3] {
104 [state.upper, state.lower, state.corners]
105}
106
107pub fn lrc_blend(a: &LipRetractState, b: &LipRetractState, t: f32) -> LipRetractState {
109 let t = t.clamp(0.0, 1.0);
110 LipRetractState {
111 upper: a.upper + (b.upper - a.upper) * t,
112 lower: a.lower + (b.lower - a.lower) * t,
113 corners: a.corners + (b.corners - a.corners) * t,
114 config: a.config.clone(),
115 }
116}
117
118pub fn lrc_to_json(state: &LipRetractState) -> String {
120 format!(
121 r#"{{"upper":{:.4},"lower":{:.4},"corners":{:.4}}}"#,
122 state.upper, state.lower, state.corners
123 )
124}
125
126#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn make() -> LipRetractState {
134 new_lip_retract_state(default_lip_retract_config())
135 }
136
137 #[test]
138 fn neutral_on_creation() {
139 assert!(lrc_is_neutral(&make()));
140 }
141
142 #[test]
143 fn set_upper() {
144 let mut s = make();
145 lrc_set_retract(&mut s, LipSide::Upper, 0.5);
146 assert!((s.upper - 0.5).abs() < 1e-5);
147 }
148
149 #[test]
150 fn set_both_syncs() {
151 let mut s = make();
152 lrc_set_both(&mut s, 0.7);
153 assert!((s.upper - s.lower).abs() < 1e-5);
154 }
155
156 #[test]
157 fn reset_clears() {
158 let mut s = make();
159 lrc_set_both(&mut s, 1.0);
160 lrc_reset(&mut s);
161 assert!(lrc_is_neutral(&s));
162 }
163
164 #[test]
165 fn angle_positive_when_retracted() {
166 let mut s = make();
167 lrc_set_both(&mut s, 0.5);
168 assert!(lrc_angle_rad(&s) > 0.0);
169 }
170
171 #[test]
172 fn weights_in_range() {
173 let mut s = make();
174 lrc_set_both(&mut s, 0.6);
175 for v in lrc_to_weights(&s) {
176 assert!((0.0..=1.0).contains(&v));
177 }
178 }
179
180 #[test]
181 fn blend_midpoint() {
182 let mut b = make();
183 lrc_set_both(&mut b, 1.0);
184 let m = lrc_blend(&make(), &b, 0.5);
185 assert!((m.upper - 0.5).abs() < 1e-5);
186 }
187
188 #[test]
189 fn json_has_upper() {
190 assert!(lrc_to_json(&make()).contains("upper"));
191 }
192
193 #[test]
194 fn corners_clamped_high() {
195 let mut s = make();
196 lrc_set_corners(&mut s, 10.0);
197 assert!((s.corners - 1.0).abs() < 1e-5);
198 }
199}