mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 11:37:02 -06:00
Merge pull request #1332 from Themolx/null-visibility-switch
Null layers: the Video switch hides them in the Composition viewer, as in After Effects
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -1996,8 +1996,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);
|
||||
|
||||
@@ -2306,3 +2306,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