mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 14:41:52 +00:00
Merge pull request #7388 from dotCooCoo/fix/snap-glyph-parametric-slice
Smart Guides: Parametric Shape and Slice tools snap as the drawing tools do
This commit is contained in:
@@ -4,7 +4,9 @@
|
||||
//! dragging, ←/→ its second value or corner radius. A click selects a parametric shape and shows
|
||||
//! its handles (centre, size, rotation, corner radius and one of the kind's own), whose drags run
|
||||
//! `addon.pathtools.shape.set`; a double-click on the canvas places one with the last settings.
|
||||
//! Each gesture is one undo step.
|
||||
//! Each gesture is one undo step. A new shape's center (from a press or a double-click) and the
|
||||
//! point dragged out from it snap as a drawing tool's points do ([`DrawSnap`]), hovering before
|
||||
//! the press too.
|
||||
//!
|
||||
//! A parametric shape is an ordinary path that keeps its numbers in one entry of its object data
|
||||
//! ([`KEY`]) with a fingerprint of the outline they drew. Moved, turned or scaled by any tool it
|
||||
@@ -17,6 +19,7 @@ use vectorcraft_geom::{Affine, BezPath, PathData, Point, Vec2};
|
||||
use vectorcraft_pathops::addons::pathtools::shapes::fingerprint;
|
||||
pub use vectorcraft_pathops::addons::pathtools::shapes::{MAX_COUNT, MAX_SIZE, MIN_COUNT, MIN_SIZE, ParametricShape, ShapeKind};
|
||||
|
||||
use crate::guides::DrawSnap;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The tool's id.
|
||||
@@ -209,7 +212,7 @@ fn angle(c: Point, p: Point, shift: bool) -> f64 {
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
enum Drag {
|
||||
/// Drawing a shape out from its centre.
|
||||
Create { center: Point },
|
||||
Create { center: Point, from: Point },
|
||||
/// Dragging a handle of shape `id`, which was `shape` when the drag began at `from`.
|
||||
Edit { id: NodeId, shape: ParametricShape, grip: Grip, from: Point },
|
||||
}
|
||||
@@ -221,11 +224,18 @@ pub struct ParametricShapeTool {
|
||||
drag: Option<Drag>,
|
||||
/// Where the pointer last was during the drag, and the keys held.
|
||||
last: (Point, Mods),
|
||||
/// Smart Guides for a new shape's center and the point dragged out from it.
|
||||
snap: DrawSnap,
|
||||
}
|
||||
|
||||
impl Default for ParametricShapeTool {
|
||||
fn default() -> Self {
|
||||
Self { next: ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0), drag: None, last: (Point::ZERO, Mods::default()) }
|
||||
Self {
|
||||
next: ParametricShape::new(ShapeKind::Polygon, Point::ZERO, 100.0, 100.0),
|
||||
drag: None,
|
||||
last: (Point::ZERO, Mods::default()),
|
||||
snap: DrawSnap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +257,7 @@ impl ParametricShapeTool {
|
||||
fn shaped(&self, drag: &Drag, p: Point, mods: Mods) -> (&'static str, ParametricShape, Option<NodeId>) {
|
||||
let fit = |v: f64| v.clamp(MIN_SIZE, MAX_SIZE);
|
||||
match *drag {
|
||||
Drag::Create { center } => {
|
||||
Drag::Create { center, .. } => {
|
||||
let r = center.distance(p);
|
||||
let aspect = self.next.width / self.next.height;
|
||||
let s = ParametricShape {
|
||||
@@ -288,6 +298,18 @@ impl ParametricShapeTool {
|
||||
}
|
||||
Some(Action::Preview(cmd.into(), p))
|
||||
}
|
||||
|
||||
/// Show the Smart Guides of a new shape's center at `p`, the pointer hovering; none over a
|
||||
/// handle of the selected shape or over a parametric shape, where a press does not draw.
|
||||
fn hover(&mut self, cx: &ToolContext, p: Point) {
|
||||
let tol = cx.point_tol();
|
||||
let on_grip = selected(cx).is_some_and(|(_, s)| grips(cx, &s).iter().any(|(_, at)| at.distance(p) <= tol));
|
||||
if on_grip || shape_at(cx, p).is_some() {
|
||||
self.snap.clear();
|
||||
} else {
|
||||
self.snap.hover(cx, p, &[], None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Tool for ParametricShapeTool {
|
||||
@@ -300,6 +322,9 @@ impl Tool for ParametricShapeTool {
|
||||
}
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
if ev.kind == PointerKind::Move && self.drag.is_none() {
|
||||
self.hover(cx, ev.pos);
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Move => vec![],
|
||||
PointerKind::Down => {
|
||||
@@ -309,15 +334,17 @@ impl Tool for ParametricShapeTool {
|
||||
near.min_by(|a, b| a.1.distance(ev.pos).total_cmp(&b.1.distance(ev.pos))).map(|(g, _)| (id, s, g))
|
||||
});
|
||||
if let Some((id, shape, grip)) = grip {
|
||||
self.snap.clear();
|
||||
self.drag = Some(Drag::Edit { id, shape, grip, from: ev.pos });
|
||||
self.last = (ev.pos, ev.mods);
|
||||
return vec![Action::Begin(EDIT.into())];
|
||||
}
|
||||
if let Some(id) = shape_at(cx, ev.pos) {
|
||||
self.snap.clear();
|
||||
return if cx.selection.objects == [id] { vec![] } else { vec![Action::Exec("select.set".into(), json!({ "ids": [id.0] }))] };
|
||||
}
|
||||
let (center, _) = crate::guides::snap_draw(cx, ev.pos, &[]);
|
||||
self.drag = Some(Drag::Create { center });
|
||||
let center = self.snap.press(cx, ev.pos, &[], None);
|
||||
self.drag = Some(Drag::Create { center, from: ev.pos });
|
||||
self.last = (center, ev.mods);
|
||||
vec![Action::Begin(CREATE.into())]
|
||||
}
|
||||
@@ -325,15 +352,19 @@ impl Tool for ParametricShapeTool {
|
||||
if self.drag.is_none() {
|
||||
return vec![];
|
||||
}
|
||||
self.last = (ev.pos, ev.mods);
|
||||
// The point a new shape is dragged out to snaps; a handle's drag does not.
|
||||
let pos = if matches!(self.drag, Some(Drag::Create { .. })) { self.snap.drag(cx, ev.pos, None) } else { ev.pos };
|
||||
self.last = (pos, ev.mods);
|
||||
let Some(preview) = self.preview() else { return vec![] };
|
||||
if ev.kind == PointerKind::Drag {
|
||||
return vec![preview];
|
||||
}
|
||||
self.snap.clear();
|
||||
let Some(drag) = self.drag.take() else { return vec![] };
|
||||
match drag {
|
||||
// A click draws nothing (a double-click places a shape).
|
||||
Drag::Create { center } if center.distance(ev.pos) <= cx.tol(2.0) => vec![Action::Cancel],
|
||||
// A click draws nothing (a double-click places a shape), even when its center
|
||||
// snapped a few pixels from where it was pressed.
|
||||
Drag::Create { from, .. } if from.distance(ev.pos) <= cx.tol(2.0) => vec![Action::Cancel],
|
||||
Drag::Create { .. } => {
|
||||
if let Action::Preview(_, p) = &preview
|
||||
&& let Ok(s) = apply(self.next, p)
|
||||
@@ -350,7 +381,8 @@ impl Tool for ParametricShapeTool {
|
||||
if self.drag.is_some() || shape_at(cx, ev.pos).is_some() {
|
||||
return vec![];
|
||||
}
|
||||
let s = ParametricShape { center: ev.pos, ..self.next };
|
||||
let s = ParametricShape { center: self.snap.press(cx, ev.pos, &[], None), ..self.next };
|
||||
self.snap.clear();
|
||||
vec![Action::Exec("addon.pathtools.shape.create".into(), to_json(&s))]
|
||||
}
|
||||
}
|
||||
@@ -358,6 +390,7 @@ impl Tool for ParametricShapeTool {
|
||||
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
if key == ToolKey::Escape && self.drag.take().is_some() {
|
||||
self.snap.clear();
|
||||
return vec![Action::Cancel];
|
||||
}
|
||||
if !matches!(self.drag, Some(Drag::Create { .. })) {
|
||||
@@ -385,13 +418,16 @@ impl Tool for ParametricShapeTool {
|
||||
}
|
||||
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
if let Some(Drag::Create { center }) = &self.drag {
|
||||
let (_, s, _) = self.shaped(&Drag::Create { center: *center }, self.last.0, self.last.1);
|
||||
if let Some(drag @ Drag::Create { .. }) = &self.drag {
|
||||
let (_, s, _) = self.shaped(drag, self.last.0, self.last.1);
|
||||
let text = format!("{}\nAngle: {:.1}°", cx.size_label(s.width, s.height), s.rotation);
|
||||
return vec![Overlay::Measure { p: self.last.0 + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text }];
|
||||
let mut out = self.snap.guides().to_vec();
|
||||
out.push(Overlay::Measure { p: self.last.0 + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text });
|
||||
return out;
|
||||
}
|
||||
let Some((_, s)) = selected(cx) else { return vec![] };
|
||||
// The shape's box, then its handles.
|
||||
// The hovering pointer's Smart Guides, then the selected shape's box and handles.
|
||||
let mut out = self.snap.guides().to_vec();
|
||||
let Some((_, s)) = selected(cx) else { return out };
|
||||
let (rx, ry) = (s.width / 2.0, s.height / 2.0);
|
||||
let mut bp = BezPath::new();
|
||||
let f: Affine = s.frame();
|
||||
@@ -400,7 +436,7 @@ impl Tool for ParametricShapeTool {
|
||||
if i == 0 { bp.move_to(*p) } else { bp.line_to(*p) }
|
||||
}
|
||||
bp.close_path();
|
||||
let mut out = vec![Overlay::Path { path: bp, color: BLUE, width: 1.0, dashed: true }];
|
||||
out.push(Overlay::Path { path: bp, color: BLUE, width: 1.0, dashed: true });
|
||||
for (grip, p) in grips(cx, &s) {
|
||||
if grip == Grip::Rotate {
|
||||
out.push(Overlay::Line { a: f * Point::new(0.0, -ry), b: p, color: BLUE, dashed: false });
|
||||
@@ -418,6 +454,11 @@ impl Tool for ParametricShapeTool {
|
||||
Cursor::ParametricShape
|
||||
}
|
||||
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.snap.clear();
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// The next shape's numbers (its size and rotation are a double-click's).
|
||||
fn options(&self) -> Value {
|
||||
let mut v = to_json(&self.next);
|
||||
|
||||
@@ -714,6 +714,81 @@ fn pen_click_near_a_glyph_anchor_lands_on_it() {
|
||||
assert!(q.distance(anchor) > 1e-3, "{q:?} landed on the glyph anchor with Smart Guides off");
|
||||
}
|
||||
|
||||
/// Snap to Glyph: the Parametric Shape tool's center (a drag and a double-click) and the point
|
||||
/// dragged out from it, and the Slice tool's corners, land on a glyph's anchor point as a Pen
|
||||
/// click does, with the guides shown while hovering, and not with Snap to Glyph off.
|
||||
#[test]
|
||||
fn parametric_shape_and_slice_points_land_on_a_glyph_anchor() {
|
||||
use crate::addons::parametric_shape::ParametricShapeTool;
|
||||
use crate::slice::SliceTool;
|
||||
use crate::{Action, PointerEvent, PointerKind, Tool};
|
||||
let (d, t) = glyph_doc("H", 0.0);
|
||||
let lay = layout_text(FontDb::global(), &t);
|
||||
let g = lay.glyphs.iter().find(|g| !g.outline.elements().is_empty()).expect("a glyph with an outline");
|
||||
// An anchor inside the glyph's bounds, which no glyph bounds line passes through.
|
||||
let bb = g.outline.bounding_box();
|
||||
let inside = |p: Point| p.x > bb.x0 + 1.0 && p.x < bb.x1 - 1.0 && p.y > bb.y0 + 1.0 && p.y < bb.y1 - 1.0;
|
||||
let anchor = t.xf * g.outline.segments().map(|s| s.start()).find(|p| inside(*p)).expect("an anchor inside the glyph");
|
||||
let (s, p) = (Selection::default(), paint());
|
||||
let click = anchor + Vec2::new(1.5, -1.0);
|
||||
let ev = |kind, q: Point| PointerEvent::new(kind, q.x, q.y);
|
||||
let center = |v: &serde_json::Value| Point::new(v["center"][0].as_f64().unwrap(), v["center"][1].as_f64().unwrap());
|
||||
// A press away from the type, for drags that end at the click.
|
||||
let away = anchor + Vec2::new(-83.3, -61.7);
|
||||
for glyphs in [true, false] {
|
||||
let c = ToolContext { snap_to_glyph: glyphs, ..cx(&d, &s, &p) };
|
||||
let lands = |q: Point| q.distance(anchor) < 1e-9;
|
||||
// A Parametric Shape drawn out from the click.
|
||||
let mut tool = ParametricShapeTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Down, click));
|
||||
let acts = tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)));
|
||||
let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") };
|
||||
assert_eq!(lands(center(v)), glyphs, "drag, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
|
||||
// A Parametric Shape dragged out to the click is sized and turned as one dragged out to
|
||||
// the anchor itself.
|
||||
let drawn_to = |q: Point| {
|
||||
let mut tool = ParametricShapeTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Down, away));
|
||||
tool.pointer(&c, &ev(PointerKind::Drag, q))
|
||||
};
|
||||
assert_eq!(drawn_to(click) == drawn_to(anchor), glyphs, "dragged point, Snap to Glyph {glyphs}");
|
||||
// A click 3 px from the anchor draws nothing, though its center snaps onto the anchor.
|
||||
let near = anchor + Vec2::new(2.5, -1.5);
|
||||
let mut tool = ParametricShapeTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Down, near));
|
||||
let acts = tool.pointer(&c, &ev(PointerKind::Drag, near));
|
||||
let Some(Action::Preview(_, v)) = acts.first() else { panic!("{acts:?}") };
|
||||
assert_eq!(lands(center(v)), glyphs, "click, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
|
||||
assert_eq!(tool.pointer(&c, &ev(PointerKind::Up, near)), vec![Action::Cancel], "click, Snap to Glyph {glyphs}");
|
||||
// One placed with a double-click.
|
||||
let mut tool = ParametricShapeTool::default();
|
||||
let acts = tool.pointer(&c, &ev(PointerKind::DoubleClick, click));
|
||||
let Some(Action::Exec(_, v)) = acts.first() else { panic!("{acts:?}") };
|
||||
assert_eq!(lands(center(v)), glyphs, "double-click, Snap to Glyph {glyphs}: {:?} vs {anchor:?}", center(v));
|
||||
// The pointer hovering at the click shows the guides of the anchor it would land on.
|
||||
let mut tool = ParametricShapeTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Move, click));
|
||||
assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "hover, Snap to Glyph {glyphs}");
|
||||
// A slice dragged from the click down and to the right starts at its top-left corner.
|
||||
let mut tool = SliceTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Move, click));
|
||||
assert_eq!(!tool.overlays(&c).is_empty(), glyphs, "slice hover, Snap to Glyph {glyphs}");
|
||||
tool.pointer(&c, &ev(PointerKind::Down, click));
|
||||
let acts = tool.pointer(&c, &ev(PointerKind::Drag, click + Vec2::new(80.0, 60.0)));
|
||||
let Some(Action::Preview(_, v)) = acts.get(1) else { panic!("{acts:?}") };
|
||||
let corner = Point::new(v["x"].as_f64().unwrap(), v["y"].as_f64().unwrap());
|
||||
assert_eq!(lands(corner), glyphs, "slice, Snap to Glyph {glyphs}: {corner:?} vs {anchor:?}");
|
||||
// One dragged from up and to the left ends at its bottom-right corner.
|
||||
let mut tool = SliceTool::default();
|
||||
tool.pointer(&c, &ev(PointerKind::Down, away));
|
||||
let acts = tool.pointer(&c, &ev(PointerKind::Drag, click));
|
||||
let Some(Action::Preview(_, v)) = acts.get(1) else { panic!("{acts:?}") };
|
||||
let f = |k: &str| v[k].as_f64().unwrap();
|
||||
let end = Point::new(f("x") + f("width"), f("y") + f("height"));
|
||||
assert_eq!(end.distance(anchor) < 1e-6, glyphs, "slice end, Snap to Glyph {glyphs}: {end:?} vs {anchor:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Snap to Glyph: a single line of type has an x-height guide; a paragraph shows glyph bounds only.
|
||||
#[test]
|
||||
fn drawn_points_line_up_with_the_x_height_of_a_single_line() {
|
||||
|
||||
+44
-13
@@ -14,7 +14,7 @@ use vectorcraft_doc::{NodeId, OrientedBox, SliceSource};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
use crate::bbox::{Handle, hit_handle, move_delta, scale_for_drag};
|
||||
use crate::guides::{PointSnap, Targets};
|
||||
use crate::guides::{DrawSnap, PointSnap, Targets};
|
||||
use crate::shape::drag_rect;
|
||||
use crate::xform::{BLUE, polygon, rect_corners};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids};
|
||||
@@ -37,9 +37,26 @@ fn has_area(r: Rect) -> bool {
|
||||
r.width() > 1e-6 && r.height() > 1e-6
|
||||
}
|
||||
|
||||
/// Snap `p` as the drawing tools do, then to the nearest ruler guide within reach (guides win).
|
||||
fn snap(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
let (mut q, ov) = crate::guides::snap_draw(cx, p, &[]);
|
||||
/// When the Slice tool snaps a point.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Phase {
|
||||
/// The pointer hovers before a press.
|
||||
Hover,
|
||||
/// The first corner is pressed: the targets are gathered afresh.
|
||||
Press,
|
||||
/// The other corner is dragged: the press's targets are kept.
|
||||
Drag,
|
||||
}
|
||||
|
||||
/// Snap `p` as the drawing tools do ([`DrawSnap`]), then to the nearest ruler guide within reach
|
||||
/// (guides win, and the Smart Guides are then not shown).
|
||||
fn snap(draw: &mut DrawSnap, cx: &ToolContext, p: Point, phase: Phase) -> (Point, Vec<Overlay>) {
|
||||
let mut q = match phase {
|
||||
Phase::Hover => draw.hover(cx, p, &[], None),
|
||||
Phase::Press => draw.press(cx, p, &[], None),
|
||||
Phase::Drag => draw.drag(cx, p, None),
|
||||
};
|
||||
let mut ov = draw.guides().to_vec();
|
||||
let tol = cx.tol(5.0);
|
||||
let guides = cx.doc.ruler_guides();
|
||||
let nearest = |vertical: bool, v: f64| {
|
||||
@@ -50,9 +67,11 @@ fn snap(cx: &ToolContext, p: Point) -> (Point, Vec<Overlay>) {
|
||||
};
|
||||
if let Some(g) = nearest(true, p.x) {
|
||||
q.x = g.pos;
|
||||
ov.clear();
|
||||
}
|
||||
if let Some(g) = nearest(false, p.y) {
|
||||
q.y = g.pos;
|
||||
ov.clear();
|
||||
}
|
||||
(q, ov)
|
||||
}
|
||||
@@ -64,6 +83,8 @@ pub struct SliceTool {
|
||||
last: Point,
|
||||
began: bool,
|
||||
guides: Vec<Overlay>,
|
||||
/// Smart Guides for the slice's corners, and for the pointer hovering before a press.
|
||||
snap: DrawSnap,
|
||||
}
|
||||
|
||||
impl Tool for SliceTool {
|
||||
@@ -77,14 +98,20 @@ impl Tool for SliceTool {
|
||||
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
if self.start.is_none() {
|
||||
self.guides = snap(&mut self.snap, cx, ev.pos, Phase::Hover).1;
|
||||
}
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Down => {
|
||||
let (p, _) = snap(cx, ev.pos);
|
||||
(self.start, self.last, self.began) = (Some(p), p, false);
|
||||
let (p, guides) = snap(&mut self.snap, cx, ev.pos, Phase::Press);
|
||||
(self.start, self.last, self.began, self.guides) = (Some(p), p, false, guides);
|
||||
vec![]
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let Some(start) = self.start else { return vec![] };
|
||||
let (p, guides) = snap(cx, ev.pos);
|
||||
let (p, guides) = snap(&mut self.snap, cx, ev.pos, Phase::Drag);
|
||||
(self.last, self.guides) = (p, guides);
|
||||
let mut out = vec![];
|
||||
if !self.began {
|
||||
@@ -102,6 +129,7 @@ impl Tool for SliceTool {
|
||||
}
|
||||
PointerKind::Up => {
|
||||
self.guides.clear();
|
||||
self.snap.clear();
|
||||
let began = std::mem::take(&mut self.began);
|
||||
if self.start.take().is_some() && began { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
@@ -123,6 +151,8 @@ impl Tool for SliceTool {
|
||||
}
|
||||
o
|
||||
}
|
||||
// The hovering pointer's Smart Guides.
|
||||
None => self.guides.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
@@ -134,6 +164,7 @@ impl Tool for SliceTool {
|
||||
fn deactivate(&mut self, _cx: &ToolContext) -> Vec<Action> {
|
||||
self.start = None;
|
||||
self.guides.clear();
|
||||
self.snap.clear();
|
||||
if std::mem::take(&mut self.began) { vec![Action::Cancel] } else { vec![] }
|
||||
}
|
||||
}
|
||||
@@ -152,8 +183,8 @@ impl Drag {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a user slice's handle snaps to, gathered when it is pressed: what the Slice tool's drag
|
||||
/// snaps to ([`snap`]), the art through [`PointSnap`] and the ruler guides on their own.
|
||||
/// What a user slice's handle snaps to, gathered when it is pressed: the art's bounds through
|
||||
/// [`PointSnap`], as a bounding-box handle snaps, and the ruler guides on their own.
|
||||
struct ResizeSnap {
|
||||
art: PointSnap,
|
||||
rulers: Targets,
|
||||
@@ -164,10 +195,10 @@ impl ResizeSnap {
|
||||
Self { art: PointSnap::scaling(cx, || Targets::collect(cx.doc, &[], None)), rulers: Targets::rulers(cx) }
|
||||
}
|
||||
|
||||
/// Snap the resize `a` ([`scale_for_drag`]) of the slice `rect` by `handle` as [`snap`] snaps
|
||||
/// a drawn slice's corner: the handle snaps as a bounding-box handle does
|
||||
/// ([`PointSnap::snap_scale`]), then a ruler guide within 5 px of where that leaves it takes
|
||||
/// what the handle moves on its axis. Returns the scale and the guides showing why.
|
||||
/// Snap the resize `a` ([`scale_for_drag`]) of the slice `rect` by `handle`: the handle snaps
|
||||
/// as a bounding-box handle does ([`PointSnap::snap_scale`]), then a ruler guide within 5 px of
|
||||
/// where that leaves it takes what the handle moves on its axis, as [`snap`] does for a drawn
|
||||
/// slice's corner. Returns the scale and the guides showing why.
|
||||
fn scale(&self, cx: &ToolContext, rect: Rect, handle: Handle, a: Affine, m: Mods) -> (Affine, Vec<Overlay>) {
|
||||
let bx = OrientedBox::aligned(rect);
|
||||
let (a, ov) = self.art.snap_scale(cx, &bx, handle, a, m.shift, m.alt);
|
||||
|
||||
+4
-3
@@ -7380,8 +7380,8 @@ restore the selection that was cleared. While the slices are locked or hidden, a
|
||||
```
|
||||
|
||||
The Slice tool (`slice`, Shift+K) and the Slice Selection tool (`sliceSelection`) reduce to commands:
|
||||
`object.slice.create {x, y, width, height}` (the Slice tool's drag; it snaps to ruler guides), `object.slice.select
|
||||
{slices?, toggle?}`, `object.slice.move {slices?, dx, dy}` (an object slice moves its object),
|
||||
`object.slice.create {x, y, width, height}` (the Slice tool's drag; it snaps as drawn points do, then to ruler guides),
|
||||
`object.slice.select {slices?, toggle?}`, `object.slice.move {slices?, dx, dy}` (an object slice moves its object),
|
||||
`object.slice.setRect {id, x, y, width, height}` (user slices) and `object.slice.delete {slices?}`. Slice ids are user
|
||||
slice ids and the ids of objects with an object slice. `object.slice.select` with `slices` left out or null deselects
|
||||
the slices.
|
||||
@@ -10726,7 +10726,8 @@ Tools:
|
||||
centre (move), the box's lower right corner (size; Shift keeps the proportions), a handle above the top (rotation;
|
||||
Shift: 45° steps), a dot on the top edge (corner radius) and one of the kind's own (a star's or ring's inner radius, a
|
||||
gear's tooth height, an arrow's head, a cross's bar, a heart's notch, a bubble's tail tip); each drag runs
|
||||
`addon.pathtools.shape.set`. A double-click on the canvas places a shape with the last settings. Options
|
||||
`addon.pathtools.shape.set`. A double-click on the canvas places a shape with the last settings. A new shape's
|
||||
center and the point dragged out from it snap as drawn points do. Options
|
||||
(`tool.setOption`, or the panel with no shape selected) are the next shape's numbers, as the commands take them.
|
||||
- **Corner Tool** (`cornerTool`, or the UI command `addon.pathtools.cornerTool`, which also shows the Anchor Inspector
|
||||
panel with its Corners section): edits the app's Live Corners on any path, selected or not. Over an anchor where two
|
||||
|
||||
@@ -1192,8 +1192,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Distance Guides: with Smart Guides on (View › Smart Guides and Preferences › Smart Guides › Invoke Distance Guides, on by default), something selected and the Selection tool, holding Alt/Option over another object shows the distances between the selection's bounds and that object's: on an axis on which they lie apart, one line across the gap between their facing edges; on one on which they overlap, the offsets of their left and right (top and bottom) edges, each line running across the middle of where they overlap, with a dashed line out to the other object's edge when it passes beside it. Over an empty part of an artboard it shows the selection's distances from the artboard's left, right, top and bottom edges. Each line has ticks at its ends and its length in a pill of the smart guide colour, in the General unit; letting Alt go, pressing (a pan or zoom press too), moving off them, leaving the canvas or a floating panel, menu or dialog over the pointer takes them away, and with the pointer still they follow what lies under it as the view scrolls, zooms or turns, as the document, the tab or the selection changes, and when the Selection tool is switched to. Behaviour from the public [Use Distance Guides for accurate placement](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-distance-guides-for-accurate-placement.html) and [Smart Guides options](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/smart-guides-options.html) pages, which describe hovering only: nothing shows while moving or nudging. Tests: `select::tests::alt_over_*`, `select::tests::distance_guides_need_alt_a_selection_and_smart_guides`, `tests_prefs::invoke_distance_guides_measures_the_selection_to_the_object_under_alt`, `canvas::tests::alt_over_another_object_draws_the_distance_guide_label`, `canvas::tests::distance_guides_go_when_the_hover_ends`, `canvas::tests::distance_guides_follow_what_lies_under_a_still_pointer`.
|
||||
A move pressed on a path's stroke, or within 6 px of its outline away from its anchors and center, is held by the nearest point of that path, with the Selection, Group Selection or Direct Selection tool, and a straight segment Direct Selection drags is held by its own nearest point. That point is what snaps, so the path lands exactly on a ruler guide (with "align") instead of the pressed spot landing there and the path a few pixels beside it, an offset that showed only zoomed far in (#3701). A press in the middle of a fill still snaps the pressed spot: the public [Align objects with guides](https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/align-graphic-objects-with-guides.html) page has Snap to Point align the point under the pointer, which is placed exactly where the object should meet the guide.
|
||||
A bounding-box handle dragged with the Selection or Free Transform tool snaps what it moves as a dragged point snaps (#2054). With View › Snap to Grid on, it snaps to the nearest gridline, and with Snap to Pixel on, it snaps to whole pixels. With Smart Guides off and Snap to Point on, it snaps to the other objects' anchors and the ruler guides within the Snap to Point distance. With Smart Guides and Snap to Point both on, it lines up with the ruler guides as well as the other objects. Shift still keeps the proportions, and Alt still scales about the center.
|
||||
A user slice's edge or corner handle dragged with the Slice Selection tool snaps as the Slice tool's drag does. With View › Snap to Grid on, it snaps to the nearest gridline, and with Snap to Pixel on, it snaps to whole pixels. Otherwise, with Smart Guides on, it lands on or lines up with the anchors, centers and edges of the art and the artboards, and with Smart Guides off and Snap to Point on, it snaps to the art's anchors and the ruler guides within the Snap to Point distance. Then a ruler guide within 5 px, shown and unlocked, moves the side or corner onto it. Shift still keeps the proportions, and Alt still resizes about the center.
|
||||
View › Snap to Glyph: with Smart Guides on, a point drawn near a single line of live type lands on its glyphs' anchor points and lines up with its baseline, its x-height and the edges of each character's advance width (proximity guides), as well as the glyph bounds: each glyph's box gives edge guides and four corners a point lands on, even where its outline curves (the right of `B`), and the box follows the glyph's size, scaling and rotation (#5112). In rotated type these guides run at the type's angle, and type of one glyph also has angular guides along its slanted straight segments. A paragraph shows glyph bounds only.
|
||||
A user slice's edge or corner handle dragged with the Slice Selection tool snaps as a bounding-box handle does. With View › Snap to Grid on, it snaps to the nearest gridline, and with Snap to Pixel on, it snaps to whole pixels. Otherwise, with Smart Guides on, it lands on or lines up with the anchors, centers and edges of the art and the artboards, and with Smart Guides off and Snap to Point on, it snaps to the art's anchors and the ruler guides within the Snap to Point distance. Then a ruler guide within 5 px, shown and unlocked, moves the side or corner onto it. Shift still keeps the proportions, and Alt still resizes about the center.
|
||||
View › Snap to Glyph: with Smart Guides on, a point drawn near a single line of live type lands on its glyphs' anchor points and lines up with its baseline, its x-height and the edges of each character's advance width (proximity guides), as well as the glyph bounds: each glyph's box gives edge guides and four corners a point lands on, even where its outline curves (the right of `B`), and the box follows the glyph's size, scaling and rotation (#5112). In rotated type these guides run at the type's angle, and type of one glyph also has angular guides along its slanted straight segments. A paragraph shows glyph bounds only. A Parametric Shape's center (drawn with a drag or placed with a double-click) and the point dragged out from it, and the corners of a slice drawn with the Slice tool, snap as other drawn points do: to these guides, and to the anchors and segments of paths. Tested in `crates/tools/src/guides/tests.rs`.
|
||||
Snap to Glyph options (#4232, [Snap to Glyph options](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/snap-to-glyph-options.html)): the Character panel menu's Show Snap to Glyph Options (Hide Snap to Glyph Options) shows a Snap to Glyph section in the panel, with or without type selected, with a checkbox for each kind of guide: Baseline, x-height, Glyph Bounds, Proximity Guides (the edges of each character's width), Angular Guides and Anchor Point. Each off takes its guides away and leaves the others (`view.snapToGlyph.options`, the `glyphSnap` preference group, all on by default). Angular Guides covers a glyph's slanted strokes and every guide of rotated type, which runs at its angle; the boxes are dimmed while View › Snap to Glyph is off, and the section starts with a Snap to Glyph checkbox for it (VectorCraft choices; the page settles neither). Showing the section scrolls the panel to it, since it comes after every character attribute (#5273). Tested in `crates/tools/src/guides/tests.rs`, `crates/ui-egui/src/panels/character/tests.rs` and `crates/engine/src/tests/prefs.rs`.
|
||||
- **Artboard click-to-fit (#1227):** a plain click fits the topmost editable object or group to a new artboard, including its stroke like Fit to Selected Art, and makes the new artboard active. Art and its selection stay unchanged; creation is one undo step. Handles, modified clicks, drags, Escape and tool changes retain their meaning. Tested inside an artboard and on the pasteboard (`crates/engine/tests/it/artboard_click.rs`); the public [CS6 Printing Guide](https://helpx.adobe.com/content/dam/help/en/indesign/pdf/CS6_PrintGuide.pdf) documents the object-click gesture. This legacy gesture adds evidence without regrading the 2026 canvas controls.
|
||||
- **Rotated views:** with the view rotated, Fit Artboard in Window, Fit All in Window and the status bar's artboard navigator fit the artboard or the art as the view turns it, and a Zoom tool marquee (Animated Zoom or GPU Performance off) zooms to the area dragged across. Ruler guides on no artboard turn with the view. The canvas draws the selection marquee where the tools select, Show Hidden Characters draws the marks of type partly inside the canvas, the Contextual Task Bar stays under the selection, and the Navigator's red box outlines what the window shows.
|
||||
|
||||
Reference in New Issue
Block a user