oxihuman_morph/
jaw_protrusion_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_8;
8
9#[allow(dead_code)]
11#[derive(Clone, Debug)]
12pub struct JawProtrusionState {
13 pub protrusion: f32,
15 pub mandibular_plane_deg: f32,
17 pub lateral_shift: f32,
19}
20
21#[allow(dead_code)]
23#[derive(Clone, Debug)]
24pub struct JawProtrusionConfig {
25 pub max_protrusion: f32,
26 pub max_plane_deg: f32,
27}
28
29impl Default for JawProtrusionConfig {
30 fn default() -> Self {
31 Self {
32 max_protrusion: 1.0,
33 max_plane_deg: 15.0,
34 }
35 }
36}
37impl Default for JawProtrusionState {
38 fn default() -> Self {
39 Self {
40 protrusion: 0.0,
41 mandibular_plane_deg: 0.0,
42 lateral_shift: 0.0,
43 }
44 }
45}
46
47#[allow(dead_code)]
48pub fn new_jaw_protrusion_state() -> JawProtrusionState {
49 JawProtrusionState::default()
50}
51
52#[allow(dead_code)]
53pub fn default_jaw_protrusion_config() -> JawProtrusionConfig {
54 JawProtrusionConfig::default()
55}
56
57#[allow(dead_code)]
58pub fn jp_set_protrusion(state: &mut JawProtrusionState, cfg: &JawProtrusionConfig, v: f32) {
59 state.protrusion = v.clamp(-cfg.max_protrusion, cfg.max_protrusion);
60}
61
62#[allow(dead_code)]
63pub fn jp_set_plane(state: &mut JawProtrusionState, cfg: &JawProtrusionConfig, deg: f32) {
64 state.mandibular_plane_deg = deg.clamp(-cfg.max_plane_deg, cfg.max_plane_deg);
65}
66
67#[allow(dead_code)]
68pub fn jp_set_lateral(state: &mut JawProtrusionState, v: f32) {
69 state.lateral_shift = v.clamp(-1.0, 1.0);
70}
71
72#[allow(dead_code)]
73pub fn jp_reset(state: &mut JawProtrusionState) {
74 *state = JawProtrusionState::default();
75}
76
77#[allow(dead_code)]
78pub fn jp_is_neutral(state: &JawProtrusionState) -> bool {
79 state.protrusion.abs() < 1e-4
80 && state.mandibular_plane_deg.abs() < 1e-4
81 && state.lateral_shift.abs() < 1e-4
82}
83
84#[allow(dead_code)]
85pub fn jp_blend(a: &JawProtrusionState, b: &JawProtrusionState, t: f32) -> JawProtrusionState {
86 let t = t.clamp(0.0, 1.0);
87 JawProtrusionState {
88 protrusion: a.protrusion + (b.protrusion - a.protrusion) * t,
89 mandibular_plane_deg: a.mandibular_plane_deg
90 + (b.mandibular_plane_deg - a.mandibular_plane_deg) * t,
91 lateral_shift: a.lateral_shift + (b.lateral_shift - a.lateral_shift) * t,
92 }
93}
94
95#[allow(dead_code)]
97pub fn jp_horizontal_offset(state: &JawProtrusionState) -> f32 {
98 state.protrusion * (state.mandibular_plane_deg.to_radians() + FRAC_PI_8).cos()
99}
100
101#[allow(dead_code)]
102pub fn jp_to_weights(state: &JawProtrusionState) -> [f32; 3] {
103 [
104 state.protrusion,
105 state.mandibular_plane_deg / 15.0,
106 state.lateral_shift,
107 ]
108}
109
110#[allow(dead_code)]
111pub fn jp_to_json(state: &JawProtrusionState) -> String {
112 format!(
113 "{{\"protrusion\":{:.4},\"plane_deg\":{:.4},\"lateral\":{:.4}}}",
114 state.protrusion, state.mandibular_plane_deg, state.lateral_shift
115 )
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn default_neutral() {
124 assert!(jp_is_neutral(&new_jaw_protrusion_state()));
125 }
126
127 #[test]
128 fn protrusion_clamp_max() {
129 let mut s = new_jaw_protrusion_state();
130 let cfg = default_jaw_protrusion_config();
131 jp_set_protrusion(&mut s, &cfg, 5.0);
132 assert!(s.protrusion <= cfg.max_protrusion);
133 }
134
135 #[test]
136 fn protrusion_clamp_min() {
137 let mut s = new_jaw_protrusion_state();
138 let cfg = default_jaw_protrusion_config();
139 jp_set_protrusion(&mut s, &cfg, -5.0);
140 assert!(s.protrusion >= -cfg.max_protrusion);
141 }
142
143 #[test]
144 fn plane_clamp() {
145 let mut s = new_jaw_protrusion_state();
146 let cfg = default_jaw_protrusion_config();
147 jp_set_plane(&mut s, &cfg, 999.0);
148 assert!(s.mandibular_plane_deg <= cfg.max_plane_deg);
149 }
150
151 #[test]
152 fn lateral_clamp() {
153 let mut s = new_jaw_protrusion_state();
154 jp_set_lateral(&mut s, 5.0);
155 assert!(s.lateral_shift <= 1.0);
156 }
157
158 #[test]
159 fn reset_neutral() {
160 let mut s = new_jaw_protrusion_state();
161 let cfg = default_jaw_protrusion_config();
162 jp_set_protrusion(&mut s, &cfg, 0.5);
163 jp_reset(&mut s);
164 assert!(jp_is_neutral(&s));
165 }
166
167 #[test]
168 fn blend_half() {
169 let cfg = default_jaw_protrusion_config();
170 let mut a = new_jaw_protrusion_state();
171 let mut b = new_jaw_protrusion_state();
172 jp_set_protrusion(&mut a, &cfg, 0.0);
173 jp_set_protrusion(&mut b, &cfg, 1.0);
174 let m = jp_blend(&a, &b, 0.5);
175 assert!((m.protrusion - 0.5).abs() < 1e-4);
176 }
177
178 #[test]
179 fn horizontal_offset_finite() {
180 let s = new_jaw_protrusion_state();
181 assert!(jp_horizontal_offset(&s).is_finite());
182 }
183
184 #[test]
185 fn weights_len() {
186 assert_eq!(jp_to_weights(&new_jaw_protrusion_state()).len(), 3);
187 }
188
189 #[test]
190 fn json_has_protrusion() {
191 assert!(jp_to_json(&new_jaw_protrusion_state()).contains("protrusion"));
192 }
193}