Skip to main content

oxihuman_export/
keyframe_set_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export a set of keyframes for multiple channels.
6
7/// A single keyframe value.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy)]
10pub struct KeyframeValue {
11    pub time: f32,
12    pub value: f32,
13}
14
15/// A named channel with keyframes.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct KeyframeChannel {
19    pub name: String,
20    pub keys: Vec<KeyframeValue>,
21}
22
23/// A keyframe set export.
24#[allow(dead_code)]
25#[derive(Debug, Clone)]
26pub struct KeyframeSetExport {
27    pub channels: Vec<KeyframeChannel>,
28}
29
30/// Create a new keyframe set.
31#[allow(dead_code)]
32pub fn new_keyframe_set_export() -> KeyframeSetExport {
33    KeyframeSetExport {
34        channels: Vec::new(),
35    }
36}
37
38/// Add a channel.
39#[allow(dead_code)]
40pub fn add_channel(export: &mut KeyframeSetExport, channel: KeyframeChannel) {
41    export.channels.push(channel);
42}
43
44/// Count channels.
45#[allow(dead_code)]
46pub fn channel_count_ks(export: &KeyframeSetExport) -> usize {
47    export.channels.len()
48}
49
50/// Total keyframes across all channels.
51#[allow(dead_code)]
52pub fn total_keyframe_count(export: &KeyframeSetExport) -> usize {
53    export.channels.iter().map(|c| c.keys.len()).sum()
54}
55
56/// Find channel by name.
57#[allow(dead_code)]
58pub fn find_channel_ks<'a>(
59    export: &'a KeyframeSetExport,
60    name: &str,
61) -> Option<&'a KeyframeChannel> {
62    export.channels.iter().find(|c| c.name == name)
63}
64
65/// Duration = max time across all channels.
66#[allow(dead_code)]
67pub fn keyframe_set_duration(export: &KeyframeSetExport) -> f32 {
68    export
69        .channels
70        .iter()
71        .flat_map(|c| c.keys.iter().map(|k| k.time))
72        .fold(0.0f32, f32::max)
73}
74
75/// Sample a channel at time t (linear interpolation).
76#[allow(dead_code)]
77pub fn sample_channel_ks(channel: &KeyframeChannel, t: f32) -> f32 {
78    if channel.keys.is_empty() {
79        return 0.0;
80    }
81    if channel.keys.len() == 1 {
82        return channel.keys[0].value;
83    }
84    let idx = channel
85        .keys
86        .partition_point(|k| k.time <= t)
87        .saturating_sub(1);
88    let i0 = idx.min(channel.keys.len() - 1);
89    let i1 = (idx + 1).min(channel.keys.len() - 1);
90    if i0 == i1 {
91        return channel.keys[i0].value;
92    }
93    let k0 = channel.keys[i0];
94    let k1 = channel.keys[i1];
95    let dt = k1.time - k0.time;
96    if dt <= 0.0 {
97        return k0.value;
98    }
99    let f = ((t - k0.time) / dt).clamp(0.0, 1.0);
100    k0.value + f * (k1.value - k0.value)
101}
102
103/// Serialize to JSON.
104#[allow(dead_code)]
105pub fn keyframe_set_to_json(export: &KeyframeSetExport) -> String {
106    format!(
107        "{{\"channels\":{},\"total_keys\":{}}}",
108        export.channels.len(),
109        total_keyframe_count(export)
110    )
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    fn linear_channel() -> KeyframeChannel {
118        KeyframeChannel {
119            name: "tx".to_string(),
120            keys: vec![
121                KeyframeValue {
122                    time: 0.0,
123                    value: 0.0,
124                },
125                KeyframeValue {
126                    time: 1.0,
127                    value: 1.0,
128                },
129            ],
130        }
131    }
132
133    #[test]
134    fn test_add_and_count() {
135        let mut e = new_keyframe_set_export();
136        add_channel(&mut e, linear_channel());
137        assert_eq!(channel_count_ks(&e), 1);
138    }
139
140    #[test]
141    fn test_total_keyframe_count() {
142        let mut e = new_keyframe_set_export();
143        add_channel(&mut e, linear_channel());
144        assert_eq!(total_keyframe_count(&e), 2);
145    }
146
147    #[test]
148    fn test_find_channel() {
149        let mut e = new_keyframe_set_export();
150        add_channel(&mut e, linear_channel());
151        assert!(find_channel_ks(&e, "tx").is_some());
152    }
153
154    #[test]
155    fn test_duration() {
156        let mut e = new_keyframe_set_export();
157        add_channel(&mut e, linear_channel());
158        assert!((keyframe_set_duration(&e) - 1.0).abs() < 1e-5);
159    }
160
161    #[test]
162    fn test_sample_at_start() {
163        let c = linear_channel();
164        assert!(sample_channel_ks(&c, 0.0).abs() < 1e-5);
165    }
166
167    #[test]
168    fn test_sample_at_end() {
169        let c = linear_channel();
170        assert!((sample_channel_ks(&c, 1.0) - 1.0).abs() < 1e-5);
171    }
172
173    #[test]
174    fn test_sample_at_mid() {
175        let c = linear_channel();
176        assert!((sample_channel_ks(&c, 0.5) - 0.5).abs() < 1e-5);
177    }
178
179    #[test]
180    fn test_keyframe_set_to_json() {
181        let e = new_keyframe_set_export();
182        let j = keyframe_set_to_json(&e);
183        assert!(j.contains("channels"));
184    }
185
186    #[test]
187    fn test_sample_empty_channel() {
188        let c = KeyframeChannel {
189            name: "x".to_string(),
190            keys: vec![],
191        };
192        assert!(sample_channel_ks(&c, 0.5).abs() < 1e-6);
193    }
194
195    #[test]
196    fn test_empty_duration_zero() {
197        let e = new_keyframe_set_export();
198        assert!(keyframe_set_duration(&e).abs() < 1e-6);
199    }
200}