oxihuman_morph/
chin_flat_control.rs1#![allow(dead_code)]
3
4use std::f32::consts::FRAC_PI_6;
7
8#[allow(dead_code)]
10#[derive(Debug, Clone, PartialEq)]
11pub struct ChinFlatConfig {
12 pub max_flatten_m: f32,
13}
14
15impl Default for ChinFlatConfig {
16 fn default() -> Self {
17 Self {
18 max_flatten_m: 0.008,
19 }
20 }
21}
22
23#[allow(dead_code)]
25#[derive(Debug, Clone, Default)]
26pub struct ChinFlatState {
27 pub flatten: f32,
29 pub v_bias: f32,
31}
32
33#[allow(dead_code)]
34pub fn new_chin_flat_state() -> ChinFlatState {
35 ChinFlatState::default()
36}
37
38#[allow(dead_code)]
39pub fn default_chin_flat_config() -> ChinFlatConfig {
40 ChinFlatConfig::default()
41}
42
43#[allow(dead_code)]
44pub fn cf_set_flatten(state: &mut ChinFlatState, v: f32) {
45 state.flatten = v.clamp(0.0, 1.0);
46}
47
48#[allow(dead_code)]
49pub fn cf_set_v_bias(state: &mut ChinFlatState, v: f32) {
50 state.v_bias = v.clamp(-1.0, 1.0);
51}
52
53#[allow(dead_code)]
54pub fn cf_reset(state: &mut ChinFlatState) {
55 *state = ChinFlatState::default();
56}
57
58#[allow(dead_code)]
59pub fn cf_is_neutral(state: &ChinFlatState) -> bool {
60 state.flatten < 1e-4 && state.v_bias.abs() < 1e-4
61}
62
63#[allow(dead_code)]
65pub fn cf_angle_rad(state: &ChinFlatState) -> f32 {
66 state.flatten * FRAC_PI_6
67}
68
69#[allow(dead_code)]
71pub fn cf_to_weights(state: &ChinFlatState, cfg: &ChinFlatConfig) -> f32 {
72 state.flatten * cfg.max_flatten_m
73}
74
75#[allow(dead_code)]
77pub fn cf_blend(a: &ChinFlatState, b: &ChinFlatState, t: f32) -> ChinFlatState {
78 let t = t.clamp(0.0, 1.0);
79 let inv = 1.0 - t;
80 ChinFlatState {
81 flatten: a.flatten * inv + b.flatten * t,
82 v_bias: a.v_bias * inv + b.v_bias * t,
83 }
84}
85
86#[allow(dead_code)]
88pub fn cf_to_json(state: &ChinFlatState) -> String {
89 format!(
90 "{{\"flatten\":{:.4},\"v_bias\":{:.4}}}",
91 state.flatten, state.v_bias
92 )
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn default_is_neutral() {
101 assert!(cf_is_neutral(&new_chin_flat_state()));
102 }
103
104 #[test]
105 fn flatten_clamps_high() {
106 let mut s = new_chin_flat_state();
107 cf_set_flatten(&mut s, 2.0);
108 assert!((s.flatten - 1.0).abs() < 1e-6);
109 }
110
111 #[test]
112 fn flatten_clamps_low() {
113 let mut s = new_chin_flat_state();
114 cf_set_flatten(&mut s, -1.0);
115 assert!(s.flatten < 1e-6);
116 }
117
118 #[test]
119 fn v_bias_clamps() {
120 let mut s = new_chin_flat_state();
121 cf_set_v_bias(&mut s, 5.0);
122 assert!((s.v_bias - 1.0).abs() < 1e-6);
123 }
124
125 #[test]
126 fn reset_clears() {
127 let mut s = new_chin_flat_state();
128 cf_set_flatten(&mut s, 0.9);
129 cf_reset(&mut s);
130 assert!(cf_is_neutral(&s));
131 }
132
133 #[test]
134 fn angle_zero_at_neutral() {
135 let s = new_chin_flat_state();
136 assert!(cf_angle_rad(&s).abs() < 1e-6);
137 }
138
139 #[test]
140 fn angle_positive_when_flat() {
141 let mut s = new_chin_flat_state();
142 cf_set_flatten(&mut s, 1.0);
143 assert!(cf_angle_rad(&s) > 0.0);
144 }
145
146 #[test]
147 fn weight_scales_with_config() {
148 let cfg = default_chin_flat_config();
149 let mut s = new_chin_flat_state();
150 cf_set_flatten(&mut s, 1.0);
151 assert!((cf_to_weights(&s, &cfg) - cfg.max_flatten_m).abs() < 1e-6);
152 }
153
154 #[test]
155 fn blend_midpoint() {
156 let mut a = new_chin_flat_state();
157 let b = ChinFlatState {
158 flatten: 1.0,
159 v_bias: 0.0,
160 };
161 cf_set_flatten(&mut a, 0.0);
162 let r = cf_blend(&a, &b, 0.5);
163 assert!((r.flatten - 0.5).abs() < 1e-5);
164 }
165
166 #[test]
167 fn json_contains_flatten() {
168 let j = cf_to_json(&new_chin_flat_state());
169 assert!(j.contains("flatten"));
170 }
171}