From 4bc619b010dd9eb633ecf99fdba712f52dc400dd Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:58:22 +0700 Subject: [PATCH] fix(script): deselect individual properties via Property.selected=false (#548) --- crates/engine/src/commands/prop.rs | 14 ++++++++++++++ crates/script/src/prelude.js | 4 +++- crates/script/src/tests.rs | 29 +++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/commands/prop.rs b/crates/engine/src/commands/prop.rs index 063b179f..04585063 100644 --- a/crates/engine/src/commands/prop.rs +++ b/crates/engine/src/commands/prop.rs @@ -346,6 +346,19 @@ fn select_prop(s: &mut Session, p: &Value) -> Result { Ok(Value::Null) } +/// Deselect only the named property, preserving unrelated property, layer, key and +/// mask-vertex selections. Selected keys also imply selectedProperties in the script API. +fn deselect_prop(s: &mut Session, params: &Value) -> Result { + let (_, lid, uid) = prop_ref(s, params, "prop.deselect")?; + let mask = s.active_comp().and_then(|c| c.layer(lid)).and_then(|l| path_points(l, uid, l.layer_time(s.time()))).map(|(mask, _)| mask); + s.state.selected_props.retain(|(layer, prop)| *layer != lid || *prop != uid); + s.state.selected_keys.retain(|key| key.layer != lid || key.prop != uid); + if let Some(mask) = mask { + s.state.selected_vertices.retain(|v| v.layer != lid || v.mask != mask); + } + Ok(Value::Null) +} + /// The mask or shape Path item that `uid` (the group, or its Path property) is, with its number /// of points at layer time `lt`. fn path_points(l: &Layer, uid: Uid, lt: Tick) -> Option<(Uid, usize)> { @@ -820,6 +833,7 @@ pub fn specs() -> Vec { reset ), cmd!("prop.select", "Select Property", [], None, "{layer?, path|prop, add?, selectKeys?}", has_layers, select_prop), + cmd!("prop.deselect", "Deselect Property", [], None, "{layer?, path|prop}", has_layers, deselect_prop), cmd!( "prop.convertExpressionToKeyframes", "Convert Expression to Keyframes", diff --git a/crates/script/src/prelude.js b/crates/script/src/prelude.js index f52765e7..0e9b088a 100644 --- a/crates/script/src/prelude.js +++ b/crates/script/src/prelude.js @@ -574,7 +574,9 @@ PropertyBase.prototype = { return false; }, set selected(v) { - if (v) __call("prop.select", { comp: this.__layer.__comp, layer: this.__layer.__id, prop: this.__uid, add: true }); + var ref = { comp: this.__layer.__comp, layer: this.__layer.__id, prop: this.__uid }; + if (v) __call("prop.select", { comp: ref.comp, layer: ref.layer, prop: ref.prop, add: true }); + else __call("prop.deselect", ref); }, propertyGroup: function (n) { var p = this; diff --git a/crates/script/src/tests.rs b/crates/script/src/tests.rs index 10deb79b..7d7e4616 100644 --- a/crates/script/src/tests.rs +++ b/crates/script/src/tests.rs @@ -524,3 +524,32 @@ fn comp_preserves_nested_frame_rate_and_resolution() { let (_, c) = comp_named(&s, "Nested"); assert!(c.preserve_frame_rate && c.preserve_resolution); } + +#[test] +fn property_selected_false_preserves_other_property_and_key_selections() { + let mut s = session(); + let out = ok( + &mut s, + r#" + var c = app.project.items.addComp("Selection", 64, 64, 1, 1, 10); + c.openInViewer(); + var layer = c.layers.addSolid([1, 0, 0], "Layer", 8, 8, 1); + var opacity = layer.transform.opacity; + var rotation = layer.transform.rotation; + rotation.setValueAtTime(0, 0); + rotation.setValueAtTime(0.5, 45); + app.run("edit.deselectAll", {}); + rotation.selected = true; + opacity.selected = true; + var states = [[opacity.selected, rotation.selected]]; + opacity.selected = false; + states.push([opacity.selected, rotation.selected]); + opacity.selected = false; + states.push([opacity.selected, rotation.selected]); + opacity.selected = true; + states.push([opacity.selected, rotation.selected]); + [states, rotation.selectedKeys.length, c.selectedProperties.length]; + "#, + ); + assert_eq!(out.result, json!([[[true, true], [false, true], [false, true], [true, true]], 2, 2])); +}