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.
This commit is contained in:
texjer committed 2026-11-04 10:31:22 -05:00
1 parent d890289a92
commit cb97fcb0bd
3 files changed
+59 -15

No files matched your search

+27 -14
View File
@@ -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<crate::Overlay> {
/// 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<designcraft_geom::Rect> {
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()
+31
View File
@@ -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:?}");
}
+1 -1
View File
@@ -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.