Smart Guides: "align" also on a handle already in line, Free Transform's move offset and slice readouts follow Measurement Labels (review of #453)

- `Targets::align_rects` tells `snap_point_with` an axis lined up even when
  nothing moved or Alignment Guides hide the line, so the "align" label stays.
- Free Transform's move offset is a measurement label (as the Selection
  tool's), its scale/rotate readouts Transform Tools; slice move/resize
  readouts follow Measurement Labels.
- Snap to Point (Smart Guides off) keeps the default look for moves too, as
  for drawn points and guides.
- Test: guides dragged out of a ruler (#451) follow Snapping Tolerance,
  Anchor/Path Labels and Measurement Labels; docs/mcp.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bflatasticandClaude Opus 5.5 committed 2026-10-08 15:46:15 +07:00
1 parent 14d7b3ddce
commit 42fced4725
5 files changed
+43 -12

No files matched your search

+16 -5
View File
@@ -243,7 +243,7 @@ impl Targets {
/// What a dragged selection's grabbed point snaps to with View → Snap to Point and Smart
/// Guides off: the anchors of the art but `exclude` and the ruler guides.
pub fn snap_to_point(cx: &ToolContext, exclude: &[NodeId]) -> Option<Self> {
(cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx).styled(cx))
(cx.snap_to_point && !cx.smart_guides).then(|| Self::collect(cx.doc, exclude, None).for_snap_to_point(cx))
}
/// Anchors and centres of the visible leaves under `roots` (the roots included) whose bounds
@@ -338,9 +338,9 @@ impl Targets {
return self.snap_point(p, tol);
}
let rects: Vec<Rect> = std::iter::once(Vec2::ZERO).chain(offsets.iter().copied()).map(|o| Rect::from_points(p + o, p + o)).collect();
let (adj, mut ov) = self.snap_rects(&rects, tol);
let (adj, mut ov, aligned) = self.align_rects(&rects, tol);
let out = p + adj;
if adj != Vec2::ZERO {
if aligned {
ov.extend(self.label(out, "align"));
}
(out, ov)
@@ -354,6 +354,13 @@ impl Targets {
/// Snap rectangles that move together (an artboard and its bleed): the edge or centre of any
/// of them nearest a target, per axis. Returns the shift and the guides.
pub fn snap_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec<Overlay>) {
let (d, ov, _) = self.align_rects(rects, tol);
(d, ov)
}
/// [`Self::snap_rects`], also saying whether an axis lined up (even with Alignment Guides
/// off, or already in line, when there is no line or shift to tell).
fn align_rects(&self, rects: &[Rect], tol: f64) -> (Vec2, Vec<Overlay>, bool) {
let nearest = |targets: &[(f64, Point, Kind)], along: fn(&Rect) -> [f64; 3]| {
rects
.iter()
@@ -378,7 +385,7 @@ impl Targets {
let (x0, x1) = (from.x.min(r.x0), from.x.max(r.x1));
ov.extend(self.line(Point::new(x0, y), Point::new(x1, y)));
}
(d, ov)
(d, ov, best_x.is_some() || best_y.is_some())
}
/// Snap a bounding-box resize. `a` is the scale [`crate::bbox::scale_for_drag`] gave for
@@ -646,7 +653,7 @@ mod tests {
assert!(
ov.iter().any(|o| matches!(o, Overlay::Line { .. })) && ov.iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align"))
);
// Alignment Guides off: the point still lines up, no line and no "align" label.
// Alignment Guides off: the point still lines up and says so, without the line.
let c = ToolContext { alignment_guides: false, ..cx(&d, &s, &p) };
let t = Targets::collect(&d, &[], None).styled(&c);
let (q, ov) = t.snap_point(Point::new(301.0, 199.0), 4.0);
@@ -655,6 +662,10 @@ mod tests {
assert!(ov.iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align")), "{ov:?}");
let (dv, ov) = t.snap_rect(Rect::new(2.0, 50.0, 52.0, 90.0), 4.0);
assert_eq!((dv.x, ov.len()), (-2.0, 0));
// A handle already in line moves nowhere, yet is still "align".
let (q, ov) = t.snap_point_with(Point::new(400.0, 200.0), &[], 4.0);
assert_eq!(q, Point::new(400.0, 200.0));
assert!(matches!(&ov[..], [Overlay::Label { text, .. }] if text == "align"), "{ov:?}");
// Anchor/Path Labels off: on the anchor, without saying so.
let c = ToolContext { anchor_path_labels: false, ..cx(&d, &s, &p) };
let t = Targets::collect(&d, &[], None).styled(&c);
+19
View File
@@ -380,6 +380,25 @@ mod tests {
assert_eq!(drag(true, 320.0), (320.0, None), "nothing within reach");
}
/// Preferences › Smart Guides (#394) reach a guide dragged out of a ruler: Snapping
/// Tolerance sets how far a target pulls it, Anchor/Path Labels and Measurement Labels what
/// shows by it.
#[test]
fn a_new_guide_follows_the_smart_guide_preferences() {
let (d, _) = doc_with_rect();
let (s, p) = (Selection::default(), paint());
let drag = |c: &ToolContext| {
let mut g = NewGuide::new(c, true, None);
let a = g.pointer(c, &ev(PointerKind::Drag, 497.0, 50.0, Mods::default()), true);
let Some(Action::Preview(_, p)) = a.last() else { panic!("{a:?}") };
(p["pos"].as_f64().unwrap(), g.overlays(c))
};
let (at, ov) = drag(&ToolContext { snapping_tolerance: 2.0, ..cx(&d, &s, &p) });
assert_eq!((at, ov.len()), (497.0, 1), "3 px off is beyond 2: only the readout {ov:?}");
let (at, ov) = drag(&ToolContext { anchor_path_labels: false, measurement_labels: false, ..cx(&d, &s, &p) });
assert_eq!((at, ov), (500.0, vec![]), "onto the artboard's edge, nothing said");
}
#[test]
fn a_new_guide_is_made_on_release_over_the_canvas_only() {
let (d, _) = doc_with_rect();
+2 -1
View File
@@ -282,7 +282,8 @@ impl Tool for SliceSelectionTool {
o.extend(Handle::ALL.iter().map(|h| Overlay::Anchor { p: h.pos(r), color: BLUE, filled: false, size: 7.0 }));
}
}
match &self.drag {
// Smart Guides › Measurement Labels: the size readouts while resizing or moving slices.
match self.drag.as_ref().filter(|_| cx.measurement_labels) {
Some(Drag::Resize { id, began: true, .. }) => {
if let Some(r) = cx.doc.slice_bounds(*id) {
o.push(Overlay::Measure {
+4 -4
View File
@@ -192,7 +192,7 @@ impl FreeTransformTool {
}
Op::Move => {
let v = move_delta(d.start, p, shift);
self.measure = Some((p, cx.offset_label(v.x, v.y)));
self.measure = cx.measurement_labels.then(|| (p, cx.offset_label(v.x, v.y)));
Affine::translate(v)
}
Op::Scale(h) => {
@@ -201,13 +201,13 @@ impl FreeTransformTool {
(a, self.guides) = t.snap_scale(&vectorcraft_doc::OrientedBox::aligned(d.rect), h, a, shift, m.alt, cx.snap_tol());
}
let nr = a.transform_rect_bbox(d.rect);
self.measure = Some((p, cx.size_label(nr.width(), nr.height())));
self.measure = cx.transform_tools_guides.then(|| (p, cx.size_label(nr.width(), nr.height())));
a
}
Op::Shear(h) => shear_for_side(d.rect, h, delta, m.alt),
Op::Rotate => {
let (a, deg) = rotate_for_drag(d.rect.center(), d.start, p, shift);
self.measure = Some((p, format!("{:.1}°", -deg)));
self.measure = cx.transform_tools_guides.then(|| (p, format!("{:.1}°", -deg)));
a
}
};
@@ -304,7 +304,7 @@ impl Tool for FreeTransformTool {
}
}
o.extend(self.guides.iter().cloned());
if let (Some((p, t)), true) = (&self.measure, cx.transform_tools_guides) {
if let Some((p, t)) = &self.measure {
o.push(Overlay::Measure { p: *p + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: t.clone() });
}
o
+2 -2
View File
@@ -1170,8 +1170,8 @@ point lands:
- `alignmentGuides` (on by default): off, no line is drawn along the edge or centre the art lines up with; the art
still snaps into line.
- `anchorPathLabels` (on by default): off, no "anchor", "center", "path" or "align" label.
- `measurementLabels` (on by default): off, no size or offset readout while drawing, moving, dragging a ruler guide or
drawing and resizing an artboard.
- `measurementLabels` (on by default): off, no size or offset readout while drawing, moving (Free Transform's move
too), dragging a ruler guide, drawing and resizing an artboard, or moving and resizing a slice.
- `transformToolsGuides` (on by default): off, no readout while scaling or rotating with the Selection tool, the
Free Transform tool or the Rotate, Scale, Shear and Reflect tools.
- `objectHighlighting` (on by default; desktop app): off, the Selection tools no longer outline the object under the