Ripple trim: a head trim grabbed by its early sound uses the picture's cut

Sync-locked tracks rippled from the grabbed member's start, so grabbing a
J cut's early sound checked and shifted from before the picture's cut and
was refused. For a head trim the cut is the latest member's start. Adds the
test only the new stretch catches: a clip starting at the cut on a
sync-locked track, which the old stretch let fall out of sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Peet Guercio
2026-10-06 14:22:13 -07:00
co-authored by Claude Opus 5.5
parent 27124080bf
commit 7f77bdfd3a
3 changed files with 34 additions and 2 deletions
+5 -1
View File
@@ -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()
};
+18
View File
@@ -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");
}
}
@@ -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");