Timeline: parent and matte selected layers together (#1260)

On a selected layer's row, the Parent menu and pick whip and the Track
Matte menu now set every selected layer's parent or matte, as moving a
selected layer's bar moves them all; on an unselected row they set that
layer's alone. Each is one undo step. A selected layer chosen as the
parent or the matte is left out of the layers it links, and parenting
that would make a loop is refused as a whole.

A new matte layer keeps each layer's mode (layer.setTrackMatte with
kind: preserve); a mode and No Track Matte apply to all of them.

The Track Matte column gets a pick whip, like the parent's
(timeline.layer.<id>.trackMattePickWhip, with a translated tooltip):
dropped on an audio-video layer's row, it makes that layer the matte.
The layers a parent or matte pick whip links are taken when its drag
starts.

Tests (ui_batch_links): the Parent menu and pick whip, and the Track
Matte menu, mode, No Track Matte and pick whip, with real clicks and
drags: selected layers change together and unselected ones don't, modes
are kept, one undo puts links, compensated transforms and the matte's
visibility back, and a refused cycle changes nothing. Both fail on main.
This commit is contained in:
tylerjenningsw
2026-10-13 10:52:40 -04:00
parent f51b4b7e1b
commit 4ce9569454
4 changed files with 228 additions and 21 deletions
+7
View File
@@ -383,6 +383,7 @@ const JAPANESE: &[(&str, &str)] = &[
("Overlay", "オーバーレイ"),
("Parameter Name:", "パラメーター名:"),
("Parent pick whip: drag onto a layer", "親ピックウィップ:レイヤー上にドラッグ"),
("Track Matte pick whip: drag onto a layer", "トラックマットピックウィップ:レイヤー上にドラッグ"),
("Pixel Aspect Ratio:", "ピクセルアスペクト比:"),
("Playing on Video Preview", "ビデオプレビューで再生中"),
("Post-Render Action: {}", "レンダリング後の処理:{}"),
@@ -924,6 +925,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Overlay", "叠加"),
("Parameter Name:", "参数名称:"),
("Parent pick whip: drag onto a layer", "父级拾取器:拖到图层上"),
("Track Matte pick whip: drag onto a layer", "轨道遮罩拾取器:拖到图层上"),
("Pixel Aspect Ratio:", "像素长宽比:"),
("Playing on Video Preview", "在视频预览中播放"),
("Post-Render Action: {}", "渲染后动作:{}"),
@@ -1656,6 +1658,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Overlay", "疊加"),
("Parameter Name:", "參數名稱:"),
("Parent pick whip: drag onto a layer", "上層拾取線:拖到圖層上"),
("Track Matte pick whip: drag onto a layer", "軌道遮罩拾取線:拖到圖層上"),
("Pixel Aspect Ratio:", "像素長寬比例:"),
("Playing on Video Preview", "在視訊預覽中播放"),
("Post-Render Action: {}", "算圖後動作:{}"),
@@ -2397,6 +2400,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
("Overlay", "Накладання"),
("Parameter Name:", "Назва параметра:"),
("Parent pick whip: drag onto a layer", "Зв’язування батьківського шару: перетягніть на шар"),
("Track Matte pick whip: drag onto a layer", "Зв’язування трекової маски: перетягніть на шар"),
("Pixel Aspect Ratio:", "Пропорції пікселя:"),
("Playing on Video Preview", "Відтворення на відеоперегляді"),
("Post-Render Action: {}", "Дія після рендерингу: {}"),
@@ -2942,6 +2946,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
("Output To:", "Saída para:"),
("Parameter Name:", "Nome do parâmetro:"),
("Parent pick whip: drag onto a layer", "Pick whip de pai: arraste até uma camada"),
("Track Matte pick whip: drag onto a layer", "Pick whip de track matte: arraste até uma camada"),
("Pixel Aspect Ratio:", "Proporção de pixel:"),
("Playing on Video Preview", "Reproduzindo na visualização de vídeo"),
("Post-Render Action: {}", "Ação pós-renderização: {}"),
@@ -3393,6 +3398,7 @@ const CZECH: &[(&str, &str)] = &[
("Overlay", "Překrytí"),
("Parameter Name:", "Název parametru:"),
("Parent pick whip: drag onto a layer", "Propojovací nástroj nadřazené vrstvy: přetáhněte na vrstvu"),
("Track Matte pick whip: drag onto a layer", "Propojovací nástroj masky stopy: přetáhněte na vrstvu"),
("Pixel Aspect Ratio:", "Poměr stran pixelů:"),
("Playing on Video Preview", "Přehrávání ve videonáhledu"),
("Post-Render Action: {}", "Akce po vykreslení: {}"),
@@ -3949,6 +3955,7 @@ const SPANISH: &[(&str, &str)] = &[
("Overlay", "Superposición"),
("Parameter Name:", "Nombre del parámetro:"),
("Parent pick whip: drag onto a layer", "Selector de capa padre: arrastra hasta una capa"),
("Track Matte pick whip: drag onto a layer", "Selector de mate de seguimiento: arrastra hasta una capa"),
("Pixel Aspect Ratio:", "Relación de aspecto de píxel:"),
("Playing on Video Preview", "Reproduciendo en la previsualización de vídeo"),
("Post-Render Action: {}", "Acción posterior al procesamiento: {}"),
+60 -21
View File
@@ -155,16 +155,29 @@ fn with_expr_rows(rows: Vec<Row>, comp: &Comp, tl: &crate::state::TimelineState,
}
/// 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).
/// layer's parent pick whip, an expression's, a property's (Parent & Link column) and the layer's
/// track matte pick whip.
type PickWhip = (u8, u64, u64, Pos2);
const WHIP_PARENT: u8 = 0;
const WHIP_EXPR: u8 = 1;
const WHIP_PROP: u8 = 2;
const WHIP_MATTE: u8 = 3;
fn pick_whip_id() -> egui::Id {
egui::Id::new("tl-pickwhip")
}
/// The layers a layer pick whip (parent, track matte) links, taken as its drag starts.
fn whip_layers_id() -> egui::Id {
egui::Id::new("tl-pickwhip-layers")
}
/// The layers an edit made on `layer`'s row applies to (its parent, its track matte, moving its
/// bar): the selection when the row is selected, else that layer alone (#1260).
fn edited_layers(layer: LayerId, selected: &[LayerId]) -> Vec<u64> {
if selected.contains(&layer) { selected.iter().map(|l| l.0).collect() } else { vec![layer.0] }
}
/// 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)]
@@ -183,7 +196,11 @@ fn pick_whip(
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));
let layers = edited_layers(LayerId(layer), &app.session.state.selected_layers);
ui.ctx().data_mut(|d| {
d.insert_temp(pick_whip_id(), state);
d.insert_temp(whip_layers_id(), layers);
});
}
resp
}
@@ -2149,8 +2166,19 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::new(("pt", layer.id.0))).clicked() {
actions.push(("layer.setSwitch".into(), json!({"layers": [layer.id.0], "switch": "preserveTransparency"})));
}
// Track matte: layer picker + kind.
let mr = Rect::from_min_size(pos2(cw.trkmat + 2.0, cy - 9.0), vec2(104.0, 18.0));
// Track matte: a pick whip, then the layer picker with the mode. Like the parent,
// they set the matte of every selected layer when the row is selected (#1260).
pick_whip(
app,
ui,
&lp,
Rect::from_center_size(pos2(cw.trkmat + 9.0, cy), vec2(15.0, 15.0)),
(WHIP_MATTE, layer.id.0, 0),
&format!("timeline.layer.{}.trackMattePickWhip", layer.id.0),
"Track Matte pick whip",
crate::i18n::tr("Track Matte pick whip: drag onto a layer"),
);
let mr = Rect::from_min_size(pos2(cw.trkmat + 20.0, cy - 9.0), vec2(88.0, 18.0));
let label = match layer.track_matte {
Some(m) => comp.layer(m.layer).map(|l| format!("{} {}", idx_of(l.id), l.name)).unwrap_or("None".into()),
None => "No Track Matte".into(),
@@ -2180,7 +2208,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let current = layer.track_matte.and_then(|m| opts.iter().position(|o| o == m.kind.label()));
if let Some(i) = widgets::popup_menu(ui, pop, mr.left_bottom(), &opts, current) {
let ids: Vec<LayerId> = candidates.iter().map(|l| l.id).collect();
if let Some(a) = opts.get(i).and_then(|o| track_matte_pick(layer, &ids, i, o)) {
if let Some(a) = opts.get(i).and_then(|o| track_matte_pick(&edited_layers(layer.id, &selected), &ids, i, o)) {
actions.push(a);
}
}
@@ -2218,8 +2246,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
}
}
if let Some(i) = widgets::popup_menu(ui, pop, pr_rect.left_bottom(), &popts, None) {
let par = if i == 0 { serde_json::Value::Null } else { json!(cands[i - 2].id.0) };
actions.push(("layer.setParent".into(), json!({"layers": [layer.id.0], "parent": par})));
let par = cands.get(i.wrapping_sub(2)).map(|l| l.id.0);
// (The new parent itself, if selected, stays as it is.)
let layers: Vec<u64> = edited_layers(layer.id, &selected).into_iter().filter(|l| Some(*l) != par).collect();
actions.push(("layer.setParent".into(), json!({"layers": layers, "parent": par})));
}
}
// Layer bar.
@@ -2353,7 +2383,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
let d = frames as f64 * fd;
// Moving or trimming a selected layer acts on the whole
// selection; each layer trims from its own in/out point.
let targets = if is_sel { json!(selected.iter().map(|l| l.0).collect::<Vec<_>>()) } else { json!([layer.id.0]) };
let targets = json!(edited_layers(layer.id, &selected));
let params = match kind {
"move" => json!({"layers": targets, "delta": d, "merge": drag_key}),
"in" => json!({"layers": targets, "inDelta": d, "merge": drag_key}),
@@ -3016,7 +3046,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
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)
let elsewhere = (target.is_none() && !matches!(kind, WHIP_PARENT | WHIP_MATTE))
.then(|| ctx.data(|d| d.get_temp::<WhipTargets>(whip_targets_id())))
.flatten()
.filter(|(p, _)| p.saturating_add(1) >= pass)
@@ -3027,10 +3057,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
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| {
let whip_layers = ctx.data_mut(|d| {
d.remove::<PickWhip>(pick_whip_id());
d.remove::<WhipTargets>(whip_targets_id());
d.remove_temp::<Vec<u64>>(whip_layers_id())
});
// The layers a layer pick whip links, less the one it was dropped on.
let linked = |onto: u64| -> Vec<u64> { whip_layers.clone().unwrap_or_else(|| vec![src_layer]).into_iter().filter(|l| *l != onto).collect() };
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));
@@ -3043,7 +3076,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
};
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})));
actions.push(("layer.setParent".into(), json!({"layers": linked(row.layer.0), "parent": row.layer.0})));
}
// Each layer keeps its own matte mode.
(WHIP_MATTE, Some((_, row)), _) if row.layer.0 != src_layer && comp.layer(row.layer).is_some_and(|l| l.source.is_av()) => {
actions.push(("layer.setTrackMatte".into(), json!({"layers": linked(row.layer.0), "matte": row.layer.0, "kind": "preserve"})));
}
// 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.
@@ -3756,19 +3793,21 @@ fn text_anim_popups(
}
}
/// The command for the Track Matte menu item `i` (`choice`) of `layer`, whose menu lists "No Track
/// Matte", a separator, the matte `candidates`, a separator and the modes. A mode without a matte
/// layer takes the layer above, like Layer ▸ Track Matte (#864: it used to do nothing).
fn track_matte_pick(layer: &Layer, candidates: &[LayerId], i: usize, choice: &str) -> Option<(String, serde_json::Value)> {
/// The command for the Track Matte menu item `i` (`choice`) of a row, setting `layers`' mattes,
/// whose menu lists "No Track Matte", a separator, the matte `candidates`, a separator and the
/// modes. A new matte layer keeps each layer's mode (the matte itself, if among them, is left
/// out); a mode without a matte layer takes the layer above, like Layer ▸ Track Matte (#864: it
/// used to do nothing).
fn track_matte_pick(layers: &[u64], candidates: &[LayerId], i: usize, choice: &str) -> Option<(String, serde_json::Value)> {
if i == 0 {
return Some(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": null})));
return Some(("layer.setTrackMatte".into(), json!({"layers": layers, "matte": null})));
}
if let Some(m) = i.checked_sub(2).and_then(|j| candidates.get(j)) {
let kind = layer.track_matte.map(|m| m.kind).unwrap_or_default();
return Some(("layer.setTrackMatte".into(), json!({"layer": layer.id.0, "matte": m.0, "kind": kind_name(kind)})));
let layers: Vec<u64> = layers.iter().copied().filter(|l| *l != m.0).collect();
return (!layers.is_empty()).then(|| ("layer.setTrackMatte".into(), json!({"layers": layers, "matte": m.0, "kind": "preserve"})));
}
let k = MatteKind::ALL.into_iter().find(|k| k.label() == choice)?;
Some(("layer.trackMatte".into(), json!({"layers": [layer.id.0], "op": kind_name(k)})))
Some(("layer.trackMatte".into(), json!({"layers": layers, "op": kind_name(k)})))
}
fn kind_name(k: MatteKind) -> &'static str {
@@ -4221,7 +4260,7 @@ mod tests {
let (bottom, matte, top) = (find(&s, "Chain Bottom"), find(&s, "Matte"), find(&s, "Chain Top"));
let candidates = [top.id, matte.id];
let pick = |s: &mut effectcraft_engine::Session, name: &str, i: usize, choice: &str| {
let (id, params) = track_matte_pick(&find(s, name), &candidates, i, choice).expect("a menu item is an action");
let (id, params) = track_matte_pick(&[find(s, name).id.0], &candidates, i, choice).expect("a menu item is an action");
s.execute(&id, params)
};
pick(&mut s, "Chain Bottom", 5, "Alpha Inverted Matte").unwrap();
@@ -4239,7 +4278,7 @@ mod tests {
assert!(pick(&mut s, "Chain Top", 5, "Alpha Matte").is_err());
pick(&mut s, "Chain Bottom", 0, "No Track Matte").unwrap();
assert!(find(&s, "Chain Bottom").track_matte.is_none());
assert!(track_matte_pick(&bottom, &candidates, 1, "-").is_none(), "a separator does nothing");
assert!(track_matte_pick(&[bottom.id.0], &candidates, 1, "-").is_none(), "a separator does nothing");
}
/// #957: vector footage gets the Continuously Rasterize switch; raster footage does not.
+154
View File
@@ -0,0 +1,154 @@
//! Parent and Track Matte for several layers at once (#1260): on a selected layer's row, the
//! Timeline's Parent and Track Matte menus and pick whips set every selected layer; on an
//! unselected row, that layer alone. One undo step each; each layer keeps its own matte mode.
use effectcraft_engine::Session;
use effectcraft_engine::project::{LayerId, MatteKind};
use effectcraft_ui_egui::EffectcraftApp;
use egui::{Event, Modifiers, PointerButton, Pos2, Rect, pos2, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable as _;
use serde_json::json;
/// Solids A, B, C, P (moved and scaled, a parent) and the mattes X and Y; stacked Y, X, P, C, B, A
/// from the top. The Track Matte column is shown.
fn harness() -> (Harness<'static, EffectcraftApp>, [u64; 6]) {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Links", "width": 320, "height": 180, "duration": 4})).unwrap();
let ids = ["A", "B", "C", "P", "X", "Y"]
.map(|n| s.execute("layer.newSolid", json!({"name": n, "color": "#406080", "width": 40, "height": 40})).unwrap()["layer"].as_u64().unwrap());
s.execute("prop.set", json!({"layer": ids[3], "path": "transform/position", "value": [60, 40]})).unwrap();
s.execute("prop.set", json!({"layer": ids[3], "path": "transform/scale", "value": [50, 50]})).unwrap();
s.history.undo.clear();
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.state_mut().ui.timeline.show_modes = true;
h.state_mut().ui.timeline.columns_w = 900.0;
h.run_steps(3);
(h, ids)
}
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
}
fn press(h: &mut Harness<'_, EffectcraftApp>, at: Pos2, pressed: bool) {
h.event(Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
}
fn click(h: &mut Harness<'_, EffectcraftApp>, at: Pos2) {
h.event(Event::PointerMoved(at));
h.step();
press(h, at, true);
h.step();
press(h, at, false);
h.run_steps(3);
}
/// Open the menu at automation id `dropdown` and choose `item` in it.
fn choose(h: &mut Harness<'_, EffectcraftApp>, dropdown: &str, item: &str) {
let menu = rect(h, dropdown);
click(h, menu.center());
// (The menu may open above it to fit the window: the entry nearest to it.)
let off = |r: &Rect| (r.center().y - menu.center().y).abs();
let at = h.query_all_by_label(item).map(|n| n.rect()).min_by(|a, b| off(a).total_cmp(&off(b)));
let at = at.unwrap_or_else(|| panic!("no {item} in the {dropdown} menu")).center();
click(h, at);
}
/// Drag the pick whip `whip` onto layer `onto`'s row.
fn whip(h: &mut Harness<'_, EffectcraftApp>, whip: &str, onto: u64) {
let (from, to) = (rect(h, whip).center(), rect(h, &format!("timeline.layer.{onto}.row")).center());
h.event(Event::PointerMoved(from));
h.step();
press(h, from, true);
h.step();
for i in 1..=10 {
h.event(Event::PointerMoved(from + (to - from) * (i as f32 / 10.0)));
h.step();
}
press(h, to, false);
h.run_steps(3);
}
fn select(h: &mut Harness<'_, EffectcraftApp>, ids: &[u64]) {
h.state_mut().session.execute("layer.select", json!({"layers": ids})).unwrap();
h.run_steps(2);
}
fn parent(h: &Harness<'_, EffectcraftApp>, id: u64) -> Option<u64> {
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().parent.map(|p| p.0)
}
fn position(h: &Harness<'_, EffectcraftApp>, id: u64) -> [f64; 3] {
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().props.prop("transform/position").unwrap().value.as_vec3()
}
fn matte(h: &Harness<'_, EffectcraftApp>, id: u64) -> Option<(u64, MatteKind)> {
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().track_matte.map(|m| (m.layer.0, m.kind))
}
/// The project and its undo steps as they are now, to compare with after an undo or a refusal.
fn mark(h: &Harness<'_, EffectcraftApp>) -> (std::sync::Arc<effectcraft_engine::project::Project>, usize) {
(h.state().session.project.clone(), h.state().session.history.undo.len())
}
fn unchanged(h: &Harness<'_, EffectcraftApp>, m: &(std::sync::Arc<effectcraft_engine::project::Project>, usize)) -> bool {
h.state().session.project.same_content(&m.0) && h.state().session.history.undo.len() == m.1
}
#[test]
fn parent_menu_and_pick_whip_parent_the_selected_layers() {
let (mut h, [a, b, c, p, ..]) = harness();
let start = position(&h, a);
select(&mut h, &[a, b]);
let before = mark(&h);
// The Parent menu on A's (selected) row parents A and B, kept where they are.
choose(&mut h, &format!("timeline.layer.{a}.parent"), "3. P");
assert_eq!([parent(&h, a), parent(&h, b), parent(&h, c)], [Some(p), Some(p), None]);
assert!(position(&h, a) != start && position(&h, a) == position(&h, b), "both re-expressed in P's space");
assert_eq!(h.state().session.history.undo.len(), before.1 + 1, "one undo step");
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
h.run_steps(2);
assert!(unchanged(&h, &before), "one undo puts both back");
// The pick whip of an unselected row parents that layer alone.
whip(&mut h, &format!("timeline.layer.{c}.pickWhip"), p);
assert_eq!([parent(&h, a), parent(&h, b), parent(&h, c)], [None, None, Some(p)]);
// A selected row's whip parents the selection, less the parent itself if selected.
select(&mut h, &[a, b, p]);
whip(&mut h, &format!("timeline.layer.{a}.pickWhip"), p);
assert_eq!([parent(&h, a), parent(&h, b), parent(&h, p)], [Some(p), Some(p), None]);
// A cycle (P under A, while A is under P) is refused as a whole.
select(&mut h, &[p, c]);
let before = mark(&h);
choose(&mut h, &format!("timeline.layer.{p}.parent"), "6. A");
assert!(unchanged(&h, &before), "the refused parenting changed nothing");
assert_eq!([parent(&h, p), parent(&h, c)], [None, Some(p)]);
}
#[test]
fn track_matte_menu_and_pick_whip_set_the_selected_layers_mattes() {
let (mut h, [a, b, c, _, x, y]) = harness();
h.state_mut().session.execute("layer.setTrackMatte", json!({"layer": a, "matte": x, "kind": "luma"})).unwrap();
h.state_mut().session.execute("layer.setTrackMatte", json!({"layer": b, "matte": x, "kind": "alpha"})).unwrap();
select(&mut h, &[a, b]);
let before = mark(&h);
// A new matte layer for both; each keeps its mode; C (not selected) keeps none.
choose(&mut h, &format!("timeline.layer.{a}.trackMatte"), "1. Y");
assert_eq!([matte(&h, a), matte(&h, b), matte(&h, c)], [Some((y, MatteKind::Luma)), Some((y, MatteKind::Alpha)), None]);
assert!(!h.state().session.active_comp().unwrap().layer(LayerId(y)).unwrap().switches.video, "Y is hidden as a matte");
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
h.run_steps(2);
assert!(unchanged(&h, &before), "one undo puts both mattes and Y's visibility back");
// A mode for both, then No Track Matte for both.
choose(&mut h, &format!("timeline.layer.{b}.trackMatte"), "Luma Inverted Matte");
assert_eq!([matte(&h, a), matte(&h, b)], [Some((x, MatteKind::LumaInverted)), Some((x, MatteKind::LumaInverted))]);
choose(&mut h, &format!("timeline.layer.{a}.trackMatte"), "No Track Matte");
assert_eq!([matte(&h, a), matte(&h, b)], [None, None]);
// The track matte pick whip: a selected row's sets the selection's, an unselected row's
// its own.
whip(&mut h, &format!("timeline.layer.{a}.trackMattePickWhip"), y);
assert_eq!([matte(&h, a), matte(&h, b), matte(&h, c)], [Some((y, MatteKind::Alpha)), Some((y, MatteKind::Alpha)), None]);
whip(&mut h, &format!("timeline.layer.{c}.trackMattePickWhip"), x);
assert_eq!([matte(&h, a), matte(&h, c)], [Some((y, MatteKind::Alpha)), Some((x, MatteKind::Alpha))]);
}
+7
View File
@@ -610,6 +610,13 @@ Extension points and the bundled extensions: [plugins.md](plugins.md).
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.
* Parent and Track Matte for several layers: on a selected layer's row, the Timeline's Parent
menu and pick whip (`timeline.layer.<id>.parent`, `timeline.layer.<id>.pickWhip`) and its Track
Matte menu and pick whip (`timeline.layer.<id>.trackMatte`,
`timeline.layer.<id>.trackMattePickWhip`) set every selected layer's (on an unselected row,
that layer's), in one undo step. Directly: `layer.setParent {layers, parent}` and
`layer.setTrackMatte {layers, matte, kind}`, where `kind: "preserve"` keeps each layer's
own matte mode.
* A property's menu (right-click its name; entries `timeline.prop.<uid>.menu.<entry>`): Reset
(`prop.reset`: no expression, and the property's default value, an effect parameter's or a
Transform property's; an animated property keeps its keyframes and gets the default as a