mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-05 18:18:49 +00:00
Merge PR #2885
This commit is contained in:
commit
5dc64bb227
3 files changed
+104
-11
No files matched your search
@@ -60,6 +60,62 @@ fn snap_lines_snap_axes_independently_and_priority_breaks_ties() {
|
||||
assert_eq!(snap(&[[0.0, 0.0]], &t3, 5.0).unwrap().hits[0].source, SnapSource::Vertex(LayerId(3)));
|
||||
}
|
||||
|
||||
/// #2855: an edge midpoint lies on its edge line, so one line nearer than the point must not win
|
||||
/// and leave the other axis free. Two lines do win when together they are nearer.
|
||||
#[test]
|
||||
fn snap_point_beats_one_line_and_loses_only_to_two_nearer_lines() {
|
||||
let src = SnapSource::Layer(LayerId(1));
|
||||
let mid = SnapTarget::point([100.0, 50.0], PRI_LAYER, src);
|
||||
let top = SnapTarget::hline(50.0, PRI_LAYER, src);
|
||||
// The line alone is nearer (1 against √10), and the point still snaps both axes.
|
||||
let s = snap(&[[103.0, 51.0]], &[top, mid], 5.0).unwrap();
|
||||
assert_eq!(s.at, [100.0, 50.0]);
|
||||
assert_eq!(s.delta, [-3.0, -1.0]);
|
||||
assert_eq!(s.hits, [mid]);
|
||||
// Lines through the point itself tie with it: the point.
|
||||
let centre = SnapTarget::vline(100.0, PRI_LAYER, src);
|
||||
assert_eq!(snap(&[[103.0, 51.0]], &[top, centre, mid], 5.0).unwrap().hits, [mid]);
|
||||
// Two lines together nearer than the point: both snap.
|
||||
let far = SnapTarget::point([101.0, 55.0], PRI_LAYER, src);
|
||||
let v = SnapTarget::vline(102.0, PRI_GUIDE, SnapSource::Guide);
|
||||
let h = SnapTarget::hline(52.0, PRI_GUIDE, SnapSource::Guide);
|
||||
let s = snap(&[[103.0, 51.0]], &[v, h, far], 5.0).unwrap();
|
||||
assert_eq!(s.at, [102.0, 52.0]);
|
||||
assert_eq!(s.hits, [v, h]);
|
||||
}
|
||||
|
||||
/// #2855: an unrotated layer's centre lines follow its edges' span rule, reach the outer comp
|
||||
/// through a collapsed precomp layer, and a rotated layer keeps its points only.
|
||||
#[test]
|
||||
fn layer_centre_lines_follow_the_edges() {
|
||||
use crate::viewer::{SnapFeatures, SnapOptions, targets};
|
||||
let mut s = comp();
|
||||
// A 100×50 solid moved off the comp's centre lines: x 70–170, y 55–105, centre (120, 80).
|
||||
let a = s.execute("layer.newSolid", json!({"color": "#ff0000", "width": 100, "height": 50})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/position", "value": [120, 80, 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 lines = |t: &[SnapTarget], l: LayerId| {
|
||||
let of = |kind: SnapKind, i: usize, c: f64| t.iter().find(|t| t.kind == kind && t.source == SnapSource::Layer(l) && t.pos[i] == c).map(|t| t.span);
|
||||
(of(SnapKind::VLine, 0, 120.0), of(SnapKind::HLine, 1, 80.0))
|
||||
};
|
||||
assert_eq!(lines(&at(&s, SnapFeatures::default()), LayerId(a)), (Some(None), Some(None)));
|
||||
let off = SnapFeatures { edges_extended: false, ..Default::default() };
|
||||
assert_eq!(lines(&at(&s, off), LayerId(a)), (Some(Some([55.0, 105.0])), Some(Some([70.0, 170.0]))));
|
||||
// Through a collapsed precomp layer (it fills the comp, unmoved, so the numbers stay).
|
||||
s.execute("layer.precompose", json!({"layers": [a], "name": "Inner"})).unwrap();
|
||||
let pre = s.active_comp().unwrap().layers[0].id;
|
||||
s.execute("layer.setSwitch", json!({"layers": [pre.0], "switch": "collapse", "value": true})).unwrap();
|
||||
assert_eq!(lines(&at(&s, off), pre), (Some(Some([55.0, 105.0])), Some(Some([70.0, 170.0]))));
|
||||
// Rotated: no lines at all.
|
||||
s.execute("prop.set", json!({"layer": pre.0, "path": "transform/rotation", "value": 30})).unwrap();
|
||||
assert_eq!(lines(&at(&s, off), pre), (None, None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snap_targets_cover_layers_comp_guides_and_grid() {
|
||||
let mut s = comp();
|
||||
|
||||
+12
-11
@@ -48,7 +48,7 @@ pub struct SnapTarget {
|
||||
pub priority: u8,
|
||||
pub source: SnapSource,
|
||||
/// A line's extent along itself (y of a vertical line, x of a horizontal one): a layer's
|
||||
/// edge without Snap Edges Extended. `None` runs across the whole comp.
|
||||
/// edge or centre line without Snap Edges Extended. `None` runs across the whole comp.
|
||||
#[serde(default)]
|
||||
pub span: Option<[f64; 2]>,
|
||||
}
|
||||
@@ -70,7 +70,7 @@ impl SnapTarget {
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// (layer edge and centre lines 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
|
||||
@@ -126,9 +126,10 @@ fn better(a: (f64, u8), b: (f64, u8)) -> bool {
|
||||
}
|
||||
|
||||
/// Snap feature points `sources` (comp pixels) to the nearest target within `tol` comp pixels.
|
||||
/// A point target snaps both axes and wins when it is at least as near as the best lines;
|
||||
/// otherwise the nearest vertical and horizontal lines snap x and y independently. Ties go to
|
||||
/// the lower priority value.
|
||||
/// A point target snaps both axes and wins unless a vertical and a horizontal line both resolve
|
||||
/// and together lie nearer than it (one line alone leaves the other axis free, so it never beats
|
||||
/// a point: a layer's edge midpoint lies on its edge line); otherwise the nearest vertical and
|
||||
/// horizontal lines snap x and y independently. Ties go to the lower priority value.
|
||||
pub fn snap(sources: &[[f64; 2]], targets: &[SnapTarget], tol: f64) -> Option<Snap> {
|
||||
let mut best_pt: Option<((f64, u8), [f64; 2], SnapTarget)> = None;
|
||||
let mut best_x: Option<((f64, u8), [f64; 2], SnapTarget)> = None;
|
||||
@@ -150,9 +151,9 @@ pub fn snap(sources: &[[f64; 2]], targets: &[SnapTarget], tol: f64) -> Option<Sn
|
||||
}
|
||||
}
|
||||
}
|
||||
let line_best = [best_x.map(|b| b.0.0), best_y.map(|b| b.0.0)].into_iter().flatten().fold(f64::INFINITY, f64::min);
|
||||
let both_lines = best_x.zip(best_y).map_or(f64::INFINITY, |(x, y)| x.0.0.hypot(y.0.0));
|
||||
if let Some((k, s, t)) = best_pt
|
||||
&& k.0 <= line_best + 1e-9
|
||||
&& k.0 <= both_lines + 1e-9
|
||||
{
|
||||
return Some(Snap { delta: [t.pos[0] - s[0], t.pos[1] - s[1]], hits: vec![t], at: t.pos });
|
||||
}
|
||||
@@ -366,8 +367,8 @@ fn visible_layers<'a>(ctx: &EvalCtx<'a>, exclude: &'a [LayerId]) -> impl Iterato
|
||||
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
|
||||
/// A 2D layer's snap targets: its box points, its edges and centre 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.
|
||||
@@ -386,10 +387,10 @@ fn layer_targets_at(ctx: &EvalCtx, l: &Layer, anchor: bool, f: SnapFeatures, dep
|
||||
let [nw, ne, _, sw] = b.corners;
|
||||
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]] {
|
||||
for x in [nw[0], b.center[0], ne[0]] {
|
||||
out.push(SnapTarget::vline(x, PRI_LAYER, src).within(ys));
|
||||
}
|
||||
for y in [nw[1], sw[1]] {
|
||||
for y in [nw[1], b.center[1], sw[1]] {
|
||||
out.push(SnapTarget::hline(y, PRI_LAYER, src).within(xs));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,6 +420,42 @@ fn layer_drag_snaps_to_other_layers_edges_ctrl_toggles_and_options_apply() {
|
||||
assert!(!snapped(p), "edges not extended: {p:?}");
|
||||
}
|
||||
|
||||
/// #2855: a dragged layer's edge midpoints snap to another layer's on both axes, side by side and
|
||||
/// stacked, with Snap Edges Extended on and off.
|
||||
#[test]
|
||||
fn layer_drag_snaps_to_other_layers_midpoints() {
|
||||
let mut h = harness();
|
||||
let box_id = h.state().session.active_comp().unwrap().layers[0].id;
|
||||
// A 100×100 target at x 450–550, y 50–150, centre (500, 100).
|
||||
let target = h.state_mut().session.execute("layer.newSolid", json!({"name": "Target", "color": "#20e040", "width": 100, "height": 100})).unwrap()["layer"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
// Grab the 80×80 box (at x 60–140, y 210–290) by the feature at `grab` and take that feature
|
||||
// to `to`: returns where the box's centre lands.
|
||||
let drop = |h: &mut Harness<'_, EffectcraftApp>, grab: [f32; 2], to: [f32; 2]| {
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("prop.set", json!({"layer": target, "path": "transform/position", "value": [500, 100, 0]})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": box_id.0, "path": "transform/position", "value": [100, 250, 0]})).unwrap();
|
||||
s.execute("edit.deselectAll", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
let (from, to) = (screen(h, grab), screen(h, to));
|
||||
hold_drag(h, from, to, Default::default());
|
||||
let l = h.state().session.active_comp().unwrap().layer(box_id).unwrap().clone();
|
||||
let p = l.props.prop("transform/position").unwrap().value.as_vec3();
|
||||
[p[0], p[1]]
|
||||
};
|
||||
let near = |p: [f64; 2], x: f64, y: f64| (p[0] - x).abs() < 0.01 && (p[1] - y).abs() < 0.01;
|
||||
for extended in [true, false] {
|
||||
h.state_mut().session.execute("view.snappingOptions", json!({"edgesExtended": extended})).unwrap();
|
||||
// Side by side: the box's left midpoint onto the target's right midpoint (550, 100).
|
||||
let p = drop(&mut h, [60.5, 250.0], [553.0, 102.0]);
|
||||
assert!(near(p, 590.0, 100.0), "left to right midpoint, extended {extended}: {p:?}");
|
||||
// Stacked: the box's top midpoint onto the target's bottom midpoint (500, 150).
|
||||
let p = drop(&mut h, [100.0, 210.5], [502.0, 153.0]);
|
||||
assert!(near(p, 500.0, 190.0), "top to bottom midpoint, extended {extended}: {p:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// With a shape layer selected the Rectangle tool draws a new group into its Contents (After
|
||||
/// Effects' behaviour); with nothing selected it draws a new shape layer (#227).
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user