Skip to main content

oxihuman_morph/
hair_curl_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Hair curl/wave pattern strength morph parameters.
6
7/// Hair curl pattern type.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum CurlPattern {
11    Straight,
12    Wavy,
13    Curly,
14    Coily,
15    Kinky,
16}
17
18impl CurlPattern {
19    #[allow(dead_code)]
20    pub fn name(self) -> &'static str {
21        match self {
22            CurlPattern::Straight => "straight",
23            CurlPattern::Wavy => "wavy",
24            CurlPattern::Curly => "curly",
25            CurlPattern::Coily => "coily",
26            CurlPattern::Kinky => "kinky",
27        }
28    }
29
30    #[allow(dead_code)]
31    pub fn curl_index(self) -> f32 {
32        match self {
33            CurlPattern::Straight => 0.0,
34            CurlPattern::Wavy => 0.25,
35            CurlPattern::Curly => 0.5,
36            CurlPattern::Coily => 0.75,
37            CurlPattern::Kinky => 1.0,
38        }
39    }
40}
41
42/// Parameters for hair curl morph.
43#[allow(dead_code)]
44#[derive(Debug, Clone)]
45pub struct HairCurlParams {
46    pub pattern: CurlPattern,
47    pub strength: f32,
48    pub frequency: f32,
49    pub random_seed: u32,
50}
51
52impl Default for HairCurlParams {
53    fn default() -> Self {
54        HairCurlParams {
55            pattern: CurlPattern::Straight,
56            strength: 0.0,
57            frequency: 1.0,
58            random_seed: 42,
59        }
60    }
61}
62
63#[allow(dead_code)]
64pub fn default_hair_curl_params() -> HairCurlParams {
65    HairCurlParams::default()
66}
67
68#[allow(dead_code)]
69pub fn hc_set_pattern(p: &mut HairCurlParams, pat: CurlPattern) {
70    p.pattern = pat;
71}
72
73#[allow(dead_code)]
74pub fn hc_set_strength(p: &mut HairCurlParams, v: f32) {
75    p.strength = v.clamp(0.0, 1.0);
76}
77
78#[allow(dead_code)]
79pub fn hc_set_frequency(p: &mut HairCurlParams, v: f32) {
80    p.frequency = v.clamp(0.1, 10.0);
81}
82
83#[allow(dead_code)]
84pub fn hc_reset(p: &mut HairCurlParams) {
85    *p = HairCurlParams::default();
86}
87
88#[allow(dead_code)]
89pub fn hc_is_straight(p: &HairCurlParams) -> bool {
90    p.strength < 1e-6 || p.pattern == CurlPattern::Straight
91}
92
93#[allow(dead_code)]
94pub fn hc_effective_curl(p: &HairCurlParams) -> f32 {
95    p.pattern.curl_index() * p.strength
96}
97
98#[allow(dead_code)]
99pub fn hc_blend(a: &HairCurlParams, b: &HairCurlParams, t: f32) -> HairCurlParams {
100    let t = t.clamp(0.0, 1.0);
101    let pattern = if t < 0.5 { a.pattern } else { b.pattern };
102    HairCurlParams {
103        pattern,
104        strength: a.strength + (b.strength - a.strength) * t,
105        frequency: a.frequency + (b.frequency - a.frequency) * t,
106        random_seed: a.random_seed,
107    }
108}
109
110#[allow(dead_code)]
111pub fn hc_to_json(p: &HairCurlParams) -> String {
112    format!(
113        r#"{{"pattern":"{}","strength":{:.4},"frequency":{:.4}}}"#,
114        p.pattern.name(),
115        p.strength,
116        p.frequency
117    )
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn default_is_straight() {
126        assert!(hc_is_straight(&default_hair_curl_params()));
127    }
128
129    #[test]
130    fn curl_index_range() {
131        assert!((CurlPattern::Straight.curl_index()).abs() < 1e-6);
132        assert!((CurlPattern::Kinky.curl_index() - 1.0).abs() < 1e-6);
133    }
134
135    #[test]
136    fn set_strength_clamps() {
137        let mut p = default_hair_curl_params();
138        hc_set_strength(&mut p, 5.0);
139        assert!((p.strength - 1.0).abs() < 1e-6);
140    }
141
142    #[test]
143    fn set_frequency_clamps_low() {
144        let mut p = default_hair_curl_params();
145        hc_set_frequency(&mut p, 0.0);
146        assert!((p.frequency - 0.1).abs() < 1e-6);
147    }
148
149    #[test]
150    fn reset_to_straight() {
151        let mut p = default_hair_curl_params();
152        hc_set_pattern(&mut p, CurlPattern::Kinky);
153        hc_set_strength(&mut p, 1.0);
154        hc_reset(&mut p);
155        assert!(hc_is_straight(&p));
156    }
157
158    #[test]
159    fn effective_curl_zero_when_straight() {
160        let p = default_hair_curl_params();
161        assert!(hc_effective_curl(&p).abs() < 1e-6);
162    }
163
164    #[test]
165    fn effective_curl_with_kinky_full() {
166        let mut p = default_hair_curl_params();
167        hc_set_pattern(&mut p, CurlPattern::Kinky);
168        hc_set_strength(&mut p, 1.0);
169        assert!((hc_effective_curl(&p) - 1.0).abs() < 1e-5);
170    }
171
172    #[test]
173    fn blend_strength_midpoint() {
174        let a = default_hair_curl_params();
175        let mut b = default_hair_curl_params();
176        hc_set_strength(&mut b, 1.0);
177        let m = hc_blend(&a, &b, 0.5);
178        assert!((m.strength - 0.5).abs() < 1e-5);
179    }
180
181    #[test]
182    fn to_json_contains_pattern() {
183        assert!(hc_to_json(&default_hair_curl_params()).contains("pattern"));
184    }
185}