mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Null layers: the Video switch hides them in the Composition viewer, as in After Effects
Every layer gets the Video switch (eye) in the Timeline (nulls, cameras and lights were excluded by an is_av() check). A selected layer whose eye is off no longer draws its box, handles, anchor or paths in the viewer. Alt-click on an eye and Layer > Switches > Hide Other Video now take nulls along. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
549e9d8251
commit
a15a7a4818
@@ -417,6 +417,8 @@ 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),
|
||||
_ => l.source.is_av() && !mattes.contains(&l.id),
|
||||
};
|
||||
let mut alone = true;
|
||||
|
||||
@@ -115,7 +115,7 @@ fn hide_other_video(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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() {
|
||||
if !ids.contains(&l.id) && (l.source.is_av() || matches!(l.source, LayerSource::Null)) {
|
||||
l.switches.video = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,38 @@ fn set_switch_others() {
|
||||
assert!(s.execute("layer.setSwitch", json!({"layers": [c], "switch": "nope", "others": true})).is_err());
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[test]
|
||||
fn null_video_switch() {
|
||||
let mut s = comp();
|
||||
let a = solid(&mut s, "#ff0000");
|
||||
let n = s.execute("layer.newNull", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [n], "switch": "video"})).unwrap();
|
||||
assert!(!layer(&s, n).switches.video);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(layer(&s, n).switches.video);
|
||||
// Alt-click on the solid's eye hides the null too; on the null's, the solid.
|
||||
s.execute("layer.setSwitch", json!({"layers": [a], "switch": "video", "others": true})).unwrap();
|
||||
assert!(layer(&s, a).switches.video && !layer(&s, n).switches.video);
|
||||
s.execute("layer.setSwitch", json!({"layers": [n], "switch": "video", "others": true})).unwrap();
|
||||
assert!(!layer(&s, a).switches.video && layer(&s, n).switches.video);
|
||||
s.execute("layer.showAllVideo", json!({})).unwrap();
|
||||
s.execute("layer.hideOtherVideo", json!({"layers": [a]})).unwrap();
|
||||
assert!(layer(&s, a).switches.video && !layer(&s, n).switches.video);
|
||||
// The hidden null still carries its child.
|
||||
s.execute("layer.setParent", json!({"layers": [a], "parent": n})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": n, "path": "transform/position", "value": [420, 180, 0]})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let c = s.project.comp(cid).unwrap().clone();
|
||||
let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, &c, Tick::ZERO);
|
||||
let child = c.layer(effectcraft_project::LayerId(a)).unwrap();
|
||||
let (m, _) = ctx.layer_to_comp(child);
|
||||
let centre = m.apply(effectcraft_geom::vec2(100.0, 50.0));
|
||||
assert!((centre.x - 420.0).abs() < 1e-6 && (centre.y - 180.0).abs() < 1e-6, "{centre:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_switches_menu() {
|
||||
let mut s = comp();
|
||||
|
||||
@@ -1992,8 +1992,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
gp.line_segment([pos2(graph_x0, r.max.y - 0.5), pos2(r.max.x, r.max.y - 0.5)], Stroke::new(1.0, t.app_bg));
|
||||
// A/V features.
|
||||
let sw = &layer.switches;
|
||||
// Every layer has the Video switch (the eye), nulls, cameras and lights included,
|
||||
// as in After Effects: it hides a null's outline in the viewer and turns a camera or
|
||||
// light off.
|
||||
let av_items: [(Icon, bool, &str, bool); 4] = [
|
||||
(Icon::Eye, sw.video, "video", layer.source.is_av()),
|
||||
(Icon::Eye, sw.video, "video", true),
|
||||
(Icon::Speaker, sw.audio, "audio", layer.props.sub("audio").is_some()),
|
||||
(Icon::Solo, sw.solo, "solo", layer.source.is_av()),
|
||||
(Icon::Lock, sw.locked, "lock", true),
|
||||
|
||||
@@ -996,7 +996,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
// Layer Handles and Paths.
|
||||
let hs = app.session.prefs.general.path_point_size as f32 + 2.0;
|
||||
let label_handles = app.session.prefs.appearance.use_label_color_for_handles;
|
||||
for l in comp.layers.iter().filter(|l| selected.contains(&l.id) && l.is_active_at(time)) {
|
||||
// A layer whose Video switch is off has no box, handles or paths in the viewer (a hidden
|
||||
// null disappears from the comp, as in After Effects), even while it is selected.
|
||||
for l in comp.layers.iter().filter(|l| selected.contains(&l.id) && l.is_active_at(time) && l.switches.video) {
|
||||
let col = if label_handles { t.label(l.label) } else { t.accent };
|
||||
// Motion path of animated position (keyframes and spatial tangents are draggable).
|
||||
ov::motion_path(app, &painter, &map, &ectx, l, col, &mut key_hits, &mut motion_frames);
|
||||
|
||||
@@ -2270,3 +2270,41 @@ fn mask_marquee_selects_vertices() {
|
||||
let sel = &h.state().session.state.selected_vertices;
|
||||
assert_eq!(sel.len(), 2, "shift-marquee adds: {sel:?}");
|
||||
}
|
||||
|
||||
/// A null's eye in the Timeline hides it in the Composition viewer, as in After Effects: its box
|
||||
/// and handles go even while it is selected and a click on its box doesn't pick it; the eye back
|
||||
/// on brings them back.
|
||||
#[test]
|
||||
fn null_eye_hides_it_in_the_viewer() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Nulls", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
let n = s.execute("layer.newNull", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.select", json!({"layers": [n]})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(move |_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
let handle = format!("viewer.handle.{n}.0");
|
||||
let eye = format!("timeline.layer.{n}.video");
|
||||
assert!(h.state().auto.find(&handle).is_some(), "a shown null has handles");
|
||||
// The eye is there for a null and turns its video off.
|
||||
let at = rect(&h, &eye).center();
|
||||
click(&mut h, at);
|
||||
assert!(!h.state().session.active_comp().unwrap().layer(LayerId(n)).unwrap().switches.video);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().auto.find(&handle).is_none(), "a hidden null has no handles");
|
||||
// A click on its box doesn't pick it.
|
||||
h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap();
|
||||
h.run_steps(40); // A new click, not a double-click.
|
||||
let centre = screen(&h, [320.0, 180.0]);
|
||||
click(&mut h, centre);
|
||||
assert!(h.state().session.state.selected_layers.is_empty());
|
||||
// The eye back on: selectable again, with its handles.
|
||||
h.run_steps(40);
|
||||
let at = rect(&h, &eye).center();
|
||||
click(&mut h, at);
|
||||
assert!(h.state().session.active_comp().unwrap().layer(LayerId(n)).unwrap().switches.video);
|
||||
h.run_steps(40);
|
||||
click(&mut h, centre);
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![LayerId(n)]);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().auto.find(&handle).is_some(), "handles back with the eye on");
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ feel and platform differences, and the 26.x interface changes are not in yet.
|
||||
|---|---|---:|---|
|
||||
| Menu bar | After Effects' menus, order, submenus and labels; native on macOS; keyboard-operable in-window bar | 95% | 587 entries in `crates/engine/src/menus.rs`; Adobe-service entries omitted by design; `ui_menus` tests (#279) |
|
||||
| Panels and workspaces | Every After Effects panel; all standard workspaces; drag-to-dock, tabs, floating panels, maximise | 75% | `ui_tabs`, `ui_panels`; panels move into their own native windows on any screen (#486 open: no dragging tabs between those windows); #302, #301 and #320 (docking, stacked-panel headers, workspace persistence) are closed |
|
||||
| Timeline | Switches, modes, parent & link, twirl-downs, reveal shortcuts (U, UU, P, S, R, T, A, E, M, F, Alt+Shift…), pick whips, graph editor, work area, markers | 75% | `ui_keyframes`, `ui_timeline_*`; expand precomps in place (#601); #456, #457, #569 and #355 (Graph Editor jitter and buttons, negative rotation, bulk keyframing) are closed; Proportional Scrubbing (26.2: Ctrl+Alt / Cmd+Opt scrub of hot text spreads the change over the selected layers by selection order, #770) matched for layers selected as a whole, not yet for individually selected properties |
|
||||
| Timeline | Switches, modes, parent & link, twirl-downs, reveal shortcuts (U, UU, P, S, R, T, A, E, M, F, Alt+Shift…), pick whips, graph editor, work area, markers | 75% | `ui_keyframes`, `ui_timeline_*`; expand precomps in place (#601); #456, #457, #569 and #355 (Graph Editor jitter and buttons, negative rotation, bulk keyframing) are closed; Proportional Scrubbing (26.2: Ctrl+Alt / Cmd+Opt scrub of hot text spreads the change over the selected layers by selection order, #770) matched for layers selected as a whole, not yet for individually selected properties; every layer has the Video switch, nulls, cameras and lights included (a null with its eye off has no box or handles in the viewer and isn't picked there) |
|
||||
| Composition viewer | Selection, handles with direction-aware cursors, rulers, guides, grids, snapping, channels, exposure, region of interest, snapshots, 3D views, Spacebar hand | 70% | `ui_viewer`, `ui_viewers`; Cmd/Ctrl +/− doesn't zoom (#570); anchor point grab on Linux (#492); #352 (snapping) is closed; no 3D transform gizmos (#392, #347); percentage guides (26.5) exist as View ▸ Add Percentage Guide and Convert Guides to Percentage / Pixels (a guide stores a percent of the comp, so it follows comp-size changes; behaviour inferred from the release note, not compared with After Effects); Copy Frame to Clipboard (26.3) is in the Composition menu and the viewer's context menu |
|
||||
| Tools panel | All After Effects tools; long-press flyouts; Tool Creates Shape / Mask; Fill / Stroke options | 80% | #213, #227 fixed; #553 (pen and vertex tools on motion paths) is closed |
|
||||
| Nudging and modifiers | Arrow nudge (1 px, Shift 10 px), Alt/Option duplicates and slips, Shift constrains | 75% | #290 nudging fixed; #491 (Linux multi-layer edits) is closed |
|
||||
|
||||
Reference in New Issue
Block a user