From f72e17a2bb2a594375e813a9cae4f9459e7906b4 Mon Sep 17 00:00:00 2001 From: Camille Moine Date: Mon, 19 Oct 2026 10:25:17 +0200 Subject: [PATCH] Engine: Video's Alt-click and Hide Other Video take cameras and lights along The Timeline draws the Video switch (the eye) on every layer's row, nulls, cameras and lights included, but set_switch_others and hide_other_video skipped cameras and lights: Alt-clicking the eye of a selected layer neither set them apart with the selection nor hid them with the rest. Both now use the rule the eye cell shows: every layer but track mattes. --- crates/engine/src/commands/layer.rs | 6 ++++-- crates/engine/src/commands/layer_menu.rs | 8 ++++---- crates/engine/src/tests_menu_cmds.rs | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/commands/layer.rs b/crates/engine/src/commands/layer.rs index b798493b..0832163d 100644 --- a/crates/engine/src/commands/layer.rs +++ b/crates/engine/src/commands/layer.rs @@ -417,8 +417,10 @@ fn set_switch_others(s: &mut Session, cid: ItemId, ids: &[LayerId], name: &str, let applies = |l: &Layer| match name { "audio" => l.props.sub("audio").is_some(), "lock" | "locked" => true, - // Nulls have a Video switch too (it hides their outline in the viewer). - "video" | "eye" => (l.source.is_av() || matches!(l.source, LayerSource::Null)) && !mattes.contains(&l.id), + // Every layer has a Video switch (it hides a null's outline in the viewer and turns a + // camera or light off), as the Timeline's eye cell shows; track mattes keep theirs off + // by design. + "video" | "eye" => !mattes.contains(&l.id), _ => l.source.is_av() && !mattes.contains(&l.id), }; let mut alone = true; diff --git a/crates/engine/src/commands/layer_menu.rs b/crates/engine/src/commands/layer_menu.rs index 9cac8041..064b2fb3 100644 --- a/crates/engine/src/commands/layer_menu.rs +++ b/crates/engine/src/commands/layer_menu.rs @@ -114,10 +114,10 @@ fn frame_blending(s: &mut Session, p: &Value) -> Result { fn hide_other_video(s: &mut Session, p: &Value) -> Result { let (cid, ids) = layers_p(s, p)?; s.edit("Hide Other Video", None, |proj, _| { - for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut() { - if !ids.contains(&l.id) && (l.source.is_av() || matches!(l.source, LayerSource::Null)) { - l.switches.video = false; - } + // Every layer has a Video switch (nulls, cameras and lights included, as the Timeline's + // eye cell shows): the selection stays shown. + for l in proj.comp_mut(cid).ok_or(EngineError::NoComp)?.layers.iter_mut().filter(|l| !ids.contains(&l.id)) { + l.switches.video = false; } Ok(()) })?; diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index 6da07dd4..d50257aa 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -111,6 +111,29 @@ fn set_switch_others() { assert!(s.execute("layer.setSwitch", json!({"layers": [c], "switch": "nope", "others": true})).is_err()); } +/// The Video switch has a cell on every layer's row, cameras and lights included, so Alt-clicking +/// it (and Hide Other Video) takes them along with the rest. +#[test] +fn video_others_takes_cameras_and_lights() { + let mut s = comp(); + let a = solid(&mut s, "#ff0000"); + let cam = s.execute("layer.newCamera", json!({})).unwrap()["layer"].as_u64().unwrap(); + let light = s.execute("layer.newLight", json!({"kind": "Point"})).unwrap()["layer"].as_u64().unwrap(); + let get = |s: &Session, f: fn(&effectcraft_project::Layer) -> bool| [a, cam, light].map(|l| f(&layer(s, l))); + // The camera and the light are hidden: Alt-click on the solid's eye brings them back, since + // they stand apart with it, and the next Alt-click hides them with every other layer. + s.execute("layer.setSwitch", json!({"layers": [cam, light], "switch": "video", "value": false})).unwrap(); + s.execute("layer.setSwitch", json!({"layers": [a], "switch": "video", "others": true})).unwrap(); + assert_eq!(get(&s, |l| l.switches.video), [true; 3]); + s.execute("layer.setSwitch", json!({"layers": [a], "switch": "video", "others": true})).unwrap(); + assert_eq!(get(&s, |l| l.switches.video), [true, false, false]); + // Hide Other Video takes them too. + s.execute("layer.showAllVideo", json!({})).unwrap(); + assert_eq!(get(&s, |l| l.switches.video), [true; 3]); + s.execute("layer.hideOtherVideo", json!({"layers": [a]})).unwrap(); + assert_eq!(get(&s, |l| l.switches.video), [true, false, false]); +} + /// A null's Video switch (the eye) works like any layer's, as in After Effects: it toggles, /// Alt-click and Hide Other Video take nulls along, and its children still follow it while it is /// hidden (parenting ignores the eye).