oxihuman_morph/
chin_pad_control.rs1#![allow(dead_code)]
4
5use std::f32::consts::FRAC_PI_6;
8
9#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct ChinPadConfig {
13 pub ref_arc_rad: f32,
15 pub max_projection: f32,
17}
18
19impl Default for ChinPadConfig {
20 fn default() -> Self {
21 ChinPadConfig {
22 ref_arc_rad: FRAC_PI_6,
23 max_projection: 1.0,
24 }
25 }
26}
27
28#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct ChinPadState {
32 volume: f32,
34 projection: f32,
36 spread: f32,
38 config: ChinPadConfig,
39}
40
41pub fn default_chin_pad_config() -> ChinPadConfig {
43 ChinPadConfig::default()
44}
45
46pub fn new_chin_pad_state(config: ChinPadConfig) -> ChinPadState {
48 ChinPadState {
49 volume: 0.0,
50 projection: 0.0,
51 spread: 0.0,
52 config,
53 }
54}
55
56pub fn cpd_set_volume(state: &mut ChinPadState, v: f32) {
58 state.volume = v.clamp(0.0, 1.0);
59}
60
61pub fn cpd_set_projection(state: &mut ChinPadState, v: f32) {
63 state.projection = v.clamp(0.0, 1.0);
64}
65
66pub fn cpd_set_spread(state: &mut ChinPadState, v: f32) {
68 state.spread = v.clamp(0.0, 1.0);
69}
70
71pub fn cpd_reset(state: &mut ChinPadState) {
73 state.volume = 0.0;
74 state.projection = 0.0;
75 state.spread = 0.0;
76}
77
78pub fn cpd_is_neutral(state: &ChinPadState) -> bool {
80 state.volume < 1e-5 && state.projection < 1e-5 && state.spread < 1e-5
81}
82
83pub fn cpd_pad_size(state: &ChinPadState) -> f32 {
85 (state.volume * 0.6 + state.projection * 0.4).clamp(0.0, 1.0)
86}
87
88pub fn cpd_to_weights(state: &ChinPadState) -> [f32; 3] {
90 [state.volume, state.projection, state.spread]
91}
92
93pub fn cpd_blend(a: &ChinPadState, b: &ChinPadState, t: f32) -> ChinPadState {
95 let t = t.clamp(0.0, 1.0);
96 ChinPadState {
97 volume: a.volume + (b.volume - a.volume) * t,
98 projection: a.projection + (b.projection - a.projection) * t,
99 spread: a.spread + (b.spread - a.spread) * t,
100 config: a.config.clone(),
101 }
102}
103
104pub fn cpd_to_json(state: &ChinPadState) -> String {
106 format!(
107 r#"{{"volume":{:.4},"projection":{:.4},"spread":{:.4}}}"#,
108 state.volume, state.projection, state.spread
109 )
110}
111
112#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn make() -> ChinPadState {
120 new_chin_pad_state(default_chin_pad_config())
121 }
122
123 #[test]
124 fn neutral_on_creation() {
125 assert!(cpd_is_neutral(&make()));
126 }
127
128 #[test]
129 fn set_volume_clamps() {
130 let mut s = make();
131 cpd_set_volume(&mut s, 2.0);
132 assert!((s.volume - 1.0).abs() < 1e-5);
133 }
134
135 #[test]
136 fn reset_zeros_all() {
137 let mut s = make();
138 cpd_set_volume(&mut s, 0.5);
139 cpd_reset(&mut s);
140 assert!(cpd_is_neutral(&s));
141 }
142
143 #[test]
144 fn pad_size_in_range() {
145 let mut s = make();
146 cpd_set_volume(&mut s, 0.5);
147 cpd_set_projection(&mut s, 0.5);
148 assert!((0.0..=1.0).contains(&cpd_pad_size(&s)));
149 }
150
151 #[test]
152 fn weights_all_in_range() {
153 let mut s = make();
154 cpd_set_volume(&mut s, 0.3);
155 cpd_set_projection(&mut s, 0.7);
156 for v in cpd_to_weights(&s) {
157 assert!((0.0..=1.0).contains(&v));
158 }
159 }
160
161 #[test]
162 fn blend_midpoint() {
163 let mut b = make();
164 cpd_set_volume(&mut b, 1.0);
165 let m = cpd_blend(&make(), &b, 0.5);
166 assert!((m.volume - 0.5).abs() < 1e-5);
167 }
168
169 #[test]
170 fn blend_at_zero_is_a() {
171 let a = make();
172 let m = cpd_blend(&a, &make(), 0.0);
173 assert!((m.volume - a.volume).abs() < 1e-5);
174 }
175
176 #[test]
177 fn json_has_volume() {
178 assert!(cpd_to_json(&make()).contains("volume"));
179 }
180
181 #[test]
182 fn spread_clamped_negative() {
183 let mut s = make();
184 cpd_set_spread(&mut s, -5.0);
185 assert!(s.spread >= 0.0);
186 }
187}