Skip to main content

oxihuman_morph/
spine_curve_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Spine curve control — lordosis and kyphosis morph for the spinal column.
5
6use std::f32::consts::PI;
7
8/// Config.
9#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct SpineCurveConfig {
12    /// Maximum lordosis angle in radians (lumbar inward curve).
13    pub max_lordosis_rad: f32,
14    /// Maximum kyphosis angle in radians (thoracic outward curve).
15    pub max_kyphosis_rad: f32,
16    /// Maximum lateral scoliosis angle in radians.
17    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/// Spinal curvature state, all values -1..=1.
31#[allow(dead_code)]
32#[derive(Debug, Clone, Default)]
33pub struct SpineCurveState {
34    /// Lumbar lordosis: positive = increased inward curve.
35    pub lordosis: f32,
36    /// Thoracic kyphosis: positive = increased outward hunch.
37    pub kyphosis: f32,
38    /// Lateral scoliosis: positive = rightward lean.
39    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/// Lordosis angle in radians.
78#[allow(dead_code)]
79pub fn scc_lordosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
80    state.lordosis * cfg.max_lordosis_rad
81}
82
83/// Kyphosis angle in radians.
84#[allow(dead_code)]
85pub fn scc_kyphosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
86    state.kyphosis * cfg.max_kyphosis_rad
87}
88
89/// Scoliosis angle in radians.
90#[allow(dead_code)]
91pub fn scc_scoliosis_angle_rad(state: &SpineCurveState, cfg: &SpineCurveConfig) -> f32 {
92    state.scoliosis * cfg.max_scoliosis_rad
93}
94
95/// Returns total curvature magnitude (0..=3).
96#[allow(dead_code)]
97pub fn scc_total_curvature(state: &SpineCurveState) -> f32 {
98    state.lordosis.abs() + state.kyphosis.abs() + state.scoliosis.abs()
99}
100
101/// Returns morph weights \[lordosis+, lordosis-, kyphosis+, kyphosis-, scoliosis+, scoliosis-\].
102#[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}