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 <noreply@anthropic.com>
This commit is contained in:
parent
b9d1f79f7d
commit
6ce58040fd
|
|
@ -467,7 +467,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
"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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Sn
|
|||
Some(Snap { delta, hits, at })
|
||||
}
|
||||
|
||||
/// Snap features of a layer in comp space that `f` turns on: 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, f: SnapFeatures) -> 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<SnapTarget> {
|
||||
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<Sna
|
|||
out.extend(x.iter().map(|x| SnapTarget::vline(*x, PRI_COMP, SnapSource::Comp).within(ys)));
|
||||
out.extend(y.iter().map(|y| SnapTarget::hline(*y, PRI_COMP, SnapSource::Comp).within(xs)));
|
||||
};
|
||||
if f.edges {
|
||||
lines(&[0.0, w], &[0.0, h]);
|
||||
}
|
||||
if f.centers {
|
||||
lines(&[w / 2.0], &[h / 2.0]);
|
||||
out.push(SnapTarget::point([w / 2.0, h / 2.0], PRI_COMP, SnapSource::Comp));
|
||||
}
|
||||
// Visible layers: eye on, and soloed while any layer is.
|
||||
let any_solo = comp.layers.iter().any(|l| l.switches.solo && l.source.is_av() && l.is_active_at(ctx.time));
|
||||
let visible = |l: &&Layer| l.is_active_at(ctx.time) && l.switches.video && (!any_solo || l.switches.solo) && !exclude.contains(&l.id);
|
||||
for l in comp.layers.iter().filter(visible) {
|
||||
lines(&[0.0, w], &[0.0, h]);
|
||||
lines(&[w / 2.0], &[h / 2.0]);
|
||||
out.push(SnapTarget::point([w / 2.0, h / 2.0], PRI_COMP, SnapSource::Comp));
|
||||
for l in visible_layers(ctx, exclude) {
|
||||
out.extend(layer_targets(ctx, l, true, f));
|
||||
}
|
||||
}
|
||||
|
|
@ -354,11 +321,27 @@ pub fn targets(ctx: &EvalCtx, exclude: &[LayerId], opts: SnapOptions) -> Vec<Sna
|
|||
out
|
||||
}
|
||||
|
||||
/// A 2D layer's snap targets as `f` allows: 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) and its mask and shape path
|
||||
/// vertices. None for cameras, lights and 3D layers.
|
||||
/// How deep Snap to Features in Collapsed Compositions looks into nested collapsed precomps.
|
||||
const MAX_COLLAPSED_DEPTH: usize = 8;
|
||||
|
||||
/// Visible layers of a comp at the context time: active, eye on, and soloed while any layer is.
|
||||
fn visible_layers<'a>(ctx: &EvalCtx<'a>, exclude: &'a [LayerId]) -> impl Iterator<Item = &'a Layer> {
|
||||
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<SnapTarget> {
|
||||
layer_targets_at(ctx, l, anchor, f, 0)
|
||||
}
|
||||
|
||||
fn layer_targets_at(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures, depth: usize) -> Vec<SnapTarget> {
|
||||
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<SnapTarget> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String> = 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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -110,8 +110,9 @@ revolutions are `timeline.prop.<uid>.revolutions`, `effectControls.prop.<uid>.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),
|
||||
|
|
|
|||
Loading…
Reference in New Issue