mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
M8.6: Puppet Warp pins appear at once, warp from the rest shape and follow Undo
- Pins show as soon as the tool has art selected: automatic ones at the centre and the middle of each limb's end (at least three), until pins are placed. They stay across tool switches while the selection is unchanged and a new selection starts afresh. - The pins live in the document (Document.puppet, never saved) with the rest shape they started from: every warp starts from it, so warps don't stack and dragging a pin back restores the original; Undo/Redo take the pins back with the art. Adding and deleting pins are undo steps too. - object.puppetWarp gains rest: true (the tool's pins); the new query object.puppetWarp.pins reads them. A click off the mesh adds nothing and says so (not-allowed cursor, canvas label); the command refuses such pins. - The mesh grid covers the expanded art, so tiny art with a wide Expand no longer allocates millions of cells. - Tests on groups, compound paths and open strokes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
47ef223a02
commit
117aa0b5a3
@@ -26,7 +26,7 @@ use crate::Tool;
|
||||
|
||||
pub use liquify::LiquifyTool;
|
||||
pub use perspective::{PerspectiveGridTool, PerspectiveSelectionTool};
|
||||
pub use puppet::{PuppetWarpTool, mesh_for, mesh_input};
|
||||
pub use puppet::{PinSet, PuppetWarpTool, mesh_for, mesh_input, rest_and_pins, warp_from_rest};
|
||||
pub use width::WidthTool;
|
||||
|
||||
/// Create a distortion tool by id (None = not one of ours).
|
||||
|
||||
@@ -132,7 +132,8 @@ impl Mesh {
|
||||
/// Build a mesh covering `bounds` restricted to the region of `outlines` (filled interiors of
|
||||
/// closed subpaths plus a band around every path). With no outlines, the whole bounds.
|
||||
pub fn build(outlines: &[BezPath], bounds: Rect, opt: MeshOptions) -> Mesh {
|
||||
let side = bounds.width().max(bounds.height()).max(1e-3);
|
||||
// The cells cover the expanded art, so tiny art with a wide Expand stays a small grid.
|
||||
let side = (bounds.width().max(bounds.height()) + 2.0 * opt.expand.max(0.0)).max(1e-3);
|
||||
let cell = (side / opt.resolution.max(1) as f64).max(1e-3);
|
||||
let pad = opt.expand.max(0.0) + cell * 0.01;
|
||||
let b = bounds.inflate(pad, pad);
|
||||
@@ -267,6 +268,99 @@ impl Mesh {
|
||||
let [a, b, c] = self.tris[t].map(|i| pos[i]);
|
||||
(b - a).cross(c - a) / 2.0
|
||||
}
|
||||
|
||||
/// Is `p` (rest space) on the mesh?
|
||||
pub fn contains(&self, p: Point) -> bool {
|
||||
self.cell_at(p).is_some_and(|k| self.cells.get(k).is_some_and(Option::is_some))
|
||||
}
|
||||
|
||||
/// The grid cell under `p`, if inside the grid.
|
||||
fn cell_at(&self, p: Point) -> Option<usize> {
|
||||
let fi = ((p.x - self.origin.x) / self.cell).floor();
|
||||
let fj = ((p.y - self.origin.y) / self.cell).floor();
|
||||
(fi >= 0.0 && fj >= 0.0 && fi < self.nx as f64 && fj < self.ny as f64).then(|| fj as usize * self.nx + fi as usize)
|
||||
}
|
||||
|
||||
/// The rest-space point that the `deformed` mesh shows at `q` (None: `q` is off the mesh).
|
||||
/// Where the warp folds the mesh over itself, the triangle `q` is deepest inside wins.
|
||||
pub fn unmap(&self, deformed: &[Point], q: Point) -> Option<Point> {
|
||||
let mut best: Option<(f64, Point)> = None;
|
||||
for t in &self.tris {
|
||||
let [a, b, c] = t.map(|i| deformed.get(i).copied().unwrap_or(self.verts[i]));
|
||||
let den = (b - a).cross(c - a);
|
||||
if den.abs() < 1e-18 {
|
||||
continue;
|
||||
}
|
||||
let l1 = (q - a).cross(c - a) / den;
|
||||
let l2 = (b - a).cross(q - a) / den;
|
||||
let w = [1.0 - l1 - l2, l1, l2];
|
||||
let depth = w.iter().copied().fold(f64::MAX, f64::min);
|
||||
if depth >= -1e-9 && best.is_none_or(|(d, _)| depth > d) {
|
||||
let [ra, rb, rc] = t.map(|i| self.verts[i].to_vec2());
|
||||
best = Some((depth, (ra * w[0] + rb * w[1] + rc * w[2]).to_point()));
|
||||
}
|
||||
}
|
||||
best.map(|b| b.1)
|
||||
}
|
||||
|
||||
/// How far (radians) the `deformed` mesh turned the art at rest point `p`.
|
||||
pub fn turn_at(&self, deformed: &[Point], p: Point) -> Option<f64> {
|
||||
let (t, _) = self.locate(p)?;
|
||||
let tri = self.tris.get(t)?;
|
||||
let cur = [*deformed.get(tri[0])?, *deformed.get(tri[1])?, *deformed.get(tri[2])?];
|
||||
Some(fit_angle(&tri.map(|i| self.verts[i]), &cur))
|
||||
}
|
||||
|
||||
/// Centre of grid cell `k`.
|
||||
fn cell_centre(&self, k: usize) -> Point {
|
||||
Point::new(self.origin.x + ((k % self.nx) as f64 + 0.5) * self.cell, self.origin.y + ((k / self.nx) as f64 + 0.5) * self.cell)
|
||||
}
|
||||
|
||||
/// The cells pins go in: those inside the art (or every kept cell when none is).
|
||||
fn pin_cells(&self) -> Vec<bool> {
|
||||
let any_core = self.core.iter().any(|c| *c);
|
||||
self.cells.iter().zip(&self.core).map(|(c, core)| c.is_some() && (*core || !any_core)).collect()
|
||||
}
|
||||
|
||||
/// The 8 neighbours of cell `k` inside the grid, with the step length (in cells).
|
||||
fn neighbours(&self, k: usize) -> impl Iterator<Item = (usize, f64)> + use<> {
|
||||
let (nx, ny) = (self.nx as i64, self.ny as i64);
|
||||
let (i, j) = ((k % self.nx) as i64, (k / self.nx) as i64);
|
||||
(-1i64..=1).flat_map(move |dj| (-1i64..=1).map(move |di| (di, dj))).filter_map(move |(di, dj)| {
|
||||
let (a, b) = (i + di, j + dj);
|
||||
let step = if di != 0 && dj != 0 { std::f64::consts::SQRT_2 } else { 1.0 };
|
||||
((di, dj) != (0, 0) && a >= 0 && b >= 0 && a < nx && b < ny).then_some(((b * nx + a) as usize, step))
|
||||
})
|
||||
}
|
||||
|
||||
/// Distances (in cells, through the cells where `inside`) from `sources` (cells with their
|
||||
/// start distances) to every cell; cells out of reach stay at infinity.
|
||||
fn geodesic(&self, inside: &[bool], sources: &[(usize, f64)]) -> Vec<f64> {
|
||||
let mut dist = vec![f64::INFINITY; self.cells.len()];
|
||||
for &(k, d) in sources {
|
||||
if let Some(v) = dist.get_mut(k) {
|
||||
*v = v.min(d);
|
||||
}
|
||||
}
|
||||
let inside = |k: usize| inside.get(k).copied().unwrap_or(false);
|
||||
// Relax until nothing improves (the grid is small: a few dozen cells a side).
|
||||
let mut changed = true;
|
||||
while changed {
|
||||
changed = false;
|
||||
for k in 0..dist.len() {
|
||||
if !inside(k) || !dist[k].is_finite() {
|
||||
continue;
|
||||
}
|
||||
for (m, step) in self.neighbours(k) {
|
||||
if inside(m) && dist[k] + step < dist[m] - 1e-12 {
|
||||
dist[m] = dist[k] + step;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dist
|
||||
}
|
||||
}
|
||||
|
||||
/// Winding number of the closed subpaths of `o` at `p` (open subpaths ignored).
|
||||
@@ -401,16 +495,8 @@ pub fn deform(mesh: &Mesh, pins: &[Pin]) -> Vec<Point> {
|
||||
for tri in &mesh.tris {
|
||||
let rest = tri.map(|i| mesh.verts[i]);
|
||||
let cur = tri.map(|i| step1[i]);
|
||||
let rc = centroid(&rest);
|
||||
let cc = centroid(&cur);
|
||||
let (mut sc, mut ss) = (0.0, 0.0);
|
||||
for k in 0..3 {
|
||||
let (r, c) = (rest[k] - rc, cur[k] - cc);
|
||||
sc += r.dot(c);
|
||||
ss += r.cross(c);
|
||||
}
|
||||
let th = ss.atan2(sc);
|
||||
let (s, co) = th.sin_cos();
|
||||
let (rc, cc) = (centroid(&rest), centroid(&cur));
|
||||
let (s, co) = fit_angle(&rest, &cur).sin_cos();
|
||||
let fit = rest.map(|r| {
|
||||
let d = r - rc;
|
||||
cc + Vec2::new(d.x * co - d.y * s, d.x * s + d.y * co)
|
||||
@@ -455,24 +541,88 @@ fn centroid(p: &[Point; 3]) -> Point {
|
||||
Point::new((p[0].x + p[1].x + p[2].x) / 3.0, (p[0].y + p[1].y + p[2].y) / 3.0)
|
||||
}
|
||||
|
||||
/// The rotation (radians) that best fits triangle `rest` onto `cur` (least squares, about their
|
||||
/// centroids).
|
||||
fn fit_angle(rest: &[Point; 3], cur: &[Point; 3]) -> f64 {
|
||||
let (rc, cc) = (centroid(rest), centroid(cur));
|
||||
let (mut sc, mut ss) = (0.0, 0.0);
|
||||
for k in 0..3 {
|
||||
let (r, c) = (rest[k] - rc, cur[k] - cc);
|
||||
sc += r.dot(c);
|
||||
ss += r.cross(c);
|
||||
}
|
||||
ss.atan2(sc)
|
||||
}
|
||||
|
||||
/// Pick `count` well-spread pin positions inside the mesh (farthest-point sampling over the
|
||||
/// kept cell centres, starting from the one nearest the centroid).
|
||||
pub fn auto_pins(mesh: &Mesh, count: usize) -> Vec<Point> {
|
||||
let centres: Vec<Point> = mesh
|
||||
.cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(k, c)| c.is_some() && (mesh.core[*k] || !mesh.core.iter().any(|x| *x)))
|
||||
.map(|(k, _)| Point::new(mesh.origin.x + ((k % mesh.nx) as f64 + 0.5) * mesh.cell, mesh.origin.y + ((k / mesh.nx) as f64 + 0.5) * mesh.cell))
|
||||
let mut out: Vec<Point> = centre_of(mesh).map(|c| c.1).into_iter().collect();
|
||||
spread_pins(mesh, &mut out, count);
|
||||
out
|
||||
}
|
||||
|
||||
/// The centre of the cells pins go in: the cell nearest their centroid, and the centroid itself
|
||||
/// (that cell's centre when the centroid falls off the art, as in a ring).
|
||||
fn centre_of(mesh: &Mesh) -> Option<(usize, Point)> {
|
||||
let cells: Vec<usize> = mesh.pin_cells().iter().enumerate().filter(|(_, c)| **c).map(|(k, _)| k).collect();
|
||||
let c = (cells.iter().fold(Vec2::ZERO, |a, k| a + mesh.cell_centre(*k).to_vec2()) / cells.len().max(1) as f64).to_point();
|
||||
let k = cells.into_iter().min_by(|a, b| mesh.cell_centre(*a).distance_squared(c).total_cmp(&mesh.cell_centre(*b).distance_squared(c)))?;
|
||||
Some((k, if mesh.contains(c) { c } else { mesh.cell_centre(k) }))
|
||||
}
|
||||
|
||||
/// The most pins placed automatically.
|
||||
const MAX_AUTO_PINS: usize = 12;
|
||||
|
||||
/// The pins the Puppet Warp tool places by itself: one at the centre of the art and one at the
|
||||
/// end of each limb (a part reaching well beyond the art's thickness, like the arms of a star or
|
||||
/// the ends of a long bar); at least three in all where the art has room.
|
||||
pub fn default_pins(mesh: &Mesh) -> Vec<Point> {
|
||||
let Some((centre, middle)) = centre_of(mesh) else { return vec![] };
|
||||
let inside = mesh.pin_cells();
|
||||
// Thickness: the largest distance from inside the art to its edge.
|
||||
let edge: Vec<(usize, f64)> = (0..inside.len())
|
||||
.filter(|k| inside[*k] && (mesh.neighbours(*k).count() < 8 || mesh.neighbours(*k).any(|(m, _)| !inside[m])))
|
||||
.map(|k| (k, 0.5))
|
||||
.collect();
|
||||
if centres.is_empty() {
|
||||
return vec![];
|
||||
let radius = mesh.geodesic(&inside, &edge).into_iter().filter(|d| d.is_finite()).fold(0.0, f64::max);
|
||||
// Limb ends: the cells farthest from the centre (through the art), well beyond the thickness.
|
||||
let from_centre = mesh.geodesic(&inside, &[(centre, 0.0)]);
|
||||
let mut ends: Vec<usize> = (0..inside.len())
|
||||
.filter(|k| {
|
||||
let d = from_centre[*k];
|
||||
inside[*k] && d.is_finite() && d >= 1.5 * radius && mesh.neighbours(*k).all(|(m, _)| !inside[m] || from_centre[m] <= d)
|
||||
})
|
||||
.collect();
|
||||
ends.sort_by(|a, b| from_centre[*b].total_cmp(&from_centre[*a]));
|
||||
// One pin per limb: ends closer than about the art's width belong to the same limb.
|
||||
let apart = 2.2 * radius * mesh.cell;
|
||||
let mut out = vec![middle];
|
||||
let mut tips: Vec<Point> = vec![];
|
||||
for k in ends {
|
||||
let p = mesh.cell_centre(k);
|
||||
if out.len() >= MAX_AUTO_PINS || tips.iter().any(|q| q.distance(p) < apart) {
|
||||
continue;
|
||||
}
|
||||
tips.push(p);
|
||||
// The pin goes in the middle of the limb's end: the centroid of the cells about as far out
|
||||
// near its tip (the tip itself if that falls off the art).
|
||||
let cap: Vec<Point> = (0..inside.len())
|
||||
.filter(|m| inside[*m] && from_centre[*m] >= from_centre[k] - radius)
|
||||
.map(|m| mesh.cell_centre(m))
|
||||
.filter(|q| q.distance(p) < apart)
|
||||
.collect();
|
||||
let mid = (cap.iter().fold(Vec2::ZERO, |a, q| a + q.to_vec2()) / cap.len().max(1) as f64).to_point();
|
||||
out.push(if mesh.contains(mid) { mid } else { p });
|
||||
}
|
||||
let c = centres.iter().fold(Vec2::ZERO, |a, p| a + p.to_vec2()) / centres.len() as f64;
|
||||
let Some(&first) = centres.iter().min_by(|a, b| a.distance_squared(c.to_point()).total_cmp(&b.distance_squared(c.to_point()))) else {
|
||||
return vec![];
|
||||
};
|
||||
let mut out = vec![first];
|
||||
spread_pins(mesh, &mut out, 3);
|
||||
out
|
||||
}
|
||||
|
||||
/// Add pins to `out` until there are `count`, each as far as possible from the others
|
||||
/// (farthest-point sampling over the cells pins go in).
|
||||
fn spread_pins(mesh: &Mesh, out: &mut Vec<Point>, count: usize) {
|
||||
let centres: Vec<Point> = mesh.pin_cells().iter().enumerate().filter(|(_, c)| **c).map(|(k, _)| mesh.cell_centre(k)).collect();
|
||||
while out.len() < count.min(centres.len()) {
|
||||
let Some(&next) = centres.iter().max_by(|a, b| {
|
||||
let da = out.iter().map(|o| o.distance_squared(**a)).fold(f64::MAX, f64::min);
|
||||
@@ -483,7 +633,6 @@ pub fn auto_pins(mesh: &Mesh, count: usize) -> Vec<Point> {
|
||||
};
|
||||
out.push(next);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -623,6 +772,46 @@ mod tests {
|
||||
assert!(m.map(&d, Point::new(190.0, 80.0)).distance(Point::new(190.0, 80.0)) < 5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_pins_go_at_the_centre_and_the_end_of_each_limb() {
|
||||
// A long bar: the centre and both ends.
|
||||
let m = rect_mesh();
|
||||
let p = default_pins(&m);
|
||||
assert_eq!(p.len(), 3, "{p:?}");
|
||||
assert!(p[0].distance(Point::new(100.0, 50.0)) < 10.0, "{p:?}");
|
||||
let mut xs: Vec<f64> = p[1..].iter().map(|q| q.x).collect();
|
||||
xs.sort_by(f64::total_cmp);
|
||||
assert!(xs[0] < 25.0 && xs[1] > 175.0, "{p:?}");
|
||||
// A five-pointed star: the centre and its five tips.
|
||||
let star = vectorcraft_geom::shapes::star(Point::new(100.0, 100.0), 100.0, 35.0, 5, 0.0).to_bezpath();
|
||||
let sm = Mesh::build(std::slice::from_ref(&star), star.bounding_box(), MeshOptions { resolution: 30, expand: 0.0 });
|
||||
let p = default_pins(&sm);
|
||||
assert_eq!(p.len(), 6, "{p:?}");
|
||||
assert!(p[1..].iter().all(|q| q.distance(Point::new(100.0, 100.0)) > 55.0), "{p:?}");
|
||||
// A disc has no limbs: the centre, topped up to three.
|
||||
let disc = vectorcraft_geom::kurbo::Circle::new((50.0, 50.0), 50.0).to_path(0.1);
|
||||
let dm = Mesh::build(std::slice::from_ref(&disc), disc.bounding_box(), MeshOptions::default());
|
||||
assert_eq!(default_pins(&dm).len(), 3);
|
||||
// A point with a wide Expand stays a small grid.
|
||||
let dot = Mesh::build(&[], Rect::new(5.0, 5.0, 5.0, 5.0), MeshOptions { expand: 1000.0, ..Default::default() });
|
||||
assert!(dot.verts.len() < 30 * 30 && default_pins(&dot).len() == 3, "{}", dot.verts.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmap_and_turn_read_a_warped_mesh_back() {
|
||||
let m = rect_mesh();
|
||||
let rot = |p: Point| {
|
||||
let (s, c) = 0.3f64.sin_cos();
|
||||
Point::new(p.x * c - p.y * s + 20.0, p.x * s + p.y * c)
|
||||
};
|
||||
let d: Vec<Point> = m.verts.iter().map(|v| rot(*v)).collect();
|
||||
let p = Point::new(60.0, 40.0);
|
||||
assert!(m.unmap(&d, rot(p)).unwrap().distance(p) < 1e-6);
|
||||
assert!(m.unmap(&d, Point::new(-500.0, 0.0)).is_none(), "off the mesh");
|
||||
assert!((m.turn_at(&d, p).unwrap() - 0.3).abs() < 1e-9);
|
||||
assert!(m.contains(p) && !m.contains(Point::new(300.0, 50.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_pins_are_spread_inside_the_shape() {
|
||||
let m = rect_mesh();
|
||||
|
||||
+508
-209
@@ -1,31 +1,42 @@
|
||||
//! Puppet Warp tool.
|
||||
//!
|
||||
//! With artwork selected the tool places three pins automatically (spread over the mesh). A click
|
||||
//! on the artwork adds a pin and selects it; Shift-click adds pins to the selection (or takes them
|
||||
//! out); dragging a pin moves every selected pin and warps the art; Alt-dragging near (not on) a
|
||||
//! selected pin turns the art around it, and that pin keeps the turn through later warps;
|
||||
//! Delete/Backspace remove the selected pins. Every drag previews
|
||||
//! `object.puppetWarp {ids, pins, moved, angles, expand}` (pins = current pin positions, moved =
|
||||
//! where they go, angles = the turn held at each pin), committed on release as one undo step. The
|
||||
//! mesh follows the current shape, so successive drags compose.
|
||||
//! With artwork selected the tool shows pins at once: the ones already placed on it, else
|
||||
//! automatic pins at the centre and the end of each limb ([`default_pins`]). A click on the artwork
|
||||
//! adds a pin and selects it; Shift-click adds pins to the selection (or takes them out); dragging
|
||||
//! a pin moves every selected pin and warps the art; Alt-dragging near (not on) a selected pin
|
||||
//! turns the art around it, and that pin keeps the turn; Delete/Backspace remove the selected pins.
|
||||
//! A click off the mesh places nothing and says so.
|
||||
//!
|
||||
//! The pins live in the document ([`PuppetPins`]) with the rest shape they started from, so every
|
||||
//! warp starts from that shape (dragging a pin back restores the original, warps don't stack) and
|
||||
//! Undo/Redo take the pins back with the art. They stay while the same artwork is selected; a new
|
||||
//! selection starts afresh. Every change runs `object.puppetWarp {rest: true, ids, pins, moved,
|
||||
//! angles, expand}` (pins = rest positions, moved = where they are now): adding or deleting pins
|
||||
//! at once, drags as a preview committed on release, each one undo step.
|
||||
//!
|
||||
//! Control bar (`tool.setOption`): `showMesh` (on by default), `expand` (Expand Mesh, points, 0
|
||||
//! allowed) and `selectAllPins`.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Document, NodeId};
|
||||
use vectorcraft_doc::{Document, Node, NodeId, PuppetPin, PuppetPins};
|
||||
use vectorcraft_geom::{BezPath, Point, Rect};
|
||||
|
||||
use super::arap::{Mesh, MeshOptions, auto_pins};
|
||||
use super::{BLUE, collect_points};
|
||||
use super::arap::{self, Mesh, MeshOptions, default_pins};
|
||||
use super::{BLUE, collect_points, warp_node_with};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// Outlines and bounds of the given objects (what the mesh is built over).
|
||||
pub fn mesh_input(doc: &Document, ids: &[NodeId]) -> Option<(Vec<BezPath>, Rect)> {
|
||||
let nodes: Vec<&Node> = ids.iter().map(|id| doc.node(*id)).collect::<Option<_>>()?;
|
||||
outlines_of(&nodes)
|
||||
}
|
||||
|
||||
fn outlines_of(nodes: &[&Node]) -> Option<(Vec<BezPath>, Rect)> {
|
||||
let mut outlines = vec![];
|
||||
let mut pts = vec![];
|
||||
for id in ids {
|
||||
let n = doc.node(*id)?;
|
||||
for n in nodes {
|
||||
collect_points(n, &mut pts);
|
||||
n.walk(&mut |c| {
|
||||
if let Some(p) = c.path_data() {
|
||||
@@ -44,108 +55,264 @@ pub fn mesh_for(doc: &Document, ids: &[NodeId], expand: f64) -> Option<Mesh> {
|
||||
Some(Mesh::build(&o, b, MeshOptions { expand, ..Default::default() }))
|
||||
}
|
||||
|
||||
/// The mesh over `nodes` (a rest shape).
|
||||
fn mesh_of(nodes: &[Arc<Node>], expand: f64) -> Option<Mesh> {
|
||||
let refs: Vec<&Node> = nodes.iter().map(|n| n.as_ref()).collect();
|
||||
let (o, b) = outlines_of(&refs)?;
|
||||
Some(Mesh::build(&o, b, MeshOptions { expand, ..Default::default() }))
|
||||
}
|
||||
|
||||
/// The solver's view of the pins.
|
||||
fn solver_pins(pins: &[PuppetPin]) -> Vec<arap::Pin> {
|
||||
pins.iter().map(|q| arap::Pin { angle: q.angle, ..arap::Pin::new(q.rest, q.at) }).collect()
|
||||
}
|
||||
|
||||
/// Do the pins leave the art as it is (each where it started, turned by nothing)?
|
||||
fn at_rest(pins: &[PuppetPin]) -> bool {
|
||||
pins.iter().all(|q| q.rest == q.at && q.angle.is_none_or(|a| a == 0.0))
|
||||
}
|
||||
|
||||
/// A rest shape and the pins placed on it (`None`: none yet).
|
||||
pub type RestPins = (Vec<Arc<Node>>, Option<Vec<PuppetPin>>);
|
||||
|
||||
/// The rest shape of `ids` and the pins placed on it: the document's pins when they belong to
|
||||
/// these objects as the last warp left them; when something else edited them since, the same pins
|
||||
/// starting again from the objects as they are; `None` pins: none placed yet.
|
||||
pub fn rest_and_pins(doc: &Document, ids: &[NodeId]) -> Option<RestPins> {
|
||||
let current: Vec<Arc<Node>> = ids.iter().map(|id| doc.node_arc(*id).cloned()).collect::<Option<_>>()?;
|
||||
Some(match doc.puppet.as_deref().filter(|p| p.ids == ids) {
|
||||
Some(p) if p.holds(doc, ids) => (p.rest.clone(), Some(p.pins.clone())),
|
||||
Some(p) => (current, Some(p.pins.iter().map(|q| PuppetPin { rest: q.at, at: q.at, angle: q.angle.map(|_| 0.0) }).collect())),
|
||||
None => (current, None),
|
||||
})
|
||||
}
|
||||
|
||||
/// The Puppet Warp pins in effect on some objects, with the mesh they warp.
|
||||
pub struct PinSet {
|
||||
pub ids: Vec<NodeId>,
|
||||
pub pins: Vec<PuppetPin>,
|
||||
/// No pin was placed yet: these are the automatic ones.
|
||||
pub auto: bool,
|
||||
pub mesh: Mesh,
|
||||
/// The mesh vertices as the pins warp them.
|
||||
pub deformed: Vec<Point>,
|
||||
/// The pins warp the art (else `deformed` is the rest mesh).
|
||||
warped: bool,
|
||||
}
|
||||
|
||||
impl PinSet {
|
||||
/// The pins on `ids` (see [`rest_and_pins`]) with the mesh built with Expand `expand`.
|
||||
pub fn of(doc: &Document, ids: &[NodeId], expand: f64) -> Option<Self> {
|
||||
let (rest, pins) = rest_and_pins(doc, ids)?;
|
||||
let mesh = mesh_of(&rest, expand)?;
|
||||
let auto = pins.is_none();
|
||||
let pins = pins.unwrap_or_else(|| default_pins(&mesh).into_iter().map(|p| PuppetPin { rest: p, at: p, angle: None }).collect());
|
||||
let warped = !at_rest(&pins);
|
||||
let deformed = if warped { arap::deform(&mesh, &solver_pins(&pins)) } else { mesh.verts.clone() };
|
||||
Some(Self { ids: ids.to_vec(), pins, auto, mesh, deformed, warped })
|
||||
}
|
||||
/// Where on the rest shape the art now at `q` comes from (None: off the mesh).
|
||||
pub fn rest_of(&self, q: Point) -> Option<Point> {
|
||||
if self.warped { self.mesh.unmap(&self.deformed, q) } else { self.mesh.contains(q).then_some(q) }
|
||||
}
|
||||
/// The turn (radians) the art has around pin `i`: the one it holds, else the warp's there.
|
||||
pub fn turn_of(&self, i: usize) -> f64 {
|
||||
self.pins.get(i).map_or(0.0, |q| q.angle.or_else(|| self.mesh.turn_at(&self.deformed, q.rest)).unwrap_or(0.0))
|
||||
}
|
||||
/// The `object.puppetWarp` params that set `pins` on these objects.
|
||||
pub fn params(&self, pins: &[PuppetPin], expand: f64) -> Value {
|
||||
json!({
|
||||
"rest": true,
|
||||
"ids": crate::json_ids(&self.ids),
|
||||
"pins": pins.iter().map(|q| json!([q.rest.x, q.rest.y])).collect::<Vec<_>>(),
|
||||
"moved": pins.iter().map(|q| json!([q.at.x, q.at.y])).collect::<Vec<_>>(),
|
||||
"angles": pins.iter().map(|q| q.angle.map(f64::to_degrees)).collect::<Vec<_>>(),
|
||||
"expand": expand,
|
||||
})
|
||||
}
|
||||
/// The warped mesh as one path (for the overlay).
|
||||
fn mesh_path(&self) -> BezPath {
|
||||
let mut bp = BezPath::new();
|
||||
for t in &self.mesh.tris {
|
||||
let [a, b, c] = t.map(|i| self.deformed.get(i).copied().unwrap_or(self.mesh.verts[i]));
|
||||
bp.move_to(a);
|
||||
bp.line_to(b);
|
||||
bp.line_to(c);
|
||||
bp.close_path();
|
||||
}
|
||||
bp
|
||||
}
|
||||
}
|
||||
|
||||
/// Put `pins` on `ids`: warp their rest shape (see [`rest_and_pins`]) so each pin lands where it
|
||||
/// is now, and keep the pins in the document. With no pins the art stays as it is and the pins
|
||||
/// start again from it.
|
||||
pub fn warp_from_rest(doc: &mut Document, ids: &[NodeId], pins: Vec<PuppetPin>, expand: f64) -> Result<(), String> {
|
||||
let (rest, _) = rest_and_pins(doc, ids).ok_or("nothing to warp")?;
|
||||
let rest = if pins.is_empty() {
|
||||
ids.iter().map(|id| doc.node_arc(*id).cloned()).collect::<Option<Vec<_>>>().ok_or("nothing to warp")?
|
||||
} else {
|
||||
let mesh = mesh_of(&rest, expand).ok_or("nothing to warp")?;
|
||||
if let Some(i) = pins.iter().position(|q| !mesh.contains(q.rest)) {
|
||||
return Err(format!("pin {i} is off the artwork (outside its mesh)"));
|
||||
}
|
||||
let deformed = arap::deform(&mesh, &solver_pins(&pins));
|
||||
let f = |q: Point| mesh.map(&deformed, q);
|
||||
for (id, r) in ids.iter().zip(&rest) {
|
||||
let mut n = Node::clone(r);
|
||||
warp_node_with(&mut n, &f);
|
||||
*doc.node_mut(*id).ok_or("the artwork is gone")? = n;
|
||||
}
|
||||
rest
|
||||
};
|
||||
let result = ids.iter().map(|id| doc.node_arc(*id).cloned()).collect::<Option<Vec<_>>>().ok_or("the artwork is gone")?;
|
||||
doc.puppet = Some(Arc::new(PuppetPins { ids: ids.to_vec(), rest, result, pins }));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Within this many pixels of a selected pin (and not on it), Alt-dragging turns the art around it.
|
||||
const TURN_REACH_PX: f64 = 24.0;
|
||||
/// Radius (pixels) of the dotted circle shown around the pin the art would turn around.
|
||||
const TURN_RING_PX: f64 = 16.0;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
struct PinState {
|
||||
p: Point,
|
||||
/// The art was turned around the pin: later warps keep that turn.
|
||||
held: bool,
|
||||
}
|
||||
/// The feedback for a click off the mesh.
|
||||
const OFF_MESH: &str = "Pins go on the artwork";
|
||||
const WARN: [u8; 3] = [0xd9, 0x3f, 0x3f];
|
||||
|
||||
/// A drag in progress.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum Gesture {
|
||||
/// The selected pins move by `to - from`.
|
||||
Move { from: Point, to: Point },
|
||||
/// The art turns by `angle` (radians) around pin `pin`; `from`: the pointer's angle around it
|
||||
/// when the drag began.
|
||||
Turn { pin: usize, from: f64, angle: f64 },
|
||||
/// The art turns around pin `pin`: `base` (radians) is its turn when the drag began, `from`
|
||||
/// the pointer's angle around it then, `angle` how far the drag has turned it since.
|
||||
Turn { pin: usize, base: f64, from: f64, angle: f64 },
|
||||
}
|
||||
|
||||
/// What the cached pins were computed from (holding the objects keeps the pointers unique).
|
||||
struct Key {
|
||||
ids: Vec<NodeId>,
|
||||
nodes: Vec<Arc<Node>>,
|
||||
puppet: Option<Arc<PuppetPins>>,
|
||||
expand: f64,
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn of(cx: &ToolContext, expand: f64) -> Option<Self> {
|
||||
let ids = cx.selection.objects.clone();
|
||||
let nodes = ids.iter().map(|id| cx.doc.node_arc(*id).cloned()).collect::<Option<_>>()?;
|
||||
Some(Self { ids, nodes, puppet: cx.doc.puppet.clone(), expand })
|
||||
}
|
||||
fn same(&self, o: &Key) -> bool {
|
||||
let ptrs = |a: &[Arc<Node>], b: &[Arc<Node>]| a.len() == b.len() && a.iter().zip(b).all(|(x, y)| Arc::ptr_eq(x, y));
|
||||
let puppet = match (&self.puppet, &o.puppet) {
|
||||
(Some(a), Some(b)) => Arc::ptr_eq(a, b),
|
||||
(a, b) => a.is_none() && b.is_none(),
|
||||
};
|
||||
self.ids == o.ids && ptrs(&self.nodes, &o.nodes) && puppet && self.expand == o.expand
|
||||
}
|
||||
}
|
||||
|
||||
struct Cached {
|
||||
key: Key,
|
||||
set: PinSet,
|
||||
mesh: BezPath,
|
||||
}
|
||||
|
||||
pub struct PuppetWarpTool {
|
||||
/// The objects the selected pins belong to.
|
||||
ids: Vec<NodeId>,
|
||||
pins: Vec<PinState>,
|
||||
/// Selected pins (indices into `pins`).
|
||||
/// Selected pins (indices into the pins).
|
||||
selected: Vec<usize>,
|
||||
drag: Option<Gesture>,
|
||||
/// The pins as the drag began (each preview starts from them).
|
||||
start: Option<Arc<Cached>>,
|
||||
/// The pointer and the modifiers held while hovering (for the turn ring).
|
||||
hover: Option<(Point, Mods)>,
|
||||
/// Where a click off the mesh was (its feedback shows until the pointer moves on).
|
||||
off_mesh: Option<Point>,
|
||||
/// Control bar: Show Mesh.
|
||||
show_mesh: bool,
|
||||
/// Control bar: Expand Mesh (points, 0 allowed).
|
||||
expand: f64,
|
||||
/// The pins and mesh of the selection, recomputed when the art, its pins or Expand change
|
||||
/// (overlays ask every frame).
|
||||
cache: Mutex<Option<Arc<Cached>>>,
|
||||
}
|
||||
|
||||
impl Default for PuppetWarpTool {
|
||||
fn default() -> Self {
|
||||
Self { ids: vec![], pins: vec![], selected: vec![], drag: None, hover: None, show_mesh: true, expand: 3.0 }
|
||||
Self {
|
||||
ids: vec![],
|
||||
selected: vec![],
|
||||
drag: None,
|
||||
start: None,
|
||||
hover: None,
|
||||
off_mesh: None,
|
||||
show_mesh: true,
|
||||
expand: 3.0,
|
||||
cache: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PuppetWarpTool {
|
||||
/// Re-seed pins when the selection changed.
|
||||
fn sync(&mut self, cx: &ToolContext) {
|
||||
let ids = cx.selection.objects.clone();
|
||||
if ids == self.ids {
|
||||
return;
|
||||
/// The pins on the selection (None: nothing selected).
|
||||
fn pins(&self, cx: &ToolContext) -> Option<Arc<Cached>> {
|
||||
let key = Key::of(cx, self.expand).filter(|k| !k.ids.is_empty())?;
|
||||
let mut cache = self.cache.lock().ok();
|
||||
if let Some(c) = cache.as_deref().and_then(Option::as_ref).filter(|c| c.key.same(&key)) {
|
||||
return Some(c.clone());
|
||||
}
|
||||
self.ids = ids;
|
||||
self.selected.clear();
|
||||
self.pins = if self.ids.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
let auto = mesh_for(cx.doc, &self.ids, self.expand).map(|m| auto_pins(&m, 3)).unwrap_or_default();
|
||||
auto.into_iter().map(|p| PinState { p, held: false }).collect()
|
||||
};
|
||||
let set = PinSet::of(cx.doc, &key.ids, self.expand)?;
|
||||
let c = Arc::new(Cached { mesh: set.mesh_path(), key, set });
|
||||
if let Some(slot) = cache.as_deref_mut() {
|
||||
*slot = Some(c.clone());
|
||||
}
|
||||
Some(c)
|
||||
}
|
||||
fn pin_at(&self, cx: &ToolContext, p: Point) -> Option<usize> {
|
||||
/// The last pins computed (for `options`, which has no document).
|
||||
fn last_pins(&self) -> Option<Arc<Cached>> {
|
||||
self.cache.lock().ok().and_then(|c| c.clone())
|
||||
}
|
||||
/// A new selection starts with no pin selected.
|
||||
fn sync(&mut self, cx: &ToolContext) {
|
||||
if cx.selection.objects != self.ids {
|
||||
self.ids = cx.selection.objects.clone();
|
||||
self.selected.clear();
|
||||
}
|
||||
}
|
||||
fn pin_at(cx: &ToolContext, set: &PinSet, p: Point) -> Option<usize> {
|
||||
let tol = cx.tol(7.0);
|
||||
self.pins
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, q)| q.p.distance(p) <= tol)
|
||||
.min_by(|a, b| a.1.p.distance(p).total_cmp(&b.1.p.distance(p)))
|
||||
.map(|(i, _)| i)
|
||||
set.pins.iter().enumerate().map(|(i, q)| (i, q.at.distance(p))).filter(|(_, d)| *d <= tol).min_by(|a, b| a.1.total_cmp(&b.1)).map(|(i, _)| i)
|
||||
}
|
||||
/// The selected pin the art would turn around with Alt held at `p`: near it, not on a pin.
|
||||
fn turn_pin_at(&self, cx: &ToolContext, p: Point) -> Option<usize> {
|
||||
if self.pin_at(cx, p).is_some() {
|
||||
fn turn_pin_at(&self, cx: &ToolContext, set: &PinSet, p: Point) -> Option<usize> {
|
||||
if Self::pin_at(cx, set, p).is_some() {
|
||||
return None;
|
||||
}
|
||||
let reach = cx.tol(TURN_REACH_PX);
|
||||
self.selected
|
||||
.iter()
|
||||
.filter_map(|&i| Some((i, self.pins.get(i)?.p.distance(p))))
|
||||
.filter_map(|&i| Some((i, set.pins.get(i)?.at.distance(p))))
|
||||
.filter(|(_, d)| *d <= reach)
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
/// Where each pin is during gesture `g`, and the turn it holds (radians).
|
||||
fn targets(&self, g: Gesture) -> Vec<(Point, Option<f64>)> {
|
||||
self.pins
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, q)| match g {
|
||||
Gesture::Move { from, to } if self.selected.contains(&i) => (q.p + (to - from), q.held.then_some(0.0)),
|
||||
Gesture::Turn { pin, angle, .. } if pin == i => (q.p, Some(angle)),
|
||||
_ => (q.p, q.held.then_some(0.0)),
|
||||
})
|
||||
.collect()
|
||||
/// The pins during gesture `g`.
|
||||
fn targets(&self, set: &PinSet, g: Gesture) -> Vec<PuppetPin> {
|
||||
let mut pins = set.pins.clone();
|
||||
for (i, q) in pins.iter_mut().enumerate() {
|
||||
match g {
|
||||
Gesture::Move { from, to } if self.selected.contains(&i) => q.at += to - from,
|
||||
Gesture::Turn { pin, base, angle, .. } if pin == i => q.angle = Some(base + angle),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
pins
|
||||
}
|
||||
fn params(&self, g: Gesture) -> Value {
|
||||
let moved = self.targets(g);
|
||||
let pj = |v: &mut dyn Iterator<Item = Point>| Value::Array(v.map(|p| json!([p.x, p.y])).collect());
|
||||
json!({
|
||||
"ids": crate::json_ids(&self.ids),
|
||||
"pins": pj(&mut self.pins.iter().map(|q| q.p)),
|
||||
"moved": pj(&mut moved.iter().map(|m| m.0)),
|
||||
"angles": moved.iter().map(|m| m.1.map(f64::to_degrees)).collect::<Vec<_>>(),
|
||||
"expand": self.expand,
|
||||
})
|
||||
/// Run `object.puppetWarp` with `pins`, then look at the pins it left.
|
||||
fn apply(&self, set: &PinSet, pins: &[PuppetPin]) -> Vec<Action> {
|
||||
vec![Action::Exec("object.puppetWarp".into(), set.params(pins, self.expand)), Action::Notify("pins".into())]
|
||||
}
|
||||
/// Select pin `i` (Shift: add it to the selection or take it out). → whether it is selected.
|
||||
fn pick(&mut self, i: usize, shift: bool) -> bool {
|
||||
@@ -177,59 +344,74 @@ impl Tool for PuppetWarpTool {
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
self.hover = Some((p, ev.mods));
|
||||
if self.off_mesh.is_some_and(|q| q.distance(p) > cx.tol(6.0)) {
|
||||
self.off_mesh = None;
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
self.sync(cx);
|
||||
if self.ids.is_empty() {
|
||||
// Click an object to select it; pins appear on the next event.
|
||||
self.off_mesh = None;
|
||||
let Some(c) = self.pins(cx) else {
|
||||
// Click an object to select it: its pins appear at once.
|
||||
if let Some(h) = vectorcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options()) {
|
||||
return vec![Action::Exec("select.set".into(), json!({"ids": [h.top_object(cx.isolation).0]}))];
|
||||
return vec![
|
||||
Action::Exec("select.set".into(), json!({"ids": [h.top_object(cx.isolation).0]})),
|
||||
Action::Notify("pins".into()),
|
||||
];
|
||||
}
|
||||
return vec![];
|
||||
}
|
||||
};
|
||||
let set = &c.set;
|
||||
if ev.mods.alt
|
||||
&& let Some(i) = self.turn_pin_at(cx, p)
|
||||
&& let Some(c) = self.pins.get(i).map(|q| q.p)
|
||||
&& let Some(i) = self.turn_pin_at(cx, set, p)
|
||||
&& let Some(q) = set.pins.get(i)
|
||||
{
|
||||
self.drag = Some(Gesture::Turn { pin: i, from: (p - c).atan2(), angle: 0.0 });
|
||||
self.drag = Some(Gesture::Turn { pin: i, base: set.turn_of(i), from: (p - q.at).atan2(), angle: 0.0 });
|
||||
self.start = Some(c.clone());
|
||||
return vec![Action::Begin("Puppet Warp".into())];
|
||||
}
|
||||
if let Some(i) = self.pin_at(cx, p) {
|
||||
if let Some(i) = Self::pin_at(cx, set, p) {
|
||||
if !self.pick(i, ev.mods.shift) {
|
||||
return vec![];
|
||||
}
|
||||
self.drag = Some(Gesture::Move { from: p, to: p });
|
||||
self.start = Some(c.clone());
|
||||
return vec![Action::Begin("Puppet Warp".into())];
|
||||
}
|
||||
let inside = cx.doc.bounds_of(&self.ids, false).is_some_and(|b| b.inflate(cx.tol(4.0), cx.tol(4.0)).contains(p));
|
||||
if inside {
|
||||
self.pins.push(PinState { p, held: false });
|
||||
self.pick(self.pins.len() - 1, ev.mods.shift);
|
||||
} else if !ev.mods.shift {
|
||||
self.selected.clear();
|
||||
match set.rest_of(p) {
|
||||
Some(rest) => {
|
||||
let mut pins = set.pins.clone();
|
||||
pins.push(PuppetPin { rest, at: p, angle: None });
|
||||
self.pick(pins.len() - 1, ev.mods.shift);
|
||||
self.apply(set, &pins)
|
||||
}
|
||||
None => {
|
||||
// Off the mesh: nothing to pin there (the feedback says so).
|
||||
self.off_mesh = Some(p);
|
||||
if !ev.mods.shift {
|
||||
self.selected.clear();
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some(g) = self.drag.as_mut() else { return vec![] };
|
||||
match g {
|
||||
let (Some(mut g), Some(c)) = (self.drag, self.start.clone()) else { return vec![] };
|
||||
match &mut g {
|
||||
Gesture::Move { to, .. } => *to = p,
|
||||
Gesture::Turn { pin, from, angle } => {
|
||||
let Some(c) = self.pins.get(*pin).map(|q| q.p) else { return vec![] };
|
||||
*angle = (p - c).atan2() - *from;
|
||||
Gesture::Turn { pin, from, angle, .. } => {
|
||||
let Some(q) = c.set.pins.get(*pin) else { return vec![] };
|
||||
*angle = (p - q.at).atan2() - *from;
|
||||
}
|
||||
}
|
||||
let g = *g;
|
||||
vec![Action::Preview("object.puppetWarp".into(), self.params(g))]
|
||||
self.drag = Some(g);
|
||||
vec![Action::Preview("object.puppetWarp".into(), c.set.params(&self.targets(&c.set, g), self.expand))]
|
||||
}
|
||||
PointerKind::Up => match self.drag.take() {
|
||||
Some(g) => {
|
||||
let moved = self.targets(g);
|
||||
for (q, (to, angle)) in self.pins.iter_mut().zip(moved) {
|
||||
(q.p, q.held) = (to, angle.is_some());
|
||||
}
|
||||
vec![Action::Commit]
|
||||
Some(_) => {
|
||||
self.start = None;
|
||||
vec![Action::Commit, Action::Notify("pins".into())]
|
||||
}
|
||||
None => vec![],
|
||||
},
|
||||
@@ -242,68 +424,66 @@ impl Tool for PuppetWarpTool {
|
||||
matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
||||
&& self.drag.is_none()
|
||||
&& cx.selection.objects == self.ids
|
||||
&& self.selected.iter().any(|i| *i < self.pins.len())
|
||||
&& self.pins(cx).is_some_and(|c| self.selected.iter().any(|i| *i < c.set.pins.len()))
|
||||
}
|
||||
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if matches!(key, ToolKey::Delete | ToolKey::Backspace) && self.drag.is_none() {
|
||||
let gone = std::mem::take(&mut self.selected);
|
||||
self.pins = std::mem::take(&mut self.pins).into_iter().enumerate().filter(|(i, _)| !gone.contains(i)).map(|(_, q)| q).collect();
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if !self.claims_key(cx, key) {
|
||||
return vec![];
|
||||
}
|
||||
vec![]
|
||||
let Some(c) = self.pins(cx) else { return vec![] };
|
||||
let gone = std::mem::take(&mut self.selected);
|
||||
let pins: Vec<PuppetPin> = c.set.pins.iter().enumerate().filter(|(i, _)| !gone.contains(i)).map(|(_, q)| *q).collect();
|
||||
self.apply(&c.set, &pins)
|
||||
}
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let mut out = vec![];
|
||||
if self.show_mesh
|
||||
&& cx.selection.objects == self.ids
|
||||
&& let Some(m) = mesh_for(cx.doc, &self.ids, self.expand)
|
||||
{
|
||||
let mut bp = BezPath::new();
|
||||
for t in &m.tris {
|
||||
bp.move_to(m.verts[t[0]]);
|
||||
bp.line_to(m.verts[t[1]]);
|
||||
bp.line_to(m.verts[t[2]]);
|
||||
bp.close_path();
|
||||
if let Some(c) = self.pins(cx) {
|
||||
if self.show_mesh {
|
||||
out.push(Overlay::Path { path: c.mesh.clone(), color: [0x9a, 0x9a, 0x9a], width: 0.5, dashed: false });
|
||||
}
|
||||
let r = cx.tol(5.0);
|
||||
for (i, q) in c.set.pins.iter().enumerate() {
|
||||
let sel = self.selected.contains(&i) && cx.selection.objects == self.ids;
|
||||
out.push(Overlay::Path {
|
||||
path: super::ellipse_path(q.at, r, r, 0.0),
|
||||
color: BLUE,
|
||||
width: if sel { 3.0 } else { 1.5 },
|
||||
dashed: false,
|
||||
});
|
||||
out.push(Overlay::Anchor { p: q.at, color: BLUE, filled: sel, size: 3.0 });
|
||||
}
|
||||
// The dotted circle of a turn: around the pin being turned, or the one Alt would turn.
|
||||
let ring = match (self.drag, self.hover) {
|
||||
(Some(Gesture::Turn { pin, .. }), _) => Some(pin),
|
||||
(None, Some((h, mods))) if mods.alt => self.turn_pin_at(cx, &c.set, h),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(q) = ring.and_then(|i| c.set.pins.get(i)) {
|
||||
let rr = cx.tol(TURN_RING_PX);
|
||||
out.push(Overlay::Path { path: super::ellipse_path(q.at, rr, rr, 0.0), color: BLUE, width: 1.0, dashed: true });
|
||||
}
|
||||
out.push(Overlay::Path { path: bp, color: [0x9a, 0x9a, 0x9a], width: 0.5, dashed: false });
|
||||
}
|
||||
let r = cx.tol(5.0);
|
||||
let shown: Vec<Point> = match self.drag {
|
||||
Some(g) => self.targets(g).into_iter().map(|t| t.0).collect(),
|
||||
None => self.pins.iter().map(|q| q.p).collect(),
|
||||
};
|
||||
for (i, q) in shown.iter().enumerate() {
|
||||
let sel = self.selected.contains(&i);
|
||||
out.push(Overlay::Path { path: super::ellipse_path(*q, r, r, 0.0), color: BLUE, width: if sel { 3.0 } else { 1.5 }, dashed: false });
|
||||
out.push(Overlay::Anchor { p: *q, color: BLUE, filled: sel, size: 3.0 });
|
||||
}
|
||||
// The dotted circle of a turn: around the pin being turned, or the one Alt would turn.
|
||||
let ring = match (self.drag, self.hover) {
|
||||
(Some(Gesture::Turn { pin, .. }), _) => Some(pin),
|
||||
(None, Some((h, mods))) if mods.alt => self.turn_pin_at(cx, h),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(c) = ring.and_then(|i| self.pins.get(i)) {
|
||||
let rr = cx.tol(TURN_RING_PX);
|
||||
out.push(Overlay::Path { path: super::ellipse_path(c.p, rr, rr, 0.0), color: BLUE, width: 1.0, dashed: true });
|
||||
if let Some(p) = self.off_mesh {
|
||||
out.push(Overlay::Label { p, text: OFF_MESH.into(), color: WARN });
|
||||
}
|
||||
out
|
||||
}
|
||||
fn cursor(&self, cx: &ToolContext, p: Point, mods: Mods) -> Cursor {
|
||||
if mods.alt && self.turn_pin_at(cx, p).is_some() {
|
||||
return Cursor::Rotate;
|
||||
}
|
||||
match self.pin_at(cx, p) {
|
||||
Some(_) => Cursor::Move,
|
||||
None => Cursor::PenAdd,
|
||||
let Some(c) = self.pins(cx) else { return Cursor::Arrow };
|
||||
if mods.alt && self.turn_pin_at(cx, &c.set, p).is_some() {
|
||||
Cursor::Rotate
|
||||
} else if Self::pin_at(cx, &c.set, p).is_some() {
|
||||
Cursor::Move
|
||||
} else if c.set.rest_of(p).is_some() {
|
||||
Cursor::PenAdd
|
||||
} else {
|
||||
// Off the mesh: no pin goes there.
|
||||
Cursor::NotAllowed
|
||||
}
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
json!({
|
||||
"showMesh": self.show_mesh,
|
||||
"expand": self.expand,
|
||||
"pins": self.pins.iter().map(|q| json!([q.p.x, q.p.y])).collect::<Vec<_>>(),
|
||||
"selected": self.selected,
|
||||
})
|
||||
let pins = self.last_pins().map(|c| c.set.pins.iter().map(|q| json!([q.at.x, q.at.y])).collect::<Vec<_>>()).unwrap_or_default();
|
||||
json!({"showMesh": self.show_mesh, "expand": self.expand, "pins": pins, "selected": self.selected})
|
||||
}
|
||||
fn set_option(&mut self, key: &str, value: &Value) {
|
||||
match key {
|
||||
@@ -314,7 +494,10 @@ impl Tool for PuppetWarpTool {
|
||||
}
|
||||
}
|
||||
// Select All Pins (false: deselect them all).
|
||||
"selectAllPins" => self.selected = if value.as_bool() == Some(false) { vec![] } else { (0..self.pins.len()).collect() },
|
||||
"selectAllPins" => {
|
||||
let n = self.last_pins().map_or(0, |c| c.set.pins.len());
|
||||
self.selected = if value.as_bool() == Some(false) { vec![] } else { (0..n).collect() };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -323,12 +506,18 @@ impl Tool for PuppetWarpTool {
|
||||
}
|
||||
fn notify(&mut self, cx: &ToolContext, _what: &str) {
|
||||
self.sync(cx);
|
||||
// Pick up the pins the command left (and drop selected pins that are gone).
|
||||
let n = self.pins(cx).map_or(0, |c| c.set.pins.len());
|
||||
self.selected.retain(|i| *i < n);
|
||||
}
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.ids.clear();
|
||||
self.pins.clear();
|
||||
self.selected.clear();
|
||||
self.hover = None;
|
||||
self.off_mesh = None;
|
||||
if let Ok(mut c) = self.cache.lock() {
|
||||
*c = None;
|
||||
}
|
||||
self.start = None;
|
||||
if self.drag.take().is_some() { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
}
|
||||
@@ -347,94 +536,171 @@ mod tests {
|
||||
Mods { shift: true, ..Default::default() }
|
||||
}
|
||||
|
||||
/// The tool on a document, running the `object.puppetWarp` actions it emits the way the
|
||||
/// engine does (previews on the snapshot taken at Begin).
|
||||
struct Rig {
|
||||
doc: Document,
|
||||
sel: Selection,
|
||||
base: Option<Document>,
|
||||
tool: PuppetWarpTool,
|
||||
/// The params of the last command run.
|
||||
last: Value,
|
||||
}
|
||||
|
||||
impl Rig {
|
||||
fn new() -> Self {
|
||||
let (doc, id) = doc_with_rect();
|
||||
Self { doc, sel: Selection { objects: vec![id], ..Default::default() }, base: None, tool: PuppetWarpTool::default(), last: Value::Null }
|
||||
}
|
||||
fn run(&mut self, p: &Value) {
|
||||
let pts = |k: &str| -> Vec<Point> {
|
||||
p[k].as_array().unwrap().iter().map(|q| Point::new(q[0].as_f64().unwrap(), q[1].as_f64().unwrap())).collect()
|
||||
};
|
||||
let angles = p["angles"].as_array().unwrap().iter().map(|a| a.as_f64().map(f64::to_radians));
|
||||
let pins = pts("pins").into_iter().zip(pts("moved")).zip(angles).map(|((rest, at), angle)| PuppetPin { rest, at, angle }).collect();
|
||||
let ids: Vec<NodeId> = p["ids"].as_array().unwrap().iter().map(|i| NodeId(i.as_u64().unwrap())).collect();
|
||||
assert_eq!(p["rest"], json!(true));
|
||||
warp_from_rest(&mut self.doc, &ids, pins, p["expand"].as_f64().unwrap()).unwrap();
|
||||
self.last = p.clone();
|
||||
}
|
||||
fn apply(&mut self, acts: Vec<Action>) -> Vec<Action> {
|
||||
for a in &acts {
|
||||
match a {
|
||||
Action::Begin(_) => self.base = Some(self.doc.clone()),
|
||||
Action::Preview(_, p) => {
|
||||
self.doc = self.base.clone().unwrap();
|
||||
self.run(p);
|
||||
}
|
||||
Action::Commit => self.base = None,
|
||||
Action::Exec(c, p) if c == "object.puppetWarp" => self.run(p),
|
||||
Action::Notify(w) => {
|
||||
let p = paint();
|
||||
self.tool.notify(&cx(&self.doc, &self.sel, &p), w);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
acts
|
||||
}
|
||||
fn ev(&mut self, kind: PointerKind, x: f64, y: f64, mods: Mods) -> Vec<Action> {
|
||||
let p = paint();
|
||||
let acts = self.tool.pointer(&cx(&self.doc, &self.sel, &p), &PointerEvent::new(kind, x, y).with_mods(mods));
|
||||
self.apply(acts)
|
||||
}
|
||||
fn click(&mut self, x: f64, y: f64, mods: Mods) -> Vec<Action> {
|
||||
let mut a = self.ev(PointerKind::Down, x, y, mods);
|
||||
a.extend(self.ev(PointerKind::Up, x, y, mods));
|
||||
a
|
||||
}
|
||||
fn drag(&mut self, from: (f64, f64), to: (f64, f64), mods: Mods) -> Vec<Action> {
|
||||
let mut a = self.ev(PointerKind::Down, from.0, from.1, mods);
|
||||
a.extend(self.ev(PointerKind::Drag, (from.0 + to.0) / 2.0, (from.1 + to.1) / 2.0, mods));
|
||||
a.extend(self.ev(PointerKind::Drag, to.0, to.1, mods));
|
||||
a.extend(self.ev(PointerKind::Up, to.0, to.1, mods));
|
||||
a
|
||||
}
|
||||
fn key(&mut self, k: ToolKey) -> Vec<Action> {
|
||||
let p = paint();
|
||||
let acts = self.tool.key(&cx(&self.doc, &self.sel, &p), k, Mods::default());
|
||||
self.apply(acts)
|
||||
}
|
||||
fn claims(&self, k: ToolKey) -> bool {
|
||||
let p = paint();
|
||||
self.tool.claims_key(&cx(&self.doc, &self.sel, &p), k)
|
||||
}
|
||||
fn overlays(&self) -> Vec<Overlay> {
|
||||
let p = paint();
|
||||
self.tool.overlays(&cx(&self.doc, &self.sel, &p))
|
||||
}
|
||||
fn cursor(&self, x: f64, y: f64, mods: Mods) -> Cursor {
|
||||
let p = paint();
|
||||
self.tool.cursor(&cx(&self.doc, &self.sel, &p), Point::new(x, y), mods)
|
||||
}
|
||||
fn pins(&self) -> Vec<PuppetPin> {
|
||||
self.doc.puppet.as_ref().map(|p| p.pins.clone()).unwrap_or_default()
|
||||
}
|
||||
fn bounds(&self) -> Rect {
|
||||
self.doc.node(self.sel.objects[0]).unwrap().geometric_bounds().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn puppet_tool_autopins_adds_and_drags() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let s = Selection { objects: vec![id], ..Default::default() };
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = PuppetWarpTool::default();
|
||||
// Click inside the rect away from auto pins: adds a pin.
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Down, 130.0, 170.0));
|
||||
assert!(acts.is_empty());
|
||||
assert_eq!(t.pins.len(), 4, "3 auto pins + 1");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 130.0, 170.0));
|
||||
let mut r = Rig::new();
|
||||
// The automatic pins show as soon as there is art selected.
|
||||
assert_eq!(r.overlays().iter().filter(|o| matches!(o, Overlay::Anchor { .. })).count(), 3, "3 automatic pins");
|
||||
// Click inside the rect away from them: adds a pin (one command, the pins kept in the document).
|
||||
let acts = r.click(130.0, 170.0, Mods::default());
|
||||
assert!(matches!(&acts[0], Action::Exec(c, _) if c == "object.puppetWarp"), "{acts:?}");
|
||||
assert_eq!(r.pins().len(), 4, "3 auto pins + 1");
|
||||
assert_eq!(r.tool.options()["selected"], json!([3]));
|
||||
// Drag the new pin.
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 130.0, 170.0)), vec![Action::Begin("Puppet Warp".into())]);
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 120.0, 190.0));
|
||||
let Action::Preview(cmd, v) = &acts[0] else { panic!() };
|
||||
assert_eq!(r.ev(PointerKind::Down, 130.0, 170.0, Mods::default()), vec![Action::Begin("Puppet Warp".into())]);
|
||||
let acts = r.ev(PointerKind::Drag, 120.0, 190.0, Mods::default());
|
||||
let Action::Preview(cmd, v) = &acts[0] else { panic!("{acts:?}") };
|
||||
assert_eq!(cmd, "object.puppetWarp");
|
||||
assert_eq!(v["moved"][3], json!([120.0, 190.0]));
|
||||
assert_eq!(v["pins"][3], json!([130.0, 170.0]));
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 120.0, 190.0)), vec![Action::Commit]);
|
||||
// Alt-click on a pin no longer deletes it (Alt turns the art around a pin): Delete does.
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 120.0, 190.0).with_mods(alt()));
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 120.0, 190.0).with_mods(alt()));
|
||||
assert_eq!(t.pins.len(), 4);
|
||||
assert!(t.claims_key(&c, ToolKey::Delete));
|
||||
t.key(&c, ToolKey::Delete, Mods::default());
|
||||
assert_eq!(t.pins.len(), 3);
|
||||
assert!(!t.claims_key(&c, ToolKey::Delete), "nothing selected: Delete is Clear's");
|
||||
assert_eq!(r.ev(PointerKind::Up, 120.0, 190.0, Mods::default())[0], Action::Commit);
|
||||
// Alt-click on a pin doesn't delete it (Alt turns the art around a pin): Delete does.
|
||||
r.click(120.0, 190.0, alt());
|
||||
assert_eq!(r.pins().len(), 4);
|
||||
assert!(r.claims(ToolKey::Delete));
|
||||
r.key(ToolKey::Delete);
|
||||
assert_eq!(r.pins().len(), 3);
|
||||
assert!(!r.claims(ToolKey::Delete), "nothing selected: Delete is Clear's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_click_selects_several_pins_that_move_together() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let s = Selection { objects: vec![id], ..Default::default() };
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = PuppetWarpTool::default();
|
||||
let ev = |k, x, y, m| PointerEvent::new(k, x, y).with_mods(m);
|
||||
let mut r = Rig::new();
|
||||
// Two new pins, the second Shift-added to the selection.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 110.0, 110.0, Mods::default()));
|
||||
t.pointer(&c, &ev(PointerKind::Down, 190.0, 110.0, shift()));
|
||||
let (a, b) = (t.pins.len() - 2, t.pins.len() - 1);
|
||||
assert_eq!(t.selected, vec![a, b]);
|
||||
r.click(110.0, 110.0, Mods::default());
|
||||
r.click(190.0, 110.0, shift());
|
||||
let (a, b) = (r.pins().len() - 2, r.pins().len() - 1);
|
||||
assert_eq!(r.tool.selected, vec![a, b]);
|
||||
// Dragging one moves both.
|
||||
t.pointer(&c, &ev(PointerKind::Down, 110.0, 110.0, Mods::default()));
|
||||
let acts = t.pointer(&c, &ev(PointerKind::Drag, 110.0, 100.0, Mods::default()));
|
||||
r.ev(PointerKind::Down, 110.0, 110.0, Mods::default());
|
||||
let acts = r.ev(PointerKind::Drag, 110.0, 100.0, Mods::default());
|
||||
let Action::Preview(_, v) = &acts[0] else { panic!("{acts:?}") };
|
||||
assert_eq!((v["moved"][a].clone(), v["moved"][b].clone()), (json!([110.0, 100.0]), json!([190.0, 100.0])));
|
||||
assert_eq!(v["moved"][0], v["pins"][0], "unselected pins stay");
|
||||
t.pointer(&c, &ev(PointerKind::Up, 110.0, 100.0, Mods::default()));
|
||||
r.ev(PointerKind::Up, 110.0, 100.0, Mods::default());
|
||||
// Shift-click takes a pin out of the selection without dragging.
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, 190.0, 100.0, shift())).is_empty());
|
||||
assert_eq!(t.selected, vec![a]);
|
||||
// Select All Pins.
|
||||
t.set_option("selectAllPins", &json!(true));
|
||||
assert_eq!(t.selected, (0..t.pins.len()).collect::<Vec<_>>());
|
||||
t.key(&c, ToolKey::Backspace, Mods::default());
|
||||
assert!(t.pins.is_empty());
|
||||
assert!(r.ev(PointerKind::Down, 190.0, 100.0, shift()).is_empty());
|
||||
assert_eq!(r.tool.selected, vec![a]);
|
||||
// Select All Pins, then delete them all: the art stays as it is.
|
||||
let before = r.bounds();
|
||||
r.tool.set_option("selectAllPins", &json!(true));
|
||||
assert_eq!(r.tool.selected, (0..r.pins().len()).collect::<Vec<_>>());
|
||||
r.key(ToolKey::Backspace);
|
||||
assert!(r.pins().is_empty());
|
||||
assert_eq!(r.bounds(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_drag_near_a_selected_pin_turns_the_art_around_it() {
|
||||
let (d, id) = doc_with_rect();
|
||||
let s = Selection { objects: vec![id], ..Default::default() };
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let mut t = PuppetWarpTool::default();
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 110.0, 150.0));
|
||||
let i = t.pins.len() - 1;
|
||||
let mut r = Rig::new();
|
||||
r.click(110.0, 150.0, Mods::default());
|
||||
let i = r.pins().len() - 1;
|
||||
// Alt near (not on) the selected pin: the turn ring and cursor.
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Move, 125.0, 150.0).with_mods(alt()));
|
||||
assert_eq!(t.cursor(&c, Point::new(125.0, 150.0), alt()), Cursor::Rotate);
|
||||
assert!(t.overlays(&c).iter().any(|o| matches!(o, Overlay::Path { dashed: true, .. })), "dotted ring");
|
||||
assert_ne!(t.cursor(&c, Point::new(125.0, 150.0), Mods::default()), Cursor::Rotate, "only with Alt");
|
||||
r.ev(PointerKind::Move, 125.0, 150.0, alt());
|
||||
assert_eq!(r.cursor(125.0, 150.0, alt()), Cursor::Rotate);
|
||||
assert!(r.overlays().iter().any(|o| matches!(o, Overlay::Path { dashed: true, .. })), "dotted ring");
|
||||
assert_ne!(r.cursor(125.0, 150.0, Mods::default()), Cursor::Rotate, "only with Alt");
|
||||
// Drag a quarter turn around it.
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 125.0, 150.0).with_mods(alt())), vec![Action::Begin("Puppet Warp".into())]);
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 110.0, 165.0).with_mods(alt()));
|
||||
assert_eq!(r.ev(PointerKind::Down, 125.0, 150.0, alt()), vec![Action::Begin("Puppet Warp".into())]);
|
||||
let acts = r.ev(PointerKind::Drag, 110.0, 165.0, alt());
|
||||
let Action::Preview(cmd, v) = &acts[0] else { panic!("{acts:?}") };
|
||||
assert_eq!(cmd, "object.puppetWarp");
|
||||
assert!((v["angles"][i].as_f64().unwrap() - 90.0).abs() < 1e-6, "{v}");
|
||||
assert_eq!(v["moved"][i], v["pins"][i], "the pin stays put");
|
||||
assert_eq!(v["angles"][0], Value::Null, "other pins turn freely");
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Up, 110.0, 165.0).with_mods(alt()));
|
||||
r.ev(PointerKind::Up, 110.0, 165.0, alt());
|
||||
// The turned pin keeps its turn through the next warp.
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, t.pins[0].p.x, t.pins[0].p.y));
|
||||
let acts = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, t.pins[0].p.x + 5.0, t.pins[0].p.y));
|
||||
let Action::Preview(_, v) = &acts[0] else { panic!("{acts:?}") };
|
||||
assert_eq!(v["angles"][i], json!(0.0));
|
||||
let q = r.pins()[0].at;
|
||||
r.drag((q.x, q.y), (q.x + 5.0, q.y), Mods::default());
|
||||
assert!((r.last["angles"][i].as_f64().unwrap() - 90.0).abs() < 1e-6, "{}", r.last);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -447,4 +713,37 @@ mod tests {
|
||||
t.set_option("expand", &json!("x"));
|
||||
assert_eq!(t.options()["expand"], json!(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warps_start_from_the_rest_shape() {
|
||||
let mut r = Rig::new();
|
||||
let rest = r.doc.node(r.sel.objects[0]).cloned().unwrap();
|
||||
r.click(190.0, 190.0, Mods::default());
|
||||
// Two drags out and back: the second starts from where the first left the pin.
|
||||
r.drag((190.0, 190.0), (230.0, 230.0), Mods::default());
|
||||
assert!(r.bounds().x1 > 215.0, "{:?}", r.bounds());
|
||||
r.drag((230.0, 230.0), (190.0, 190.0), Mods::default());
|
||||
// Back where it started: the original shape, not a warp of a warp.
|
||||
let back = r.doc.node(r.sel.objects[0]).unwrap();
|
||||
let (a, b) = (rest.path_data().unwrap(), back.path_data().unwrap());
|
||||
let worst = a.subpaths[0].anchors.iter().zip(&b.subpaths[0].anchors).map(|(x, y)| x.p.distance(y.p)).fold(0.0, f64::max);
|
||||
assert!(worst < 0.01, "{worst}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_click_off_the_mesh_places_no_pin_and_says_so() {
|
||||
let mut r = Rig::new();
|
||||
r.click(130.0, 170.0, Mods::default());
|
||||
assert_eq!(r.cursor(400.0, 400.0, Mods::default()), Cursor::NotAllowed);
|
||||
assert!(r.click(400.0, 400.0, Mods::default()).is_empty());
|
||||
assert_eq!(r.pins().len(), 4);
|
||||
assert!(r.tool.selected.is_empty(), "the click deselects the pins");
|
||||
assert!(r.overlays().iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == OFF_MESH)));
|
||||
r.ev(PointerKind::Move, 450.0, 450.0, Mods::default());
|
||||
assert!(!r.overlays().iter().any(|o| matches!(o, Overlay::Label { .. })), "gone once the pointer moves on");
|
||||
// The command refuses a pin off the mesh.
|
||||
let ids = r.sel.objects.clone();
|
||||
let off = PuppetPin { rest: Point::new(400.0, 400.0), at: Point::new(400.0, 400.0), angle: None };
|
||||
assert!(warp_from_rest(&mut r.doc, &ids, vec![off], 3.0).unwrap_err().contains("off the artwork"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user