Viewer: keep hidden AV layers' controls visible (#1769)

This commit is contained in:
Camille Moine
2026-10-20 14:46:05 +02:00
parent de3fec28ed
commit 14453386ac
3 changed files with 75 additions and 4 deletions
+6 -3
View File
@@ -1011,9 +1011,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
painter.add(egui::Shape::closed_line(outline.into_iter().map(|p| map.to_screen(p)).collect(), Stroke::new(1.0, col)));
}
}
// 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) {
// The eye hides a layer's pixels, not its controls: a selected AV layer keeps its box,
// handles, anchor, motion path and mask/shape paths so it stays editable over the visible
// artwork (#1769). Nulls, cameras and lights have no pixels and disappear from the viewer
// when hidden (#1332/#1642). Picking still uses `selectable_layers`, so a hidden layer is
// selected from the Timeline; its controls are interactive once selected.
for l in comp.layers.iter().filter(|l| selected.contains(&l.id) && l.is_active_at(time) && (l.switches.video || l.source.is_av())) {
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);
+68
View File
@@ -759,6 +759,74 @@ fn path_editing_hides_layer_bounds_and_keeps_corner_vertices_editable() {
}
}
/// #1769: the eye hides a layer's pixels, not the controls of a selected AV layer: a hidden shape
/// layer keeps its transform box, anchor and shape path, and its handles still scale it.
#[test]
fn hidden_shape_layer_keeps_its_controls_and_can_still_scale() {
let mut h = harness();
let (layer, path) = {
let r = h
.state_mut()
.session
.execute(
"shape.newPath",
json!({
"vertices": [[200.0, 100.0], [400.0, 100.0], [400.0, 260.0], [200.0, 260.0]],
"closed": true,
"fill": false,
"stroke": "#ffffff",
"strokeWidth": 4
}),
)
.unwrap();
(r["layer"].as_u64().unwrap(), r["path"].as_u64().unwrap())
};
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
h.state_mut().session.state.selected_vertices.clear();
h.state_mut().session.execute("layer.setSwitch", json!({"layers": [layer], "switch": "video", "value": false})).unwrap();
h.run_steps(2);
let shown = h.state().session.active_comp().unwrap().layer(LayerId(layer)).unwrap().clone();
assert!(!shown.switches.video);
assert!(shown.source.is_av());
assert_eq!(h.state().session.state.selected_layers, vec![LayerId(layer)]);
assert!(h.state().auto.find(&format!("viewer.handle.{layer}.0")).is_some(), "a hidden shape keeps its transform box");
assert!(h.state().auto.find(&format!("viewer.anchor.{layer}")).is_some(), "and its anchor");
assert!(h.state().auto.find(&format!("viewer.shapePath.{path}.vertex.0")).is_some(), "and its shape path");
let scale = |h: &Harness<'_, EffectcraftApp>| {
h.state().session.active_comp().unwrap().layer(LayerId(layer)).unwrap().props.prop("transform/scale").unwrap().value.as_vec3()
};
let before = scale(&h);
let from = rect(&h, &format!("viewer.handle.{layer}.5")).center();
drag(&mut h, from, from + vec2(24.0, 0.0));
assert_ne!(scale(&h), before, "a hidden layer's handle still scales it");
}
/// #1769: a hidden masked layer keeps its mask path and vertices, and they remain editable.
#[test]
fn hidden_masked_layer_keeps_its_mask_path_editable() {
let mut h = harness();
let plate = layer_id(&h, "Plate");
let corners = [[100.0, 60.0], [500.0, 60.0], [500.0, 300.0], [100.0, 300.0]];
let mask = {
let s = &mut h.state_mut().session;
s.execute("layer.select", json!({"layers": [plate]})).unwrap();
let m = s.execute("mask.new", json!({"layer": plate, "vertices": corners, "closed": true})).unwrap()["mask"].as_u64().unwrap();
s.state.selected_vertices.clear();
s.execute("layer.setSwitch", json!({"layers": [plate], "switch": "video", "value": false})).unwrap();
m
};
h.run_steps(2);
assert!(h.state().auto.find(&format!("viewer.handle.{plate}.0")).is_some(), "a hidden masked layer keeps its transform box");
let vertex = format!("viewer.mask.{mask}.vertex.0");
assert!(h.state().auto.find(&vertex).is_some(), "and its mask path");
let before = mask_path(&h, "Plate");
let from = rect(&h, &vertex).center();
drag(&mut h, from, from + vec2(15.0, 12.0));
let after = mask_path(&h, "Plate");
assert_ne!(after.vertices[0], before.vertices[0], "the hidden layer's mask vertex can still be moved");
assert_eq!(after.vertices[1..], before.vertices[1..]);
}
#[test]
fn path_tools_keep_bounds_on_layers_without_visible_paths() {
let mut h = harness();
+1 -1
View File
@@ -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; 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) |
| 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 hidden null, camera or light has no box or handles in the viewer and isn't picked there; other selected hidden layers keep their layer controls and paths, #1769) |
| 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 |