Skip to main content

oxihuman_morph/
neck_flexion_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Neck flexion morph — controls forward/backward neck flexion and lateral tilt.
6
7use std::f32::consts::FRAC_PI_2;
8
9/// Configuration for neck flexion control.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct NeckFlexionConfig {
13    pub max_flexion: f32,
14    pub max_lateral: f32,
15}
16
17/// Runtime state.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct NeckFlexionState {
21    pub forward_flexion: f32,
22    pub backward_extension: f32,
23    pub lateral_left: f32,
24    pub lateral_right: f32,
25}
26
27#[allow(dead_code)]
28pub fn default_neck_flexion_config() -> NeckFlexionConfig {
29    NeckFlexionConfig {
30        max_flexion: 1.0,
31        max_lateral: 1.0,
32    }
33}
34
35#[allow(dead_code)]
36pub fn new_neck_flexion_state() -> NeckFlexionState {
37    NeckFlexionState {
38        forward_flexion: 0.0,
39        backward_extension: 0.0,
40        lateral_left: 0.0,
41        lateral_right: 0.0,
42    }
43}
44
45#[allow(dead_code)]
46pub fn nf_set_forward(state: &mut NeckFlexionState, cfg: &NeckFlexionConfig, v: f32) {
47    state.forward_flexion = v.clamp(0.0, cfg.max_flexion);
48    state.backward_extension = 0.0;
49}
50
51#[allow(dead_code)]
52pub fn nf_set_backward(state: &mut NeckFlexionState, cfg: &NeckFlexionConfig, v: f32) {
53    state.backward_extension = v.clamp(0.0, cfg.max_flexion);
54    state.forward_flexion = 0.0;
55}
56
57#[allow(dead_code)]
58pub fn nf_set_lateral(
59    state: &mut NeckFlexionState,
60    cfg: &NeckFlexionConfig,
61    left: f32,
62    right: f32,
63) {
64    state.lateral_left = left.clamp(0.0, cfg.max_lateral);
65    state.lateral_right = right.clamp(0.0, cfg.max_lateral);
66}
67
68#[allow(dead_code)]
69pub fn nf_reset(state: &mut NeckFlexionState) {
70    *state = new_neck_flexion_state();
71}
72
73#[allow(dead_code)]
74pub fn nf_is_neutral(state: &NeckFlexionState) -> bool {
75    let vals = [
76        state.forward_flexion,
77        state.backward_extension,
78        state.lateral_left,
79        state.lateral_right,
80    ];
81    !vals.is_empty() && vals.iter().all(|v| v.abs() < 1e-6)
82}
83
84#[allow(dead_code)]
85pub fn nf_sagittal_angle_rad(state: &NeckFlexionState) -> f32 {
86    (state.forward_flexion - state.backward_extension) * FRAC_PI_2 * 0.5
87}
88
89#[allow(dead_code)]
90pub fn nf_lateral_angle_rad(state: &NeckFlexionState) -> f32 {
91    (state.lateral_left - state.lateral_right) * FRAC_PI_2 * 0.3
92}
93
94#[allow(dead_code)]
95pub fn nf_blend(a: &NeckFlexionState, b: &NeckFlexionState, t: f32) -> NeckFlexionState {
96    let t = t.clamp(0.0, 1.0);
97    NeckFlexionState {
98        forward_flexion: a.forward_flexion + (b.forward_flexion - a.forward_flexion) * t,
99        backward_extension: a.backward_extension
100            + (b.backward_extension - a.backward_extension) * t,
101        lateral_left: a.lateral_left + (b.lateral_left - a.lateral_left) * t,
102        lateral_right: a.lateral_right + (b.lateral_right - a.lateral_right) * t,
103    }
104}
105
106#[allow(dead_code)]
107pub fn nf_to_weights(state: &NeckFlexionState) -> Vec<(String, f32)> {
108    vec![
109        ("neck_flexion_fwd".to_string(), state.forward_flexion),
110        ("neck_extension_bwd".to_string(), state.backward_extension),
111        ("neck_lateral_l".to_string(), state.lateral_left),
112        ("neck_lateral_r".to_string(), state.lateral_right),
113    ]
114}
115
116#[allow(dead_code)]
117pub fn nf_to_json(state: &NeckFlexionState) -> String {
118    format!(
119        r#"{{"forward_flexion":{:.4},"backward_extension":{:.4},"lateral_left":{:.4},"lateral_right":{:.4}}}"#,
120        state.forward_flexion, state.backward_extension, state.lateral_left, state.lateral_right
121    )
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_config() {
130        let cfg = default_neck_flexion_config();
131        assert!((cfg.max_flexion - 1.0).abs() < 1e-6);
132    }
133
134    #[test]
135    fn new_state_neutral() {
136        let s = new_neck_flexion_state();
137        assert!(nf_is_neutral(&s));
138    }
139
140    #[test]
141    fn set_forward_clears_backward() {
142        let cfg = default_neck_flexion_config();
143        let mut s = new_neck_flexion_state();
144        nf_set_backward(&mut s, &cfg, 0.5);
145        nf_set_forward(&mut s, &cfg, 0.3);
146        assert_eq!(s.backward_extension, 0.0);
147        assert!((s.forward_flexion - 0.3).abs() < 1e-6);
148    }
149
150    #[test]
151    fn set_backward_clears_forward() {
152        let cfg = default_neck_flexion_config();
153        let mut s = new_neck_flexion_state();
154        nf_set_forward(&mut s, &cfg, 0.5);
155        nf_set_backward(&mut s, &cfg, 0.4);
156        assert_eq!(s.forward_flexion, 0.0);
157        assert!((s.backward_extension - 0.4).abs() < 1e-6);
158    }
159
160    #[test]
161    fn set_lateral() {
162        let cfg = default_neck_flexion_config();
163        let mut s = new_neck_flexion_state();
164        nf_set_lateral(&mut s, &cfg, 0.4, 0.6);
165        assert!((s.lateral_left - 0.4).abs() < 1e-6);
166        assert!((s.lateral_right - 0.6).abs() < 1e-6);
167    }
168
169    #[test]
170    fn sagittal_angle_forward_positive() {
171        let cfg = default_neck_flexion_config();
172        let mut s = new_neck_flexion_state();
173        nf_set_forward(&mut s, &cfg, 1.0);
174        assert!(nf_sagittal_angle_rad(&s) > 0.0);
175    }
176
177    #[test]
178    fn reset_clears() {
179        let cfg = default_neck_flexion_config();
180        let mut s = new_neck_flexion_state();
181        nf_set_forward(&mut s, &cfg, 0.5);
182        nf_reset(&mut s);
183        assert!(nf_is_neutral(&s));
184    }
185
186    #[test]
187    fn blend_midpoint() {
188        let a = new_neck_flexion_state();
189        let cfg = default_neck_flexion_config();
190        let mut b = new_neck_flexion_state();
191        nf_set_forward(&mut b, &cfg, 1.0);
192        let mid = nf_blend(&a, &b, 0.5);
193        assert!((mid.forward_flexion - 0.5).abs() < 1e-6);
194    }
195
196    #[test]
197    fn to_weights_count() {
198        let s = new_neck_flexion_state();
199        assert_eq!(nf_to_weights(&s).len(), 4);
200    }
201
202    #[test]
203    fn to_json_fields() {
204        let s = new_neck_flexion_state();
205        let j = nf_to_json(&s);
206        assert!(j.contains("forward_flexion"));
207        assert!(j.contains("lateral_right"));
208    }
209}