Skip to main content

oxihuman_morph/
ear_lobe_size.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Ear lobe size control — volume and pendulousness of the ear lobe.
5
6/// Ear side.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum EarSide {
10    Left,
11    Right,
12}
13
14/// Config.
15#[allow(dead_code)]
16#[derive(Debug, Clone, PartialEq)]
17pub struct EarLobeSizeConfig {
18    pub max_size_m: f32,
19    pub max_droop_m: f32,
20}
21
22impl Default for EarLobeSizeConfig {
23    fn default() -> Self {
24        Self {
25            max_size_m: 0.010,
26            max_droop_m: 0.008,
27        }
28    }
29}
30
31/// State.
32#[allow(dead_code)]
33#[derive(Debug, Clone, Default)]
34pub struct EarLobeSizeState {
35    pub left_size: f32,
36    pub right_size: f32,
37    pub left_droop: f32,
38    pub right_droop: f32,
39}
40
41#[allow(dead_code)]
42pub fn new_ear_lobe_size_state() -> EarLobeSizeState {
43    EarLobeSizeState::default()
44}
45
46#[allow(dead_code)]
47pub fn default_ear_lobe_size_config() -> EarLobeSizeConfig {
48    EarLobeSizeConfig::default()
49}
50
51#[allow(dead_code)]
52pub fn els_set_size(state: &mut EarLobeSizeState, side: EarSide, v: f32) {
53    let v = v.clamp(0.0, 1.0);
54    match side {
55        EarSide::Left => state.left_size = v,
56        EarSide::Right => state.right_size = v,
57    }
58}
59
60#[allow(dead_code)]
61pub fn els_set_droop(state: &mut EarLobeSizeState, side: EarSide, v: f32) {
62    let v = v.clamp(0.0, 1.0);
63    match side {
64        EarSide::Left => state.left_droop = v,
65        EarSide::Right => state.right_droop = v,
66    }
67}
68
69#[allow(dead_code)]
70pub fn els_set_both_size(state: &mut EarLobeSizeState, v: f32) {
71    let v = v.clamp(0.0, 1.0);
72    state.left_size = v;
73    state.right_size = v;
74}
75
76#[allow(dead_code)]
77pub fn els_reset(state: &mut EarLobeSizeState) {
78    *state = EarLobeSizeState::default();
79}
80
81#[allow(dead_code)]
82pub fn els_is_neutral(state: &EarLobeSizeState) -> bool {
83    state.left_size < 1e-4
84        && state.right_size < 1e-4
85        && state.left_droop < 1e-4
86        && state.right_droop < 1e-4
87}
88
89#[allow(dead_code)]
90pub fn els_symmetry(state: &EarLobeSizeState) -> f32 {
91    1.0 - (state.left_size - state.right_size).abs()
92}
93
94#[allow(dead_code)]
95pub fn els_to_weights(state: &EarLobeSizeState, cfg: &EarLobeSizeConfig) -> [f32; 4] {
96    [
97        state.left_size * cfg.max_size_m,
98        state.right_size * cfg.max_size_m,
99        state.left_droop * cfg.max_droop_m,
100        state.right_droop * cfg.max_droop_m,
101    ]
102}
103
104#[allow(dead_code)]
105pub fn els_blend(a: &EarLobeSizeState, b: &EarLobeSizeState, t: f32) -> EarLobeSizeState {
106    let t = t.clamp(0.0, 1.0);
107    let inv = 1.0 - t;
108    EarLobeSizeState {
109        left_size: a.left_size * inv + b.left_size * t,
110        right_size: a.right_size * inv + b.right_size * t,
111        left_droop: a.left_droop * inv + b.left_droop * t,
112        right_droop: a.right_droop * inv + b.right_droop * t,
113    }
114}
115
116#[allow(dead_code)]
117pub fn els_to_json(state: &EarLobeSizeState) -> String {
118    format!(
119        "{{\"left_size\":{:.4},\"right_size\":{:.4},\"left_droop\":{:.4},\"right_droop\":{:.4}}}",
120        state.left_size, state.right_size, state.left_droop, state.right_droop
121    )
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_is_neutral() {
130        assert!(els_is_neutral(&new_ear_lobe_size_state()));
131    }
132
133    #[test]
134    fn size_clamps() {
135        let mut s = new_ear_lobe_size_state();
136        els_set_size(&mut s, EarSide::Left, 5.0);
137        assert!((s.left_size - 1.0).abs() < 1e-6);
138    }
139
140    #[test]
141    fn droop_clamps() {
142        let mut s = new_ear_lobe_size_state();
143        els_set_droop(&mut s, EarSide::Right, -2.0);
144        assert!(s.right_droop < 1e-6);
145    }
146
147    #[test]
148    fn reset_clears() {
149        let mut s = new_ear_lobe_size_state();
150        els_set_both_size(&mut s, 0.8);
151        els_reset(&mut s);
152        assert!(els_is_neutral(&s));
153    }
154
155    #[test]
156    fn symmetry_one_when_equal() {
157        let mut s = new_ear_lobe_size_state();
158        els_set_both_size(&mut s, 0.5);
159        assert!((els_symmetry(&s) - 1.0).abs() < 1e-6);
160    }
161
162    #[test]
163    fn symmetry_less_when_asymmetric() {
164        let mut s = new_ear_lobe_size_state();
165        els_set_size(&mut s, EarSide::Left, 1.0);
166        assert!(els_symmetry(&s) < 1.0);
167    }
168
169    #[test]
170    fn weights_four_values() {
171        let w = els_to_weights(&new_ear_lobe_size_state(), &default_ear_lobe_size_config());
172        assert_eq!(w.len(), 4);
173    }
174
175    #[test]
176    fn blend_midpoint() {
177        let mut b = new_ear_lobe_size_state();
178        els_set_both_size(&mut b, 1.0);
179        let r = els_blend(&new_ear_lobe_size_state(), &b, 0.5);
180        assert!((r.left_size - 0.5).abs() < 1e-5);
181    }
182
183    #[test]
184    fn json_has_keys() {
185        let j = els_to_json(&new_ear_lobe_size_state());
186        assert!(j.contains("left_size"));
187    }
188
189    #[test]
190    fn set_both_size_equal() {
191        let mut s = new_ear_lobe_size_state();
192        els_set_both_size(&mut s, 0.6);
193        assert!((s.left_size - s.right_size).abs() < 1e-6);
194    }
195}