mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #1037 from vbasky/fix/1001-selection-persist
# Conflicts: # crates/engine/src/lib.rs
This commit is contained in:
commit
286a521598
5 files changed
+149
-15
No files matched your search
@@ -154,10 +154,11 @@ fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let id = comp_id(s, p)?;
|
||||
s.state.open_comps.retain(|c| *c != id);
|
||||
if s.state.active_comp == Some(id) {
|
||||
s.state.active_comp = s.state.open_comps.last().copied();
|
||||
s.state.selected_layers.clear();
|
||||
s.state.selected_keys.clear();
|
||||
s.state.selected_props.clear();
|
||||
if let Some(next) = s.state.open_comps.last().copied() {
|
||||
s.activate_comp(next);
|
||||
} else {
|
||||
s.activate_comp_none();
|
||||
}
|
||||
}
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
@@ -194,6 +194,22 @@ pub struct VertexRef {
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
/// One composition's selection. The active comp's copy lives in [`EditorState::selected_layers`]
|
||||
/// (and the property, key, and vertex lists); every other comp keeps its own here (#1001).
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CompSelection {
|
||||
pub layers: Vec<LayerId>,
|
||||
pub props: Vec<(LayerId, Uid)>,
|
||||
pub keys: Vec<KeyRef>,
|
||||
pub vertices: Vec<VertexRef>,
|
||||
}
|
||||
|
||||
impl CompSelection {
|
||||
fn is_empty(&self) -> bool {
|
||||
self.layers.is_empty() && self.props.is_empty() && self.keys.is_empty() && self.vertices.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Editing state that commands depend on (headless-relevant, serde for agents).
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct EditorState {
|
||||
@@ -205,7 +221,11 @@ pub struct EditorState {
|
||||
pub recent_comps: Vec<ItemId>,
|
||||
/// Current time per comp (the CTI).
|
||||
pub times: std::collections::BTreeMap<ItemId, Tick>,
|
||||
/// Layer selection of the active comp. Other comps' selections are in [`Self::comp_selections`].
|
||||
pub selected_layers: Vec<LayerId>,
|
||||
/// Selections of comps that are not active, keyed by comp id (#1001).
|
||||
#[serde(default)]
|
||||
pub comp_selections: std::collections::BTreeMap<ItemId, CompSelection>,
|
||||
/// Selected properties / groups (by uid) of selected layers.
|
||||
pub selected_props: Vec<(LayerId, Uid)>,
|
||||
pub selected_keys: Vec<KeyRef>,
|
||||
@@ -760,12 +780,26 @@ impl Session {
|
||||
true
|
||||
}
|
||||
|
||||
/// Drop selections that no longer exist.
|
||||
/// Drop selections that no longer exist. Each comp keeps its own selection (#1001); a layer id
|
||||
/// counts only inside the comp that owns it, so the active comp never sees another comp's layers.
|
||||
pub fn sanitize_state(&mut self) {
|
||||
let p = self.project.clone();
|
||||
self.state.open_comps.retain(|c| p.comp(*c).is_some());
|
||||
self.state.comp_selections.retain(|cid, sel| {
|
||||
let Some(c) = p.comp(*cid) else { return false };
|
||||
let ids: Vec<LayerId> = c.layers.iter().map(|l| l.id).collect();
|
||||
sel.layers.retain(|l| ids.contains(l));
|
||||
sel.props.retain(|(l, _)| ids.contains(l));
|
||||
sel.keys.retain(|k| ids.contains(&k.layer));
|
||||
sel.vertices.retain(|v| ids.contains(&v.layer));
|
||||
!sel.is_empty()
|
||||
});
|
||||
if self.state.active_comp.is_some_and(|c| p.comp(c).is_none()) {
|
||||
self.clear_live_selection();
|
||||
self.state.active_comp = self.state.open_comps.first().copied();
|
||||
if let Some(id) = self.state.active_comp {
|
||||
self.recall_selection(id);
|
||||
}
|
||||
}
|
||||
if let Some(c) = self.active_comp() {
|
||||
let ids: Vec<LayerId> = c.layers.iter().map(|l| l.id).collect();
|
||||
@@ -873,6 +907,59 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
/// Make `id` the active comp, parking the previous comp's selection and restoring this one's.
|
||||
pub fn activate_comp(&mut self, id: ItemId) {
|
||||
if self.project.comp(id).is_none() || self.state.active_comp == Some(id) {
|
||||
return;
|
||||
}
|
||||
self.remember_selection();
|
||||
self.state.active_comp = Some(id);
|
||||
self.recall_selection(id);
|
||||
self.state.current_track = None;
|
||||
}
|
||||
|
||||
/// Leave no comp active, keeping the one that was active in [`EditorState::comp_selections`].
|
||||
pub fn activate_comp_none(&mut self) {
|
||||
self.remember_selection();
|
||||
self.state.active_comp = None;
|
||||
self.clear_live_selection();
|
||||
self.state.current_track = None;
|
||||
}
|
||||
|
||||
fn remember_selection(&mut self) {
|
||||
let Some(id) = self.state.active_comp else { return };
|
||||
let sel = CompSelection {
|
||||
layers: self.state.selected_layers.clone(),
|
||||
props: self.state.selected_props.clone(),
|
||||
keys: self.state.selected_keys.clone(),
|
||||
vertices: self.state.selected_vertices.clone(),
|
||||
};
|
||||
if sel.is_empty() {
|
||||
self.state.comp_selections.remove(&id);
|
||||
} else {
|
||||
self.state.comp_selections.insert(id, sel);
|
||||
}
|
||||
}
|
||||
|
||||
fn recall_selection(&mut self, id: ItemId) {
|
||||
match self.state.comp_selections.remove(&id) {
|
||||
Some(sel) => {
|
||||
self.state.selected_layers = sel.layers;
|
||||
self.state.selected_props = sel.props;
|
||||
self.state.selected_keys = sel.keys;
|
||||
self.state.selected_vertices = sel.vertices;
|
||||
}
|
||||
None => self.clear_live_selection(),
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_live_selection(&mut self) {
|
||||
self.state.selected_layers.clear();
|
||||
self.state.selected_props.clear();
|
||||
self.state.selected_keys.clear();
|
||||
self.state.selected_vertices.clear();
|
||||
}
|
||||
|
||||
/// Open a comp in the viewer/timeline and make it active.
|
||||
pub fn open_comp(&mut self, id: ItemId) {
|
||||
if self.project.comp(id).is_none() {
|
||||
@@ -888,13 +975,8 @@ impl Session {
|
||||
self.state.recent_comps.insert(0, previous);
|
||||
self.state.recent_comps.truncate(32);
|
||||
}
|
||||
self.state.active_comp = Some(id);
|
||||
self.state.selected_layers.clear();
|
||||
self.state.selected_props.clear();
|
||||
self.state.selected_keys.clear();
|
||||
self.state.selected_vertices.clear();
|
||||
self.state.current_track = None;
|
||||
}
|
||||
self.activate_comp(id);
|
||||
self.events.push(Event::OpenComp(id));
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,27 @@ fn label_index(l: Label) -> usize {
|
||||
/// A comp's selected properties as `[layer, uid]`: the selected ones, then those with selected
|
||||
/// keyframes (selecting a keyframe selects its property in After Effects, so scripts find
|
||||
/// selected keys through `selectedProperties`).
|
||||
/// Layer ids selected in `comp`, including a comp that is not the active one (#1001).
|
||||
fn selected_layer_ids(s: &Session, comp: effectcraft_project::ItemId) -> Vec<u64> {
|
||||
let layers = if s.active_comp_id() == Some(comp) {
|
||||
&s.state.selected_layers
|
||||
} else {
|
||||
match s.state.comp_selections.get(&comp) {
|
||||
Some(sel) => &sel.layers,
|
||||
None => return Vec::new(),
|
||||
}
|
||||
};
|
||||
let Some(c) = s.project.comp(comp) else { return Vec::new() };
|
||||
layers.iter().filter(|l| c.layer(**l).is_some()).map(|l| l.0).collect()
|
||||
}
|
||||
|
||||
fn layer_selected(s: &Session, comp: effectcraft_project::ItemId, layer: effectcraft_project::LayerId) -> bool {
|
||||
if s.active_comp_id() == Some(comp) {
|
||||
return s.state.selected_layers.contains(&layer);
|
||||
}
|
||||
s.state.comp_selections.get(&comp).is_some_and(|sel| sel.layers.contains(&layer))
|
||||
}
|
||||
|
||||
fn selected_props(s: &Session, c: &effectcraft_project::Comp) -> Vec<J> {
|
||||
let mut v: Vec<(u64, u64)> = vec![];
|
||||
let keyed = s.state.selected_keys.iter().map(|k| (k.layer, k.prop));
|
||||
@@ -135,7 +156,7 @@ fn item(s: &Session, a: &J) -> R {
|
||||
ItemKind::Comp(c) => {
|
||||
let cid = id;
|
||||
o["kind"] = json!("comp");
|
||||
let sel: Vec<u64> = s.state.selected_layers.iter().filter(|l| c.layer(**l).is_some()).map(|l| l.0).collect();
|
||||
let sel = selected_layer_ids(s, cid);
|
||||
let fps = c.frame_rate.as_f64();
|
||||
if let Some(m) = o.as_object_mut() {
|
||||
m.extend(
|
||||
@@ -246,7 +267,7 @@ fn layer(s: &Session, a: &J) -> R {
|
||||
"isTrackMatte": c.layers.iter().any(|x| x.track_matte.is_some_and(|m| m.layer == l.id)),
|
||||
"label": label_index(l.label),
|
||||
"comment": l.comment,
|
||||
"selected": s.state.selected_layers.contains(&l.id),
|
||||
"selected": layer_selected(s, cid, l.id),
|
||||
"autoOrient": format!("{:?}", l.auto_orient),
|
||||
"width": w, "height": h,
|
||||
"hasVideo": l.source.is_av(),
|
||||
|
||||
@@ -151,7 +151,7 @@ pub(crate) fn exec(s: &mut Session, id: &str, params: J) -> Result<J, String> {
|
||||
if let Some(c) = target
|
||||
&& prev != Some(c)
|
||||
{
|
||||
s.state.active_comp = Some(c);
|
||||
s.activate_comp(c);
|
||||
}
|
||||
if CREATES_LAYER.contains(&id) {
|
||||
s.state.selected_layers.clear();
|
||||
@@ -163,7 +163,7 @@ pub(crate) fn exec(s: &mut Session, id: &str, params: J) -> Result<J, String> {
|
||||
&& id != "comp.open"
|
||||
{
|
||||
match prev {
|
||||
Some(p) if s.project.comp(p).is_some() => s.state.active_comp = Some(p),
|
||||
Some(p) if s.project.comp(p).is_some() => s.activate_comp(p),
|
||||
_ => {
|
||||
if !s.state.open_comps.contains(&c) {
|
||||
s.state.open_comps.push(c);
|
||||
|
||||
@@ -766,6 +766,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 + " " + a.selectedLayers.length
|
||||
"#,
|
||||
);
|
||||
assert_eq!(out.result, json!("1 true 0"));
|
||||
// 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 {
|
||||
|
||||
Reference in new issue
Block a user