1#[allow(dead_code)]
4#[derive(Clone, Debug)]
5pub struct EyeState {
6 pub yaw: f32,
8 pub pitch: f32,
10 pub blink_fraction: f32,
12 pub blink_timer: f32,
14 pub blink_duration: f32,
16 pub blinking: bool,
18 pub lcg_state: u64,
20}
21
22#[allow(dead_code)]
23#[derive(Clone, Debug)]
24pub enum GazeTarget {
25 Point { origin: [f32; 3], target: [f32; 3] },
27 Angles { yaw: f32, pitch: f32 },
29 Forward,
31}
32
33#[allow(dead_code)]
34#[derive(Clone, Debug)]
35pub struct EyeControlConfig {
36 pub max_yaw: f32,
38 pub max_pitch: f32,
40 pub blink_interval: f32,
42 pub blink_duration: f32,
44 pub saccade_speed: f32,
46 pub blink_variation: f32,
48}
49
50#[allow(dead_code)]
55fn lcg_step(state: &mut u64) -> f32 {
56 *state = state
57 .wrapping_mul(6_364_136_223_846_793_005)
58 .wrapping_add(1_442_695_040_888_963_407);
59 let bits = (*state >> 33) as u32;
60 (bits as f32) / (u32::MAX as f32 + 1.0)
61}
62
63#[allow(dead_code)]
69pub fn default_eye_config() -> EyeControlConfig {
70 EyeControlConfig {
71 max_yaw: std::f32::consts::FRAC_PI_4,
72 max_pitch: std::f32::consts::FRAC_PI_6,
73 blink_interval: 4.0,
74 blink_duration: 0.15,
75 saccade_speed: 5.0,
76 blink_variation: 0.3,
77 }
78}
79
80#[allow(dead_code)]
82pub fn new_eye_state(lcg_seed: u64) -> EyeState {
83 EyeState {
84 yaw: 0.0,
85 pitch: 0.0,
86 blink_fraction: 0.0,
87 blink_timer: 0.0,
88 blink_duration: 0.0,
89 blinking: false,
90 lcg_state: lcg_seed.max(1),
91 }
92}
93
94#[allow(dead_code)]
101pub fn look_at_target(origin: [f32; 3], target: [f32; 3]) -> (f32, f32) {
102 let dx = target[0] - origin[0];
103 let dy = target[1] - origin[1];
104 let dz = target[2] - origin[2];
105 let horiz = (dx * dx + dz * dz).sqrt();
106 let yaw = dx.atan2(dz);
107 let pitch = (-dy).atan2(horiz);
108 (yaw, pitch)
109}
110
111#[allow(dead_code)]
113pub fn eye_yaw_deg(state: &EyeState) -> f32 {
114 state.yaw.to_degrees()
115}
116
117#[allow(dead_code)]
119pub fn eye_pitch_deg(state: &EyeState) -> f32 {
120 state.pitch.to_degrees()
121}
122
123#[allow(dead_code)]
130pub fn saccade_towards(
131 state: &mut EyeState,
132 target_yaw: f32,
133 target_pitch: f32,
134 speed: f32,
135 dt: f32,
136) {
137 let max_step = speed * dt;
138 let dy = target_yaw - state.yaw;
139 let dp = target_pitch - state.pitch;
140 let dist = (dy * dy + dp * dp).sqrt();
141 if dist <= max_step || dist < 1e-6 {
142 state.yaw = target_yaw;
143 state.pitch = target_pitch;
144 } else {
145 let s = max_step / dist;
146 state.yaw += dy * s;
147 state.pitch += dp * s;
148 }
149}
150
151#[allow(dead_code)]
153pub fn update_eye_gaze(
154 state: &mut EyeState,
155 target: &GazeTarget,
156 config: &EyeControlConfig,
157 dt: f32,
158) {
159 let (ty, tp) = match target {
160 GazeTarget::Forward => (0.0_f32, 0.0_f32),
161 GazeTarget::Angles { yaw, pitch } => (*yaw, *pitch),
162 GazeTarget::Point {
163 origin,
164 target: tgt,
165 } => look_at_target(*origin, *tgt),
166 };
167 saccade_towards(state, ty, tp, config.saccade_speed, dt);
168 clamp_gaze(state, config);
169}
170
171#[allow(dead_code)]
173pub fn clamp_gaze(state: &mut EyeState, config: &EyeControlConfig) {
174 state.yaw = state.yaw.clamp(-config.max_yaw, config.max_yaw);
175 state.pitch = state.pitch.clamp(-config.max_pitch, config.max_pitch);
176}
177
178#[allow(dead_code)]
184pub fn blink_factor(state: &EyeState) -> f32 {
185 state.blink_fraction
186}
187
188#[allow(dead_code)]
190pub fn trigger_blink(state: &mut EyeState, duration: f32) {
191 state.blinking = true;
192 state.blink_duration = duration.max(0.01);
193 state.blink_timer = 0.0;
194 state.blink_fraction = 0.0;
195}
196
197#[allow(dead_code)]
200pub fn auto_blink_tick(state: &mut EyeState, config: &EyeControlConfig, dt: f32) {
201 if state.blinking {
202 state.blink_timer += dt;
203 let half = state.blink_duration * 0.5;
204 if state.blink_timer < half {
205 state.blink_fraction = state.blink_timer / half;
206 } else if state.blink_timer < state.blink_duration {
207 state.blink_fraction = 1.0 - (state.blink_timer - half) / half;
208 } else {
209 state.blink_fraction = 0.0;
210 state.blinking = false;
211 let noise = lcg_step(&mut state.lcg_state) * 2.0 - 1.0;
213 state.blink_timer = -config.blink_interval * (1.0 + noise * config.blink_variation);
214 }
215 } else {
216 state.blink_timer += dt;
217 if state.blink_timer >= config.blink_interval {
218 trigger_blink(state, config.blink_duration);
219 }
220 }
221}
222
223#[allow(dead_code)]
225pub fn is_blinking_eye(state: &EyeState) -> bool {
226 state.blinking
227}
228
229#[allow(dead_code)]
235pub fn gaze_blend(a: &EyeState, b: &EyeState, t: f32) -> EyeState {
236 let t = t.clamp(0.0, 1.0);
237 let u = 1.0 - t;
238 EyeState {
239 yaw: a.yaw * u + b.yaw * t,
240 pitch: a.pitch * u + b.pitch * t,
241 blink_fraction: a.blink_fraction * u + b.blink_fraction * t,
242 blink_timer: a.blink_timer * u + b.blink_timer * t,
243 blink_duration: a.blink_duration * u + b.blink_duration * t,
244 blinking: if t < 0.5 { a.blinking } else { b.blinking },
245 lcg_state: a.lcg_state,
246 }
247}
248
249#[allow(dead_code)]
251pub fn gaze_distance(a: &EyeState, b: &EyeState) -> f32 {
252 let dy = a.yaw - b.yaw;
253 let dp = a.pitch - b.pitch;
254 (dy * dy + dp * dp).sqrt()
255}
256
257#[cfg(test)]
262mod tests {
263 use super::*;
264
265 fn cfg() -> EyeControlConfig {
266 default_eye_config()
267 }
268
269 #[test]
270 fn test_default_eye_config() {
271 let c = cfg();
272 assert!(c.max_yaw > 0.0);
273 assert!(c.max_pitch > 0.0);
274 assert!(c.blink_interval > 0.0);
275 }
276
277 #[test]
278 fn test_new_eye_state() {
279 let s = new_eye_state(42);
280 assert_eq!(s.yaw, 0.0);
281 assert_eq!(s.pitch, 0.0);
282 assert!(!s.blinking);
283 }
284
285 #[test]
286 fn test_look_at_target_forward() {
287 let origin = [0.0_f32, 0.0, 0.0];
288 let target = [0.0_f32, 0.0, 10.0];
289 let (y, p) = look_at_target(origin, target);
290 assert!(y.abs() < 1e-4, "yaw should be ~0 for forward target");
291 assert!(p.abs() < 1e-4, "pitch should be ~0 for forward target");
292 }
293
294 #[test]
295 fn test_look_at_target_right() {
296 let origin = [0.0_f32, 0.0, 0.0];
297 let target = [1.0_f32, 0.0, 1.0];
298 let (y, _p) = look_at_target(origin, target);
299 assert!(y > 0.0, "yaw should be positive looking right");
300 }
301
302 #[test]
303 fn test_eye_yaw_pitch_deg() {
304 let mut s = new_eye_state(1);
305 s.yaw = std::f32::consts::FRAC_PI_4;
306 s.pitch = std::f32::consts::FRAC_PI_6;
307 assert!((eye_yaw_deg(&s) - 45.0).abs() < 0.01);
308 assert!((eye_pitch_deg(&s) - 30.0).abs() < 0.01);
309 }
310
311 #[test]
312 fn test_saccade_towards_reaches() {
313 let mut s = new_eye_state(1);
314 saccade_towards(&mut s, 1.0, 0.5, 10.0, 1.0);
315 assert!((s.yaw - 1.0).abs() < 1e-5);
316 assert!((s.pitch - 0.5).abs() < 1e-5);
317 }
318
319 #[test]
320 fn test_saccade_towards_partial() {
321 let mut s = new_eye_state(1);
322 saccade_towards(&mut s, 1.0, 0.0, 0.1, 1.0);
323 assert!(s.yaw > 0.0 && s.yaw < 1.0, "should partially approach");
324 }
325
326 #[test]
327 fn test_clamp_gaze() {
328 let mut s = new_eye_state(1);
329 s.yaw = 999.0;
330 s.pitch = -999.0;
331 let c = cfg();
332 clamp_gaze(&mut s, &c);
333 assert!(s.yaw <= c.max_yaw);
334 assert!(s.pitch >= -c.max_pitch);
335 }
336
337 #[test]
338 fn test_trigger_blink() {
339 let mut s = new_eye_state(1);
340 trigger_blink(&mut s, 0.2);
341 assert!(s.blinking);
342 assert!((s.blink_duration - 0.2).abs() < 1e-6);
343 }
344
345 #[test]
346 fn test_blink_factor_initial() {
347 let s = new_eye_state(1);
348 assert_eq!(blink_factor(&s), 0.0);
349 }
350
351 #[test]
352 fn test_auto_blink_tick_starts_blink() {
353 let mut s = new_eye_state(1);
354 let c = EyeControlConfig {
355 blink_interval: 0.1,
356 ..cfg()
357 };
358 auto_blink_tick(&mut s, &c, 0.2);
360 assert!(s.blinking || s.blink_fraction > 0.0 || s.blink_timer != 0.2);
361 }
362
363 #[test]
364 fn test_auto_blink_tick_closure() {
365 let mut s = new_eye_state(1);
366 let c = EyeControlConfig {
367 blink_interval: 0.01,
368 blink_duration: 0.2,
369 ..cfg()
370 };
371 auto_blink_tick(&mut s, &c, 0.05);
373 if s.blinking {
375 auto_blink_tick(&mut s, &c, 0.05);
376 assert!(s.blink_fraction >= 0.0);
377 }
378 }
379
380 #[test]
381 fn test_is_blinking_eye() {
382 let mut s = new_eye_state(1);
383 assert!(!is_blinking_eye(&s));
384 trigger_blink(&mut s, 0.15);
385 assert!(is_blinking_eye(&s));
386 }
387
388 #[test]
389 fn test_gaze_blend_midpoint() {
390 let mut a = new_eye_state(1);
391 let mut b = new_eye_state(2);
392 a.yaw = 0.0;
393 b.yaw = 1.0;
394 let m = gaze_blend(&a, &b, 0.5);
395 assert!((m.yaw - 0.5).abs() < 1e-5);
396 }
397
398 #[test]
399 fn test_gaze_blend_extremes() {
400 let a = new_eye_state(1);
401 let b = new_eye_state(2);
402 let m0 = gaze_blend(&a, &b, 0.0);
403 assert!((m0.yaw - a.yaw).abs() < 1e-6);
404 let m1 = gaze_blend(&a, &b, 1.0);
405 assert!((m1.yaw - b.yaw).abs() < 1e-6);
406 }
407
408 #[test]
409 fn test_gaze_distance_zero() {
410 let s = new_eye_state(1);
411 assert!(gaze_distance(&s, &s) < 1e-6);
412 }
413
414 #[test]
415 fn test_gaze_distance_nonzero() {
416 let mut a = new_eye_state(1);
417 let b = new_eye_state(2);
418 a.yaw = 1.0;
419 assert!(gaze_distance(&a, &b) > 0.5);
420 }
421
422 #[test]
423 fn test_update_eye_gaze_converges() {
424 let mut s = new_eye_state(1);
425 let c = cfg();
426 let target = GazeTarget::Angles {
427 yaw: 0.3,
428 pitch: 0.1,
429 };
430 for _ in 0..200 {
431 update_eye_gaze(&mut s, &target, &c, 0.05);
432 }
433 assert!((s.yaw - 0.3).abs() < 0.01);
434 assert!((s.pitch - 0.1).abs() < 0.01);
435 }
436
437 #[test]
438 fn test_update_eye_gaze_clamped() {
439 let mut s = new_eye_state(1);
440 let c = cfg();
441 let target = GazeTarget::Angles {
442 yaw: 99.0,
443 pitch: 99.0,
444 };
445 for _ in 0..200 {
446 update_eye_gaze(&mut s, &target, &c, 0.1);
447 }
448 assert!(s.yaw <= c.max_yaw + 1e-4);
449 assert!(s.pitch <= c.max_pitch + 1e-4);
450 }
451}