Skip to main content

oxihuman_morph/
neck_thickness_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Neck width/thickness morph control.
5
6#![allow(dead_code)]
7
8use std::f32::consts::PI;
9
10#[allow(dead_code)]
11#[derive(Debug, Clone)]
12pub struct NeckThicknessConfig {
13    pub min_radius: f32,
14    pub max_radius: f32,
15}
16
17#[allow(dead_code)]
18#[derive(Debug, Clone)]
19pub struct NeckThicknessState {
20    pub width: f32,
21    pub depth: f32,
22    pub length: f32,
23}
24
25#[allow(dead_code)]
26pub fn default_neck_thickness_config() -> NeckThicknessConfig {
27    NeckThicknessConfig {
28        min_radius: 0.1,
29        max_radius: 1.0,
30    }
31}
32
33#[allow(dead_code)]
34pub fn new_neck_thickness_state() -> NeckThicknessState {
35    NeckThicknessState {
36        width: 0.4,
37        depth: 0.4,
38        length: 0.5,
39    }
40}
41
42#[allow(dead_code)]
43pub fn neck_set_width(state: &mut NeckThicknessState, cfg: &NeckThicknessConfig, value: f32) {
44    state.width = value.clamp(cfg.min_radius, cfg.max_radius);
45}
46
47#[allow(dead_code)]
48pub fn neck_set_depth(state: &mut NeckThicknessState, cfg: &NeckThicknessConfig, value: f32) {
49    state.depth = value.clamp(cfg.min_radius, cfg.max_radius);
50}
51
52#[allow(dead_code)]
53pub fn neck_set_length(state: &mut NeckThicknessState, value: f32) {
54    state.length = value.clamp(0.0, 1.0);
55}
56
57#[allow(dead_code)]
58pub fn neck_reset(state: &mut NeckThicknessState) {
59    *state = new_neck_thickness_state();
60}
61
62#[allow(dead_code)]
63pub fn neck_to_weights(state: &NeckThicknessState) -> Vec<(String, f32)> {
64    vec![
65        ("neck_width".to_string(), state.width),
66        ("neck_depth".to_string(), state.depth),
67        ("neck_length".to_string(), state.length),
68    ]
69}
70
71#[allow(dead_code)]
72pub fn neck_to_json(state: &NeckThicknessState) -> String {
73    format!(
74        r#"{{"width":{:.4},"depth":{:.4},"length":{:.4}}}"#,
75        state.width, state.depth, state.length
76    )
77}
78
79#[allow(dead_code)]
80pub fn neck_clamp(state: &mut NeckThicknessState, cfg: &NeckThicknessConfig) {
81    state.width = state.width.clamp(cfg.min_radius, cfg.max_radius);
82    state.depth = state.depth.clamp(cfg.min_radius, cfg.max_radius);
83    state.length = state.length.clamp(0.0, 1.0);
84}
85
86/// Approximate volume of neck as an elliptic cylinder (V = π * a * b * h).
87#[allow(dead_code)]
88pub fn neck_compute_volume(state: &NeckThicknessState, height_m: f32) -> f32 {
89    PI * state.width * state.depth * (state.length * height_m)
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_default_config() {
98        let cfg = default_neck_thickness_config();
99        assert!((cfg.min_radius - 0.1).abs() < 1e-6);
100        assert_eq!(cfg.max_radius, 1.0);
101    }
102
103    #[test]
104    fn test_new_state_defaults() {
105        let s = new_neck_thickness_state();
106        assert!((s.width - 0.4).abs() < 1e-6);
107        assert!((s.length - 0.5).abs() < 1e-6);
108    }
109
110    #[test]
111    fn test_set_width_clamps() {
112        let cfg = default_neck_thickness_config();
113        let mut s = new_neck_thickness_state();
114        neck_set_width(&mut s, &cfg, 0.0);
115        assert!((s.width - cfg.min_radius).abs() < 1e-6);
116        neck_set_width(&mut s, &cfg, 5.0);
117        assert_eq!(s.width, cfg.max_radius);
118    }
119
120    #[test]
121    fn test_set_depth_clamps() {
122        let cfg = default_neck_thickness_config();
123        let mut s = new_neck_thickness_state();
124        neck_set_depth(&mut s, &cfg, 0.6);
125        assert!((s.depth - 0.6).abs() < 1e-6);
126    }
127
128    #[test]
129    fn test_set_length_clamps() {
130        let mut s = new_neck_thickness_state();
131        neck_set_length(&mut s, 2.0);
132        assert_eq!(s.length, 1.0);
133        neck_set_length(&mut s, -1.0);
134        assert_eq!(s.length, 0.0);
135    }
136
137    #[test]
138    fn test_reset() {
139        let cfg = default_neck_thickness_config();
140        let mut s = new_neck_thickness_state();
141        neck_set_width(&mut s, &cfg, 0.9);
142        neck_reset(&mut s);
143        assert!((s.width - 0.4).abs() < 1e-6);
144    }
145
146    #[test]
147    fn test_to_weights_count() {
148        let s = new_neck_thickness_state();
149        assert_eq!(neck_to_weights(&s).len(), 3);
150    }
151
152    #[test]
153    fn test_to_json_has_keys() {
154        let s = new_neck_thickness_state();
155        let j = neck_to_json(&s);
156        assert!(j.contains("width"));
157        assert!(j.contains("length"));
158    }
159
160    #[test]
161    fn test_compute_volume_positive() {
162        let s = new_neck_thickness_state();
163        let v = neck_compute_volume(&s, 0.15);
164        assert!(v > 0.0);
165    }
166
167    #[test]
168    fn test_clamp_enforces_bounds() {
169        let cfg = default_neck_thickness_config();
170        let mut s = NeckThicknessState {
171            width: 0.0,
172            depth: 5.0,
173            length: -1.0,
174        };
175        neck_clamp(&mut s, &cfg);
176        assert!((s.width - cfg.min_radius).abs() < 1e-6);
177        assert_eq!(s.depth, cfg.max_radius);
178        assert_eq!(s.length, 0.0);
179    }
180}