From 40e87709d56c43848ca7b9eb6acb1499787966e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 19:28:59 +0000 Subject: [PATCH] Import: numbered stills import as stills unless the Sequence option is on; dropped folders make sequences (#431) Cause: SequenceOptions::from_params defaulted `sequence` to true for file.import, so any numbered still (IMG_0001.jpg) brought its whole numbered run from its folder, and several picked or dropped stills with a common prefix became one sequence. Dropped files never asked, and the Import dialog's " Sequence" checkbox started ticked. Fix, matching After Effects: file.import takes picked and dropped files as stills unless `sequence: true` (the Import dialog's Sequence checkbox, which now starts unticked; Force Alphabetical Order implies it), while the numbered stills inside a picked or dropped folder still import as sequences (SequenceOptions::folders), and `sequence: false` (Alt-drag of a folder) imports a folder's files one by one. Replace Footage, reload, proxies and render outputs keep detecting their sequence (SequenceOptions::default and from_params are unchanged), and mediaBrowser.import keeps bringing a numbered still's run unless `sequence: false`. docs/footage.md and docs/agents.md describe the new defaults. Tests: sequence import_takes_stills_unless_asked_for_a_sequence and engine numbered_stills_import_as_stills_by_default (one dropped still and two picked IMG_ stills stay stills, a dropped folder makes one sequence, the checkbox brings the run); the existing sequence tests pass `sequence: true`; ui import_dialog_offers_the_image_sequence_option (unticked by default: a still; ticked: the run) and a_dropped_folder_imports_as_one_sequence_with_sequence_interpretation (two dropped frames: two stills; the folder: one sequence). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f (cherry picked from commit aa3c31db9ab3a774ee2f8ba867e1bafc1fce7aba) --- crates/engine/src/commands/file.rs | 4 +- crates/engine/src/commands/panels_cmds.rs | 7 ++- crates/engine/src/sequence.rs | 69 +++++++++++++++++------ crates/engine/src/tests_sequence.rs | 26 +++++++-- crates/ui-egui/src/panels/forms.rs | 7 ++- crates/ui-egui/src/prefs_live.rs | 8 ++- crates/ui-egui/tests/ui_drag_drop.rs | 28 +++++---- docs/agents.md | 7 ++- docs/footage.md | 17 ++++-- 9 files changed, 123 insertions(+), 50 deletions(-) diff --git a/crates/engine/src/commands/file.rs b/crates/engine/src/commands/file.rs index f0396e5d..fb557de0 100644 --- a/crates/engine/src/commands/file.rs +++ b/crates/engine/src/commands/file.rs @@ -376,7 +376,7 @@ fn prepare_import(s: &mut Session, p: &Value) -> Result { } // Numbered stills as image sequences (the Import dialog's " Sequence" and Force // Alphabetical Order), at `frameRate` or Settings ▸ Import ▸ Sequence Footage. - let opts = SequenceOptions::from_params(p); + let opts = SequenceOptions::for_import(p); let fps = match p.get("frameRate") { None | Some(Value::Null) => s.prefs.import.sequence_fps, Some(v) => v.as_f64().filter(|x| *x > 0.0 && *x <= 999.0).ok_or_else(|| bad("file.import", "frameRate: frames per second, up to 999"))?, @@ -680,7 +680,7 @@ pub fn specs() -> Vec { "File...", ["File", "Import"], Some("Cmd+I"), - "{paths: [string] (files or folders), sequence?: bool (default true: numbered stills of one run import as one image sequence; one picked file brings its whole run, several picked files that range), alphabetical?: bool (Force Alphabetical Order: every image of that type in the folder, by name), frameRate?: fps (image sequences; default Settings ▸ Import ▸ Sequence Footage), importAs?: footage|composition|compositionLayerSizes (Photoshop, PDF, Illustrator and EPS files), layer?: name|index (footage of one Photoshop layer), page?: number from 1 (PDF / Illustrator page), drag?: bool (dropped files: Settings ▸ Import ▸ Default Drag Import As), addToComp?: bool (also add them to the active comp, at time?, index?, position? as in layer.addItem), background?: bool (probe the files in a background job, jobs.list / jobs.wait; returns {job})}", + "{paths: [string] (files or folders), sequence?: bool (the Sequence checkbox: true imports numbered stills of one run as one image sequence, one picked file bringing its whole run and several picked files that range; by default picked files import as stills and folders' numbered stills as sequences; false imports a folder's files individually), alphabetical?: bool (Force Alphabetical Order: every image of that type in the folder, by name), frameRate?: fps (image sequences; default Settings ▸ Import ▸ Sequence Footage), importAs?: footage|composition|compositionLayerSizes (Photoshop, PDF, Illustrator and EPS files), layer?: name|index (footage of one Photoshop layer), page?: number from 1 (PDF / Illustrator page), drag?: bool (dropped files: Settings ▸ Import ▸ Default Drag Import As), addToComp?: bool (also add them to the active comp, at time?, index?, position? as in layer.addItem), background?: bool (probe the files in a background job, jobs.list / jobs.wait; returns {job})}", always, import_cmd ), diff --git a/crates/engine/src/commands/panels_cmds.rs b/crates/engine/src/commands/panels_cmds.rs index bcdb7a73..40241e6f 100644 --- a/crates/engine/src/commands/panels_cmds.rs +++ b/crates/engine/src/commands/panels_cmds.rs @@ -105,7 +105,10 @@ fn browser_import(s: &mut Session, p: &Value) -> Result { return Ok(json!({"pending": true, "paths": list})); } let target = s.active_comp_id(); - let mut r = s.execute("file.import", json!({"paths": paths}))?; + // A numbered still picked in the browser still brings its image sequence (`sequence: false` + // for the picked stills alone); `file.import` alone imports stills. + let sequence = p.get("sequence").and_then(Value::as_bool).unwrap_or(true); + let mut r = s.execute("file.import", json!({"paths": paths, "sequence": sequence}))?; // Dragged into the Timeline or the Composition viewer: into the comp, where they were dropped. let errors = super::file::add_to_comp(s, &r, target, p); if !errors.is_empty() @@ -236,7 +239,7 @@ pub fn specs() -> Vec { "Import", [], None, - "{paths, addToComp?, time?, index?, position? (where the layers go, as in layer.addItem)}", + "{paths, sequence? (default true: a numbered still brings its image sequence), addToComp?, time?, index?, position? (where the layers go, as in layer.addItem)}", browser_enabled, browser_import ), diff --git a/crates/engine/src/sequence.rs b/crates/engine/src/sequence.rs index 22180350..79142955 100644 --- a/crates/engine/src/sequence.rs +++ b/crates/engine/src/sequence.rs @@ -20,15 +20,17 @@ use crate::{Importer, Services}; /// How an import treats numbered stills. #[derive(Clone, Copy, Debug)] pub struct SequenceOptions { - /// The " Sequence" checkbox: numbered stills of one run import as a sequence. + /// The " Sequence" checkbox: picked numbered stills of one run import as a sequence. pub sequence: bool, + /// The numbered stills inside a picked or dropped folder import as sequences. + pub folders: bool, /// Force Alphabetical Order. pub alphabetical: bool, } impl Default for SequenceOptions { fn default() -> Self { - SequenceOptions { sequence: true, alphabetical: false } + SequenceOptions { sequence: true, folders: true, alphabetical: false } } } @@ -36,7 +38,19 @@ impl SequenceOptions { /// From command parameters `sequence?` (default on) and `alphabetical?`. pub fn from_params(p: &serde_json::Value) -> SequenceOptions { let flag = |k: &str| p.get(k).and_then(serde_json::Value::as_bool); - SequenceOptions { sequence: flag("sequence").unwrap_or(true), alphabetical: flag("alphabetical").unwrap_or(false) } + let sequence = flag("sequence").unwrap_or(true); + SequenceOptions { sequence, folders: sequence, alphabetical: flag("alphabetical").unwrap_or(false) } + } + + /// `file.import`'s options, as in After Effects: picked or dropped files import as stills + /// unless `sequence: true` (the Import dialog's " Sequence" checkbox, which is off + /// unless ticked), while a folder's numbered stills import as sequences unless + /// `sequence: false` (Alt-drag of a folder). + pub fn for_import(p: &serde_json::Value) -> SequenceOptions { + let flag = |k: &str| p.get(k).and_then(serde_json::Value::as_bool); + let (sequence, alphabetical) = (flag("sequence"), flag("alphabetical").unwrap_or(false)); + // Force Alphabetical Order is part of the Sequence option. + SequenceOptions { sequence: sequence.unwrap_or(alphabetical), folders: sequence.unwrap_or(true), alphabetical } } } @@ -120,28 +134,27 @@ fn run_files(services: &dyn Services, path: &str, alphabetical: bool) -> Vec Vec { - let mut files: Vec = vec![]; + // Each file, and whether it may join a sequence. + let mut files: Vec<(String, bool)> = vec![]; for p in paths { if services.is_dir(p) { let mut inside: Vec = services.list_dir(p).into_iter().filter(|f| matches!(crate::media_browser::kind_of(f), Some(k) if k != "project")).collect(); inside.sort_by_key(|f| file_name(f).to_lowercase()); - files.extend(inside); + files.extend(inside.into_iter().map(|f| (f, opts.folders))); } else { - files.push(p.clone()); + files.push((p.clone(), opts.sequence)); } } - if !opts.sequence { - return files.into_iter().map(Source::File).collect(); - } // Runs in the order their first file was picked. let mut runs: Vec<((String, String, String), Vec)> = vec![]; let mut out: Vec> = vec![]; let mut slot: Vec = vec![]; - for f in files { - match run_key(&f, opts.alphabetical) { + for (f, joins) in files { + match run_key(&f, opts.alphabetical).filter(|_| joins) { Some(key) => match runs.iter_mut().find(|(k, _)| *k == key) { Some((_, run)) => run.push(f), None => { @@ -155,8 +168,10 @@ pub fn group(services: &dyn Services, paths: &[String], opts: SequenceOptions) - } for ((_, mut picked), at) in runs.into_iter().zip(slot) { sort_run(&mut picked, opts.alphabetical); - // One file: its whole run from the folder. - if let [one] = picked.as_slice() { + // One picked file: its whole run from the folder. + if let [one] = picked.as_slice() + && opts.sequence + { let all = run_files(services, one, opts.alphabetical); if all.len() >= 2 { picked = all; @@ -176,7 +191,7 @@ pub fn group(services: &dyn Services, paths: &[String], opts: SequenceOptions) - /// The image sequences importing `paths` would make (the Import dialog asks about them). pub fn sequences(services: &dyn Services, paths: &[String], alphabetical: bool) -> Vec> { - group(services, paths, SequenceOptions { sequence: true, alphabetical }) + group(services, paths, SequenceOptions { sequence: true, folders: true, alphabetical }) .into_iter() .filter_map(|s| match s { Source::Sequence { files, .. } => Some(files), @@ -282,7 +297,10 @@ mod tests { ] ); // Sequence off: every file on its own. - assert_eq!(group(&t, &strs(&["/s/a_0001.png"]), SequenceOptions { sequence: false, alphabetical: false }), [Source::File("/s/a_0001.png".into())]); + assert_eq!( + group(&t, &strs(&["/s/a_0001.png"]), SequenceOptions { sequence: false, folders: false, alphabetical: false }), + [Source::File("/s/a_0001.png".into())] + ); // A folder: its media files, runs as sequences. let g = group(&t, &strs(&["/s"]), on); assert_eq!(g.len(), 4, "{g:?}"); @@ -290,12 +308,29 @@ mod tests { assert_eq!(g[0].name(), "a_[0001-0010].png"); assert!(!g.iter().any(|s| s.path().ends_with(".txt"))); // Force Alphabetical Order: every PNG in the folder, by name. - let a = group(&t, &strs(&["/s/c.png"]), SequenceOptions { sequence: true, alphabetical: true }); + let a = group(&t, &strs(&["/s/c.png"]), SequenceOptions { sequence: true, folders: true, alphabetical: true }); assert_eq!(a, [Source::Sequence { files: strs(&["/s/a_0001.png", "/s/a_0002.png", "/s/a_0010.png", "/s/b_1.png", "/s/c.png"]), alphabetical: true }]); assert_eq!(a[0].name(), "a_0001.png"); assert_eq!(sequences(&t, &strs(&["/s/a_0001.png", "/s/c.png"]), false).len(), 1); } + /// #431: `file.import` brings picked or dropped numbered stills in as stills; only a folder or + /// an explicit `sequence: true` makes a sequence, as After Effects' Sequence checkbox does. + #[test] + fn import_takes_stills_unless_asked_for_a_sequence() { + let t = Table(vec!["/s/a_0001.png", "/s/a_0002.png", "/s/a_0003.png", "/s/IMG_0001.jpg", "/s/IMG_0042.jpg"]); + let opts = |p: serde_json::Value| SequenceOptions::for_import(&p); + let none = opts(serde_json::json!({"drag": true})); + assert_eq!(group(&t, &strs(&["/s/a_0002.png"]), none), [Source::File("/s/a_0002.png".into())]); + assert_eq!(group(&t, &strs(&["/s/IMG_0001.jpg", "/s/IMG_0042.jpg"]), none).len(), 2); + // A dropped folder: its runs as sequences (Alt-drag: `sequence: false`, files on their own). + assert_eq!(group(&t, &strs(&["/s"]), none).len(), 2); + assert_eq!(group(&t, &strs(&["/s"]), opts(serde_json::json!({"sequence": false}))).len(), 5); + // The Sequence checkbox: one picked file brings its whole run. + let on = group(&t, &strs(&["/s/a_0002.png"]), opts(serde_json::json!({"sequence": true}))); + assert!(matches!(on.as_slice(), [Source::Sequence { files, .. }] if files.len() == 3), "{on:?}"); + } + #[test] fn windows_paths_and_reports() { let t = Table(vec!["C:\\r\\f_01.exr", "C:\\r\\f_02.exr", "C:\\r\\f_05.exr"]); diff --git a/crates/engine/src/tests_sequence.rs b/crates/engine/src/tests_sequence.rs index cd406726..d59a9298 100644 --- a/crates/engine/src/tests_sequence.rs +++ b/crates/engine/src/tests_sequence.rs @@ -54,7 +54,7 @@ const RUN: [&str; 5] = ["shot_0001.png", "shot_0002.png", "shot_0004.png", "shot fn one_picked_numbered_file_imports_its_run_as_one_sequence() { let dir = folder(&RUN); let mut s = session(); - let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0002.png")]})).unwrap(); + let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0002.png")], "sequence": true})).unwrap(); assert_eq!(r["items"].as_array().unwrap().len(), 1, "{r}"); let (name, f) = footage(&s, &r["items"][0]); assert_eq!(name, "shot_[0001-0005].png"); @@ -75,17 +75,33 @@ fn picking_every_frame_imports_one_sequence() { let dir = folder(&RUN); let mut s = session(); let all: Vec = RUN[..4].iter().map(|n| at(&dir, n)).collect(); - let r = s.execute("file.import", json!({"paths": all, "frameRate": 24})).unwrap(); + let r = s.execute("file.import", json!({"paths": all, "sequence": true, "frameRate": 24})).unwrap(); assert_eq!(r["items"].as_array().unwrap().len(), 1, "{r}"); let (_, f) = footage(&s, &r["items"][0]); assert_eq!(f.sequence.len(), 4); // `frameRate` sets the sequence's rate on import. assert_eq!((f.frame_rate, f.duration), (FrameRate::FPS_24, FrameRate::FPS_24.tick_of(5))); // A picked range is just that range. - let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0004.png"), at(&dir, "shot_0005.png")]})).unwrap(); + let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0004.png"), at(&dir, "shot_0005.png")], "sequence": true})).unwrap(); let (name, f) = footage(&s, &r["items"][0]); assert_eq!((name.as_str(), f.sequence.len()), ("shot_[0004-0005].png", 2)); - assert!(s.execute("file.import", json!({"paths": all, "frameRate": 0})).is_err()); + assert!(s.execute("file.import", json!({"paths": all, "sequence": true, "frameRate": 0})).is_err()); +} + +/// #431: without the Sequence option, picked or dropped numbered stills import as stills (one +/// item each), as in After Effects; a dropped folder still makes its runs sequences. +#[test] +fn numbered_stills_import_as_stills_by_default() { + let dir = folder(&RUN); + let mut s = session(); + let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0002.png")], "drag": true})).unwrap(); + let (name, f) = footage(&s, &r["items"][0]); + assert_eq!((name.as_str(), f.kind), ("shot_0002.png", FootageKind::Still)); + let r = s.execute("file.import", json!({"paths": [at(&dir, "shot_0001.png"), at(&dir, "shot_0004.png")]})).unwrap(); + assert_eq!(r["items"].as_array().unwrap().len(), 2, "{r}"); + let r = s.execute("file.import", json!({"paths": [dir.clone()], "drag": true})).unwrap(); + let (name, f) = footage(&s, &r["items"][0]); + assert_eq!((name.as_str(), f.kind), ("shot_[0001-0005].png", FootageKind::Sequence)); } #[test] @@ -127,7 +143,7 @@ fn force_alphabetical_order_takes_every_image_of_the_type() { fn interpret_footage_start_timecode_relabels_source_time() { let dir = folder(&RUN); let mut s = session(); - let item = s.execute("file.import", json!({"paths": [at(&dir, "shot_0001.png")]})).unwrap()["items"][0].clone(); + let item = s.execute("file.import", json!({"paths": [at(&dir, "shot_0001.png")], "sequence": true})).unwrap()["items"][0].clone(); s.execute("comp.new", json!({"name": "Main", "width": 64, "height": 32, "frameRate": 30, "duration": 10})).unwrap(); let layer = s.execute("layer.addItem", json!({"item": item})).unwrap()["layer"].as_u64().unwrap(); let out = |s: &Session| s.active_comp().unwrap().layer(effectcraft_project::LayerId(layer)).unwrap().out_point; diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index 93e05105..dfc00642 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -154,8 +154,9 @@ fn is_pdf(p: &Value) -> bool { /// File ▸ Import ▸ File… picked numbered stills that import as image sequences: ask, as After /// Effects' " Sequence" checkbox in its Import dialog does (rfd's native dialog can't /// carry a checkbox), with Force Alphabetical Order and the frame rate. `p`: the import's -/// parameters. Returns whether the dialog opened. Dropped files and agents' `file.import` -/// don't ask (`sequence` defaults to on). +/// parameters. Returns whether the dialog opened. The checkbox starts off, as in After Effects: +/// unticked, the picked files import as stills. Dropped files and agents' `file.import` don't +/// ask (files import as stills, folders' runs as sequences). pub fn open_import_sequence(app: &mut EffectcraftApp, p: &Value) -> bool { if has(p, &["sequence", "alphabetical"]) { return false; @@ -187,7 +188,7 @@ fn sequence_fields(app: &EffectcraftApp, runs: &[Vec]) -> Vec { }; vec![ Field::note("note", ¬e), - Field::bool("sequence", &format!("{ext} Sequence"), true), + Field::bool("sequence", &format!("{ext} Sequence"), false), Field::bool("alphabetical", "Force alphabetical order", false), Field::num("frameRate", "Frame rate (fps)", app.session.prefs.import.sequence_fps), Field::note("after", "Change the frame rate, alpha and start frame later with File ▸ Interpret Footage."), diff --git a/crates/ui-egui/src/prefs_live.rs b/crates/ui-egui/src/prefs_live.rs index ed497dc4..b18d3e87 100644 --- a/crates/ui-egui/src/prefs_live.rs +++ b/crates/ui-egui/src/prefs_live.rs @@ -88,7 +88,10 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { None }; - let params = json!({ + // Dropped files import as stills and folders' numbered stills as sequences; Alt-drag + // imports a folder's files one by one (After Effects). + let alt = ctx.input(|i| i.modifiers.alt); + let mut params = json!({ "paths": rest, "drag": true, "importAs": app.session.prefs.drag_import_as(), @@ -96,6 +99,9 @@ pub fn dropped_files(app: &mut EffectcraftApp, ui: &egui::Ui, bounds: Rect) { "position": at, "background": true }); + if alt { + params["sequence"] = json!(false); + } if let Err(e) = crate::menus::invoke(app, &ctx, "file.import", params) { errors.push(crate::i18n::tr_args("Cannot import dropped files: {}", &[&e])); diff --git a/crates/ui-egui/tests/ui_drag_drop.rs b/crates/ui-egui/tests/ui_drag_drop.rs index 3dee2727..d9e04ec3 100644 --- a/crates/ui-egui/tests/ui_drag_drop.rs +++ b/crates/ui-egui/tests/ui_drag_drop.rs @@ -408,7 +408,7 @@ fn footage_items(h: &Harness<'_, EffectcraftApp>) -> Vec<(String, usize)> { /// #297: File ▸ Import ▸ File… on one numbered still asks, as After Effects' "PNG Sequence" /// checkbox does, whether to import its whole run as one image sequence (and at what frame -/// rate); unticked, the file imports as a still. +/// rate); unticked (the default, as in After Effects), the file imports as a still. #[test] fn import_dialog_offers_the_image_sequence_option() { let (mut h, _) = harness(); @@ -424,35 +424,41 @@ fn import_dialog_offers_the_image_sequence_option() { assert_eq!(label(&h, "form.field.sequence").as_deref(), Some("PNG Sequence")); assert!(label(&h, "form.field.alphabetical").is_some() && label(&h, "form.field.frameRate").is_some()); assert!(footage_items(&h).is_empty(), "nothing imported before OK"); + // Unticked (the default, #431): the picked file alone, as a still. click_id(&mut h, "form.ok"); step_until(&mut h, |h| !footage_items(h).is_empty()); - assert_eq!(footage_items(&h), [("frame_[0001-0003].png".to_string(), 3)]); + assert_eq!(footage_items(&h), [("frame_0002.png".to_string(), 0)]); - // Unticked: the picked file alone, as a still. + // Ticked: the whole run as one sequence. effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "file.import", json!({})).unwrap(); h.run_steps(2); click_id(&mut h, "form.field.sequence"); click_id(&mut h, "form.ok"); step_until(&mut h, |h| footage_items(h).len() == 2); - assert_eq!(footage_items(&h)[1], ("frame_0002.png".to_string(), 0)); + assert_eq!(footage_items(&h)[1], ("frame_[0001-0003].png".to_string(), 3)); let _ = std::fs::remove_dir_all(&dir); } -/// #297: every frame of a sequence dropped on the window arrives as one image sequence (it used -/// to make one item per frame), and Interpret Footage shows its frames and Start Timecode (missing frames -/// always show colour bars, as in After Effects: there is no Missing Frames choice). +/// #297: a folder of frames dropped on the window arrives as one image sequence, and Interpret +/// Footage shows its frames and Start Timecode (missing frames always show colour bars, as in +/// After Effects: there is no Missing Frames choice). #431: dropped files import as stills, one +/// item each, as in After Effects. #[test] -fn dropped_frames_import_as_one_sequence_with_sequence_interpretation() { +fn a_dropped_folder_imports_as_one_sequence_with_sequence_interpretation() { let (mut h, _) = harness(); let dir = frames("drop", 4); h.state_mut().session.importer = Some(Arc::new(Stills)); - for k in 1..=4 { + for k in 1..=2 { h.input_mut().dropped_files.push(Arc::new(Dropped(dir.join(format!("frame_{k:04}.png"))))); } h.run_steps(2); - step_until(&mut h, |h| !footage_items(h).is_empty()); + step_until(&mut h, |h| footage_items(h).len() == 2); + assert_eq!(footage_items(&h), [("frame_0001.png".to_string(), 0), ("frame_0002.png".to_string(), 0)]); + h.input_mut().dropped_files.push(Arc::new(Dropped(dir.clone()))); h.run_steps(2); - assert_eq!(footage_items(&h), [("frame_[0001-0004].png".to_string(), 4)]); + step_until(&mut h, |h| footage_items(h).len() == 3); + h.run_steps(2); + assert_eq!(footage_items(&h)[2], ("frame_[0001-0004].png".to_string(), 4)); assert!(h.state().dialog.is_none(), "drops don't ask"); // Interpret Footage on it (the import selected it). let ctx = h.ctx.clone(); diff --git a/docs/agents.md b/docs/agents.md index a0f4e7d0..62fcd216 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -251,9 +251,10 @@ build real projects through these interfaces; they are worked examples of everyt then `keys.velocity {"outSpeed":0,"outInfluence":33.33}`. * **Gradients**: `set_property {"path":"contents/gfill/colors","value":["#0080ff","#ffff00"]}` or `{"colors":[[0,"#0080ff"],[1,[1,1,0]]],"opacities":[[0,1],[1,0.5]]}`. -* **Image sequences**: `file.import {"paths":["/r/shot_0001.png"]}` imports the file's whole - numbered run as one item (several picked frames: that range; a folder: each run in it; - `"sequence": false`: one still; `"alphabetical": true`: Force Alphabetical Order, the files +* **Image sequences**: `file.import {"paths":["/r/shot_0001.png"], "sequence": true}` imports the + file's whole numbered run as one item (several picked frames: that range; a folder, even without + `sequence`: each run in it; without `sequence` picked files import as stills, as in After + Effects; `"alphabetical": true`: Force Alphabetical Order, the files play one after another) at `frameRate` or 30 fps (Settings ▸ Import). Missing frames show colour bars, as in After Effects. `file.interpretFootage {"items":[id], "frameRate":12, "startTimecode":1001}` conforms them and sets Override Start (the item and layers that ran to its end get the new diff --git a/docs/footage.md b/docs/footage.md index 784a75ad..d25dcc34 100644 --- a/docs/footage.md +++ b/docs/footage.md @@ -26,7 +26,7 @@ dialog shows the run it found (and any missing frames) with these options: | Option | What it does | |---|---| -| **PNG Sequence** (EXR, TIFF, JPG… after the file type) | On: the whole run imports as one sequence. Off: just the picked file imports, as a still. | +| **PNG Sequence** (EXR, TIFF, JPG… after the file type) | On: the whole run imports as one sequence. Off (the default, as in After Effects): the picked files import as stills. | | **Force alphabetical order** | Imports every image of that type in the folder in name order, whatever the numbering, and plays the files one after another (gaps in the numbering don't count). Use it for files that aren't numbered consistently. | | **Frame rate (fps)** | The sequence's frame rate. The default comes from Settings ▸ Import ▸ Sequence Footage (30 fps). | @@ -34,10 +34,14 @@ Other ways in: - **Pick part of a run** (Shift-click the first and the last frame in the file dialog) to import just that range as one sequence. -- **Pick or drop every frame** of the run: it arrives as one sequence, not one item per frame. +- **Pick every frame** of the run with the Sequence option on: it arrives as one sequence, not + one item per frame. - **Drop a folder**: its media files import, and each numbered run in it becomes a sequence. -- **In the browser** (the web app) there are no folders to look in, so pick or drop all the - frames of the sequence at once. + Alt-drag the folder to import its files one by one. +- **Dropped files** import as stills, one item each, as in After Effects; drop the folder (or use + File ▸ Import ▸ File… with the Sequence option) for a sequence. +- **In the browser** (the web app) there are no folders to look in, so pick all the frames of + the sequence at once with the Sequence option on. Stills of any format EffectCraft reads can form a sequence: PNG, JPEG, TIFF, OpenEXR, BMP and WebP. Layered and vector documents (PSD, SVG, PDF) import one by one. @@ -48,8 +52,9 @@ Effects does, so every other frame stays at its number. Render or copy the missi folder and choose File ▸ Reload Footage to fill the gaps. Agents: `file.import {"paths": ["/shots/shot_0001.png"], "sequence": true, "frameRate": 24}`. -`sequence` is on by default; `"sequence": false` imports the file as a still, and -`"alphabetical": true` is Force Alphabetical Order. +Without `sequence`, picked files import as stills and a folder's numbered runs as sequences; +`"sequence": true` is the Sequence option (one file brings its whole run), `"sequence": false` +imports a folder's files one by one, and `"alphabetical": true` is Force Alphabetical Order. ## Interpret Footage