#[allow(dead_code)]
#[derive(Debug, Clone, PartialEq)]
pub enum AnimInterpolation {
Step,
Linear,
Bezier,
Hermite,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnimExportConfig {
pub frame_rate: f32,
pub loop_mode: bool,
pub bake_to_frames: bool,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnimKey {
pub time: f32,
pub value: f32,
pub interp: AnimInterpolation,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnimChannel {
pub name: String,
pub keys: Vec<AnimKey>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnimClipExport {
pub name: String,
pub channels: Vec<AnimChannel>,
pub duration: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct AnimExportResult {
pub clips: Vec<String>,
pub total_keys: usize,
pub duration_sec: f32,
}
#[allow(dead_code)]
pub fn default_anim_export_config() -> AnimExportConfig {
AnimExportConfig {
frame_rate: 30.0,
loop_mode: false,
bake_to_frames: false,
}
}
#[allow(dead_code)]
pub fn new_anim_clip(name: &str, duration: f32) -> AnimClipExport {
AnimClipExport {
name: name.to_string(),
channels: Vec::new(),
duration,
}
}
#[allow(dead_code)]
pub fn add_channel(clip: &mut AnimClipExport, ch: AnimChannel) {
clip.channels.push(ch);
}
#[allow(dead_code)]
pub fn new_anim_channel(name: &str) -> AnimChannel {
AnimChannel {
name: name.to_string(),
keys: Vec::new(),
}
}
#[allow(dead_code)]
pub fn add_anim_key_to_channel(ch: &mut AnimChannel, key: AnimKey) {
ch.keys.push(key);
}
#[allow(dead_code)]
pub fn export_anim_clip(clip: &AnimClipExport, cfg: &AnimExportConfig) -> String {
let total_keys: usize = clip.channels.iter().map(|c| c.keys.len()).sum();
format!(
"{{\"name\":\"{}\",\"duration\":{:.4},\"frame_rate\":{:.2},\"loop\":{},\"channels\":{},\"total_keys\":{}}}",
clip.name,
clip.duration,
cfg.frame_rate,
cfg.loop_mode,
clip.channels.len(),
total_keys,
)
}
#[allow(dead_code)]
pub fn export_anim_clips(clips: &[AnimClipExport], cfg: &AnimExportConfig) -> AnimExportResult {
let mut total_keys = 0usize;
let mut duration_sec = 0.0f32;
let mut names = Vec::with_capacity(clips.len());
for clip in clips {
let keys: usize = clip.channels.iter().map(|c| c.keys.len()).sum();
total_keys += keys;
if clip.duration > duration_sec {
duration_sec = clip.duration;
}
names.push(export_anim_clip(clip, cfg));
}
AnimExportResult {
clips: names,
total_keys,
duration_sec,
}
}
#[allow(dead_code)]
pub fn channel_key_count(ch: &AnimChannel) -> usize {
ch.keys.len()
}
#[allow(dead_code)]
pub fn clip_channel_count(clip: &AnimClipExport) -> usize {
clip.channels.len()
}
#[allow(dead_code)]
pub fn anim_interp_name(key: &AnimKey) -> &'static str {
match key.interp {
AnimInterpolation::Step => "step",
AnimInterpolation::Linear => "linear",
AnimInterpolation::Bezier => "bezier",
AnimInterpolation::Hermite => "hermite",
}
}
#[allow(dead_code)]
pub fn anim_export_result_to_json(r: &AnimExportResult) -> String {
let clips_json = r
.clips
.iter()
.map(|s| format!("\"{}\"", s.replace('"', "\\\"")))
.collect::<Vec<_>>()
.join(",");
format!(
"{{\"clip_count\":{},\"total_keys\":{},\"duration_sec\":{:.4},\"clips\":[{}]}}",
r.clips.len(),
r.total_keys,
r.duration_sec,
clips_json,
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_anim_export_config();
assert!((cfg.frame_rate - 30.0).abs() < 1e-6);
assert!(!cfg.loop_mode);
assert!(!cfg.bake_to_frames);
}
#[test]
fn new_clip_has_correct_fields() {
let clip = new_anim_clip("idle", 2.5);
assert_eq!(clip.name, "idle");
assert!((clip.duration - 2.5).abs() < 1e-6);
assert!(clip.channels.is_empty());
}
#[test]
fn add_channel_grows_clip() {
let mut clip = new_anim_clip("run", 1.0);
let ch = new_anim_channel("position_x");
add_channel(&mut clip, ch);
assert_eq!(clip_channel_count(&clip), 1);
}
#[test]
fn add_keys_to_channel() {
let mut ch = new_anim_channel("rot_y");
let key = AnimKey {
time: 0.0,
value: 0.0,
interp: AnimInterpolation::Linear,
};
add_anim_key_to_channel(&mut ch, key);
assert_eq!(channel_key_count(&ch), 1);
}
#[test]
fn interp_names_correct() {
let cases = [
(AnimInterpolation::Step, "step"),
(AnimInterpolation::Linear, "linear"),
(AnimInterpolation::Bezier, "bezier"),
(AnimInterpolation::Hermite, "hermite"),
];
for (interp, expected) in cases {
let key = AnimKey { time: 0.0, value: 0.0, interp };
assert_eq!(anim_interp_name(&key), expected);
}
}
#[test]
fn export_single_clip_contains_name() {
let cfg = default_anim_export_config();
let clip = new_anim_clip("walk", 1.2);
let s = export_anim_clip(&clip, &cfg);
assert!(s.contains("walk"));
}
#[test]
fn export_multiple_clips_result() {
let cfg = default_anim_export_config();
let clip1 = new_anim_clip("a", 1.0);
let clip2 = new_anim_clip("b", 3.0);
let result = export_anim_clips(&[clip1, clip2], &cfg);
assert_eq!(result.clips.len(), 2);
assert!((result.duration_sec - 3.0).abs() < 1e-6);
assert_eq!(result.total_keys, 0);
}
#[test]
fn result_to_json_contains_clip_count() {
let r = AnimExportResult {
clips: vec!["c1".to_string(), "c2".to_string()],
total_keys: 10,
duration_sec: 5.0,
};
let json = anim_export_result_to_json(&r);
assert!(json.contains("\"clip_count\":2"));
assert!(json.contains("\"total_keys\":10"));
}
}