Skip to main content

oxihuman_morph/
jaw_shift_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Jaw shift morph — controls lateral and anterior/posterior jaw displacement.
6
7/// Configuration for jaw shift control.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct JawShiftConfig {
11    pub max_lateral: f32,
12    pub max_ap: f32,
13}
14
15/// Runtime state.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct JawShiftState {
19    pub lateral: f32,
20    pub anterior: f32,
21    pub posterior: f32,
22    pub torsion: f32,
23}
24
25#[allow(dead_code)]
26pub fn default_jaw_shift_config() -> JawShiftConfig {
27    JawShiftConfig {
28        max_lateral: 1.0,
29        max_ap: 1.0,
30    }
31}
32
33#[allow(dead_code)]
34pub fn new_jaw_shift_state() -> JawShiftState {
35    JawShiftState {
36        lateral: 0.0,
37        anterior: 0.0,
38        posterior: 0.0,
39        torsion: 0.0,
40    }
41}
42
43#[allow(dead_code)]
44pub fn js_set_lateral(state: &mut JawShiftState, cfg: &JawShiftConfig, v: f32) {
45    state.lateral = v.clamp(-cfg.max_lateral, cfg.max_lateral);
46}
47
48#[allow(dead_code)]
49pub fn js_set_anterior(state: &mut JawShiftState, cfg: &JawShiftConfig, v: f32) {
50    state.anterior = v.clamp(0.0, cfg.max_ap);
51    state.posterior = 0.0;
52}
53
54#[allow(dead_code)]
55pub fn js_set_posterior(state: &mut JawShiftState, cfg: &JawShiftConfig, v: f32) {
56    state.posterior = v.clamp(0.0, cfg.max_ap);
57    state.anterior = 0.0;
58}
59
60#[allow(dead_code)]
61pub fn js_set_torsion(state: &mut JawShiftState, v: f32) {
62    state.torsion = v.clamp(-1.0, 1.0);
63}
64
65#[allow(dead_code)]
66pub fn js_reset(state: &mut JawShiftState) {
67    *state = new_jaw_shift_state();
68}
69
70#[allow(dead_code)]
71pub fn js_is_neutral(state: &JawShiftState) -> bool {
72    state.lateral.abs() < 1e-6
73        && state.anterior.abs() < 1e-6
74        && state.posterior.abs() < 1e-6
75        && state.torsion.abs() < 1e-6
76}
77
78#[allow(dead_code)]
79pub fn js_net_ap(state: &JawShiftState) -> f32 {
80    state.anterior - state.posterior
81}
82
83#[allow(dead_code)]
84pub fn js_displacement_magnitude(state: &JawShiftState) -> f32 {
85    let ap = js_net_ap(state);
86    (state.lateral * state.lateral + ap * ap).sqrt()
87}
88
89#[allow(dead_code)]
90pub fn js_blend(a: &JawShiftState, b: &JawShiftState, t: f32) -> JawShiftState {
91    let t = t.clamp(0.0, 1.0);
92    JawShiftState {
93        lateral: a.lateral + (b.lateral - a.lateral) * t,
94        anterior: a.anterior + (b.anterior - a.anterior) * t,
95        posterior: a.posterior + (b.posterior - a.posterior) * t,
96        torsion: a.torsion + (b.torsion - a.torsion) * t,
97    }
98}
99
100#[allow(dead_code)]
101pub fn js_to_weights(state: &JawShiftState) -> Vec<(String, f32)> {
102    vec![
103        ("jaw_lateral_shift".to_string(), state.lateral),
104        ("jaw_anterior".to_string(), state.anterior),
105        ("jaw_posterior".to_string(), state.posterior),
106        ("jaw_torsion".to_string(), state.torsion),
107    ]
108}
109
110#[allow(dead_code)]
111pub fn js_to_json(state: &JawShiftState) -> String {
112    format!(
113        r#"{{"lateral":{:.4},"anterior":{:.4},"posterior":{:.4},"torsion":{:.4}}}"#,
114        state.lateral, state.anterior, state.posterior, state.torsion
115    )
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn default_config() {
124        let cfg = default_jaw_shift_config();
125        assert!((cfg.max_lateral - 1.0).abs() < 1e-6);
126    }
127
128    #[test]
129    fn new_state_neutral() {
130        let s = new_jaw_shift_state();
131        assert!(js_is_neutral(&s));
132    }
133
134    #[test]
135    fn set_lateral_signed() {
136        let cfg = default_jaw_shift_config();
137        let mut s = new_jaw_shift_state();
138        js_set_lateral(&mut s, &cfg, -0.5);
139        assert!((s.lateral + 0.5).abs() < 1e-6);
140    }
141
142    #[test]
143    fn set_anterior_clears_posterior() {
144        let cfg = default_jaw_shift_config();
145        let mut s = new_jaw_shift_state();
146        js_set_posterior(&mut s, &cfg, 0.5);
147        js_set_anterior(&mut s, &cfg, 0.3);
148        assert_eq!(s.posterior, 0.0);
149        assert!((s.anterior - 0.3).abs() < 1e-6);
150    }
151
152    #[test]
153    fn set_posterior_clears_anterior() {
154        let cfg = default_jaw_shift_config();
155        let mut s = new_jaw_shift_state();
156        js_set_anterior(&mut s, &cfg, 0.5);
157        js_set_posterior(&mut s, &cfg, 0.4);
158        assert_eq!(s.anterior, 0.0);
159        assert!((s.posterior - 0.4).abs() < 1e-6);
160    }
161
162    #[test]
163    fn net_ap_anterior() {
164        let cfg = default_jaw_shift_config();
165        let mut s = new_jaw_shift_state();
166        js_set_anterior(&mut s, &cfg, 0.7);
167        assert!((js_net_ap(&s) - 0.7).abs() < 1e-6);
168    }
169
170    #[test]
171    fn reset_clears() {
172        let cfg = default_jaw_shift_config();
173        let mut s = new_jaw_shift_state();
174        js_set_lateral(&mut s, &cfg, 0.5);
175        js_reset(&mut s);
176        assert!(js_is_neutral(&s));
177    }
178
179    #[test]
180    fn displacement_magnitude_zero_at_neutral() {
181        let s = new_jaw_shift_state();
182        assert!(js_displacement_magnitude(&s) < 1e-6);
183    }
184
185    #[test]
186    fn blend_midpoint() {
187        let a = new_jaw_shift_state();
188        let cfg = default_jaw_shift_config();
189        let mut b = new_jaw_shift_state();
190        js_set_lateral(&mut b, &cfg, 1.0);
191        let mid = js_blend(&a, &b, 0.5);
192        assert!((mid.lateral - 0.5).abs() < 1e-6);
193    }
194
195    #[test]
196    fn to_weights_count() {
197        let s = new_jaw_shift_state();
198        assert_eq!(js_to_weights(&s).len(), 4);
199    }
200}