Skip to main content

oxihuman_export/
blend_shape_inbetween_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5/// An inbetween blend shape key.
6#[allow(dead_code)]
7pub struct InbetweenKey {
8    pub name: String,
9    pub target_weight: f32,
10    pub deltas: Vec<[f32; 3]>,
11}
12
13/// Export bundle for blend shape inbetweens.
14#[allow(dead_code)]
15pub struct BlendShapeInbetweenExport {
16    pub base_name: String,
17    pub keys: Vec<InbetweenKey>,
18}
19
20/// Create a new inbetween export.
21#[allow(dead_code)]
22pub fn new_bsi_export(base_name: &str) -> BlendShapeInbetweenExport {
23    BlendShapeInbetweenExport {
24        base_name: base_name.to_string(),
25        keys: Vec::new(),
26    }
27}
28
29/// Add an inbetween key.
30#[allow(dead_code)]
31pub fn add_inbetween_key(
32    export: &mut BlendShapeInbetweenExport,
33    name: &str,
34    target_weight: f32,
35    deltas: Vec<[f32; 3]>,
36) {
37    export.keys.push(InbetweenKey {
38        name: name.to_string(),
39        target_weight,
40        deltas,
41    });
42}
43
44/// Count inbetween keys.
45#[allow(dead_code)]
46pub fn inbetween_key_count(export: &BlendShapeInbetweenExport) -> usize {
47    export.keys.len()
48}
49
50/// Total delta vertices across all keys.
51#[allow(dead_code)]
52pub fn total_inbetween_deltas(export: &BlendShapeInbetweenExport) -> usize {
53    export.keys.iter().map(|k| k.deltas.len()).sum()
54}
55
56/// Find key by name.
57#[allow(dead_code)]
58pub fn find_inbetween<'a>(
59    export: &'a BlendShapeInbetweenExport,
60    name: &str,
61) -> Option<&'a InbetweenKey> {
62    export.keys.iter().find(|k| k.name == name)
63}
64
65/// Maximum magnitude delta in a key.
66#[allow(dead_code)]
67pub fn max_delta_magnitude_bsi(key: &InbetweenKey) -> f32 {
68    key.deltas
69        .iter()
70        .map(|d| (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt())
71        .fold(0.0_f32, f32::max)
72}
73
74/// Validate that keys are sorted by target weight.
75#[allow(dead_code)]
76pub fn keys_sorted_by_weight(export: &BlendShapeInbetweenExport) -> bool {
77    export
78        .keys
79        .windows(2)
80        .all(|w| w[0].target_weight <= w[1].target_weight)
81}
82
83/// Interpolate between two inbetween keys.
84#[allow(dead_code)]
85pub fn interpolate_inbetween(k0: &InbetweenKey, k1: &InbetweenKey, t: f32) -> Vec<[f32; 3]> {
86    let n = k0.deltas.len().min(k1.deltas.len());
87    (0..n)
88        .map(|i| {
89            let d0 = k0.deltas[i];
90            let d1 = k1.deltas[i];
91            [
92                d0[0] + t * (d1[0] - d0[0]),
93                d0[1] + t * (d1[1] - d0[1]),
94                d0[2] + t * (d1[2] - d0[2]),
95            ]
96        })
97        .collect()
98}
99
100/// Serialize to JSON.
101#[allow(dead_code)]
102pub fn bsi_to_json(export: &BlendShapeInbetweenExport) -> String {
103    format!(
104        r#"{{"base":"{}","keys":{}}}"#,
105        export.base_name,
106        export.keys.len()
107    )
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn create_and_add() {
116        let mut e = new_bsi_export("mouth_open");
117        add_inbetween_key(&mut e, "half", 0.5, vec![[0.0, 0.1, 0.0]; 4]);
118        assert_eq!(inbetween_key_count(&e), 1);
119    }
120
121    #[test]
122    fn total_deltas() {
123        let mut e = new_bsi_export("eye");
124        add_inbetween_key(&mut e, "k1", 0.5, vec![[0.0; 3]; 3]);
125        add_inbetween_key(&mut e, "k2", 1.0, vec![[0.0; 3]; 3]);
126        assert_eq!(total_inbetween_deltas(&e), 6);
127    }
128
129    #[test]
130    fn find_key() {
131        let mut e = new_bsi_export("brow");
132        add_inbetween_key(&mut e, "mid", 0.5, vec![]);
133        assert!(find_inbetween(&e, "mid").is_some());
134    }
135
136    #[test]
137    fn max_delta_mag() {
138        let k = InbetweenKey {
139            name: "k".to_string(),
140            target_weight: 1.0,
141            deltas: vec![[3.0, 4.0, 0.0]],
142        };
143        assert!((max_delta_magnitude_bsi(&k) - 5.0).abs() < 1e-5);
144    }
145
146    #[test]
147    fn sorted_by_weight() {
148        let mut e = new_bsi_export("test");
149        add_inbetween_key(&mut e, "a", 0.25, vec![]);
150        add_inbetween_key(&mut e, "b", 0.75, vec![]);
151        assert!(keys_sorted_by_weight(&e));
152    }
153
154    #[test]
155    fn not_sorted() {
156        let mut e = new_bsi_export("test");
157        add_inbetween_key(&mut e, "a", 0.75, vec![]);
158        add_inbetween_key(&mut e, "b", 0.25, vec![]);
159        assert!(!keys_sorted_by_weight(&e));
160    }
161
162    #[test]
163    fn interpolate_midpoint() {
164        let k0 = InbetweenKey {
165            name: "k0".to_string(),
166            target_weight: 0.0,
167            deltas: vec![[0.0; 3]],
168        };
169        let k1 = InbetweenKey {
170            name: "k1".to_string(),
171            target_weight: 1.0,
172            deltas: vec![[2.0, 0.0, 0.0]],
173        };
174        let mid = interpolate_inbetween(&k0, &k1, 0.5);
175        assert!((mid[0][0] - 1.0).abs() < 1e-5);
176    }
177
178    #[test]
179    fn json_has_base() {
180        let e = new_bsi_export("cheek");
181        let j = bsi_to_json(&e);
182        assert!(j.contains("\"base\":\"cheek\""));
183    }
184
185    #[test]
186    fn empty_keys() {
187        let e = new_bsi_export("x");
188        assert_eq!(inbetween_key_count(&e), 0);
189    }
190
191    #[test]
192    fn find_missing() {
193        let e = new_bsi_export("x");
194        assert!(find_inbetween(&e, "y").is_none());
195    }
196}