summaryrefslogtreecommitdiff
path: root/rust/crates/timeline/tests/placement_resolve.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/timeline/tests/placement_resolve.rs')
-rw-r--r--rust/crates/timeline/tests/placement_resolve.rs624
1 files changed, 624 insertions, 0 deletions
diff --git a/rust/crates/timeline/tests/placement_resolve.rs b/rust/crates/timeline/tests/placement_resolve.rs
new file mode 100644
index 00000000..f79b00dd
--- /dev/null
+++ b/rust/crates/timeline/tests/placement_resolve.rs
@@ -0,0 +1,624 @@
+use scene::{
+ AudioElement, BaseTimelineElement, Element, GraphicElement, ParamValue, ParamValues,
+ SceneTracks, TextElement, Track, UploadAudioElement, VideoElement,
+};
+use time::MediaTime;
+use timeline::placement::{
+ InsertDirection, PlacementResult, PlacementStrategy, PlacementSubject, PlacementTimeSpan,
+ ResolveTrackPlacementParams, VerticalDragDirection, resolve_track_placement,
+};
+use timeline::types::{ElementType, TrackType};
+
+fn ticks(value: i64) -> MediaTime {
+ MediaTime::from_ticks(value)
+}
+
+fn base_element(id: &str, start_time: i64, duration: i64) -> BaseTimelineElement {
+ BaseTimelineElement {
+ id: id.to_string(),
+ name: id.to_string(),
+ start_time: ticks(start_time),
+ duration: ticks(duration),
+ trim_start: MediaTime::ZERO,
+ trim_end: MediaTime::ZERO,
+ source_duration: None,
+ animations: None,
+ params: ParamValues::new(),
+ }
+}
+
+fn visual_params() -> ParamValues {
+ 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)),
+ ])
+}
+
+fn build_element(element_type: ElementType, id: &str, start_time: i64, duration: i64) -> Element {
+ match element_type {
+ ElementType::Audio => Element::Audio(AudioElement::Upload(UploadAudioElement {
+ base: BaseTimelineElement {
+ params: ParamValues::from([
+ ("volume".to_string(), ParamValue::Number(1.0)),
+ ("muted".to_string(), ParamValue::Bool(false)),
+ ]),
+ ..base_element(id, start_time, duration)
+ },
+ retime: None,
+ media_id: format!("media-{id}"),
+ })),
+ ElementType::Graphic => Element::Graphic(GraphicElement {
+ base: BaseTimelineElement {
+ params: visual_params(),
+ ..base_element(id, start_time, duration)
+ },
+ definition_id: format!("graphic-{id}"),
+ hidden: None,
+ effects: None,
+ masks: None,
+ }),
+ ElementType::Text => Element::Text(TextElement {
+ base: BaseTimelineElement {
+ params: visual_params(),
+ ..base_element(id, start_time, duration)
+ },
+ hidden: None,
+ effects: None,
+ }),
+ ElementType::Video => Element::Video(VideoElement {
+ base: BaseTimelineElement {
+ params: visual_params(),
+ ..base_element(id, start_time, duration)
+ },
+ media_id: format!("media-{id}"),
+ is_source_audio_enabled: None,
+ hidden: None,
+ retime: None,
+ effects: None,
+ masks: None,
+ }),
+ _ => panic!("unsupported test element type"),
+ }
+}
+
+fn build_track(track_type: TrackType, id: &str, elements: Vec<Element>) -> Track {
+ match track_type {
+ TrackType::Audio => Track::Audio {
+ id: id.to_string(),
+ name: id.to_string(),
+ elements,
+ muted: false,
+ },
+ TrackType::Graphic => Track::Graphic {
+ id: id.to_string(),
+ name: id.to_string(),
+ elements,
+ hidden: false,
+ },
+ TrackType::Text => Track::Text {
+ id: id.to_string(),
+ name: id.to_string(),
+ elements,
+ hidden: false,
+ },
+ TrackType::Video => Track::Video {
+ id: id.to_string(),
+ name: id.to_string(),
+ elements,
+ muted: false,
+ hidden: false,
+ },
+ TrackType::Effect => Track::Effect {
+ id: id.to_string(),
+ name: id.to_string(),
+ elements,
+ hidden: false,
+ },
+ }
+}
+
+fn build_time_span(start_time: i64, duration: i64, exclude_element_id: Option<&str>) -> PlacementTimeSpan {
+ PlacementTimeSpan {
+ start_time: ticks(start_time),
+ duration: ticks(duration),
+ exclude_element_id: exclude_element_id.map(str::to_string),
+ }
+}
+
+fn build_scene_tracks(overlay: Vec<Track>, main: Option<Track>, audio: Vec<Track>) -> SceneTracks {
+ SceneTracks {
+ overlay,
+ main: main.unwrap_or_else(|| build_track(TrackType::Video, "video-main", Vec::new())),
+ audio,
+ }
+}
+
+fn resolve(
+ tracks: SceneTracks,
+ element_type: ElementType,
+ time_spans: Vec<PlacementTimeSpan>,
+ strategy: PlacementStrategy,
+) -> Option<PlacementResult> {
+ resolve_track_placement(&ResolveTrackPlacementParams {
+ tracks,
+ subject: PlacementSubject::ElementType(element_type),
+ time_spans,
+ strategy,
+ })
+}
+
+#[test]
+fn explicit_returns_the_requested_compatible_track() {
+ let tracks = build_scene_tracks(
+ vec![build_track(TrackType::Text, "text-1", Vec::new())],
+ None,
+ Vec::new(),
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Text,
+ vec![build_time_span(2, 3, None)],
+ PlacementStrategy::Explicit {
+ track_id: "text-1".to_string(),
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::ExistingTrack {
+ track_id: "text-1".to_string(),
+ track_index: 0,
+ track_type: TrackType::Text,
+ adjusted_start_time: None,
+ })
+ );
+}
+
+#[test]
+fn explicit_rejects_missing_and_incompatible_tracks() {
+ let tracks = || {
+ build_scene_tracks(
+ Vec::new(),
+ Some(build_track(TrackType::Video, "video-1", Vec::new())),
+ Vec::new(),
+ )
+ };
+
+ let missing = resolve(
+ tracks(),
+ ElementType::Text,
+ vec![build_time_span(0, 1, None)],
+ PlacementStrategy::Explicit {
+ track_id: "missing".to_string(),
+ },
+ );
+ assert_eq!(missing, None);
+
+ let incompatible = resolve(
+ tracks(),
+ ElementType::Text,
+ vec![build_time_span(0, 1, None)],
+ PlacementStrategy::Explicit {
+ track_id: "video-1".to_string(),
+ },
+ );
+ assert_eq!(incompatible, None);
+}
+
+#[test]
+fn first_available_picks_the_first_compatible_track_without_overlap() {
+ let tracks = build_scene_tracks(
+ vec![
+ build_track(
+ TrackType::Text,
+ "text-1",
+ vec![build_element(ElementType::Text, "a", 0, 5)],
+ ),
+ build_track(TrackType::Text, "text-2", Vec::new()),
+ ],
+ None,
+ Vec::new(),
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Text,
+ vec![build_time_span(2, 1, None)],
+ PlacementStrategy::FirstAvailable,
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::ExistingTrack {
+ track_id: "text-2".to_string(),
+ track_index: 1,
+ track_type: TrackType::Text,
+ adjusted_start_time: None,
+ })
+ );
+}
+
+#[test]
+fn first_available_creates_a_new_track_when_all_compatible_tracks_are_full() {
+ let tracks = build_scene_tracks(
+ vec![build_track(
+ TrackType::Graphic,
+ "graphic-1",
+ vec![build_element(ElementType::Graphic, "a", 0, 5)],
+ )],
+ Some(build_track(TrackType::Video, "video-main", Vec::new())),
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Graphic,
+ vec![build_time_span(1, 1, None)],
+ PlacementStrategy::FirstAvailable,
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Graphic,
+ insert_index: 0,
+ insert_position: None,
+ })
+ );
+}
+
+#[test]
+fn prefer_index_uses_the_preferred_track_when_it_fits() {
+ let tracks = build_scene_tracks(
+ Vec::new(),
+ None,
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Audio,
+ vec![build_time_span(3, 2, None)],
+ PlacementStrategy::PreferIndex {
+ track_index: 0,
+ hover_direction: InsertDirection::Below,
+ vertical_drag_direction: None,
+ create_new_track_only: false,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 1,
+ insert_position: Some(InsertDirection::Below),
+ })
+ );
+}
+
+#[test]
+fn prefer_index_creates_a_new_overlay_track_above_the_main_track() {
+ let tracks = build_scene_tracks(
+ Vec::new(),
+ Some(build_track(TrackType::Video, "video-main", Vec::new())),
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Graphic,
+ vec![build_time_span(1, 2, None)],
+ PlacementStrategy::PreferIndex {
+ track_index: 1,
+ hover_direction: InsertDirection::Below,
+ vertical_drag_direction: None,
+ create_new_track_only: false,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Graphic,
+ insert_index: 0,
+ insert_position: Some(InsertDirection::Above),
+ })
+ );
+}
+
+#[test]
+fn prefer_index_keeps_audio_tracks_below_the_main_track() {
+ let tracks = build_scene_tracks(
+ vec![build_track(TrackType::Text, "text-1", Vec::new())],
+ Some(build_track(TrackType::Video, "video-main", Vec::new())),
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Audio,
+ vec![build_time_span(0, 1, None)],
+ PlacementStrategy::PreferIndex {
+ track_index: 0,
+ hover_direction: InsertDirection::Above,
+ vertical_drag_direction: None,
+ create_new_track_only: true,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 2,
+ insert_position: Some(InsertDirection::Below),
+ })
+ );
+}
+
+#[test]
+fn above_source_tries_the_track_above_source_then_any_compatible_track() {
+ let tracks = build_scene_tracks(
+ vec![
+ build_track(TrackType::Text, "text-top", Vec::new()),
+ build_track(
+ TrackType::Text,
+ "text-middle",
+ vec![build_element(ElementType::Text, "a", 0, 5)],
+ ),
+ build_track(TrackType::Text, "text-source", Vec::new()),
+ ],
+ None,
+ Vec::new(),
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Text,
+ vec![build_time_span(1, 1, None)],
+ PlacementStrategy::AboveSource {
+ source_track_index: 2,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::ExistingTrack {
+ track_id: "text-top".to_string(),
+ track_index: 0,
+ track_type: TrackType::Text,
+ adjusted_start_time: None,
+ })
+ );
+}
+
+#[test]
+fn above_source_creates_a_new_overlay_track_in_the_overlay_zone_when_none_fit() {
+ let tracks = build_scene_tracks(
+ vec![
+ build_track(
+ TrackType::Text,
+ "text-top",
+ vec![build_element(ElementType::Text, "a", 0, 5)],
+ ),
+ build_track(
+ TrackType::Text,
+ "text-source",
+ vec![build_element(ElementType::Text, "b", 0, 5)],
+ ),
+ ],
+ None,
+ Vec::new(),
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Text,
+ vec![build_time_span(1, 1, None)],
+ PlacementStrategy::AboveSource {
+ source_track_index: 1,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Text,
+ insert_index: 0,
+ insert_position: None,
+ })
+ );
+}
+
+#[test]
+fn always_new_honors_highest_and_default_insertion_rules() {
+ use timeline::placement::NewTrackPosition;
+
+ let tracks = || {
+ build_scene_tracks(
+ Vec::new(),
+ Some(build_track(TrackType::Video, "video-main", Vec::new())),
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ )
+ };
+
+ let highest = resolve(
+ tracks(),
+ ElementType::Audio,
+ Vec::new(),
+ PlacementStrategy::AlwaysNew {
+ position: NewTrackPosition::Highest,
+ },
+ );
+ assert_eq!(
+ highest,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 1,
+ insert_position: None,
+ })
+ );
+
+ let default = resolve(
+ tracks(),
+ ElementType::Audio,
+ Vec::new(),
+ PlacementStrategy::AlwaysNew {
+ position: NewTrackPosition::Default,
+ },
+ );
+ assert_eq!(
+ default,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 2,
+ insert_position: None,
+ })
+ );
+}
+
+#[test]
+fn batch_time_spans_reject_tracks_when_any_span_overlaps() {
+ let tracks = build_scene_tracks(
+ Vec::new(),
+ None,
+ vec![build_track(
+ TrackType::Audio,
+ "audio-1",
+ vec![
+ build_element(ElementType::Audio, "a", 0, 2),
+ build_element(ElementType::Audio, "b", 5, 2),
+ ],
+ )],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Audio,
+ vec![build_time_span(3, 1, None), build_time_span(6, 1, None)],
+ PlacementStrategy::FirstAvailable,
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 1,
+ insert_position: None,
+ })
+ );
+}
+
+#[test]
+fn handles_main_only_timelines_and_track_type_derivation() {
+ use timeline::placement::NewTrackPosition;
+
+ let main_only = resolve(
+ build_scene_tracks(Vec::new(), None, Vec::new()),
+ ElementType::Video,
+ vec![build_time_span(0, 3, None)],
+ PlacementStrategy::PreferIndex {
+ track_index: 0,
+ hover_direction: InsertDirection::Below,
+ vertical_drag_direction: None,
+ create_new_track_only: true,
+ },
+ );
+ assert_eq!(
+ main_only,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Video,
+ insert_index: 0,
+ insert_position: Some(InsertDirection::Above),
+ })
+ );
+
+ let audio_default = resolve(
+ build_scene_tracks(
+ Vec::new(),
+ None,
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ ),
+ ElementType::Audio,
+ Vec::new(),
+ PlacementStrategy::AlwaysNew {
+ position: NewTrackPosition::Default,
+ },
+ );
+ assert_eq!(
+ audio_default,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 2,
+ insert_position: None,
+ })
+ );
+}
+
+#[test]
+fn existing_track_on_main_video_includes_adjusted_start_time_when_start_snaps() {
+ let tracks = build_scene_tracks(
+ Vec::new(),
+ Some(build_track(
+ TrackType::Video,
+ "video-main",
+ vec![build_element(ElementType::Video, "a", 5, 5)],
+ )),
+ Vec::new(),
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Video,
+ vec![build_time_span(2, 2, None)],
+ PlacementStrategy::Explicit {
+ track_id: "video-main".to_string(),
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::ExistingTrack {
+ track_id: "video-main".to_string(),
+ track_index: 0,
+ track_type: TrackType::Video,
+ adjusted_start_time: Some(MediaTime::ZERO),
+ })
+ );
+}
+
+#[test]
+fn prefer_index_uses_vertical_drag_direction_when_hovered_track_is_incompatible() {
+ let tracks = build_scene_tracks(
+ vec![build_track(TrackType::Text, "text-1", Vec::new())],
+ Some(build_track(TrackType::Video, "video-main", Vec::new())),
+ vec![build_track(TrackType::Audio, "audio-1", Vec::new())],
+ );
+
+ let result = resolve(
+ tracks,
+ ElementType::Audio,
+ vec![build_time_span(0, 1, None)],
+ PlacementStrategy::PreferIndex {
+ track_index: 0,
+ hover_direction: InsertDirection::Above,
+ vertical_drag_direction: Some(VerticalDragDirection::Down),
+ create_new_track_only: false,
+ },
+ );
+
+ assert_eq!(
+ result,
+ Some(PlacementResult::NewTrack {
+ track_type: TrackType::Audio,
+ insert_index: 2,
+ insert_position: Some(InsertDirection::Below),
+ })
+ );
+}