engine: keep a one-frame run a sequence when replacing footage

This commit is contained in:
Madhav Agarwal
2026-10-29 13:26:48 +05:30
parent 9fd14225eb
commit 0c6ca242e5
2 changed files with 43 additions and 3 deletions
+19 -3
View File
@@ -884,12 +884,28 @@ fn replace_footage(s: &mut Session, p: &Value) -> Result<Value> {
Some(ItemKind::Footage(f)) if f.kind == FootageKind::Sequence => (true, true, f.alphabetical),
_ => (false, false, false),
};
// Only `sequence` decides whether the item keeps its kind; `alphabetical` is a sequence
// modifier, not a request to make one.
let inherited = b_p(p, "sequence").is_none();
let mut opts = SequenceOptions::from_params(p);
if b_p(p, "sequence").is_none() && b_p(p, "alphabetical").is_none() {
(opts.sequence, opts.folders, opts.alphabetical) = previous;
if inherited {
(opts.sequence, opts.folders) = (previous.0, previous.1);
if previous.0 && b_p(p, "alphabetical").is_none() {
opts.alphabetical = previous.2;
}
}
let src = crate::sequence::source_of(s.services.as_ref(), &path, opts);
let f = crate::sequence::probe(importer.as_ref(), &src, s.sequence_rate()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let mut f = crate::sequence::probe(importer.as_ref(), &src, s.sequence_rate()).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
// A single-file source comes back as a still; an inherited sequence kind keeps it a
// one-frame sequence, so a later default replacement still brings numbered runs (#2073).
if inherited && previous.0 && matches!(src, crate::sequence::Source::File(_)) && f.kind == FootageKind::Still {
f.kind = FootageKind::Sequence;
f.sequence = vec![f.path.clone()];
f.alphabetical = opts.alphabetical;
f.frame_rate = s.sequence_rate();
f.native_rate = None;
f.sync_sequence_duration();
}
let name = src.name();
// Replacing a missing file relinks the other missing files that moved with it (as After Effects
// does when you locate one).
+24
View File
@@ -194,12 +194,36 @@ fn replace_footage_keeps_a_still_a_still() {
s.execute("file.replaceFootage", json!({"path": at(&dir, "shot_0004.png"), "sequence": null})).unwrap();
let (name, f) = footage(&s, &item);
assert_eq!((name.as_str(), f.kind), ("shot_0004.png", FootageKind::Still));
// Alphabetical is a sequence modifier, not a request to make one.
s.execute("file.replaceFootage", json!({"path": at(&dir, "shot_0004.png"), "alphabetical": true})).unwrap();
let (name, f) = footage(&s, &item);
assert_eq!((name.as_str(), f.kind), ("shot_0004.png", FootageKind::Still));
// Asking for the run still brings it.
s.execute("file.replaceFootage", json!({"path": at(&dir, "shot_0004.png"), "sequence": true})).unwrap();
let (name, f) = footage(&s, &item);
assert_eq!((name.as_str(), f.kind, f.sequence.len()), ("shot_[0001-0005].png", FootageKind::Sequence, 4));
}
#[test]
fn replace_footage_keeps_a_one_frame_run_a_sequence() {
let dir = folder(&["shot_0001.png", "shot_0002.png", "solo_0007.png", "cover.png"]);
let mut s = session();
let item = s.execute("file.import", json!({"paths": [at(&dir, "shot_0001.png")], "sequence": true})).unwrap()["items"][0].clone();
s.state.project_selection = vec![ItemId(item.as_u64().unwrap())];
// A numbered file with no other frame in its run is still the replacement for a sequence.
s.execute("file.replaceFootage", json!({"path": at(&dir, "solo_0007.png")})).unwrap();
let (name, f) = footage(&s, &item);
assert_eq!((name.as_str(), f.kind, f.sequence.len()), ("solo_0007.png", FootageKind::Sequence, 1));
// A single-file source keeps the sequence kind too.
s.execute("file.replaceFootage", json!({"path": at(&dir, "cover.png")})).unwrap();
let (name, f) = footage(&s, &item);
assert_eq!((name.as_str(), f.kind, f.sequence.len()), ("cover.png", FootageKind::Sequence, 1));
// An explicit `sequence: false` still asks for one frame.
s.execute("file.replaceFootage", json!({"path": at(&dir, "cover.png"), "sequence": false})).unwrap();
let (_, f) = footage(&s, &item);
assert_eq!((f.kind, f.sequence.len()), (FootageKind::Still, 0));
}
#[test]
fn replace_footage_and_reload_keep_sequences() {
let dir = folder(&RUN);