From de26297c368f4caa90b94a54f5f42044b9514396 Mon Sep 17 00:00:00 2001 From: Vikram Bhaskaran Date: Tue, 13 Oct 2026 01:34:58 +1100 Subject: [PATCH] Sample footage pixels in script expressions (#1000) A script reading an expression that calls sampleImage on a footage layer got [0,0,0,0]: the script evaluator built its EvalCtx with EvalCtx::new, which has no footage source, so the renderer substituted transparent frames (solids and shapes stayed correct because they are procedural). Pass the session's footage source, as the app's renderer and diagnostics already do. --- crates/script/src/model.rs | 3 +++ crates/script/src/tests.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/crates/script/src/model.rs b/crates/script/src/model.rs index 4445fbbf..f912f111 100644 --- a/crates/script/src/model.rs +++ b/crates/script/src/model.rs @@ -468,6 +468,9 @@ fn value(s: &Session, a: &J) -> R { } else { let mut ctx = EvalCtx::new(&s.project, cid, c, t); ctx.expr = s.expr.as_deref(); + // Sample expressions (`sampleImage`) read footage through the session's source; without + // it footage layers sample as transparent (#1000). + ctx.footage = Some(s.footage.as_ref()); ctx.value(l, p) }; Ok(js_value(c, &v)) diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 303bd820..0ef923f6 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -731,6 +731,42 @@ fn import_options_sequence_picks_one_still_or_the_run() { let _ = std::fs::remove_dir_all(&dir); } +/// A flat green footage source, so `sampleImage` on a footage layer has known pixels. +struct Flat; +impl effectcraft_engine::render::FootageSource for Flat { + fn frame( + &self, + _item: effectcraft_engine::project::ItemId, + f: &effectcraft_engine::project::Footage, + _t: effectcraft_engine::time::Tick, + ) -> Option> { + Some(Arc::new(effectcraft_engine::render::Image::filled(f.width.max(1), f.height.max(1), [0.0, 1.0, 0.0, 1.0]))) + } +} + +/// `sampleImage` on a footage layer reads the footage's pixels from a script, not transparent +/// black: the expression evaluator needs the session's footage source (#1000). +#[test] +fn sample_image_reads_footage_pixels_in_scripts() { + let mut s = session(); + s.footage = Arc::new(Flat); + s.importer = Some(Arc::new(Stills)); + let out = ok( + &mut s, + r#" + var comp = app.project.items.addComp("Scene", 640, 360, 1, 1, 25); + comp.openInViewer(); + var img = comp.layers.add(app.project.importFile(new ImportOptions(new File("/tmp/ec-flat.png")))); + img.name = "Img"; + var cc = comp.layers.addNull().property("ADBE Effect Parade").addProperty("ADBE Color Control").property(1); + cc.expression = 'thisComp.layer("Img").sampleImage([32, 16], [0.5, 0.5], true, time);'; + var v = cc.valueAtTime(0, false); + [v[0], v[1], v[2], v[3], cc.expressionError]; + "#, + ); + assert_eq!(out.result, json!([0, 1, 0, 1, ""]), "green footage sampled, not transparent black"); +} + #[test] fn binary_file_io_preserves_all_byte_values() { let dir = std::env::temp_dir().join(format!("ec-script-binary-{}", std::process::id()));