Skip to main content

oxihuman_morph/
body_hair_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Body hair density and length parameters.
6
7/// Body region for hair distribution.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum BodyHairRegion {
11    Arms,
12    Legs,
13    Chest,
14    Abdomen,
15    Back,
16    Shoulders,
17}
18
19impl BodyHairRegion {
20    #[allow(dead_code)]
21    pub fn name(self) -> &'static str {
22        match self {
23            BodyHairRegion::Arms => "arms",
24            BodyHairRegion::Legs => "legs",
25            BodyHairRegion::Chest => "chest",
26            BodyHairRegion::Abdomen => "abdomen",
27            BodyHairRegion::Back => "back",
28            BodyHairRegion::Shoulders => "shoulders",
29        }
30    }
31}
32
33/// Per-region body hair entry.
34#[allow(dead_code)]
35#[derive(Debug, Clone)]
36pub struct BodyHairEntry {
37    pub region: BodyHairRegion,
38    pub density: f32,
39    pub length: f32,
40    pub color_darkness: f32,
41}
42
43impl BodyHairEntry {
44    #[allow(dead_code)]
45    pub fn new(region: BodyHairRegion) -> Self {
46        BodyHairEntry {
47            region,
48            density: 0.0,
49            length: 0.0,
50            color_darkness: 0.5,
51        }
52    }
53}
54
55/// Overall body hair state.
56#[allow(dead_code)]
57#[derive(Debug, Clone)]
58pub struct BodyHairState {
59    pub entries: Vec<BodyHairEntry>,
60    pub global_density: f32,
61}
62
63#[allow(dead_code)]
64pub fn default_body_hair_state() -> BodyHairState {
65    let entries = vec![
66        BodyHairEntry::new(BodyHairRegion::Arms),
67        BodyHairEntry::new(BodyHairRegion::Legs),
68        BodyHairEntry::new(BodyHairRegion::Chest),
69        BodyHairEntry::new(BodyHairRegion::Abdomen),
70        BodyHairEntry::new(BodyHairRegion::Back),
71        BodyHairEntry::new(BodyHairRegion::Shoulders),
72    ];
73    BodyHairState {
74        entries,
75        global_density: 0.0,
76    }
77}
78
79#[allow(dead_code)]
80pub fn bh_set_global(state: &mut BodyHairState, v: f32) {
81    let v = v.clamp(0.0, 1.0);
82    state.global_density = v;
83    for e in &mut state.entries {
84        e.density = v;
85    }
86}
87
88#[allow(dead_code)]
89pub fn bh_set_region_density(state: &mut BodyHairState, region: BodyHairRegion, v: f32) {
90    for e in &mut state.entries {
91        if e.region == region {
92            e.density = v.clamp(0.0, 1.0);
93            return;
94        }
95    }
96}
97
98#[allow(dead_code)]
99pub fn bh_set_region_length(state: &mut BodyHairState, region: BodyHairRegion, v: f32) {
100    for e in &mut state.entries {
101        if e.region == region {
102            e.length = v.clamp(0.0, 1.0);
103            return;
104        }
105    }
106}
107
108#[allow(dead_code)]
109pub fn bh_reset(state: &mut BodyHairState) {
110    for e in &mut state.entries {
111        e.density = 0.0;
112        e.length = 0.0;
113        e.color_darkness = 0.5;
114    }
115    state.global_density = 0.0;
116}
117
118#[allow(dead_code)]
119pub fn bh_is_smooth(state: &BodyHairState) -> bool {
120    state.entries.iter().all(|e| e.density < 1e-6)
121}
122
123#[allow(dead_code)]
124pub fn bh_average_density(state: &BodyHairState) -> f32 {
125    if state.entries.is_empty() {
126        return 0.0;
127    }
128    let sum: f32 = state.entries.iter().map(|e| e.density).sum();
129    sum / state.entries.len() as f32
130}
131
132#[allow(dead_code)]
133pub fn bh_to_json(state: &BodyHairState) -> String {
134    format!(
135        r#"{{"global_density":{:.4},"region_count":{}}}"#,
136        state.global_density,
137        state.entries.len()
138    )
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn default_is_smooth() {
147        assert!(bh_is_smooth(&default_body_hair_state()));
148    }
149
150    #[test]
151    fn set_global_changes_all() {
152        let mut s = default_body_hair_state();
153        bh_set_global(&mut s, 0.5);
154        assert!(!bh_is_smooth(&s));
155    }
156
157    #[test]
158    fn set_region_density_clamps() {
159        let mut s = default_body_hair_state();
160        bh_set_region_density(&mut s, BodyHairRegion::Chest, 5.0);
161        let e = s
162            .entries
163            .iter()
164            .find(|e| e.region == BodyHairRegion::Chest)
165            .expect("should succeed");
166        assert!((e.density - 1.0).abs() < 1e-6);
167    }
168
169    #[test]
170    fn set_region_length() {
171        let mut s = default_body_hair_state();
172        bh_set_region_length(&mut s, BodyHairRegion::Arms, 0.6);
173        let e = s
174            .entries
175            .iter()
176            .find(|e| e.region == BodyHairRegion::Arms)
177            .expect("should succeed");
178        assert!((e.length - 0.6).abs() < 1e-5);
179    }
180
181    #[test]
182    fn reset_clears() {
183        let mut s = default_body_hair_state();
184        bh_set_global(&mut s, 1.0);
185        bh_reset(&mut s);
186        assert!(bh_is_smooth(&s));
187    }
188
189    #[test]
190    fn average_density_zero_by_default() {
191        assert!(bh_average_density(&default_body_hair_state()).abs() < 1e-6);
192    }
193
194    #[test]
195    fn average_density_after_global() {
196        let mut s = default_body_hair_state();
197        bh_set_global(&mut s, 0.4);
198        assert!((bh_average_density(&s) - 0.4).abs() < 1e-5);
199    }
200
201    #[test]
202    fn region_names_valid() {
203        assert_eq!(BodyHairRegion::Arms.name(), "arms");
204        assert_eq!(BodyHairRegion::Back.name(), "back");
205    }
206
207    #[test]
208    fn to_json_has_global_density() {
209        assert!(bh_to_json(&default_body_hair_state()).contains("global_density"));
210    }
211}