Skip to main content

oxihuman_morph/
eye_fissure_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Eye fissure (palpebral aperture) height control per eye.
6
7use std::f32::consts::FRAC_PI_6;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum EyeSide {
12    Left,
13    Right,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct EyeFissureConfig {
19    pub max_opening: f32,
20}
21
22impl Default for EyeFissureConfig {
23    fn default() -> Self {
24        Self { max_opening: 1.0 }
25    }
26}
27
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct EyeFissureState {
31    pub left: f32,
32    pub right: f32,
33    pub config: EyeFissureConfig,
34}
35
36#[allow(dead_code)]
37pub fn default_eye_fissure_config() -> EyeFissureConfig {
38    EyeFissureConfig::default()
39}
40
41#[allow(dead_code)]
42pub fn new_eye_fissure_state(config: EyeFissureConfig) -> EyeFissureState {
43    EyeFissureState {
44        left: 0.0,
45        right: 0.0,
46        config,
47    }
48}
49
50#[allow(dead_code)]
51pub fn ef_set(state: &mut EyeFissureState, side: EyeSide, v: f32) {
52    let v = v.clamp(0.0, state.config.max_opening);
53    match side {
54        EyeSide::Left => state.left = v,
55        EyeSide::Right => state.right = v,
56    }
57}
58
59#[allow(dead_code)]
60pub fn ef_set_both(state: &mut EyeFissureState, v: f32) {
61    let v = v.clamp(0.0, state.config.max_opening);
62    state.left = v;
63    state.right = v;
64}
65
66#[allow(dead_code)]
67pub fn ef_reset(state: &mut EyeFissureState) {
68    state.left = 0.0;
69    state.right = 0.0;
70}
71
72#[allow(dead_code)]
73pub fn ef_is_neutral(state: &EyeFissureState) -> bool {
74    state.left.abs() < 1e-6 && state.right.abs() < 1e-6
75}
76
77#[allow(dead_code)]
78pub fn ef_average(state: &EyeFissureState) -> f32 {
79    (state.left + state.right) * 0.5
80}
81
82#[allow(dead_code)]
83pub fn ef_asymmetry(state: &EyeFissureState) -> f32 {
84    (state.left - state.right).abs()
85}
86
87#[allow(dead_code)]
88pub fn ef_opening_angle_rad(state: &EyeFissureState) -> f32 {
89    ef_average(state) * FRAC_PI_6
90}
91
92#[allow(dead_code)]
93pub fn ef_to_weights(state: &EyeFissureState) -> [f32; 2] {
94    let m = state.config.max_opening;
95    let n = |v: f32| if m > 1e-9 { v / m } else { 0.0 };
96    [n(state.left), n(state.right)]
97}
98
99#[allow(dead_code)]
100pub fn ef_blend(a: &EyeFissureState, b: &EyeFissureState, t: f32) -> [f32; 2] {
101    let t = t.clamp(0.0, 1.0);
102    [
103        a.left * (1.0 - t) + b.left * t,
104        a.right * (1.0 - t) + b.right * t,
105    ]
106}
107
108#[allow(dead_code)]
109pub fn ef_to_json(state: &EyeFissureState) -> String {
110    format!(
111        "{{\"left\":{:.4},\"right\":{:.4}}}",
112        state.left, state.right
113    )
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    #[test]
120    fn default_neutral() {
121        assert!(ef_is_neutral(&new_eye_fissure_state(
122            default_eye_fissure_config()
123        )));
124    }
125    #[test]
126    fn set_clamps() {
127        let mut s = new_eye_fissure_state(default_eye_fissure_config());
128        ef_set(&mut s, EyeSide::Left, 10.0);
129        assert!((0.0..=1.0).contains(&s.left));
130    }
131    #[test]
132    fn set_both_mirrors() {
133        let mut s = new_eye_fissure_state(default_eye_fissure_config());
134        ef_set_both(&mut s, 0.5);
135        assert!((s.right - 0.5).abs() < 1e-5);
136    }
137    #[test]
138    fn reset_zeroes() {
139        let mut s = new_eye_fissure_state(default_eye_fissure_config());
140        ef_set_both(&mut s, 0.9);
141        ef_reset(&mut s);
142        assert!(ef_is_neutral(&s));
143    }
144    #[test]
145    fn average_mid() {
146        let mut s = new_eye_fissure_state(default_eye_fissure_config());
147        ef_set(&mut s, EyeSide::Left, 0.2);
148        ef_set(&mut s, EyeSide::Right, 0.8);
149        assert!((ef_average(&s) - 0.5).abs() < 1e-5);
150    }
151    #[test]
152    fn asymmetry_abs() {
153        let mut s = new_eye_fissure_state(default_eye_fissure_config());
154        ef_set(&mut s, EyeSide::Left, 0.3);
155        ef_set(&mut s, EyeSide::Right, 0.7);
156        assert!((ef_asymmetry(&s) - 0.4).abs() < 1e-5);
157    }
158    #[test]
159    fn angle_nonneg() {
160        let s = new_eye_fissure_state(default_eye_fissure_config());
161        assert!(ef_opening_angle_rad(&s) >= 0.0);
162    }
163    #[test]
164    fn to_weights_at_max() {
165        let mut s = new_eye_fissure_state(default_eye_fissure_config());
166        ef_set(&mut s, EyeSide::Left, 1.0);
167        assert!((ef_to_weights(&s)[0] - 1.0).abs() < 1e-5);
168    }
169    #[test]
170    fn blend_at_zero_is_a() {
171        let mut a = new_eye_fissure_state(default_eye_fissure_config());
172        let b = new_eye_fissure_state(default_eye_fissure_config());
173        ef_set(&mut a, EyeSide::Left, 0.7);
174        let w = ef_blend(&a, &b, 0.0);
175        assert!((w[0] - 0.7).abs() < 1e-5);
176    }
177    #[test]
178    fn to_json_has_left() {
179        assert!(
180            ef_to_json(&new_eye_fissure_state(default_eye_fissure_config())).contains("\"left\"")
181        );
182    }
183}