Skip to main content

oxihuman_export/
action_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Action (animation action) export utilities.
6
7/* ── legacy API (keep for existing lib.rs exports) ── */
8
9#[derive(Debug, Clone)]
10pub struct ActionKeyframe {
11    pub time: f32,
12    pub value: f32,
13}
14
15#[derive(Debug, Clone)]
16pub struct ActionExport {
17    pub name: String,
18    pub keyframes: Vec<ActionKeyframe>,
19}
20
21pub fn new_action_export(name: &str) -> ActionExport {
22    ActionExport {
23        name: name.to_string(),
24        keyframes: vec![],
25    }
26}
27
28pub fn add_keyframe(action: &mut ActionExport, time: f32, value: f32) {
29    action.keyframes.push(ActionKeyframe { time, value });
30}
31
32pub fn keyframe_count(action: &ActionExport) -> usize {
33    action.keyframes.len()
34}
35
36pub fn action_duration(action: &ActionExport) -> f32 {
37    if action.keyframes.is_empty() {
38        return 0.0;
39    }
40    let min_t = action
41        .keyframes
42        .iter()
43        .map(|k| k.time)
44        .fold(f32::MAX, f32::min);
45    let max_t = action
46        .keyframes
47        .iter()
48        .map(|k| k.time)
49        .fold(f32::MIN, f32::max);
50    max_t - min_t
51}
52
53pub fn sample_action(action: &ActionExport, time: f32) -> f32 {
54    if action.keyframes.is_empty() {
55        return 0.0;
56    }
57    if action.keyframes.len() == 1 {
58        return action.keyframes[0].value;
59    }
60    let mut sorted: Vec<&ActionKeyframe> = action.keyframes.iter().collect();
61    sorted.sort_by(|a, b| {
62        a.time
63            .partial_cmp(&b.time)
64            .unwrap_or(std::cmp::Ordering::Equal)
65    });
66    if time <= sorted[0].time {
67        return sorted[0].value;
68    }
69    if time >= sorted[sorted.len() - 1].time {
70        return sorted[sorted.len() - 1].value;
71    }
72    for i in 0..sorted.len() - 1 {
73        if time >= sorted[i].time && time <= sorted[i + 1].time {
74            let dt = sorted[i + 1].time - sorted[i].time;
75            if dt < 1e-12 {
76                return sorted[i].value;
77            }
78            let t = (time - sorted[i].time) / dt;
79            return sorted[i].value + (sorted[i + 1].value - sorted[i].value) * t;
80        }
81    }
82    0.0
83}
84
85pub fn validate_action(action: &ActionExport) -> bool {
86    action.keyframes.iter().all(|k| k.time >= 0.0)
87}
88
89pub fn action_to_json(action: &ActionExport) -> String {
90    format!(
91        "{{\"name\":\"{}\",\"keyframes\":{},\"duration\":{:.6}}}",
92        action.name,
93        keyframe_count(action),
94        action_duration(action)
95    )
96}
97
98pub fn clear_keyframes(action: &mut ActionExport) {
99    action.keyframes.clear();
100}
101
102/* ── spec functions (wave 150B) ── */
103
104/// Spec-style action export (multi-fcurve).
105#[derive(Debug, Clone)]
106pub struct ActionExportSpec {
107    pub name: String,
108    pub fcurves: Vec<String>,
109    pub fps: f32,
110}
111
112/// Create a new `ActionExportSpec`.
113pub fn new_action_export_spec(name: &str, fps: f32) -> ActionExportSpec {
114    ActionExportSpec {
115        name: name.to_string(),
116        fcurves: Vec::new(),
117        fps,
118    }
119}
120
121/// Push an fcurve data path string.
122pub fn action_push_fcurve(a: &mut ActionExportSpec, path: &str) {
123    a.fcurves.push(path.to_string());
124}
125
126/// Duration in frames (stub: returns 0).
127pub fn action_duration_frames(a: &ActionExportSpec) -> usize {
128    let _ = a;
129    0
130}
131
132/// Serialize to JSON.
133pub fn action_spec_to_json(a: &ActionExportSpec) -> String {
134    format!(
135        "{{\"name\":\"{}\",\"fps\":{},\"fcurves\":{}}}",
136        a.name,
137        a.fps,
138        a.fcurves.len()
139    )
140}
141
142/// Number of fcurves.
143pub fn action_fcurve_count(a: &ActionExportSpec) -> usize {
144    a.fcurves.len()
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_new_action_export() {
153        let a = new_action_export("walk");
154        assert_eq!(a.name, "walk");
155        assert_eq!(keyframe_count(&a), 0);
156    }
157
158    #[test]
159    fn test_add_keyframe() {
160        let mut a = new_action_export("test");
161        add_keyframe(&mut a, 0.0, 1.0);
162        assert_eq!(keyframe_count(&a), 1);
163    }
164
165    #[test]
166    fn test_duration() {
167        let mut a = new_action_export("test");
168        add_keyframe(&mut a, 1.0, 0.0);
169        add_keyframe(&mut a, 3.0, 1.0);
170        assert!((action_duration(&a) - 2.0).abs() < 1e-6);
171    }
172
173    #[test]
174    fn test_action_push_fcurve() {
175        let mut a = new_action_export_spec("run", 24.0);
176        action_push_fcurve(&mut a, "loc.x");
177        assert_eq!(action_fcurve_count(&a), 1);
178    }
179
180    #[test]
181    fn test_action_spec_to_json() {
182        let a = new_action_export_spec("idle", 30.0);
183        let j = action_spec_to_json(&a);
184        assert!(j.contains("idle"));
185    }
186}