oxihuman_morph/
cheek_nasal_fold.rs1#![allow(dead_code)]
4
5use std::f32::consts::PI;
8
9#[allow(dead_code)]
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum FoldSide {
12 Left,
13 Right,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct CheekNasalFoldConfig {
19 pub max_depth: f32,
20}
21
22impl Default for CheekNasalFoldConfig {
23 fn default() -> Self {
24 Self { max_depth: 1.0 }
25 }
26}
27
28#[allow(dead_code)]
29#[derive(Debug, Clone)]
30pub struct CheekNasalFoldState {
31 pub left: f32,
32 pub right: f32,
33 pub config: CheekNasalFoldConfig,
34}
35
36#[allow(dead_code)]
37pub fn default_cheek_nasal_fold_config() -> CheekNasalFoldConfig {
38 CheekNasalFoldConfig::default()
39}
40
41#[allow(dead_code)]
42pub fn new_cheek_nasal_fold_state(config: CheekNasalFoldConfig) -> CheekNasalFoldState {
43 CheekNasalFoldState {
44 left: 0.0,
45 right: 0.0,
46 config,
47 }
48}
49
50#[allow(dead_code)]
51pub fn cnf_set(state: &mut CheekNasalFoldState, side: FoldSide, v: f32) {
52 let v = v.clamp(0.0, state.config.max_depth);
53 match side {
54 FoldSide::Left => state.left = v,
55 FoldSide::Right => state.right = v,
56 }
57}
58
59#[allow(dead_code)]
60pub fn cnf_set_both(state: &mut CheekNasalFoldState, v: f32) {
61 let v = v.clamp(0.0, state.config.max_depth);
62 state.left = v;
63 state.right = v;
64}
65
66#[allow(dead_code)]
67pub fn cnf_reset(state: &mut CheekNasalFoldState) {
68 state.left = 0.0;
69 state.right = 0.0;
70}
71
72#[allow(dead_code)]
73pub fn cnf_is_neutral(state: &CheekNasalFoldState) -> bool {
74 state.left.abs() < 1e-6 && state.right.abs() < 1e-6
75}
76
77#[allow(dead_code)]
78pub fn cnf_average(state: &CheekNasalFoldState) -> f32 {
79 (state.left + state.right) * 0.5
80}
81
82#[allow(dead_code)]
83pub fn cnf_asymmetry(state: &CheekNasalFoldState) -> f32 {
84 (state.left - state.right).abs()
85}
86
87#[allow(dead_code)]
88pub fn cnf_fold_angle_rad(state: &CheekNasalFoldState) -> f32 {
89 cnf_average(state) * PI * 0.3
90}
91
92#[allow(dead_code)]
93pub fn cnf_to_weights(state: &CheekNasalFoldState) -> [f32; 2] {
94 let m = state.config.max_depth;
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 cnf_blend(a: &CheekNasalFoldState, b: &CheekNasalFoldState, 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 cnf_to_json(state: &CheekNasalFoldState) -> 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
120 #[test]
121 fn default_neutral() {
122 assert!(cnf_is_neutral(&new_cheek_nasal_fold_state(
123 default_cheek_nasal_fold_config()
124 )));
125 }
126 #[test]
127 fn set_clamps() {
128 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
129 cnf_set(&mut s, FoldSide::Left, 9.0);
130 assert!((0.0..=1.0).contains(&s.left));
131 }
132 #[test]
133 fn set_both_applies() {
134 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
135 cnf_set_both(&mut s, 0.5);
136 assert!((s.left - 0.5).abs() < 1e-5 && (s.right - 0.5).abs() < 1e-5);
137 }
138 #[test]
139 fn reset_zeroes() {
140 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
141 cnf_set_both(&mut s, 0.4);
142 cnf_reset(&mut s);
143 assert!(cnf_is_neutral(&s));
144 }
145 #[test]
146 fn average_mid() {
147 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
148 cnf_set(&mut s, FoldSide::Left, 0.2);
149 cnf_set(&mut s, FoldSide::Right, 0.6);
150 assert!((cnf_average(&s) - 0.4).abs() < 1e-5);
151 }
152 #[test]
153 fn asymmetry_abs_diff() {
154 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
155 cnf_set(&mut s, FoldSide::Left, 0.1);
156 cnf_set(&mut s, FoldSide::Right, 0.5);
157 assert!((cnf_asymmetry(&s) - 0.4).abs() < 1e-5);
158 }
159 #[test]
160 fn fold_angle_nonneg() {
161 let s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
162 assert!(cnf_fold_angle_rad(&s) >= 0.0);
163 }
164 #[test]
165 fn to_weights_one_at_max() {
166 let mut s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
167 cnf_set(&mut s, FoldSide::Right, 1.0);
168 assert!((cnf_to_weights(&s)[1] - 1.0).abs() < 1e-5);
169 }
170 #[test]
171 fn blend_midpoint() {
172 let mut a = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
173 let b = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
174 cnf_set(&mut a, FoldSide::Left, 0.8);
175 let w = cnf_blend(&a, &b, 0.5);
176 assert!((w[0] - 0.4).abs() < 1e-5);
177 }
178 #[test]
179 fn to_json_has_right() {
180 let s = new_cheek_nasal_fold_state(default_cheek_nasal_fold_config());
181 assert!(cnf_to_json(&s).contains("\"right\""));
182 }
183}