Skip to main content

oxihuman_morph/
brow_tail_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Brow-tail (lateral end) raise/lower and angle control.
6
7/// Side selector for brow tail.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum BrowTailSide {
11    Left,
12    Right,
13    Both,
14}
15
16/// Runtime state for brow-tail positions.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct BrowTailState {
20    /// Tail raise left  (0 = neutral, 1 = max raise, -1 = max lower).
21    pub raise_left: f32,
22    /// Tail raise right (0 = neutral, 1 = max raise, -1 = max lower).
23    pub raise_right: f32,
24    /// Angular tilt of the tail (positive = upward tilt, degrees).
25    pub angle_left: f32,
26    pub angle_right: f32,
27}
28
29/// Configuration limits.
30#[allow(dead_code)]
31#[derive(Clone, Debug)]
32pub struct BrowTailConfig {
33    pub max_raise: f32,
34    pub max_lower: f32,
35    pub max_angle_deg: f32,
36}
37
38impl Default for BrowTailConfig {
39    fn default() -> Self {
40        Self {
41            max_raise: 1.0,
42            max_lower: 1.0,
43            max_angle_deg: 30.0,
44        }
45    }
46}
47
48impl Default for BrowTailState {
49    fn default() -> Self {
50        Self {
51            raise_left: 0.0,
52            raise_right: 0.0,
53            angle_left: 0.0,
54            angle_right: 0.0,
55        }
56    }
57}
58
59#[allow(dead_code)]
60pub fn new_brow_tail_state() -> BrowTailState {
61    BrowTailState::default()
62}
63
64#[allow(dead_code)]
65pub fn default_brow_tail_config() -> BrowTailConfig {
66    BrowTailConfig::default()
67}
68
69#[allow(dead_code)]
70pub fn bt_set_raise(state: &mut BrowTailState, cfg: &BrowTailConfig, side: BrowTailSide, v: f32) {
71    let v = v.clamp(-cfg.max_lower, cfg.max_raise);
72    match side {
73        BrowTailSide::Left => state.raise_left = v,
74        BrowTailSide::Right => state.raise_right = v,
75        BrowTailSide::Both => {
76            state.raise_left = v;
77            state.raise_right = v;
78        }
79    }
80}
81
82#[allow(dead_code)]
83pub fn bt_set_angle(state: &mut BrowTailState, cfg: &BrowTailConfig, side: BrowTailSide, deg: f32) {
84    let d = deg.clamp(-cfg.max_angle_deg, cfg.max_angle_deg);
85    match side {
86        BrowTailSide::Left => state.angle_left = d,
87        BrowTailSide::Right => state.angle_right = d,
88        BrowTailSide::Both => {
89            state.angle_left = d;
90            state.angle_right = d;
91        }
92    }
93}
94
95#[allow(dead_code)]
96pub fn bt_reset(state: &mut BrowTailState) {
97    *state = BrowTailState::default();
98}
99
100#[allow(dead_code)]
101pub fn bt_symmetry(state: &BrowTailState) -> f32 {
102    1.0 - (state.raise_left - state.raise_right).abs().min(1.0)
103}
104
105#[allow(dead_code)]
106pub fn bt_blend(a: &BrowTailState, b: &BrowTailState, t: f32) -> BrowTailState {
107    let t = t.clamp(0.0, 1.0);
108    BrowTailState {
109        raise_left: a.raise_left + (b.raise_left - a.raise_left) * t,
110        raise_right: a.raise_right + (b.raise_right - a.raise_right) * t,
111        angle_left: a.angle_left + (b.angle_left - a.angle_left) * t,
112        angle_right: a.angle_right + (b.angle_right - a.angle_right) * t,
113    }
114}
115
116#[allow(dead_code)]
117pub fn bt_to_morph_weights(state: &BrowTailState) -> [f32; 4] {
118    [
119        state.raise_left,
120        state.raise_right,
121        state.angle_left / 30.0,
122        state.angle_right / 30.0,
123    ]
124}
125
126#[allow(dead_code)]
127pub fn bt_is_neutral(state: &BrowTailState) -> bool {
128    state.raise_left.abs() < 1e-4
129        && state.raise_right.abs() < 1e-4
130        && state.angle_left.abs() < 1e-4
131        && state.angle_right.abs() < 1e-4
132}
133
134#[allow(dead_code)]
135pub fn bt_to_json(state: &BrowTailState) -> String {
136    format!(
137        "{{\"raise_left\":{:.4},\"raise_right\":{:.4},\"angle_left\":{:.4},\"angle_right\":{:.4}}}",
138        state.raise_left, state.raise_right, state.angle_left, state.angle_right
139    )
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn default_is_neutral() {
148        assert!(bt_is_neutral(&new_brow_tail_state()));
149    }
150
151    #[test]
152    fn set_raise_clamps_max() {
153        let mut s = new_brow_tail_state();
154        let cfg = default_brow_tail_config();
155        bt_set_raise(&mut s, &cfg, BrowTailSide::Left, 2.0);
156        assert!(s.raise_left <= cfg.max_raise);
157    }
158
159    #[test]
160    fn set_raise_clamps_min() {
161        let mut s = new_brow_tail_state();
162        let cfg = default_brow_tail_config();
163        bt_set_raise(&mut s, &cfg, BrowTailSide::Right, -5.0);
164        assert!(s.raise_right >= -cfg.max_lower);
165    }
166
167    #[test]
168    fn both_side_sets_both() {
169        let mut s = new_brow_tail_state();
170        let cfg = default_brow_tail_config();
171        bt_set_raise(&mut s, &cfg, BrowTailSide::Both, 0.5);
172        assert!((s.raise_left - 0.5).abs() < 1e-5);
173        assert!((s.raise_right - 0.5).abs() < 1e-5);
174    }
175
176    #[test]
177    fn reset_clears_state() {
178        let mut s = new_brow_tail_state();
179        let cfg = default_brow_tail_config();
180        bt_set_raise(&mut s, &cfg, BrowTailSide::Both, 0.8);
181        bt_reset(&mut s);
182        assert!(bt_is_neutral(&s));
183    }
184
185    #[test]
186    fn blend_midpoint() {
187        let mut a = new_brow_tail_state();
188        let mut b = new_brow_tail_state();
189        let cfg = default_brow_tail_config();
190        bt_set_raise(&mut a, &cfg, BrowTailSide::Left, 0.0);
191        bt_set_raise(&mut b, &cfg, BrowTailSide::Left, 1.0);
192        let mid = bt_blend(&a, &b, 0.5);
193        assert!((mid.raise_left - 0.5).abs() < 1e-4);
194    }
195
196    #[test]
197    fn symmetry_one_when_equal() {
198        let s = new_brow_tail_state();
199        assert!((bt_symmetry(&s) - 1.0).abs() < 1e-5);
200    }
201
202    #[test]
203    fn morph_weights_len() {
204        let s = new_brow_tail_state();
205        assert_eq!(bt_to_morph_weights(&s).len(), 4);
206    }
207
208    #[test]
209    fn angle_clamp() {
210        let mut s = new_brow_tail_state();
211        let cfg = default_brow_tail_config();
212        bt_set_angle(&mut s, &cfg, BrowTailSide::Left, 999.0);
213        assert!(s.angle_left <= cfg.max_angle_deg);
214    }
215
216    #[test]
217    fn json_output_not_empty() {
218        let s = new_brow_tail_state();
219        assert!(!bt_to_json(&s).is_empty());
220    }
221}