mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 10:47:11 -06:00
Files dropped on the canvas of an open document are placed at the drop point (linked; Shift embeds); dropped on the tab bar or with no document open they open and join Open Recent Files. The canvas is outlined while files are dragged over it. On the web, drag events give the drop point and the files arrive through the inbox. File > Place picks several files. file.place.queue loads the place cursor (the `place` tool): a click places the current file at 100% with its top-left corner there, a drag places it at the dragged size with its aspect kept, the arrow keys cycle the files and Esc discards one; the previous tool returns after the last. The cursor shows the file's thumbnail and a count badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.6 KiB
Rust
59 lines
2.6 KiB
Rust
//! File → Place over MCP, headless: `file.place` through `run_command`, and the place cursor driven
|
||
//! by pointer gestures and keys.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use crate::{Backend, Headless, call_tool};
|
||
|
||
fn text(r: &crate::ToolResult) -> Value {
|
||
assert!(!r.is_error, "{r:?}");
|
||
serde_json::from_str(r.content[0]["text"].as_str().unwrap()).unwrap()
|
||
}
|
||
|
||
/// A headless session with a 400×300 document, and a 30×20 PNG as base64.
|
||
fn setup() -> (Headless, String) {
|
||
let mut h = Headless::new();
|
||
h.call("engine.execute", json!({"command": "file.new", "params": {"width": 30, "height": 20}})).unwrap();
|
||
let png = h.call("engine.execute", json!({"command": "document.serialize", "params": {"format": "png"}})).unwrap();
|
||
h.call("engine.execute", json!({"command": "file.new", "params": {"width": 400, "height": 300}})).unwrap();
|
||
(h, png["dataBase64"].as_str().unwrap().to_string())
|
||
}
|
||
|
||
fn images(h: &Headless) -> usize {
|
||
let mut n = 0;
|
||
h.session.doc().unwrap().doc.walk(|x| n += usize::from(matches!(x.kind, vectorcraft_doc::NodeKind::Image(_))));
|
||
n
|
||
}
|
||
|
||
#[test]
|
||
fn headless_run_command_file_place() {
|
||
let (mut h, png) = setup();
|
||
let r = text(&call_tool(
|
||
&mut h,
|
||
"run_command",
|
||
&json!({"command": "file.place", "params": {"name": "tile.png", "dataBase64": png, "at": [100, 50]}}),
|
||
));
|
||
assert_eq!((r["format"].as_str(), r["width"].as_f64(), r["height"].as_f64()), (Some("png"), Some(30.0), Some(20.0)), "{r}");
|
||
assert_eq!(images(&h), 1);
|
||
let sel = h.session.doc().unwrap().selection.objects.clone();
|
||
assert_eq!(sel.len(), 1);
|
||
assert_eq!(sel[0].0, r["ids"][0].as_u64().unwrap());
|
||
}
|
||
|
||
#[test]
|
||
fn headless_place_cursor_places_on_clicks_and_esc_ends_it() {
|
||
let (mut h, png) = setup();
|
||
let files = json!([{"name": "a.png", "dataBase64": png}, {"name": "b.png", "dataBase64": png}, {"name": "c.png", "dataBase64": png}]);
|
||
let r = text(&call_tool(&mut h, "run_command", &json!({"command": "file.place.queue", "params": {"files": files}})));
|
||
assert_eq!(r["count"], 3);
|
||
assert_eq!(h.session.tool_id(), "place");
|
||
let click = |x: f64| json!({"events": [{"kind": "down", "x": x, "y": 10}, {"kind": "up", "x": x, "y": 10}]});
|
||
text(&call_tool(&mut h, "pointer_gesture", &click(10.0)));
|
||
text(&call_tool(&mut h, "pointer_gesture", &click(60.0)));
|
||
assert_eq!(images(&h), 2);
|
||
let r = text(&call_tool(&mut h, "press_key", &json!({"key": "Escape"})));
|
||
assert_eq!(r["handledBy"], "tool");
|
||
assert_eq!(h.session.tool_id(), "selection", "the last file discarded: back to the previous tool");
|
||
assert_eq!(images(&h), 2);
|
||
}
|