mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-26 11:36:58 -06:00
Canvas: resizing snaps the edge being dragged (#961)
snap_resize ran snap_rect on the whole box, which looks at all three edges on each axis and takes the nearest line. A fixed edge already on a line (distance 0) always won, so the dragged edge never snapped; a fixed edge near a line moved the dragged edge by its own distance. Resizing now looks only at the dragged edges (snap_edges), also for the grid fallback. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
|||||||
|
|
||||||
| Date | What landed |
|
| Date | What landed |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
| 2026-10-18 | Resizing with the mouse snaps the edge being dragged to the slide, guides and other shapes: a fixed edge that already lined up used to take the snap and leave the dragged edge free, and a fixed edge near a line shifted the dragged one (#961) |
|
||||||
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
| 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort |
|
||||||
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
| 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape |
|
||||||
| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) |
|
| 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) |
|
||||||
|
|||||||
@@ -4072,3 +4072,43 @@ fn a_shape_in_front_of_edited_text_takes_the_click() {
|
|||||||
let st = s.doc().unwrap();
|
let st = s.doc().unwrap();
|
||||||
assert!(st.selection.text.as_ref().is_none_or(|t| t.shape.0 as u64 != tb), "the click on the member in front left the text: {:?}", st.selection);
|
assert!(st.selection.text.as_ref().is_none_or(|t| t.shape.0 as u64 != tb), "the click on the member in front left the text: {:?}", st.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A box at `rect` and a second box at `other` on a fresh slide; smart guides on, the grid off.
|
||||||
|
/// The first box is selected.
|
||||||
|
fn snap_scene(rect: [f64; 4], other: [f64; 4]) -> (Session, u64) {
|
||||||
|
let mut s = session();
|
||||||
|
s.prefs.snap_to_grid = false;
|
||||||
|
s.execute("shape.insert", &json!({"preset": "rect", "rect": other})).unwrap();
|
||||||
|
let id = s.execute("shape.insert", &json!({"preset": "rect", "rect": rect})).unwrap()["id"].as_u64().unwrap();
|
||||||
|
s.execute("edit.select", &json!({"ids": [id]})).unwrap();
|
||||||
|
s.prefs.smart_guides = true;
|
||||||
|
(s, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn box_of(s: &mut Session, id: u64) -> serde_json::Value {
|
||||||
|
s.execute("shape.inspect", &json!({"id": id})).unwrap()["box"].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #961: dragging the east handle snaps the dragged (right) edge, even when the fixed left edge
|
||||||
|
/// already lines up with another shape. The nearest line used to be the fixed edge's, at distance 0.
|
||||||
|
#[test]
|
||||||
|
fn resizing_snaps_the_dragged_edge_when_the_fixed_edge_is_already_aligned() {
|
||||||
|
// The box spans x 100..200; the other box starts at 100 too and ends at 300.
|
||||||
|
let (mut s, id) = snap_scene([100.0, 100.0, 100.0, 80.0], [100.0, 300.0, 200.0, 80.0]);
|
||||||
|
drag_gesture(&mut s, (200.0, 140.0), (297.0, 140.0));
|
||||||
|
let b = box_of(&mut s, id);
|
||||||
|
assert_eq!(b["x"], 100.0, "the fixed edge stays: {b}");
|
||||||
|
assert_eq!(b["w"], 200.0, "the right edge snaps to the other box's right edge at 300: {b}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #961: only the dragged edge looks for a line. A fixed edge near a line must not shift the
|
||||||
|
/// edge being dragged by its own distance.
|
||||||
|
#[test]
|
||||||
|
fn resizing_is_not_shifted_by_a_fixed_edge_near_a_line() {
|
||||||
|
// The other box starts at 103, 3 pt from the fixed left edge; the dragged edge ends far from any line.
|
||||||
|
let (mut s, id) = snap_scene([100.0, 100.0, 100.0, 80.0], [103.0, 300.0, 50.0, 80.0]);
|
||||||
|
drag_gesture(&mut s, (200.0, 140.0), (421.0, 140.0));
|
||||||
|
let b = box_of(&mut s, id);
|
||||||
|
assert_eq!(b["x"], 100.0, "the fixed edge stays: {b}");
|
||||||
|
assert_eq!(b["w"], 321.0, "the dragged edge goes where the pointer went: {b}");
|
||||||
|
}
|
||||||
|
|||||||
@@ -1449,6 +1449,13 @@ impl Session {
|
|||||||
/// Smart guides: snap `r` edges/centres to the slide and other shapes. Returns the offset to
|
/// Smart guides: snap `r` edges/centres to the slide and other shapes. Returns the offset to
|
||||||
/// apply and the guides to draw.
|
/// apply and the guides to draw.
|
||||||
fn snap_rect(&self, r: Rect, exclude: &[ShapeId], tol: f64, enabled: bool) -> (Vec2, Vec<Guide>) {
|
fn snap_rect(&self, r: Rect, exclude: &[ShapeId], tol: f64, enabled: bool) -> (Vec2, Vec<Guide>) {
|
||||||
|
self.snap_edges(r, exclude, tol, enabled, [true; 3], [true; 3])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `snap_rect` looking only at some of `r`'s edges: `xs_on` is left, centre, right and `ys_on`
|
||||||
|
/// top, centre, bottom. Resizing passes just the edges being dragged, so a fixed edge that
|
||||||
|
/// already sits on a line (distance 0) doesn't win and leave the dragged edge unsnapped.
|
||||||
|
fn snap_edges(&self, r: Rect, exclude: &[ShapeId], tol: f64, enabled: bool, xs_on: [bool; 3], ys_on: [bool; 3]) -> (Vec2, Vec<Guide>) {
|
||||||
let Some(st) = self.active() else { return (Vec2::ZERO, vec![]) };
|
let Some(st) = self.active() else { return (Vec2::ZERO, vec![]) };
|
||||||
if !enabled || !self.prefs.smart_guides {
|
if !enabled || !self.prefs.smart_guides {
|
||||||
return (Vec2::ZERO, vec![]);
|
return (Vec2::ZERO, vec![]);
|
||||||
@@ -1486,7 +1493,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut best_x: Option<(f64, f64, Rect)> = None;
|
let mut best_x: Option<(f64, f64, Rect)> = None;
|
||||||
for edge in [r.x0, r.center().x, r.x1] {
|
for edge in [r.x0, r.center().x, r.x1].into_iter().zip(xs_on).filter_map(|(e, on)| on.then_some(e)) {
|
||||||
for (x, b) in &xs {
|
for (x, b) in &xs {
|
||||||
let d = x - edge;
|
let d = x - edge;
|
||||||
if d.abs() <= tol && best_x.is_none_or(|(bd, _, _)| d.abs() < bd.abs()) {
|
if d.abs() <= tol && best_x.is_none_or(|(bd, _, _)| d.abs() < bd.abs()) {
|
||||||
@@ -1495,7 +1502,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut best_y: Option<(f64, f64, Rect)> = None;
|
let mut best_y: Option<(f64, f64, Rect)> = None;
|
||||||
for edge in [r.y0, r.center().y, r.y1] {
|
for edge in [r.y0, r.center().y, r.y1].into_iter().zip(ys_on).filter_map(|(e, on)| on.then_some(e)) {
|
||||||
for (y, b) in &ys {
|
for (y, b) in &ys {
|
||||||
let d = y - edge;
|
let d = y - edge;
|
||||||
if d.abs() <= tol && best_y.is_none_or(|(bd, _, _)| d.abs() < bd.abs()) {
|
if d.abs() <= tol && best_y.is_none_or(|(bd, _, _)| d.abs() < bd.abs()) {
|
||||||
@@ -1509,15 +1516,29 @@ impl Session {
|
|||||||
off.x = d;
|
off.x = d;
|
||||||
let rr = r + Vec2::new(d, 0.0);
|
let rr = r + Vec2::new(d, 0.0);
|
||||||
guides.push(Guide { vertical: true, pos: x, from: b.y0.min(rr.y0), to: b.y1.max(rr.y1) });
|
guides.push(Guide { vertical: true, pos: x, from: b.y0.min(rr.y0), to: b.y1.max(rr.y1) });
|
||||||
} else if self.prefs.snap_to_grid {
|
} else if self.prefs.snap_to_grid && xs_on.contains(&true) {
|
||||||
off.x = deckcraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0;
|
let at = if xs_on[0] {
|
||||||
|
r.x0
|
||||||
|
} else if xs_on[2] {
|
||||||
|
r.x1
|
||||||
|
} else {
|
||||||
|
r.center().x
|
||||||
|
};
|
||||||
|
off.x = deckcraft_geom::snap(at, self.prefs.grid_spacing) - at;
|
||||||
}
|
}
|
||||||
if let Some((d, y, b)) = best_y {
|
if let Some((d, y, b)) = best_y {
|
||||||
off.y = d;
|
off.y = d;
|
||||||
let rr = r + Vec2::new(0.0, d);
|
let rr = r + Vec2::new(0.0, d);
|
||||||
guides.push(Guide { vertical: false, pos: y, from: b.x0.min(rr.x0), to: b.x1.max(rr.x1) });
|
guides.push(Guide { vertical: false, pos: y, from: b.x0.min(rr.x0), to: b.x1.max(rr.x1) });
|
||||||
} else if self.prefs.snap_to_grid {
|
} else if self.prefs.snap_to_grid && ys_on.contains(&true) {
|
||||||
off.y = deckcraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0;
|
let at = if ys_on[0] {
|
||||||
|
r.y0
|
||||||
|
} else if ys_on[2] {
|
||||||
|
r.y1
|
||||||
|
} else {
|
||||||
|
r.center().y
|
||||||
|
};
|
||||||
|
off.y = deckcraft_geom::snap(at, self.prefs.grid_spacing) - at;
|
||||||
}
|
}
|
||||||
(off, guides)
|
(off, guides)
|
||||||
}
|
}
|
||||||
@@ -1528,13 +1549,13 @@ impl Session {
|
|||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
let r = x.rect();
|
let r = x.rect();
|
||||||
let (off, guides) = self.snap_rect(r, &[id], tol * 1.5, true);
|
// Only the dragged edges move, so only they look for a line to snap to.
|
||||||
let mut nx = x;
|
|
||||||
// Only the dragged edges move.
|
|
||||||
let moves_left = matches!(handle, 0 | 6 | 7);
|
let moves_left = matches!(handle, 0 | 6 | 7);
|
||||||
let moves_right = matches!(handle, 2..=4);
|
let moves_right = matches!(handle, 2..=4);
|
||||||
let moves_top = matches!(handle, 0..=2);
|
let moves_top = matches!(handle, 0..=2);
|
||||||
let moves_bottom = matches!(handle, 4..=6);
|
let moves_bottom = matches!(handle, 4..=6);
|
||||||
|
let (off, guides) = self.snap_edges(r, &[id], tol * 1.5, true, [moves_left, false, moves_right], [moves_top, false, moves_bottom]);
|
||||||
|
let mut nx = x;
|
||||||
if moves_left {
|
if moves_left {
|
||||||
nx.x += off.x;
|
nx.x += off.x;
|
||||||
nx.w -= off.x;
|
nx.w -= off.x;
|
||||||
|
|||||||
Reference in New Issue
Block a user