Skip to main content

oxihuman_export/
camera_clip_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Camera clip plane export.
6
7/// A camera clip plane configuration.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct CameraClipExport {
11    pub near: f32,
12    pub far: f32,
13    pub fov_y_rad: f32,
14    pub aspect: f32,
15}
16
17/// Keyframe for animated clip planes.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct ClipKeyframe {
21    pub time: f32,
22    pub near: f32,
23    pub far: f32,
24}
25
26/// Animated clip plane export.
27#[allow(dead_code)]
28#[derive(Debug, Clone)]
29pub struct CameraClipAnimation {
30    pub keyframes: Vec<ClipKeyframe>,
31}
32
33/// Default perspective clip config.
34#[allow(dead_code)]
35pub fn default_camera_clip() -> CameraClipExport {
36    use std::f32::consts::FRAC_PI_4;
37    CameraClipExport {
38        near: 0.01,
39        far: 1000.0,
40        fov_y_rad: FRAC_PI_4,
41        aspect: 16.0 / 9.0,
42    }
43}
44
45/// Clip range (far - near).
46#[allow(dead_code)]
47pub fn clip_range(cam: &CameraClipExport) -> f32 {
48    cam.far - cam.near
49}
50
51/// Validate clip: near > 0, far > near.
52#[allow(dead_code)]
53pub fn validate_clip(cam: &CameraClipExport) -> bool {
54    cam.near > 0.0 && cam.far > cam.near && cam.aspect > 0.0
55}
56
57/// New animation.
58#[allow(dead_code)]
59pub fn new_clip_animation() -> CameraClipAnimation {
60    CameraClipAnimation {
61        keyframes: Vec::new(),
62    }
63}
64
65/// Add a keyframe.
66#[allow(dead_code)]
67pub fn add_clip_keyframe(anim: &mut CameraClipAnimation, time: f32, near: f32, far: f32) {
68    anim.keyframes.push(ClipKeyframe { time, near, far });
69}
70
71/// Keyframe count.
72#[allow(dead_code)]
73pub fn clip_keyframe_count(anim: &CameraClipAnimation) -> usize {
74    anim.keyframes.len()
75}
76
77/// Duration of clip animation.
78#[allow(dead_code)]
79pub fn clip_animation_duration(anim: &CameraClipAnimation) -> f32 {
80    anim.keyframes
81        .iter()
82        .map(|k| k.time)
83        .fold(0.0_f32, f32::max)
84}
85
86/// Sample near/far at time t (linear interpolation between keyframes).
87#[allow(dead_code)]
88pub fn sample_clip_at(anim: &CameraClipAnimation, t: f32) -> Option<(f32, f32)> {
89    if anim.keyframes.is_empty() {
90        return None;
91    }
92    let kf = &anim.keyframes;
93    if t <= kf[0].time {
94        return Some((kf[0].near, kf[0].far));
95    }
96    if t >= kf[kf.len() - 1].time {
97        let last = &kf[kf.len() - 1];
98        return Some((last.near, last.far));
99    }
100    for i in 0..kf.len() - 1 {
101        let a = &kf[i];
102        let b = &kf[i + 1];
103        if t >= a.time && t <= b.time {
104            let dt = b.time - a.time;
105            let alpha = if dt < 1e-12 { 0.0 } else { (t - a.time) / dt };
106            return Some((
107                a.near + alpha * (b.near - a.near),
108                a.far + alpha * (b.far - a.far),
109            ));
110        }
111    }
112    None
113}
114
115/// Export to JSON.
116#[allow(dead_code)]
117pub fn camera_clip_to_json(cam: &CameraClipExport) -> String {
118    format!(
119        "{{\"near\":{:.6},\"far\":{:.6},\"fov_y\":{:.6},\"aspect\":{:.6}}}",
120        cam.near, cam.far, cam.fov_y_rad, cam.aspect
121    )
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    use std::f32::consts::FRAC_PI_4;
128
129    #[test]
130    fn test_default_camera_clip() {
131        let cam = default_camera_clip();
132        assert!(validate_clip(&cam));
133    }
134
135    #[test]
136    fn test_clip_range() {
137        let cam = CameraClipExport {
138            near: 0.1,
139            far: 100.0,
140            fov_y_rad: FRAC_PI_4,
141            aspect: 1.0,
142        };
143        assert!((clip_range(&cam) - 99.9).abs() < 1e-3);
144    }
145
146    #[test]
147    fn test_validate_clip_invalid() {
148        let cam = CameraClipExport {
149            near: -1.0,
150            far: 10.0,
151            fov_y_rad: FRAC_PI_4,
152            aspect: 1.0,
153        };
154        assert!(!validate_clip(&cam));
155    }
156
157    #[test]
158    fn test_add_clip_keyframe() {
159        let mut anim = new_clip_animation();
160        add_clip_keyframe(&mut anim, 0.0, 0.01, 100.0);
161        assert_eq!(clip_keyframe_count(&anim), 1);
162    }
163
164    #[test]
165    fn test_clip_animation_duration() {
166        let mut anim = new_clip_animation();
167        add_clip_keyframe(&mut anim, 0.0, 0.01, 100.0);
168        add_clip_keyframe(&mut anim, 2.0, 0.01, 200.0);
169        assert!((clip_animation_duration(&anim) - 2.0).abs() < 1e-6);
170    }
171
172    #[test]
173    fn test_sample_clip_empty() {
174        let anim = new_clip_animation();
175        assert!(sample_clip_at(&anim, 1.0).is_none());
176    }
177
178    #[test]
179    fn test_sample_clip_at_midpoint() {
180        let mut anim = new_clip_animation();
181        add_clip_keyframe(&mut anim, 0.0, 0.1, 100.0);
182        add_clip_keyframe(&mut anim, 2.0, 0.1, 200.0);
183        let (_, far) = sample_clip_at(&anim, 1.0).expect("should succeed");
184        assert!((far - 150.0).abs() < 1e-3);
185    }
186
187    #[test]
188    fn test_camera_clip_to_json() {
189        let cam = default_camera_clip();
190        let j = camera_clip_to_json(&cam);
191        assert!(j.contains("\"near\":"));
192    }
193
194    #[test]
195    fn test_fov_in_range() {
196        let cam = default_camera_clip();
197        assert!((0.0..=std::f32::consts::PI).contains(&cam.fov_y_rad));
198    }
199
200    #[test]
201    fn test_clip_range_positive() {
202        let cam = default_camera_clip();
203        assert!(clip_range(&cam) > 0.0);
204    }
205}