From cb97fcb0bdcccea9a192b5eeedfc6b533bb4f667 Mon Sep 17 00:00:00 2001 From: texjer Date: Wed, 4 Nov 2026 10:31:22 -0500 Subject: [PATCH] Place: a grid drag too small for its cells previews and places one frame The place cursor's grid preview fell back to the raw drag rectangle when the cells had no positive size, while the release still sent `place.grid`, which rejected the area. Both now use the same cell calculation (`grid_cells`), and a grid with no room for its cells shows and places the single proportional frame of an ordinary drag. --- crates/tools/src/nav.rs | 41 +++++++++++++++++++++++------------ crates/tools/src/tests.rs | 31 ++++++++++++++++++++++++++ docs/multi-image-placement.md | 2 +- 3 files changed, 59 insertions(+), 15 deletions(-) diff --git a/crates/tools/src/nav.rs b/crates/tools/src/nav.rs index 2b84010d1..762a4f0f3 100644 --- a/crates/tools/src/nav.rs +++ b/crates/tools/src/nav.rs @@ -149,9 +149,10 @@ impl Tool for PlaceGun { if click { return vec![Action::Exec("place.drop".into(), json!({"spread": crate::spread_json(sr), "x": sa.x, "y": sa.y}))]; } - // Arrow keys during the drag set a grid: the queued images fill its cells. - if self.columns > 1 || self.rows > 1 { - let r = designcraft_geom::Rect::from_points(sa, b); + // Arrow keys during the drag set a grid: the queued images fill its cells. A drag + // too small for the grid's cells places one frame instead, as the overlay shows. + let r = designcraft_geom::Rect::from_points(sa, b); + if !grid_cells(r, self.columns, self.rows).is_empty() { return vec![Action::Exec( "place.grid".into(), json!({"spread": crate::spread_json(sr), "rect": [r.x0, r.y0, r.x1, r.y1], "columns": self.columns, "rows": self.rows}), @@ -195,8 +196,13 @@ impl Tool for PlaceGun { // Under the drag threshold the release is a click (actual size where it was pressed): // no frame is drawn yet, the loaded cursor shows what lands. let click = (self.cur - a).hypot() < cx.tol(3.0); - if !click && (self.columns > 1 || self.rows > 1) { - return grid_overlays(designcraft_geom::Rect::from_points(a, self.cur), self.columns, self.rows); + // A grid draws its cells; one too small for them falls back to the single frame, as the + // release does. + if !click { + let cells = grid_cells(designcraft_geom::Rect::from_points(a, self.cur), self.columns, self.rows); + if !cells.is_empty() { + return cells.into_iter().map(crate::Overlay::Marquee).collect(); + } } let Some(size) = cx.loaded else { return vec![crate::Overlay::Marquee(designcraft_geom::Rect::from_points(a, self.cur))]; @@ -228,21 +234,28 @@ Y: {pct}" } } -/// The cells of a `columns` × `rows` place grid drawn over `rect`, 6 pt apart, as `place.grid` -/// lays them out; the whole rect when the cells would vanish. -fn grid_overlays(rect: designcraft_geom::Rect, columns: usize, rows: usize) -> Vec { +/// The gutter between place-grid cells, in points: `place.grid`'s default. +const PLACE_GRID_GUTTER: f64 = 6.0; + +/// The cells of a `columns` × `rows` place grid drawn over `rect`, [`PLACE_GRID_GUTTER`] apart, as +/// `place.grid` lays them out. None for a 1 × 1 grid, or when `rect` is too small for the cells to +/// have a positive size: then the drag places one frame. +fn grid_cells(rect: designcraft_geom::Rect, columns: usize, rows: usize) -> Vec { let (columns, rows) = (columns.clamp(1, 100), rows.clamp(1, 100)); - let w = (rect.width() - 6.0 * (columns - 1) as f64) / columns as f64; - let h = (rect.height() - 6.0 * (rows - 1) as f64) / rows as f64; + if columns == 1 && rows == 1 { + return vec![]; + } + let w = (rect.width() - PLACE_GRID_GUTTER * (columns - 1) as f64) / columns as f64; + let h = (rect.height() - PLACE_GRID_GUTTER * (rows - 1) as f64) / rows as f64; if !(w > 0.0 && h > 0.0) { - return vec![crate::Overlay::Marquee(rect)]; + return vec![]; } (0..rows) .flat_map(|row| { (0..columns).map(move |column| { - let x = rect.x0 + column as f64 * (w + 6.0); - let y = rect.y0 + row as f64 * (h + 6.0); - crate::Overlay::Marquee(designcraft_geom::Rect::new(x, y, x + w, y + h)) + let x = rect.x0 + column as f64 * (w + PLACE_GRID_GUTTER); + let y = rect.y0 + row as f64 * (h + PLACE_GRID_GUTTER); + designcraft_geom::Rect::new(x, y, x + w, y + h) }) }) .collect() diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index 28cfecf01..790b3c7f8 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -2369,3 +2369,34 @@ fn place_gun_grid_arrow_keys_are_handled_during_a_drag() { } assert!(t.key(&cx, ToolKey::Delete, Mods::default()).is_empty()); } + +/// 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] +fn place_gun_grid_too_small_for_its_cells_previews_and_places_one_frame() { + let d = Document::new(&NewDocument::default()); + let (s, c, l) = (Selection::default(), Cache::new(), CanvasLayout::new(&d, false)); + let mut cx = ctx(&d, &s, &c, &l); + cx.loaded = Some((40.0, 20.0)); + // Drag from (10, 10) to `to`, pressing ↑ (two rows) when `grid`: the overlay before release, + // and the release. + let drag = |to: (f64, f64), grid: bool| { + let mut t = create("placeGun"); + let _ = t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0)); + let _ = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, to.0, to.1)); + if grid { + assert_eq!(t.key(&cx, ToolKey::Up, Mods::default()), vec![Action::Handled]); + } + let shown = t.overlays(&cx); + (shown, t.pointer(&cx, &PointerEvent::new(PointerKind::Up, to.0, to.1))) + }; + // 100 × 8 pt: two rows 6 pt apart leave each a 1 pt cell; 100 × 4 pt leaves none. + let (shown, out) = drag((110.0, 18.0), true); + assert_eq!(shown.len(), 2, "{shown:?}"); + assert!(matches!(out.as_slice(), [Action::Exec(cmd, p)] if cmd == "place.grid" && p["rows"] == 2), "{out:?}"); + let (shown, out) = drag((110.0, 14.0), true); + let (single_shown, single_out) = drag((110.0, 14.0), false); + assert_eq!(shown, single_shown); + assert_eq!(out, single_out); + assert!(matches!(out.as_slice(), [Action::Exec(cmd, _)] if cmd == "place.drop"), "{out:?}"); +} diff --git a/docs/multi-image-placement.md b/docs/multi-image-placement.md index 997414bcd..da8f180bf 100644 --- a/docs/multi-image-placement.md +++ b/docs/multi-image-placement.md @@ -5,7 +5,7 @@ Choose **File > Place** (Cmd+D on macOS) and select several images. They load in - Click to place the next image at its natural size. - Drag to draw its frame: it keeps the image's proportions, and shows the scale beside the pointer. - Click an existing graphic frame to replace its contents. -- While drawing, Right/Left add/remove columns and Up/Down add/remove rows. Release to place a grid in row order with 6-point gutters. Extra images remain in the cursor; unused grid cells create no empty frames. +- While drawing, Right/Left add/remove columns and Up/Down add/remove rows. Release to place a grid in row order with 6-point gutters. Extra images remain in the cursor; unused grid cells create no empty frames. A drag too small for the grid's cells shows and places one frame instead, as an ordinary drag does. - Press Escape or select another tool to cancel the remaining placement. A single selected file retains the normal Place behavior, including filling a selected frame and PDF import options. Image batches support up to 1000 images. Rows and columns each range from 1 to 100. A batch containing an unreadable image fails without replacing the existing queue or changing the document. An invalid grid area also leaves the document and queue unchanged.