mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Line a moved selection's edges up with ruler guides
Fixes #5746 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
a88fbcedb8
commit
568bb9315d
@@ -2001,6 +2001,52 @@ fn a_move_held_by_a_paths_stroke_lands_the_path_on_a_ruler_guide() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #5746: a rectangle dragged until its left edge and its top edge are each 1 px from a ruler
|
||||
/// guide lands on both guides, with "align", whichever guide was added last and wherever it was
|
||||
/// grabbed; between two upright guides, its left edge lands on the one within reach; and an edge
|
||||
/// Smart Guides line up with another object stays there, not on a ruler guide as near.
|
||||
#[test]
|
||||
fn a_moved_rectangle_lines_its_edges_up_with_two_ruler_guides() {
|
||||
use vectorcraft_tools::Overlay;
|
||||
use vectorcraft_tools::PointerKind::{Down, Drag, Up};
|
||||
let v = ViewInfo::default();
|
||||
// (guides as (vertical, pos), where it is grabbed, the top left it lands on)
|
||||
type Case = (&'static [(bool, f64)], (f64, f64), (f64, f64));
|
||||
let cases: [Case; 3] = [
|
||||
(&[(true, 250.0), (false, 320.0)], (200.0, 100.0), (250.0, 320.0)),
|
||||
(&[(false, 320.0), (true, 250.0)], (100.0, 175.0), (250.0, 320.0)),
|
||||
(&[(true, 250.0), (true, 455.0)], (200.0, 175.0), (250.0, 100.0)),
|
||||
];
|
||||
for (guides, (x, y), want) in cases {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 600, "height": 600})).unwrap();
|
||||
let id = NodeId(s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 150})).unwrap()["id"].as_u64().unwrap());
|
||||
for (vertical, pos) in guides {
|
||||
s.execute("guide.add", &json!({"vertical": vertical, "pos": pos})).unwrap();
|
||||
}
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
// 1 px past the guides: the left edge at 251, the top edge at 321 (or where it was).
|
||||
let (to_x, to_y) = (x + 151.0, if want.1 == 100.0 { y } else { y + 221.0 });
|
||||
gesture(&mut s, v, &[(Down, x, y), (Drag, (x + to_x) / 2.0, (y + to_y) / 2.0), (Drag, to_x, to_y)]);
|
||||
let align = s.overlays(v).iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align"));
|
||||
gesture(&mut s, v, &[(Up, to_x, to_y)]);
|
||||
assert_eq!(top_left(&s, id), want, "guides {guides:?}, grabbed at ({x}, {y})");
|
||||
assert!(align, "guides {guides:?}, grabbed at ({x}, {y}): no align");
|
||||
}
|
||||
// Dragged by its centre until its left edge is at 401: 1 px from another rectangle's left
|
||||
// edge (400) and 1 px from a ruler guide (402).
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let id = NodeId(s.execute("shape.rectangle", &json!({"x": 100, "y": 100, "width": 200, "height": 150})).unwrap()["id"].as_u64().unwrap());
|
||||
s.execute("shape.rectangle", &json!({"x": 400, "y": 400, "width": 60, "height": 60})).unwrap();
|
||||
s.execute("guide.add", &json!({"vertical": true, "pos": 402.0})).unwrap();
|
||||
s.execute("select.none", &json!({})).unwrap();
|
||||
s.select_tool("selection", v).unwrap();
|
||||
gesture(&mut s, v, &[(Down, 200.0, 175.0), (Drag, 350.0, 175.0), (Drag, 501.0, 175.0), (Up, 501.0, 175.0)]);
|
||||
assert_eq!(top_left(&s, id), (400.0, 100.0), "a Smart Guides alignment gave way to a ruler guide");
|
||||
}
|
||||
|
||||
/// #3701: at 28,757%, Direct Selection pressed 1.5 px beside a rectangle's straight left side and
|
||||
/// dragged until the side is 1 px from a ruler guide lands both its anchors on the guide; so does
|
||||
/// the same press and drag on the rectangle selected whole, which moves it.
|
||||
|
||||
@@ -964,7 +964,14 @@ impl Targets {
|
||||
/// ([`Self::for_move`]) is spaced evenly along an axis that lines up with nothing
|
||||
/// ([`Self::even_spacing`]). Returns the shift and the guides.
|
||||
pub fn snap_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec<Overlay>) {
|
||||
let (d, ov, _) = self.snap_rects_axes(rects, tol);
|
||||
(d, ov)
|
||||
}
|
||||
|
||||
/// [`Self::snap_rects`], also saying which axes (x, y) lined up or were spaced evenly.
|
||||
pub(crate) fn snap_rects_axes(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec<Overlay>, [bool; 2]) {
|
||||
let (mut d, mut ov, aligned) = self.align_rects(rects, tol);
|
||||
let mut taken = aligned;
|
||||
if let [r] = rects
|
||||
&& !self.boxes.is_empty()
|
||||
{
|
||||
@@ -973,6 +980,7 @@ impl Targets {
|
||||
for (x, lined_up) in [true, false].into_iter().zip(aligned) {
|
||||
if let Some((shift, _)) = self.even_spacing(moved, x, tol).filter(|_| !lined_up) {
|
||||
*(if x { &mut d.x } else { &mut d.y }) += shift;
|
||||
taken[usize::from(!x)] = true;
|
||||
spaced = true;
|
||||
}
|
||||
}
|
||||
@@ -985,7 +993,7 @@ impl Targets {
|
||||
}
|
||||
}
|
||||
}
|
||||
(d, ov)
|
||||
(d, ov, taken)
|
||||
}
|
||||
|
||||
/// Spacing Guides along the x axis (`x`) or the y axis for the moved rectangle `r`, among the
|
||||
@@ -1088,6 +1096,50 @@ impl Targets {
|
||||
(d, ov, [best_x.is_some(), best_y.is_some()])
|
||||
}
|
||||
|
||||
/// Snap a selection grabbed at `p` and moved as a whole, its bounds now `r`, to the ruler
|
||||
/// guides: the grabbed point goes into line with a guide running within `tol` of it
|
||||
/// ([`Self::snap_point`]), and on an axis where none does and that is not `taken` (x, y)
|
||||
/// already, the edge or centre of `r` nearest a guide running past it within `tol` goes onto it
|
||||
/// (#5746). Returns the shift, the guides (none when nothing lines up) and whether a guide held
|
||||
/// the grabbed point.
|
||||
pub(crate) fn snap_moved_to_rulers(&self, p: Point, r: Option<Rect>, tol: f64, taken: [bool; 2]) -> (Vec2, Vec<Overlay>, bool) {
|
||||
let near = |vertical: bool| {
|
||||
let v = if vertical { p.x } else { p.y };
|
||||
self.rulers.iter().any(|g| g.vertical == vertical && (g.pos - v).abs() <= tol && g.line_at(p, tol).is_some())
|
||||
};
|
||||
let held = [near(true), near(false)];
|
||||
let (q, mut ov) = if held.contains(&true) { self.snap_point(p, tol) } else { (p, vec![]) };
|
||||
let mut d = q - p;
|
||||
let Some(r) = r.map(|r| r + d) else { return (d, ov, held.contains(&true)) };
|
||||
// The guide upright (`vertical`) or level nearest one of `vs`, running past `r` across.
|
||||
let nearest = |vertical: bool, vs: [f64; 3]| {
|
||||
let (c, half) =
|
||||
if vertical { (Point::new(0.0, r.center().y), r.height() / 2.0) } else { (Point::new(r.center().x, 0.0), r.width() / 2.0) };
|
||||
self.rulers
|
||||
.iter()
|
||||
.filter(|g| g.vertical == vertical)
|
||||
.filter_map(|g| g.line_at(c, tol + half))
|
||||
.flat_map(|(t, from, _)| vs.into_iter().map(move |v| (t - v, t, from)))
|
||||
.filter(|(s, ..)| s.abs() <= tol)
|
||||
.min_by(|a, b| a.0.abs().total_cmp(&b.0.abs()))
|
||||
};
|
||||
let hx = if held[0] || taken[0] { None } else { nearest(true, [r.x0, r.center().x, r.x1]) };
|
||||
let hy = if held[1] || taken[1] { None } else { nearest(false, [r.y0, r.center().y, r.y1]) };
|
||||
let shift = Vec2::new(hx.map_or(0.0, |h| h.0), hy.map_or(0.0, |h| h.0));
|
||||
let (r, lined) = (r + shift, hx.is_some() || hy.is_some());
|
||||
d += shift;
|
||||
if let Some((_, x, from)) = hx {
|
||||
ov.extend(self.line(Point::new(x, from.y.min(r.y0)), Point::new(x, from.y.max(r.y1))));
|
||||
}
|
||||
if let Some((_, y, from)) = hy {
|
||||
ov.extend(self.line(Point::new(from.x.min(r.x0), y), Point::new(from.x.max(r.x1), y)));
|
||||
}
|
||||
if lined && !held.contains(&true) {
|
||||
ov.extend(self.label(p + d, "align"));
|
||||
}
|
||||
(d, ov, held.contains(&true))
|
||||
}
|
||||
|
||||
/// Snap a bounding-box resize. `a` is the scale [`crate::bbox::scale_for_drag`] gave for
|
||||
/// dragging `handle` of the box `bx` (about the centre when `from_center`), in the box's own
|
||||
/// frame, and so is the result. The handle lands on an anchor or centre within `tol` on the page
|
||||
|
||||
@@ -206,7 +206,7 @@ pub(crate) struct MoveSnap {
|
||||
copy_targets: Option<Targets>,
|
||||
copy_points: Option<Targets>,
|
||||
/// With Smart Guides on, the ruler guides View › Snap to Point still pulls the grabbed point
|
||||
/// onto (#1534); `points` holds them with Smart Guides off.
|
||||
/// onto (#1534), and lines the bounds up with (#5746); `points` holds them with Smart Guides off.
|
||||
rulers: Option<Targets>,
|
||||
}
|
||||
|
||||
@@ -245,6 +245,8 @@ impl MoveSnap {
|
||||
d = q - start;
|
||||
guides = ov;
|
||||
}
|
||||
// The axes (x, y) Smart Guides already put in line with other art.
|
||||
let mut taken = [false; 2];
|
||||
if let (Some(t), Some(b)) = (targets, self.bounds) {
|
||||
// The point the selection was grabbed by, near another object's anchor or centre,
|
||||
// lands on it, rather than the bounds lining up a little beside it (#955).
|
||||
@@ -252,18 +254,26 @@ impl MoveSnap {
|
||||
let (q, ov) = t.snap_point(start + d, cx.snap_tol());
|
||||
d = q - start;
|
||||
guides = ov;
|
||||
taken = [true; 2];
|
||||
} else {
|
||||
let (adj, ov) = t.snap_rect(b + d, cx.snap_tol());
|
||||
let (adj, ov, axes) = t.snap_rects_axes(&[b + d], cx.snap_tol());
|
||||
d += adj;
|
||||
guides = ov;
|
||||
taken = axes;
|
||||
}
|
||||
}
|
||||
// A ruler guide within reach of the grabbed point wins over lining the bounds up.
|
||||
// A ruler guide within reach of the grabbed point wins over lining the bounds up; on an
|
||||
// axis without one that Smart Guides left free, the bounds line up with the ruler guides
|
||||
// (#5746), alongside the Smart Guides.
|
||||
if let Some(t) = &self.rulers {
|
||||
let (q, ov) = t.snap_point(start + d, cx.tol(cx.snap_tolerance));
|
||||
if q != start + d {
|
||||
d = q - start;
|
||||
guides = ov;
|
||||
let (adj, ov, held) = t.snap_moved_to_rulers(start + d, self.bounds.map(|b| b + d), cx.tol(cx.snap_tolerance), taken);
|
||||
if adj != Vec2::ZERO {
|
||||
d += adj;
|
||||
if held {
|
||||
guides = ov;
|
||||
} else {
|
||||
guides.extend(ov);
|
||||
}
|
||||
}
|
||||
}
|
||||
if cx.snap_to_pixel
|
||||
|
||||
Reference in New Issue
Block a user