Skip to main content

oxihuman_morph/
head_tilt_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Head tilt / torticollis morph.
6
7/// Head tilt configuration.
8#[derive(Debug, Clone)]
9pub struct HeadTiltMorphConfig {
10    pub lateral_tilt: f32,
11    pub forward_tilt: f32,
12    pub axial_rotation: f32,
13}
14
15impl Default for HeadTiltMorphConfig {
16    fn default() -> Self {
17        Self {
18            lateral_tilt: 0.0,
19            forward_tilt: 0.0,
20            axial_rotation: 0.0,
21        }
22    }
23}
24
25/// Head tilt morph state.
26#[derive(Debug, Clone)]
27pub struct HeadTiltMorph {
28    pub config: HeadTiltMorphConfig,
29    pub intensity: f32,
30    pub enabled: bool,
31}
32
33impl HeadTiltMorph {
34    pub fn new() -> Self {
35        Self {
36            config: HeadTiltMorphConfig::default(),
37            intensity: 0.0,
38            enabled: true,
39        }
40    }
41}
42
43impl Default for HeadTiltMorph {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49/// Create a new HeadTiltMorph.
50pub fn new_head_tilt_morph() -> HeadTiltMorph {
51    HeadTiltMorph::new()
52}
53
54/// Set lateral head tilt (-1.0 left, 1.0 right).
55pub fn head_tilt_set_lateral(morph: &mut HeadTiltMorph, v: f32) {
56    morph.config.lateral_tilt = v.clamp(-1.0, 1.0);
57}
58
59/// Set forward tilt factor (positive = forward).
60pub fn head_tilt_set_forward(morph: &mut HeadTiltMorph, v: f32) {
61    morph.config.forward_tilt = v.clamp(-1.0, 1.0);
62}
63
64/// Set axial rotation factor.
65pub fn head_tilt_set_axial(morph: &mut HeadTiltMorph, v: f32) {
66    morph.config.axial_rotation = v.clamp(-1.0, 1.0);
67}
68
69/// Compute total head displacement magnitude.
70pub fn head_tilt_magnitude(morph: &HeadTiltMorph) -> f32 {
71    let l = morph.config.lateral_tilt;
72    let f = morph.config.forward_tilt;
73    let r = morph.config.axial_rotation;
74    ((l * l + f * f + r * r).sqrt() * morph.intensity).min(1.0)
75}
76
77/// Serialize to JSON.
78pub fn head_tilt_to_json(morph: &HeadTiltMorph) -> String {
79    format!(
80        r#"{{"intensity":{},"lateral_tilt":{},"forward_tilt":{},"axial_rotation":{}}}"#,
81        morph.intensity,
82        morph.config.lateral_tilt,
83        morph.config.forward_tilt,
84        morph.config.axial_rotation,
85    )
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn test_new() {
94        let m = new_head_tilt_morph();
95        assert!((m.config.lateral_tilt - 0.0).abs() < 1e-6 /* default */);
96    }
97
98    #[test]
99    fn test_lateral_clamp_high() {
100        let mut m = new_head_tilt_morph();
101        head_tilt_set_lateral(&mut m, 5.0);
102        assert!((m.config.lateral_tilt - 1.0).abs() < 1e-6 /* clamped */);
103    }
104
105    #[test]
106    fn test_lateral_clamp_low() {
107        let mut m = new_head_tilt_morph();
108        head_tilt_set_lateral(&mut m, -5.0);
109        assert!((m.config.lateral_tilt - (-1.0)).abs() < 1e-6 /* clamped */);
110    }
111
112    #[test]
113    fn test_forward() {
114        let mut m = new_head_tilt_morph();
115        head_tilt_set_forward(&mut m, 0.6);
116        assert!((m.config.forward_tilt - 0.6).abs() < 1e-6 /* stored */);
117    }
118
119    #[test]
120    fn test_axial() {
121        let mut m = new_head_tilt_morph();
122        head_tilt_set_axial(&mut m, 0.3);
123        assert!((m.config.axial_rotation - 0.3).abs() < 1e-6 /* stored */);
124    }
125
126    #[test]
127    fn test_magnitude_zero() {
128        let m = new_head_tilt_morph();
129        assert!((head_tilt_magnitude(&m) - 0.0).abs() < 1e-6 /* zero */);
130    }
131
132    #[test]
133    fn test_magnitude_nonzero() {
134        let mut m = new_head_tilt_morph();
135        head_tilt_set_lateral(&mut m, 1.0);
136        m.intensity = 1.0;
137        assert!(head_tilt_magnitude(&m) > 0.0 /* nonzero */);
138    }
139
140    #[test]
141    fn test_json_key() {
142        let m = new_head_tilt_morph();
143        let j = head_tilt_to_json(&m);
144        assert!(j.contains("lateral_tilt") /* key */);
145    }
146
147    #[test]
148    fn test_default() {
149        let m = HeadTiltMorph::default();
150        assert!(m.enabled /* enabled */);
151    }
152}