Skip to main content

oxihuman_morph/
brow_arch_height.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Brow arch height — controls the vertical peak height of each brow arch.
6
7use std::f32::consts::PI;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum BrowSide {
12    Left,
13    Right,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct BrowArchHeightConfig {
19    pub max_height: f32,
20}
21
22impl Default for BrowArchHeightConfig {
23    fn default() -> Self {
24        Self { max_height: 1.0 }
25    }
26}
27
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct BrowArchHeightState {
31    pub left: f32,
32    pub right: f32,
33    pub config: BrowArchHeightConfig,
34}
35
36#[allow(dead_code)]
37pub fn default_brow_arch_height_config() -> BrowArchHeightConfig {
38    BrowArchHeightConfig::default()
39}
40
41#[allow(dead_code)]
42pub fn new_brow_arch_height_state(config: BrowArchHeightConfig) -> BrowArchHeightState {
43    BrowArchHeightState {
44        left: 0.0,
45        right: 0.0,
46        config,
47    }
48}
49
50#[allow(dead_code)]
51pub fn bah_set(state: &mut BrowArchHeightState, side: BrowSide, v: f32) {
52    let v = v.clamp(0.0, state.config.max_height);
53    match side {
54        BrowSide::Left => state.left = v,
55        BrowSide::Right => state.right = v,
56    }
57}
58
59#[allow(dead_code)]
60pub fn bah_set_both(state: &mut BrowArchHeightState, v: f32) {
61    let v = v.clamp(0.0, state.config.max_height);
62    state.left = v;
63    state.right = v;
64}
65
66#[allow(dead_code)]
67pub fn bah_reset(state: &mut BrowArchHeightState) {
68    state.left = 0.0;
69    state.right = 0.0;
70}
71
72#[allow(dead_code)]
73pub fn bah_is_neutral(state: &BrowArchHeightState) -> bool {
74    state.left.abs() < 1e-6 && state.right.abs() < 1e-6
75}
76
77#[allow(dead_code)]
78pub fn bah_average(state: &BrowArchHeightState) -> f32 {
79    (state.left + state.right) * 0.5
80}
81
82#[allow(dead_code)]
83pub fn bah_asymmetry(state: &BrowArchHeightState) -> f32 {
84    (state.left - state.right).abs()
85}
86
87#[allow(dead_code)]
88pub fn bah_arch_angle_rad(state: &BrowArchHeightState, side: BrowSide) -> f32 {
89    let h = match side {
90        BrowSide::Left => state.left,
91        BrowSide::Right => state.right,
92    };
93    (h * PI * 0.25).clamp(0.0, PI * 0.5)
94}
95
96#[allow(dead_code)]
97pub fn bah_to_weights(state: &BrowArchHeightState) -> [f32; 2] {
98    let max = state.config.max_height;
99    let norm = |v: f32| if max > 1e-9 { v / max } else { 0.0 };
100    [norm(state.left), norm(state.right)]
101}
102
103#[allow(dead_code)]
104pub fn bah_blend(a: &BrowArchHeightState, b: &BrowArchHeightState, t: f32) -> [f32; 2] {
105    let t = t.clamp(0.0, 1.0);
106    [
107        a.left * (1.0 - t) + b.left * t,
108        a.right * (1.0 - t) + b.right * t,
109    ]
110}
111
112#[allow(dead_code)]
113pub fn bah_to_json(state: &BrowArchHeightState) -> String {
114    format!(
115        "{{\"left\":{:.4},\"right\":{:.4}}}",
116        state.left, state.right
117    )
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn default_is_neutral() {
126        let s = new_brow_arch_height_state(default_brow_arch_height_config());
127        assert!(bah_is_neutral(&s));
128    }
129
130    #[test]
131    fn set_clamps_to_max() {
132        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
133        bah_set(&mut s, BrowSide::Left, 5.0);
134        assert!((0.0..=1.0).contains(&s.left));
135    }
136
137    #[test]
138    fn set_both_applies_to_both_sides() {
139        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
140        bah_set_both(&mut s, 0.7);
141        assert!((s.left - 0.7).abs() < 1e-5);
142        assert!((s.right - 0.7).abs() < 1e-5);
143    }
144
145    #[test]
146    fn reset_zeroes_values() {
147        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
148        bah_set_both(&mut s, 0.5);
149        bah_reset(&mut s);
150        assert!(bah_is_neutral(&s));
151    }
152
153    #[test]
154    fn average_midpoint() {
155        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
156        bah_set(&mut s, BrowSide::Left, 0.4);
157        bah_set(&mut s, BrowSide::Right, 0.6);
158        assert!((bah_average(&s) - 0.5).abs() < 1e-5);
159    }
160
161    #[test]
162    fn asymmetry_is_abs_diff() {
163        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
164        bah_set(&mut s, BrowSide::Left, 0.3);
165        bah_set(&mut s, BrowSide::Right, 0.7);
166        assert!((bah_asymmetry(&s) - 0.4).abs() < 1e-5);
167    }
168
169    #[test]
170    fn arch_angle_nonneg() {
171        let s = new_brow_arch_height_state(default_brow_arch_height_config());
172        assert!(bah_arch_angle_rad(&s, BrowSide::Left) >= 0.0);
173    }
174
175    #[test]
176    fn to_weights_max_gives_one() {
177        let mut s = new_brow_arch_height_state(default_brow_arch_height_config());
178        bah_set(&mut s, BrowSide::Left, 1.0);
179        let w = bah_to_weights(&s);
180        assert!((w[0] - 1.0).abs() < 1e-5);
181    }
182
183    #[test]
184    fn blend_at_zero_is_a() {
185        let mut a = new_brow_arch_height_state(default_brow_arch_height_config());
186        let b = new_brow_arch_height_state(default_brow_arch_height_config());
187        bah_set(&mut a, BrowSide::Left, 0.6);
188        let w = bah_blend(&a, &b, 0.0);
189        assert!((w[0] - 0.6).abs() < 1e-5);
190    }
191
192    #[test]
193    fn to_json_contains_left() {
194        let s = new_brow_arch_height_state(default_brow_arch_height_config());
195        assert!(bah_to_json(&s).contains("\"left\""));
196    }
197}