Skip to main content

oxihuman_morph/
freckle_map_control.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Freckle placement map parameters.
6
7/// Freckle distribution type.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq)]
10pub enum FreckleDistribution {
11    Sparse,
12    Moderate,
13    Dense,
14    Clustered,
15    Uniform,
16}
17
18impl FreckleDistribution {
19    #[allow(dead_code)]
20    pub fn name(self) -> &'static str {
21        match self {
22            FreckleDistribution::Sparse => "sparse",
23            FreckleDistribution::Moderate => "moderate",
24            FreckleDistribution::Dense => "dense",
25            FreckleDistribution::Clustered => "clustered",
26            FreckleDistribution::Uniform => "uniform",
27        }
28    }
29
30    #[allow(dead_code)]
31    pub fn density_index(self) -> f32 {
32        match self {
33            FreckleDistribution::Sparse => 0.1,
34            FreckleDistribution::Moderate => 0.4,
35            FreckleDistribution::Dense => 0.7,
36            FreckleDistribution::Clustered => 0.6,
37            FreckleDistribution::Uniform => 0.5,
38        }
39    }
40}
41
42/// Freckle map parameters.
43#[allow(dead_code)]
44#[derive(Debug, Clone)]
45pub struct FreckleParams {
46    pub distribution: FreckleDistribution,
47    pub density: f32,
48    pub size: f32,
49    pub darkness: f32,
50    pub sun_exposure: f32,
51    pub face_coverage: f32,
52    pub body_coverage: f32,
53}
54
55impl Default for FreckleParams {
56    fn default() -> Self {
57        FreckleParams {
58            distribution: FreckleDistribution::Sparse,
59            density: 0.0,
60            size: 0.0,
61            darkness: 0.0,
62            sun_exposure: 0.0,
63            face_coverage: 0.0,
64            body_coverage: 0.0,
65        }
66    }
67}
68
69#[allow(dead_code)]
70pub fn default_freckle_params() -> FreckleParams {
71    FreckleParams::default()
72}
73
74#[allow(dead_code)]
75pub fn fm_set_density(p: &mut FreckleParams, v: f32) {
76    p.density = v.clamp(0.0, 1.0);
77}
78
79#[allow(dead_code)]
80pub fn fm_set_size(p: &mut FreckleParams, v: f32) {
81    p.size = v.clamp(0.0, 1.0);
82}
83
84#[allow(dead_code)]
85pub fn fm_set_darkness(p: &mut FreckleParams, v: f32) {
86    p.darkness = v.clamp(0.0, 1.0);
87}
88
89#[allow(dead_code)]
90pub fn fm_set_sun_exposure(p: &mut FreckleParams, v: f32) {
91    p.sun_exposure = v.clamp(0.0, 1.0);
92}
93
94#[allow(dead_code)]
95pub fn fm_set_face_coverage(p: &mut FreckleParams, v: f32) {
96    p.face_coverage = v.clamp(0.0, 1.0);
97}
98
99#[allow(dead_code)]
100pub fn fm_set_body_coverage(p: &mut FreckleParams, v: f32) {
101    p.body_coverage = v.clamp(0.0, 1.0);
102}
103
104#[allow(dead_code)]
105pub fn fm_set_distribution(p: &mut FreckleParams, d: FreckleDistribution) {
106    p.distribution = d;
107}
108
109#[allow(dead_code)]
110pub fn fm_reset(p: &mut FreckleParams) {
111    *p = FreckleParams::default();
112}
113
114#[allow(dead_code)]
115pub fn fm_is_neutral(p: &FreckleParams) -> bool {
116    p.density < 1e-6 && p.face_coverage < 1e-6 && p.body_coverage < 1e-6
117}
118
119#[allow(dead_code)]
120pub fn fm_effective_density(p: &FreckleParams) -> f32 {
121    let sun_boost = p.sun_exposure * 0.3;
122    (p.density + sun_boost).clamp(0.0, 1.0)
123}
124
125#[allow(dead_code)]
126pub fn fm_blend(a: &FreckleParams, b: &FreckleParams, t: f32) -> FreckleParams {
127    let t = t.clamp(0.0, 1.0);
128    FreckleParams {
129        distribution: if t < 0.5 {
130            a.distribution
131        } else {
132            b.distribution
133        },
134        density: a.density + (b.density - a.density) * t,
135        size: a.size + (b.size - a.size) * t,
136        darkness: a.darkness + (b.darkness - a.darkness) * t,
137        sun_exposure: a.sun_exposure + (b.sun_exposure - a.sun_exposure) * t,
138        face_coverage: a.face_coverage + (b.face_coverage - a.face_coverage) * t,
139        body_coverage: a.body_coverage + (b.body_coverage - a.body_coverage) * t,
140    }
141}
142
143#[allow(dead_code)]
144pub fn fm_to_json(p: &FreckleParams) -> String {
145    format!(
146        r#"{{"distribution":"{}","density":{:.4},"size":{:.4},"darkness":{:.4}}}"#,
147        p.distribution.name(),
148        p.density,
149        p.size,
150        p.darkness
151    )
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn default_is_neutral() {
160        assert!(fm_is_neutral(&default_freckle_params()));
161    }
162
163    #[test]
164    fn set_density_clamps() {
165        let mut p = default_freckle_params();
166        fm_set_density(&mut p, 5.0);
167        assert!((p.density - 1.0).abs() < 1e-6);
168    }
169
170    #[test]
171    fn set_sun_exposure() {
172        let mut p = default_freckle_params();
173        fm_set_sun_exposure(&mut p, 0.7);
174        assert!((p.sun_exposure - 0.7).abs() < 1e-5);
175    }
176
177    #[test]
178    fn effective_density_boosted_by_sun() {
179        let mut p = default_freckle_params();
180        fm_set_density(&mut p, 0.5);
181        fm_set_sun_exposure(&mut p, 1.0);
182        assert!(fm_effective_density(&p) > 0.5);
183    }
184
185    #[test]
186    fn reset_clears() {
187        let mut p = default_freckle_params();
188        fm_set_density(&mut p, 0.8);
189        fm_reset(&mut p);
190        assert!(fm_is_neutral(&p));
191    }
192
193    #[test]
194    fn distribution_density_index() {
195        assert!((FreckleDistribution::Sparse.density_index() - 0.1).abs() < 1e-6);
196        assert!((FreckleDistribution::Dense.density_index() - 0.7).abs() < 1e-6);
197    }
198
199    #[test]
200    fn blend_midpoint() {
201        let a = default_freckle_params();
202        let mut b = default_freckle_params();
203        fm_set_density(&mut b, 1.0);
204        let m = fm_blend(&a, &b, 0.5);
205        assert!((m.density - 0.5).abs() < 1e-5);
206    }
207
208    #[test]
209    fn set_distribution() {
210        let mut p = default_freckle_params();
211        fm_set_distribution(&mut p, FreckleDistribution::Dense);
212        assert_eq!(p.distribution, FreckleDistribution::Dense);
213    }
214
215    #[test]
216    fn to_json_has_distribution() {
217        assert!(fm_to_json(&default_freckle_params()).contains("distribution"));
218    }
219}