Skip to main content

oxihuman_morph/
sternum_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Sternum control — sternal length and manubrium prominence.
6
7use std::f32::consts::FRAC_PI_4;
8
9/// Configuration.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct SternumConfig {
13    /// Reference tilt angle for xiphoid process.
14    pub xiphoid_ref_rad: f32,
15}
16
17impl Default for SternumConfig {
18    fn default() -> Self {
19        SternumConfig {
20            xiphoid_ref_rad: FRAC_PI_4,
21        }
22    }
23}
24
25/// Runtime state.
26#[allow(dead_code)]
27#[derive(Debug, Clone)]
28pub struct SternumState {
29    /// Sternal length in `[0.0, 1.0]`.
30    length: f32,
31    /// Manubrium protrusion in `[0.0, 1.0]`.
32    manubrium: f32,
33    /// Xiphoid angle in `[-1.0, 1.0]` (positive = flared).
34    xiphoid_angle: f32,
35    config: SternumConfig,
36}
37
38/// Default config.
39pub fn default_sternum_config() -> SternumConfig {
40    SternumConfig::default()
41}
42
43/// New neutral state.
44pub fn new_sternum_state(config: SternumConfig) -> SternumState {
45    SternumState {
46        length: 0.5,
47        manubrium: 0.0,
48        xiphoid_angle: 0.0,
49        config,
50    }
51}
52
53/// Set sternal length.
54pub fn stc_set_length(state: &mut SternumState, v: f32) {
55    state.length = v.clamp(0.0, 1.0);
56}
57
58/// Set manubrium protrusion.
59pub fn stc_set_manubrium(state: &mut SternumState, v: f32) {
60    state.manubrium = v.clamp(0.0, 1.0);
61}
62
63/// Set xiphoid angle.
64pub fn stc_set_xiphoid_angle(state: &mut SternumState, v: f32) {
65    state.xiphoid_angle = v.clamp(-1.0, 1.0);
66}
67
68/// Reset.
69pub fn stc_reset(state: &mut SternumState) {
70    state.length = 0.5;
71    state.manubrium = 0.0;
72    state.xiphoid_angle = 0.0;
73}
74
75/// True when neutral.
76pub fn stc_is_neutral(state: &SternumState) -> bool {
77    (state.length - 0.5).abs() < 1e-5 && state.manubrium < 1e-5 && state.xiphoid_angle.abs() < 1e-5
78}
79
80/// Xiphoid angle in radians.
81pub fn stc_xiphoid_angle_rad(state: &SternumState) -> f32 {
82    state.xiphoid_angle * state.config.xiphoid_ref_rad
83}
84
85/// Morph weights: `[length, manubrium, xiphoid_norm]`.
86pub fn stc_to_weights(state: &SternumState) -> [f32; 3] {
87    [
88        state.length,
89        state.manubrium,
90        (state.xiphoid_angle * 0.5 + 0.5).clamp(0.0, 1.0),
91    ]
92}
93
94/// Blend.
95pub fn stc_blend(a: &SternumState, b: &SternumState, t: f32) -> SternumState {
96    let t = t.clamp(0.0, 1.0);
97    SternumState {
98        length: a.length + (b.length - a.length) * t,
99        manubrium: a.manubrium + (b.manubrium - a.manubrium) * t,
100        xiphoid_angle: a.xiphoid_angle + (b.xiphoid_angle - a.xiphoid_angle) * t,
101        config: a.config.clone(),
102    }
103}
104
105/// Serialise.
106pub fn stc_to_json(state: &SternumState) -> String {
107    format!(
108        r#"{{"length":{:.4},"manubrium":{:.4},"xiphoid_angle":{:.4}}}"#,
109        state.length, state.manubrium, state.xiphoid_angle
110    )
111}
112
113// ---------------------------------------------------------------------------
114// Tests
115// ---------------------------------------------------------------------------
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    fn make() -> SternumState {
121        new_sternum_state(default_sternum_config())
122    }
123
124    #[test]
125    fn neutral_on_creation() {
126        assert!(stc_is_neutral(&make()));
127    }
128
129    #[test]
130    fn set_length_clamps() {
131        let mut s = make();
132        stc_set_length(&mut s, 5.0);
133        assert!((s.length - 1.0).abs() < 1e-5);
134    }
135
136    #[test]
137    fn reset_restores_neutral() {
138        let mut s = make();
139        stc_set_manubrium(&mut s, 0.8);
140        stc_reset(&mut s);
141        assert!(stc_is_neutral(&s));
142    }
143
144    #[test]
145    fn xiphoid_angle_rad_computation() {
146        let mut s = make();
147        stc_set_xiphoid_angle(&mut s, 1.0);
148        assert!(stc_xiphoid_angle_rad(&s) > 0.0);
149    }
150
151    #[test]
152    fn weights_in_range() {
153        let s = make();
154        for v in stc_to_weights(&s) {
155            assert!((0.0..=1.0).contains(&v));
156        }
157    }
158
159    #[test]
160    fn blend_midpoint() {
161        let mut a = make();
162        let mut b = make();
163        stc_set_length(&mut a, 0.0);
164        stc_set_length(&mut b, 1.0);
165        let m = stc_blend(&a, &b, 0.5);
166        assert!((m.length - 0.5).abs() < 1e-5);
167    }
168
169    #[test]
170    fn blend_at_zero_is_a() {
171        let a = make();
172        let r = stc_blend(&a, &make(), 0.0);
173        assert!((r.length - a.length).abs() < 1e-5);
174    }
175
176    #[test]
177    fn json_has_length() {
178        assert!(stc_to_json(&make()).contains("length"));
179    }
180
181    #[test]
182    fn xiphoid_clamped_negative() {
183        let mut s = make();
184        stc_set_xiphoid_angle(&mut s, -5.0);
185        assert!((s.xiphoid_angle + 1.0).abs() < 1e-5);
186    }
187}