Skip to main content

oxihuman_morph/
skin_fold_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan) / SPDX-License-Identifier: Apache-2.0
2#![allow(dead_code)]
3
4//! Skin fold control — crease/fold morph driven by joint proximity.
5
6/// Named joint site where skin folds form.
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum FoldSite {
10    ElbowInner,
11    ElbowOuter,
12    KneeInner,
13    KneeOuter,
14    ArmPit,
15    Groin,
16    NeckBase,
17    WristInner,
18}
19
20/// Config.
21#[allow(dead_code)]
22#[derive(Debug, Clone, PartialEq)]
23pub struct SkinFoldConfig {
24    /// Depth scale: multiplied by fold weight to get displacement.
25    pub depth_scale: f32,
26    /// Width scale for the crease region.
27    pub width_scale: f32,
28}
29
30impl Default for SkinFoldConfig {
31    fn default() -> Self {
32        Self {
33            depth_scale: 0.005,
34            width_scale: 0.012,
35        }
36    }
37}
38
39/// State for all skin fold sites.
40#[allow(dead_code)]
41#[derive(Debug, Clone, Default)]
42pub struct SkinFoldState {
43    folds: Vec<(FoldSite, f32)>,
44}
45
46#[allow(dead_code)]
47pub fn new_skin_fold_state() -> SkinFoldState {
48    SkinFoldState::default()
49}
50
51#[allow(dead_code)]
52pub fn default_skin_fold_config() -> SkinFoldConfig {
53    SkinFoldConfig::default()
54}
55
56#[allow(dead_code)]
57pub fn sf_set(state: &mut SkinFoldState, site: FoldSite, weight: f32) {
58    let weight = weight.clamp(0.0, 1.0);
59    if let Some(entry) = state.folds.iter_mut().find(|(s, _)| *s == site) {
60        entry.1 = weight;
61    } else {
62        state.folds.push((site, weight));
63    }
64}
65
66#[allow(dead_code)]
67pub fn sf_get(state: &SkinFoldState, site: FoldSite) -> f32 {
68    state
69        .folds
70        .iter()
71        .find(|(s, _)| *s == site)
72        .map_or(0.0, |(_, w)| *w)
73}
74
75#[allow(dead_code)]
76pub fn sf_reset(state: &mut SkinFoldState) {
77    state.folds.clear();
78}
79
80#[allow(dead_code)]
81pub fn sf_is_neutral(state: &SkinFoldState) -> bool {
82    state.folds.iter().all(|(_, w)| *w < 1e-4)
83}
84
85/// Active fold count (weight > threshold).
86#[allow(dead_code)]
87pub fn sf_active_count(state: &SkinFoldState) -> usize {
88    state.folds.iter().filter(|(_, w)| *w > 1e-4).count()
89}
90
91/// Depth displacement in metres for a site.
92#[allow(dead_code)]
93pub fn sf_depth_m(state: &SkinFoldState, site: FoldSite, cfg: &SkinFoldConfig) -> f32 {
94    sf_get(state, site) * cfg.depth_scale
95}
96
97/// Width of the crease in metres for a site.
98#[allow(dead_code)]
99pub fn sf_width_m(state: &SkinFoldState, site: FoldSite, cfg: &SkinFoldConfig) -> f32 {
100    sf_get(state, site) * cfg.width_scale
101}
102
103#[allow(dead_code)]
104pub fn sf_blend(a: &SkinFoldState, b: &SkinFoldState, t: f32) -> SkinFoldState {
105    let t = t.clamp(0.0, 1.0);
106    let inv = 1.0 - t;
107    let mut result = SkinFoldState::default();
108    for &(site, wa) in &a.folds {
109        let wb = sf_get(b, site);
110        result.folds.push((site, wa * inv + wb * t));
111    }
112    result
113}
114
115#[allow(dead_code)]
116pub fn sf_to_json(state: &SkinFoldState) -> String {
117    format!(
118        "{{\"site_count\":{},\"active\":{}}}",
119        state.folds.len(),
120        sf_active_count(state)
121    )
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn default_neutral() {
130        assert!(sf_is_neutral(&new_skin_fold_state()));
131    }
132
133    #[test]
134    fn set_and_get() {
135        let mut s = new_skin_fold_state();
136        sf_set(&mut s, FoldSite::ElbowInner, 0.7);
137        assert!((sf_get(&s, FoldSite::ElbowInner) - 0.7).abs() < 1e-6);
138    }
139
140    #[test]
141    fn clamps_high() {
142        let mut s = new_skin_fold_state();
143        sf_set(&mut s, FoldSite::KneeInner, 5.0);
144        assert!((sf_get(&s, FoldSite::KneeInner) - 1.0).abs() < 1e-6);
145    }
146
147    #[test]
148    fn clamps_low() {
149        let mut s = new_skin_fold_state();
150        sf_set(&mut s, FoldSite::KneeOuter, -2.0);
151        assert!(sf_get(&s, FoldSite::KneeOuter) < 1e-6);
152    }
153
154    #[test]
155    fn reset_clears() {
156        let mut s = new_skin_fold_state();
157        sf_set(&mut s, FoldSite::ArmPit, 1.0);
158        sf_reset(&mut s);
159        assert!(sf_is_neutral(&s));
160    }
161
162    #[test]
163    fn active_count() {
164        let mut s = new_skin_fold_state();
165        sf_set(&mut s, FoldSite::Groin, 0.5);
166        sf_set(&mut s, FoldSite::NeckBase, 0.0);
167        assert_eq!(sf_active_count(&s), 1);
168    }
169
170    #[test]
171    fn depth_nonzero_when_active() {
172        let cfg = default_skin_fold_config();
173        let mut s = new_skin_fold_state();
174        sf_set(&mut s, FoldSite::WristInner, 1.0);
175        assert!(sf_depth_m(&s, FoldSite::WristInner, &cfg) > 0.0);
176    }
177
178    #[test]
179    fn blend_midpoint() {
180        let mut a = new_skin_fold_state();
181        sf_set(&mut a, FoldSite::ElbowOuter, 1.0);
182        let b = new_skin_fold_state();
183        let r = sf_blend(&a, &b, 0.5);
184        assert!((sf_get(&r, FoldSite::ElbowOuter) - 0.5).abs() < 1e-5);
185    }
186
187    #[test]
188    fn update_existing_entry() {
189        let mut s = new_skin_fold_state();
190        sf_set(&mut s, FoldSite::ElbowInner, 0.3);
191        sf_set(&mut s, FoldSite::ElbowInner, 0.9);
192        assert_eq!(s.folds.len(), 1);
193    }
194
195    #[test]
196    fn json_has_keys() {
197        let j = sf_to_json(&new_skin_fold_state());
198        assert!(j.contains("site_count") && j.contains("active"));
199    }
200}