Expressions: Add Expression applies to the selected properties (#476)
This commit is contained in:
parent
30aaddf7c2
commit
56cd3f1bf5
|
|
@ -5,7 +5,7 @@ use effectcraft_project::{GroupKind, ItemId, Layer, LayerId, Property, Uid};
|
|||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, b_p, bad, f_p, has_comp, has_keys, has_layers, layer_mut, layer_p, layers_p, merge_p, str_p};
|
||||
use super::{CommandSpec, b_p, bad, comp_id, f_p, has_comp, has_keys, has_layers, layer_mut, layer_p, layers_p, merge_p, selected_leaf_props, str_p};
|
||||
use crate::{EngineError, KeyRef, Result, Session, VertexRef, cmd, query};
|
||||
|
||||
/// Resolve `{layer, path}` (or `{layer, prop: uid}`) to (comp, layer, prop uid).
|
||||
|
|
@ -233,8 +233,38 @@ fn toggle_transform_key(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
Ok(Value::Array(added))
|
||||
}
|
||||
|
||||
/// Add, toggle or set one property's expression (`def` is the starting text of a new one).
|
||||
fn apply_expr(pr: &mut Property, text: &Option<String>, enabled: Option<bool>, def: &str) {
|
||||
match (text, enabled) {
|
||||
(Some(t), _) if t.trim().is_empty() => pr.expr = None,
|
||||
(Some(t), e) => pr.expr = Some(effectcraft_project::Expression { text: t.clone(), enabled: e.unwrap_or(true) }),
|
||||
(None, Some(e)) => {
|
||||
if let Some(x) = &mut pr.expr {
|
||||
x.enabled = e;
|
||||
} else if e {
|
||||
pr.expr = Some(effectcraft_project::Expression { text: def.to_string(), enabled: true });
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
pr.expr = if pr.expr.is_some() { None } else { Some(effectcraft_project::Expression { text: def.to_string(), enabled: true }) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `{layer?, path|prop, ...}` for one property, or (Animation > Add Expression) no `path`/`prop`
|
||||
/// for the properties selected in the timeline, all in one undo step.
|
||||
fn set_expr(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid, uid) = prop_ref(s, p, "prop.setExpression")?;
|
||||
const C: &str = "prop.setExpression";
|
||||
let explicit = p.get("path").is_some() || p.get("prop").is_some();
|
||||
let targets: Vec<(ItemId, LayerId, Uid)> = if explicit {
|
||||
vec![prop_ref(s, p, C)?]
|
||||
} else {
|
||||
let cid = comp_id(s, p)?;
|
||||
selected_leaf_props(s).into_iter().map(|(l, u)| (cid, l, u)).collect()
|
||||
};
|
||||
if targets.is_empty() {
|
||||
return Err(bad(C, "select a property in the timeline (or give `layer` and `path`)"));
|
||||
}
|
||||
let text = p.get("expression").and_then(Value::as_str).map(str::to_string);
|
||||
let mut enabled = b_p(p, "enabled");
|
||||
// A syntax error keeps the text but disables the expression (AE shows the warning bar).
|
||||
|
|
@ -246,24 +276,22 @@ fn set_expr(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
enabled = Some(false);
|
||||
}
|
||||
// Alt-click on a stopwatch starts with the property's own reference (`transform.opacity`).
|
||||
let def = s.project.comp(cid).and_then(|c| c.layer(lid).and_then(|l| super::link::reference(c, l, l, uid, true))).unwrap_or_else(|| "value".into());
|
||||
let r = with_prop(s, "Expression", merge_p(p), cid, lid, uid, |pr, _| {
|
||||
match (&text, enabled) {
|
||||
(Some(t), _) if t.trim().is_empty() => pr.expr = None,
|
||||
(Some(t), e) => pr.expr = Some(effectcraft_project::Expression { text: t.clone(), enabled: e.unwrap_or(true) }),
|
||||
(None, Some(e)) => {
|
||||
if let Some(x) = &mut pr.expr {
|
||||
x.enabled = e;
|
||||
} else if e {
|
||||
pr.expr = Some(effectcraft_project::Expression { text: def.clone(), enabled: true });
|
||||
}
|
||||
}
|
||||
(None, None) => {
|
||||
pr.expr = if pr.expr.is_some() { None } else { Some(effectcraft_project::Expression { text: def.clone(), enabled: true }) };
|
||||
}
|
||||
let defs: Vec<String> = targets
|
||||
.iter()
|
||||
.map(|(cid, lid, uid)| {
|
||||
s.project.comp(*cid).and_then(|c| c.layer(*lid).and_then(|l| super::link::reference(c, l, l, *uid, true))).unwrap_or_else(|| "value".into())
|
||||
})
|
||||
.collect();
|
||||
let results = s.edit("Expression", merge_p(p), |proj, _| {
|
||||
let mut out = vec![];
|
||||
for ((cid, lid, uid), def) in targets.iter().zip(&defs) {
|
||||
let pr = layer_mut(proj, *cid, *lid)?.props.find_mut(*uid).ok_or_else(|| bad(C, "property vanished"))?;
|
||||
apply_expr(pr, &text, enabled, def);
|
||||
out.push(json!(pr.expr.as_ref().map(|e| e.text.clone())));
|
||||
}
|
||||
Ok(json!(pr.expr.as_ref().map(|e| e.text.clone())))
|
||||
Ok(out)
|
||||
})?;
|
||||
let r = if explicit { results.into_iter().next().unwrap_or(Value::Null) } else { Value::Array(results) };
|
||||
if let Some(e) = error {
|
||||
s.toast(format!("Expression disabled: {e}"));
|
||||
return Ok(json!({"expression": r, "error": e}));
|
||||
|
|
@ -809,7 +837,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
|||
has_layers,
|
||||
toggle_transform_key
|
||||
),
|
||||
cmd!("prop.setExpression", "Add Expression", ["Animation"], Some("Alt+Shift+="), "{layer?, path|prop, expression?, enabled?}", has_layers, set_expr),
|
||||
cmd!(
|
||||
"prop.setExpression",
|
||||
"Add Expression",
|
||||
["Animation"],
|
||||
Some("Alt+Shift+="),
|
||||
"{layer?, path|prop?, expression?, enabled?} (no path/prop: the selected properties)",
|
||||
has_layers,
|
||||
set_expr
|
||||
),
|
||||
cmd!(
|
||||
"prop.reset",
|
||||
"Reset Property",
|
||||
|
|
|
|||
|
|
@ -322,6 +322,22 @@ fn alt_click_stopwatch_toggles_a_self_reference_expression() {
|
|||
assert!(prop(&s, l, "transform/opacity").expr.unwrap().enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_expression_without_path_uses_the_selected_property() {
|
||||
let (mut s, l) = setup();
|
||||
// Nothing selected: the same hint as Add Property to Essential Graphics.
|
||||
let err = s.execute("prop.setExpression", json!({})).unwrap_err().to_string();
|
||||
assert!(err.contains("select a property in the timeline"), "{err}");
|
||||
assert!(prop(&s, l, "transform/opacity").expr.is_none());
|
||||
// Animation > Add Expression sends no parameters.
|
||||
s.execute("prop.select", json!({"layer": l, "path": "transform/opacity"})).unwrap();
|
||||
s.execute("prop.setExpression", json!({})).unwrap();
|
||||
assert_eq!(prop(&s, l, "transform/opacity").expr.unwrap().text, "transform.opacity");
|
||||
// One undo step removes it.
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(prop(&s, l, "transform/opacity").expr.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layer_comp_and_key_times_stay_frame_aligned() {
|
||||
let aligned = |fr: effectcraft_time::FrameRate, t: effectcraft_time::Tick| fr.snap(t) == t;
|
||||
|
|
|
|||
Loading…
Reference in New Issue