mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 20:03:56 +00:00
232 lines
9.1 KiB
Rust
232 lines
9.1 KiB
Rust
//! Puppet Warp tool.
|
|
//!
|
|
//! With artwork selected the tool places three pins automatically (spread over the mesh). Click
|
|
//! on the artwork adds a pin, Alt-click deletes one, Delete/Backspace removes the selected pin,
|
|
//! and dragging a pin warps the art: every drag previews
|
|
//! `object.puppetWarp {ids, pins, moved}` (pins = current pin positions, moved = the same list
|
|
//! with the dragged pin at the pointer), committed on release as one undo step. The mesh follows
|
|
//! the current shape, so successive drags compose.
|
|
|
|
use serde_json::{Value, json};
|
|
use vectorcraft_doc::{Document, NodeId};
|
|
use vectorcraft_geom::{BezPath, Point, Rect};
|
|
|
|
use super::arap::{Mesh, MeshOptions, auto_pins};
|
|
use super::{BLUE, collect_points};
|
|
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 mut outlines = vec![];
|
|
let mut pts = vec![];
|
|
for id in ids {
|
|
let n = doc.node(*id)?;
|
|
collect_points(n, &mut pts);
|
|
n.walk(&mut |c| {
|
|
if let Some(p) = c.path_data() {
|
|
outlines.push(p.to_bezpath());
|
|
}
|
|
});
|
|
}
|
|
let first = *pts.first()?;
|
|
let b = pts.iter().fold(Rect::from_points(first, first), |r, p| r.union_pt(*p));
|
|
Some((outlines, b))
|
|
}
|
|
|
|
/// The mesh the engine uses for `ids` (shared so tool overlays match the command).
|
|
pub fn mesh_for(doc: &Document, ids: &[NodeId], expand: f64) -> Option<Mesh> {
|
|
let (o, b) = mesh_input(doc, ids)?;
|
|
Some(Mesh::build(&o, b, MeshOptions { expand, ..Default::default() }))
|
|
}
|
|
|
|
#[derive(Default)]
|
|
pub struct PuppetWarpTool {
|
|
ids: Vec<NodeId>,
|
|
pins: Vec<Point>,
|
|
selected: Option<usize>,
|
|
drag: Option<(usize, Point)>,
|
|
/// Control bar: Show Mesh.
|
|
show_mesh: bool,
|
|
/// Control bar: Expand Mesh (points).
|
|
expand: f64,
|
|
}
|
|
|
|
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;
|
|
}
|
|
self.ids = ids;
|
|
self.selected = None;
|
|
self.pins = if self.ids.is_empty() {
|
|
vec![]
|
|
} else {
|
|
mesh_for(cx.doc, &self.ids, self.expand_or_default()).map(|m| auto_pins(&m, 3)).unwrap_or_default()
|
|
};
|
|
}
|
|
fn expand_or_default(&self) -> f64 {
|
|
if self.expand > 0.0 { self.expand } else { 3.0 }
|
|
}
|
|
fn pin_at(&self, cx: &ToolContext, p: Point) -> Option<usize> {
|
|
let tol = cx.tol(7.0);
|
|
self.pins.iter().enumerate().filter(|(_, q)| q.distance(p) <= tol).min_by(|a, b| a.1.distance(p).total_cmp(&b.1.distance(p))).map(|(i, _)| i)
|
|
}
|
|
fn params(&self, i: usize, to: Point) -> Value {
|
|
let mut moved = self.pins.clone();
|
|
moved[i] = to;
|
|
let pj = |v: &[Point]| Value::Array(v.iter().map(|p| json!([p.x, p.y])).collect());
|
|
json!({"ids": crate::json_ids(&self.ids), "pins": pj(&self.pins), "moved": pj(&moved), "expand": self.expand_or_default()})
|
|
}
|
|
}
|
|
|
|
impl Tool for PuppetWarpTool {
|
|
fn id(&self) -> &'static str {
|
|
"puppetWarp"
|
|
}
|
|
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
|
let p = ev.pos;
|
|
match ev.kind {
|
|
PointerKind::Down => {
|
|
self.sync(cx);
|
|
if self.ids.is_empty() {
|
|
// Click an object to select it; pins appear on the next event.
|
|
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![];
|
|
}
|
|
if let Some(i) = self.pin_at(cx, p) {
|
|
if ev.mods.alt {
|
|
self.pins.remove(i);
|
|
self.selected = None;
|
|
return vec![];
|
|
}
|
|
self.selected = Some(i);
|
|
self.drag = Some((i, p));
|
|
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 && !ev.mods.alt {
|
|
self.pins.push(p);
|
|
self.selected = Some(self.pins.len() - 1);
|
|
}
|
|
vec![]
|
|
}
|
|
PointerKind::Drag => match self.drag {
|
|
Some((i, _)) => {
|
|
self.drag = Some((i, p));
|
|
vec![Action::Preview("object.puppetWarp".into(), self.params(i, p))]
|
|
}
|
|
None => vec![],
|
|
},
|
|
PointerKind::Up => match self.drag.take() {
|
|
Some((i, _)) => {
|
|
self.pins[i] = p;
|
|
vec![Action::Commit]
|
|
}
|
|
None => vec![],
|
|
},
|
|
_ => vec![],
|
|
}
|
|
}
|
|
fn key(&mut self, _cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
|
if matches!(key, ToolKey::Delete | ToolKey::Backspace)
|
|
&& let Some(i) = self.selected.take()
|
|
&& i < self.pins.len()
|
|
{
|
|
self.pins.remove(i);
|
|
}
|
|
vec![]
|
|
}
|
|
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_or_default())
|
|
{
|
|
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();
|
|
}
|
|
out.push(Overlay::Path { path: bp, color: [0x9a, 0x9a, 0x9a], width: 0.5, dashed: false });
|
|
}
|
|
let r = cx.tol(5.0);
|
|
for (i, q) in self.pins.iter().enumerate() {
|
|
let q = match self.drag {
|
|
Some((d, to)) if d == i => to,
|
|
_ => *q,
|
|
};
|
|
let circle = super::ellipse_path(q, r, r, 0.0);
|
|
out.push(Overlay::Path { path: circle, color: BLUE, width: if self.selected == Some(i) { 3.0 } else { 1.5 }, dashed: false });
|
|
out.push(Overlay::Anchor { p: q, color: BLUE, filled: self.selected == Some(i), size: 3.0 });
|
|
}
|
|
out
|
|
}
|
|
fn cursor(&self, cx: &ToolContext, p: Point, mods: Mods) -> Cursor {
|
|
match self.pin_at(cx, p) {
|
|
Some(_) if mods.alt => Cursor::PenDelete,
|
|
Some(_) => Cursor::Move,
|
|
None => Cursor::PenAdd,
|
|
}
|
|
}
|
|
fn options(&self) -> Value {
|
|
json!({"showMesh": self.show_mesh, "expand": self.expand_or_default(), "pins": self.pins.iter().map(|p| json!([p.x, p.y])).collect::<Vec<_>>()})
|
|
}
|
|
fn set_option(&mut self, key: &str, value: &Value) {
|
|
match key {
|
|
"showMesh" => self.show_mesh = value.as_bool().unwrap_or(self.show_mesh),
|
|
"expand" => self.expand = value.as_f64().unwrap_or(self.expand).clamp(0.0, 1000.0),
|
|
"selectAllPins" => self.selected = None,
|
|
_ => {}
|
|
}
|
|
}
|
|
fn busy(&self) -> bool {
|
|
self.drag.is_some()
|
|
}
|
|
fn notify(&mut self, cx: &ToolContext, _what: &str) {
|
|
self.sync(cx);
|
|
}
|
|
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
|
self.ids.clear();
|
|
self.pins.clear();
|
|
if self.drag.take().is_some() { vec![Action::Commit] } else { vec![] }
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::testutil::*;
|
|
use vectorcraft_doc::Selection;
|
|
|
|
#[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));
|
|
// 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!(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 deletes it.
|
|
t.pointer(&c, &PointerEvent::new(PointerKind::Down, 120.0, 190.0).with_mods(Mods { alt: true, ..Default::default() }));
|
|
assert_eq!(t.pins.len(), 3);
|
|
}
|
|
}
|