From 6f57b9eee3632adeecf106d8ccf80ff5aad0bdba Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Tue, 13 Oct 2026 12:19:35 -0400 Subject: [PATCH] Parent and Track Matte: locked layers keep theirs (#1260) With several layers set at once from one selected row, a selected layer locked after it was selected (its Lock switch doesn't deselect it) had its parent, its compensated transform or its matte changed too. layer.setParent, layer.setTrackMatte and layer.trackMatte now leave locked layers as they are, the rule the other layer edits follow (unlocked): among several, the others change; alone, it is an error that changes nothing. Done in the engine, a layer locked while a pick whip is being dragged is covered too. Test: locked_layers_keep_their_parent_and_matte. --- crates/engine/src/commands/layer.rs | 34 ++++++++++++++++++++++-- crates/engine/src/commands/layer_menu.rs | 1 + 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/commands/layer.rs b/crates/engine/src/commands/layer.rs index 4ccfbe3e6..bbaa87c86 100644 --- a/crates/engine/src/commands/layer.rs +++ b/crates/engine/src/commands/layer.rs @@ -551,8 +551,8 @@ fn blend(s: &mut Session, p: &Value) -> Result { Ok(Value::Null) } -/// `layer.setTrackMatte`: `layers` (or `layer`, else the first selected layer) use `matte` as -/// their track matte (none when null), in one undo step. `kind` sets the mode; `preserve` keeps +/// `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. fn track_matte(s: &mut Session, p: &Value) -> Result { @@ -560,6 +560,8 @@ fn track_matte(s: &mut Session, p: &Value) -> Result { 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, @@ -906,6 +908,8 @@ fn check_parent_chain(comp: &Comp, ids: &[LayerId], parent: Option, max fn set_parent(s: &mut Session, p: &Value) -> Result { 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, @@ -1922,4 +1926,30 @@ mod track_matte_batch_tests { 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"); + } + } } diff --git a/crates/engine/src/commands/layer_menu.rs b/crates/engine/src/commands/layer_menu.rs index 7737b8afb..898e5d4f9 100644 --- a/crates/engine/src/commands/layer_menu.rs +++ b/crates/engine/src/commands/layer_menu.rs @@ -609,6 +609,7 @@ fn delete_all_markers(s: &mut Session, p: &Value) -> Result { fn track_matte_op(s: &mut Session, p: &Value) -> Result { 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),