mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Timeline: every property has a pick whip in the Parent & Link column (#363)
What: each property row that can take an expression (it has a stopwatch) now has a property pick whip in the Parent & Link column, as in After Effects CC 2018 and later (automation id `timeline.prop.<uid>.propertyPickWhip`). Dragged onto another property's name or one of its values, it runs `prop.pickWhip`: the reference is added as the property's expression, or replaces the one it has, and the expression is enabled. A drop on one value picks that dimension (`position[1]`). The parent, expression and property pick whips share one widget and one drop path (rubber band, autoscroll, dimension pick); only the expression pick whip inserts at the cursor of an expression being edited. Pick whips (expression and property) now also drop on properties in Effect Controls: its property rows register as targets while a pick whip is dragged, with their per-dimension value fields. Why: the last part of #363. Adobe's docs say the Property Link pick whip adds the expression without enabling expressions first, and a pick whip outside an edit replaces the whole expression text. No modifier keys for the property pick whip are documented, so none are implemented. Tests: `the_property_pick_whip_links_a_property_to_another` (ui_inline_edit) drops it on a property's name, on one of Position's values, on a property with a disabled expression (replaced and enabled), on an Effect Controls slider and one value of a point, and on itself (no change). It fails before the change; with Effect Controls' targets removed it fails at the slider. Looked at the result with the headless snapshot. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4b7bd135ec
commit
67b9b20a07
6 files changed
+202
-54
No files matched your search
@@ -355,6 +355,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
),
|
||||
("Project: {}", "项目:{}"),
|
||||
("Projects", "项目"),
|
||||
("Property pick whip: drag onto a property", "属性拾取器:拖到属性上"),
|
||||
("Protected Region", "受保护区域"),
|
||||
("Proxy Use: {}", "代理使用:{}"),
|
||||
("Push All to Comp", "全部推送到合成"),
|
||||
@@ -771,6 +772,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
),
|
||||
("Project: {}", "專案:{}"),
|
||||
("Projects", "專案"),
|
||||
("Property pick whip: drag onto a property", "屬性拾取線:拖到屬性上"),
|
||||
("Protected Region", "受保護區域"),
|
||||
("Proxy Use: {}", "代理使用:{}"),
|
||||
("Push All to Comp", "全部推送到合成"),
|
||||
|
||||
@@ -146,7 +146,8 @@ fn toggle_pick(app: &mut EffectcraftApp, kind: &str, layer: &Layer, prop: &Prope
|
||||
}
|
||||
}
|
||||
|
||||
/// One property row: stopwatch, name, value control (and the keyframe navigator).
|
||||
/// One property row: stopwatch, name, value control (and the keyframe navigator). Returns the
|
||||
/// value fields of a vector, one per dimension, for the pick whip.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prop_row(
|
||||
app: &mut EffectcraftApp,
|
||||
@@ -159,8 +160,9 @@ fn prop_row(
|
||||
r: Rect,
|
||||
indent: f32,
|
||||
actions: &mut Actions,
|
||||
) {
|
||||
) -> Vec<Rect> {
|
||||
let t = app.tokens;
|
||||
let mut dims = vec![];
|
||||
let cy = r.center().y;
|
||||
let uid = prop.uid;
|
||||
let swr = Rect::from_center_size(pos2(r.min.x + indent, cy), vec2(14.0, 14.0));
|
||||
@@ -304,6 +306,7 @@ fn prop_row(
|
||||
for d in 0..n.min(c.len()) {
|
||||
let (vr, nv, _) = widgets::hot_number_at(ui, pos2(x, cy - 9.0), egui::Id::new(("ec-v", uid, d)), c[d], 1.0, (-1e9, 1e9), 1, "", &t);
|
||||
app.auto.add(&format!("effectControls.prop.{uid}.value.{d}"), vr, &prop.name);
|
||||
dims.push(vr);
|
||||
if let Some(nv) = nv {
|
||||
let mut nc = c.clone();
|
||||
nc[d] = nv;
|
||||
@@ -408,6 +411,7 @@ fn prop_row(
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
dims
|
||||
}
|
||||
|
||||
/// How many of a vector parameter's values show (a 3D point's Z only on a 3D layer).
|
||||
@@ -550,7 +554,9 @@ fn group_rows(
|
||||
let visible = !(r.max.y < rect.min.y || r.min.y > rect.max.y);
|
||||
if visible {
|
||||
app.auto.add(&format!("effectControls.row.{}", pr.uid), r, &pr.name);
|
||||
prop_row(app, ui, p, layer, g, pr, ectx, r, indent, actions);
|
||||
let dims = prop_row(app, ui, p, layer, g, pr, ectx, r, indent, actions);
|
||||
// A Timeline pick whip can be dropped here (After Effects).
|
||||
super::timeline::whip_target(ui.ctx(), r.intersect(rect), layer.id.0, pr.uid, dims);
|
||||
}
|
||||
if app.ui.fx_slider_open.contains(&pr.uid) {
|
||||
if matches!(pr.ui, ParamUi::Angle) {
|
||||
|
||||
@@ -139,13 +139,64 @@ fn with_expr_rows(rows: Vec<Row>, comp: &Comp, tl: &crate::state::TimelineState,
|
||||
out
|
||||
}
|
||||
|
||||
/// A pick-whip drag in progress: (parent = 0 / property = 1, layer, prop uid, start point).
|
||||
/// A pick-whip drag in progress: (kind, layer, prop uid, start point). The kinds are the
|
||||
/// layer's parent pick whip, an expression's and a property's (Parent & Link column).
|
||||
type PickWhip = (u8, u64, u64, Pos2);
|
||||
const WHIP_PARENT: u8 = 0;
|
||||
const WHIP_EXPR: u8 = 1;
|
||||
const WHIP_PROP: u8 = 2;
|
||||
|
||||
fn pick_whip_id() -> egui::Id {
|
||||
egui::Id::new("tl-pickwhip")
|
||||
}
|
||||
|
||||
/// A pick whip at `rect`: dragging it starts a pick whip of `kind` from `layer`'s `prop` (0 for
|
||||
/// the layer's own), drawn and dropped at the end of [`show`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn pick_whip(
|
||||
app: &mut EffectcraftApp,
|
||||
ui: &egui::Ui,
|
||||
p: &egui::Painter,
|
||||
rect: Rect,
|
||||
(kind, layer, prop): (u8, u64, u64),
|
||||
auto: &str,
|
||||
label: &str,
|
||||
hint: &str,
|
||||
) -> egui::Response {
|
||||
let resp = ui.interact(rect, egui::Id::new(("pick-whip", kind, layer, prop)), Sense::drag()).on_hover_text(hint);
|
||||
icons::paint(p, rect.shrink(1.0), Icon::PickWhip, if resp.hovered() || resp.dragged() { app.tokens.text } else { app.tokens.text_dim });
|
||||
app.auto.add(auto, rect, label);
|
||||
if resp.drag_started() {
|
||||
let state: PickWhip = (kind, layer, prop, rect.center());
|
||||
ui.ctx().data_mut(|d| d.insert_temp(pick_whip_id(), state));
|
||||
}
|
||||
resp
|
||||
}
|
||||
|
||||
/// Properties shown outside the Timeline (Effect Controls) that a pick whip can be dropped on:
|
||||
/// (row, layer, prop uid, its value fields per dimension), and the pass they were shown in.
|
||||
type WhipTargets = (u64, Vec<(Rect, u64, u64, Vec<Rect>)>);
|
||||
|
||||
fn whip_targets_id() -> egui::Id {
|
||||
egui::Id::new("tl-pickwhip-targets")
|
||||
}
|
||||
|
||||
/// While a pick whip is being dragged, `layer`'s property `prop` shown at `row` in another panel
|
||||
/// is a target too; `dims` are its value fields, one per dimension.
|
||||
pub(crate) fn whip_target(ctx: &egui::Context, row: Rect, layer: u64, prop: u64, dims: Vec<Rect>) {
|
||||
if ctx.data(|d| d.get_temp::<PickWhip>(pick_whip_id())).is_none() {
|
||||
return;
|
||||
}
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
ctx.data_mut(|d| {
|
||||
let targets = d.get_temp_mut_or_default::<WhipTargets>(whip_targets_id());
|
||||
if targets.0 != pass {
|
||||
*targets = (pass, vec![]);
|
||||
}
|
||||
targets.1.push((row, layer, prop, dims));
|
||||
});
|
||||
}
|
||||
|
||||
/// Autoscroll speed (points per second, up negative) of a drag held at height `y` within
|
||||
/// `zone` of the top or bottom edge of `rows`, or past it: faster the deeper.
|
||||
fn edge_scroll_speed(rows: Rect, y: f32, zone: f32) -> f32 {
|
||||
@@ -1846,16 +1897,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
// Parent.
|
||||
if vis.parent {
|
||||
let pw_rect = Rect::from_center_size(pos2(cw.parent + 9.0, cy), vec2(15.0, 15.0));
|
||||
let pw = ui
|
||||
.interact(pw_rect, egui::Id::new(("parent-whip", layer.id.0)), Sense::drag())
|
||||
.on_hover_text(crate::i18n::tr("Parent pick whip: drag onto a layer"));
|
||||
icons::paint(&lp, pw_rect.shrink(1.0), Icon::PickWhip, if pw.hovered() || pw.dragged() { t.text } else { t.text_dim });
|
||||
app.auto.add(&format!("timeline.layer.{}.pickWhip", layer.id.0), pw_rect, "Parent pick whip");
|
||||
if pw.drag_started() {
|
||||
let pw_state: PickWhip = (0, layer.id.0, 0, pw_rect.center());
|
||||
ctx.data_mut(|d| d.insert_temp(pick_whip_id(), pw_state));
|
||||
}
|
||||
pick_whip(
|
||||
app,
|
||||
ui,
|
||||
&lp,
|
||||
Rect::from_center_size(pos2(cw.parent + 9.0, cy), vec2(15.0, 15.0)),
|
||||
(WHIP_PARENT, layer.id.0, 0),
|
||||
&format!("timeline.layer.{}.pickWhip", layer.id.0),
|
||||
"Parent pick whip",
|
||||
crate::i18n::tr("Parent pick whip: drag onto a layer"),
|
||||
);
|
||||
let pr_rect = Rect::from_min_size(pos2(cw.parent + 20.0, cy - 9.0), vec2(94.0, 18.0));
|
||||
let plabel = layer.parent.and_then(|p| comp.layer(p)).map(|l| format!("{}. {}", idx_of(l.id), l.name)).unwrap_or_else(|| "None".into());
|
||||
let presp = widgets::dropdown(ui, pr_rect, &plabel, &t, egui::Id::new(("parent", layer.id.0)));
|
||||
@@ -2143,12 +2194,16 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if en.clicked() {
|
||||
actions.push(("prop.setExpression".into(), json!({"layer": layer.id.0, "prop": uid, "enabled": !ex.enabled})));
|
||||
}
|
||||
let pw_rect = Rect::from_center_size(pos2(cw.switches + 30.0, cy), vec2(15.0, 15.0));
|
||||
let pw = ui
|
||||
.interact(pw_rect, egui::Id::new(("expr-whip", uid)), Sense::drag())
|
||||
.on_hover_text(crate::i18n::tr("Expression pick whip: drag onto a property"));
|
||||
icons::paint(&lp, pw_rect.shrink(1.0), Icon::PickWhip, if pw.hovered() || pw.dragged() { t.text } else { t.text_dim });
|
||||
app.auto.add(&format!("timeline.prop.{uid}.pickWhip"), pw_rect, "Expression pick whip");
|
||||
let pw = pick_whip(
|
||||
app,
|
||||
ui,
|
||||
&lp,
|
||||
Rect::from_center_size(pos2(cw.switches + 30.0, cy), vec2(15.0, 15.0)),
|
||||
(WHIP_EXPR, layer.id.0, *uid),
|
||||
&format!("timeline.prop.{uid}.pickWhip"),
|
||||
"Expression pick whip",
|
||||
crate::i18n::tr("Expression pick whip: drag onto a property"),
|
||||
);
|
||||
if pw.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed()) {
|
||||
// Pressed while the expression is being edited (its text is buffered while
|
||||
// the editor has the focus): the reference will go in at the cursor,
|
||||
@@ -2168,10 +2223,6 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
}
|
||||
});
|
||||
}
|
||||
if pw.drag_started() {
|
||||
let pw_state: PickWhip = (1, layer.id.0, *uid, pw_rect.center());
|
||||
ctx.data_mut(|d| d.insert_temp(pick_whip_id(), pw_state));
|
||||
}
|
||||
// Syntax errors: warning in the outline (the expression is disabled).
|
||||
if let Some(check) = app.session.expr_check
|
||||
&& let Err(e) = check(&ex.text)
|
||||
@@ -2364,6 +2415,20 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
if !dims.is_empty() {
|
||||
dim_hits.push((*uid, dims));
|
||||
}
|
||||
// The property pick whip (Parent & Link column, AE CC 2018 and later): dropped on
|
||||
// a property it links this one to it, adding the expression if there is none.
|
||||
if vis.parent && !prop.static_only {
|
||||
pick_whip(
|
||||
app,
|
||||
ui,
|
||||
&lp,
|
||||
Rect::from_center_size(pos2(cw.parent + 9.0, cy), vec2(15.0, 15.0)),
|
||||
(WHIP_PROP, layer.id.0, *uid),
|
||||
&format!("timeline.prop.{uid}.propertyPickWhip"),
|
||||
"Property pick whip",
|
||||
crate::i18n::tr("Property pick whip: drag onto a property"),
|
||||
);
|
||||
}
|
||||
ui.set_clip_rect(right_clip);
|
||||
// Expression text row hint.
|
||||
if let Some(e) = prop.expr.as_ref().filter(|e| e.enabled && app.ui.timeline.expr_closed.contains(uid)) {
|
||||
@@ -2576,27 +2641,50 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
.collect();
|
||||
actions.push(("layer.select".into(), json!({"layers": layers, "add": ui.input(|i| i.modifiers.shift || i.modifiers.command)})));
|
||||
}
|
||||
// Pick whips: a line follows the pointer; releasing over a row links to it.
|
||||
// Pick whips: a line follows the pointer; releasing over a row links to it (an expression's
|
||||
// or a property's, also over a property in Effect Controls).
|
||||
if let Some((kind, src_layer, src_prop, start)) = ctx.data(|d| d.get_temp::<PickWhip>(pick_whip_id())) {
|
||||
let ptr = ctx.input(|i| i.pointer.latest_pos()).unwrap_or(start);
|
||||
let fg = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, egui::Id::new("tl-pickwhip-line")));
|
||||
fg.line_segment([start, ptr], Stroke::new(1.5, t.accent));
|
||||
fg.circle_stroke(ptr, 4.0, Stroke::new(1.5, t.accent));
|
||||
let target = hit_rows.iter().find(|(r, _)| r.contains(ptr)).map(|(r, row)| (*r, row.clone()));
|
||||
// Shown this pass or the last one (Effect Controls may draw after the Timeline).
|
||||
let pass = ctx.cumulative_pass_nr();
|
||||
let elsewhere = (target.is_none() && kind != WHIP_PARENT)
|
||||
.then(|| ctx.data(|d| d.get_temp::<WhipTargets>(whip_targets_id())))
|
||||
.flatten()
|
||||
.filter(|(p, _)| p.saturating_add(1) >= pass)
|
||||
.and_then(|(_, targets)| targets.into_iter().find(|(r, ..)| r.contains(ptr)));
|
||||
if let Some((tr, _)) = &target {
|
||||
fg.rect_stroke(Rect::from_min_max(tr.min, pos2(graph_x0 - 1.0, tr.max.y)), 0.0, Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
} else if let Some((tr, ..)) = &elsewhere {
|
||||
fg.rect_stroke(*tr, 0.0, Stroke::new(1.0, t.accent), StrokeKind::Inside);
|
||||
}
|
||||
if ctx.input(|i| !i.pointer.any_down()) {
|
||||
ctx.data_mut(|d| d.remove::<PickWhip>(pick_whip_id()));
|
||||
let into = (kind == 1).then(|| ctx.data_mut(|d| d.remove_temp::<WhipInto>(whip_into_id(src_prop)))).flatten();
|
||||
match (kind, target) {
|
||||
(0, Some((_, row))) if row.layer.0 != src_layer => {
|
||||
ctx.data_mut(|d| {
|
||||
d.remove::<PickWhip>(pick_whip_id());
|
||||
d.remove::<WhipTargets>(whip_targets_id());
|
||||
});
|
||||
let into = (kind == WHIP_EXPR).then(|| ctx.data_mut(|d| d.remove_temp::<WhipInto>(whip_into_id(src_prop)))).flatten();
|
||||
// The property dropped on, and the value (dimension) if on one of them: `position[0]`.
|
||||
let dim = |fields: &[Rect]| fields.iter().position(|r| r.contains(ptr));
|
||||
let prop_target = match (&target, elsewhere) {
|
||||
(Some((_, Row { layer, kind: RowKind::Prop { uid }, .. })), _) => {
|
||||
Some((layer.0, *uid, dim_hits.iter().find(|(u, _)| u == uid).and_then(|(_, rs)| dim(rs))))
|
||||
}
|
||||
(None, Some((_, l, u, fields))) => Some((l, u, dim(&fields))),
|
||||
_ => None,
|
||||
};
|
||||
match (kind, target, prop_target) {
|
||||
(WHIP_PARENT, Some((_, row)), _) if row.layer.0 != src_layer => {
|
||||
actions.push(("layer.setParent".into(), json!({"layers": [src_layer], "parent": row.layer.0})));
|
||||
}
|
||||
(1, Some((_, Row { layer, kind: RowKind::Prop { uid }, .. }))) if uid != src_prop => {
|
||||
let mut params = json!({"layer": src_layer, "prop": src_prop, "target": {"layer": layer.0, "prop": uid}});
|
||||
// On one of its values: that dimension (`position[0]`).
|
||||
if let Some(d) = dim_hits.iter().find(|(u, _)| *u == uid).and_then(|(_, rs)| rs.iter().position(|r| r.contains(ptr))) {
|
||||
// The property pick whip replaces the expression with the reference (or adds one),
|
||||
// as the expression pick whip does when its expression isn't being edited.
|
||||
(WHIP_EXPR | WHIP_PROP, _, Some((tl, tu, d))) if tu != src_prop => {
|
||||
let mut params = json!({"layer": src_layer, "prop": src_prop, "target": {"layer": tl, "prop": tu}});
|
||||
if let Some(d) = d {
|
||||
params["target"]["dimension"] = json!(d);
|
||||
}
|
||||
if let Some((text, [a, b])) = into {
|
||||
|
||||
@@ -32,6 +32,20 @@ fn click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, times: usize) {
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// A primary-button drag from `from` to `to`, in steps.
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key) {
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.input_mut().events.push(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
|
||||
@@ -96,16 +110,7 @@ fn the_expression_pick_whip_inserts_at_the_cursor_while_editing() {
|
||||
};
|
||||
let whip = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.pickWhip")), center(h, &format!("timeline.prop.{rotation}.name")));
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
drag(h, from, to);
|
||||
};
|
||||
// Editing: the cursor at the end after typing " * ".
|
||||
let p = center(&h, &format!("timeline.prop.{opacity}.expression"));
|
||||
@@ -149,16 +154,7 @@ fn the_expression_pick_whip_picks_the_dimension_it_is_dropped_on() {
|
||||
};
|
||||
let whip_to = |h: &mut Harness<'_, EffectcraftApp>, target: &str| {
|
||||
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.pickWhip")), center(h, target));
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (k as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
drag(h, from, to);
|
||||
};
|
||||
whip_to(&mut h, &format!("timeline.prop.{position}.value.1"));
|
||||
assert_eq!(expr(&h), "transform.position[1]");
|
||||
@@ -166,6 +162,57 @@ fn the_expression_pick_whip_picks_the_dimension_it_is_dropped_on() {
|
||||
assert_eq!(expr(&h), "transform.position[0]", "the whole Position into one value: its first, as before");
|
||||
}
|
||||
|
||||
/// #363: every property has a pick whip in the Parent & Link column (After Effects CC 2018 and
|
||||
/// later). Dropped on another property's name or one of its values, in the Timeline or in Effect
|
||||
/// Controls, it gives the property an expression referencing it; one it already has is replaced
|
||||
/// by the reference, and a disabled one is enabled.
|
||||
#[test]
|
||||
fn the_property_pick_whip_links_a_property_to_another() {
|
||||
let (mut h, _) = harness();
|
||||
let (layer, opacity, rotation, position, transform) = {
|
||||
let s = &mut h.state_mut().session;
|
||||
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
||||
for fx in ["Slider Control", "Point Control"] {
|
||||
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
|
||||
}
|
||||
let tr = s.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(l)).unwrap().props.sub("transform").unwrap().clone();
|
||||
(l, tr.get("opacity").unwrap().uid, tr.get("rotation").unwrap().uid, tr.get("position").unwrap().uid, tr.uid)
|
||||
};
|
||||
h.state_mut().ui.timeline.open_layers.insert(layer);
|
||||
h.state_mut().ui.timeline.open_groups.insert(transform);
|
||||
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::EffectControls);
|
||||
h.run_steps(3);
|
||||
let props =
|
||||
|h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(layer)).unwrap().props.clone();
|
||||
let expr = |h: &Harness<'_, EffectcraftApp>| props(h).find(opacity).unwrap().expr.clone().map(|e| (e.text, e.enabled));
|
||||
let whip_to = |h: &mut Harness<'_, EffectcraftApp>, target: &str| {
|
||||
let (from, to) = (center(h, &format!("timeline.prop.{opacity}.propertyPickWhip")), center(h, target));
|
||||
drag(h, from, to);
|
||||
};
|
||||
assert_eq!(expr(&h), None);
|
||||
// Onto another property's name: no expression yet, one is added.
|
||||
whip_to(&mut h, &format!("timeline.prop.{rotation}.name"));
|
||||
assert_eq!(expr(&h), Some(("transform.rotation".into(), true)));
|
||||
// Onto one of a property's values: that dimension.
|
||||
whip_to(&mut h, &format!("timeline.prop.{position}.value.1"));
|
||||
assert_eq!(expr(&h), Some(("transform.position[1]".into(), true)));
|
||||
// A disabled expression is replaced by the reference, and enabled.
|
||||
h.state_mut().session.execute("prop.setExpression", json!({"layer": layer, "prop": opacity, "expression": "50 + 1", "enabled": false})).unwrap();
|
||||
h.run_steps(3);
|
||||
whip_to(&mut h, &format!("timeline.prop.{rotation}.name"));
|
||||
assert_eq!(expr(&h), Some(("transform.rotation".into(), true)));
|
||||
// Onto an effect's property in Effect Controls: its value, then one of a point's values.
|
||||
let (slider, point) = (props(&h).prop("effects/#1/slider").unwrap().uid, props(&h).prop("effects/#2/point").unwrap().uid);
|
||||
whip_to(&mut h, &format!("effectControls.prop.{slider}.value"));
|
||||
assert_eq!(expr(&h), Some(("effect(\"Slider Control\")(\"Slider\")".into(), true)));
|
||||
whip_to(&mut h, &format!("effectControls.prop.{point}.value.1"));
|
||||
assert_eq!(expr(&h), Some(("effect(\"Point Control\")(\"Point\")[1]".into(), true)));
|
||||
// Onto itself: nothing changes.
|
||||
whip_to(&mut h, &format!("timeline.prop.{opacity}.name"));
|
||||
assert_eq!(expr(&h), Some(("effect(\"Point Control\")(\"Point\")[1]".into(), true)));
|
||||
}
|
||||
|
||||
/// #362: an expression pick whip held near the bottom or top of the Timeline's layers scrolls
|
||||
/// them (After Effects), so a property out of view can be picked.
|
||||
#[test]
|
||||
|
||||
@@ -495,6 +495,11 @@ Extension points and the bundled extensions: [plugins.md](plugins.md).
|
||||
* `prop.pickWhip {layer, prop, target: {layer, prop, dimension?}}` links a property as its
|
||||
expression pick whip does; `dimension` picks one of the target's values (`position[0]`), as a
|
||||
drop on that value field does.
|
||||
* Every property row has a property pick whip in the Parent & Link column
|
||||
(`timeline.prop.<uid>.propertyPickWhip`; the expression row's is `timeline.prop.<uid>.pickWhip`).
|
||||
Dragged onto another property's name or value, in the Timeline or Effect Controls
|
||||
(`effectControls.prop.<uid>.value[.<d>]`), it runs `prop.pickWhip`: the reference replaces the
|
||||
property's expression, or is added as one, and the expression is enabled.
|
||||
* A property's menu (right-click its name; entries `timeline.prop.<uid>.menu.<entry>`): Reset
|
||||
(`prop.reset`: no keyframes or expression, and the property's default value, an effect
|
||||
parameter's or a Transform property's), Edit Value… (`prop.set {layer, prop}` without `value`
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||
| Automation | ≈ 99% | 0.1 | `.jsxbin` (AUT-2: concave / self-intersecting `onDraw` fills, real images in `drawImage`, image controls and icon buttons landed in M13.11; ScriptUI resource strings, `onDraw`/ScriptUIGraphics, live `onChanging`, `Socket`, Essential Graphics hooks — `addToMotionGraphicsTemplate(As)`, `canAddToMotionGraphicsTemplate`, `exportAsMotionGraphicsTemplate`, `motionGraphicsTemplateName`, controller count/names — and Watch Folder landed in M13.7; the core object model landed in M14.4; ScriptUI windows/dialogs/dockable panels with `scriptui.*` agent commands, File ▸ Scripts install + sample scripts, and the effect plug-in API (EFF-6, WebAssembly) landed in M13.1) |
|
||||
| Shapes | ≈ 80% | 1.5 | Lottie can't carry stroke taper/wave (stroke Taper and Wave, Dash 2/Gap 2/Dash 3/Gap 3 and radial-gradient Highlight Length/Angle landed in M13.5; pen tool for shape paths and vertex editing in M6.5) |
|
||||
| Compositions | ≈ 81% | 3.3 | Mocha-style planar tracks for templates; nested comp markers on the precomp layer bar (Preserve frame rate / resolution when nested and nested comps showing nothing past their end landed in M4.9–M4.10; Pre-compose keeping the comp's settings and New Comp from Selection's dialog in M3.12; CMP-7: Essential Graphics mirrored and linked properties landed in M13.11; Font and uniform Scale controls, Composition ▸ Open in Essential Graphics, Save Frame As ▸ Photoshop Layers / ProEXR and the VR Comp Editor landed in M13.7; the marker dialog, Composition Flowchart, Essential Graphics with master properties, `.ectemplate` templates and Responsive Design — Time landed: CMP-6, CMP-7) |
|
||||
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (G2: the expression pick whip scrolls the Timeline when held at its top or bottom, #362; dropped on one of a property's values it picks that dimension (`position[0]`), #363; the inline expression field grows while typing and its bottom edge drags it taller or shorter, #371; an angle's revolutions scrub and take typing in Effect Controls, the Timeline and Properties, #93; puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
|
||||
| Animation | 70% | 9.0 | puppet depth beyond pins, recording, rigging and follow-through (G2: the expression pick whip scrolls the Timeline when held at its top or bottom, #362; dropped on one of a property's values it picks that dimension (`position[0]`), #363; every property has a property pick whip in the Parent & Link column, which adds the reference as its expression or replaces the one it has, and pick whips drop on Effect Controls properties too, #363; the inline expression field grows while typing and its bottom edge drags it taller or shorter, #371; an angle's revolutions scrub and take typing in Effect Controls, the Timeline and Properties, #93; puppet pin recording with Record Options landed in M13.1; pin selection, rotate/scale handles, nulls for pins and Follow-Through in M13.14–M13.16), Wiggler/Smoother/Motion Sketch (motion-path handles and the graph editor transform box landed in M5.8; keyframe colour labels and Select Keyframe Label Group, Graph Editor snapping to markers / layer ends in M13.5) |
|
||||
| Text | ≈ 96% | 0.3 | no extruded strokes (G2: the font menus list every installed family with its own styles — before, only a project asking for a font read the system fonts — and `text.fonts` / MCP `list_fonts` list them for agents; M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) |
|
||||
| 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
|
||||
Reference in new issue
Block a user