Skip to main content

oxihuman_morph/
piercing_deform.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Piercing deformation morph stub.
6
7/// Body location of a piercing.
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum PiercingLocation {
10    EarLobe,
11    Nostril,
12    Septum,
13    Eyebrow,
14    Lip,
15    Navel,
16    Other,
17}
18
19/// A single piercing entry.
20#[derive(Debug, Clone)]
21pub struct PiercingEntry {
22    pub id: u32,
23    pub location: PiercingLocation,
24    pub gauge: f32,
25    pub deform_radius: f32,
26}
27
28/// Piercing deformation morph controller.
29#[derive(Debug, Clone)]
30pub struct PiercingDeform {
31    pub piercings: Vec<PiercingEntry>,
32    pub morph_count: usize,
33    pub enabled: bool,
34}
35
36impl PiercingDeform {
37    pub fn new(morph_count: usize) -> Self {
38        PiercingDeform {
39            piercings: Vec::new(),
40            morph_count,
41            enabled: true,
42        }
43    }
44}
45
46/// Create a new piercing deform controller.
47pub fn new_piercing_deform(morph_count: usize) -> PiercingDeform {
48    PiercingDeform::new(morph_count)
49}
50
51/// Add a piercing.
52pub fn pd_add_piercing(ctrl: &mut PiercingDeform, entry: PiercingEntry) {
53    ctrl.piercings.push(entry);
54}
55
56/// Remove a piercing by id.
57pub fn pd_remove_piercing(ctrl: &mut PiercingDeform, id: u32) {
58    ctrl.piercings.retain(|p| p.id != id);
59}
60
61/// Evaluate morph weights (stub: sum of deform radii capped at 1).
62pub fn pd_evaluate(ctrl: &PiercingDeform) -> Vec<f32> {
63    /* Stub: sum deform radii, capped at 1.0 */
64    if !ctrl.enabled || ctrl.morph_count == 0 {
65        return vec![];
66    }
67    let w: f32 = ctrl
68        .piercings
69        .iter()
70        .map(|p| p.deform_radius)
71        .sum::<f32>()
72        .min(1.0);
73    vec![w; ctrl.morph_count]
74}
75
76/// Enable or disable.
77pub fn pd_set_enabled(ctrl: &mut PiercingDeform, enabled: bool) {
78    ctrl.enabled = enabled;
79}
80
81/// Return piercing count.
82pub fn pd_piercing_count(ctrl: &PiercingDeform) -> usize {
83    ctrl.piercings.len()
84}
85
86/// Serialize to JSON-like string.
87pub fn pd_to_json(ctrl: &PiercingDeform) -> String {
88    format!(
89        r#"{{"piercing_count":{},"morph_count":{},"enabled":{}}}"#,
90        ctrl.piercings.len(),
91        ctrl.morph_count,
92        ctrl.enabled
93    )
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn make_piercing(id: u32) -> PiercingEntry {
101        PiercingEntry {
102            id,
103            location: PiercingLocation::EarLobe,
104            gauge: 1.0,
105            deform_radius: 0.1,
106        }
107    }
108
109    #[test]
110    fn test_initial_empty() {
111        let c = new_piercing_deform(4);
112        assert_eq!(pd_piercing_count(&c), 0 /* no piercings initially */);
113    }
114
115    #[test]
116    fn test_add_piercing() {
117        let mut c = new_piercing_deform(4);
118        pd_add_piercing(&mut c, make_piercing(1));
119        assert_eq!(pd_piercing_count(&c), 1 /* one piercing after add */);
120    }
121
122    #[test]
123    fn test_remove_piercing() {
124        let mut c = new_piercing_deform(4);
125        pd_add_piercing(&mut c, make_piercing(1));
126        pd_remove_piercing(&mut c, 1);
127        assert_eq!(pd_piercing_count(&c), 0 /* piercing removed */);
128    }
129
130    #[test]
131    fn test_evaluate_length() {
132        let c = new_piercing_deform(6);
133        assert_eq!(
134            pd_evaluate(&c).len(),
135            6 /* output must match morph_count */
136        );
137    }
138
139    #[test]
140    fn test_evaluate_disabled() {
141        let mut c = new_piercing_deform(4);
142        pd_set_enabled(&mut c, false);
143        assert!(pd_evaluate(&c).is_empty() /* disabled must return empty */);
144    }
145
146    #[test]
147    fn test_evaluate_capped() {
148        let mut c = new_piercing_deform(2);
149        for i in 0..20 {
150            pd_add_piercing(
151                &mut c,
152                PiercingEntry {
153                    id: i,
154                    location: PiercingLocation::Other,
155                    gauge: 1.0,
156                    deform_radius: 0.2,
157                },
158            );
159        }
160        let out = pd_evaluate(&c);
161        assert!(out[0] <= 1.0 /* weight must not exceed 1.0 */);
162    }
163
164    #[test]
165    fn test_to_json_has_count() {
166        let c = new_piercing_deform(4);
167        let j = pd_to_json(&c);
168        assert!(j.contains("\"piercing_count\"") /* JSON must contain piercing_count */);
169    }
170
171    #[test]
172    fn test_enabled_default() {
173        let c = new_piercing_deform(4);
174        assert!(c.enabled /* must be enabled by default */);
175    }
176
177    #[test]
178    fn test_remove_nonexistent_is_noop() {
179        let mut c = new_piercing_deform(4);
180        pd_remove_piercing(&mut c, 999);
181        assert_eq!(
182            pd_piercing_count(&c),
183            0 /* removing nonexistent must be noop */
184        );
185    }
186}