mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
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:
1 parent
14d7b3ddce
commit
42fced4725
5 files changed
+43
-12
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user