Merge pull request #515 from Hathor875/feat/clip-link-pairs
clip.link: pair video clips with overlapping audio clips (#507)
This commit is contained in:
commit
b962329b0d
|
|
@ -41,6 +41,10 @@ sync_github_name = true
|
|||
[people.ItzBloxy]
|
||||
display_name = "Bloxy"
|
||||
|
||||
[people.Hathor875]
|
||||
real_name = "Krzysztof Cieślik"
|
||||
sync_github_name = true
|
||||
|
||||
[people.ReReddo]
|
||||
display_name = "ReReddo"
|
||||
sync_github_name = false
|
||||
|
|
|
|||
|
|
@ -1363,10 +1363,70 @@ fn build() -> Vec<CommandSpec> {
|
|||
let sel = clips_p(s, p);
|
||||
let linked = sel.iter().all(|c| s.active_sequence().and_then(|q| q.find_item(*c)).is_some_and(|(_, i)| i.link.is_some()));
|
||||
s.edit_sequence(if linked { "Unlink" } else { "Link" }, |q, ctx, _| {
|
||||
let l = if linked { None } else { Some(ctx.alloc()) };
|
||||
for c in &sel {
|
||||
if let Some((_, i)) = q.find_item_mut(*c) {
|
||||
i.link = l;
|
||||
if linked {
|
||||
for c in &sel {
|
||||
if let Some((_, i)) = q.find_item_mut(*c) {
|
||||
i.link = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// each selected clip with its track kind, worked out once
|
||||
type Spec = (ClipId, filmcraft_project::ItemId, filmcraft_time::TimeRange);
|
||||
let (mut v_clips, mut audio_specs): (Vec<Spec>, Vec<Spec>) = (Vec::new(), Vec::new());
|
||||
for c in &sel {
|
||||
if let Some((tid, i)) = q.find_item(*c) {
|
||||
if q.video_tracks.iter().any(|t| t.id == tid) {
|
||||
v_clips.push((*c, i.item, i.range()));
|
||||
} else {
|
||||
audio_specs.push((*c, i.item, i.range()));
|
||||
}
|
||||
}
|
||||
}
|
||||
if !audio_specs.is_empty() && v_clips.len() > 1 {
|
||||
let mut linked_audios = std::collections::HashSet::new();
|
||||
for (vid, item_id, vrange) in &v_clips {
|
||||
// Pair with overlapping audio clips from the same item, or overlapping audio clips if no same-item audio
|
||||
let matching: Vec<ClipId> = audio_specs
|
||||
.iter()
|
||||
.filter(|(aid, aitem, arange)| !linked_audios.contains(aid) && aitem == item_id && arange.overlaps(vrange))
|
||||
.map(|(aid, _, _)| *aid)
|
||||
.collect();
|
||||
let to_link = if !matching.is_empty() {
|
||||
matching
|
||||
} else {
|
||||
audio_specs
|
||||
.iter()
|
||||
.filter(|(aid, _, arange)| !linked_audios.contains(aid) && arange.overlaps(vrange))
|
||||
.map(|(aid, _, _)| *aid)
|
||||
.collect()
|
||||
};
|
||||
// a video clip with no audio partner stays unlinked: a one-member link would read as "linked"
|
||||
let l = (!to_link.is_empty()).then(|| ctx.alloc());
|
||||
if let Some((_, vi)) = q.find_item_mut(*vid) {
|
||||
vi.link = l;
|
||||
}
|
||||
for aid in to_link {
|
||||
linked_audios.insert(aid);
|
||||
if let Some((_, ai)) = q.find_item_mut(aid) {
|
||||
ai.link = l;
|
||||
}
|
||||
}
|
||||
}
|
||||
// selected audio clips with no overlapping video stay unlinked
|
||||
for (aid, _, _) in audio_specs {
|
||||
if !linked_audios.contains(&aid)
|
||||
&& let Some((_, ai)) = q.find_item_mut(aid)
|
||||
{
|
||||
ai.link = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let l = ctx.alloc();
|
||||
for c in &sel {
|
||||
if let Some((_, i)) = q.find_item_mut(*c) {
|
||||
i.link = Some(l);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -211,3 +211,56 @@ fn project_delete_takes_explicit_bins_and_items_without_a_selection() {
|
|||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(s.project.items.len(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn link_multiple_video_and_audio_pairs_creates_one_link_per_pair() {
|
||||
let mut s = demo();
|
||||
let v_clips = v1(&s);
|
||||
let a_clips = a1(&s);
|
||||
assert!(v_clips.len() >= 3 && a_clips.len() >= 3);
|
||||
let (v0, v1, v2) = (v_clips[0].id, v_clips[1].id, v_clips[2].id);
|
||||
let (a0, a1, a2) = (a_clips[0].id, a_clips[1].id, a_clips[2].id);
|
||||
|
||||
// First, unlink all three pairs
|
||||
let r = s.execute("clip.link", json!({"clips": [v0.0, a0.0, v1.0, a1.0, v2.0, a2.0]})).unwrap();
|
||||
assert_eq!(r["linked"], false);
|
||||
for cid in [v0, a0, v1, a1, v2, a2] {
|
||||
assert_eq!(clip(&s, cid).unwrap().link, None);
|
||||
}
|
||||
|
||||
// Now, run clip.link on all six clips. In Premiere parity (#507),
|
||||
// it should create 3 separate links (one per pair), not one big link for all six.
|
||||
let r = s.execute("clip.link", json!({"clips": [v0.0, a0.0, v1.0, a1.0, v2.0, a2.0]})).unwrap();
|
||||
assert_eq!(r["linked"], true);
|
||||
|
||||
let l0 = clip(&s, v0).unwrap().link;
|
||||
let la0 = clip(&s, a0).unwrap().link;
|
||||
let l1 = clip(&s, v1).unwrap().link;
|
||||
let la1 = clip(&s, a1).unwrap().link;
|
||||
let l2 = clip(&s, v2).unwrap().link;
|
||||
let la2 = clip(&s, a2).unwrap().link;
|
||||
|
||||
assert!(l0.is_some() && l1.is_some() && l2.is_some());
|
||||
assert_eq!(l0, la0, "v0 linked to a0");
|
||||
assert_eq!(l1, la1, "v1 linked to a1");
|
||||
assert_eq!(l2, la2, "v2 linked to a2");
|
||||
|
||||
assert_ne!(l0, l1, "different pairs must not share the same link id");
|
||||
assert_ne!(l1, l2, "different pairs must not share the same link id");
|
||||
assert_ne!(l0, l2, "different pairs must not share the same link id");
|
||||
}
|
||||
|
||||
/// A video clip with no overlapping selected audio, and audio with no overlapping video, stay
|
||||
/// unlinked: a one-member link would make the clip read as "linked" with no partner.
|
||||
#[test]
|
||||
fn link_leaves_clips_without_a_partner_unlinked() {
|
||||
let mut s = demo();
|
||||
let (v, a) = (v1(&s), a1(&s));
|
||||
let (v0, v1, a0, a2) = (v[0].id, v[1].id, a[0].id, a[2].id);
|
||||
s.execute("clip.link", json!({"clips": [v0.0, v1.0, a0.0, a2.0]})).unwrap();
|
||||
s.execute("clip.link", json!({"clips": [v0.0, v1.0, a0.0, a2.0]})).unwrap();
|
||||
assert!(clip(&s, v0).unwrap().link.is_some());
|
||||
assert_eq!(clip(&s, v0).unwrap().link, clip(&s, a0).unwrap().link);
|
||||
assert_eq!(clip(&s, v1).unwrap().link, None, "v1 has no overlapping audio in the selection");
|
||||
assert_eq!(clip(&s, a2).unwrap().link, None, "a2 has no overlapping video in the selection");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue