1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_1_SQRT_2;
8
9#[derive(Debug, Clone, PartialEq)]
11#[allow(dead_code)]
12pub struct NeckTendonConfig {
13 pub definition_min: f32,
14 pub definition_max: f32,
15}
16
17impl Default for NeckTendonConfig {
18 fn default() -> Self {
19 Self {
20 definition_min: 0.0,
21 definition_max: 1.0,
22 }
23 }
24}
25
26#[derive(Debug, Clone, PartialEq, Default)]
28#[allow(dead_code)]
29pub struct NeckTendonState {
30 pub scm_left: f32,
31 pub scm_right: f32,
32 pub platysma: f32,
33 pub atlas_protrusion: f32,
34}
35
36#[derive(Debug, Clone, PartialEq, Default)]
38#[allow(dead_code)]
39pub struct NeckTendonWeights {
40 pub scm_definition_l: f32,
41 pub scm_definition_r: f32,
42 pub platysma_weight: f32,
43 pub atlas_weight: f32,
44}
45
46#[allow(dead_code)]
48pub fn default_neck_tendon_config() -> NeckTendonConfig {
49 NeckTendonConfig::default()
50}
51
52#[allow(dead_code)]
54pub fn new_neck_tendon_state() -> NeckTendonState {
55 NeckTendonState::default()
56}
57
58#[allow(dead_code)]
60pub fn nt_set_scm_left(s: &mut NeckTendonState, cfg: &NeckTendonConfig, v: f32) {
61 s.scm_left = v.clamp(cfg.definition_min, cfg.definition_max);
62}
63
64#[allow(dead_code)]
66pub fn nt_set_scm_right(s: &mut NeckTendonState, cfg: &NeckTendonConfig, v: f32) {
67 s.scm_right = v.clamp(cfg.definition_min, cfg.definition_max);
68}
69
70#[allow(dead_code)]
72pub fn nt_set_scm_both(s: &mut NeckTendonState, cfg: &NeckTendonConfig, v: f32) {
73 let v = v.clamp(cfg.definition_min, cfg.definition_max);
74 s.scm_left = v;
75 s.scm_right = v;
76}
77
78#[allow(dead_code)]
80pub fn nt_set_platysma(s: &mut NeckTendonState, v: f32) {
81 s.platysma = v.clamp(0.0, 1.0);
82}
83
84#[allow(dead_code)]
86pub fn nt_set_atlas(s: &mut NeckTendonState, v: f32) {
87 s.atlas_protrusion = v.clamp(0.0, 1.0);
88}
89
90#[allow(dead_code)]
92pub fn nt_reset(s: &mut NeckTendonState) {
93 *s = NeckTendonState::default();
94}
95
96#[allow(dead_code)]
98pub fn nt_blend(a: &NeckTendonState, b: &NeckTendonState, t: f32) -> NeckTendonState {
99 let t = t.clamp(0.0, 1.0);
100 NeckTendonState {
101 scm_left: a.scm_left + (b.scm_left - a.scm_left) * t,
102 scm_right: a.scm_right + (b.scm_right - a.scm_right) * t,
103 platysma: a.platysma + (b.platysma - a.platysma) * t,
104 atlas_protrusion: a.atlas_protrusion + (b.atlas_protrusion - a.atlas_protrusion) * t,
105 }
106}
107
108#[allow(dead_code)]
110pub fn nt_to_weights(s: &NeckTendonState) -> NeckTendonWeights {
111 NeckTendonWeights {
112 scm_definition_l: s.scm_left,
113 scm_definition_r: s.scm_right,
114 platysma_weight: s.platysma,
115 atlas_weight: s.atlas_protrusion,
116 }
117}
118
119#[allow(dead_code)]
121pub fn nt_asymmetry(s: &NeckTendonState) -> f32 {
122 ((s.scm_left - s.scm_right).abs() * FRAC_1_SQRT_2).min(1.0)
123}
124
125#[allow(dead_code)]
127pub fn nt_to_json(s: &NeckTendonState) -> String {
128 format!(
129 r#"{{"scm_left":{:.4},"scm_right":{:.4},"platysma":{:.4},"atlas":{:.4}}}"#,
130 s.scm_left, s.scm_right, s.platysma, s.atlas_protrusion
131 )
132}
133
134#[allow(dead_code)]
136pub fn nt_is_neutral(s: &NeckTendonState) -> bool {
137 [s.scm_left, s.scm_right, s.platysma, s.atlas_protrusion]
138 .iter()
139 .all(|v| v.abs() < 1e-6)
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn default_is_neutral() {
148 assert!(nt_is_neutral(&new_neck_tendon_state()));
149 }
150
151 #[test]
152 fn set_scm_left_clamped() {
153 let cfg = default_neck_tendon_config();
154 let mut s = new_neck_tendon_state();
155 nt_set_scm_left(&mut s, &cfg, 2.0);
156 assert!((s.scm_left - 1.0).abs() < 1e-6);
157 }
158
159 #[test]
160 fn set_scm_both() {
161 let cfg = default_neck_tendon_config();
162 let mut s = new_neck_tendon_state();
163 nt_set_scm_both(&mut s, &cfg, 0.6);
164 assert!((s.scm_left - 0.6).abs() < 1e-6);
165 assert!((s.scm_right - 0.6).abs() < 1e-6);
166 }
167
168 #[test]
169 fn platysma_clamped() {
170 let mut s = new_neck_tendon_state();
171 nt_set_platysma(&mut s, 1.5);
172 assert!((s.platysma - 1.0).abs() < 1e-6);
173 }
174
175 #[test]
176 fn reset_works() {
177 let cfg = default_neck_tendon_config();
178 let mut s = new_neck_tendon_state();
179 nt_set_scm_left(&mut s, &cfg, 0.8);
180 nt_reset(&mut s);
181 assert!(nt_is_neutral(&s));
182 }
183
184 #[test]
185 fn blend_midpoint() {
186 let a = NeckTendonState {
187 scm_left: 0.0,
188 scm_right: 0.0,
189 platysma: 0.0,
190 atlas_protrusion: 0.0,
191 };
192 let b = NeckTendonState {
193 scm_left: 1.0,
194 scm_right: 1.0,
195 platysma: 1.0,
196 atlas_protrusion: 1.0,
197 };
198 let m = nt_blend(&a, &b, 0.5);
199 assert!((m.scm_left - 0.5).abs() < 1e-5);
200 }
201
202 #[test]
203 fn weights_correct() {
204 let s = NeckTendonState {
205 scm_left: 0.3,
206 scm_right: 0.7,
207 platysma: 0.5,
208 atlas_protrusion: 0.2,
209 };
210 let w = nt_to_weights(&s);
211 assert!((w.scm_definition_l - 0.3).abs() < 1e-6);
212 assert!((w.scm_definition_r - 0.7).abs() < 1e-6);
213 }
214
215 #[test]
216 fn asymmetry_symmetric_is_zero() {
217 let s = NeckTendonState {
218 scm_left: 0.5,
219 scm_right: 0.5,
220 platysma: 0.0,
221 atlas_protrusion: 0.0,
222 };
223 assert!(nt_asymmetry(&s) < 1e-6);
224 }
225
226 #[test]
227 fn asymmetry_uses_frac1sqrt2() {
228 let s = NeckTendonState {
229 scm_left: 1.0,
230 scm_right: 0.0,
231 platysma: 0.0,
232 atlas_protrusion: 0.0,
233 };
234 let a = nt_asymmetry(&s);
235 assert!((a - FRAC_1_SQRT_2).abs() < 1e-5);
236 }
237
238 #[test]
239 fn json_contains_platysma() {
240 let s = NeckTendonState {
241 scm_left: 0.0,
242 scm_right: 0.0,
243 platysma: 0.4,
244 atlas_protrusion: 0.0,
245 };
246 assert!(nt_to_json(&s).contains("platysma"));
247 }
248
249 #[test]
250 fn contains_range_check() {
251 let v = 0.7f32;
252 assert!((0.0..=1.0).contains(&v));
253 }
254}