Puppet: the tools stay on the rigged layer and pins go on the deformed mesh (#284)
With a Puppet tool, a click off the pins picked the topmost layer under the pointer by its own bounds, and selected it. So a click where the deformation had stretched the mesh past the layer's bounds went to the layer behind (selecting it and giving it a Puppet effect), and any click on another layer switched to it. While selected layers have a Puppet effect, a click (or a press that might start a pin marquee) now works only on them, as in After Effects: on the layer whose mesh, as deformed now, is under the pointer (the pin lands on the deformed shape), else the one whose own art is (a new mesh); elsewhere nothing happens and the selection stays. Without a rigged layer selected, the tools pick and rig the layer under the pointer as before. `puppet::contains` tests a point against a deformed mesh; `puppet.addPin` uses it too (its own test only held for one triangle winding). Test: puppet_pins_go_on_the_deformed_mesh_and_the_tool_stays_on_the_rigged_layer (ui_viewer). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
065f0c91e7
commit
02bc2b8f9a
|
|
@ -387,6 +387,14 @@ pub fn unmap(mesh: &Mesh, deformed: &[[f64; 2]], p: [f64; 2]) -> [f64; 2] {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether `p` (layer space) is on the mesh as `deformed` places its vertices.
|
||||
pub fn contains(mesh: &Mesh, deformed: &[[f64; 2]], p: [f64; 2]) -> bool {
|
||||
mesh.tris.iter().any(|t| match (deformed.get(t[0]), deformed.get(t[1]), deformed.get(t[2])) {
|
||||
(Some(a), Some(b), Some(c)) => bary(*a, *b, *c, p).is_some_and(|l| l.iter().all(|x| *x >= -1e-9)),
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn bary(a: [f64; 2], b: [f64; 2], c: [f64; 2], p: [f64; 2]) -> Option<[f64; 3]> {
|
||||
let d = (b[1] - c[1]) * (a[0] - c[0]) + (c[0] - b[0]) * (a[1] - c[1]);
|
||||
|
|
|
|||
|
|
@ -87,11 +87,7 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
|||
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 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)));
|
||||
|
|
|
|||
|
|
@ -82,6 +82,20 @@ pub fn overlay(app: &EffectcraftApp, ctx: &egui::Context, cid: ItemId, layer: &L
|
|||
Some(overlay)
|
||||
}
|
||||
|
||||
/// The layer a Puppet tool press at comp point `cpt` works on, of the selected `layers` with a
|
||||
/// Puppet effect (stack order): the one whose mesh, as deformed now, is under it (pins go on the
|
||||
/// deformed shape, also where it reaches past the layer's own bounds), else the one whose art is
|
||||
/// (a new mesh there). Never a layer without a Puppet effect: the tools stay on the layers being
|
||||
/// rigged, as in After Effects (#284).
|
||||
pub fn target(ectx: &EvalCtx, layers: &[(&Layer, Overlay)], cpt: [f64; 2], on_art: impl Fn(&Layer) -> bool) -> Option<LayerId> {
|
||||
let on_mesh = |l: &Layer, ov: &Overlay| {
|
||||
let Some(inv) = l2c(ectx, l).0.inverse() else { return false };
|
||||
let p = inv.apply(gv2(cpt[0], cpt[1]));
|
||||
ov.iter().any(|(_, mesh, def, _)| puppet::contains(mesh, def, [p.x, p.y]))
|
||||
};
|
||||
layers.iter().find(|(l, ov)| on_mesh(l, ov)).or_else(|| layers.iter().find(|(l, _)| on_art(l))).map(|(l, _)| l.id)
|
||||
}
|
||||
|
||||
/// Keep a pin's keyframed Position in view in the Timeline after placing, moving or recording the
|
||||
/// pin in the viewer (#273).
|
||||
pub fn reveal_pin(app: &mut EffectcraftApp, layer: LayerId, pin: u64) {
|
||||
|
|
|
|||
|
|
@ -919,6 +919,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
|
||||
// Puppet pins and meshes of the selected layers (Puppet tools).
|
||||
let mut pin_hits: Vec<super::puppet_tool::PinHit> = vec![];
|
||||
// The selected layers with a Puppet effect, top of the stack first (the layers the Puppet
|
||||
// tools work on).
|
||||
let mut puppet_layers: Vec<(&Layer, super::puppet_tool::Overlay)> = vec![];
|
||||
if app.ui.tool.puppet_kind().is_some() {
|
||||
let sel_props: Vec<u64> = app.session.state.selected_props.iter().map(|(_, u)| *u).collect();
|
||||
for lid in &selected {
|
||||
|
|
@ -927,8 +930,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
{
|
||||
let hover = ui.input(|i| i.pointer.hover_pos());
|
||||
pin_hits.extend(super::puppet_tool::draw(&painter, &map, &ectx, l, &ov, app.session.state.puppet.show_mesh, &sel_props, hover));
|
||||
puppet_layers.push((l, ov));
|
||||
}
|
||||
}
|
||||
puppet_layers.sort_by_key(|(l, _)| comp.index_of(l.id));
|
||||
for h in &pin_hits {
|
||||
app.auto.add(&format!("viewer.puppetPin.{}", h.pin), Rect::from_center_size(h.pos, vec2(10.0, 10.0)), "Puppet pin");
|
||||
if let Some(hp) = h.handle {
|
||||
|
|
@ -938,6 +943,14 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
}
|
||||
// The layer a Puppet tool press works on: while selected layers are rigged, only those
|
||||
// ([`super::puppet_tool::target`]); otherwise the layer under the pointer, selected.
|
||||
let puppet_layer = |app: &mut EffectcraftApp, cpt: [f64; 2]| -> Option<LayerId> {
|
||||
if puppet_layers.is_empty() {
|
||||
return pick(app, &ectx, cpt, false);
|
||||
}
|
||||
super::puppet_tool::target(&ectx, &puppet_layers, cpt, |l| layers_at(&ectx, cpt).any(|h| h.id == l.id))
|
||||
};
|
||||
|
||||
// Project items, files and effects dropped on the viewer.
|
||||
super::viewer_drop::show(app, ui, &painter, &map, &ectx);
|
||||
|
|
@ -1121,7 +1134,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
t if t.is_shape() => Some(Gesture::Create { tool: t, start: cpt }),
|
||||
t if t.puppet_kind().is_some() && super::puppet_tool::hit(&pin_hits, press).is_none() => {
|
||||
// Off the pins: Alt or a press outside the art drags a marquee that selects pins.
|
||||
(mods.alt || pick(app, &ectx, cpt, false).is_none()).then_some(Gesture::PinMarquee { start: press })
|
||||
(mods.alt || puppet_layer(app, cpt).is_none()).then_some(Gesture::PinMarquee { start: press })
|
||||
}
|
||||
t if t.puppet_kind().is_some() => super::puppet_tool::hit(&pin_hits, press).and_then(|(h, part)| {
|
||||
use super::puppet_tool::PinPart;
|
||||
|
|
@ -1557,7 +1570,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if let Some((h, _)) = super::puppet_tool::hit(&pin_hits, pos) {
|
||||
// Click selects the pin; Shift-click adds or removes it.
|
||||
let _ = app.session.execute("puppet.selectPins", json!({"layer": h.layer.0, "pins": [h.pin], "toggle": mods.shift}));
|
||||
} else if let Some(l) = pick(app, &ectx, cpt, false).and_then(|l| comp.layer(l))
|
||||
} else if let Some(l) = puppet_layer(app, cpt).and_then(|l| comp.layer(l))
|
||||
&& let Some(inv) = l2c(&ectx, l).0.inverse()
|
||||
{
|
||||
let lp = inv.apply(gv2(cpt[0], cpt[1]));
|
||||
|
|
|
|||
|
|
@ -891,6 +891,43 @@ fn placing_a_pin_opens_its_groups_on_a_twirled_open_layer() {
|
|||
assert_eq!(timeline_keys(&h, pin_keys(&h, id, pin).0).len(), 1, "the new pin's Position key shows");
|
||||
}
|
||||
|
||||
/// #284: the Puppet tools stay on the layer being rigged: a pin goes on its mesh where the
|
||||
/// deformation has stretched it past the layer's own bounds (over another layer), and a click on
|
||||
/// another layer neither selects it nor rigs it.
|
||||
#[test]
|
||||
fn puppet_pins_go_on_the_deformed_mesh_and_the_tool_stays_on_the_rigged_layer() {
|
||||
let mut h = harness();
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("layer.select", json!({"layers": ["Box"]})).unwrap();
|
||||
let (boxl, plate) = (s.state.selected_layers[0], s.active_comp().unwrap().layers[1].id);
|
||||
s.execute("puppet.addPin", json!({"layer": boxl.0, "position": [10, 40]})).unwrap();
|
||||
let right = s.execute("puppet.addPin", json!({"layer": boxl.0, "position": [70, 40]})).unwrap()["pin"].as_u64().unwrap();
|
||||
// Stretch the Box to the right: its right edge (layer x 80, comp x 360) moves well past 400.
|
||||
s.execute("puppet.movePin", json!({"layer": boxl.0, "pin": right, "position": [140, 40]})).unwrap();
|
||||
h.state_mut().ui.tool = Tool::Puppet;
|
||||
h.run_steps(3);
|
||||
let pins = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
let info = h.state_mut().session.execute("puppet.info", json!({"layer": boxl.0})).unwrap();
|
||||
info["meshes"].as_array().unwrap().iter().map(|m| m["pins"].as_array().unwrap().len()).sum::<usize>()
|
||||
};
|
||||
let rigged = |h: &Harness<'_, EffectcraftApp>, l: LayerId| {
|
||||
h.state().session.active_comp().unwrap().layer(l).unwrap().effects().is_some_and(|f| f.groups().next().is_some())
|
||||
};
|
||||
// Comp (380, 180) is layer (100, 40): past the Box's own edge, over the Plate, on the
|
||||
// stretched mesh.
|
||||
let at = screen(&h, [380.0, 180.0]);
|
||||
click(&mut h, at);
|
||||
assert_eq!(pins(&mut h), 3, "the pin went on the deformed mesh");
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![boxl]);
|
||||
assert!(!rigged(&h, plate), "the Plate got no Puppet");
|
||||
// Far from the Box, on the Plate only: nothing.
|
||||
let at = screen(&h, [40.0, 40.0]);
|
||||
click(&mut h, at);
|
||||
assert_eq!(pins(&mut h), 3);
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![boxl], "the tool stays on the Box");
|
||||
assert!(!rigged(&h, plate));
|
||||
}
|
||||
|
||||
/// Filled circles painted this frame (flattening nested shape lists).
|
||||
fn circles(h: &Harness<'_, EffectcraftApp>) -> Vec<egui::epaint::CircleShape> {
|
||||
fn walk(s: &egui::Shape, out: &mut Vec<egui::epaint::CircleShape>) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue