Skip to main content

oxihuman_morph/
eye_outer_corner.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Eye outer corner control — lateral canthus position and angle.
6
7/// Configuration for eye outer corner.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct EyeOuterCornerConfig {
11    pub max_depth: f32,
12    pub max_tilt_rad: f32,
13}
14
15/// Side selector.
16#[allow(dead_code)]
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EyeOuterSide {
19    Left,
20    Right,
21}
22
23/// Runtime state.
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct EyeOuterCornerState {
27    pub left_depth: f32,
28    pub right_depth: f32,
29    pub left_tilt_rad: f32,
30    pub right_tilt_rad: f32,
31}
32
33#[allow(dead_code)]
34pub fn default_eye_outer_corner_config() -> EyeOuterCornerConfig {
35    use std::f32::consts::FRAC_PI_6;
36    EyeOuterCornerConfig {
37        max_depth: 1.0,
38        max_tilt_rad: FRAC_PI_6,
39    }
40}
41
42#[allow(dead_code)]
43pub fn new_eye_outer_corner_state() -> EyeOuterCornerState {
44    EyeOuterCornerState {
45        left_depth: 0.0,
46        right_depth: 0.0,
47        left_tilt_rad: 0.0,
48        right_tilt_rad: 0.0,
49    }
50}
51
52#[allow(dead_code)]
53pub fn eoc_set_depth(
54    state: &mut EyeOuterCornerState,
55    cfg: &EyeOuterCornerConfig,
56    side: EyeOuterSide,
57    v: f32,
58) {
59    let clamped = v.clamp(-cfg.max_depth, cfg.max_depth);
60    match side {
61        EyeOuterSide::Left => state.left_depth = clamped,
62        EyeOuterSide::Right => state.right_depth = clamped,
63    }
64}
65
66#[allow(dead_code)]
67pub fn eoc_set_tilt(
68    state: &mut EyeOuterCornerState,
69    cfg: &EyeOuterCornerConfig,
70    side: EyeOuterSide,
71    v: f32,
72) {
73    let clamped = v.clamp(-cfg.max_tilt_rad, cfg.max_tilt_rad);
74    match side {
75        EyeOuterSide::Left => state.left_tilt_rad = clamped,
76        EyeOuterSide::Right => state.right_tilt_rad = clamped,
77    }
78}
79
80#[allow(dead_code)]
81pub fn eoc_set_both_depth(state: &mut EyeOuterCornerState, cfg: &EyeOuterCornerConfig, v: f32) {
82    let clamped = v.clamp(-cfg.max_depth, cfg.max_depth);
83    state.left_depth = clamped;
84    state.right_depth = clamped;
85}
86
87#[allow(dead_code)]
88pub fn eoc_reset(state: &mut EyeOuterCornerState) {
89    *state = new_eye_outer_corner_state();
90}
91
92#[allow(dead_code)]
93pub fn eoc_is_neutral(state: &EyeOuterCornerState) -> bool {
94    let vals = [
95        state.left_depth,
96        state.right_depth,
97        state.left_tilt_rad,
98        state.right_tilt_rad,
99    ];
100    vals.iter().all(|v| v.abs() < 1e-6)
101}
102
103#[allow(dead_code)]
104pub fn eoc_average_depth(state: &EyeOuterCornerState) -> f32 {
105    (state.left_depth + state.right_depth) * 0.5
106}
107
108#[allow(dead_code)]
109pub fn eoc_symmetry(state: &EyeOuterCornerState) -> f32 {
110    (state.left_depth - state.right_depth).abs()
111}
112
113#[allow(dead_code)]
114pub fn eoc_blend(a: &EyeOuterCornerState, b: &EyeOuterCornerState, t: f32) -> EyeOuterCornerState {
115    let t = t.clamp(0.0, 1.0);
116    EyeOuterCornerState {
117        left_depth: a.left_depth + (b.left_depth - a.left_depth) * t,
118        right_depth: a.right_depth + (b.right_depth - a.right_depth) * t,
119        left_tilt_rad: a.left_tilt_rad + (b.left_tilt_rad - a.left_tilt_rad) * t,
120        right_tilt_rad: a.right_tilt_rad + (b.right_tilt_rad - a.right_tilt_rad) * t,
121    }
122}
123
124#[allow(dead_code)]
125pub fn eoc_to_weights(state: &EyeOuterCornerState) -> Vec<(String, f32)> {
126    use std::f32::consts::FRAC_PI_6;
127    let norm = 1.0 / FRAC_PI_6;
128    vec![
129        ("eye_outer_depth_l".to_string(), state.left_depth),
130        ("eye_outer_depth_r".to_string(), state.right_depth),
131        ("eye_outer_tilt_l".to_string(), state.left_tilt_rad * norm),
132        ("eye_outer_tilt_r".to_string(), state.right_tilt_rad * norm),
133    ]
134}
135
136#[allow(dead_code)]
137pub fn eoc_to_json(state: &EyeOuterCornerState) -> String {
138    format!(
139        r#"{{"left_depth":{:.4},"right_depth":{:.4},"left_tilt_rad":{:.4},"right_tilt_rad":{:.4}}}"#,
140        state.left_depth, state.right_depth, state.left_tilt_rad, state.right_tilt_rad
141    )
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn default_config() {
150        let cfg = default_eye_outer_corner_config();
151        assert!((cfg.max_depth - 1.0).abs() < 1e-6);
152    }
153
154    #[test]
155    fn new_state_neutral() {
156        let s = new_eye_outer_corner_state();
157        assert!(eoc_is_neutral(&s));
158    }
159
160    #[test]
161    fn set_depth_left() {
162        let cfg = default_eye_outer_corner_config();
163        let mut s = new_eye_outer_corner_state();
164        eoc_set_depth(&mut s, &cfg, EyeOuterSide::Left, 0.6);
165        assert!((s.left_depth - 0.6).abs() < 1e-6);
166    }
167
168    #[test]
169    fn set_depth_clamps() {
170        let cfg = default_eye_outer_corner_config();
171        let mut s = new_eye_outer_corner_state();
172        eoc_set_depth(&mut s, &cfg, EyeOuterSide::Right, 5.0);
173        assert!((s.right_depth - 1.0).abs() < 1e-6);
174    }
175
176    #[test]
177    fn set_both_depth() {
178        let cfg = default_eye_outer_corner_config();
179        let mut s = new_eye_outer_corner_state();
180        eoc_set_both_depth(&mut s, &cfg, 0.4);
181        assert!(eoc_symmetry(&s) < 1e-6);
182    }
183
184    #[test]
185    fn set_tilt_clamps() {
186        let cfg = default_eye_outer_corner_config();
187        let mut s = new_eye_outer_corner_state();
188        eoc_set_tilt(&mut s, &cfg, EyeOuterSide::Left, 10.0);
189        assert!((s.left_tilt_rad - cfg.max_tilt_rad).abs() < 1e-6);
190    }
191
192    #[test]
193    fn reset_clears() {
194        let cfg = default_eye_outer_corner_config();
195        let mut s = new_eye_outer_corner_state();
196        eoc_set_both_depth(&mut s, &cfg, 0.8);
197        eoc_reset(&mut s);
198        assert!(eoc_is_neutral(&s));
199    }
200
201    #[test]
202    fn blend_midpoint() {
203        let a = new_eye_outer_corner_state();
204        let cfg = default_eye_outer_corner_config();
205        let mut b = new_eye_outer_corner_state();
206        eoc_set_both_depth(&mut b, &cfg, 1.0);
207        let m = eoc_blend(&a, &b, 0.5);
208        assert!((m.left_depth - 0.5).abs() < 1e-6);
209    }
210
211    #[test]
212    fn to_weights_count() {
213        let s = new_eye_outer_corner_state();
214        assert_eq!(eoc_to_weights(&s).len(), 4);
215    }
216
217    #[test]
218    fn to_json_fields() {
219        let s = new_eye_outer_corner_state();
220        let j = eoc_to_json(&s);
221        assert!(j.contains("left_depth"));
222    }
223}