//! 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { let filter = str_p(p, "filter").map(str::to_ascii_lowercase); let v: Vec = 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::>()})) .collect(); Ok(json!(v)) } pub fn specs() -> Vec { 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), ] }