diff --git a/crates/effects/src/puppet/mod.rs b/crates/effects/src/puppet/mod.rs index c7ce9ea0..6ec2ce5b 100644 --- a/crates/effects/src/puppet/mod.rs +++ b/crates/effects/src/puppet/mod.rs @@ -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]); diff --git a/crates/engine/src/commands/puppet.rs b/crates/engine/src/commands/puppet.rs index 8d11fc57..cc0c4c6b 100644 --- a/crates/engine/src/commands/puppet.rs +++ b/crates/engine/src/commands/puppet.rs @@ -87,11 +87,7 @@ fn add_pin(s: &mut Session, p: &Value) -> Result { 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))); diff --git a/crates/ui-egui/src/panels/puppet_tool.rs b/crates/ui-egui/src/panels/puppet_tool.rs index 4b0b8e9a..361a914f 100644 --- a/crates/ui-egui/src/panels/puppet_tool.rs +++ b/crates/ui-egui/src/panels/puppet_tool.rs @@ -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 { + 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) { diff --git a/crates/ui-egui/src/panels/viewer.rs b/crates/ui-egui/src/panels/viewer.rs index b2107570..52172910 100644 --- a/crates/ui-egui/src/panels/viewer.rs +++ b/crates/ui-egui/src/panels/viewer.rs @@ -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 = 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 = 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 { + 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])); diff --git a/crates/ui-egui/tests/ui_viewer.rs b/crates/ui-egui/tests/ui_viewer.rs index 61ea76ef..4a84659f 100644 --- a/crates/ui-egui/tests/ui_viewer.rs +++ b/crates/ui-egui/tests/ui_viewer.rs @@ -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::() + }; + 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 { fn walk(s: &egui::Shape, out: &mut Vec) {