diff --git a/crates/edit/src/lib.rs b/crates/edit/src/lib.rs index 8364e0aa0..2436c73bc 100644 --- a/crates/edit/src/lib.rs +++ b/crates/edit/src/lib.rs @@ -652,7 +652,11 @@ pub fn ripple_trim_group(seq: &mut Sequence, clips: &[ClipId], edge: Edge, delta origins.push((tid, from)); } } - let main_from = { + // a shorter or longer head: the cut is the latest member's start (a sound that leads its picture + // starts earlier), whichever member was grabbed + let main_from = if edge == Edge::In { + origins.iter().map(|o| o.1).max().unwrap_or_default() + } else { let (tid, _) = seq.find_item(first).ok_or(EditError::NoItem(first))?; origins.iter().find(|(t, _)| *t == tid).map(|o| o.1).unwrap_or_default() }; diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 189a36320..03f2926e9 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -600,4 +600,22 @@ fn shorter_head_closes_the_stretch_after_the_cut_on_sync_locked_tracks() { }; assert_eq!(r, Err(EditError::SyncLockConflict), "group {group}"); } + // a clip on a sync-locked track that starts at the cut stays put while the pictures after it move + // up: only the stretch after the cut sees it (the old stretch, before the cut, let it through) + for group in [false, true] { + let mut fx = Fx::new(); + let (v1, a1) = (fx.v(0), fx.a(0)); + fx.put(v1, 0, 10, 0); + let b = fx.put(v1, 10, 10, 0); + fx.put(v1, 20, 10, 0); + fx.put(a1, 10, 5, 0); + let mut n = fx.next; + let r = if group { + ripple_trim_group(&mut fx.seq, &[b], Edge::In, f(3), &mut Fx::ctx(&mut n)) + } else { + trim(&mut fx.seq, b, Edge::In, TrimMode::Ripple, f(3), &mut Fx::ctx(&mut n)) + }; + assert_eq!(r, Err(EditError::SyncLockConflict), "group {group}"); + assert_eq!(fx.spans(v1), vec![(0, 10), (10, 10), (20, 10)], "unchanged on failure"); + } } diff --git a/crates/engine/src/split_edit_ripple_trim_tests.rs b/crates/engine/src/split_edit_ripple_trim_tests.rs index 01b121b5d..d3f362583 100644 --- a/crates/engine/src/split_edit_ripple_trim_tests.rs +++ b/crates/engine/src/split_edit_ripple_trim_tests.rs @@ -92,6 +92,16 @@ fn ripple_trim_is_refused_when_the_early_sound_has_no_room() { /// the shorter head takes away. #[test] fn shorter_head_on_a_j_cut_take_shortens_picture_and_sound_together() { + shorter_head_on_a_j_cut_take(false); +} + +/// The same trim grabbed by the early sound instead of the picture gives the same result. +#[test] +fn shorter_head_grabbed_by_the_early_sound_gives_the_same_result() { + shorter_head_on_a_j_cut_take(true); +} + +fn shorter_head_on_a_j_cut_take(grab_sound: bool) { let mut s = Session::default(); let (first, second, sound) = j_cut(&mut s); let rate = s.sequence_rate(); @@ -100,7 +110,7 @@ fn shorter_head_on_a_j_cut_take_shortens_picture_and_sound_together() { let ((pic_len, pic_in), (snd_len, snd_in)) = (len(&s, second), len(&s, sound)); let first_before = len(&s, first); let before = s.project.clone(); - let r = s.execute("timeline.trim", json!({"clip": second.0, "edge": "in", "mode": "ripple", "deltaFrames": 8})).unwrap(); + let r = s.execute("timeline.trim", json!({"clip": if grab_sound { sound.0 } else { second.0 }, "edge": "in", "mode": "ripple", "deltaFrames": 8})).unwrap(); let by = rate.tick_of(8); assert_eq!(r["delta"], json!(by.0)); assert_eq!((start(&s, second), start(&s, sound)), (picture_at, sound_at), "both stay where they start");