summaryrefslogtreecommitdiff
path: root/rust/crates/timeline/tests/split.rs
blob: ee9d4558e62c24eff6dc041a53b29c0010c5c20f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use scene::{
    BaseTimelineElement, Element, ElementRef, ParamValue, ParamValues, RetimeConfig, SceneTracks,
    Track, VideoElement,
};
use time::MediaTime;
use timeline::split::{RetainSide, split_elements};

fn seconds(value: f64) -> MediaTime {
    MediaTime::from_seconds_f64(value).unwrap()
}

fn build_video_element(id: &str, duration: MediaTime, retime: Option<RetimeConfig>) -> Element {
    Element::Video(VideoElement {
        base: BaseTimelineElement {
            id: id.to_string(),
            name: id.to_string(),
            start_time: MediaTime::ZERO,
            duration,
            trim_start: MediaTime::ZERO,
            trim_end: MediaTime::ZERO,
            source_duration: None,
            animations: None,
            params: ParamValues::from([
                ("transform.positionX".to_string(), ParamValue::Number(0.0)),
                ("transform.positionY".to_string(), ParamValue::Number(0.0)),
                ("transform.scaleX".to_string(), ParamValue::Number(1.0)),
                ("transform.scaleY".to_string(), ParamValue::Number(1.0)),
                ("transform.rotate".to_string(), ParamValue::Number(0.0)),
                ("opacity".to_string(), ParamValue::Number(1.0)),
            ]),
        },
        media_id: format!("media-{id}"),
        is_source_audio_enabled: None,
        hidden: None,
        retime,
        effects: None,
        masks: None,
    })
}

fn seed_tracks(element: Element) -> (SceneTracks, ElementRef) {
    let reference = ElementRef {
        track_id: "main-track".to_string(),
        element_id: element.base().id.clone(),
    };
    (
        SceneTracks {
            overlay: Vec::new(),
            main: Track::Video {
                id: "main-track".to_string(),
                name: "Main Track".to_string(),
                elements: vec![element],
                muted: false,
                hidden: false,
            },
            audio: Vec::new(),
        },
        reference,
    )
}

#[test]
fn split_produces_left_and_right_halves_with_shifted_trims() {
    let (tracks, reference) = seed_tracks(build_video_element("video-1", seconds(10.0), None));

    let result = split_elements(&tracks, &[reference], seconds(4.0), RetainSide::Both);

    assert_eq!(result.right_side_elements.len(), 1);
    let elements = result.tracks.main.elements();
    assert_eq!(elements.len(), 2);

    let left = &elements[0];
    assert_eq!(left.base().id, "video-1");
    assert_eq!(left.base().name, "video-1 (left)");
    assert_eq!(left.base().start_time, MediaTime::ZERO);
    assert_eq!(left.base().duration, seconds(4.0));
    assert_eq!(left.base().trim_start, MediaTime::ZERO);
    assert_eq!(left.base().trim_end, seconds(6.0));

    let right = &elements[1];
    assert_eq!(right.base().id, result.right_side_elements[0].element_id);
    assert_eq!(right.base().name, "video-1 (right)");
    assert_eq!(right.base().start_time, seconds(4.0));
    assert_eq!(right.base().duration, seconds(6.0));
    assert_eq!(right.base().trim_start, seconds(4.0));
    assert_eq!(right.base().trim_end, MediaTime::ZERO);
}

#[test]
fn split_scales_source_spans_by_the_retime_rate() {
    let retime = RetimeConfig {
        rate: 2.0,
        maintain_pitch: None,
    };
    let (tracks, reference) =
        seed_tracks(build_video_element("video-1", seconds(10.0), Some(retime)));

    let result = split_elements(&tracks, &[reference], seconds(4.0), RetainSide::Both);

    let elements = result.tracks.main.elements();
    let left = &elements[0];
    let right = &elements[1];
    assert_eq!(left.base().trim_end, seconds(12.0));
    assert_eq!(right.base().trim_start, seconds(8.0));
    assert_eq!(
        left.base().trim_start + left.base().duration * 2 + left.base().trim_end,
        right.base().trim_start + right.base().duration * 2 + right.base().trim_end
    );
    assert_eq!(timeline::element_retime(left), Some(retime));
    assert_eq!(timeline::element_retime(right), Some(retime));
}

#[test]
fn retain_left_keeps_only_the_left_half() {
    let (tracks, reference) = seed_tracks(build_video_element("video-1", seconds(10.0), None));

    let result = split_elements(&tracks, &[reference], seconds(4.0), RetainSide::Left);

    assert_eq!(result.right_side_elements.len(), 0);
    let elements = result.tracks.main.elements();
    assert_eq!(elements.len(), 1);
    assert_eq!(elements[0].base().id, "video-1");
    assert_eq!(elements[0].base().duration, seconds(4.0));
    assert_eq!(elements[0].base().trim_end, seconds(6.0));
}

#[test]
fn retain_right_keeps_only_the_right_half_with_a_new_id() {
    let (tracks, reference) = seed_tracks(build_video_element("video-1", seconds(10.0), None));

    let result = split_elements(&tracks, &[reference], seconds(4.0), RetainSide::Right);

    assert_eq!(result.right_side_elements.len(), 1);
    let elements = result.tracks.main.elements();
    assert_eq!(elements.len(), 1);
    let right = &elements[0];
    assert_ne!(right.base().id, "video-1");
    assert_eq!(right.base().id, result.right_side_elements[0].element_id);
    assert_eq!(right.base().start_time, seconds(4.0));
    assert_eq!(right.base().duration, seconds(6.0));
    assert_eq!(right.base().trim_start, seconds(4.0));
}

#[test]
fn split_outside_the_element_bounds_is_a_no_op() {
    let (tracks, reference) = seed_tracks(build_video_element("video-1", seconds(10.0), None));

    for split_time in [MediaTime::ZERO, seconds(10.0), seconds(12.0)] {
        let result = split_elements(
            &tracks,
            std::slice::from_ref(&reference),
            split_time,
            RetainSide::Both,
        );
        assert_eq!(result.right_side_elements.len(), 0);
        assert_eq!(result.tracks.main.elements().len(), 1);
        assert_eq!(
            result.tracks.main.elements()[0].base().duration,
            seconds(10.0)
        );
    }
}

#[test]
fn shifting_the_right_side_and_inserting_into_the_gap_keeps_layout_consistent() {
    let (tracks, reference) = seed_tracks(build_video_element("video-1", seconds(10.0), None));

    let result = split_elements(&tracks, &[reference], seconds(4.0), RetainSide::Both);
    let right_ref = result.right_side_elements[0].clone();

    let shifted = timeline::update_element_in_scene_tracks(
        &result.tracks,
        &right_ref.track_id,
        &right_ref.element_id,
        |element| {
            let mut next = element.clone();
            next.base_mut().start_time = element.base().start_time + seconds(3.0);
            next
        },
        None,
    );

    let elements = shifted.main.elements();
    assert_eq!(elements[0].base().duration, seconds(4.0));
    assert_eq!(elements[1].base().start_time, seconds(7.0));
    assert_eq!(elements[1].base().duration, seconds(6.0));
}