#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CameraPathExportConfig {
pub precision: usize,
pub include_fov: bool,
}
#[derive(Debug, Clone)]
pub struct CameraKeyframe {
pub time_sec: f32,
pub position: [f64; 3],
pub target: [f64; 3],
pub up: [f64; 3],
pub fov_deg: f32,
}
#[derive(Debug, Clone)]
pub struct CameraPathExportResult {
pub keyframes: Vec<CameraKeyframe>,
pub total_bytes: usize,
}
#[allow(dead_code)]
pub fn default_camera_path_config() -> CameraPathExportConfig {
CameraPathExportConfig {
precision: 6,
include_fov: true,
}
}
#[allow(dead_code)]
pub fn new_camera_path_export() -> CameraPathExportResult {
CameraPathExportResult {
keyframes: Vec::new(),
total_bytes: 0,
}
}
#[allow(dead_code)]
pub fn camera_path_add_keyframe(result: &mut CameraPathExportResult, kf: CameraKeyframe) {
result.keyframes.push(kf);
}
#[allow(dead_code)]
pub fn camera_path_to_json(
result: &CameraPathExportResult,
cfg: &CameraPathExportConfig,
) -> 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 pos = format!(
"[{:.prec$},{:.prec$},{:.prec$}]",
kf.position[0], kf.position[1], kf.position[2]
);
let tgt = format!(
"[{:.prec$},{:.prec$},{:.prec$}]",
kf.target[0], kf.target[1], kf.target[2]
);
let up = format!(
"[{:.prec$},{:.prec$},{:.prec$}]",
kf.up[0], kf.up[1], kf.up[2]
);
let mut entry = format!(
" {{\"time\":{:.prec$},\"position\":{},\"target\":{},\"up\":{}",
kf.time_sec, pos, tgt, up
);
if cfg.include_fov {
entry.push_str(&format!(",\"fov\":{:.prec$}", kf.fov_deg));
}
entry.push('}');
out.push_str(&entry);
out.push_str(comma);
out.push('\n');
}
out.push_str("]}");
out
}
#[allow(dead_code)]
pub fn camera_path_to_csv(
result: &CameraPathExportResult,
cfg: &CameraPathExportConfig,
) -> String {
let prec = cfg.precision;
let mut header = String::from("time,px,py,pz,tx,ty,tz,ux,uy,uz");
if cfg.include_fov {
header.push_str(",fov");
}
header.push('\n');
let mut out = header;
for kf in &result.keyframes {
let row = format!(
"{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$},{:.prec$}",
kf.time_sec,
kf.position[0], kf.position[1], kf.position[2],
kf.target[0], kf.target[1], kf.target[2],
kf.up[0], kf.up[1], kf.up[2],
);
out.push_str(&row);
if cfg.include_fov {
out.push_str(&format!(",{:.prec$}", kf.fov_deg));
}
out.push('\n');
}
out
}
#[allow(dead_code)]
pub fn camera_path_keyframe_count(result: &CameraPathExportResult) -> usize {
result.keyframes.len()
}
#[allow(dead_code)]
pub fn camera_path_duration(result: &CameraPathExportResult) -> f32 {
match (result.keyframes.first(), result.keyframes.last()) {
(Some(first), Some(last)) => last.time_sec - first.time_sec,
_ => 0.0,
}
}
#[allow(dead_code)]
pub fn camera_path_write_to_file(
result: &mut CameraPathExportResult,
cfg: &CameraPathExportConfig,
_path: &str,
) -> usize {
let json = camera_path_to_json(result, cfg);
result.total_bytes = json.len();
result.total_bytes
}
#[allow(dead_code)]
pub fn camera_path_clear(result: &mut CameraPathExportResult) {
result.keyframes.clear();
result.total_bytes = 0;
}
#[allow(dead_code)]
pub fn camera_path_total_bytes(result: &CameraPathExportResult) -> usize {
result.total_bytes
}
fn sample_keyframe(t: f32) -> CameraKeyframe {
CameraKeyframe {
time_sec: t,
position: [0.0, 2.0, -5.0],
target: [0.0, 0.9, 0.0],
up: [0.0, 1.0, 0.0],
fov_deg: 60.0,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_includes_fov() {
let cfg = default_camera_path_config();
assert!(cfg.include_fov);
assert_eq!(cfg.precision, 6);
}
#[test]
fn new_export_is_empty() {
let r = new_camera_path_export();
assert_eq!(camera_path_keyframe_count(&r), 0);
}
#[test]
fn add_keyframe_increases_count() {
let mut r = new_camera_path_export();
camera_path_add_keyframe(&mut r, sample_keyframe(0.0));
assert_eq!(camera_path_keyframe_count(&r), 1);
}
#[test]
fn duration_of_two_frames() {
let mut r = new_camera_path_export();
camera_path_add_keyframe(&mut r, sample_keyframe(0.0));
camera_path_add_keyframe(&mut r, sample_keyframe(2.0));
let dur = camera_path_duration(&r);
assert!((dur - 2.0).abs() < 1e-5);
}
#[test]
fn duration_empty_is_zero() {
let r = new_camera_path_export();
assert!((camera_path_duration(&r) - 0.0).abs() < 1e-5);
}
#[test]
fn json_contains_time_and_position() {
let mut r = new_camera_path_export();
camera_path_add_keyframe(&mut r, sample_keyframe(1.5));
let cfg = default_camera_path_config();
let json = camera_path_to_json(&r, &cfg);
assert!(json.contains("\"time\""));
assert!(json.contains("\"position\""));
assert!(json.contains("\"fov\""));
}
#[test]
fn csv_starts_with_header() {
let r = new_camera_path_export();
let cfg = default_camera_path_config();
let csv = camera_path_to_csv(&r, &cfg);
assert!(csv.starts_with("time,px,py,pz"));
}
#[test]
fn write_to_file_updates_bytes() {
let mut r = new_camera_path_export();
camera_path_add_keyframe(&mut r, sample_keyframe(0.0));
let cfg = default_camera_path_config();
let bytes = camera_path_write_to_file(&mut r, &cfg, "/tmp/cam.json");
assert!(bytes > 0);
assert_eq!(camera_path_total_bytes(&r), bytes);
}
#[test]
fn clear_resets_state() {
let mut r = new_camera_path_export();
camera_path_add_keyframe(&mut r, sample_keyframe(0.0));
camera_path_clear(&mut r);
assert_eq!(camera_path_keyframe_count(&r), 0);
assert_eq!(camera_path_total_bytes(&r), 0);
}
#[test]
fn multiple_keyframes_in_json() {
let mut r = new_camera_path_export();
for i in 0..4 {
camera_path_add_keyframe(&mut r, sample_keyframe(i as f32));
}
assert_eq!(camera_path_keyframe_count(&r), 4);
let cfg = default_camera_path_config();
let json = camera_path_to_json(&r, &cfg);
assert!(json.len() > 10);
}
}