oxihuman_morph/
jaw_rest_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::PI;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct JawRestConfig {
13 pub max_gap_rad: f32,
15 pub micro_relax: bool,
17}
18
19impl Default for JawRestConfig {
20 fn default() -> Self {
21 JawRestConfig {
22 max_gap_rad: PI / 12.0,
23 micro_relax: true,
24 }
25 }
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct JawRestState {
32 gap: f32,
34 lateral: f32,
36 relaxation: f32,
38 config: JawRestConfig,
39}
40
41pub fn default_jaw_rest_config() -> JawRestConfig {
43 JawRestConfig::default()
44}
45
46pub fn new_jaw_rest_state(config: JawRestConfig) -> JawRestState {
48 JawRestState {
49 gap: 0.1,
50 lateral: 0.0,
51 relaxation: 0.5,
52 config,
53 }
54}
55
56pub fn jr_set_gap(state: &mut JawRestState, v: f32) {
58 state.gap = v.clamp(0.0, 1.0);
59}
60
61pub fn jr_set_lateral(state: &mut JawRestState, v: f32) {
63 state.lateral = v.clamp(-1.0, 1.0);
64}
65
66pub fn jr_set_relaxation(state: &mut JawRestState, v: f32) {
68 state.relaxation = v.clamp(0.0, 1.0);
69}
70
71pub fn jr_reset(state: &mut JawRestState) {
73 state.gap = 0.1;
74 state.lateral = 0.0;
75 state.relaxation = 0.5;
76}
77
78pub fn jr_is_neutral(state: &JawRestState) -> bool {
80 (state.gap - 0.1).abs() < 1e-5
81 && state.lateral.abs() < 1e-5
82 && (state.relaxation - 0.5).abs() < 1e-5
83}
84
85pub fn jr_gap_rad(state: &JawRestState) -> f32 {
87 state.gap * state.config.max_gap_rad
88}
89
90pub fn jr_to_weights(state: &JawRestState) -> [f32; 3] {
92 [
93 state.gap,
94 (state.lateral * 0.5 + 0.5).clamp(0.0, 1.0),
95 state.relaxation,
96 ]
97}
98
99pub fn jr_blend(a: &JawRestState, b: &JawRestState, t: f32) -> JawRestState {
101 let t = t.clamp(0.0, 1.0);
102 JawRestState {
103 gap: a.gap + (b.gap - a.gap) * t,
104 lateral: a.lateral + (b.lateral - a.lateral) * t,
105 relaxation: a.relaxation + (b.relaxation - a.relaxation) * t,
106 config: a.config.clone(),
107 }
108}
109
110pub fn jr_to_json(state: &JawRestState) -> String {
112 format!(
113 r#"{{"gap":{:.4},"lateral":{:.4},"relaxation":{:.4}}}"#,
114 state.gap, state.lateral, state.relaxation
115 )
116}
117
118#[cfg(test)]
122mod tests {
123 use super::*;
124
125 fn make() -> JawRestState {
126 new_jaw_rest_state(default_jaw_rest_config())
127 }
128
129 #[test]
130 fn neutral_on_creation() {
131 assert!(jr_is_neutral(&make()));
132 }
133
134 #[test]
135 fn gap_clamped_high() {
136 let mut s = make();
137 jr_set_gap(&mut s, 5.0);
138 assert!((s.gap - 1.0).abs() < 1e-5);
139 }
140
141 #[test]
142 fn lateral_clamped_negative() {
143 let mut s = make();
144 jr_set_lateral(&mut s, -5.0);
145 assert!((s.lateral + 1.0).abs() < 1e-5);
146 }
147
148 #[test]
149 fn reset_restores_neutral() {
150 let mut s = make();
151 jr_set_gap(&mut s, 0.9);
152 jr_reset(&mut s);
153 assert!(jr_is_neutral(&s));
154 }
155
156 #[test]
157 fn gap_rad_positive() {
158 let mut s = make();
159 jr_set_gap(&mut s, 0.5);
160 assert!(jr_gap_rad(&s) > 0.0);
161 }
162
163 #[test]
164 fn weights_in_range() {
165 let s = make();
166 for v in jr_to_weights(&s) {
167 assert!((0.0..=1.0).contains(&v));
168 }
169 }
170
171 #[test]
172 fn blend_midpoint() {
173 let mut b = make();
174 jr_set_gap(&mut b, 1.0);
175 let m = jr_blend(&make(), &b, 0.5);
176 assert!(m.gap > 0.1 && m.gap < 1.0);
178 }
179
180 #[test]
181 fn json_has_gap() {
182 assert!(jr_to_json(&make()).contains("gap"));
183 }
184
185 #[test]
186 fn blend_at_zero_is_a() {
187 let a = make();
188 let r = jr_blend(&a, &make(), 0.0);
189 assert!((r.gap - a.gap).abs() < 1e-5);
190 }
191}