oxihuman_morph/
brow_arch_height.rs1#![allow(dead_code)]
4
5use 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}