Skip to main content

oxihuman_export/
keyframe_blend_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Keyframe blending mode and weight export.
6
7#[allow(dead_code)]
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub enum KeyBlendMode {
10    Linear,
11    Ease,
12    Constant,
13    Bezier,
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct KeyframeBlendEntry {
19    pub time: f32,
20    pub value: f32,
21    pub blend_mode: KeyBlendMode,
22    pub blend_weight: f32,
23}
24
25#[allow(dead_code)]
26#[derive(Debug, Clone)]
27pub struct KeyframeBlendExport {
28    pub channel_name: String,
29    pub keys: Vec<KeyframeBlendEntry>,
30}
31
32#[allow(dead_code)]
33pub fn new_keyframe_blend_export(channel: &str) -> KeyframeBlendExport {
34    KeyframeBlendExport {
35        channel_name: channel.to_string(),
36        keys: Vec::new(),
37    }
38}
39
40#[allow(dead_code)]
41pub fn add_blend_key(exp: &mut KeyframeBlendExport, time: f32, value: f32, mode: KeyBlendMode) {
42    exp.keys.push(KeyframeBlendEntry {
43        time,
44        value,
45        blend_mode: mode,
46        blend_weight: 1.0,
47    });
48}
49
50#[allow(dead_code)]
51pub fn key_count_kbe(exp: &KeyframeBlendExport) -> usize {
52    exp.keys.len()
53}
54
55#[allow(dead_code)]
56pub fn channel_duration(exp: &KeyframeBlendExport) -> f32 {
57    exp.keys.iter().map(|k| k.time).fold(0.0_f32, f32::max)
58}
59
60#[allow(dead_code)]
61pub fn keys_of_mode(exp: &KeyframeBlendExport, mode: KeyBlendMode) -> usize {
62    exp.keys.iter().filter(|k| k.blend_mode == mode).count()
63}
64
65#[allow(dead_code)]
66pub fn sort_keys_by_time(exp: &mut KeyframeBlendExport) {
67    exp.keys.sort_by(|a, b| {
68        a.time
69            .partial_cmp(&b.time)
70            .unwrap_or(std::cmp::Ordering::Equal)
71    });
72}
73
74#[allow(dead_code)]
75pub fn sample_linear(exp: &KeyframeBlendExport, t: f32) -> f32 {
76    if exp.keys.is_empty() {
77        return 0.0;
78    }
79    let before: Vec<&KeyframeBlendEntry> = exp.keys.iter().filter(|k| k.time <= t).collect();
80    let after: Vec<&KeyframeBlendEntry> = exp.keys.iter().filter(|k| k.time > t).collect();
81    match (before.last(), after.first()) {
82        (Some(a), Some(b)) => {
83            let dt = b.time - a.time;
84            if dt.abs() < 1e-6 {
85                a.value
86            } else {
87                let alpha = (t - a.time) / dt;
88                a.value + alpha * (b.value - a.value)
89            }
90        }
91        (Some(a), None) => a.value,
92        (None, Some(b)) => b.value,
93        _ => 0.0,
94    }
95}
96
97#[allow(dead_code)]
98pub fn keyframe_blend_to_json(exp: &KeyframeBlendExport) -> String {
99    format!(
100        "{{\"channel\":\"{}\",\"key_count\":{}}}",
101        exp.channel_name,
102        key_count_kbe(exp)
103    )
104}
105
106#[allow(dead_code)]
107pub fn blend_mode_name(mode: KeyBlendMode) -> &'static str {
108    match mode {
109        KeyBlendMode::Linear => "linear",
110        KeyBlendMode::Ease => "ease",
111        KeyBlendMode::Constant => "constant",
112        KeyBlendMode::Bezier => "bezier",
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_empty() {
122        let exp = new_keyframe_blend_export("pos_x");
123        assert_eq!(key_count_kbe(&exp), 0);
124    }
125
126    #[test]
127    fn test_add_key() {
128        let mut exp = new_keyframe_blend_export("pos_x");
129        add_blend_key(&mut exp, 0.0, 0.0, KeyBlendMode::Linear);
130        assert_eq!(key_count_kbe(&exp), 1);
131    }
132
133    #[test]
134    fn test_channel_duration() {
135        let mut exp = new_keyframe_blend_export("pos_x");
136        add_blend_key(&mut exp, 0.0, 0.0, KeyBlendMode::Linear);
137        add_blend_key(&mut exp, 2.0, 1.0, KeyBlendMode::Linear);
138        assert!((channel_duration(&exp) - 2.0).abs() < 1e-5);
139    }
140
141    #[test]
142    fn test_keys_of_mode() {
143        let mut exp = new_keyframe_blend_export("rot_y");
144        add_blend_key(&mut exp, 0.0, 0.0, KeyBlendMode::Linear);
145        add_blend_key(&mut exp, 1.0, 0.5, KeyBlendMode::Ease);
146        assert_eq!(keys_of_mode(&exp, KeyBlendMode::Ease), 1);
147    }
148
149    #[test]
150    fn test_sort_keys() {
151        let mut exp = new_keyframe_blend_export("x");
152        add_blend_key(&mut exp, 2.0, 1.0, KeyBlendMode::Linear);
153        add_blend_key(&mut exp, 0.5, 0.5, KeyBlendMode::Linear);
154        sort_keys_by_time(&mut exp);
155        assert!((exp.keys[0].time - 0.5).abs() < 1e-5);
156    }
157
158    #[test]
159    fn test_sample_linear_midpoint() {
160        let mut exp = new_keyframe_blend_export("x");
161        add_blend_key(&mut exp, 0.0, 0.0, KeyBlendMode::Linear);
162        add_blend_key(&mut exp, 1.0, 1.0, KeyBlendMode::Linear);
163        let v = sample_linear(&exp, 0.5);
164        assert!((v - 0.5).abs() < 1e-5);
165    }
166
167    #[test]
168    fn test_blend_mode_name() {
169        assert_eq!(blend_mode_name(KeyBlendMode::Bezier), "bezier");
170    }
171
172    #[test]
173    fn test_json_output() {
174        let exp = new_keyframe_blend_export("scale");
175        let j = keyframe_blend_to_json(&exp);
176        assert!(j.contains("channel"));
177    }
178
179    #[test]
180    fn test_sample_empty_zero() {
181        let exp = new_keyframe_blend_export("x");
182        assert!((sample_linear(&exp, 0.5)).abs() < 1e-6);
183    }
184
185    #[test]
186    fn test_channel_name_stored() {
187        let exp = new_keyframe_blend_export("my_channel");
188        assert_eq!(exp.channel_name, "my_channel".to_string());
189    }
190
191    #[test]
192    fn test_constant_mode_name() {
193        assert_eq!(blend_mode_name(KeyBlendMode::Constant), "constant");
194    }
195}