Skip to main content

oxihuman_export/
camera_path_export_v2.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export a camera path (v2) as a spline sequence.
6
7/// A single camera path keyframe.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct CamPathKeyV2 {
11    pub time: f32,
12    pub position: [f32; 3],
13    pub target: [f32; 3],
14    pub fov_deg: f32,
15}
16
17/// A camera path v2 export.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct CameraPathV2 {
21    pub keys: Vec<CamPathKeyV2>,
22    pub name: String,
23}
24
25/// Create a new camera path v2.
26#[allow(dead_code)]
27pub fn new_camera_path_v2(name: &str) -> CameraPathV2 {
28    CameraPathV2 {
29        keys: Vec::new(),
30        name: name.to_string(),
31    }
32}
33
34/// Add a keyframe.
35#[allow(dead_code)]
36pub fn add_cam_path_key_v2(path: &mut CameraPathV2, key: CamPathKeyV2) {
37    path.keys.push(key);
38    path.keys.sort_by(|a, b| {
39        a.time
40            .partial_cmp(&b.time)
41            .unwrap_or(std::cmp::Ordering::Equal)
42    });
43}
44
45/// Count keyframes.
46#[allow(dead_code)]
47pub fn cam_path_v2_key_count(path: &CameraPathV2) -> usize {
48    path.keys.len()
49}
50
51/// Duration of the path.
52#[allow(dead_code)]
53pub fn cam_path_v2_duration(path: &CameraPathV2) -> f32 {
54    if path.keys.is_empty() {
55        return 0.0;
56    }
57    path.keys.last().map_or(0.0, |k| k.time) - path.keys[0].time
58}
59
60/// Linear interpolate position at time t.
61#[allow(dead_code)]
62pub fn cam_path_v2_position_at(path: &CameraPathV2, t: f32) -> [f32; 3] {
63    if path.keys.is_empty() {
64        return [0.0; 3];
65    }
66    if path.keys.len() == 1 {
67        return path.keys[0].position;
68    }
69    let idx = path.keys.partition_point(|k| k.time <= t).saturating_sub(1);
70    let i0 = idx.min(path.keys.len() - 1);
71    let i1 = (idx + 1).min(path.keys.len() - 1);
72    if i0 == i1 {
73        return path.keys[i0].position;
74    }
75    let k0 = &path.keys[i0];
76    let k1 = &path.keys[i1];
77    let dt = k1.time - k0.time;
78    let f = if dt > 0.0 {
79        ((t - k0.time) / dt).clamp(0.0, 1.0)
80    } else {
81        0.0
82    };
83    [
84        k0.position[0] + f * (k1.position[0] - k0.position[0]),
85        k0.position[1] + f * (k1.position[1] - k0.position[1]),
86        k0.position[2] + f * (k1.position[2] - k0.position[2]),
87    ]
88}
89
90/// Validate the path (ascending times, positive fov).
91#[allow(dead_code)]
92pub fn validate_cam_path_v2(path: &CameraPathV2) -> bool {
93    path.keys.windows(2).all(|w| w[1].time > w[0].time) && path.keys.iter().all(|k| k.fov_deg > 0.0)
94}
95
96/// Serialize to JSON.
97#[allow(dead_code)]
98pub fn cam_path_v2_to_json(path: &CameraPathV2) -> String {
99    format!(
100        "{{\"name\":\"{}\",\"key_count\":{}}}",
101        path.name,
102        path.keys.len()
103    )
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    fn sample_path() -> CameraPathV2 {
111        let mut p = new_camera_path_v2("main");
112        add_cam_path_key_v2(
113            &mut p,
114            CamPathKeyV2 {
115                time: 0.0,
116                position: [0.0, 0.0, 0.0],
117                target: [0.0, 0.0, 1.0],
118                fov_deg: 60.0,
119            },
120        );
121        add_cam_path_key_v2(
122            &mut p,
123            CamPathKeyV2 {
124                time: 1.0,
125                position: [1.0, 0.0, 0.0],
126                target: [1.0, 0.0, 1.0],
127                fov_deg: 60.0,
128            },
129        );
130        p
131    }
132
133    #[test]
134    fn test_key_count() {
135        let p = sample_path();
136        assert_eq!(cam_path_v2_key_count(&p), 2);
137    }
138
139    #[test]
140    fn test_duration() {
141        let p = sample_path();
142        assert!((cam_path_v2_duration(&p) - 1.0).abs() < 1e-5);
143    }
144
145    #[test]
146    fn test_position_at_start() {
147        let p = sample_path();
148        let pos = cam_path_v2_position_at(&p, 0.0);
149        assert!(pos[0].abs() < 1e-5);
150    }
151
152    #[test]
153    fn test_position_at_end() {
154        let p = sample_path();
155        let pos = cam_path_v2_position_at(&p, 1.0);
156        assert!((pos[0] - 1.0).abs() < 1e-5);
157    }
158
159    #[test]
160    fn test_position_at_mid() {
161        let p = sample_path();
162        let pos = cam_path_v2_position_at(&p, 0.5);
163        assert!((pos[0] - 0.5).abs() < 1e-5);
164    }
165
166    #[test]
167    fn test_validate_valid() {
168        let p = sample_path();
169        assert!(validate_cam_path_v2(&p));
170    }
171
172    #[test]
173    fn test_validate_empty() {
174        let p = new_camera_path_v2("x");
175        assert!(validate_cam_path_v2(&p));
176    }
177
178    #[test]
179    fn test_cam_path_v2_to_json() {
180        let p = sample_path();
181        let j = cam_path_v2_to_json(&p);
182        assert!(j.contains("key_count"));
183    }
184
185    #[test]
186    fn test_empty_path_duration_zero() {
187        let p = new_camera_path_v2("x");
188        assert!(cam_path_v2_duration(&p).abs() < 1e-6);
189    }
190
191    #[test]
192    fn test_empty_path_position_zero() {
193        let p = new_camera_path_v2("x");
194        let pos = cam_path_v2_position_at(&p, 0.5);
195        assert_eq!(pos, [0.0; 3]);
196    }
197}