Keep closed comps' layer selections across scripts (#1001)

sanitize_state dropped every selected layer not in the active comp at the
end of each script run, so a layer selected by script in a comp that was not
open was gone by the next script call. Every comp keeps its own selection as
in After Effects; only ids gone from every comp are dropped now. UI and
engine lookups resolve selections per comp and tolerate foreign ids.
This commit is contained in:
Vikram Bhaskaran committed 2026-10-13 17:39:16 +11:00
1 parent 470ea7a3af
commit 8c986a1bec
2 files changed
+35 -3

No files matched your search

+5 -3
View File
@@ -757,15 +757,17 @@ impl Session {
true
}
/// Drop selections that no longer exist.
/// Drop selections that no longer exist. Every comp keeps its own layer selection (After
/// Effects), so selections in comps that are not open survive; only ids that are gone from
/// every comp are dropped (#1001).
pub fn sanitize_state(&mut self) {
let p = self.project.clone();
self.state.open_comps.retain(|c| p.comp(*c).is_some());
if self.state.active_comp.is_some_and(|c| p.comp(c).is_none()) {
self.state.active_comp = self.state.open_comps.first().copied();
}
if let Some(c) = self.active_comp() {
let ids: Vec<LayerId> = c.layers.iter().map(|l| l.id).collect();
{
let ids: Vec<LayerId> = p.comps().flat_map(|(_, c)| c.layers.iter().map(|l| l.id)).collect();
self.state.selected_layers.retain(|l| ids.contains(l));
self.state.selected_props.retain(|(l, _)| ids.contains(l));
self.state.selected_keys.retain(|k| ids.contains(&k.layer));
+30
View File
@@ -698,6 +698,36 @@ fn group_selected_false_deselects_masks_and_effects() {
assert_eq!(out.result, json!([[true, true], [false, true], [false, false]]));
}
/// A layer selected by script in a comp that is not open stays selected for the next script:
/// every comp keeps its own layer selection (#1001).
#[test]
fn layer_selection_in_a_closed_comp_survives_to_the_next_script() {
let mut s = session();
let out = ok(
&mut s,
r#"
var a = app.project.items.addComp("Active", 400, 300, 1, 2, 25);
a.openInViewer();
var c = app.project.items.addComp("Other", 400, 300, 1, 2, 25);
c.layers.addText("A");
c.layers.addText("B");
c.layer("B").selected = true;
c.selectedLayers.length + " " + c.layer("B").selected
"#,
);
assert_eq!(out.result, json!("1 true"));
// A second script call, the way a panel talks to the app over the control channel.
let out = ok(
&mut s,
r#"
var c = null;
for (var k = 1; k <= app.project.numItems; k++) { if (app.project.item(k).name === "Other") c = app.project.item(k); }
c.selectedLayers.length + " " + c.layer("B").selected
"#,
);
assert_eq!(out.result, json!("1 true"));
}
/// Probes every file as a 64x32 still (the media layer's single-file probe).
struct Stills;
impl effectcraft_engine::Importer for Stills {