mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #1291 from tylerjenningsw/enhance/batch-parent-matte
Timeline: parent and matte selected layers together (#1260)
This commit is contained in:
@@ -121,7 +121,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Track Matte",
|
||||
[],
|
||||
None,
|
||||
"{layer?, matte: layer|null, kind?: alpha|alphaInverted|luma|lumaInverted}",
|
||||
"{layers? | layer?, matte: layer|null, kind?: alpha|alphaInverted|luma|lumaInverted|preserve}",
|
||||
has_layers,
|
||||
track_matte
|
||||
),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{Comp, EngineError, ItemId, KV, Layer, LayerId, PropGroup, Result, Session, Tick, Value, b_p, bad, layers_p, resolve_layer};
|
||||
use super::{Comp, EngineError, ItemId, KV, Layer, LayerId, PropGroup, Result, Session, Tick, Value, b_p, bad, layers_p, resolve_layer, unlocked};
|
||||
|
||||
/// Re-express a layer in its new parent space without moving its anchor.
|
||||
pub(crate) struct ParentFix {
|
||||
@@ -309,6 +309,8 @@ fn check_parent_chain(comp: &Comp, ids: &[LayerId], parent: Option<LayerId>, max
|
||||
|
||||
pub(super) fn set_parent(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
// Locked layers keep their parent (several at once: the others are parented).
|
||||
let ids = unlocked(s, cid, ids, "layer.setParent")?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let parent = match p.get("parent") {
|
||||
None | Some(Value::Null) => None,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
BlendMode, EngineError, FrameBlend, ItemId, KV, Layer, LayerId, MatteKind, Quality, Result, Session, TrackMatte, Value, b_p, bad, json, layer_mut, layer_p,
|
||||
layers_p, merge_p, resolve_layer, str_p,
|
||||
layers_p, merge_p, resolve_layer, str_p, unlocked,
|
||||
};
|
||||
|
||||
pub(super) fn set_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
@@ -181,33 +181,122 @@ pub(super) fn blend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// `layer.setTrackMatte`: `layers` (or `layer`, else the first selected layer; locked ones left
|
||||
/// as they are) use `matte` as their track matte (none when null), in one undo step. `kind` sets the mode; `preserve` keeps
|
||||
/// each layer's own (Alpha for a layer without a matte), so changing the matte of several layers
|
||||
/// at once keeps their modes.
|
||||
pub(super) fn track_matte(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid) = layer_p(s, p, "layer.setTrackMatte")?;
|
||||
let (cid, ids) = match p.get("layers") {
|
||||
Some(_) => layers_p(s, p)?,
|
||||
None => layer_p(s, p, "layer.setTrackMatte").map(|(c, l)| (c, vec![l]))?,
|
||||
};
|
||||
// Locked layers keep their matte (several at once: the others get it).
|
||||
let ids = unlocked(s, cid, ids, "layer.setTrackMatte")?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let matte = match p.get("matte") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(v) => Some(resolve_layer(comp, v).ok_or_else(|| bad("layer.setTrackMatte", "no such matte layer"))?),
|
||||
};
|
||||
let kind = str_p(p, "kind")
|
||||
.map(|k| match k.to_ascii_lowercase().replace([' ', '_', '-'], "").as_str() {
|
||||
if ids.is_empty() {
|
||||
return Err(bad("layer.setTrackMatte", "no layers"));
|
||||
}
|
||||
if matte.is_some_and(|m| ids.contains(&m)) {
|
||||
return Err(bad("layer.setTrackMatte", "a layer can't be its own matte"));
|
||||
}
|
||||
// None: each layer keeps its own mode.
|
||||
let kind = match str_p(p, "kind").map(|k| k.to_ascii_lowercase().replace([' ', '_', '-'], "")) {
|
||||
Some(k) if k == "preserve" => None,
|
||||
Some(k) => Some(match k.as_str() {
|
||||
"alphainverted" | "alphainv" => MatteKind::AlphaInverted,
|
||||
"luma" => MatteKind::Luma,
|
||||
"lumainverted" | "lumainv" => MatteKind::LumaInverted,
|
||||
_ => MatteKind::Alpha,
|
||||
})
|
||||
.unwrap_or(MatteKind::Alpha);
|
||||
}),
|
||||
None => Some(MatteKind::Alpha),
|
||||
};
|
||||
s.edit("Track Matte", None, |proj, _| {
|
||||
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
||||
if let Some(m) = matte {
|
||||
if m == lid {
|
||||
return Err(bad("layer.setTrackMatte", "a layer can't be its own matte"));
|
||||
}
|
||||
if let Some(ml) = comp.layer_mut(m) {
|
||||
ml.switches.video = false;
|
||||
}
|
||||
if let Some(ml) = matte.and_then(|m| comp.layer_mut(m)) {
|
||||
ml.switches.video = false;
|
||||
}
|
||||
for lid in &ids {
|
||||
let l = comp.layer_mut(*lid).ok_or(EngineError::NoComp)?;
|
||||
let kind = kind.unwrap_or_else(|| l.track_matte.map(|m| m.kind).unwrap_or_default());
|
||||
l.track_matte = matte.map(|layer| TrackMatte { layer, kind });
|
||||
}
|
||||
comp.layer_mut(lid).ok_or(EngineError::NoComp)?.track_matte = matte.map(|layer| TrackMatte { layer, kind });
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod track_matte_batch_tests {
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn matte_of(s: &Session, id: u64) -> Option<(u64, MatteKind)> {
|
||||
s.active_comp().unwrap().layer(LayerId(id)).unwrap().track_matte.map(|m| (m.layer.0, m.kind))
|
||||
}
|
||||
|
||||
/// #1260: several layers take one matte layer at once, in one undo step; `kind: "preserve"`
|
||||
/// keeps each one's mode; refused calls change nothing; one `layer` still works.
|
||||
#[test]
|
||||
fn several_layers_take_one_matte_and_keep_their_modes() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name":"Mattes","width":16,"height":16,"duration":1})).unwrap();
|
||||
let [a, b, c, x, y] = ["A", "B", "C", "X", "Y"].map(|n| s.execute("layer.newSolid", json!({"name": n})).unwrap()["layer"].as_u64().unwrap());
|
||||
s.execute("layer.setTrackMatte", json!({"layer": a, "matte": x, "kind": "luma"})).unwrap();
|
||||
s.execute("layer.setTrackMatte", json!({"layer": b, "matte": x})).unwrap();
|
||||
assert_eq!((matte_of(&s, a), matte_of(&s, b)), (Some((x, MatteKind::Luma)), Some((x, MatteKind::Alpha))), "one layer, as before");
|
||||
let (before, undo) = (s.project.clone(), s.history.undo.len());
|
||||
s.execute("layer.setTrackMatte", json!({"layers": [a, b, c], "matte": y, "kind": "preserve"})).unwrap();
|
||||
assert_eq!(
|
||||
[matte_of(&s, a), matte_of(&s, b), matte_of(&s, c)],
|
||||
[Some((y, MatteKind::Luma)), Some((y, MatteKind::Alpha)), Some((y, MatteKind::Alpha))],
|
||||
"each keeps its mode (Alpha without one)"
|
||||
);
|
||||
assert!(!s.active_comp().unwrap().layer(LayerId(y)).unwrap().switches.video, "the matte is hidden");
|
||||
assert_eq!(s.history.undo.len(), undo + 1, "one undo step");
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(s.project.same_content(&before));
|
||||
// Refused: a layer as its own matte, an unknown layer, no layers. Nothing changes.
|
||||
for p in [
|
||||
json!({"layers": [a, y], "matte": y}),
|
||||
json!({"layers": [a, 9999], "matte": y}),
|
||||
json!({"layers": [], "matte": y}),
|
||||
json!({"layers": [a], "matte": 9999}),
|
||||
] {
|
||||
let (project, revision) = (s.project.clone(), s.revision);
|
||||
assert!(s.execute("layer.setTrackMatte", p.clone()).is_err(), "{p}");
|
||||
assert!(Arc::ptr_eq(&project, &s.project) && s.revision == revision, "{p} changed nothing");
|
||||
}
|
||||
s.execute("layer.setTrackMatte", json!({"layers": [a, b], "matte": null})).unwrap();
|
||||
assert_eq!((matte_of(&s, a), matte_of(&s, b)), (None, None), "both lose it");
|
||||
}
|
||||
|
||||
/// Locked layers keep their parent and matte when they are among several (the others change),
|
||||
/// and alone they are an error that changes nothing.
|
||||
#[test]
|
||||
fn locked_layers_keep_their_parent_and_matte() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name":"Locks","width":16,"height":16,"duration":1})).unwrap();
|
||||
let [a, b, p, m] = ["A", "B", "P", "M"].map(|n| s.execute("layer.newSolid", json!({"name": n})).unwrap()["layer"].as_u64().unwrap());
|
||||
s.execute("layer.setSwitch", json!({"layers": [b], "switch": "lock", "value": true})).unwrap();
|
||||
let layer = |s: &Session, id: u64| s.active_comp().unwrap().layer(LayerId(id)).unwrap().clone();
|
||||
s.execute("layer.setParent", json!({"layers": [a, b], "parent": p})).unwrap();
|
||||
s.execute("layer.setTrackMatte", json!({"layers": [a, b], "matte": m, "kind": "preserve"})).unwrap();
|
||||
s.execute("layer.trackMatte", json!({"layers": [a, b], "op": "luma"})).unwrap();
|
||||
assert_eq!((layer(&s, a).parent, matte_of(&s, a)), (Some(LayerId(p)), Some((m, MatteKind::Luma))));
|
||||
assert_eq!((layer(&s, b).parent, matte_of(&s, b)), (None, None), "the locked layer is left as it was");
|
||||
for (cmd, p) in [
|
||||
("layer.setParent", json!({"layers": [b], "parent": p})),
|
||||
("layer.setTrackMatte", json!({"layers": [b], "matte": m})),
|
||||
("layer.trackMatte", json!({"layers": [b], "op": "alpha"})),
|
||||
] {
|
||||
let (project, revision) = (s.project.clone(), s.revision);
|
||||
let err = s.execute(cmd, p).unwrap_err();
|
||||
assert!(err.to_string().contains("is locked"), "{cmd}: {err}");
|
||||
assert!(Arc::ptr_eq(&project, &s.project) && s.revision == revision, "{cmd} changed nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,6 +609,7 @@ fn delete_all_markers(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
fn track_matte_op(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, ids) = layers_p(s, p)?;
|
||||
let ids = super::unlocked(s, cid, ids, "layer.trackMatte")?;
|
||||
let op = str_p(p, "op").ok_or_else(|| bad("layer.trackMatte", "missing `op`"))?.to_string();
|
||||
let kind = match op.as_str() {
|
||||
"alpha" => Some(MatteKind::Alpha),
|
||||
|
||||
@@ -467,6 +467,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: {}", "レンダリング後の処理:{}"),
|
||||
@@ -1199,6 +1200,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: {}", "渲染后动作:{}"),
|
||||
@@ -1939,6 +1941,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: {}", "算圖後動作:{}"),
|
||||
@@ -2639,6 +2642,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: {}", "Дія після рендерингу: {}"),
|
||||
@@ -3192,6 +3196,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: {}"),
|
||||
@@ -3950,6 +3955,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í: {}"),
|
||||
@@ -4513,6 +4519,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: {}"),
|
||||
|
||||
@@ -158,11 +158,13 @@ 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;
|
||||
|
||||
/// The `egui::Id` under which a pick-whip drag in progress ([`PickWhip`]) is kept during the
|
||||
/// drag.
|
||||
@@ -170,6 +172,11 @@ fn pick_whip_id() -> egui::Id {
|
||||
egui::Id::unique("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::unique("tl-pickwhip-layers")
|
||||
}
|
||||
|
||||
/// The automation id, label and hover hint of a pick whip.
|
||||
struct WhipText<'a> {
|
||||
auto: &'a str,
|
||||
@@ -193,7 +200,15 @@ 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 selected = &app.session.state.selected_layers;
|
||||
let layers = match (kind, app.session.active_comp()) {
|
||||
(WHIP_MATTE, Some(comp)) => video_targets(comp, LayerId(layer), selected),
|
||||
_ => edited_layers(LayerId(layer), selected),
|
||||
};
|
||||
ui.ctx().data_mut(|d| {
|
||||
d.insert_temp(pick_whip_id(), state);
|
||||
d.insert_temp(whip_layers_id(), layers);
|
||||
});
|
||||
}
|
||||
resp
|
||||
}
|
||||
@@ -1400,8 +1415,9 @@ fn switch_gesture(ui: &egui::Ui, resp: &egui::Response, row: Rect, switch: &'sta
|
||||
(a.min(b) < row.max.y && a.max(b) >= row.min.y).then_some((d.value, d.merge))
|
||||
}
|
||||
|
||||
/// The layers an edit made on `layer`'s row applies to (a switch, its label, its blending mode):
|
||||
/// the selection when the row is selected, else that layer alone, as in After Effects.
|
||||
/// The layers an edit made on `layer`'s row applies to (a switch, its label, its blending mode,
|
||||
/// its parent or track matte, moving its bar): the selection when the row is selected, else that
|
||||
/// layer alone, as in After Effects (#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] }
|
||||
}
|
||||
@@ -1427,9 +1443,10 @@ fn switch_targets(comp: &Comp, project: &Project, selected: &[LayerId], layer: u
|
||||
edited_layers(LayerId(layer), selected).into_iter().filter(|l| comp.layer(LayerId(*l)).is_some_and(|l| switch_applies(l, name, project))).collect()
|
||||
}
|
||||
|
||||
/// The layers a Modes column edit on `layer`'s row (its blending mode, Preserve Transparency)
|
||||
/// acts on: [`edited_layers`] that have video.
|
||||
fn mode_targets(comp: &Comp, layer: LayerId, selected: &[LayerId]) -> Vec<u64> {
|
||||
/// The layers a Modes column edit (blending mode, Preserve Transparency) or a Track Matte edit
|
||||
/// on `layer`'s row acts on: [`edited_layers`] that have video. Selected nulls, cameras and
|
||||
/// lights (no Modes or Track Matte on their rows) are left out.
|
||||
fn video_targets(comp: &Comp, layer: LayerId, selected: &[LayerId]) -> Vec<u64> {
|
||||
edited_layers(layer, selected).into_iter().filter(|l| comp.layer(LayerId(*l)).is_some_and(|l| l.source.is_av())).collect()
|
||||
}
|
||||
|
||||
@@ -3013,7 +3030,7 @@ fn layer_mode_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
|
||||
let opts: Vec<String> =
|
||||
BlendMode::ALL.iter().flat_map(|m| if m.ends_group() { vec![m.label().to_string(), "-".to_string()] } else { vec![m.label().to_string()] }).collect();
|
||||
if let Some(i) = widgets::popup_menu(ui, pop, mr.left_bottom(), &opts, opts.iter().position(|o| o == layer.blend_mode.label())) {
|
||||
actions.push(("layer.setBlendMode".into(), json!({"layers": mode_targets(&comp, layer.id, selected), "mode": opts[i]})));
|
||||
actions.push(("layer.setBlendMode".into(), json!({"layers": video_targets(&comp, layer.id, selected), "mode": opts[i]})));
|
||||
}
|
||||
let tr = Rect::from_center_size(pos2(cw.mode + 76.0 + 6.0, cy), vec2(14.0, 14.0));
|
||||
app.auto.add(&format!("timeline.layer.{}.preserveTransparency", layer.id.0), tr, "Preserve Transparency");
|
||||
@@ -3023,7 +3040,7 @@ fn layer_mode_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
|
||||
actions.push((
|
||||
"layer.setSwitch".into(),
|
||||
json!({
|
||||
"layers": mode_targets(&comp, layer.id, selected),
|
||||
"layers": video_targets(&comp, layer.id, selected),
|
||||
"switch": "preserveTransparency",
|
||||
"value": !layer.preserve_transparency
|
||||
}),
|
||||
@@ -3037,10 +3054,25 @@ fn layer_matte_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
|
||||
let comp = g.tl.comp.clone();
|
||||
let cw = &g.tl.cw;
|
||||
let actions = &mut g.tl.actions;
|
||||
let lp = g.lp.clone();
|
||||
let selected = &g.selected;
|
||||
let layer = rc.layer;
|
||||
let cy = rc.cy;
|
||||
// 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),
|
||||
WhipText {
|
||||
auto: &format!("timeline.layer.{}.trackMattePickWhip", layer.id.0),
|
||||
label: "Track Matte pick whip",
|
||||
hint: 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!("{} {}", comp.index_of(l.id).unwrap_or(0), l.name)).unwrap_or("None".into()),
|
||||
None => "No Track Matte".into(),
|
||||
@@ -3070,7 +3102,7 @@ fn layer_matte_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
|
||||
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(&video_targets(&comp, layer.id, selected), &ids, i, o)) {
|
||||
actions.push(a);
|
||||
}
|
||||
}
|
||||
@@ -3087,6 +3119,7 @@ fn layer_parent_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc
|
||||
let cw = &g.tl.cw;
|
||||
let actions = &mut g.tl.actions;
|
||||
let lp = g.lp.clone();
|
||||
let selected = &g.selected;
|
||||
let layer = rc.layer;
|
||||
let cy = rc.cy;
|
||||
pick_whip(
|
||||
@@ -3118,8 +3151,10 @@ fn layer_parent_cell(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc
|
||||
}
|
||||
}
|
||||
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})));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3287,7 +3322,6 @@ fn layer_bar_drag(g: &mut Rw, rc: &RowCtx<'_>, bars: &BarResps, quick: bool) {
|
||||
let selected = &g.selected;
|
||||
let ui_actions = &mut g.ui_actions;
|
||||
let layer = rc.layer;
|
||||
let is_sel = rc.is_sel;
|
||||
let drag_key = format!("bar-{}", layer.id.0);
|
||||
for (resp, kind) in [(&body, "move"), (&lin, "in"), (&lout, "out")] {
|
||||
if kind == "move" && quick {
|
||||
@@ -3303,7 +3337,7 @@ fn layer_bar_drag(g: &mut Rw, rc: &RowCtx<'_>, bars: &BarResps, quick: bool) {
|
||||
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}),
|
||||
@@ -4260,6 +4294,7 @@ fn pick_whip_drag(g: &mut Rw) {
|
||||
let t = g.tl.t;
|
||||
let graph_x0 = g.tl.graph_x0;
|
||||
let ctx = g.tl.ctx.clone();
|
||||
let comp = g.tl.comp.clone();
|
||||
let actions = &mut g.tl.actions;
|
||||
let hit_rows = &mut g.hit_rows;
|
||||
let dim_hits = &mut g.dim_hits;
|
||||
@@ -4272,7 +4307,7 @@ fn pick_whip_drag(g: &mut Rw) {
|
||||
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)
|
||||
@@ -4283,10 +4318,13 @@ fn pick_whip_drag(g: &mut Rw) {
|
||||
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));
|
||||
@@ -4299,7 +4337,11 @@ fn pick_whip_drag(g: &mut Rw) {
|
||||
};
|
||||
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.
|
||||
@@ -5141,19 +5183,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 {
|
||||
|
||||
@@ -79,7 +79,7 @@ fn a_track_matte_mode_without_a_matte_takes_the_layer_above() {
|
||||
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();
|
||||
@@ -97,7 +97,7 @@ fn a_track_matte_mode_without_a_matte_takes_the_layer_above() {
|
||||
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.
|
||||
|
||||
@@ -8,6 +8,7 @@ mod ui_agent_audit;
|
||||
mod ui_angles;
|
||||
mod ui_audio_scrub;
|
||||
mod ui_azerty_shortcuts;
|
||||
mod ui_batch_links;
|
||||
mod ui_batch_switches;
|
||||
mod ui_blank_context_menus;
|
||||
mod ui_camera_tracker;
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
//! 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);
|
||||
// Other rows' menus show their choice as well ("No Track Matte"): the entry is a node the
|
||||
// opened menu added.
|
||||
let shown: Vec<Rect> = h.query_all_by_label(item).map(|n| n.rect()).collect();
|
||||
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()).filter(|r| !shown.contains(r)).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))]);
|
||||
}
|
||||
|
||||
fn video(h: &Harness<'_, EffectcraftApp>, id: u64) -> bool {
|
||||
h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().switches.video
|
||||
}
|
||||
|
||||
/// A locked layer in the selection stays selected, and keeps its parent, its transform and its
|
||||
/// matte while another selected row's menus and pick whips change the rest.
|
||||
#[test]
|
||||
fn locked_selected_layers_keep_their_parent_and_matte() {
|
||||
let (mut h, [a, b, c, _, x, _]) = harness();
|
||||
select(&mut h, &[a, b]);
|
||||
// B alone (a click on its Lock switch locks the whole selection).
|
||||
h.state_mut().session.execute("layer.setSwitch", json!({"layers": [b], "switch": "lock", "value": true})).unwrap();
|
||||
h.run_steps(2);
|
||||
let selected: Vec<u64> = h.state().session.state.selected_layers.iter().map(|l| l.0).collect();
|
||||
assert!(h.state().session.active_comp().unwrap().layer(LayerId(b)).unwrap().switches.locked && selected == vec![a, b], "{selected:?}");
|
||||
let b_position = position(&h, b);
|
||||
choose(&mut h, &format!("timeline.layer.{a}.parent"), "3. P");
|
||||
whip(&mut h, &format!("timeline.layer.{a}.pickWhip"), c);
|
||||
choose(&mut h, &format!("timeline.layer.{a}.trackMatte"), "1. Y");
|
||||
whip(&mut h, &format!("timeline.layer.{a}.trackMattePickWhip"), x);
|
||||
assert_eq!((parent(&h, a), matte(&h, a)), (Some(c), Some((x, MatteKind::Alpha))), "A changed each time");
|
||||
assert_eq!((parent(&h, b), matte(&h, b), position(&h, b)), (None, None, b_position), "locked B didn't");
|
||||
}
|
||||
|
||||
/// Selected nulls (no Track Matte on their rows) get no matte from another row's menu or pick
|
||||
/// whip: at the top of the comp one used to refuse the whole mode change (nothing above it to
|
||||
/// take as a matte), at the bottom it took the layer above as its matte and hid it.
|
||||
#[test]
|
||||
fn selected_nulls_get_no_matte() {
|
||||
let (mut h, [a, b, _, _, x, y]) = harness();
|
||||
h.state_mut().session.execute("layer.setTrackMatte", json!({"layers": [a, b], "matte": x, "kind": "alpha"})).unwrap();
|
||||
let n = h.state_mut().session.execute("layer.newNull", json!({"name": "N"})).unwrap()["layer"].as_u64().unwrap();
|
||||
select(&mut h, &[a, b, n]);
|
||||
choose(&mut h, &format!("timeline.layer.{a}.trackMatte"), "Luma Matte");
|
||||
assert_eq!([matte(&h, a), matte(&h, b), matte(&h, n)], [Some((x, MatteKind::Luma)), Some((x, MatteKind::Luma)), None], "a null on top");
|
||||
h.state_mut().session.execute("layer.arrange", json!({"layers": [n], "to": "back"})).unwrap();
|
||||
select(&mut h, &[a, b, n]);
|
||||
choose(&mut h, &format!("timeline.layer.{a}.trackMatte"), "Alpha Inverted Matte");
|
||||
assert_eq!(
|
||||
[matte(&h, a), matte(&h, b), matte(&h, n)],
|
||||
[Some((x, MatteKind::AlphaInverted)), Some((x, MatteKind::AlphaInverted)), None],
|
||||
"a null at the bottom"
|
||||
);
|
||||
assert!(video(&h, a), "A wasn't hidden as the null's matte");
|
||||
whip(&mut h, &format!("timeline.layer.{a}.trackMattePickWhip"), y);
|
||||
assert_eq!(
|
||||
[matte(&h, a), matte(&h, b), matte(&h, n)],
|
||||
[Some((y, MatteKind::AlphaInverted)), Some((y, MatteKind::AlphaInverted)), None],
|
||||
"the pick whip too"
|
||||
);
|
||||
}
|
||||
@@ -622,6 +622,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
|
||||
|
||||
Reference in New Issue
Block a user