oxihuman_morph/
neck_forward_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_3;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct NeckForwardConfig {
13 pub max_angle_rad: f32,
15 pub thoracic_compensate: bool,
17}
18
19impl Default for NeckForwardConfig {
20 fn default() -> Self {
21 NeckForwardConfig {
22 max_angle_rad: FRAC_PI_3,
23 thoracic_compensate: false,
24 }
25 }
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct NeckForwardState {
32 forward: f32,
34 lateral: f32,
36 protrusion: f32,
38 config: NeckForwardConfig,
39}
40
41pub fn default_neck_forward_config() -> NeckForwardConfig {
43 NeckForwardConfig::default()
44}
45
46pub fn new_neck_forward_state(config: NeckForwardConfig) -> NeckForwardState {
48 NeckForwardState {
49 forward: 0.0,
50 lateral: 0.0,
51 protrusion: 0.0,
52 config,
53 }
54}
55
56pub fn nfc_set_forward(state: &mut NeckForwardState, v: f32) {
58 state.forward = v.clamp(0.0, 1.0);
59}
60
61pub fn nfc_set_lateral(state: &mut NeckForwardState, v: f32) {
63 state.lateral = v.clamp(-1.0, 1.0);
64}
65
66pub fn nfc_set_protrusion(state: &mut NeckForwardState, v: f32) {
68 state.protrusion = v.clamp(0.0, 1.0);
69}
70
71pub fn nfc_reset(state: &mut NeckForwardState) {
73 state.forward = 0.0;
74 state.lateral = 0.0;
75 state.protrusion = 0.0;
76}
77
78pub fn nfc_is_neutral(state: &NeckForwardState) -> bool {
80 state.forward < 1e-5 && state.lateral.abs() < 1e-5 && state.protrusion < 1e-5
81}
82
83pub fn nfc_angle_rad(state: &NeckForwardState) -> f32 {
85 state.forward * state.config.max_angle_rad
86}
87
88pub fn nfc_to_weights(state: &NeckForwardState) -> [f32; 3] {
90 [
91 state.forward,
92 (state.lateral * 0.5 + 0.5).clamp(0.0, 1.0),
93 state.protrusion,
94 ]
95}
96
97pub fn nfc_blend(a: &NeckForwardState, b: &NeckForwardState, t: f32) -> NeckForwardState {
99 let t = t.clamp(0.0, 1.0);
100 NeckForwardState {
101 forward: a.forward + (b.forward - a.forward) * t,
102 lateral: a.lateral + (b.lateral - a.lateral) * t,
103 protrusion: a.protrusion + (b.protrusion - a.protrusion) * t,
104 config: a.config.clone(),
105 }
106}
107
108pub fn nfc_to_json(state: &NeckForwardState) -> String {
110 format!(
111 r#"{{"forward":{:.4},"lateral":{:.4},"protrusion":{:.4}}}"#,
112 state.forward, state.lateral, state.protrusion
113 )
114}
115
116#[cfg(test)]
120mod tests {
121 use super::*;
122
123 fn make() -> NeckForwardState {
124 new_neck_forward_state(default_neck_forward_config())
125 }
126
127 #[test]
128 fn neutral_on_creation() {
129 assert!(nfc_is_neutral(&make()));
130 }
131
132 #[test]
133 fn set_forward_clamps() {
134 let mut s = make();
135 nfc_set_forward(&mut s, 3.0);
136 assert!((s.forward - 1.0).abs() < 1e-5);
137 }
138
139 #[test]
140 fn reset_clears() {
141 let mut s = make();
142 nfc_set_forward(&mut s, 0.8);
143 nfc_reset(&mut s);
144 assert!(nfc_is_neutral(&s));
145 }
146
147 #[test]
148 fn angle_positive_when_forward() {
149 let mut s = make();
150 nfc_set_forward(&mut s, 0.5);
151 assert!(nfc_angle_rad(&s) > 0.0);
152 }
153
154 #[test]
155 fn weights_in_range() {
156 let s = make();
157 for v in nfc_to_weights(&s) {
158 assert!((0.0..=1.0).contains(&v));
159 }
160 }
161
162 #[test]
163 fn blend_midpoint() {
164 let mut b = make();
165 nfc_set_forward(&mut b, 1.0);
166 let m = nfc_blend(&make(), &b, 0.5);
167 assert!((m.forward - 0.5).abs() < 1e-5);
168 }
169
170 #[test]
171 fn lateral_clamped_positive() {
172 let mut s = make();
173 nfc_set_lateral(&mut s, 5.0);
174 assert!((s.lateral - 1.0).abs() < 1e-5);
175 }
176
177 #[test]
178 fn json_has_forward() {
179 assert!(nfc_to_json(&make()).contains("forward"));
180 }
181
182 #[test]
183 fn blend_at_zero_is_a() {
184 let a = make();
185 let r = nfc_blend(&a, &make(), 0.0);
186 assert!((r.forward - a.forward).abs() < 1e-5);
187 }
188}