oxihuman_morph/
spine_curve_control.rs1#![allow(dead_code)]
3
4use std::f32::consts::PI;
7
8#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct SpineCurveConfig {
12 pub max_lordosis_rad: f32,
14 pub max_kyphosis_rad: f32,
16 pub max_scoliosis_rad: f32,
18}
19
20impl Default for SpineCurveConfig {
21 fn default() -> Self {
22 Self {
23 max_lordosis_rad: PI / 8.0,
24 max_kyphosis_rad: PI / 10.0,
25 max_scoliosis_rad: PI / 16.0,
26 }
27 }
28}
29
30#[allow(dead_code)]
32#[derive(Debug, Clone, Default)]
33pub struct SpineCurveState {
34 pub lordosis: f32,
36 pub kyphosis: f32,
38 pub scoliosis: f32,
40}
41
42#[allow(dead_code)]
43pub fn new_spine_curve_state() -> SpineCurveState {
44 SpineCurveState::default()
45}
46
47#[allow(dead_code)]
48pub fn default_spine_curve_config() -> SpineCurveConfig {
49 SpineCurveConfig::default()
50}
51
52#[allow(dead_code)]
53pub fn scc_set_lordosis(state: &mut SpineCurveState, v: f32) {
54 state.lordosis = v.clamp(-1.0, 1.0);
55}
56
57#[allow(dead_code)]
58pub fn scc_set_kyphosis(state: &mut SpineCurveState, v: f32) {
59 state.kyphosis = v.clamp(-1.0, 1.0);
60}
61
62#[allow(dead_code)]
63pub fn scc_set_scoliosis(state: &mut SpineCurveState, v: f32) {
64 state.scoliosis = v.clamp(-1.0, 1.0);
65}
66
67#[allow(dead_code)]
68pub fn scc_reset(state: &mut SpineCurveState) {
69 *state = SpineCurveState::default();
70}
71
72#[allow(dead_code)]
73pub fn scc_is_neutral(state: &SpineCurveState) -> bool {
74 state.lordosis.abs() < 1e-4 && state.kyphosis.abs() < 1e-4 && state.scoliosis.abs() < 1e-4
75}
76
77#[allow(dead_code)]
79pub fn scc_lordosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
80 state.lordosis * cfg.max_lordosis_rad
81}
82
83#[allow(dead_code)]
85pub fn scc_kyphosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
86 state.kyphosis * cfg.max_kyphosis_rad
87}
88
89#[allow(dead_code)]
91pub fn scc_scoliosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
92 state.scoliosis * cfg.max_scoliosis_rad
93}
94
95#[allow(dead_code)]
97pub fn scc_total_curvature(state: &SpineCurveState) -> f32 {
98 state.lordosis.abs() + state.kyphosis.abs() + state.scoliosis.abs()
99}
100
101#[allow(dead_code)]
103pub fn scc_to_weights(state: &SpineCurveState) -> [f32; 6] {
104 [
105 state.lordosis.max(0.0),
106 (-state.lordosis).max(0.0),
107 state.kyphosis.max(0.0),
108 (-state.kyphosis).max(0.0),
109 state.scoliosis.max(0.0),
110 (-state.scoliosis).max(0.0),
111 ]
112}
113
114#[allow(dead_code)]
115pub fn scc_blend(a: &SpineCurveState, b: &SpineCurveState, t: f32) -> SpineCurveState {
116 let t = t.clamp(0.0, 1.0);
117 let inv = 1.0 - t;
118 SpineCurveState {
119 lordosis: a.lordosis * inv + b.lordosis * t,
120 kyphosis: a.kyphosis * inv + b.kyphosis * t,
121 scoliosis: a.scoliosis * inv + b.scoliosis * t,
122 }
123}
124
125#[allow(dead_code)]
126pub fn scc_to_json(state: &SpineCurveState) -> String {
127 format!(
128 "{{\"lordosis\":{:.4},\"kyphosis\":{:.4},\"scoliosis\":{:.4}}}",
129 state.lordosis, state.kyphosis, state.scoliosis
130 )
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use std::f32::consts::PI;
137
138 #[test]
139 fn default_is_neutral() {
140 assert!(scc_is_neutral(&new_spine_curve_state()));
141 }
142
143 #[test]
144 fn lordosis_clamps() {
145 let mut s = new_spine_curve_state();
146 scc_set_lordosis(&mut s, 5.0);
147 assert!((s.lordosis - 1.0).abs() < 1e-6);
148 }
149
150 #[test]
151 fn kyphosis_clamps_negative() {
152 let mut s = new_spine_curve_state();
153 scc_set_kyphosis(&mut s, -5.0);
154 assert!((s.kyphosis + 1.0).abs() < 1e-6);
155 }
156
157 #[test]
158 fn scoliosis_clamps() {
159 let mut s = new_spine_curve_state();
160 scc_set_scoliosis(&mut s, 2.0);
161 assert!((s.scoliosis - 1.0).abs() < 1e-6);
162 }
163
164 #[test]
165 fn reset_clears() {
166 let mut s = new_spine_curve_state();
167 scc_set_lordosis(&mut s, 0.8);
168 scc_reset(&mut s);
169 assert!(scc_is_neutral(&s));
170 }
171
172 #[test]
173 fn lordosis_angle_positive() {
174 let cfg = default_spine_curve_config();
175 let mut s = new_spine_curve_state();
176 scc_set_lordosis(&mut s, 1.0);
177 let a = scc_lordosis_angle_rad(&s, &cfg);
178 assert!(a > 0.0);
179 assert!(a <= PI / 8.0 + 1e-5);
180 }
181
182 #[test]
183 fn total_curvature_sums() {
184 let mut s = new_spine_curve_state();
185 scc_set_lordosis(&mut s, 0.5);
186 scc_set_kyphosis(&mut s, 0.5);
187 assert!((scc_total_curvature(&s) - 1.0).abs() < 1e-5);
188 }
189
190 #[test]
191 fn weights_six_elements() {
192 let w = scc_to_weights(&new_spine_curve_state());
193 assert_eq!(w.len(), 6);
194 }
195
196 #[test]
197 fn blend_midpoint() {
198 let mut b = new_spine_curve_state();
199 scc_set_lordosis(&mut b, 1.0);
200 let r = scc_blend(&new_spine_curve_state(), &b, 0.5);
201 assert!((r.lordosis - 0.5).abs() < 1e-5);
202 }
203
204 #[test]
205 fn json_has_keys() {
206 let j = scc_to_json(&new_spine_curve_state());
207 assert!(j.contains("lordosis") && j.contains("kyphosis"));
208 }
209}