Skip to main content

oxihuman_morph/
posture_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Standing posture and sway morph.
6
7/// Configuration for posture morph blending.
8#[derive(Debug, Clone)]
9pub struct PostureMorphConfig {
10    pub sway_amplitude: f32,
11    pub forward_lean: f32,
12    pub lateral_lean: f32,
13}
14
15impl Default for PostureMorphConfig {
16    fn default() -> Self {
17        Self {
18            sway_amplitude: 0.0,
19            forward_lean: 0.0,
20            lateral_lean: 0.0,
21        }
22    }
23}
24
25/// Posture morph state.
26#[derive(Debug, Clone)]
27pub struct PostureMorph {
28    pub config: PostureMorphConfig,
29    pub intensity: f32,
30    pub enabled: bool,
31}
32
33impl PostureMorph {
34    pub fn new() -> Self {
35        Self {
36            config: PostureMorphConfig::default(),
37            intensity: 0.0,
38            enabled: true,
39        }
40    }
41}
42
43impl Default for PostureMorph {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Create a new PostureMorph with default config.
50pub fn new_posture_morph() -> PostureMorph {
51    PostureMorph::new()
52}
53
54/// Set overall sway intensity (0.0–1.0).
55pub fn posture_set_sway(morph: &mut PostureMorph, amplitude: f32) {
56    morph.config.sway_amplitude = amplitude.clamp(0.0, 1.0);
57}
58
59/// Set forward lean offset.
60pub fn posture_set_forward_lean(morph: &mut PostureMorph, lean: f32) {
61    morph.config.forward_lean = lean.clamp(-1.0, 1.0);
62}
63
64/// Set lateral lean offset.
65pub fn posture_set_lateral_lean(morph: &mut PostureMorph, lean: f32) {
66    morph.config.lateral_lean = lean.clamp(-1.0, 1.0);
67}
68
69/// Apply morph weights to a vertex buffer.
70#[allow(clippy::needless_range_loop)]
71pub fn posture_apply_weights(morph: &PostureMorph, weights: &mut [f32]) {
72    let scale = morph.intensity * morph.config.sway_amplitude;
73    for i in 0..weights.len() {
74        weights[i] *= scale;
75    }
76}
77
78/// Serialize to JSON string.
79pub fn posture_to_json(morph: &PostureMorph) -> String {
80    format!(
81        r#"{{"intensity":{},"sway":{},"forward_lean":{},"lateral_lean":{}}}"#,
82        morph.intensity,
83        morph.config.sway_amplitude,
84        morph.config.forward_lean,
85        morph.config.lateral_lean,
86    )
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_new_posture_morph() {
95        let m = new_posture_morph();
96        assert!((m.intensity - 0.0).abs() < 1e-6 /* default intensity zero */);
97    }
98
99    #[test]
100    fn test_set_sway_clamp() {
101        let mut m = new_posture_morph();
102        posture_set_sway(&mut m, 2.5);
103        assert!((m.config.sway_amplitude - 1.0).abs() < 1e-6 /* clamped to 1 */);
104    }
105
106    #[test]
107    fn test_set_sway_negative() {
108        let mut m = new_posture_morph();
109        posture_set_sway(&mut m, -0.5);
110        assert!((m.config.sway_amplitude - 0.0).abs() < 1e-6 /* clamped to 0 */);
111    }
112
113    #[test]
114    fn test_forward_lean() {
115        let mut m = new_posture_morph();
116        posture_set_forward_lean(&mut m, 0.3);
117        assert!((m.config.forward_lean - 0.3).abs() < 1e-6 /* value stored */);
118    }
119
120    #[test]
121    fn test_lateral_lean_clamp() {
122        let mut m = new_posture_morph();
123        posture_set_lateral_lean(&mut m, 5.0);
124        assert!((m.config.lateral_lean - 1.0).abs() < 1e-6 /* clamped upper */);
125    }
126
127    #[test]
128    fn test_apply_weights_empty() {
129        let m = new_posture_morph();
130        let mut w: Vec<f32> = vec![];
131        posture_apply_weights(&m, &mut w);
132        assert!(w.is_empty() /* no panic on empty */);
133    }
134
135    #[test]
136    fn test_apply_weights_zero_intensity() {
137        let mut m = new_posture_morph();
138        posture_set_sway(&mut m, 1.0);
139        m.intensity = 0.0;
140        let mut w = vec![1.0f32, 1.0];
141        posture_apply_weights(&m, &mut w);
142        assert!((w[0] - 0.0).abs() < 1e-6 /* scaled to zero */);
143    }
144
145    #[test]
146    fn test_json_output() {
147        let m = new_posture_morph();
148        let j = posture_to_json(&m);
149        assert!(j.contains("intensity") /* json key present */);
150    }
151
152    #[test]
153    fn test_default_enabled() {
154        let m = PostureMorph::default();
155        assert!(m.enabled /* default enabled */);
156    }
157
158    #[test]
159    fn test_clone() {
160        let m = new_posture_morph();
161        let m2 = m.clone();
162        assert!((m2.intensity - m.intensity).abs() < 1e-6 /* clone equal */);
163    }
164}