From 6ce58040fd3775eeaed0b46a2ae72d7e235418e2 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 18:56:19 +0700 Subject: [PATCH] Viewer: the Snapping menu has After Effects' two options The Tools bar's Snapping options menu (#265) offered per-feature toggles (Edges, Corners, Centers, Anchor Points, Mask and Shape Path Points) that After Effects' Snapping menu doesn't have. It now has the two After Effects has: - Snap Edges Extended (as before). - Snap to Features in Collapsed Compositions and Text Layers: the visible layers inside a precomp layer with Collapse Transformations on are snap targets too, mapped into the outer comp (points always, edge lines while the precomp layer is unrotated); a snap highlights the precomp layer. Nested collapsed precomps are followed eight deep. Every layer feature snaps again (corners, edge midpoints, centre, anchor point, mask and shape path points); the snapping engine itself is unchanged. `view.snappingOptions` takes `edgesExtended` and `collapsedFeatures` (and `toggle` of either); the old keys are refused. Test: collapsed_precomp_features_snap (fails before: no such option), snapping_options_command_sets_and_toggles (the per-feature keys are refused), snapping_options_pick_the_targets (AE behaviour kept: edges extended or not, hidden layers, guides only), and the Snapping menu UI test (only the two options are listed). Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/viewer_cmds.rs | 2 +- crates/engine/src/tests_viewer.rs | 72 +++++++--- crates/engine/src/viewer.rs | 166 +++++++++++++--------- crates/ui-egui/src/header.rs | 18 +-- crates/ui-egui/src/panels/viewer.rs | 3 +- crates/ui-egui/tests/ui_viewer.rs | 5 + docs/control-protocol.md | 5 +- 7 files changed, 162 insertions(+), 109 deletions(-) diff --git a/crates/engine/src/commands/viewer_cmds.rs b/crates/engine/src/commands/viewer_cmds.rs index 1aea552d..263f7191 100644 --- a/crates/engine/src/commands/viewer_cmds.rs +++ b/crates/engine/src/commands/viewer_cmds.rs @@ -467,7 +467,7 @@ pub fn specs() -> Vec { "Snapping Options", [], None, - "{edgesExtended?, edges?, corners?, centers?, anchorPoints?, paths?: bool, toggle?: one of those keys} → the options", + "{edgesExtended? (Snap Edges Extended), collapsedFeatures? (Snap to Features in Collapsed Compositions and Text Layers): bool, toggle?: one of those keys} → the options", always, snapping_options ), diff --git a/crates/engine/src/tests_viewer.rs b/crates/engine/src/tests_viewer.rs index bf972d12..a6139668 100644 --- a/crates/engine/src/tests_viewer.rs +++ b/crates/engine/src/tests_viewer.rs @@ -84,8 +84,8 @@ fn snap_targets_cover_layers_comp_guides_and_grid() { } /// Tools bar ▸ Snapping options (#253): Snap Edges Extended off keeps a layer's edges to its -/// extent, each feature family can be turned off, hidden layers are no targets, and with -/// Snapping off only guides and the grid are left. +/// extent, every feature of a layer snaps, hidden layers are no targets, and with Snapping off +/// only guides and the grid are left. #[test] fn snapping_options_pick_the_targets() { use crate::viewer::{SnapFeatures, SnapOptions, layer_features, targets}; @@ -108,35 +108,63 @@ fn snapping_options_pick_the_targets() { let own = targets(&ctx, &[], opts(SnapFeatures { edges_extended: false, ..Default::default() })); assert!(snap(&[[268.0, 340.0]], &own, 4.0).is_none_or(|h| h.hits.iter().all(|t| t.source != SnapSource::Layer(LayerId(a))))); assert_eq!(snap(&[[268.0, 190.0]], &own, 4.0).unwrap().at, [270.0, 190.0]); - // Centres (and anchor points, a's sits in its centre) off: nothing at the comp centre, where - // a's centre is too; edges off: no edge lines or midpoints, only the comp's centre lines. - let no_centres = targets(&ctx, &[], opts(SnapFeatures { centers: false, anchor_points: false, ..Default::default() })); - assert!(!no_centres.iter().any(|t| t.pos == [320.0, 180.0])); - assert!(no_centres.iter().any(|t| t.pos == [270.0, 155.0]), "corners stay"); - let no_edges = targets(&ctx, &[], opts(SnapFeatures { edges: false, ..Default::default() })); - assert!(!no_edges.iter().any(|t| t.pos == [320.0, 155.0])); - assert!(no_edges.iter().filter(|t| t.kind != SnapKind::Point).all(|t| t.source == SnapSource::Comp && (t.pos[0] == 320.0 || t.pos[1] == 180.0))); - // The dragged layer's features follow the options too: corners only. + // Corners, edge midpoints and the centre of a are targets. + for p in [[270.0, 155.0], [320.0, 155.0], [320.0, 180.0]] { + assert!(all.iter().any(|t| t.source == SnapSource::Layer(LayerId(a)) && t.pos == p), "{p:?}"); + } + // The dragged layer's features: corners, edge midpoints, centre and anchor point. let l = comp.layer(LayerId(a)).unwrap(); - let corners = SnapFeatures { edges: false, centers: false, anchor_points: false, paths: false, ..Default::default() }; - assert_eq!(layer_features(&ctx, l, corners), vec![[270.0, 155.0], [370.0, 155.0], [370.0, 205.0], [270.0, 205.0]]); + let f = layer_features(&ctx, l); + assert_eq!(f[..4], [[270.0, 155.0], [370.0, 155.0], [370.0, 205.0], [270.0, 205.0]]); + assert_eq!(f.len(), 10, "{f:?}"); // Snapping off: guides only. let guides = targets(&ctx, &[], SnapOptions { layers: false, guides: true, ..opts(SnapFeatures::default()) }); assert!(!guides.is_empty() && guides.iter().all(|t| t.source == SnapSource::Guide)); } +/// Snap to Features in Collapsed Compositions and Text Layers: the layers inside a collapsed +/// precomp layer are targets (highlighting the precomp layer), only while the option is on and +/// the precomp layer collapses transformations. +#[test] +fn collapsed_precomp_features_snap() { + use crate::viewer::{SnapFeatures, SnapOptions, targets}; + let mut s = comp(); + // A 40×20 solid at (100, 50) in a 640×360 precomp. + let inner = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 40, "height": 20})).unwrap()["layer"].as_u64().unwrap(); + s.execute("prop.set", json!({"layer": inner, "path": "transform/position", "value": [100, 50, 0]})).unwrap(); + s.execute("layer.precompose", json!({"layers": [inner], "name": "Inner"})).unwrap(); + let pre = s.active_comp().unwrap().layers[0].id; + // Moved by (10, 20) in the outer comp: the inner solid's corner (80, 40) lands at (90, 60). + s.execute("prop.set", json!({"layer": pre.0, "path": "transform/position", "value": [330, 200, 0]})).unwrap(); + let cid = s.active_comp_id().unwrap(); + let at = |s: &Session, f: SnapFeatures| { + let comp = s.project.comp(cid).unwrap().clone(); + let ctx = effectcraft_render::EvalCtx::new(&s.project, cid, &comp, Tick::ZERO); + targets(&ctx, &[], SnapOptions { layers: true, features: f, guides: false, grid: false, grid_spacing: 0.0 }) + }; + let corner = |t: &[crate::viewer::SnapTarget]| t.iter().any(|t| t.kind == SnapKind::Point && t.pos == [90.0, 60.0] && t.source == SnapSource::Layer(pre)); + assert!(!corner(&at(&s, SnapFeatures::default())), "not collapsed"); + s.execute("layer.setSwitch", json!({"layers": [pre.0], "switch": "collapse", "value": true})).unwrap(); + let on = at(&s, SnapFeatures::default()); + assert!(corner(&on), "collapsed"); + assert_eq!(snap(&[[93.0, 63.0]], &on, 5.0).unwrap().at, [90.0, 60.0]); + assert!(!corner(&at(&s, SnapFeatures { collapsed_features: false, ..Default::default() })), "option off"); +} + #[test] fn snapping_options_command_sets_and_toggles() { let mut s = comp(); - assert!(s.state.snap_features.edges_extended && s.state.snap_features.anchor_points); - let r = s.execute("view.snappingOptions", json!({"edgesExtended": false, "anchorPoints": false})).unwrap(); - assert_eq!(r["edgesExtended"], false); - assert!(!s.state.snap_features.edges_extended && !s.state.snap_features.anchor_points && s.state.snap_features.corners); - s.execute("view.snappingOptions", json!({"toggle": "paths"})).unwrap(); - assert!(!s.state.snap_features.paths); - s.execute("view.snappingOptions", json!({"toggle": "paths"})).unwrap(); - assert!(s.state.snap_features.paths); - assert!(s.execute("view.snappingOptions", json!({"toggle": "nope"})).is_err()); + assert_eq!(s.state.snap_features, crate::viewer::SnapFeatures { edges_extended: true, collapsed_features: true }); + let r = s.execute("view.snappingOptions", json!({"edgesExtended": false})).unwrap(); + assert_eq!(r, json!({"edgesExtended": false, "collapsedFeatures": true})); + s.execute("view.snappingOptions", json!({"toggle": "collapsedFeatures"})).unwrap(); + assert!(!s.state.snap_features.collapsed_features); + s.execute("view.snappingOptions", json!({"toggle": "collapsedFeatures"})).unwrap(); + assert!(s.state.snap_features.collapsed_features); + // The per-feature toggles are gone, as in After Effects' menu. + for k in ["nope", "corners", "anchorPoints", "paths"] { + assert!(s.execute("view.snappingOptions", json!({"toggle": k})).is_err(), "{k}"); + } } // ---------------------------------------------------------------- channels, exposure, snapshot diff --git a/crates/engine/src/viewer.rs b/crates/engine/src/viewer.rs index bf39e315..b9758668 100644 --- a/crates/engine/src/viewer.rs +++ b/crates/engine/src/viewer.rs @@ -67,48 +67,34 @@ impl SnapTarget { } } -/// Tools bar ▸ Snapping options: which layer features snap (the dragged layer's and the -/// targets), and Snap Edges Extended (layer edges snap along their whole line, beyond the -/// layer's bounds). Everything is on by default. +/// Tools bar ▸ Snapping options, the two After Effects' Snapping menu has: Snap Edges Extended +/// (layer edges snap along their whole line, beyond the layer's bounds) and Snap to Features in +/// Collapsed Compositions and Text Layers (the layers inside a precomp layer with Collapse +/// Transformations on are targets too). Both are on by default. Every layer feature snaps +/// (corners, edge midpoints, centre, anchor point, mask and shape path points), as in After +/// Effects. #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct SnapFeatures { pub edges_extended: bool, - /// Edges and edge midpoints of layers, and the comp's edges. - pub edges: bool, - pub corners: bool, - /// Layer centres and the comp's centre. - pub centers: bool, - pub anchor_points: bool, - /// Mask and shape path points. - pub paths: bool, + pub collapsed_features: bool, } impl Default for SnapFeatures { fn default() -> Self { - SnapFeatures { edges_extended: true, edges: true, corners: true, centers: true, anchor_points: true, paths: true } + SnapFeatures { edges_extended: true, collapsed_features: true } } } impl SnapFeatures { /// The options in the Tools bar's Snapping menu order: (parameter key, label). - pub const OPTIONS: [(&'static str, &'static str); 6] = [ - ("edgesExtended", "Snap Edges Extended"), - ("edges", "Edges"), - ("corners", "Corners"), - ("centers", "Centers"), - ("anchorPoints", "Anchor Points"), - ("paths", "Mask and Shape Path Points"), - ]; + pub const OPTIONS: [(&'static str, &'static str); 2] = + [("edgesExtended", "Snap Edges Extended"), ("collapsedFeatures", "Snap to Features in Collapsed Compositions and Text Layers")]; /// The option called `key` (an [`OPTIONS`](Self::OPTIONS) key). pub fn option_mut(&mut self, key: &str) -> Option<&mut bool> { Some(match key { "edgesExtended" => &mut self.edges_extended, - "edges" => &mut self.edges, - "corners" => &mut self.corners, - "centers" => &mut self.centers, - "anchorPoints" => &mut self.anchor_points, - "paths" => &mut self.paths, + "collapsedFeatures" => &mut self.collapsed_features, _ => return None, }) } @@ -187,15 +173,13 @@ pub fn snap(sources: &[[f64; 2]], targets: &[SnapTarget], tol: f64) -> Option Vec<[f64; 2]> { +/// Snap features of a layer in comp space: corners, edge midpoints, centre, anchor point and +/// mask / shape path vertices (no box for layers without bounds). +pub fn layer_features(ctx: &EvalCtx, layer: &Layer) -> Vec<[f64; 2]> { let (m, _) = ctx.layer_to_comp(layer); - let mut out = layer_box(ctx, layer, &m).map(|b| b.features(f)).unwrap_or_default(); - out.extend(anchor_feature(ctx, layer, &m).filter(|_| f.anchor_points)); - if f.paths { - out.extend(layer_vertices(ctx, layer)); - } + let mut out = layer_box(ctx, layer, &m).map(|b| b.points()).unwrap_or_default(); + out.extend(anchor_feature(ctx, layer, &m)); + out.extend(layer_vertices(ctx, layer)); out } @@ -208,19 +192,9 @@ struct BoxPoints { } impl BoxPoints { - /// The points `f` turns on. - fn features(&self, f: SnapFeatures) -> Vec<[f64; 2]> { - let mut out = vec![]; - if f.corners { - out.extend(self.corners); - } - if f.edges { - out.extend(self.mids); - } - if f.centers { - out.push(self.center); - } - out + /// Corners, edge midpoints and centre. + fn points(&self) -> Vec<[f64; 2]> { + self.corners.iter().chain(&self.mids).chain([&self.center]).copied().collect() } } @@ -301,8 +275,8 @@ pub struct SnapOptions { } /// Snap targets of a comp at the context time: other visible layers' features and vertices and -/// the comp's edges and centre (with `layers`, as `features` allows), and guides and grid lines -/// (when on). Layers in `exclude` (the dragged ones) are skipped. +/// the comp's edges and centre (with `layers`, as the Snapping options allow), and guides and +/// grid lines (when on). Layers in `exclude` (the dragged ones) are skipped. pub fn targets(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec { let comp = ctx.comp; let (w, h) = (comp.width as f64, comp.height as f64); @@ -315,17 +289,10 @@ pub fn targets(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec Vec(ctx: &EvalCtx<'a>, exclude: &'a [LayerId]) -> impl Iterator { + let comp = ctx.comp; + let time = ctx.time; + let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(time)); + comp.layers.iter().filter(move |l| l.is_active_at(time) && l.switches.video && (!any_solo || l.switches.solo) && !exclude.contains(&l.id)) +} + +/// A 2D layer's snap targets: its box points, its edges as lines while unrotated (across the +/// comp with Snap Edges Extended, along the layer otherwise), its anchor point unless `anchor` is +/// false (Pan Behind drags it, the box stays put), its mask and shape path vertices and, with +/// Snap to Features in Collapsed Compositions, the targets of the visible layers inside a +/// collapsed precomp layer. None for cameras, lights and 3D layers. pub fn layer_targets(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures) -> Vec { + layer_targets_at(ctx, l, anchor, f, 0) +} + +fn layer_targets_at(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures, depth: usize) -> Vec { if l.is_camera() || l.is_light() || l.is_3d() { return vec![]; } @@ -367,7 +350,7 @@ pub fn layer_targets(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures) -> let mut out = vec![]; if let Some(b) = layer_box(ctx, l, &m) { let [nw, ne, _, sw] = b.corners; - if f.edges && (nw[1] - ne[1]).abs() < 1e-6 && (nw[0] - sw[0]).abs() < 1e-6 { + if (nw[1] - ne[1]).abs() < 1e-6 && (nw[0] - sw[0]).abs() < 1e-6 { let (xs, ys) = if f.edges_extended { (None, None) } else { (Some([nw[0], ne[0]]), Some([nw[1], sw[1]])) }; for x in [nw[0], ne[0]] { out.push(SnapTarget::vline(x, PRI_LAYER, src).within(ys)); @@ -376,11 +359,58 @@ pub fn layer_targets(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures) -> out.push(SnapTarget::hline(y, PRI_LAYER, src).within(xs)); } } - out.extend(b.features(f).into_iter().map(|p| SnapTarget::point(p, PRI_LAYER, src))); + out.extend(b.points().into_iter().map(|p| SnapTarget::point(p, PRI_LAYER, src))); } - out.extend(anchor_feature(ctx, l, &m).filter(|_| anchor && f.anchor_points).map(|p| SnapTarget::point(p, PRI_LAYER, src))); - if f.paths { - out.extend(layer_vertices(ctx, l).into_iter().map(|p| SnapTarget::point(p, PRI_VERTEX, SnapSource::Vertex(l.id)))); + out.extend(anchor_feature(ctx, l, &m).filter(|_| anchor).map(|p| SnapTarget::point(p, PRI_LAYER, src))); + out.extend(layer_vertices(ctx, l).into_iter().map(|p| SnapTarget::point(p, PRI_VERTEX, SnapSource::Vertex(l.id)))); + if f.collapsed_features + && l.switches.collapse + && depth < MAX_COLLAPSED_DEPTH + && let effectcraft_project::LayerSource::Comp { item } = l.source + && let Some(nc) = ctx.project.comp(item) + { + out.extend(collapsed_targets(ctx, l, item, nc, &m, f, depth)); + } + out +} + +/// The targets of the layers inside collapsed precomp layer `l` (showing comp `nc`), in the +/// outer comp (`m`: `l`'s layer → comp). They highlight `l` when they snap. Lines stay lines +/// only while `l` is unrotated; otherwise only the points snap. +fn collapsed_targets( + ctx: &EvalCtx, + l: &Layer, + item: effectcraft_project::ItemId, + nc: &effectcraft_project::Comp, + m: &Mat3, + f: SnapFeatures, + depth: usize, +) -> Vec { + let nctx = EvalCtx { comp_id: item, comp: nc, time: ctx.nested_time(l), ..*ctx }; + let axis_aligned = m.0[0][1].abs() < 1e-9 && m.0[1][0].abs() < 1e-9; + let (sx, ox, sy, oy) = (m.0[0][0], m.0[0][2], m.0[1][1], m.0[1][2]); + let mut out = vec![]; + for nl in visible_layers(&nctx, &[]) { + for t in layer_targets_at(&nctx, nl, true, f, depth + 1) { + let source = match t.source { + SnapSource::Vertex(_) => SnapSource::Vertex(l.id), + _ => SnapSource::Layer(l.id), + }; + let mapped = match t.kind { + SnapKind::Point => { + let q = m.apply(vec2(t.pos[0], t.pos[1])); + Some(SnapTarget::point([q.x, q.y], t.priority, source)) + } + SnapKind::VLine if axis_aligned => { + Some(SnapTarget::vline(sx * t.pos[0] + ox, t.priority, source).within(t.span.map(|[a, b]| [sy * a + oy, sy * b + oy]))) + } + SnapKind::HLine if axis_aligned => { + Some(SnapTarget::hline(sy * t.pos[1] + oy, t.priority, source).within(t.span.map(|[a, b]| [sx * a + ox, sx * b + ox]))) + } + _ => None, + }; + out.extend(mapped); + } } out } diff --git a/crates/ui-egui/src/header.rs b/crates/ui-egui/src/header.rs index bf64d987..ca9c3a17 100644 --- a/crates/ui-egui/src/header.rs +++ b/crates/ui-egui/src/header.rs @@ -251,8 +251,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } -/// The Snapping options menu beside the checkbox: Snap Edges Extended and which layer features -/// snap (`view.snappingOptions`). +/// The Snapping options menu beside the checkbox: After Effects' Snap Edges Extended and Snap to +/// Features in Collapsed Compositions and Text Layers (`view.snappingOptions`). fn snapping_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect) { let t = app.tokens; let pop = egui::Id::new("snap-options"); @@ -263,19 +263,9 @@ fn snapping_options(app: &mut EffectcraftApp, ui: &mut egui::Ui, r: Rect) { if !widgets::popup_is_open(ui, pop) { return; } - // Snap Edges Extended, then the features ("" keys the separator). let f = app.session.state.snap_features; - let mut keys = vec![]; - let mut items = vec![]; - for (i, (k, label)) in vw::SnapFeatures::OPTIONS.into_iter().enumerate() { - if i == 1 { - keys.push(""); - items.push("-".to_string()); - } - keys.push(k); - items.push(widgets::check_label(f.option(k), label)); - } - if let Some(k) = widgets::popup_menu(ui, pop, r.left_bottom(), &items, None).and_then(|i| keys.get(i)) + let items: Vec = vw::SnapFeatures::OPTIONS.iter().map(|(k, label)| widgets::check_label(f.option(k), label)).collect(); + if let Some((k, _)) = widgets::popup_menu(ui, pop, r.left_bottom(), &items, None).and_then(|i| vw::SnapFeatures::OPTIONS.get(i)) && let Err(e) = app.session.execute("view.snappingOptions", json!({"toggle": k})) { app.ui.status = e.to_string(); diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index 4d92340e..c2fb3ef2 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -1248,8 +1248,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { }) .collect(); // Snap the grabbed layer's feature nearest the pointer (AE's snap handle). - let features = app.session.state.snap_features; - let snap_src = comp.layer(l).map(|layer| effectcraft_engine::viewer::layer_features(&ectx, layer, features)).and_then(|f| { + let snap_src = comp.layer(l).map(|layer| effectcraft_engine::viewer::layer_features(&ectx, layer)).and_then(|f| { let c = map.to_comp(press); f.into_iter().min_by(|a, b| { let da = (a[0] - c[0]).powi(2) + (a[1] - c[1]).powi(2); diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index fca695ce..34b1ed3a 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -295,6 +295,11 @@ fn layer_drag_snaps_to_other_layers_edges_ctrl_toggles_and_options_apply() { // Snapping options ▸ Snap Edges Extended off: below the target its edge no longer snaps. let menu = rect(&h, "header.snappingOptions").center(); click(&mut h, menu); + // After Effects' two options, without per-feature toggles. + assert!(h.query_by_label("✓ Snap to Features in Collapsed Compositions and Text Layers").is_some()); + for gone in ["✓ Corners", "✓ Anchor Points", "✓ Mask and Shape Path Points"] { + assert!(h.query_by_label(gone).is_none(), "{gone}"); + } let item = h.query_by_label("✓ Snap Edges Extended").expect("the Snapping options menu").rect().center(); click(&mut h, item); assert!(!h.state().session.state.snap_features.edges_extended); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 08932fd9..9d785bd1 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -110,8 +110,9 @@ revolutions are `timeline.prop..revolutions`, `effectControls.prop..re `timeline.graph.snap` and `timeline.graph.reference`. Use `ui.elements` to see what is on screen. Viewer state that agents drive headless too: `view.snapping`, `view.snappingOptions -{edgesExtended?, edges?, corners?, centers?, anchorPoints?, paths?, toggle?}` (the Tools bar's -Snapping options, `header.snappingOptions`), `view.channel {channel, colorized?}`, +{edgesExtended?, collapsedFeatures?, toggle?}` (the Tools bar's Snapping options, +`header.snappingOptions`: After Effects' Snap Edges Extended and Snap to Features in Collapsed +Compositions and Text Layers), `view.channel {channel, colorized?}`, `view.exposure {stops | delta}`, `view.resetExposure`, `view.takeSnapshot`, `view.showSnapshot`, `view.fastPreviewMode {mode}`, `view.setRegionOfInterest {rect}`, `view.addGuide`, `view.moveGuide`, `view.removeGuide`; editing: `shape.newPath` (Pen on shape layers),