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:
Claude committed 2026-10-10 12:53:09 +00:00
1 parent c5af482bce
commit 57bd68c86e
8 files changed
+51 -13

No files matched your search

+1 -1
View File
@@ -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
),
+7 -1
View File
@@ -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());
}
+10 -4
View File
@@ -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",
+22 -1
View File
@@ -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));
}
+2 -4
View File
@@ -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();
+6
View File
@@ -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).
+1 -1
View File
@@ -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
View File
@@ -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