Files
vectorcraft/crates/engine/src/tests_puppetwarp.rs
T
bflatasticandClaude Opus 5.5 67b2213957 M8.5: Puppet Warp pins rotate, multi-select and Select All; Control bar and hint
- Alt-dragging near (not on) a selected pin turns the art around it; the pin
  keeps that turn through later warps. arap::Pin gains an angle constraint and
  object.puppetWarp takes angles: [deg|null...].
- Shift-click adds pins to the selection or takes them out; dragging a pin
  moves every selected pin. Select All Pins selects them all (it used to
  deselect). Alt-click no longer deletes a pin; Delete removes the selection.
- Control bar: Expand (0 allowed), Show Mesh (on by default), Select All
  Pins. Hint-bar text for the tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 10:28:05 +07:00

68 lines
3.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Puppet Warp: turning the art around a pin (`object.puppetWarp {angles}`) and the tool's pins.
use serde_json::json;
use vectorcraft_geom::{Point, Rect};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
use super::*;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
s
}
fn rect(s: &mut Session, x: f64, y: f64, w: f64, h: f64) -> NodeId {
let r = s.execute("shape.rectangle", &json!({"x": x, "y": y, "width": w, "height": h})).unwrap();
NodeId(r["id"].as_u64().unwrap())
}
fn bounds(s: &Session, id: NodeId) -> Rect {
s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap()
}
#[test]
fn a_pin_with_an_angle_turns_the_art_around_it() {
let mut s = session();
let a = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
// One pin at the centre, held at a quarter turn: the 200 × 100 rectangle stands up.
s.execute("object.puppetWarp", &json!({"id": a.0, "pins": [[200, 150]], "moved": [[200, 150]], "angles": [90]})).unwrap();
let b = bounds(&s, a);
assert!((b.width() - 100.0).abs() < 4.0 && (b.height() - 200.0).abs() < 4.0, "{b:?}");
assert!(b.center().distance(Point::new(200.0, 150.0)) < 2.0, "{b:?}");
// null leaves a pin free; the list must be as long as the pins.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("object.puppetWarp", &json!({"id": a.0, "pins": [[200, 150]], "moved": [[210, 150]], "angles": [null]})).unwrap();
assert!((bounds(&s, a).x0 - 110.0).abs() < 0.5, "a free single pin only moves the art");
for bad in [json!([90, 0]), json!("90"), json!({})] {
assert!(s.execute("object.puppetWarp", &json!({"id": a.0, "pins": [[200, 150]], "moved": [[200, 150]], "angles": bad})).is_err(), "{bad}");
}
}
#[test]
fn alt_drag_with_the_tool_turns_the_art_in_one_undo_step() {
let mut s = session();
let a = rect(&mut s, 100.0, 100.0, 200.0, 100.0);
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
let v = ViewInfo::default();
s.select_tool("puppetWarp", v).unwrap();
s.set_tool_option("selectAllPins", &json!(false));
// Select the pin in the middle, then Alt-drag about a quarter turn around it, 20 pt out.
s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 150.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 150.0), v).unwrap();
let n = s.doc().unwrap().history.undo.len();
let alt = Mods { alt: true, ..Default::default() };
for (k, x, y) in
[(PointerKind::Down, 220.0, 150.0), (PointerKind::Drag, 214.0, 164.0), (PointerKind::Drag, 200.0, 170.0), (PointerKind::Up, 200.0, 170.0)]
{
s.pointer(&PointerEvent::new(k, x, y).with_mods(alt), v).unwrap();
}
assert_eq!(s.doc().unwrap().history.undo.len(), n + 1);
let (cmd, p) = s.journal.last().unwrap();
assert_eq!(cmd, "object.puppetWarp");
let angles = p["angles"].as_array().unwrap();
assert!(angles.iter().any(|a| a.as_f64().is_some_and(|d| (d - 90.0).abs() < 15.0)), "about a quarter turn: {p}");
let b = bounds(&s, a);
assert!(b.height() > 110.0, "the art turned: {b:?}");
}