mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Import: Automatically Reload Footage is Off / Non-Sequence Footage / All Footage, as in After Effects; a reload never shows disk-cached frames from before (#578 follow-up)
Image sequences are watched only with All Footage (the default, Non-Sequence Footage, leaves them alone). Purge also forgets the viewer's per-revision disk keys, which hash footage files' size and time, so a file reloaded without a new revision is keyed again. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
8 files changed
+51
-13
No files matched your search
@@ -980,7 +980,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reload Changed Footage",
|
||||
[],
|
||||
None,
|
||||
"{} — reload the footage whose file changed on disk since it was last seen (size or modification time; Settings ▸ Import ▸ Reload Footage Changed on Disk runs this in the background); files seen for the first time are only remembered. Returns {files, reloaded}",
|
||||
"{} — reload the footage whose file changed on disk since it was last seen (size or modification time; Settings ▸ Import ▸ Automatically Reload Footage runs this in the background; image sequences only with All Footage); files seen for the first time are only remembered. Returns {files, reloaded}",
|
||||
has_items,
|
||||
crate::footage_reload::command
|
||||
),
|
||||
|
||||
@@ -29,11 +29,17 @@ pub type Stamp = (u64, u128);
|
||||
/// Stamps taken on a worker thread, waiting for [`poll_scan`].
|
||||
pub type Scan = Arc<Mutex<Option<Vec<(String, Option<Stamp>)>>>>;
|
||||
|
||||
/// The files footage items read: each item's file and every frame of an image sequence.
|
||||
/// The files footage items read: each item's file and every frame of an image sequence. Image
|
||||
/// sequences count only with Automatically Reload Footage set to All Footage (After Effects'
|
||||
/// default, Non-Sequence Footage, leaves them alone: thousands of frames on a share add up).
|
||||
fn watched(s: &Session) -> BTreeSet<String> {
|
||||
let sequences = s.prefs.import.auto_reload_footage == "all";
|
||||
let mut paths = BTreeSet::new();
|
||||
for it in s.project.items.values() {
|
||||
if let ItemKind::Footage(f) = &it.kind {
|
||||
if !f.sequence.is_empty() && !sequences {
|
||||
continue;
|
||||
}
|
||||
if !f.path.is_empty() {
|
||||
paths.insert(f.path.clone());
|
||||
}
|
||||
|
||||
@@ -182,9 +182,10 @@ page!(Import {
|
||||
still_seconds: f64 = 5.0,
|
||||
sequence_fps: f64 = 30.0,
|
||||
report_missing_frames: bool = true,
|
||||
/// Reload footage whose file another app changed (size or modification time), checked every
|
||||
/// two seconds and when the window comes back to the front.
|
||||
reload_changed_footage: bool = true,
|
||||
/// Automatically Reload Footage whose file another app changed (size or modification time),
|
||||
/// checked every two seconds and when the window comes back to the front: `off`,
|
||||
/// `nonSequence` (the default, as in After Effects: image sequences are left alone) or `all`.
|
||||
auto_reload_footage: String = "nonSequence".into(),
|
||||
/// Interpret unlabeled alpha as: `ask`, `guess`, `ignore`, `straight`, `premultiplied`.
|
||||
unlabeled_alpha: String = "ask".into(),
|
||||
/// Default drag import as: `footage`, `comp`, `compLayerSizes`.
|
||||
@@ -1087,7 +1088,12 @@ pub fn pages() -> Vec<Page> {
|
||||
s("import.stillSeconds", "Still Duration", Kind::Float(0.04, 86_400.0, "s"), true),
|
||||
s("import.sequenceFps", "Sequence Footage", Kind::Float(1.0, 999.0, "frames per second"), true),
|
||||
s("import.reportMissingFrames", "Report Missing Frames", B, true),
|
||||
s("import.reloadChangedFootage", "Reload Footage Changed on Disk", B, true),
|
||||
s(
|
||||
"import.autoReloadFootage",
|
||||
"Automatically Reload Footage",
|
||||
Kind::Choice(&[("Off", "off"), ("Non-Sequence Footage", "nonSequence"), ("All Footage", "all")]),
|
||||
true,
|
||||
),
|
||||
s(
|
||||
"import.unlabeledAlpha",
|
||||
"Interpret Unlabeled Alpha As",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Footage changed on disk by another app, through the fully wired session (media decoding
|
||||
//! included): File ▸ Reload Footage shows the new pixels and keeps a Photoshop layer item on its
|
||||
//! layer, and `footage.reloadChanged` (Settings ▸ Import ▸ Reload Footage Changed on Disk) finds
|
||||
//! layer, and `footage.reloadChanged` (Settings ▸ Import ▸ Automatically Reload Footage) finds
|
||||
//! changed files by itself.
|
||||
|
||||
use effectcraft_engine::render::RenderOpts;
|
||||
@@ -168,3 +168,24 @@ fn background_scan_reloads_changed_footage() {
|
||||
assert!(near(pixel(&s, cid, 32, 24), GREEN));
|
||||
assert!(s.footage_scan.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_sequences_reload_by_themselves_only_with_all_footage() {
|
||||
let (a, b) = (tmp("seq_0001.psd"), tmp("seq_0002.psd"));
|
||||
std::fs::write(&a, flat(RED, false)).unwrap();
|
||||
std::fs::write(&b, flat(RED, false)).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
let r = s.execute_checked("file.import", json!({"paths": [&a], "sequence": true})).unwrap();
|
||||
let item = ItemId(r["items"][0].as_u64().unwrap());
|
||||
assert_eq!(footage(&s, item).sequence.len(), 2, "imported as a sequence");
|
||||
// Non-Sequence Footage (the default, as in After Effects): sequences are never stamped.
|
||||
assert_eq!(s.prefs.import.auto_reload_footage, "nonSequence");
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
save(&a, &flat(BLUE, true));
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
// All Footage: every frame is watched (first seen now, so only remembered).
|
||||
s.prefs.import.auto_reload_footage = "all".into();
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap(), json!({"files": 0, "reloaded": 0}));
|
||||
save(&b, &flat(GREEN, true));
|
||||
assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap()["files"], json!(1));
|
||||
}
|
||||
@@ -283,10 +283,8 @@ mod tests {
|
||||
let list: Vec<AnyChannel<FlatSamples>> = chans.iter().map(|c| AnyChannel::new(*c, FlatSamples::F32(vec![v; w * h]))).collect();
|
||||
Layer::new((w, h), LayerAttributes::named(name), Encoding::FAST_LOSSLESS, AnyChannels::sort(list.into()))
|
||||
};
|
||||
let layers = vec![
|
||||
part("ViewLayer.Depth", &["ViewLayer.Depth.Z"], 4.0),
|
||||
part("ViewLayer.Combined", &["ViewLayer.Combined.R", "ViewLayer.Combined.G"], 0.5),
|
||||
];
|
||||
let layers =
|
||||
vec![part("ViewLayer.Depth", &["ViewLayer.Depth.Z"], 4.0), part("ViewLayer.Combined", &["ViewLayer.Combined.R", "ViewLayer.Combined.G"], 0.5)];
|
||||
let image = Image::from_layers(ImageAttributes::with_size((w, h)), layers);
|
||||
let mut bytes = std::io::Cursor::new(Vec::new());
|
||||
image.write().to_buffered(&mut bytes).unwrap();
|
||||
|
||||
@@ -701,6 +701,12 @@ impl Frames {
|
||||
c.bytes = 0;
|
||||
c.gpu_bytes = 0;
|
||||
}
|
||||
// Disk keys hash the footage files' size and time, but are remembered per revision; a
|
||||
// file reloaded from disk without a new revision (a re-rendered image sequence) must be
|
||||
// keyed again, or the disk cache serves the frames from before.
|
||||
if let Ok(mut k) = self.content_keys.lock() {
|
||||
k.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Queue a prefetch frame (no-op if cached or already queued/rendering).
|
||||
|
||||
@@ -1565,7 +1565,7 @@ impl EffectcraftApp {
|
||||
let focused = ctx.input(|i| i.focused);
|
||||
let was_focused: bool = ctx.data(|d| d.get_temp(focus_key)).unwrap_or(focused);
|
||||
ctx.data_mut(|d| d.insert_temp(focus_key, focused));
|
||||
if !self.session.prefs.import.reload_changed_footage || self.session.project.items.is_empty() {
|
||||
if self.session.prefs.import.auto_reload_footage == "off" || self.session.project.items.is_empty() {
|
||||
return;
|
||||
}
|
||||
let last: f64 = ctx.data(|d| d.get_temp(last_key)).unwrap_or(0.0);
|
||||
|
||||
+2
-1
@@ -109,7 +109,8 @@ Commands (CLI, MCP, control channel):
|
||||
- `type.fontNamesInEnglish`: off, fonts with a native-language family name show it
|
||||
- `import.reportMissingFrames`: gaps in an image sequence's numbering are reported on import
|
||||
(they show as colour-bar placeholders, as in After Effects; see [footage.md](footage.md))
|
||||
- `import.reloadChangedFootage`: Reload Footage Changed on Disk (on by default). Every two seconds,
|
||||
- `import.autoReloadFootage`: Automatically Reload Footage, `off`, `nonSequence` (the default, as
|
||||
in After Effects) or `all` (image sequences too). Every two seconds,
|
||||
and as soon as the window comes back to the front, the desktop app compares each footage file's
|
||||
size and modification time with what they were, and reloads what another app saved: a
|
||||
Photoshop file edited in PhotoCraft or Photoshop shows its new pixels without File ▸ Reload
|
||||
|
||||
Reference in new issue
Block a user