oxihuman_morph/
eye_squint_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_8;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct EyeSquintConfig {
13 pub ref_angle_rad: f32,
15 pub lower_lid_bias: bool,
17}
18
19impl Default for EyeSquintConfig {
20 fn default() -> Self {
21 EyeSquintConfig {
22 ref_angle_rad: FRAC_PI_8,
23 lower_lid_bias: false,
24 }
25 }
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct EyeSquintState {
32 left: f32,
33 right: f32,
34 inner: f32,
36 config: EyeSquintConfig,
37}
38
39pub fn default_eye_squint_config() -> EyeSquintConfig {
41 EyeSquintConfig::default()
42}
43
44pub fn new_eye_squint_state(config: EyeSquintConfig) -> EyeSquintState {
46 EyeSquintState {
47 left: 0.0,
48 right: 0.0,
49 inner: 0.0,
50 config,
51 }
52}
53
54pub fn esq_set_left(state: &mut EyeSquintState, v: f32) {
56 state.left = v.clamp(0.0, 1.0);
57}
58
59pub fn esq_set_right(state: &mut EyeSquintState, v: f32) {
61 state.right = v.clamp(0.0, 1.0);
62}
63
64pub fn esq_set_both(state: &mut EyeSquintState, v: f32) {
66 let v = v.clamp(0.0, 1.0);
67 state.left = v;
68 state.right = v;
69}
70
71pub fn esq_set_inner(state: &mut EyeSquintState, v: f32) {
73 state.inner = v.clamp(0.0, 1.0);
74}
75
76pub fn esq_reset(state: &mut EyeSquintState) {
78 state.left = 0.0;
79 state.right = 0.0;
80 state.inner = 0.0;
81}
82
83pub fn esq_is_neutral(state: &EyeSquintState) -> bool {
85 state.left < 1e-5 && state.right < 1e-5
86}
87
88pub fn esq_asymmetry(state: &EyeSquintState) -> f32 {
90 (state.left - state.right).abs()
91}
92
93pub fn esq_average(state: &EyeSquintState) -> f32 {
95 (state.left + state.right) * 0.5
96}
97
98pub fn esq_compression_angle(state: &EyeSquintState) -> f32 {
100 esq_average(state) * state.config.ref_angle_rad
101}
102
103pub fn esq_to_weights(state: &EyeSquintState) -> [f32; 4] {
105 [state.left, state.right, state.inner, esq_average(state)]
106}
107
108pub fn esq_blend(a: &EyeSquintState, b: &EyeSquintState, t: f32) -> EyeSquintState {
110 let t = t.clamp(0.0, 1.0);
111 EyeSquintState {
112 left: a.left + (b.left - a.left) * t,
113 right: a.right + (b.right - a.right) * t,
114 inner: a.inner + (b.inner - a.inner) * t,
115 config: a.config.clone(),
116 }
117}
118
119pub fn esq_to_json(state: &EyeSquintState) -> String {
121 format!(
122 r#"{{"left":{:.4},"right":{:.4},"inner":{:.4}}}"#,
123 state.left, state.right, state.inner
124 )
125}
126
127#[cfg(test)]
131mod tests {
132 use super::*;
133
134 fn make() -> EyeSquintState {
135 new_eye_squint_state(default_eye_squint_config())
136 }
137
138 #[test]
139 fn neutral_on_creation() {
140 assert!(esq_is_neutral(&make()));
141 }
142
143 #[test]
144 fn set_left() {
145 let mut s = make();
146 esq_set_left(&mut s, 0.5);
147 assert!((s.left - 0.5).abs() < 1e-5);
148 }
149
150 #[test]
151 fn set_both_syncs() {
152 let mut s = make();
153 esq_set_both(&mut s, 0.7);
154 assert!((s.left - s.right).abs() < 1e-5);
155 }
156
157 #[test]
158 fn reset_clears() {
159 let mut s = make();
160 esq_set_both(&mut s, 1.0);
161 esq_reset(&mut s);
162 assert!(esq_is_neutral(&s));
163 }
164
165 #[test]
166 fn asymmetry_zero_equal() {
167 let mut s = make();
168 esq_set_both(&mut s, 0.5);
169 assert!(esq_asymmetry(&s) < 1e-5);
170 }
171
172 #[test]
173 fn compression_angle_positive() {
174 let mut s = make();
175 esq_set_both(&mut s, 0.5);
176 assert!(esq_compression_angle(&s) > 0.0);
177 }
178
179 #[test]
180 fn blend_midpoint() {
181 let mut b = make();
182 esq_set_both(&mut b, 1.0);
183 let m = esq_blend(&make(), &b, 0.5);
184 assert!((m.left - 0.5).abs() < 1e-5);
185 }
186
187 #[test]
188 fn weights_in_range() {
189 let mut s = make();
190 esq_set_both(&mut s, 0.8);
191 for v in esq_to_weights(&s) {
192 assert!((0.0..=1.0).contains(&v));
193 }
194 }
195
196 #[test]
197 fn json_has_left() {
198 assert!(esq_to_json(&make()).contains("left"));
199 }
200}