diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0eed45b34..177697839 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -163,6 +163,11 @@ pub struct Session { /// While [`Session::place_queue_read`] runs `file.place.queue`: the files read for it, which /// the command loads the place cursor with instead of reading them; `None` otherwise. pub(crate) queue_hand_off: Option, + /// The automation roots in force when the place cursor was last loaded (`None`: none were). + /// The cursor places its files later, on a click that may come from the person at the + /// keyboard: those files, and the files they link to, are read under these roots, so a queue + /// an agent loaded never reads beyond what it was given. + pub(crate) place_queue_roots: Option>, /// While [`Session::import_image_read`] runs `clipboard.importImage`: the image read for it, /// which the command loads instead of reading `dataBase64`; `None` otherwise. pub(crate) image_hand_off: Option, diff --git a/crates/engine/src/session/documents.rs b/crates/engine/src/session/documents.rs index 71233cea6..51b5b5776 100644 --- a/crates/engine/src/session/documents.rs +++ b/crates/engine/src/session/documents.rs @@ -79,6 +79,7 @@ impl Session { batch_steps: None, place_hand_off: None, queue_hand_off: None, + place_queue_roots: None, image_hand_off: None, trace_hand_off: None, place_cursor_requests: false, diff --git a/crates/engine/src/tests/place.rs b/crates/engine/src/tests/place.rs index 7c3ff56ab..3c86e513e 100644 --- a/crates/engine/src/tests/place.rs +++ b/crates/engine/src/tests/place.rs @@ -722,6 +722,39 @@ fn a_queue_of_3_with_click_drag_and_esc_places_2() { assert_eq!(s.doc().unwrap().history.undo.len(), 2, "one undo step per placement"); } +/// A place cursor loaded within automation roots places its files within them, even on a later +/// click outside any roots (the person at the keyboard's): a queued file outside the roots, or +/// one it links to, is not read then. One loaded outside any roots places as before. +#[test] +fn the_place_cursor_places_within_the_roots_it_was_loaded_in() { + let base = std::env::temp_dir().join(format!("vectorcraft-place-roots-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + std::fs::create_dir_all(base.join("in")).unwrap(); + let outside = base.join("outside.png"); + std::fs::write(&outside, png(40, 20, [9, 9, 9, 255], None)).unwrap(); + let entry = json!({"params": {"path": outside.to_string_lossy()}, "name": "outside.png", "width": 40, "height": 20}); + let roots = crate::file_access::AutomationRoots::new(&[&base.join("in")], &[]).unwrap().unwrap(); + let view = ViewInfo::default(); + let click = |s: &mut Session| { + s.pointer(&PointerEvent::new(PointerKind::Down, 10.0, 10.0), view)?; + s.pointer(&PointerEvent::new(PointerKind::Up, 10.0, 10.0), view) + }; + let mut s = session(); + s.select_tool("place", view).unwrap(); + // Loaded within the roots (as a tool option, past the check tool.setOption makes of paths). + crate::file_access::confine(Some(&roots), || s.set_tool_option("queue", &json!({"entries": [entry]}))); + assert!(s.place_queue_roots().is_some()); + let placed = crate::file_access::unconfined(|| click(&mut s)); + assert!(top_children(&s).is_empty(), "{placed:?}"); + // Loaded outside any roots: the click places it. + s.select_tool("place", view).unwrap(); + crate::file_access::unconfined(|| s.set_tool_option("queue", &json!({"entries": [entry]}))); + assert!(s.place_queue_roots().is_none()); + crate::file_access::unconfined(|| click(&mut s)).unwrap(); + assert_eq!(top_children(&s).len(), 1); + let _ = std::fs::remove_dir_all(base); +} + /// With [`Session::place_cursor_requests`] set, a click of the place cursor asks the frontend to /// place its file ([`UiRequest::Place`]) and places nothing itself; the params it asks with place /// the file as the click places it without the flag. diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index ecbc45964..d5c03a651 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -658,11 +658,27 @@ impl Session { self.tool.options() } pub fn set_tool_option(&mut self, key: &str, v: &Value) { + if key == "queue" && self.tool.id() == "place" { + self.note_place_queue_roots(); + } self.tool.set_option(key, v); self.tool_set_since_reset = true; self.keep_tool_settings(); } + /// Keep the automation roots in force now as those the place cursor's queue, being loaded, + /// is placed under ([`Session::place_queue_roots`]). + fn note_place_queue_roots(&mut self) { + self.place_queue_roots = crate::file_access::current(); + } + + /// The automation roots the place cursor's files are placed under: those in force when it + /// was loaded. A frontend that places them itself ([`UiRequest::Place`]) reads them within + /// these roots ([`crate::file_access::confine`]). + pub fn place_queue_roots(&self) -> Option> { + self.place_queue_roots.clone() + } + /// The options of tool `id`: the active tool's, or those it would have if chosen now. pub fn tool_options_of(&self, id: &str) -> Value { if id == self.tool.id() { self.tool.options() } else { self.make_tool(id).options() } @@ -673,6 +689,9 @@ impl Session { /// Liquify tools' Global Brush Dimensions, the Smooth tool's Fidelity for Pencil, Paintbrush /// and Blob Brush Alt smoothing). Returns the tool's options. pub fn set_tool_options(&mut self, id: Option<&str>, values: &Map) -> Value { + if values.contains_key("queue") && id.unwrap_or(self.tool.id()) == "place" { + self.note_place_queue_roots(); + } let active = id.is_none_or(|id| id == self.tool.id()); self.tool_set_since_reset |= active; let mut other = (!active).then(|| self.make_tool(id.unwrap_or_default())); @@ -842,6 +861,13 @@ impl Session { } // The place cursor's file, placed by the frontend that asked for it. Action::Exec(cmd, p) if cmd == "file.place" && self.place_cursor_requests => ui.push(UiRequest::Place(p)), + // The place cursor's file, read within the roots of whoever loaded the cursor. + Action::Exec(cmd, p) if cmd == "file.place" => { + let roots = self.place_queue_roots.clone(); + if let Some(w) = crate::file_access::confine(roots.as_ref(), || self.execute(&cmd, &p))?.get("warning").and_then(Value::as_str) { + ui.push(UiRequest::Status(w.to_string())); + } + } Action::Exec(cmd, p) => { // A command's warning (Crop Image's when the image couldn't be decoded) goes to the // status bar too. diff --git a/crates/ui-egui/src/place.rs b/crates/ui-egui/src/place.rs index c2b920b83..d4f4412ec 100644 --- a/crates/ui-egui/src/place.rs +++ b/crates/ui-egui/src/place.rs @@ -376,13 +376,18 @@ fn queue_files(app: &mut VectorcraftApp, p: &Value, read: Read) -> Result