Skip to main content

oxihuman_export/
keyshape_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// A key shape (blend shape target with a name and weight).
6#[allow(dead_code)]
7#[derive(Clone)]
8pub struct KeyShape {
9    pub name: String,
10    pub weight: f32,
11    pub deltas: Vec<[f32; 3]>,
12}
13
14/// Key shape export bundle.
15#[allow(dead_code)]
16#[derive(Default)]
17pub struct KeyShapeExport {
18    pub shapes: Vec<KeyShape>,
19}
20
21/// Create a new key shape export.
22#[allow(dead_code)]
23pub fn new_keyshape_export() -> KeyShapeExport {
24    KeyShapeExport::default()
25}
26
27/// Add a key shape.
28#[allow(dead_code)]
29pub fn add_keyshape(export: &mut KeyShapeExport, name: &str, weight: f32, deltas: Vec<[f32; 3]>) {
30    export.shapes.push(KeyShape {
31        name: name.to_string(),
32        weight,
33        deltas,
34    });
35}
36
37/// Count shapes.
38#[allow(dead_code)]
39pub fn keyshape_count(export: &KeyShapeExport) -> usize {
40    export.shapes.len()
41}
42
43/// Find shape by name.
44#[allow(dead_code)]
45pub fn find_keyshape<'a>(export: &'a KeyShapeExport, name: &str) -> Option<&'a KeyShape> {
46    export.shapes.iter().find(|s| s.name == name)
47}
48
49/// Total deltas across all shapes.
50#[allow(dead_code)]
51pub fn total_keyshape_deltas(export: &KeyShapeExport) -> usize {
52    export.shapes.iter().map(|s| s.deltas.len()).sum()
53}
54
55/// Weighted blend of two key shapes (returns new delta list).
56#[allow(dead_code)]
57pub fn blend_keyshapes(a: &KeyShape, b: &KeyShape, t: f32) -> Vec<[f32; 3]> {
58    let n = a.deltas.len().min(b.deltas.len());
59    (0..n)
60        .map(|i| {
61            let da = a.deltas[i];
62            let db = b.deltas[i];
63            [
64                da[0] + t * (db[0] - da[0]),
65                da[1] + t * (db[1] - da[1]),
66                da[2] + t * (db[2] - da[2]),
67            ]
68        })
69        .collect()
70}
71
72/// Validate weights are in [0, 1].
73#[allow(dead_code)]
74pub fn validate_keyshape_weights(export: &KeyShapeExport) -> bool {
75    export
76        .shapes
77        .iter()
78        .all(|s| (0.0..=1.0).contains(&s.weight))
79}
80
81/// Normalize delta magnitude for a shape.
82#[allow(dead_code)]
83pub fn max_keyshape_delta(shape: &KeyShape) -> f32 {
84    shape
85        .deltas
86        .iter()
87        .map(|d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
88        .fold(0.0_f32, f32::max)
89}
90
91/// Serialize to JSON.
92#[allow(dead_code)]
93pub fn keyshape_to_json(export: &KeyShapeExport) -> String {
94    format!(
95        r#"{{"keyshapes":{},"total_deltas":{}}}"#,
96        export.shapes.len(),
97        total_keyshape_deltas(export)
98    )
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn add_and_count() {
107        let mut e = new_keyshape_export();
108        add_keyshape(&mut e, "smile", 0.5, vec![[0.1, 0.0, 0.0]; 5]);
109        assert_eq!(keyshape_count(&e), 1);
110    }
111
112    #[test]
113    fn find_shape() {
114        let mut e = new_keyshape_export();
115        add_keyshape(&mut e, "blink", 1.0, vec![]);
116        assert!(find_keyshape(&e, "blink").is_some());
117    }
118
119    #[test]
120    fn find_missing() {
121        let e = new_keyshape_export();
122        assert!(find_keyshape(&e, "x").is_none());
123    }
124
125    #[test]
126    fn total_deltas() {
127        let mut e = new_keyshape_export();
128        add_keyshape(&mut e, "a", 0.5, vec![[0.0; 3]; 3]);
129        add_keyshape(&mut e, "b", 0.5, vec![[0.0; 3]; 4]);
130        assert_eq!(total_keyshape_deltas(&e), 7);
131    }
132
133    #[test]
134    fn blend_midpoint() {
135        let a = KeyShape {
136            name: "a".to_string(),
137            weight: 0.0,
138            deltas: vec![[0.0, 0.0, 0.0]],
139        };
140        let b = KeyShape {
141            name: "b".to_string(),
142            weight: 1.0,
143            deltas: vec![[2.0, 0.0, 0.0]],
144        };
145        let blended = blend_keyshapes(&a, &b, 0.5);
146        assert!((blended[0][0] - 1.0).abs() < 1e-5);
147    }
148
149    #[test]
150    fn validate_valid() {
151        let mut e = new_keyshape_export();
152        add_keyshape(&mut e, "x", 0.7, vec![]);
153        assert!(validate_keyshape_weights(&e));
154    }
155
156    #[test]
157    fn validate_invalid() {
158        let mut e = new_keyshape_export();
159        add_keyshape(&mut e, "x", 1.5, vec![]);
160        assert!(!validate_keyshape_weights(&e));
161    }
162
163    #[test]
164    fn max_delta() {
165        let s = KeyShape {
166            name: "x".to_string(),
167            weight: 1.0,
168            deltas: vec![[3.0, 4.0, 0.0]],
169        };
170        assert!((max_keyshape_delta(&s) - 5.0).abs() < 1e-5);
171    }
172
173    #[test]
174    fn json_has_count() {
175        let e = new_keyshape_export();
176        let j = keyshape_to_json(&e);
177        assert!(j.contains("\"keyshapes\":0"));
178    }
179
180    #[test]
181    fn empty_total_deltas() {
182        let e = new_keyshape_export();
183        assert_eq!(total_keyshape_deltas(&e), 0);
184    }
185}