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.