Skip to main content

oxihuman_export/
camera_track_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Camera track export: animated camera path keyframes.
6
7/// A camera keyframe.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy)]
10pub struct CameraKeyframe {
11    pub time: f32,
12    pub position: [f32; 3],
13    pub target: [f32; 3],
14    pub fov_deg: f32,
15}
16
17/// A camera track export.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct CameraTrackExport {
21    pub name: String,
22    pub keyframes: Vec<CameraKeyframe>,
23    pub fps: f32,
24}
25
26/// Create a new camera track.
27#[allow(dead_code)]
28pub fn new_camera_track(name: &str, fps: f32) -> CameraTrackExport {
29    CameraTrackExport {
30        name: name.to_string(),
31        keyframes: Vec::new(),
32        fps,
33    }
34}
35
36/// Add a keyframe.
37#[allow(dead_code)]
38pub fn add_camera_keyframe(track: &mut CameraTrackExport, kf: CameraKeyframe) {
39    track.keyframes.push(kf);
40    track.keyframes.sort_by(|a, b| {
41        a.time
42            .partial_cmp(&b.time)
43            .unwrap_or(std::cmp::Ordering::Equal)
44    });
45}
46
47/// Keyframe count.
48#[allow(dead_code)]
49pub fn camera_keyframe_count(track: &CameraTrackExport) -> usize {
50    track.keyframes.len()
51}
52
53/// Duration (time of last keyframe).
54#[allow(dead_code)]
55pub fn camera_track_duration(track: &CameraTrackExport) -> f32 {
56    track.keyframes.last().map_or(0.0, |k| k.time)
57}
58
59/// Sample position by linear interpolation at time t.
60#[allow(dead_code)]
61pub fn sample_camera_position(track: &CameraTrackExport, t: f32) -> [f32; 3] {
62    let kfs = &track.keyframes;
63    if kfs.is_empty() {
64        return [0.0; 3];
65    }
66    if t <= kfs[0].time {
67        return kfs[0].position;
68    }
69    if t >= kfs[kfs.len() - 1].time {
70        return kfs[kfs.len() - 1].position;
71    }
72    for i in 1..kfs.len() {
73        if kfs[i].time >= t {
74            let span = kfs[i].time - kfs[i - 1].time;
75            let alpha = if span > 0.0 {
76                (t - kfs[i - 1].time) / span
77            } else {
78                0.0
79            };
80            let a = kfs[i - 1].position;
81            let b = kfs[i].position;
82            return [
83                a[0] + alpha * (b[0] - a[0]),
84                a[1] + alpha * (b[1] - a[1]),
85                a[2] + alpha * (b[2] - a[2]),
86            ];
87        }
88    }
89    kfs[kfs.len() - 1].position
90}
91
92/// Validate: fov in (0, 180).
93#[allow(dead_code)]
94pub fn validate_camera_track(track: &CameraTrackExport) -> bool {
95    track
96        .keyframes
97        .iter()
98        .all(|k| k.fov_deg > 0.0 && k.fov_deg < 180.0)
99}
100
101/// Serialise to JSON.
102#[allow(dead_code)]
103pub fn camera_track_to_json(track: &CameraTrackExport) -> String {
104    format!(
105        "{{\"name\":\"{}\",\"keyframe_count\":{},\"duration\":{}}}",
106        track.name,
107        camera_keyframe_count(track),
108        camera_track_duration(track)
109    )
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn kf(t: f32) -> CameraKeyframe {
117        CameraKeyframe {
118            time: t,
119            position: [t, 0.0, 0.0],
120            target: [0.0; 3],
121            fov_deg: 60.0,
122        }
123    }
124
125    #[test]
126    fn new_track_empty() {
127        let t = new_camera_track("main", 24.0);
128        assert_eq!(camera_keyframe_count(&t), 0);
129    }
130
131    #[test]
132    fn add_keyframe_increments() {
133        let mut t = new_camera_track("main", 24.0);
134        add_camera_keyframe(&mut t, kf(0.0));
135        assert_eq!(camera_keyframe_count(&t), 1);
136    }
137
138    #[test]
139    fn duration_last_kf() {
140        let mut t = new_camera_track("main", 24.0);
141        add_camera_keyframe(&mut t, kf(0.0));
142        add_camera_keyframe(&mut t, kf(2.0));
143        assert!((camera_track_duration(&t) - 2.0).abs() < 1e-5);
144    }
145
146    #[test]
147    fn sample_before_start() {
148        let mut t = new_camera_track("c", 24.0);
149        add_camera_keyframe(&mut t, kf(1.0));
150        let p = sample_camera_position(&t, 0.0);
151        assert!((p[0] - 1.0).abs() < 1e-5);
152    }
153
154    #[test]
155    fn sample_midpoint() {
156        let mut t = new_camera_track("c", 24.0);
157        add_camera_keyframe(&mut t, kf(0.0));
158        add_camera_keyframe(&mut t, kf(2.0));
159        let p = sample_camera_position(&t, 1.0);
160        assert!((p[0] - 1.0).abs() < 1e-5);
161    }
162
163    #[test]
164    fn validate_valid_fov() {
165        let mut t = new_camera_track("c", 24.0);
166        add_camera_keyframe(&mut t, kf(0.0));
167        assert!(validate_camera_track(&t));
168    }
169
170    #[test]
171    fn json_contains_name() {
172        let t = new_camera_track("hero_cam", 30.0);
173        let j = camera_track_to_json(&t);
174        assert!(j.contains("hero_cam"));
175    }
176
177    #[test]
178    fn fov_in_valid_range() {
179        let kf = kf(0.0);
180        assert!((0.0..=180.0).contains(&kf.fov_deg));
181    }
182
183    #[test]
184    fn keyframes_sorted_after_add() {
185        let mut t = new_camera_track("c", 24.0);
186        add_camera_keyframe(&mut t, kf(2.0));
187        add_camera_keyframe(&mut t, kf(0.5));
188        assert!(t.keyframes[0].time <= t.keyframes[1].time);
189    }
190
191    #[test]
192    fn sample_empty_returns_origin() {
193        let t = new_camera_track("c", 24.0);
194        let p = sample_camera_position(&t, 1.0);
195        assert!((p[0]).abs() < 1e-6);
196    }
197}