oxihuman_morph/
nasal_root_control.rs1#![allow(dead_code)]
4
5#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct NasalRootConfig {
11 pub max_depth: f32,
12}
13
14#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct NasalRootState {
18 pub depth: f32,
19 pub width: f32,
20 pub height: f32,
21 pub squish: f32,
22}
23
24#[allow(dead_code)]
25pub fn default_nasal_root_config() -> NasalRootConfig {
26 NasalRootConfig { max_depth: 1.0 }
27}
28
29#[allow(dead_code)]
30pub fn new_nasal_root_state() -> NasalRootState {
31 NasalRootState {
32 depth: 0.0,
33 width: 0.0,
34 height: 0.0,
35 squish: 0.0,
36 }
37}
38
39#[allow(dead_code)]
40pub fn nr_set_depth(state: &mut NasalRootState, cfg: &NasalRootConfig, v: f32) {
41 state.depth = v.clamp(0.0, cfg.max_depth);
42}
43
44#[allow(dead_code)]
45pub fn nr_set_width(state: &mut NasalRootState, cfg: &NasalRootConfig, v: f32) {
46 state.width = v.clamp(0.0, cfg.max_depth);
47}
48
49#[allow(dead_code)]
50pub fn nr_set_height(state: &mut NasalRootState, v: f32) {
51 state.height = v.clamp(-1.0, 1.0);
52}
53
54#[allow(dead_code)]
55pub fn nr_set_squish(state: &mut NasalRootState, v: f32) {
56 state.squish = v.clamp(0.0, 1.0);
57}
58
59#[allow(dead_code)]
60pub fn nr_reset(state: &mut NasalRootState) {
61 *state = new_nasal_root_state();
62}
63
64#[allow(dead_code)]
65pub fn nr_is_neutral(state: &NasalRootState) -> bool {
66 state.depth.abs() < 1e-6
67 && state.width.abs() < 1e-6
68 && state.height.abs() < 1e-6
69 && state.squish.abs() < 1e-6
70}
71
72#[allow(dead_code)]
73pub fn nr_bridge_prominence(state: &NasalRootState) -> f32 {
74 (state.depth + state.height.max(0.0)) * 0.5
75}
76
77#[allow(dead_code)]
78pub fn nr_blend(a: &NasalRootState, b: &NasalRootState, t: f32) -> NasalRootState {
79 let t = t.clamp(0.0, 1.0);
80 NasalRootState {
81 depth: a.depth + (b.depth - a.depth) * t,
82 width: a.width + (b.width - a.width) * t,
83 height: a.height + (b.height - a.height) * t,
84 squish: a.squish + (b.squish - a.squish) * t,
85 }
86}
87
88#[allow(dead_code)]
89pub fn nr_to_weights(state: &NasalRootState) -> Vec<(String, f32)> {
90 vec![
91 ("nasal_root_depth".to_string(), state.depth),
92 ("nasal_root_width".to_string(), state.width),
93 ("nasal_root_height".to_string(), state.height),
94 ("nasal_root_squish".to_string(), state.squish),
95 ]
96}
97
98#[allow(dead_code)]
99pub fn nr_to_json(state: &NasalRootState) -> String {
100 format!(
101 r#"{{"depth":{:.4},"width":{:.4},"height":{:.4},"squish":{:.4}}}"#,
102 state.depth, state.width, state.height, state.squish
103 )
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn default_config() {
112 let cfg = default_nasal_root_config();
113 assert!((cfg.max_depth - 1.0).abs() < 1e-6);
114 }
115
116 #[test]
117 fn new_state_neutral() {
118 let s = new_nasal_root_state();
119 assert!(nr_is_neutral(&s));
120 }
121
122 #[test]
123 fn set_depth_clamps() {
124 let cfg = default_nasal_root_config();
125 let mut s = new_nasal_root_state();
126 nr_set_depth(&mut s, &cfg, 5.0);
127 assert!((s.depth - 1.0).abs() < 1e-6);
128 }
129
130 #[test]
131 fn set_height_signed() {
132 let mut s = new_nasal_root_state();
133 nr_set_height(&mut s, -0.5);
134 assert!((s.height + 0.5).abs() < 1e-6);
135 }
136
137 #[test]
138 fn set_squish_clamps() {
139 let mut s = new_nasal_root_state();
140 nr_set_squish(&mut s, 5.0);
141 assert!((s.squish - 1.0).abs() < 1e-6);
142 }
143
144 #[test]
145 fn bridge_prominence_zero_at_neutral() {
146 let s = new_nasal_root_state();
147 assert!(nr_bridge_prominence(&s) < 1e-6);
148 }
149
150 #[test]
151 fn reset_clears() {
152 let cfg = default_nasal_root_config();
153 let mut s = new_nasal_root_state();
154 nr_set_depth(&mut s, &cfg, 0.5);
155 nr_reset(&mut s);
156 assert!(nr_is_neutral(&s));
157 }
158
159 #[test]
160 fn blend_midpoint() {
161 let a = new_nasal_root_state();
162 let cfg = default_nasal_root_config();
163 let mut b = new_nasal_root_state();
164 nr_set_depth(&mut b, &cfg, 1.0);
165 let mid = nr_blend(&a, &b, 0.5);
166 assert!((mid.depth - 0.5).abs() < 1e-6);
167 }
168
169 #[test]
170 fn to_weights_count() {
171 let s = new_nasal_root_state();
172 assert_eq!(nr_to_weights(&s).len(), 4);
173 }
174
175 #[test]
176 fn to_json_fields() {
177 let s = new_nasal_root_state();
178 let j = nr_to_json(&s);
179 assert!(j.contains("depth"));
180 assert!(j.contains("squish"));
181 }
182}