Skip to main content

oxihuman_export/
resolve_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! DaVinci Resolve timeline stub export.
6
7/// Resolve timeline type.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ResolveTimelineType {
10    Edit,
11    Color,
12    Fusion,
13    Deliver,
14}
15
16/// A Resolve clip entry.
17#[derive(Debug, Clone)]
18pub struct ResolveClip {
19    pub name: String,
20    pub start_frame: i32,
21    pub end_frame: i32,
22    pub reel: String,
23}
24
25/// A DaVinci Resolve timeline export.
26#[derive(Debug, Clone)]
27pub struct ResolveTimeline {
28    pub name: String,
29    pub timeline_type: ResolveTimelineType,
30    pub fps: f32,
31    pub clips: Vec<ResolveClip>,
32}
33
34/// Create a new Resolve timeline export.
35pub fn new_resolve_timeline(name: &str, fps: f32, ttype: ResolveTimelineType) -> ResolveTimeline {
36    ResolveTimeline {
37        name: name.to_string(),
38        timeline_type: ttype,
39        fps,
40        clips: Vec::new(),
41    }
42}
43
44/// Add a clip to the timeline.
45pub fn resolve_add_clip(tl: &mut ResolveTimeline, name: &str, start: i32, end: i32, reel: &str) {
46    tl.clips.push(ResolveClip {
47        name: name.to_string(),
48        start_frame: start,
49        end_frame: end,
50        reel: reel.to_string(),
51    });
52}
53
54/// Return the clip count.
55pub fn resolve_clip_count(tl: &ResolveTimeline) -> usize {
56    tl.clips.len()
57}
58
59/// Total timeline duration in frames.
60pub fn resolve_duration_frames(tl: &ResolveTimeline) -> i32 {
61    tl.clips.iter().map(|c| c.end_frame).max().unwrap_or(0)
62}
63
64/// Validate the timeline.
65pub fn validate_resolve_timeline(tl: &ResolveTimeline) -> bool {
66    tl.fps > 0.0 && tl.clips.iter().all(|c| c.end_frame > c.start_frame)
67}
68
69/// Generate a stub Resolve Python API script.
70pub fn resolve_to_python(tl: &ResolveTimeline) -> String {
71    let mut out =
72        String::from("import DaVinciResolveScript as dvr\nresolve = dvr.scriptapp('Resolve')\n");
73    out.push_str("proj = resolve.GetProjectManager().GetCurrentProject()\n");
74    out.push_str(&format!("timeline = proj.CreateTimeline('{}')\n", tl.name));
75    for clip in &tl.clips {
76        out.push_str(&format!(
77            "timeline.AddClip('{}', {}, {})\n",
78            clip.name, clip.start_frame, clip.end_frame
79        ));
80    }
81    out
82}
83
84/// Estimate the script size.
85pub fn resolve_script_size(tl: &ResolveTimeline) -> usize {
86    resolve_to_python(tl).len()
87}
88
89/// Find a clip by reel name.
90pub fn resolve_clips_for_reel<'a>(tl: &'a ResolveTimeline, reel: &str) -> Vec<&'a ResolveClip> {
91    tl.clips.iter().filter(|c| c.reel == reel).collect()
92}
93
94/// Timeline type name as string.
95pub fn timeline_type_name(tl: &ResolveTimeline) -> &'static str {
96    match tl.timeline_type {
97        ResolveTimelineType::Edit => "Edit",
98        ResolveTimelineType::Color => "Color",
99        ResolveTimelineType::Fusion => "Fusion",
100        ResolveTimelineType::Deliver => "Deliver",
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    fn sample_tl() -> ResolveTimeline {
109        let mut tl = new_resolve_timeline("MyEdit", 24.0, ResolveTimelineType::Edit);
110        resolve_add_clip(&mut tl, "shot_010", 1, 100, "A001");
111        resolve_add_clip(&mut tl, "shot_020", 101, 200, "A001");
112        tl
113    }
114
115    #[test]
116    fn test_clip_count() {
117        assert_eq!(resolve_clip_count(&sample_tl()), 2);
118    }
119
120    #[test]
121    fn test_duration() {
122        assert_eq!(resolve_duration_frames(&sample_tl()), 200);
123    }
124
125    #[test]
126    fn test_validate_valid() {
127        assert!(validate_resolve_timeline(&sample_tl()));
128    }
129
130    #[test]
131    fn test_validate_zero_fps() {
132        let tl = new_resolve_timeline("bad", 0.0, ResolveTimelineType::Edit);
133        assert!(!validate_resolve_timeline(&tl));
134    }
135
136    #[test]
137    fn test_to_python() {
138        let tl = sample_tl();
139        assert!(resolve_to_python(&tl).contains("shot_010"));
140    }
141
142    #[test]
143    fn test_clips_for_reel() {
144        let tl = sample_tl();
145        let reel_clips = resolve_clips_for_reel(&tl, "A001");
146        assert_eq!(reel_clips.len(), 2);
147    }
148
149    #[test]
150    fn test_timeline_type_name() {
151        let tl = sample_tl();
152        assert_eq!(timeline_type_name(&tl), "Edit");
153    }
154
155    #[test]
156    fn test_script_size() {
157        assert!(resolve_script_size(&sample_tl()) > 0);
158    }
159
160    #[test]
161    fn test_empty_duration() {
162        let tl = new_resolve_timeline("empty", 25.0, ResolveTimelineType::Color);
163        assert_eq!(resolve_duration_frames(&tl), 0);
164    }
165}