mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-25 18:47:07 -06:00
738 lines
33 KiB
Rust
738 lines
33 KiB
Rust
//! Puppet tools: pins on the layer's Puppet effect (Position, Advanced, Bend, Starch and
|
||
//! Overlap pins), mesh options, and a query of the current mesh for overlays and agents.
|
||
//!
|
||
//! Pins are selected like properties (`puppet.selectPins`; Edit ▸ Clear / Delete removes the
|
||
//! selected pins, not their layer). Follow-Through (`puppet.follow`) makes pins trail a leader pin
|
||
//! with a delay, for hair, cloth and overlapping action.
|
||
//!
|
||
//! Pin positions are layer space. A new pin is attached to the mesh under it: the click is
|
||
//! mapped back through the current deformation to the rest mesh (its hidden rest position), and
|
||
//! Position / Advanced pins get a Position keyframe at the current time, as in After Effects.
|
||
|
||
use effectcraft_effects::puppet::{self, MeshOpts, PinKind};
|
||
use effectcraft_effects::{Buf, Params};
|
||
use effectcraft_keyframe::{Keyframe, Value as KV};
|
||
use effectcraft_project::build::Ids;
|
||
use effectcraft_project::{ItemId, Layer, LayerId, Node, PropGroup, Uid};
|
||
use effectcraft_render::{EvalCtx, RenderOpts, Renderer};
|
||
use effectcraft_time::Tick;
|
||
use serde_json::{Value, json};
|
||
|
||
use super::{CommandSpec, bad, f_p, has_comp, layer_mut, layer_p, merge_p, str_p};
|
||
use crate::{EngineError, Result, Session, cmd};
|
||
|
||
fn pt(v: Option<&Value>) -> Option<[f64; 2]> {
|
||
let a = v?.as_array()?;
|
||
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])
|
||
}
|
||
|
||
/// The Puppet effect's input pixels and evaluated parameters at comp time `t`.
|
||
pub fn puppet_eval(s: &Session, cid: ItemId, lid: LayerId, fx_uid: Uid, t: Tick) -> Option<(Buf, Params)> {
|
||
let comp = s.project.comp(cid)?;
|
||
let layer = comp.layer(lid)?;
|
||
let fx = layer.effects()?;
|
||
let index = fx.groups().position(|g| g.uid == fx_uid)?;
|
||
let g = fx.groups().nth(index)?;
|
||
let ctx = EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
|
||
let mut r = Renderer::new(&s.project, &*s.footage, RenderOpts::default());
|
||
r.expr = s.expr.as_deref();
|
||
r.cache = Some(&s.layer_cache);
|
||
let buf = (*r.layer_input(&ctx, layer, index)?).clone();
|
||
let params = effectcraft_effects::flatten_params(g, &mut |p| ctx.value(layer, p));
|
||
Some((buf, params))
|
||
}
|
||
|
||
fn find_puppet(s: &Session, cid: ItemId, lid: LayerId) -> Option<Uid> {
|
||
s.project.comp(cid)?.layer(lid)?.effects()?.groups().find(|g| puppet::is_puppet(g)).map(|g| g.uid)
|
||
}
|
||
|
||
fn snapped(s: &Session, cid: ItemId, p: &Value) -> Result<Tick> {
|
||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||
Ok(comp.frame_rate.snap_nearest(super::time_p(s, p, Some(comp))))
|
||
}
|
||
|
||
fn layer_time(s: &Session, cid: ItemId, lid: LayerId, t: Tick) -> Result<Tick> {
|
||
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
|
||
Ok(l.layer_time(t))
|
||
}
|
||
|
||
fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.addPin";
|
||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||
let kind = match str_p(p, "kind") {
|
||
Some(k) => PinKind::from_name(k).ok_or_else(|| bad(cmd, format!("unknown kind `{k}` (position | advanced | bend | starch | overlap)")))?,
|
||
None => PinKind::Position,
|
||
};
|
||
let at = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (layer space)"))?;
|
||
let t = snapped(s, cid, p)?;
|
||
let lt = layer_time(s, cid, lid, t)?;
|
||
{
|
||
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
|
||
if l.effects().is_none() || !l.source.is_av() {
|
||
return Err(bad(cmd, "the Puppet tools work on footage, solid, text, shape and precomp layers"));
|
||
}
|
||
}
|
||
// Which mesh: the one under the click (through the current deformation), else a new one.
|
||
let fx_uid = find_puppet(s, cid, lid);
|
||
let want_mesh = p.get("mesh").and_then(Value::as_u64);
|
||
let new_mesh = p.get("newMesh").and_then(Value::as_bool).unwrap_or(false);
|
||
let mut target: Option<(Uid, [f64; 2])> = None;
|
||
if let Some(fu) = fx_uid
|
||
&& !new_mesh
|
||
&& let Some((buf, params)) = puppet_eval(s, cid, lid, fu, t)
|
||
{
|
||
let meshes = puppet::overlay(&buf, ¶ms);
|
||
for (uid, mesh, def, _) in &meshes {
|
||
if want_mesh.is_some_and(|w| w != *uid) || mesh.tris.is_empty() {
|
||
continue;
|
||
}
|
||
let inside = mesh.tris.iter().any(|tr| {
|
||
let (a, b, c) = (def[tr[0]], def[tr[1]], def[tr[2]]);
|
||
let cr = |o: [f64; 2], u: [f64; 2], v: [f64; 2]| (u[0] - o[0]) * (v[1] - o[1]) - (u[1] - o[1]) * (v[0] - o[0]);
|
||
cr(a, b, at) >= 0.0 && cr(b, c, at) >= 0.0 && cr(c, a, at) >= 0.0
|
||
});
|
||
let near = def.iter().map(|v| (v[0] - at[0]).hypot(v[1] - at[1])).fold(f64::MAX, f64::min) < 12.0;
|
||
if inside || near || want_mesh.is_some() {
|
||
target = Some((*uid, puppet::unmap(mesh, def, at)));
|
||
break;
|
||
}
|
||
}
|
||
if target.is_none()
|
||
&& let Some(w) = want_mesh
|
||
{
|
||
return Err(bad(cmd, format!("no mesh {w}")));
|
||
}
|
||
}
|
||
let opts = MeshOpts {
|
||
density: f_p(p, "density").unwrap_or(s.state.puppet.density),
|
||
expansion: f_p(p, "expansion").unwrap_or(s.state.puppet.expansion),
|
||
triangles: f_p(p, "triangles").unwrap_or(s.state.puppet.triangles),
|
||
};
|
||
let size = {
|
||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||
let l = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||
let (w, h) = effectcraft_render::source_size(&s.project, l);
|
||
if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] }
|
||
};
|
||
let label = match kind {
|
||
PinKind::Starch => "Add Starch Pin",
|
||
PinKind::Overlap => "Add Overlap Pin",
|
||
_ => "Add Puppet Pin",
|
||
};
|
||
let (fx, mesh, pin) = s.edit(label, None, |proj, st| {
|
||
let mut next = proj.next_id;
|
||
let l = layer_mut(proj, cid, lid)?;
|
||
let fxg = l.props.sub_mut("effects").ok_or_else(|| bad(cmd, "no effects"))?;
|
||
let mut ids = Ids(&mut next);
|
||
let fx_uid = match fx_uid {
|
||
Some(u) => u,
|
||
None => {
|
||
let spec = effectcraft_effects::find(puppet::ID).ok_or_else(|| EngineError::Other("Puppet effect missing".into()))?;
|
||
let g = effectcraft_effects::instantiate(spec, &mut ids, "Puppet", size);
|
||
let u = g.uid;
|
||
fxg.children.push(g.into());
|
||
u
|
||
}
|
||
};
|
||
let pg = fxg.find_group_mut(fx_uid).ok_or_else(|| bad(cmd, "Puppet effect gone"))?;
|
||
let (mesh_uid, rest) = match target {
|
||
Some((m, r)) => (m, r),
|
||
None => {
|
||
let n = puppet::meshes(pg).count();
|
||
let g = puppet::mesh_group(&mut ids, &format!("Mesh {}", n + 1), at, &opts);
|
||
let u = g.uid;
|
||
pg.children.push(g.into());
|
||
(u, at)
|
||
}
|
||
};
|
||
let name = puppet::next_pin_name(pg, kind);
|
||
let mg = pg.find_group_mut(mesh_uid).ok_or_else(|| bad(cmd, "mesh gone"))?;
|
||
let mut pin = puppet::pin_group(&mut ids, &name, kind, rest);
|
||
if let Some(pos) = pin.get_mut("position") {
|
||
if kind.moves() {
|
||
pos.value = KV::Vec2(at);
|
||
pos.set_animated(true, lt);
|
||
} else {
|
||
pos.value = KV::Vec2(rest);
|
||
}
|
||
}
|
||
let pin_uid = pin.uid;
|
||
let deform = mg.sub_mut("deform").ok_or_else(|| bad(cmd, "mesh has no Deform group"))?;
|
||
deform.children.push(pin.into());
|
||
proj.next_id = next;
|
||
st.selected_props = vec![(lid, pin_uid)];
|
||
Ok((fx_uid, mesh_uid, pin_uid))
|
||
})?;
|
||
Ok(json!({"effect": fx, "mesh": mesh, "pin": pin, "rest": [target.map(|x| x.1).unwrap_or(at)[0], target.map(|x| x.1).unwrap_or(at)[1]]}))
|
||
}
|
||
|
||
/// The pin groups of a layer's Puppet effects, in stack order.
|
||
pub(crate) fn layer_pins(l: &Layer) -> impl Iterator<Item = &PropGroup> {
|
||
l.effects().into_iter().flat_map(|fx| fx.groups().filter(|g| puppet::is_puppet(g))).flat_map(puppet::meshes).flat_map(puppet::pins)
|
||
}
|
||
|
||
/// Selected puppet pins of the active comp: (layer, pin uid).
|
||
pub(crate) fn selected_pins(s: &Session) -> Vec<(LayerId, Uid)> {
|
||
let Some(c) = s.active_comp() else { return vec![] };
|
||
s.state.selected_props.iter().filter(|(l, u)| c.layer(*l).is_some_and(|l| layer_pins(l).any(|g| g.uid == *u))).copied().collect()
|
||
}
|
||
|
||
/// Edit ▸ Select All with puppet pins selected: every pin of the selected pins' kinds on their
|
||
/// layers. `None` when no pin is selected (Select All then selects layers).
|
||
pub(crate) fn select_all_of_kind(s: &mut Session) -> Option<Value> {
|
||
let sel = selected_pins(s);
|
||
let comp = s.active_comp()?;
|
||
let kind = |g: &PropGroup| PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
|
||
let mut kinds = vec![];
|
||
for (l, u) in &sel {
|
||
if let Some(g) = comp.layer(*l).and_then(|l| l.props.find_group(*u)) {
|
||
kinds.push((*l, kind(g)));
|
||
}
|
||
}
|
||
if kinds.is_empty() {
|
||
return None;
|
||
}
|
||
let all: Vec<(LayerId, Uid)> =
|
||
comp.layers.iter().flat_map(|l| layer_pins(l).filter(|g| kinds.contains(&(l.id, kind(g)))).map(|g| (l.id, g.uid)).collect::<Vec<_>>()).collect();
|
||
s.state.selected_props = all.clone();
|
||
Some(json!({ "pins": all.iter().map(|(_, u)| *u).collect::<Vec<_>>() }))
|
||
}
|
||
|
||
/// Find a pin group (uid or name) in the layer's Puppet effects.
|
||
fn pin_uid(s: &Session, cid: ItemId, lid: LayerId, key: &Value, cmd: &str) -> Result<Uid> {
|
||
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
|
||
let hit = |pin: &&PropGroup| match key {
|
||
Value::Number(n) => n.as_u64() == Some(pin.uid),
|
||
Value::String(name) => &pin.name == name,
|
||
_ => false,
|
||
};
|
||
layer_pins(l).find(hit).map(|g| g.uid).ok_or_else(|| bad(cmd, format!("no puppet pin {key}")))
|
||
}
|
||
|
||
/// Pins named by `pin` or `pins` (uids or names) on `layer`, else the selected pins (of `layer`
|
||
/// when given): (comp, layer, pin uid).
|
||
pub(crate) fn pins_p(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(ItemId, LayerId, Uid)>> {
|
||
let keys: Vec<Value> = match (p.get("pin"), p.get("pins")) {
|
||
(Some(k), _) => vec![k.clone()],
|
||
(None, Some(Value::Array(a))) => a.clone(),
|
||
(None, Some(_)) => return Err(bad(cmd, "`pins` must be an array of pin uids or names")),
|
||
(None, None) => vec![],
|
||
};
|
||
if !keys.is_empty() {
|
||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||
return keys.iter().map(|k| pin_uid(s, cid, lid, k, cmd).map(|u| (cid, lid, u))).collect();
|
||
}
|
||
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
|
||
let only = match p.get("layer") {
|
||
Some(_) => Some(layer_p(s, p, cmd)?.1),
|
||
None => None,
|
||
};
|
||
let sel: Vec<_> = selected_pins(s).into_iter().filter(|(l, _)| only.is_none_or(|o| o == *l)).map(|(l, u)| (cid, l, u)).collect();
|
||
if sel.is_empty() {
|
||
return Err(bad(cmd, "missing `pin` / `pins` (uids or names), and no puppet pins are selected"));
|
||
}
|
||
Ok(sel)
|
||
}
|
||
|
||
/// Select pins (the viewer's click / Shift-click on a pin). Plain: only these pins; `add`: add
|
||
/// them; `toggle`: flip each. An empty `pins` deselects every pin.
|
||
fn select_pins(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.selectPins";
|
||
let want = match p.get("pins") {
|
||
Some(Value::Array(a)) if a.is_empty() => vec![],
|
||
_ => pins_p(s, p, cmd)?,
|
||
};
|
||
let (add, toggle) = (super::b_p(p, "add").unwrap_or(false), super::b_p(p, "toggle").unwrap_or(false));
|
||
let st = &mut s.state;
|
||
if !add && !toggle {
|
||
st.selected_props.clear();
|
||
st.selected_keys.clear();
|
||
}
|
||
for (_, l, u) in want {
|
||
let at = st.selected_props.iter().position(|x| *x == (l, u));
|
||
match at {
|
||
Some(i) if toggle => {
|
||
st.selected_props.remove(i);
|
||
}
|
||
Some(_) => {}
|
||
None => st.selected_props.push((l, u)),
|
||
}
|
||
if !st.selected_layers.contains(&l) {
|
||
st.selected_layers.push(l);
|
||
}
|
||
}
|
||
Ok(json!({ "pins": selected_pins(s).iter().map(|(_, u)| *u).collect::<Vec<_>>() }))
|
||
}
|
||
|
||
fn with_pin<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, f: impl FnOnce(&mut PropGroup, Tick) -> Result<T>) -> Result<T> {
|
||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||
let key = p.get("pin").cloned().ok_or_else(|| bad(cmd, "missing `pin` (uid or name)"))?;
|
||
let uid = pin_uid(s, cid, lid, &key, cmd)?;
|
||
let t = snapped(s, cid, p)?;
|
||
let lt = layer_time(s, cid, lid, t)?;
|
||
s.edit(label, merge_p(p), |proj, st| {
|
||
let l = layer_mut(proj, cid, lid)?;
|
||
let g = l.props.find_group_mut(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
|
||
let r = f(g, lt)?;
|
||
st.selected_props = vec![(lid, uid)];
|
||
Ok(r)
|
||
})
|
||
}
|
||
|
||
fn move_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.movePin";
|
||
let to = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (layer space)"))?;
|
||
with_pin(s, p, cmd, "Move Puppet Pin", |g, lt| {
|
||
let kind = PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
|
||
let pos = g.get_mut("position").ok_or_else(|| bad(cmd, "Bend pins have no position (use puppet.setPin rotation/scale)"))?;
|
||
pos.set_value_at(lt, KV::Vec2(to));
|
||
// Starch and Overlap pins sit on the rest mesh: moving them moves their region.
|
||
if !kind.moves()
|
||
&& let Some(r) = g.get_mut("rest")
|
||
{
|
||
r.value = KV::Vec2(to);
|
||
}
|
||
Ok(())
|
||
})?;
|
||
Ok(Value::Null)
|
||
}
|
||
|
||
fn set_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.setPin";
|
||
let fields: Vec<(&str, KV)> = [("scale", "scale"), ("rotation", "rotation"), ("amount", "amount"), ("extent", "extent"), ("inFront", "in_front")]
|
||
.iter()
|
||
.filter_map(|(k, m)| f_p(p, k).map(|v| (*m, KV::Scalar(v))))
|
||
.chain(pt(p.get("position")).map(|v| ("position", KV::Vec2(v))))
|
||
.collect();
|
||
if fields.is_empty() {
|
||
return Err(bad(cmd, "nothing to set (position, scale, rotation, amount, extent, inFront)"));
|
||
}
|
||
with_pin(s, p, cmd, "Edit Puppet Pin", |g, lt| {
|
||
for (m, v) in fields {
|
||
let pr = g.get_mut(m).ok_or_else(|| bad(cmd, format!("this pin has no `{m}`")))?;
|
||
pr.set_value_at(lt, v);
|
||
}
|
||
Ok(())
|
||
})?;
|
||
Ok(Value::Null)
|
||
}
|
||
|
||
fn remove_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let pins = pins_p(s, p, "puppet.removePin")?;
|
||
let label = if pins.len() == 1 { "Delete Puppet Pin" } else { "Delete Puppet Pins" };
|
||
s.edit(label, None, |proj, st| {
|
||
for (cid, lid, uid) in &pins {
|
||
let l = layer_mut(proj, *cid, *lid)?;
|
||
if let Some(parent) = l.props.parent_of_mut(*uid) {
|
||
parent.children.retain(|c| c.uid() != *uid);
|
||
}
|
||
st.selected_props.retain(|(_, u)| u != uid);
|
||
}
|
||
Ok(())
|
||
})?;
|
||
Ok(json!({ "removed": pins.len() }))
|
||
}
|
||
|
||
fn mesh_opts(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.mesh";
|
||
let fields: Vec<(&str, f64)> = ["density", "expansion", "triangles"].iter().filter_map(|k| f_p(p, k).map(|v| (*k, v))).collect();
|
||
if let Some(b) = p.get("showMesh").and_then(Value::as_bool) {
|
||
s.state.puppet.show_mesh = b;
|
||
}
|
||
if fields.is_empty() {
|
||
return Ok(serde_json::to_value(&s.state.puppet).unwrap_or(Value::Null));
|
||
}
|
||
// Without a layer: set the tool defaults for new meshes.
|
||
let has_layer = p.get("layer").is_some() || !s.state.selected_layers.is_empty();
|
||
let (cid, lid) = match layer_p(s, p, cmd) {
|
||
Ok(x) => x,
|
||
Err(_) if !has_layer => {
|
||
for (k, v) in &fields {
|
||
match *k {
|
||
"density" => s.state.puppet.density = v.clamp(0.0, 100.0),
|
||
"expansion" => s.state.puppet.expansion = *v,
|
||
_ => s.state.puppet.triangles = v.max(10.0),
|
||
}
|
||
}
|
||
return Ok(serde_json::to_value(&s.state.puppet).unwrap_or(Value::Null));
|
||
}
|
||
Err(e) => return Err(e),
|
||
};
|
||
let fx = find_puppet(s, cid, lid).ok_or_else(|| bad(cmd, "the layer has no Puppet effect (add a pin first)"))?;
|
||
let want = p.get("mesh").and_then(Value::as_u64);
|
||
let t = snapped(s, cid, p)?;
|
||
let lt = layer_time(s, cid, lid, t)?;
|
||
for (k, v) in &fields {
|
||
match *k {
|
||
"density" => s.state.puppet.density = v.clamp(0.0, 100.0),
|
||
"expansion" => s.state.puppet.expansion = *v,
|
||
_ => s.state.puppet.triangles = v.max(10.0),
|
||
}
|
||
}
|
||
s.edit("Puppet Mesh", merge_p(p), |proj, _| {
|
||
let l = layer_mut(proj, cid, lid)?;
|
||
let g = l.props.find_group_mut(fx).ok_or_else(|| bad(cmd, "Puppet effect gone"))?;
|
||
let mut done = 0;
|
||
for c in g.children.iter_mut() {
|
||
let Node::Group(m) = c else { continue };
|
||
if m.match_id != "mesh" || want.is_some_and(|w| w != m.uid) {
|
||
continue;
|
||
}
|
||
for (k, v) in &fields {
|
||
if let Some(pr) = m.get_mut(k) {
|
||
pr.set_value_at(lt, KV::Scalar(*v));
|
||
}
|
||
}
|
||
done += 1;
|
||
}
|
||
if done == 0 { Err(bad(cmd, "no such mesh")) } else { Ok(()) }
|
||
})?;
|
||
Ok(Value::Null)
|
||
}
|
||
|
||
/// Parse recorded samples `[[t, x, y]…]` (t = seconds since the drag began, layer space).
|
||
fn samples(p: &Value, cmd: &str) -> Result<Vec<(f64, [f64; 2])>> {
|
||
let arr = p
|
||
.get("samples")
|
||
.and_then(Value::as_array)
|
||
.ok_or_else(|| bad(cmd, "missing `samples`: [[t, x, y]…] (t = seconds since the drag began, layer space)"))?;
|
||
let mut rec = Vec::with_capacity(arr.len());
|
||
for (i, v) in arr.iter().enumerate() {
|
||
let a: Vec<f64> = v.as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
|
||
if a.len() < 3 || a.iter().any(|x| !x.is_finite()) {
|
||
return Err(bad(cmd, format!("sample {i}: expected [t, x, y]")));
|
||
}
|
||
rec.push((a[0], [a[1], a[2]]));
|
||
}
|
||
if rec.is_empty() {
|
||
return Err(bad(cmd, "need at least one sample"));
|
||
}
|
||
rec.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
Ok(rec)
|
||
}
|
||
|
||
/// Puppet tool Record mode (⌘/Ctrl-drag a pin): the pin's drag, sampled in real time, becomes
|
||
/// Position keyframes at the comp frame rate from the current time on, replacing the keys in
|
||
/// the recorded span. Record Options: `speed` (%: 100 plays back at the speed of the drag,
|
||
/// 200 twice as slow) and `smoothing` (removes extraneous keys like the Smoother; 0 = a key
|
||
/// on every frame). The current time stays where recording began.
|
||
fn record_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.recordPin";
|
||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||
let key = p.get("pin").cloned().ok_or_else(|| bad(cmd, "missing `pin` (uid or name)"))?;
|
||
let uid = pin_uid(s, cid, lid, &key, cmd)?;
|
||
let rec = samples(p, cmd)?;
|
||
// Several selected pins dragged together (`pins`): each follows the drag's displacement from
|
||
// where it is when recording begins; one undo step.
|
||
if let Some(Value::Array(others)) = p.get("pins")
|
||
&& !others.is_empty()
|
||
{
|
||
let start = match p.get("start").and_then(Value::as_f64) {
|
||
Some(t) => Tick::from_seconds_f64(t),
|
||
None => s.time_of(cid),
|
||
};
|
||
let lt = layer_time(s, cid, lid, start)?;
|
||
let mut pins = vec![(uid, rec[0].1)];
|
||
for k in others {
|
||
let u = pin_uid(s, cid, lid, k, cmd)?;
|
||
if pins.iter().any(|(x, _)| *x == u) {
|
||
continue;
|
||
}
|
||
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
|
||
let at = l.props.find_group(u).and_then(|g| g.get("position")).map(|pr| pr.value_at(lt).as_vec2());
|
||
pins.push((u, at.ok_or_else(|| bad(cmd, "only Position and Advanced pins record motion"))?));
|
||
}
|
||
let o = rec[0].1;
|
||
return super::app_more::grouped(s, "Record Puppet Pins", |s| {
|
||
let mut last = Value::Null;
|
||
for (u, base) in &pins {
|
||
let moved: Vec<Value> = rec.iter().map(|(t, q)| json!([t, base[0] + q[0] - o[0], base[1] + q[1] - o[1]])).collect();
|
||
let mut one = p.clone();
|
||
if let Some(m) = one.as_object_mut() {
|
||
m.remove("pins");
|
||
m.insert("pin".into(), json!(u));
|
||
m.insert("samples".into(), Value::Array(moved));
|
||
}
|
||
last = s.execute(cmd, one)?;
|
||
}
|
||
last["pins"] = json!(pins.iter().map(|(u, _)| *u).collect::<Vec<_>>());
|
||
Ok(last)
|
||
});
|
||
}
|
||
let opts = &s.state.puppet;
|
||
let speed = f_p(p, "speed").unwrap_or(opts.record_speed).clamp(1.0, 10_000.0) / 100.0;
|
||
// Smoothing 10 (the default) keeps the path within 1 layer pixel of the drag.
|
||
let smoothing = f_p(p, "smoothing").unwrap_or(opts.record_smoothing).max(0.0) / 10.0;
|
||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||
let fr = comp.frame_rate;
|
||
let fd = fr.frame_duration().seconds();
|
||
let start = match p.get("start").and_then(Value::as_f64) {
|
||
Some(t) => fr.snap_nearest(Tick::from_seconds_f64(t)),
|
||
None => s.time_of(cid),
|
||
};
|
||
let f0 = fr.frame_at(start);
|
||
let end_frame = fr.frame_at(comp.duration - comp.frame_duration());
|
||
let t_rec0 = rec[0].0;
|
||
let span = (rec[rec.len() - 1].0 - t_rec0) * speed;
|
||
let nframes = (span / fd).round().max(0.0) as i64;
|
||
let at = |secs: f64| {
|
||
let i = rec.partition_point(|r| r.0 <= secs).clamp(1, rec.len().max(2) - 1);
|
||
if rec.len() == 1 {
|
||
return rec[0].1;
|
||
}
|
||
let (a, b) = (rec[i - 1], rec[i]);
|
||
let f = if b.0 > a.0 { ((secs - a.0) / (b.0 - a.0)).clamp(0.0, 1.0) } else { 1.0 };
|
||
[a.1[0] + (b.1[0] - a.1[0]) * f, a.1[1] + (b.1[1] - a.1[1]) * f]
|
||
};
|
||
let n = s.edit("Record Puppet Pin", None, |proj, st| {
|
||
let l = layer_mut(proj, cid, lid)?;
|
||
let lc = l.clone();
|
||
let g = l.props.find_group_mut(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
|
||
let kind = PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
|
||
if !kind.moves() {
|
||
return Err(bad(cmd, "only Position and Advanced pins record motion"));
|
||
}
|
||
let pos = g.get_mut("position").ok_or_else(|| bad(cmd, "the pin has no Position"))?;
|
||
let mut new: Vec<Keyframe> = vec![];
|
||
for k in 0..=nframes {
|
||
let f = f0 + k;
|
||
if f > end_frame {
|
||
break;
|
||
}
|
||
let q = at(t_rec0 + k as f64 * fd / speed);
|
||
new.push(Keyframe::new(lc.layer_time(fr.tick_of(f)), KV::Vec2(q)));
|
||
}
|
||
let (Some(t0), Some(t1)) = (new.first().map(|k| k.time), new.last().map(|k| k.time)) else { return Ok(0) };
|
||
let mut keys: Vec<Keyframe> = pos.keys.iter().filter(|k| k.time < t0 || k.time > t1).cloned().collect();
|
||
keys.extend(new);
|
||
keys.sort_by_key(|k| k.time);
|
||
let (a, b) = (keys.iter().position(|k| k.time == t0).unwrap_or(0), keys.iter().position(|k| k.time == t1).unwrap_or(0));
|
||
if smoothing > 0.0 && b > a + 1 {
|
||
let samples: Vec<(Tick, Vec<f64>)> = keys[a..=b].iter().map(|k| (k.time, k.value.components())).collect();
|
||
keys = super::anim_tools::smooth_keys(&keys, a, b, &samples, smoothing, true);
|
||
}
|
||
pos.keys = keys;
|
||
pos.value = KV::Vec2(rec[0].1);
|
||
let puid = pos.uid;
|
||
st.selected_props = vec![(lid, uid)];
|
||
st.selected_keys = pos.keys.iter().filter(|k| k.time >= t0 && k.time <= t1).map(|k| crate::KeyRef { layer: lid, prop: puid, time: k.time }).collect();
|
||
Ok(st.selected_keys.len())
|
||
})?;
|
||
// The current time returns to where recording began.
|
||
s.state.times.insert(cid, start);
|
||
let end = fr.tick_of((f0 + nframes).min(end_frame));
|
||
Ok(json!({"keys": n, "start": start.seconds(), "end": end.seconds(), "frames": nframes + 1}))
|
||
}
|
||
|
||
/// Follow-Through (hair, cloth, tails, overlapping action): each follower pin trails the leader
|
||
/// pin's motion by a delay, through an expression on its Position. The followers nearest the
|
||
/// leader trail least; with `cascade` the k-th nearest trails k × `delay`, otherwise all trail by
|
||
/// `delay`. `amount` (%) scales the motion they inherit; their own keyframes still add on top.
|
||
/// The leader is `leader`, else the first selected pin; the followers are `pins`, else the other
|
||
/// selected pins of the leader's layer. One undo step.
|
||
fn follow(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let cmd = "puppet.follow";
|
||
let (cid, lid, leader, mut followers) = match p.get("leader") {
|
||
Some(k) => {
|
||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||
let leader = pin_uid(s, cid, lid, k, cmd)?;
|
||
let rest: Vec<Uid> = pins_p(s, p, cmd)?.into_iter().filter(|(_, l, u)| *l == lid && *u != leader).map(|x| x.2).collect();
|
||
(cid, lid, leader, rest)
|
||
}
|
||
None => {
|
||
let sel = pins_p(s, p, cmd)?;
|
||
let (cid, lid, leader) = sel[0];
|
||
(cid, lid, leader, sel.iter().skip(1).filter(|(_, l, _)| *l == lid).map(|x| x.2).collect())
|
||
}
|
||
};
|
||
followers.dedup();
|
||
if followers.is_empty() {
|
||
return Err(bad(cmd, "select the leader pin first, then the pins that follow it (or pass `leader` and `pins`)"));
|
||
}
|
||
let delay = f_p(p, "delay").unwrap_or(0.1).clamp(0.0, 10.0);
|
||
let amount = f_p(p, "amount").unwrap_or(100.0) / 100.0;
|
||
let cascade = super::b_p(p, "cascade").unwrap_or(true);
|
||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||
// (Position uid, rest) of a pin that moves.
|
||
let moving = |uid: Uid| -> Result<(Uid, [f64; 2], String)> {
|
||
let g = layer.props.find_group(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
|
||
if !PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0)).moves() {
|
||
return Err(bad(cmd, format!("`{}` has no Position (only Position and Advanced pins follow or lead)", g.name)));
|
||
}
|
||
let pos = g.get("position").ok_or_else(|| bad(cmd, "the pin has no Position"))?;
|
||
Ok((pos.uid, g.get("rest").map(|r| r.value.as_vec2()).unwrap_or([0.0; 2]), g.name.clone()))
|
||
};
|
||
let (lead_pos, lead_rest, lead_name) = moving(leader)?;
|
||
let reference = super::link::reference(comp, layer, layer, lead_pos, true).ok_or_else(|| bad(cmd, "can't reference the leader pin"))?;
|
||
let mut rigs = followers.iter().map(|u| moving(*u).map(|(pos, rest, _)| (*u, pos, rest))).collect::<Result<Vec<_>>>()?;
|
||
let dist = |r: [f64; 2]| (r[0] - lead_rest[0]).hypot(r[1] - lead_rest[1]);
|
||
rigs.sort_by(|a, b| dist(a.2).total_cmp(&dist(b.2)));
|
||
let out = super::app_more::grouped(s, "Follow-Through", |s| {
|
||
let mut out = vec![];
|
||
for (k, (pin, pos, _)) in rigs.iter().enumerate() {
|
||
let d = if cascade { delay * (k + 1) as f64 } else { delay };
|
||
let e = format!(
|
||
"// Follow-Through: trails {} by {d} s\nvar lead = {reference};\nvalue + (lead.valueAtTime(time - {d}) - [{}, {}]) * {amount}",
|
||
serde_json::to_string(&lead_name).unwrap_or_default(),
|
||
lead_rest[0],
|
||
lead_rest[1]
|
||
);
|
||
s.execute("prop.setExpression", json!({"comp": cid.0, "layer": lid.0, "prop": pos, "expression": e}))?;
|
||
out.push(json!({"pin": pin, "delay": d}));
|
||
}
|
||
Ok(out)
|
||
})?;
|
||
Ok(json!({"leader": leader, "pins": out}))
|
||
}
|
||
|
||
/// Record Options (the Puppet tool's Record Options… dialog): `speed`, `smoothing`,
|
||
/// `useDraftDeformation`, `showMesh`. Returns the options.
|
||
fn record_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let o = &mut s.state.puppet;
|
||
if let Some(v) = f_p(p, "speed") {
|
||
o.record_speed = v.clamp(1.0, 10_000.0);
|
||
}
|
||
if let Some(v) = f_p(p, "smoothing") {
|
||
o.record_smoothing = v.clamp(0.0, 100.0);
|
||
}
|
||
if let Some(v) = p.get("useDraftDeformation").and_then(Value::as_bool) {
|
||
o.record_draft = v;
|
||
}
|
||
if let Some(v) = p.get("showMesh").and_then(Value::as_bool) {
|
||
o.record_show_mesh = v;
|
||
}
|
||
Ok(json!({"speed": o.record_speed, "smoothing": o.record_smoothing, "useDraftDeformation": o.record_draft, "showMesh": o.record_show_mesh}))
|
||
}
|
||
|
||
/// Meshes, pins and the deformed mesh at the current time (layer space).
|
||
fn info(s: &mut Session, p: &Value) -> Result<Value> {
|
||
let (cid, lid) = layer_p(s, p, "puppet.info")?;
|
||
let Some(fx) = find_puppet(s, cid, lid) else { return Ok(json!({"meshes": []})) };
|
||
let t = snapped(s, cid, p)?;
|
||
let (buf, params) = puppet_eval(s, cid, lid, fx, t).ok_or_else(|| bad("puppet.info", "the layer has no pixels"))?;
|
||
let meshes: Vec<Value> = puppet::overlay(&buf, ¶ms)
|
||
.into_iter()
|
||
.map(|(uid, mesh, def, pins)| {
|
||
json!({
|
||
"mesh": uid,
|
||
"triangles": mesh.tris.len(),
|
||
"vertices": mesh.verts.len(),
|
||
"outline": mesh.outline,
|
||
"deformed": def,
|
||
"tris": mesh.tris,
|
||
"pins": pins.iter().map(|pn| json!({"pin": pn.uid, "kind": pn.kind.name(), "rest": pn.rest, "position": pn.position})).collect::<Vec<_>>(),
|
||
})
|
||
})
|
||
.collect();
|
||
Ok(json!({"effect": fx, "meshes": meshes}))
|
||
}
|
||
|
||
pub fn specs() -> Vec<CommandSpec> {
|
||
vec![
|
||
cmd!(
|
||
"puppet.addPin",
|
||
"Add Puppet Pin",
|
||
[],
|
||
None,
|
||
"{layer?, kind?: position|advanced|bend|starch|overlap, position: [x,y] (layer space), time? (s) | frame?, mesh?: uid, newMesh?, density?, expansion?, triangles?}",
|
||
has_comp,
|
||
add_pin
|
||
),
|
||
cmd!("puppet.movePin", "Move Puppet Pin", [], None, "{layer?, pin: uid | name, position: [x,y], time? | frame?, merge?}", has_comp, move_pin),
|
||
cmd!(
|
||
"puppet.setPin",
|
||
"Edit Puppet Pin",
|
||
[],
|
||
None,
|
||
"{layer?, pin: uid | name, position?, scale? (%), rotation? (°), amount? (%), extent? (px), inFront?, time? | frame?, merge?}",
|
||
has_comp,
|
||
set_pin
|
||
),
|
||
cmd!(
|
||
"puppet.removePin",
|
||
"Delete Puppet Pin",
|
||
[],
|
||
None,
|
||
"{layer?, pin?: uid | name, pins?: [uid | name…]} (default: the selected pins)",
|
||
has_comp,
|
||
remove_pin
|
||
),
|
||
cmd!("puppet.selectPins", "Select Puppet Pins", [], None, "{layer?, pins: [uid | name…] ([] deselects), add?, toggle?}", has_comp, select_pins),
|
||
cmd!(
|
||
"puppet.mesh",
|
||
"Puppet Mesh Options",
|
||
[],
|
||
None,
|
||
"{layer?, mesh?: uid, density?, expansion?, triangles?, showMesh?, time?, merge?}",
|
||
super::always,
|
||
mesh_opts
|
||
),
|
||
crate::query!("puppet.info", "Puppet Mesh Info", "{layer?, time? | frame?}", info),
|
||
cmd!(
|
||
"puppet.recordPin",
|
||
"Record Puppet Pin",
|
||
[],
|
||
None,
|
||
"{layer?, pin: uid | name, samples: [[t, x, y]…] (t = seconds since the drag began, layer space), pins?: [uid | name…] (more pins that move by the same displacement), start? (s, default current time), speed? (%, Record Options), smoothing? (Record Options)}",
|
||
has_comp,
|
||
record_pin
|
||
),
|
||
cmd!(
|
||
"puppet.follow",
|
||
"Follow-Through...",
|
||
[],
|
||
None,
|
||
"{layer?, leader?: uid | name (default: the first selected pin), pins?: [uid | name…] (default: the other selected pins), delay? (s, 0.1), amount? (%, 100), cascade? (true: the k-th nearest pin trails k × delay)}",
|
||
has_comp,
|
||
follow
|
||
),
|
||
cmd!(
|
||
"puppet.recordOptions",
|
||
"Record Options...",
|
||
[],
|
||
None,
|
||
"{speed? (%, 100), smoothing? (0-100), useDraftDeformation?, showMesh?}",
|
||
super::always,
|
||
record_options
|
||
),
|
||
]
|
||
}
|
||
|
||
/// Puppet tool options (the Tools bar's Mesh: Show / Expansion / Density for new meshes).
|
||
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
|
||
#[serde(default, rename_all = "camelCase")]
|
||
pub struct PuppetOptions {
|
||
pub density: f64,
|
||
pub expansion: f64,
|
||
pub triangles: f64,
|
||
/// Show the mesh in the viewer.
|
||
pub show_mesh: bool,
|
||
/// Record Options ▸ Speed (%): 100 plays a recording back at the speed of the drag.
|
||
pub record_speed: f64,
|
||
/// Record Options ▸ Smoothing: removes extraneous keys from recorded motion (0 = a key per
|
||
/// frame).
|
||
pub record_smoothing: f64,
|
||
/// Record Options ▸ Use Draft Deformation: the viewer shows the draft (unexpanded) mesh
|
||
/// while recording.
|
||
pub record_draft: bool,
|
||
/// Record Options ▸ Show Mesh while recording.
|
||
pub record_show_mesh: bool,
|
||
}
|
||
|
||
impl Default for PuppetOptions {
|
||
fn default() -> Self {
|
||
let d = MeshOpts::default();
|
||
PuppetOptions {
|
||
density: d.density,
|
||
expansion: d.expansion,
|
||
triangles: d.triangles,
|
||
show_mesh: false,
|
||
record_speed: 100.0,
|
||
record_smoothing: 10.0,
|
||
record_draft: false,
|
||
record_show_mesh: false,
|
||
}
|
||
}
|
||
}
|