Skip to main content

oxihuman_export/
camera_fov_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Camera field-of-view export for scene cameras.
6
7use std::f32::consts::PI;
8
9/// Camera FOV keyframe.
10#[allow(dead_code)]
11#[derive(Debug, Clone, Copy)]
12pub struct FovKeyframe {
13    pub time: f32,
14    pub fov_degrees: f32,
15}
16
17/// Camera FOV export data.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct CameraFovExport {
21    pub camera_name: String,
22    pub keyframes: Vec<FovKeyframe>,
23}
24
25/// Create new camera FOV export.
26#[allow(dead_code)]
27pub fn new_camera_fov_export(name: &str) -> CameraFovExport {
28    CameraFovExport {
29        camera_name: name.to_string(),
30        keyframes: vec![],
31    }
32}
33
34/// Add keyframe.
35#[allow(dead_code)]
36pub fn add_fov_keyframe(e: &mut CameraFovExport, time: f32, fov: f32) {
37    e.keyframes.push(FovKeyframe {
38        time,
39        fov_degrees: fov,
40    });
41}
42
43/// Keyframe count.
44#[allow(dead_code)]
45pub fn fov_keyframe_count(e: &CameraFovExport) -> usize {
46    e.keyframes.len()
47}
48
49/// Convert FOV degrees to radians.
50#[allow(dead_code)]
51pub fn fov_to_radians(fov_deg: f32) -> f32 {
52    fov_deg * PI / 180.0
53}
54
55/// Convert FOV to focal length (given sensor width).
56#[allow(dead_code)]
57pub fn fov_to_focal_length(fov_deg: f32, sensor_width: f32) -> f32 {
58    sensor_width / (2.0 * (fov_to_radians(fov_deg) / 2.0).tan())
59}
60
61/// Duration.
62#[allow(dead_code)]
63pub fn fov_duration(e: &CameraFovExport) -> f32 {
64    if e.keyframes.is_empty() {
65        return 0.0;
66    }
67    let min = e.keyframes.iter().map(|k| k.time).fold(f32::MAX, f32::min);
68    let max = e.keyframes.iter().map(|k| k.time).fold(f32::MIN, f32::max);
69    max - min
70}
71
72/// Validate.
73#[allow(dead_code)]
74pub fn fov_validate(e: &CameraFovExport) -> bool {
75    e.keyframes
76        .iter()
77        .all(|k| k.fov_degrees > 0.0 && k.fov_degrees < 180.0 && k.time >= 0.0)
78}
79
80/// Export to JSON.
81#[allow(dead_code)]
82pub fn camera_fov_to_json(e: &CameraFovExport) -> String {
83    format!(
84        "{{\"camera\":\"{}\",\"keyframes\":{},\"duration\":{:.6}}}",
85        e.camera_name,
86        fov_keyframe_count(e),
87        fov_duration(e)
88    )
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    #[test]
95    fn test_new() {
96        let e = new_camera_fov_export("cam1");
97        assert_eq!(e.camera_name, "cam1");
98    }
99    #[test]
100    fn test_add() {
101        let mut e = new_camera_fov_export("c");
102        add_fov_keyframe(&mut e, 0.0, 60.0);
103        assert_eq!(fov_keyframe_count(&e), 1);
104    }
105    #[test]
106    fn test_to_radians() {
107        assert!((fov_to_radians(90.0) - PI / 2.0).abs() < 1e-5);
108    }
109    #[test]
110    fn test_focal_length() {
111        let fl = fov_to_focal_length(90.0, 36.0);
112        assert!(fl > 0.0);
113    }
114    #[test]
115    fn test_duration() {
116        let mut e = new_camera_fov_export("c");
117        add_fov_keyframe(&mut e, 0.0, 60.0);
118        add_fov_keyframe(&mut e, 2.0, 90.0);
119        assert!((fov_duration(&e) - 2.0).abs() < 1e-6);
120    }
121    #[test]
122    fn test_duration_empty() {
123        let e = new_camera_fov_export("c");
124        assert!((fov_duration(&e)).abs() < 1e-9);
125    }
126    #[test]
127    fn test_validate() {
128        let mut e = new_camera_fov_export("c");
129        add_fov_keyframe(&mut e, 0.0, 60.0);
130        assert!(fov_validate(&e));
131    }
132    #[test]
133    fn test_validate_bad() {
134        let mut e = new_camera_fov_export("c");
135        add_fov_keyframe(&mut e, 0.0, 200.0);
136        assert!(!fov_validate(&e));
137    }
138    #[test]
139    fn test_to_json() {
140        let e = new_camera_fov_export("cam");
141        let j = camera_fov_to_json(&e);
142        assert!(j.contains("\"camera\":\"cam\""));
143    }
144    #[test]
145    fn test_focal_known() {
146        let fl = fov_to_focal_length(53.13, 36.0);
147        assert!(fl > 15.0 && fl < 40.0);
148    }
149}