mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 02:41:57 -06:00
Merge PR #2891
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
//!
|
||||
//! Pin positions are in effect 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.
|
||||
//! Position / Advanced pins get a Position keyframe at the current time, as in After Effects. Off
|
||||
//! every mesh a pin starts a new mesh, unless its art already has one (#2865).
|
||||
|
||||
use effectcraft_effects::puppet::{self, MeshOpts, PinKind};
|
||||
use effectcraft_effects::{Buf, Params};
|
||||
@@ -62,6 +63,32 @@ fn layer_time(s: &Session, cid: ItemId, lid: LayerId, t: Tick) -> Result<Tick> {
|
||||
Ok(l.layer_time(t))
|
||||
}
|
||||
|
||||
/// Whether a pin at `at` (effect space) goes on this mesh as `def` deforms it: on it or beside it.
|
||||
fn on_mesh(mesh: &puppet::Mesh, def: &[[f64; 2]], at: [f64; 2]) -> bool {
|
||||
!mesh.tris.is_empty() && (puppet::contains(mesh, def, at) || def.iter().map(|v| (v[0] - at[0]).hypot(v[1] - at[1])).fold(f64::MAX, f64::min) < 12.0)
|
||||
}
|
||||
|
||||
/// Whether a new mesh at `at` (effect space) would repeat a mesh of `params`: the art it would
|
||||
/// outline is that mesh's own, even if a deformation has carried the mesh off it (#2865).
|
||||
fn repeats_mesh(buf: &Buf, params: &Params, at: [f64; 2]) -> bool {
|
||||
let (w, h) = (buf.img.width as usize, buf.img.height as usize);
|
||||
let alpha: Vec<f32> = buf.img.data.iter().map(|p| p[3]).collect();
|
||||
puppet::parse(params).iter().any(|m| {
|
||||
let mut outline = puppet::mesh::outline(&alpha, w, h, buf.scale.max(1e-9), buf.offset, at, m.opts.expansion);
|
||||
// As `triangulate` winds it.
|
||||
if puppet::mesh::signed_area(&outline) < 0.0 {
|
||||
outline.reverse();
|
||||
}
|
||||
puppet::mesh_for(buf, m).outline == outline
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether `puppet.addPin` at `at` (effect space) places a pin on a layer with a Puppet effect.
|
||||
pub fn places_pin(s: &Session, cid: ItemId, lid: LayerId, at: [f64; 2], t: Tick) -> bool {
|
||||
let Some((buf, params)) = find_puppet(s, cid, lid).and_then(|fu| puppet_eval(s, cid, lid, fu, t)) else { return true };
|
||||
!repeats_mesh(&buf, ¶ms, at) || puppet::overlay(&buf, ¶ms).iter().any(|(_, mesh, def, _)| on_mesh(mesh, def, at))
|
||||
}
|
||||
|
||||
fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "puppet.addPin";
|
||||
let (cid, lid) = layer_p(s, p, cmd)?;
|
||||
@@ -93,9 +120,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 = 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() {
|
||||
if on_mesh(mesh, def, at) || want_mesh.is_some() {
|
||||
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
|
||||
@@ -120,6 +145,9 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
{
|
||||
return Err(bad(cmd, format!("no mesh {w}")));
|
||||
}
|
||||
if target.is_none() && repeats_mesh(&buf, ¶ms, at) {
|
||||
return Ok(json!({"pin": null, "changed": false}));
|
||||
}
|
||||
}
|
||||
let opts = MeshOpts {
|
||||
density: f_p(p, "density").unwrap_or(s.state.puppet.density),
|
||||
|
||||
@@ -225,6 +225,71 @@ fn a_pin_added_to_a_bent_mesh_moves_nothing() {
|
||||
}
|
||||
}
|
||||
|
||||
fn mesh_count(s: &mut Session, id: u64) -> usize {
|
||||
s.execute("puppet.info", json!({"layer": id})).unwrap()["meshes"].as_array().unwrap().len()
|
||||
}
|
||||
|
||||
/// #2865: where the deformed layer is empty a click makes nothing: no copy of the art it has moved
|
||||
/// off, no undo step, no error.
|
||||
#[test]
|
||||
fn a_pin_where_the_deformed_layer_is_empty_creates_nothing() {
|
||||
let (mut s, id) = setup();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [10, 30]})).unwrap();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [90, 30]})).unwrap();
|
||||
s.set_time(Tick::from_seconds_f64(1.0));
|
||||
s.execute("puppet.movePin", json!({"layer": id, "pin": "Puppet Pin 1", "position": [10, 70]})).unwrap();
|
||||
s.execute("puppet.movePin", json!({"layer": id, "pin": "Puppet Pin 2", "position": [90, 70]})).unwrap();
|
||||
let (cid, t) = (s.active_comp_id().unwrap(), Tick::from_seconds_f64(1.0));
|
||||
let before = s.render(cid, t, RenderOpts::default());
|
||||
let revision = s.revision;
|
||||
let lid = effectcraft_project::LayerId(id);
|
||||
// The solid's top, which the pins have carried 40 px down; and well off it.
|
||||
for at in [[50, 10], [300, 300]] {
|
||||
assert!(!crate::commands::puppet::places_pin(&s, cid, lid, [at[0] as f64, at[1] as f64], t), "{at:?}");
|
||||
let r = s.execute("puppet.addPin", json!({"layer": id, "position": at})).unwrap();
|
||||
assert_eq!(r["pin"], Value::Null, "{at:?}");
|
||||
assert_eq!(s.revision, revision, "{at:?} changed the project");
|
||||
}
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (1, 2));
|
||||
assert_eq!(s.render(cid, t, RenderOpts::default()), before);
|
||||
// Whatever Expansion new meshes would now get, the art is the mesh's own.
|
||||
s.state.puppet.expansion = 9.0;
|
||||
assert_eq!(s.execute("puppet.addPin", json!({"layer": id, "position": [50, 10]})).unwrap()["pin"], Value::Null);
|
||||
// On the deformed shape a pin still goes on the mesh.
|
||||
assert!(crate::commands::puppet::places_pin(&s, cid, lid, [50.0, 70.0], t));
|
||||
assert!(s.execute("puppet.addPin", json!({"layer": id, "position": [50, 70]})).unwrap()["pin"].is_u64());
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (1, 3));
|
||||
}
|
||||
|
||||
/// #2865: separate art still gets its own mesh, also from a near miss in the gap beside it (a click
|
||||
/// off the art finds the art nearest it): only a repeat of a mesh is refused.
|
||||
#[test]
|
||||
fn separate_art_still_gets_its_own_mesh() {
|
||||
let (mut s, id) = setup();
|
||||
for x in [[0, 30], [60, 100]] {
|
||||
s.execute("mask.new", json!({"layer": id, "vertices": [[x[0], 0], [x[1], 0], [x[1], 60], [x[0], 60]], "closed": true})).unwrap();
|
||||
}
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [10, 30]})).unwrap();
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (1, 1));
|
||||
// In the gap, nearer the right piece and past reach of the left piece's mesh.
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [46, 30]})).unwrap();
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (2, 2), "the near miss meshes the right piece");
|
||||
s.undo();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [80, 30]})).unwrap();
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (2, 2), "the right piece gets its own mesh");
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [80, 50]})).unwrap();
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (2, 3), "and the next pin goes on it");
|
||||
}
|
||||
|
||||
/// `newMesh` asks for a new mesh wherever the click is, the art's own mesh or not.
|
||||
#[test]
|
||||
fn new_mesh_option_starts_a_mesh_on_meshed_art() {
|
||||
let (mut s, id) = setup();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [10, 30]})).unwrap();
|
||||
s.execute("puppet.addPin", json!({"layer": id, "position": [50, 30], "newMesh": true})).unwrap();
|
||||
assert_eq!((mesh_count(&mut s, id), pin_count(&s, id)), (2, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puppet_pin_kinds_and_mesh_options() {
|
||||
let (mut s, id) = setup();
|
||||
|
||||
Reference in New Issue
Block a user