Skip to main content

oxihuman_morph/
tattoo_morph.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Tattoo deform with skin stretch stub.
6
7/// Tattoo deformation entry.
8#[derive(Debug, Clone)]
9pub struct TattooEntry {
10    pub id: u32,
11    pub anchor: [f32; 3],
12    pub stretch_factor: f32,
13    pub opacity: f32,
14}
15
16/// Tattoo morph controller.
17#[derive(Debug, Clone)]
18pub struct TattooMorph {
19    pub entries: Vec<TattooEntry>,
20    pub skin_stretch_influence: f32,
21    pub morph_count: usize,
22    pub enabled: bool,
23}
24
25impl TattooMorph {
26    pub fn new(morph_count: usize) -> Self {
27        TattooMorph {
28            entries: Vec::new(),
29            skin_stretch_influence: 0.5,
30            morph_count,
31            enabled: true,
32        }
33    }
34}
35
36/// Create a new tattoo morph controller.
37pub fn new_tattoo_morph(morph_count: usize) -> TattooMorph {
38    TattooMorph::new(morph_count)
39}
40
41/// Add a tattoo entry.
42pub fn tm_add_tattoo(morph: &mut TattooMorph, entry: TattooEntry) {
43    morph.entries.push(entry);
44}
45
46/// Set skin stretch influence on tattoo deformation.
47pub fn tm_set_stretch_influence(morph: &mut TattooMorph, influence: f32) {
48    morph.skin_stretch_influence = influence.clamp(0.0, 1.0);
49}
50
51/// Remove a tattoo by id.
52pub fn tm_remove_tattoo(morph: &mut TattooMorph, id: u32) {
53    morph.entries.retain(|e| e.id != id);
54}
55
56/// Evaluate morph weights (stub: aggregate stretch effect).
57pub fn tm_evaluate(morph: &TattooMorph) -> Vec<f32> {
58    /* Stub: proportional to number of tattoos * stretch_influence */
59    if !morph.enabled || morph.morph_count == 0 {
60        return vec![];
61    }
62    let w = (morph.entries.len() as f32 * morph.skin_stretch_influence).min(1.0);
63    vec![w; morph.morph_count]
64}
65
66/// Enable or disable.
67pub fn tm_set_enabled(morph: &mut TattooMorph, enabled: bool) {
68    morph.enabled = enabled;
69}
70
71/// Return tattoo count.
72pub fn tm_tattoo_count(morph: &TattooMorph) -> usize {
73    morph.entries.len()
74}
75
76/// Serialize to JSON-like string.
77pub fn tm_to_json(morph: &TattooMorph) -> String {
78    format!(
79        r#"{{"tattoo_count":{},"stretch_influence":{},"morph_count":{},"enabled":{}}}"#,
80        morph.entries.len(),
81        morph.skin_stretch_influence,
82        morph.morph_count,
83        morph.enabled
84    )
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    fn make_entry(id: u32) -> TattooEntry {
92        TattooEntry {
93            id,
94            anchor: [0.0, 0.0, 0.0],
95            stretch_factor: 1.0,
96            opacity: 1.0,
97        }
98    }
99
100    #[test]
101    fn test_initial_count() {
102        let m = new_tattoo_morph(4);
103        assert_eq!(tm_tattoo_count(&m), 0 /* no tattoos initially */);
104    }
105
106    #[test]
107    fn test_add_tattoo() {
108        let mut m = new_tattoo_morph(4);
109        tm_add_tattoo(&mut m, make_entry(1));
110        assert_eq!(tm_tattoo_count(&m), 1 /* one tattoo after add */);
111    }
112
113    #[test]
114    fn test_remove_tattoo() {
115        let mut m = new_tattoo_morph(4);
116        tm_add_tattoo(&mut m, make_entry(1));
117        tm_remove_tattoo(&mut m, 1);
118        assert_eq!(tm_tattoo_count(&m), 0 /* tattoo removed */);
119    }
120
121    #[test]
122    fn test_stretch_influence_clamped() {
123        let mut m = new_tattoo_morph(4);
124        tm_set_stretch_influence(&mut m, 2.0);
125        assert!((m.skin_stretch_influence - 1.0).abs() < 1e-6 /* clamped to 1.0 */);
126    }
127
128    #[test]
129    fn test_evaluate_length() {
130        let m = new_tattoo_morph(5);
131        assert_eq!(
132            tm_evaluate(&m).len(),
133            5 /* output must match morph_count */
134        );
135    }
136
137    #[test]
138    fn test_evaluate_disabled() {
139        let mut m = new_tattoo_morph(4);
140        tm_set_enabled(&mut m, false);
141        assert!(tm_evaluate(&m).is_empty() /* disabled must return empty */);
142    }
143
144    #[test]
145    fn test_to_json_has_tattoo_count() {
146        let m = new_tattoo_morph(4);
147        let j = tm_to_json(&m);
148        assert!(j.contains("\"tattoo_count\"") /* JSON must have tattoo_count */);
149    }
150
151    #[test]
152    fn test_enabled_default() {
153        let m = new_tattoo_morph(4);
154        assert!(m.enabled /* must be enabled by default */);
155    }
156
157    #[test]
158    fn test_evaluate_capped_at_one() {
159        let mut m = new_tattoo_morph(2);
160        tm_set_stretch_influence(&mut m, 1.0);
161        for i in 0..5 {
162            tm_add_tattoo(&mut m, make_entry(i));
163        }
164        let out = tm_evaluate(&m);
165        assert!(out[0] <= 1.0 /* weight must not exceed 1.0 */);
166    }
167}