Skip to main content

oxihuman_morph/
nasal_root_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Nasal root morph — controls the bridge height and width at the nasal root.
6
7/// Configuration for nasal root control.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct NasalRootConfig {
11    pub max_depth: f32,
12}
13
14/// Runtime state.
15#[allow(dead_code)]
16#[derive(Debug, Clone)]
17pub struct NasalRootState {
18    pub depth: f32,
19    pub width: f32,
20    pub height: f32,
21    pub squish: f32,
22}
23
24#[allow(dead_code)]
25pub fn default_nasal_root_config() -> NasalRootConfig {
26    NasalRootConfig { max_depth: 1.0 }
27}
28
29#[allow(dead_code)]
30pub fn new_nasal_root_state() -> NasalRootState {
31    NasalRootState {
32        depth: 0.0,
33        width: 0.0,
34        height: 0.0,
35        squish: 0.0,
36    }
37}
38
39#[allow(dead_code)]
40pub fn nr_set_depth(state: &mut NasalRootState, cfg: &NasalRootConfig, v: f32) {
41    state.depth = v.clamp(0.0, cfg.max_depth);
42}
43
44#[allow(dead_code)]
45pub fn nr_set_width(state: &mut NasalRootState, cfg: &NasalRootConfig, v: f32) {
46    state.width = v.clamp(0.0, cfg.max_depth);
47}
48
49#[allow(dead_code)]
50pub fn nr_set_height(state: &mut NasalRootState, v: f32) {
51    state.height = v.clamp(-1.0, 1.0);
52}
53
54#[allow(dead_code)]
55pub fn nr_set_squish(state: &mut NasalRootState, v: f32) {
56    state.squish = v.clamp(0.0, 1.0);
57}
58
59#[allow(dead_code)]
60pub fn nr_reset(state: &mut NasalRootState) {
61    *state = new_nasal_root_state();
62}
63
64#[allow(dead_code)]
65pub fn nr_is_neutral(state: &NasalRootState) -> bool {
66    state.depth.abs() < 1e-6
67        && state.width.abs() < 1e-6
68        && state.height.abs() < 1e-6
69        && state.squish.abs() < 1e-6
70}
71
72#[allow(dead_code)]
73pub fn nr_bridge_prominence(state: &NasalRootState) -> f32 {
74    (state.depth + state.height.max(0.0)) * 0.5
75}
76
77#[allow(dead_code)]
78pub fn nr_blend(a: &NasalRootState, b: &NasalRootState, t: f32) -> NasalRootState {
79    let t = t.clamp(0.0, 1.0);
80    NasalRootState {
81        depth: a.depth + (b.depth - a.depth) * t,
82        width: a.width + (b.width - a.width) * t,
83        height: a.height + (b.height - a.height) * t,
84        squish: a.squish + (b.squish - a.squish) * t,
85    }
86}
87
88#[allow(dead_code)]
89pub fn nr_to_weights(state: &NasalRootState) -> Vec<(String, f32)> {
90    vec![
91        ("nasal_root_depth".to_string(), state.depth),
92        ("nasal_root_width".to_string(), state.width),
93        ("nasal_root_height".to_string(), state.height),
94        ("nasal_root_squish".to_string(), state.squish),
95    ]
96}
97
98#[allow(dead_code)]
99pub fn nr_to_json(state: &NasalRootState) -> String {
100    format!(
101        r#"{{"depth":{:.4},"width":{:.4},"height":{:.4},"squish":{:.4}}}"#,
102        state.depth, state.width, state.height, state.squish
103    )
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn default_config() {
112        let cfg = default_nasal_root_config();
113        assert!((cfg.max_depth - 1.0).abs() < 1e-6);
114    }
115
116    #[test]
117    fn new_state_neutral() {
118        let s = new_nasal_root_state();
119        assert!(nr_is_neutral(&s));
120    }
121
122    #[test]
123    fn set_depth_clamps() {
124        let cfg = default_nasal_root_config();
125        let mut s = new_nasal_root_state();
126        nr_set_depth(&mut s, &cfg, 5.0);
127        assert!((s.depth - 1.0).abs() < 1e-6);
128    }
129
130    #[test]
131    fn set_height_signed() {
132        let mut s = new_nasal_root_state();
133        nr_set_height(&mut s, -0.5);
134        assert!((s.height + 0.5).abs() < 1e-6);
135    }
136
137    #[test]
138    fn set_squish_clamps() {
139        let mut s = new_nasal_root_state();
140        nr_set_squish(&mut s, 5.0);
141        assert!((s.squish - 1.0).abs() < 1e-6);
142    }
143
144    #[test]
145    fn bridge_prominence_zero_at_neutral() {
146        let s = new_nasal_root_state();
147        assert!(nr_bridge_prominence(&s) < 1e-6);
148    }
149
150    #[test]
151    fn reset_clears() {
152        let cfg = default_nasal_root_config();
153        let mut s = new_nasal_root_state();
154        nr_set_depth(&mut s, &cfg, 0.5);
155        nr_reset(&mut s);
156        assert!(nr_is_neutral(&s));
157    }
158
159    #[test]
160    fn blend_midpoint() {
161        let a = new_nasal_root_state();
162        let cfg = default_nasal_root_config();
163        let mut b = new_nasal_root_state();
164        nr_set_depth(&mut b, &cfg, 1.0);
165        let mid = nr_blend(&a, &b, 0.5);
166        assert!((mid.depth - 0.5).abs() < 1e-6);
167    }
168
169    #[test]
170    fn to_weights_count() {
171        let s = new_nasal_root_state();
172        assert_eq!(nr_to_weights(&s).len(), 4);
173    }
174
175    #[test]
176    fn to_json_fields() {
177        let s = new_nasal_root_state();
178        let j = nr_to_json(&s);
179        assert!(j.contains("depth"));
180        assert!(j.contains("squish"));
181    }
182}