Files
effectcraft/crates/engine/src/commands/effect.rs
T

165 lines
7.2 KiB
Rust

//! Effect menu and Effect Controls gestures.
use effectcraft_project::Uid;
use effectcraft_project::build::Ids;
use serde_json::{Value, json};
use super::{CommandSpec, b_p, bad, has_layers, layer_mut, layers_p, str_p};
use crate::{EngineError, Result, Session, cmd};
fn apply(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_p(p, "effect").ok_or_else(|| bad("effect.apply", "missing `effect` (id or name)"))?;
let spec = effectcraft_effects::lookup(name).ok_or_else(|| bad("effect.apply", format!("unknown effect `{name}`")))?;
let (cid, ids) = layers_p(s, p)?;
if ids.is_empty() {
return Err(bad("effect.apply", "no layer"));
}
let sizes: Vec<_> = {
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
ids.iter()
.filter_map(|id| comp.layer(*id))
.map(|l| {
let (w, h) = effectcraft_render::source_size(&s.project, l);
(l.id, if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] })
})
.collect()
};
let uids = s.edit(&format!("Apply {}", spec.name), None, |proj, st| {
let mut out = vec![];
for (lid, size) in &sizes {
let mut next = proj.next_id;
let l = layer_mut(proj, cid, *lid)?;
let fx = l.props.sub_mut("effects").ok_or_else(|| bad("effect.apply", "this layer type has no effects"))?;
let same = fx.groups().filter(|g| g.match_id == spec.id).count();
let name = if same == 0 { spec.name.to_string() } else { format!("{} {}", spec.name, same + 1) };
let g = effectcraft_effects::instantiate(spec, &mut Ids(&mut next), &name, *size);
out.push(g.uid);
fx.children.push(g.into());
proj.next_id = next;
st.selected_props = vec![(*lid, *out.last().unwrap_or(&0))];
}
st.last_effect = Some(spec.id.to_string());
Ok(out)
})?;
Ok(json!({"effects": uids}))
}
fn find_fx(s: &Session, p: &Value, cmd: &str) -> Result<(effectcraft_project::ItemId, effectcraft_project::LayerId, Uid)> {
let (cid, lid) = super::layer_p(s, p, cmd)?;
let layer = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
let fx = layer.effects().ok_or_else(|| bad(cmd, "no effects"))?;
let g = match p.get("effect") {
Some(Value::Number(n)) => {
let n = n.as_u64().unwrap_or(0);
fx.groups().find(|g| g.uid == n).or_else(|| fx.groups().nth(n.saturating_sub(1) as usize))
}
Some(Value::String(name)) => fx.groups().find(|g| &g.name == name || g.match_id == *name),
_ => s.state.selected_props.iter().find_map(|(l, u)| (*l == lid).then(|| fx.groups().find(|g| g.uid == *u)).flatten()),
}
.ok_or_else(|| bad(cmd, "no such effect"))?;
Ok((cid, lid, g.uid))
}
fn remove(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_fx(s, p, "effect.remove")?;
s.edit("Remove Effect", None, |proj, st| {
let l = layer_mut(proj, cid, lid)?;
if let Some(fx) = l.props.sub_mut("effects") {
fx.children.retain(|c| c.uid() != uid);
}
st.selected_props.retain(|(_, u)| *u != uid);
Ok(())
})?;
Ok(Value::Null)
}
fn remove_all(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
s.edit("Remove All Effects", None, |proj, _| {
for lid in &ids {
if let Some(fx) = layer_mut(proj, cid, *lid)?.props.sub_mut("effects") {
fx.children.clear();
}
}
Ok(())
})?;
Ok(Value::Null)
}
fn toggle(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_fx(s, p, "effect.toggle")?;
let v = b_p(p, "value");
let r = s.edit("Toggle Effect", None, |proj, _| {
let l = layer_mut(proj, cid, lid)?;
let g = l.props.find_group_mut(uid).ok_or_else(|| bad("effect.toggle", "gone"))?;
g.enabled = v.unwrap_or(!g.enabled);
Ok(g.enabled)
})?;
Ok(json!(r))
}
fn reorder(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_fx(s, p, "effect.reorder")?;
let to = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("effect.reorder", "missing `index` (1-based)"))? as usize;
s.edit("Reorder Effect", None, |proj, _| {
let fx = layer_mut(proj, cid, lid)?.props.sub_mut("effects").ok_or_else(|| bad("effect.reorder", "no effects"))?;
let Some(i) = fx.children.iter().position(|c| c.uid() == uid) else { return Ok(()) };
let g = fx.children.remove(i);
let to = to.saturating_sub(1).min(fx.children.len());
fx.children.insert(to, g);
Ok(())
})?;
Ok(Value::Null)
}
fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid, uid) = find_fx(s, p, "effect.duplicate")?;
let r = s.edit("Duplicate Effect", None, |proj, _| {
let mut next = proj.next_id;
let fx = layer_mut(proj, cid, lid)?.props.sub_mut("effects").ok_or_else(|| bad("effect.duplicate", "no effects"))?;
let Some(i) = fx.children.iter().position(|c| c.uid() == uid) else { return Ok(0) };
let mut g = fx.children[i].as_group().cloned().ok_or_else(|| bad("effect.duplicate", "not a group"))?;
g.reassign_uids(&mut next);
g.name = format!("{} 2", g.name);
let u = g.uid;
fx.children.insert(i + 1, g.into());
proj.next_id = next + 1;
Ok(u)
})?;
Ok(json!(r))
}
fn last(s: &mut Session, p: &Value) -> Result<Value> {
let id = s.state.last_effect.clone().ok_or_else(|| bad("effect.applyLast", "no effect applied yet"))?;
let mut q = p.clone();
if let Some(o) = q.as_object_mut() {
o.insert("effect".into(), json!(id));
} else {
q = json!({"effect": id});
}
apply(s, &q)
}
fn list(_: &mut Session, p: &Value) -> Result<Value> {
let filter = str_p(p, "filter").map(str::to_ascii_lowercase);
let v: Vec<Value> = effectcraft_effects::registry()
.iter()
.filter(|e| filter.as_ref().is_none_or(|f| e.name.to_ascii_lowercase().contains(f) || e.id.contains(f.as_str())))
.map(|e| json!({"id": e.id, "name": e.name, "category": e.category, "params": e.params.iter().map(|p| json!({"id": p.id, "name": p.name, "default": p.default.to_json()})).collect::<Vec<_>>()}))
.collect();
Ok(json!(v))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!("effect.apply", "Apply Effect", [], None, "{effect: id|name (e.g. Gaussian Blur), layers?}", has_layers, apply),
cmd!("effect.applyLast", "Last Effect", ["Effect"], Some("Cmd+Alt+Shift+E"), "{layers?}", has_layers, last),
cmd!("effect.removeAll", "Remove All", ["Effect"], Some("Cmd+Shift+E"), "{layers?}", has_layers, remove_all),
cmd!("effect.remove", "Remove Effect", [], None, "{layer?, effect: index|uid|name}", has_layers, remove),
cmd!("effect.toggle", "Toggle Effect", [], None, "{layer?, effect, value?}", has_layers, toggle),
cmd!("effect.reorder", "Reorder Effect", [], None, "{layer?, effect, index}", has_layers, reorder),
cmd!("effect.duplicate", "Duplicate Effect", [], None, "{layer?, effect}", has_layers, duplicate),
crate::query!("effect.list", "List Effects", "{filter?}", list),
]
}