summaryrefslogtreecommitdiff
path: root/rust/crates/timeline/src/resize.rs
diff options
context:
space:
mode:
Diffstat (limited to 'rust/crates/timeline/src/resize.rs')
-rw-r--r--rust/crates/timeline/src/resize.rs401
1 files changed, 401 insertions, 0 deletions
diff --git a/rust/crates/timeline/src/resize.rs b/rust/crates/timeline/src/resize.rs
new file mode 100644
index 00000000..51478a09
--- /dev/null
+++ b/rust/crates/timeline/src/resize.rs
@@ -0,0 +1,401 @@
+use scene::{ElementRef, RetimeConfig, SceneTracks};
+use time::{FrameRate, MediaTime, TICKS_PER_SECOND};
+
+use crate::retime::{
+ get_source_span_at_clip_time, get_timeline_duration_for_source_span, round_media_time,
+};
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum ResizeSide {
+ Left,
+ Right,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GroupResizePushTarget {
+ pub track_id: String,
+ pub element_id: String,
+ pub start_time: MediaTime,
+ pub duration: MediaTime,
+ pub trim_start: MediaTime,
+ pub trim_end: MediaTime,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GroupResizeMember {
+ pub track_id: String,
+ pub element_id: String,
+ pub start_time: MediaTime,
+ pub duration: MediaTime,
+ pub trim_start: MediaTime,
+ pub trim_end: MediaTime,
+ pub source_duration: Option<MediaTime>,
+ pub retime: Option<RetimeConfig>,
+ pub left_neighbor_bound: Option<MediaTime>,
+ pub right_neighbor_bound: Option<MediaTime>,
+ pub right_push_chain: Option<Vec<GroupResizePushTarget>>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GroupResizePatch {
+ pub trim_start: MediaTime,
+ pub trim_end: MediaTime,
+ pub start_time: MediaTime,
+ pub duration: MediaTime,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GroupResizeUpdate {
+ pub track_id: String,
+ pub element_id: String,
+ pub patch: GroupResizePatch,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct GroupResizeResult {
+ pub delta_time: MediaTime,
+ pub updates: Vec<GroupResizeUpdate>,
+}
+
+pub fn compute_group_resize(
+ members: &[GroupResizeMember],
+ side: ResizeSide,
+ delta_time: MediaTime,
+ fps: FrameRate,
+) -> GroupResizeResult {
+ if members.is_empty() {
+ return GroupResizeResult {
+ delta_time: MediaTime::ZERO,
+ updates: Vec::new(),
+ };
+ }
+
+ let min_duration = MediaTime::from_ticks(
+ ((TICKS_PER_SECOND as f64 * fps.denominator as f64) / fps.numerator as f64).round() as i64,
+ );
+ let mut minimum_delta_time = get_minimum_allowed_delta_time(&members[0], side, min_duration);
+ let mut maximum_delta_time = get_maximum_allowed_delta_time(&members[0], side, min_duration);
+
+ for member in &members[1..] {
+ minimum_delta_time =
+ minimum_delta_time.max(get_minimum_allowed_delta_time(member, side, min_duration));
+ if let Some(member_maximum) = get_maximum_allowed_delta_time(member, side, min_duration) {
+ maximum_delta_time = Some(match maximum_delta_time {
+ None => member_maximum,
+ Some(current) => current.min(member_maximum),
+ });
+ }
+ }
+
+ let clamped_delta_time = match maximum_delta_time {
+ None => minimum_delta_time.max(delta_time),
+ Some(maximum) => delta_time.clamp(minimum_delta_time, maximum),
+ };
+
+ let snapped_delta_time = clamped_delta_time
+ .round_to_frame(fps)
+ .unwrap_or(clamped_delta_time);
+ let final_delta_time = match maximum_delta_time {
+ None => minimum_delta_time.max(snapped_delta_time),
+ Some(maximum) => snapped_delta_time.clamp(minimum_delta_time, maximum),
+ };
+
+ let mut updates: Vec<GroupResizeUpdate> = members
+ .iter()
+ .map(|member| build_resize_update(member, side, final_delta_time))
+ .collect();
+ updates.extend(build_ripple_push_updates(members, side, final_delta_time));
+
+ GroupResizeResult {
+ delta_time: final_delta_time,
+ updates,
+ }
+}
+
+fn build_ripple_push_updates(
+ members: &[GroupResizeMember],
+ side: ResizeSide,
+ delta_time: MediaTime,
+) -> Vec<GroupResizeUpdate> {
+ if side != ResizeSide::Right || delta_time <= MediaTime::ZERO {
+ return Vec::new();
+ }
+
+ let mut overflow_by_target: Vec<(GroupResizePushTarget, MediaTime)> = Vec::new();
+ for member in members {
+ let (Some(right_neighbor_bound), Some(push_chain)) =
+ (member.right_neighbor_bound, &member.right_push_chain)
+ else {
+ continue;
+ };
+ if member.source_duration.is_some() {
+ continue;
+ }
+
+ let neighbor_ceiling = right_neighbor_bound - (member.start_time + member.duration);
+ let overflow = delta_time - neighbor_ceiling;
+ if overflow <= MediaTime::ZERO {
+ continue;
+ }
+ for target in push_chain {
+ match overflow_by_target
+ .iter_mut()
+ .find(|(existing, _)| existing.element_id == target.element_id)
+ {
+ Some((_, existing_overflow)) => {
+ if overflow > *existing_overflow {
+ *existing_overflow = overflow;
+ }
+ }
+ None => overflow_by_target.push((target.clone(), overflow)),
+ }
+ }
+ }
+
+ overflow_by_target
+ .into_iter()
+ .map(|(target, overflow)| GroupResizeUpdate {
+ track_id: target.track_id,
+ element_id: target.element_id,
+ patch: GroupResizePatch {
+ trim_start: target.trim_start,
+ trim_end: target.trim_end,
+ start_time: target.start_time + overflow,
+ duration: target.duration,
+ },
+ })
+ .collect()
+}
+
+fn build_resize_update(
+ member: &GroupResizeMember,
+ side: ResizeSide,
+ delta_time: MediaTime,
+) -> GroupResizeUpdate {
+ let source_delta = get_source_delta_for_clip_delta(member, delta_time);
+
+ match side {
+ ResizeSide::Left => GroupResizeUpdate {
+ track_id: member.track_id.clone(),
+ element_id: member.element_id.clone(),
+ patch: GroupResizePatch {
+ trim_start: MediaTime::ZERO.max(member.trim_start + source_delta),
+ trim_end: member.trim_end,
+ start_time: member.start_time + delta_time,
+ duration: member.duration - delta_time,
+ },
+ },
+ ResizeSide::Right => GroupResizeUpdate {
+ track_id: member.track_id.clone(),
+ element_id: member.element_id.clone(),
+ patch: GroupResizePatch {
+ trim_start: member.trim_start,
+ trim_end: MediaTime::ZERO.max(member.trim_end - source_delta),
+ start_time: member.start_time,
+ duration: member.duration + delta_time,
+ },
+ },
+ }
+}
+
+fn get_minimum_allowed_delta_time(
+ member: &GroupResizeMember,
+ side: ResizeSide,
+ min_duration: MediaTime,
+) -> MediaTime {
+ if side == ResizeSide::Right {
+ return min_duration - member.duration;
+ }
+
+ let left_neighbor_floor = match member.left_neighbor_bound {
+ Some(bound) => bound - member.start_time,
+ None => MediaTime::ZERO - member.start_time,
+ };
+ if member.source_duration.is_none() {
+ return left_neighbor_floor;
+ }
+
+ let maximum_source_extension = get_duration_for_visible_source_span(
+ member,
+ get_visible_source_span_for_duration(member, member.duration) + member.trim_start,
+ ) - member.duration;
+ left_neighbor_floor.max(MediaTime::ZERO - maximum_source_extension)
+}
+
+fn get_maximum_allowed_delta_time(
+ member: &GroupResizeMember,
+ side: ResizeSide,
+ min_duration: MediaTime,
+) -> Option<MediaTime> {
+ if side == ResizeSide::Left {
+ return Some(member.duration - min_duration);
+ }
+
+ let right_neighbor_ceiling = member
+ .right_neighbor_bound
+ .map(|bound| bound - (member.start_time + member.duration));
+ if member.source_duration.is_none() {
+ return if member.right_push_chain.is_none() {
+ right_neighbor_ceiling
+ } else {
+ None
+ };
+ }
+
+ let maximum_visible_source_span = get_source_duration(member) - member.trim_start;
+ let maximum_duration = get_duration_for_visible_source_span(member, maximum_visible_source_span);
+ let source_duration_ceiling = maximum_duration - member.duration;
+ Some(match right_neighbor_ceiling {
+ None => source_duration_ceiling,
+ Some(ceiling) => ceiling.min(source_duration_ceiling),
+ })
+}
+
+fn get_source_delta_for_clip_delta(member: &GroupResizeMember, clip_delta: MediaTime) -> MediaTime {
+ let Some(retime) = &member.retime else {
+ return clip_delta;
+ };
+
+ let source_delta = if clip_delta >= MediaTime::ZERO {
+ get_source_span_at_clip_time(clip_delta.as_ticks() as f64, Some(retime))
+ } else {
+ -get_source_span_at_clip_time(clip_delta.as_ticks().unsigned_abs() as f64, Some(retime))
+ };
+ round_media_time(source_delta)
+}
+
+fn get_visible_source_span_for_duration(member: &GroupResizeMember, duration: MediaTime) -> MediaTime {
+ let Some(retime) = &member.retime else {
+ return duration;
+ };
+
+ round_media_time(get_source_span_at_clip_time(
+ duration.as_ticks() as f64,
+ Some(retime),
+ ))
+}
+
+fn get_duration_for_visible_source_span(
+ member: &GroupResizeMember,
+ source_span: MediaTime,
+) -> MediaTime {
+ let Some(retime) = &member.retime else {
+ return source_span;
+ };
+
+ round_media_time(get_timeline_duration_for_source_span(
+ source_span.as_ticks() as f64,
+ Some(retime),
+ ))
+}
+
+fn get_source_duration(member: &GroupResizeMember) -> MediaTime {
+ if let Some(source_duration) = member.source_duration {
+ return source_duration;
+ }
+
+ member.trim_start
+ + get_visible_source_span_for_duration(member, member.duration)
+ + member.trim_end
+}
+
+pub fn build_resize_members(
+ tracks: &SceneTracks,
+ selected_elements: &[ElementRef],
+) -> Vec<GroupResizeMember> {
+ let selected_ids: Vec<&str> = selected_elements
+ .iter()
+ .map(|reference| reference.element_id.as_str())
+ .collect();
+
+ let mut members = Vec::new();
+ for reference in selected_elements {
+ let Some(track) = crate::track_element_update::find_track_in_scene_tracks(
+ tracks,
+ &reference.track_id,
+ ) else {
+ continue;
+ };
+ let Some(element) = track
+ .elements()
+ .iter()
+ .find(|element| element.base().id == reference.element_id)
+ else {
+ continue;
+ };
+
+ let base = element.base();
+ let other_elements: Vec<&scene::Element> = track
+ .elements()
+ .iter()
+ .filter(|candidate| !selected_ids.contains(&candidate.base().id.as_str()))
+ .collect();
+
+ let left_neighbor_bound = other_elements
+ .iter()
+ .filter(|candidate| {
+ candidate.base().start_time + candidate.base().duration <= base.start_time
+ })
+ .map(|candidate| candidate.base().start_time + candidate.base().duration)
+ .reduce(MediaTime::max);
+
+ let mut right_elements: Vec<&scene::Element> = other_elements
+ .iter()
+ .filter(|candidate| candidate.base().start_time >= base.start_time + base.duration)
+ .copied()
+ .collect();
+ let right_neighbor_bound = right_elements
+ .iter()
+ .map(|candidate| candidate.base().start_time)
+ .reduce(MediaTime::min);
+
+ let right_push_chain = if base.source_duration.is_none() {
+ right_elements.sort_by_key(|candidate| candidate.base().start_time);
+ Some(
+ right_elements
+ .iter()
+ .map(|candidate| GroupResizePushTarget {
+ track_id: reference.track_id.clone(),
+ element_id: candidate.base().id.clone(),
+ start_time: candidate.base().start_time,
+ duration: candidate.base().duration,
+ trim_start: candidate.base().trim_start,
+ trim_end: candidate.base().trim_end,
+ })
+ .collect(),
+ )
+ } else {
+ None
+ };
+
+ members.push(GroupResizeMember {
+ track_id: reference.track_id.clone(),
+ element_id: reference.element_id.clone(),
+ start_time: base.start_time,
+ duration: base.duration,
+ trim_start: base.trim_start,
+ trim_end: base.trim_end,
+ source_duration: base.source_duration,
+ retime: crate::element_retime(element),
+ left_neighbor_bound,
+ right_neighbor_bound,
+ right_push_chain,
+ });
+ }
+
+ members
+}
+
+pub fn has_resize_changes(members: &[GroupResizeMember], result: &GroupResizeResult) -> bool {
+ result.updates.iter().any(|update| {
+ let member = members
+ .iter()
+ .find(|member| member.element_id == update.element_id);
+ member.is_some_and(|member| {
+ member.trim_start != update.patch.trim_start
+ || member.trim_end != update.patch.trim_end
+ || member.start_time != update.patch.start_time
+ || member.duration != update.patch.duration
+ })
+ })
+}