//! Warp Stabilizer commands (Effect ▸ Distort ▸ Warp Stabilizer's Analyze / Cancel, Animation ▸ //! Warp Stabilizer VFX, the Tracker panel button) and its status query. use effectcraft_project::{GroupKind, ItemId, Layer, LayerId, PropGroup, Uid}; use serde_json::{Value, json}; use super::{CommandSpec, b_p, bad, has_comp, layer_p}; use crate::offload::{JobKind, WorkerJob}; use crate::{EngineError, Result, Session, cmd, query}; pub fn specs() -> Vec { vec![ cmd!("track.warpStabilizer", "Warp Stabilizer VFX", ["Animation"], None, "{layer?, wait?}", can_stabilize, warp_apply), cmd!("warp.analyze", "Analyze", [], None, "{layer?, effect?: uid|name|index, wait?: block until done}", has_comp, warp_analyze), cmd!("warp.cancel", "Cancel", [], None, "{}", is_analyzing, |s, _| Ok(json!({"stopped": s.stop_warp()}))), query!("warp.status", "Warp Stabilizer Status", "{layer?, effect?}", warp_status), ] } // ---------------------------------------------------------------- Warp Stabilizer fn can_stabilize(s: &Session) -> std::result::Result<(), String> { has_comp(s)?; let c = s.active_comp().ok_or("no composition is open")?; match s.state.selected_layers.first().and_then(|l| c.layer(*l)) { Some(l) if l.has_video() && !l.switches.adjustment => Ok(()), Some(_) => Err("Warp Stabilizer needs a footage or composition layer".into()), None => Err("select a layer to stabilize".into()), } } fn is_analyzing(s: &Session) -> std::result::Result<(), String> { if s.is_warp_analyzing() { Ok(()) } else { Err("no Warp Stabilizer analysis is running".into()) } } fn is_warp(g: &PropGroup) -> bool { matches!(&g.kind, GroupKind::Effect { effect } if effect == effectcraft_effects::warp_stab::ID) } /// (comp, layer, effect uid) of a Warp Stabilizer: `layer` / `effect` (uid, name or 1-based /// index among the layer's effects), else a selected one, else the layer's first. fn warp_p(s: &Session, p: &Value, cmd: &str) -> Result<(ItemId, LayerId, Uid)> { let cid = super::comp_id(s, p)?; let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?; let candidates: Vec = match p.get("layer") { Some(v) => vec![super::resolve_layer(comp, v).ok_or_else(|| bad(cmd, format!("no layer {v}")))?], None => { let mut v: Vec = s.state.selected_props.iter().map(|(l, _)| *l).collect(); v.extend(s.state.selected_layers.iter().copied()); v.extend(comp.layers.iter().map(|l| l.id)); v } }; for lid in candidates { let Some(fx) = comp.layer(lid).and_then(Layer::effects) else { continue }; 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 as usize).saturating_sub(1))) } Some(Value::String(name)) => fx.groups().find(|g| &g.name == name), _ => s .state .selected_props .iter() .find_map(|(l, u)| (*l == lid).then(|| fx.groups().find(|g| g.uid == *u && is_warp(g))).flatten()) .or_else(|| fx.groups().find(|g| is_warp(g))), }; if let Some(g) = g.filter(|g| is_warp(g)) { return Ok((cid, lid, g.uid)); } if p.get("layer").is_some() { break; } } Err(bad(cmd, "no Warp Stabilizer found (apply Effect ▸ Distort ▸ Warp Stabilizer first)")) } fn warp_analyze(s: &mut Session, p: &Value) -> Result { let (cid, lid, uid) = warp_p(s, p, "warp.analyze")?; let wait = b_p(p, "wait").unwrap_or(false); let n = s.start_warp(cid, lid, uid, wait).map_err(EngineError::Other)?; let mut out = json!({"layer": lid.0, "effect": uid, "frames": n, "running": s.is_warp_analyzing(), "progress": s.warp_progress()}); if wait { out["status"] = warp_status(s, &json!({"layer": lid.0, "effect": uid, "comp": cid.0}))?; } Ok(out) } /// Animation ▸ Warp Stabilizer VFX / the Tracker panel button: apply the effect to the selected /// layer and start analysing it. fn warp_apply(s: &mut Session, p: &Value) -> Result { let (cid, lid) = layer_p(s, p, "track.warpStabilizer")?; let r = s.execute("effect.apply", json!({"effect": effectcraft_effects::warp_stab::ID, "layers": [lid.0], "comp": cid.0}))?; let uid = r["effects"].get(0).and_then(Value::as_u64).ok_or_else(|| EngineError::Other("the effect could not be applied".into()))?; let wait = b_p(p, "wait").unwrap_or(false); let n = s.start_warp(cid, lid, uid, wait).map_err(EngineError::Other)?; Ok(json!({"layer": lid.0, "effect": uid, "frames": n, "running": s.is_warp_analyzing(), "progress": s.warp_progress()})) } fn warp_status(s: &mut Session, p: &Value) -> Result { s.poll_warp(false); let mut out = json!({"running": s.is_warp_analyzing(), "progress": s.warp_progress(), "banner": s.warp_progress().map(|p| p.banner())}); let Ok((cid, lid, uid)) = warp_p(s, p, "warp.status") else { return Ok(out) }; // (the project's `Arc`: the borrows outlive starting a job below) let project = s.project.clone(); let comp = project.comp(cid).ok_or(EngineError::NoComp)?; let layer = comp.layer(lid).ok_or(EngineError::NoComp)?; let g = layer.props.find_group(uid).ok_or(EngineError::NoComp)?; let ctx = effectcraft_render::EvalCtx::new(&project, cid, comp, s.time_of(cid)); let params = effectcraft_effects::flatten_params(g, &mut |pr| ctx.value(layer, pr)); let analysis = effectcraft_effects::warp_stab::analysis(¶ms); out["layer"] = json!(lid.0); out["effect"] = json!(uid); out["analyzed"] = json!(analysis.is_some()); out["pending"] = json!(s.warp_pending.contains(&(cid, lid, uid))); if let Some(a) = analysis { out["frames"] = json!(a.frames.len()); out["size"] = json!(a.size); let lt = layer.layer_time(s.time_of(cid)).seconds(); // Solving the plan takes seconds for a long Subspace Warp clip: where jobs run // elsewhere (the browser) it is never solved here but in a job worker, and the status // says "stabilizing" until the summary arrives (`WorkerReply::WarpPlan`). let plan = if s.offloads() { let cached = effectcraft_effects::warp_stab::cached_summary_at(¶ms, lt); if cached.is_none() { out["stabilizing"] = json!(true); if s.offloaded(JobKind::Warp).is_none() && s.offloaded(JobKind::WarpPlan).is_none() { let t = s.time_of(cid); if let Err(e) = s.offload_analysis(WorkerJob::WarpPlan { comp: cid, layer: lid, effect: uid, time: t }) { log::warn!("warp.status: {e}"); } } } cached } else { effectcraft_effects::warp_stab::summary_at(¶ms, lt) }; if let Some((k, plan)) = plan { out["frame"] = json!(k); out["autoScale"] = json!(plan.auto_scale * 100.0); out["crop"] = json!(plan.crop); out["validFraction"] = json!(plan.valid_fraction); out["warp"] = json!(plan.warps[k]); let pts = &a.frames[k]; out["features"] = json!(pts.features); out["inliers"] = json!(pts.inliers); } } Ok(out) }