mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 11:17:12 -04:00
Puppet: a pin added to a bent mesh holds it where it is; nothing jumps (#318)
Cause: puppet::solve binds each moving pin to its nearest mesh vertex and fixes that vertex at position + (vertex - rest), an offset in rest space with no rotation or scale. puppet.addPin set a new pin's position to the click, so in a bent region the bound vertex was pulled to where it would be unrotated, and the mesh jumped as soon as the pin was added. Fix: addPin picks the position that keeps the bound vertex at its current deformed place: the vertex's deformed position minus the rest offset. On an unbent mesh that is the click itself (round-off is snapped back to it), so existing pins behave as before. Test: a_pin_added_to_a_bent_mesh_moves_nothing (two pins, one moved to bend the layer, then pins added at three places on the bent part: at most a few edge pixels change from solver round-off; before the fix about 250 did). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f (cherry picked from commit 16151203b304d565b425873fdd33ae082cae483b)
This commit is contained in:
2 files changed
+48
-6
No files matched your search
@@ -21,6 +21,10 @@ 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 dist2(a: [f64; 2], b: [f64; 2]) -> f64 {
|
||||
(a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2)
|
||||
}
|
||||
|
||||
fn pt(v: Option<&Value>) -> Option<[f64; 2]> {
|
||||
let a = v?.as_array()?;
|
||||
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])
|
||||
@@ -77,7 +81,8 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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;
|
||||
// (mesh, the pin's rest point, where it is now)
|
||||
let mut target: Option<(Uid, [f64; 2], [f64; 2])> = None;
|
||||
if let Some(fu) = fx_uid
|
||||
&& !new_mesh
|
||||
&& let Some((buf, params)) = puppet_eval(s, cid, lid, fu, t)
|
||||
@@ -90,7 +95,22 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let inside = puppet::contains(mesh, def, at);
|
||||
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)));
|
||||
let rest = puppet::unmap(mesh, def, at);
|
||||
// A moving pin holds its nearest mesh vertex at `position + (vertex - rest)`
|
||||
// (puppet::solve). Choose the position that keeps that vertex where it is now, so
|
||||
// adding a pin to a bent mesh moves nothing (After Effects, #318); on an unbent
|
||||
// mesh that is the click itself.
|
||||
let now = (0..mesh.verts.len().min(def.len()))
|
||||
.filter(|_| kind.moves())
|
||||
.min_by(|a, b| dist2(mesh.verts[*a], rest).total_cmp(&dist2(mesh.verts[*b], rest)))
|
||||
.and_then(|v| {
|
||||
let (r, d) = (mesh.verts.get(v)?, def.get(v)?);
|
||||
Some([d[0] - (r[0] - rest[0]), d[1] - (r[1] - rest[1])])
|
||||
})
|
||||
// Round-off only (an unbent mesh): exactly the click.
|
||||
.filter(|n| dist2(*n, at) > 1e-12)
|
||||
.unwrap_or(at);
|
||||
target = Some((*uid, rest, now));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -131,14 +151,14 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
};
|
||||
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),
|
||||
let (mesh_uid, rest, now) = match target {
|
||||
Some(t) => t,
|
||||
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)
|
||||
(u, at, at)
|
||||
}
|
||||
};
|
||||
let name = puppet::next_pin_name(pg, kind);
|
||||
@@ -146,7 +166,7 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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.value = KV::Vec2(now);
|
||||
pos.set_animated(true, lt);
|
||||
} else {
|
||||
pos.value = KV::Vec2(rest);
|
||||
|
||||
@@ -203,6 +203,28 @@ fn first_puppet_pin_leaves_the_layer_still() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #318: a pin added to an already-bent mesh holds the mesh where it is: nothing jumps.
|
||||
#[test]
|
||||
fn a_pin_added_to_a_bent_mesh_moves_nothing() {
|
||||
let (mut s, id) = setup();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [5, 30]})).unwrap();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [95, 30]})).unwrap();
|
||||
s.set_time(Tick::from_seconds_f64(1.0));
|
||||
s.execute("puppet.movePin", json!({"layer": id, "pin": "Puppet Pin 2", "position": [80, 70]})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
let t = Tick::from_seconds_f64(1.0);
|
||||
let before = s.render(cid, t, RenderOpts::default());
|
||||
for at in [[50, 54], [30, 42], [70, 62]] {
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": at})).unwrap();
|
||||
let after = s.render(cid, t, RenderOpts::default());
|
||||
let moved = after.data.iter().zip(&before.data).filter(|(a, b)| (a[3] - b[3]).abs() > 0.25).count();
|
||||
// (Before #318 the pin at [50, 54] moved about 250 edge pixels; the solver's round-off
|
||||
// still touches a few.)
|
||||
assert!(moved <= 16, "a pin at {at:?} moved {moved} pixels");
|
||||
s.undo();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puppet_pin_kinds_and_mesh_options() {
|
||||
let (mut s, id) = setup();
|
||||
|
||||
Reference in new issue
Block a user