Skip to main content

oxihuman_morph/
chin_pad_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Chin pad control — soft-tissue padding / volume at the chin tip.
6
7use std::f32::consts::FRAC_PI_6;
8
9/// Configuration for chin pad.
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct ChinPadConfig {
13    /// Reference angle (informational, used in arc approximation).
14    pub ref_arc_rad: f32,
15    /// Maximum allowed projection.
16    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/// Runtime state for chin pad control.
29#[allow(dead_code)]
30#[derive(Debug, Clone)]
31pub struct ChinPadState {
32    /// Pad volume in `[0.0, 1.0]`.
33    volume: f32,
34    /// Downward projection in `[0.0, 1.0]`.
35    projection: f32,
36    /// Lateral spread in `[0.0, 1.0]`.
37    spread: f32,
38    config: ChinPadConfig,
39}
40
41/// Default config.
42pub fn default_chin_pad_config() -> ChinPadConfig {
43    ChinPadConfig::default()
44}
45
46/// Create a neutral state.
47pub 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
56/// Set the pad volume.
57pub fn cpd_set_volume(state: &mut ChinPadState, v: f32) {
58    state.volume = v.clamp(0.0, 1.0);
59}
60
61/// Set the downward projection.
62pub fn cpd_set_projection(state: &mut ChinPadState, v: f32) {
63    state.projection = v.clamp(0.0, 1.0);
64}
65
66/// Set the lateral spread.
67pub fn cpd_set_spread(state: &mut ChinPadState, v: f32) {
68    state.spread = v.clamp(0.0, 1.0);
69}
70
71/// Reset all to zero.
72pub fn cpd_reset(state: &mut ChinPadState) {
73    state.volume = 0.0;
74    state.projection = 0.0;
75    state.spread = 0.0;
76}
77
78/// True when state is effectively zero.
79pub fn cpd_is_neutral(state: &ChinPadState) -> bool {
80    state.volume < 1e-5 && state.projection < 1e-5 && state.spread < 1e-5
81}
82
83/// Combine volume and projection into an overall pad size.
84pub fn cpd_pad_size(state: &ChinPadState) -> f32 {
85    (state.volume * 0.6 + state.projection * 0.4).clamp(0.0, 1.0)
86}
87
88/// Produce morph weights: `[volume, projection, spread]`.
89pub fn cpd_to_weights(state: &ChinPadState) -> [f32; 3] {
90    [state.volume, state.projection, state.spread]
91}
92
93/// Blend between two states.
94pub 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
104/// Serialise.
105pub 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// ---------------------------------------------------------------------------
113// Tests
114// ---------------------------------------------------------------------------
115#[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}