Skip to main content

oxihuman_morph/
neck_crease_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Neck crease / horizontal neck fold control.
6
7/// Crease tier (top = just below jaw, bottom = near clavicle).
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub enum CreaseTier {
11    Top,
12    Middle,
13    Bottom,
14}
15
16/// State.
17#[allow(dead_code)]
18#[derive(Clone, Debug)]
19pub struct NeckCreaseState {
20    pub depth_top: f32,
21    pub depth_middle: f32,
22    pub depth_bottom: f32,
23    /// Vertical spread of creases (0 = tight, 1 = spread).
24    pub vertical_spread: f32,
25}
26
27/// Config.
28#[allow(dead_code)]
29#[derive(Clone, Debug)]
30pub struct NeckCreaseConfig {
31    pub max_depth: f32,
32}
33
34impl Default for NeckCreaseConfig {
35    fn default() -> Self {
36        Self { max_depth: 1.0 }
37    }
38}
39impl Default for NeckCreaseState {
40    fn default() -> Self {
41        Self {
42            depth_top: 0.0,
43            depth_middle: 0.0,
44            depth_bottom: 0.0,
45            vertical_spread: 0.5,
46        }
47    }
48}
49
50#[allow(dead_code)]
51pub fn new_neck_crease_state() -> NeckCreaseState {
52    NeckCreaseState::default()
53}
54
55#[allow(dead_code)]
56pub fn default_neck_crease_config() -> NeckCreaseConfig {
57    NeckCreaseConfig::default()
58}
59
60#[allow(dead_code)]
61pub fn nc_set_depth(state: &mut NeckCreaseState, cfg: &NeckCreaseConfig, tier: CreaseTier, v: f32) {
62    let v = v.clamp(0.0, cfg.max_depth);
63    match tier {
64        CreaseTier::Top => state.depth_top = v,
65        CreaseTier::Middle => state.depth_middle = v,
66        CreaseTier::Bottom => state.depth_bottom = v,
67    }
68}
69
70#[allow(dead_code)]
71pub fn nc_set_spread(state: &mut NeckCreaseState, v: f32) {
72    state.vertical_spread = v.clamp(0.0, 1.0);
73}
74
75#[allow(dead_code)]
76pub fn nc_reset(state: &mut NeckCreaseState) {
77    *state = NeckCreaseState::default();
78}
79
80#[allow(dead_code)]
81pub fn nc_is_neutral(state: &NeckCreaseState) -> bool {
82    state.depth_top < 1e-4 && state.depth_middle < 1e-4 && state.depth_bottom < 1e-4
83}
84
85#[allow(dead_code)]
86pub fn nc_blend(a: &NeckCreaseState, b: &NeckCreaseState, t: f32) -> NeckCreaseState {
87    let t = t.clamp(0.0, 1.0);
88    NeckCreaseState {
89        depth_top: a.depth_top + (b.depth_top - a.depth_top) * t,
90        depth_middle: a.depth_middle + (b.depth_middle - a.depth_middle) * t,
91        depth_bottom: a.depth_bottom + (b.depth_bottom - a.depth_bottom) * t,
92        vertical_spread: a.vertical_spread + (b.vertical_spread - a.vertical_spread) * t,
93    }
94}
95
96#[allow(dead_code)]
97pub fn nc_average_depth(state: &NeckCreaseState) -> f32 {
98    (state.depth_top + state.depth_middle + state.depth_bottom) / 3.0
99}
100
101#[allow(dead_code)]
102pub fn nc_to_weights(state: &NeckCreaseState) -> [f32; 4] {
103    [
104        state.depth_top,
105        state.depth_middle,
106        state.depth_bottom,
107        state.vertical_spread,
108    ]
109}
110
111#[allow(dead_code)]
112pub fn nc_to_json(state: &NeckCreaseState) -> String {
113    format!(
114        "{{\"top\":{:.4},\"mid\":{:.4},\"bot\":{:.4},\"spread\":{:.4}}}",
115        state.depth_top, state.depth_middle, state.depth_bottom, state.vertical_spread
116    )
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn default_neutral() {
125        assert!(nc_is_neutral(&new_neck_crease_state()));
126    }
127
128    #[test]
129    fn depth_clamps_max() {
130        let mut s = new_neck_crease_state();
131        let cfg = default_neck_crease_config();
132        nc_set_depth(&mut s, &cfg, CreaseTier::Top, 5.0);
133        assert!(s.depth_top <= cfg.max_depth);
134    }
135
136    #[test]
137    fn depth_not_negative() {
138        let mut s = new_neck_crease_state();
139        let cfg = default_neck_crease_config();
140        nc_set_depth(&mut s, &cfg, CreaseTier::Middle, -1.0);
141        assert!(s.depth_middle >= 0.0);
142    }
143
144    #[test]
145    fn bottom_tier() {
146        let mut s = new_neck_crease_state();
147        let cfg = default_neck_crease_config();
148        nc_set_depth(&mut s, &cfg, CreaseTier::Bottom, 0.7);
149        assert!((s.depth_bottom - 0.7).abs() < 1e-5);
150    }
151
152    #[test]
153    fn reset_neutral() {
154        let mut s = new_neck_crease_state();
155        let cfg = default_neck_crease_config();
156        nc_set_depth(&mut s, &cfg, CreaseTier::Top, 0.5);
157        nc_reset(&mut s);
158        assert!(nc_is_neutral(&s));
159    }
160
161    #[test]
162    fn blend_midpoint() {
163        let cfg = default_neck_crease_config();
164        let mut a = new_neck_crease_state();
165        let mut b = new_neck_crease_state();
166        nc_set_depth(&mut a, &cfg, CreaseTier::Top, 0.0);
167        nc_set_depth(&mut b, &cfg, CreaseTier::Top, 1.0);
168        let m = nc_blend(&a, &b, 0.5);
169        assert!((m.depth_top - 0.5).abs() < 1e-4);
170    }
171
172    #[test]
173    fn average_depth_zero() {
174        assert!((nc_average_depth(&new_neck_crease_state())).abs() < 1e-5);
175    }
176
177    #[test]
178    fn spread_clamp() {
179        let mut s = new_neck_crease_state();
180        nc_set_spread(&mut s, 5.0);
181        assert!(s.vertical_spread <= 1.0);
182    }
183
184    #[test]
185    fn weights_len() {
186        assert_eq!(nc_to_weights(&new_neck_crease_state()).len(), 4);
187    }
188
189    #[test]
190    fn json_has_spread() {
191        assert!(nc_to_json(&new_neck_crease_state()).contains("spread"));
192    }
193}