mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
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:
@@ -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: {}"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))]);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user