#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct RigAnimExportConfig {
pub frame_rate: f32,
pub precision: usize,
pub include_scale: bool,
}
#[derive(Debug, Clone)]
pub struct BoneKeyframe {
pub bone_name: String,
pub time_sec: f32,
pub translation: [f64; 3],
pub rotation: [f64; 4],
pub scale: [f64; 3],
}
#[derive(Debug, Clone)]
pub struct RigAnimExportResult {
pub keyframes: Vec<BoneKeyframe>,
pub total_bytes: usize,
}
pub fn default_rig_anim_config() -> RigAnimExportConfig {
RigAnimExportConfig {
frame_rate: 30.0,
precision: 6,
include_scale: true,
}
}
pub fn new_rig_anim_export() -> RigAnimExportResult {
RigAnimExportResult {
keyframes: Vec::new(),
total_bytes: 0,
}
}
pub fn rig_anim_add_keyframe(result: &mut RigAnimExportResult, kf: BoneKeyframe) {
result.keyframes.push(kf);
}
pub fn rig_anim_keyframe_count(result: &RigAnimExportResult) -> usize {
result.keyframes.len()
}
pub fn rig_anim_bone_count(result: &RigAnimExportResult) -> usize {
let mut names: Vec<&str> = result.keyframes.iter().map(|k| k.bone_name.as_str()).collect();
names.sort_unstable();
names.dedup();
names.len()
}
pub fn rig_anim_duration(result: &RigAnimExportResult) -> f32 {
match (result.keyframes.first(), result.keyframes.last()) {
(Some(first), Some(last)) => (last.time_sec - first.time_sec).max(0.0),
_ => 0.0,
}
}
pub fn rig_anim_frame_rate(cfg: &RigAnimExportConfig) -> f32 {
cfg.frame_rate
}
pub fn rig_anim_to_json(result: &RigAnimExportResult, cfg: &RigAnimExportConfig) -> String {
let prec = cfg.precision;
let mut out = String::from("{\"keyframes\":[\n");
for (i, kf) in result.keyframes.iter().enumerate() {
let comma = if i + 1 < result.keyframes.len() { "," } else { "" };
let t = kf.translation;
let r = kf.rotation;
let s = kf.scale;
let mut entry = format!(
" {{\"bone\":\"{}\",\"time\":{:.prec$},\
\"translation\":[{:.prec$},{:.prec$},{:.prec$}],\
\"rotation\":[{:.prec$},{:.prec$},{:.prec$},{:.prec$}]",
kf.bone_name, kf.time_sec, t[0], t[1], t[2], r[0], r[1], r[2], r[3],
);
if cfg.include_scale {
entry.push_str(&format!(
",\"scale\":[{:.prec$},{:.prec$},{:.prec$}]",
s[0], s[1], s[2]
));
}
entry.push('}');
out.push_str(&entry);
out.push_str(comma);
out.push('\n');
}
out.push_str("]}");
out
}
pub fn rig_anim_write_to_file(
result: &mut RigAnimExportResult,
cfg: &RigAnimExportConfig,
_path: &str,
) -> usize {
let json = rig_anim_to_json(result, cfg);
result.total_bytes = json.len();
result.total_bytes
}
pub fn rig_anim_clear(result: &mut RigAnimExportResult) {
result.keyframes.clear();
result.total_bytes = 0;
}
fn sample_keyframe(bone: &str, t: f32) -> BoneKeyframe {
BoneKeyframe {
bone_name: bone.to_string(),
time_sec: t,
translation: [0.0, 0.0, 0.0],
rotation: [0.0, 0.0, 0.0, 1.0],
scale: [1.0, 1.0, 1.0],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let cfg = default_rig_anim_config();
assert!((cfg.frame_rate - 30.0).abs() < 1e-5);
assert_eq!(cfg.precision, 6);
assert!(cfg.include_scale);
}
#[test]
fn new_export_is_empty() {
let r = new_rig_anim_export();
assert_eq!(rig_anim_keyframe_count(&r), 0);
}
#[test]
fn add_keyframe_increments_count() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 0.0));
assert_eq!(rig_anim_keyframe_count(&r), 1);
}
#[test]
fn bone_count_unique_only() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 0.0));
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 1.0));
rig_anim_add_keyframe(&mut r, sample_keyframe("spine", 0.0));
assert_eq!(rig_anim_bone_count(&r), 2);
}
#[test]
fn duration_two_frames() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 0.0));
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 3.0));
assert!((rig_anim_duration(&r) - 3.0).abs() < 1e-5);
}
#[test]
fn duration_empty_is_zero() {
let r = new_rig_anim_export();
assert!((rig_anim_duration(&r) - 0.0).abs() < 1e-5);
}
#[test]
fn json_contains_bone_and_rotation() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("neck", 0.0));
let cfg = default_rig_anim_config();
let json = rig_anim_to_json(&r, &cfg);
assert!(json.contains("\"bone\""));
assert!(json.contains("\"rotation\""));
assert!(json.contains("\"scale\""));
assert!(json.contains("neck"));
}
#[test]
fn frame_rate_accessor() {
let cfg = default_rig_anim_config();
assert!((rig_anim_frame_rate(&cfg) - 30.0).abs() < 1e-5);
}
#[test]
fn write_to_file_sets_total_bytes() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 0.0));
let cfg = default_rig_anim_config();
let n = rig_anim_write_to_file(&mut r, &cfg, "/tmp/rig.json");
assert!(n > 0);
assert_eq!(r.total_bytes, n);
}
#[test]
fn clear_resets_state() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("hips", 0.0));
let cfg = default_rig_anim_config();
rig_anim_write_to_file(&mut r, &cfg, "/tmp/rig.json");
rig_anim_clear(&mut r);
assert_eq!(rig_anim_keyframe_count(&r), 0);
assert_eq!(r.total_bytes, 0);
}
#[test]
fn no_scale_in_json_when_disabled() {
let mut r = new_rig_anim_export();
rig_anim_add_keyframe(&mut r, sample_keyframe("spine", 0.0));
let mut cfg = default_rig_anim_config();
cfg.include_scale = false;
let json = rig_anim_to_json(&r, &cfg);
assert!(!json.contains("\"scale\""));
}
}