diff --git a/crates/engine/src/cmd/file/place.rs b/crates/engine/src/cmd/file/place.rs index dd0d50608..b5ebe8d1f 100644 --- a/crates/engine/src/cmd/file/place.rs +++ b/crates/engine/src/cmd/file/place.rs @@ -425,6 +425,13 @@ fn advance_place_cursor(s: &mut Session) { } } +/// Drop the current loaded image without placing it and move to the next; the last one ends placing. +pub(super) fn place_skip(s: &mut Session, _p: &Value) -> Result { + validate_place_cursor(s, "place.skip")?; + advance_place_cursor(s); + Ok(json!({"remaining": loaded_count(s)})) +} + /// Validate bounded grid dimensions and a finite nonnegative gutter. fn grid_dimensions(p: &Value, command: &str, default_columns: usize, default_rows: usize) -> Result<(usize, usize, f64)> { let dimension = |key, default| -> Result { @@ -651,6 +658,32 @@ pub(super) mod image_place_tests { } } + fn test_png(shade: u8) -> String { + let png = designcraft_render::Rendered { width: 4, height: 4, pixels: vec![shade; 4 * 4 * 4] }.to_png(); + crate::cmd::base64_encode(&png) + } + + fn loaded_names(s: &Session) -> Vec { + let d = &s.doc().unwrap().doc; + s.loaded.iter().chain(s.loaded_queue.iter()).map(|g| d.assets[&g.asset].name.clone()).collect() + } + + #[test] + fn skip_drops_the_current_image_and_the_last_one_ends_placing() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let files: Vec<_> = (0..3).map(|i| json!({"base64": test_png(i as u8), "name": format!("{i}.png")})).collect(); + s.execute("place.load", &json!({"files": files})).unwrap(); + assert_eq!(s.execute("place.skip", &json!({})).unwrap()["remaining"], 2); + assert_eq!(loaded_names(&s), ["1.png", "2.png"]); + s.execute("place.skip", &json!({})).unwrap(); + assert_eq!(s.tool_id(), "placeGun"); + s.execute("place.skip", &json!({})).unwrap(); + assert!(s.loaded.is_none()); + assert_eq!(s.tool_id(), "selection"); + assert!(s.execute("place.skip", &json!({})).is_err()); + } + #[test] fn consecutive_path_placements_create_distinct_graphics() { let images = [image_file_with_color(30).unwrap(), image_file_with_color(31).unwrap(), image_file_with_color(32).unwrap()]; diff --git a/crates/engine/src/cmd/file/registry.rs b/crates/engine/src/cmd/file/registry.rs index 4092bbff2..0cc8425e1 100644 --- a/crates/engine/src/cmd/file/registry.rs +++ b/crates/engine/src/cmd/file/registry.rs @@ -3,7 +3,7 @@ use super::EngineError; use super::bytes::{base64_encode, to_bytes, to_bytes_with_preview, with_document_extension}; use super::new::{file_new, file_sample, save_preset}; use super::open::{file_open, file_open_bytes, file_open_recent, has_recent, recent_entry}; -use super::place::{file_place, place_contact_sheet, place_drop, place_grid, place_load, read_source}; +use super::place::{file_place, place_contact_sheet, place_drop, place_grid, place_load, place_skip, read_source}; use super::save::{FolderAccess, can_revert, file_revert, file_save, recovery_dir}; use super::snippets::{snippet_export, snippet_place}; use super::{CommandSpec, PRESETS, Value, always, bad, cmd, has_doc, json, ok, str_param}; @@ -160,7 +160,8 @@ pub fn specs() -> Vec { has_doc, place_contact_sheet ), - cmd!(noundo "place.cancel", "Cancel Image Placement", [], None, "{}", has_doc, |s, _| { + cmd!(noundo "place.skip", "Skip Loaded Image", [], None, "{} — drops the current loaded image without placing it and loads the next; the last one ends placing", has_doc, place_skip), + cmd!(noundo "place.cancel", "Cancel Image Placement", [], None, "{} — clears every loaded image", has_doc, |s, _| { s.loaded = None; s.loaded_queue.clear(); s.set_tool("selection"); diff --git a/crates/tools/src/nav.rs b/crates/tools/src/nav.rs index c3f79a6cc..c7ed65751 100644 --- a/crates/tools/src/nav.rs +++ b/crates/tools/src/nav.rs @@ -173,9 +173,10 @@ impl Tool for PlaceGun { } fn key(&mut self, _cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec { use crate::ToolKey; + // Escape drops the current image and loads the next; `place.cancel` clears them all. if key == ToolKey::Escape { self.start = None; - return vec![Action::Exec("place.cancel".into(), json!({}))]; + return vec![Action::Exec("place.skip".into(), json!({}))]; } // During a drag the arrow keys change the grid, and are handled even when it is already // at its limit, so they don't go on to nudge the selection. diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index 66e71adb3..1155dcca5 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -2527,6 +2527,16 @@ fn place_gun_grid_arrow_keys_are_handled_during_a_drag() { assert!(t.key(&cx, ToolKey::Delete, Mods::default()).is_empty()); } +/// Escape drops only the current loaded image; `place.cancel` clears them all. +#[test] +fn place_gun_escape_skips_the_current_image() { + let d = Document::new(&NewDocument::default()); + let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false)); + let cx = ctx(&d, &s, &c, &l); + let mut t = create("placeGun"); + assert_eq!(t.key(&cx, ToolKey::Escape, Mods::default()), vec![Action::Exec("place.skip".into(), serde_json::json!({}))]); +} + /// A place-cursor grid drag too thin for its cells shows and places the single frame an ordinary /// drag would; one with room shows its cells and places the grid. #[test]