mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 09:33:36 +00:00
Shape tools: the selection moves from its centre point and turns outside a corner
A shape just drawn keeps its bounding box with the Rectangle, Rounded Rectangle, Ellipse, Polygon and Star tools, but its centre point (Attributes › Show Center) did nothing and the box's handles only drew: a press anywhere started a new shape, and with the Selection tool a press on the centre of an unfilled shape started a marquee. - The Selection tool now takes a press on a selected object's centre point as the start of a move (held by that point, so it snaps and Alt copies), as Direct Selection has since #4891. A click there still makes the object the key object of a multi-selection. - The shape tools take it too, and take a press in the rotate zone just outside a bounding-box corner (the Selection tool's own zone and cursor) as the start of a rotation about the box's centre. Live Corners widgets keep the corners themselves, so a press on a corner still rounds it; every other press still draws a new shape. Escape cancels, as for the Selection tool's drags. `hit_center` moves from direct.rs to select.rs, shared by the three tools.
This commit is contained in:
committed by
bflatastic
parent
9c879301b4
commit
adbcb3a224
@@ -94,6 +94,7 @@ id = "Move"
|
||||
group = "Selection"
|
||||
shown_when = [
|
||||
"The Selection tool over a selected object, or over any object with Alt held (once an Alt-drag starts, SelectionCopy shows).",
|
||||
"The shape tools over a selected object's centre point (Attributes › Show Center), and while dragging it.",
|
||||
"Reshape over a path or while dragging.",
|
||||
"Inside the Free Transform and Crop Image boxes.",
|
||||
"Puppet Warp over a pin.",
|
||||
@@ -376,7 +377,9 @@ look = "An arc with an arrowhead at each end. RotateToward is the same arc, turn
|
||||
[[cursor]]
|
||||
id = "RotateToward"
|
||||
group = "Transform and resize"
|
||||
shown_when = ["Selection and Free Transform in the rotate zone just outside a bounding-box corner, and while rotating there. The Rotate, Gradient and Puppet Warp rotate cursors don't turn."]
|
||||
shown_when = [
|
||||
"Selection, Free Transform and the shape tools (Rectangle, Rounded Rectangle, Ellipse, Polygon, Star) in the rotate zone just outside a bounding-box corner, and while rotating there. The Rotate, Gradient and Puppet Warp rotate cursors don't turn.",
|
||||
]
|
||||
hotspot = "Below the arc, in its empty middle, as for Rotate."
|
||||
look = "The Rotate arc drawn for the top-right corner (direction 1). VectorCraft turns it in 45° steps so it wraps whichever corner the pointer is at."
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ use crate::guides::{HandleSnap, Leave, PointSnap, Targets};
|
||||
use crate::meshedit::{MeshEdit, grid_of};
|
||||
use crate::pathtype::{self, BracketDrag, near_bracket};
|
||||
use crate::rulerguide::GuideEdit;
|
||||
use crate::select::{MoveSnap, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at};
|
||||
use crate::select::{MoveSnap, hit_center, hold_point, is_area_type, is_path_type, matrix_json, release_guide_at};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -530,16 +530,6 @@ fn nearest_anchor(pd: &PathData, p: Point, tol: f64) -> Option<(usize, usize, Po
|
||||
pd.anchors().map(|(si, ai, a)| (si, ai, a.p)).filter(|(.., a)| a.distance(p) <= tol).min_by(|x, y| x.2.distance(p).total_cmp(&y.2.distance(p)))
|
||||
}
|
||||
|
||||
/// The selected object whose centre point (Attributes panel › Show Center), drawn while it is
|
||||
/// selected, is within `tol` of `p`: (object, where the point is).
|
||||
fn hit_center(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Point)> {
|
||||
let (symbols, editable) = (cx.doc.symbol_boxes(), cx.doc.editable());
|
||||
cx.selection.objects.iter().copied().filter(|id| editable(*id) && in_scope(cx.doc, cx.isolation, *id)).find_map(|id| {
|
||||
let c = cx.doc.node(id).filter(|n| n.shows_center())?.geometric_bounds_with(&symbols)?.center();
|
||||
(c.distance(p) <= tol).then_some((id, c))
|
||||
})
|
||||
}
|
||||
|
||||
/// An edge of an area type frame under `p`, topmost first: (text, the edge's two anchors).
|
||||
fn hit_frame_edge(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Vec<AnchorRef>)> {
|
||||
anchor_owners(cx, is_area_type).into_iter().find_map(|id| {
|
||||
|
||||
@@ -348,6 +348,28 @@ pub(crate) mod testutil {
|
||||
(d, id)
|
||||
}
|
||||
|
||||
/// A live 100 × 100 rectangle at (100, 100)–(200, 200) with no paint: its centre point
|
||||
/// (Attributes › Show Center) is at (150, 150), and a press in the middle of it hits no art.
|
||||
pub fn doc_with_live_rect() -> (Document, NodeId) {
|
||||
use vectorcraft_doc::{LiveShape, NodeKind};
|
||||
let shape = LiveShape::Rectangle {
|
||||
w: 100.0,
|
||||
h: 100.0,
|
||||
radii: [0.0; 4],
|
||||
kinds: Default::default(),
|
||||
radii_relative: false,
|
||||
xf: vectorcraft_geom::Affine::translate((100.0, 100.0)),
|
||||
};
|
||||
let mut d = Document::new(500.0, 500.0);
|
||||
let (l, id) = (d.layers[0].id, d.alloc_id());
|
||||
let mut n = Node::path(id, shape.to_path(), Appearance::default());
|
||||
if let NodeKind::Path { live: slot, .. } = &mut n.kind {
|
||||
*slot = Some(Box::new(shape));
|
||||
}
|
||||
d.insert(Some(l), 0, n).unwrap();
|
||||
(d, id)
|
||||
}
|
||||
|
||||
/// [`doc_with_rect`] plus a clip group: a filled 200 × 200 square (350, 350)–(550, 550)
|
||||
/// clipped to an unpainted rectangle (400, 400)–(500, 500), so the art lies over the mask's
|
||||
/// edges. The group, the clipping path and the art.
|
||||
|
||||
@@ -469,10 +469,23 @@ pub fn selection_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
/// [`selection_box`] while the canvas shows it (View › Show Bounding Box, and no anchors selected
|
||||
/// on their own): its handles take a press only then. A path the Pen is drawing keeps its end
|
||||
/// anchor selected, so Cmd/Ctrl lending the Selection tool doesn't scale it by hidden handles (#4980).
|
||||
fn shown_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
/// The shape tools press it too (#5477).
|
||||
pub(crate) fn shown_box(cx: &ToolContext) -> Option<OrientedBox> {
|
||||
(cx.show_bbox && cx.selection.anchors.is_empty()).then(|| selection_box(cx)).flatten()
|
||||
}
|
||||
|
||||
/// The selected object whose centre point (Attributes panel › Show Center), drawn while it is
|
||||
/// selected, is within `tol` of `p`: (object, where the point is). A press there grabs the object
|
||||
/// by that point — it moves the selection even where no art is painted (an unfilled shape) — as
|
||||
/// Direct Selection's does (#4891).
|
||||
pub(crate) fn hit_center(cx: &ToolContext, p: Point, tol: f64) -> Option<(NodeId, Point)> {
|
||||
let (symbols, editable) = (cx.doc.symbol_boxes(), cx.doc.editable());
|
||||
cx.selection.objects.iter().copied().filter(|id| editable(*id) && crate::direct::in_scope(cx.doc, cx.isolation, *id)).find_map(|id| {
|
||||
let c = cx.doc.node(id).filter(|n| n.shows_center())?.geometric_bounds_with(&symbols)?.center();
|
||||
(c.distance(p) <= tol).then_some((id, c))
|
||||
})
|
||||
}
|
||||
|
||||
/// The bounds of object `id`, measured as [`selection_bounds`] measures the selection.
|
||||
fn object_bounds(cx: &ToolContext, id: NodeId) -> Option<Rect> {
|
||||
with_object_bounds(cx, |bounds| bounds(id))
|
||||
@@ -822,6 +835,18 @@ impl Tool for SelectionTool {
|
||||
self.state = State::Idle;
|
||||
return out;
|
||||
}
|
||||
// 2b. A selected object's centre point (Attributes › Show Center), drawn while it
|
||||
// is selected: a drag from it moves the selection, also where no art is painted
|
||||
// (an unfilled shape), which a press there would otherwise take for a marquee. A
|
||||
// click there is a click on that object, so it makes it the key object too.
|
||||
if !m.shift
|
||||
&& !m.cmd
|
||||
&& let Some((id, center)) = hit_center(cx, p, cx.point_tol())
|
||||
{
|
||||
let key = ((cx.selection.objects.len() > 1 || !cx.selection.guides.is_empty()) && !m.alt).then_some(id);
|
||||
self.state = State::Moving { start: p, press: p, hold: center, began: false, deselect: None, key, exclude: None };
|
||||
return vec![];
|
||||
}
|
||||
// 3. Objects: Cmd/Ctrl-click selects the one under the selected one (cycling).
|
||||
if m.cmd
|
||||
&& cx.select_behind
|
||||
@@ -1071,6 +1096,11 @@ impl Tool for SelectionTool {
|
||||
if near_selected && pie::over_widget(cx, p) {
|
||||
return Cursor::ArrowHollow;
|
||||
}
|
||||
// A selected object's centre point (Show Center): a press there moves it, where the art
|
||||
// may not reach.
|
||||
if near_selected && hit_center(cx, p, cx.point_tol()).is_some() {
|
||||
return Cursor::Move;
|
||||
}
|
||||
if over_rotate_all(cx, p) {
|
||||
return Cursor::Rotate;
|
||||
}
|
||||
|
||||
@@ -2139,3 +2139,33 @@ fn a_double_click_on_a_symbol_instance_edits_its_symbol() {
|
||||
assert_eq!(double(&d, Some(l), 300.0, 300.0), vec![]);
|
||||
assert_eq!(double(&d, Some(l), 450.0, 50.0), vec![Action::Exec("object.exitIsolation".into(), json!({}))]);
|
||||
}
|
||||
|
||||
/// #4891 for the Selection tool: a press on a selected object's centre point (Attributes › Show
|
||||
/// Center) moves the selection, also where no art is painted (an unfilled shape), which the press
|
||||
/// would otherwise take for a marquee; anywhere else a press still starts one.
|
||||
#[test]
|
||||
fn a_press_on_the_centre_point_moves_an_unfilled_shape() {
|
||||
let (d, id) = doc_with_live_rect();
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let centre = Point::new(150.0, 150.0);
|
||||
let mut t = SelectionTool::default();
|
||||
assert_eq!(t.cursor(&c, centre, Mods::default()), Cursor::Move);
|
||||
assert!(t.pointer(&c, &ev(PointerKind::Down, centre.x, centre.y)).is_empty());
|
||||
let a = t.pointer(&c, &ev(PointerKind::Drag, 170.0, 160.0));
|
||||
assert_eq!(a.first(), Some(&Action::Begin("Move".into())), "{a:?}");
|
||||
assert!(
|
||||
matches!(a.last(), Some(Action::Preview(cmd, v)) if cmd == "object.transform" && v["matrix"] == json!([1.0, 0.0, 0.0, 1.0, 20.0, 10.0])),
|
||||
"{a:?}"
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &ev(PointerKind::Up, 170.0, 160.0)), vec![Action::Commit]);
|
||||
// Empty canvas away from it: a marquee, as before.
|
||||
let mut t = SelectionTool::default();
|
||||
assert_eq!(t.cursor(&c, Point::new(400.0, 400.0), Mods::default()), Cursor::Arrow);
|
||||
t.pointer(&c, &ev(PointerKind::Down, 400.0, 400.0));
|
||||
let a = t.pointer(&c, &ev(PointerKind::Drag, 450.0, 450.0));
|
||||
assert!(a.is_empty(), "{a:?}");
|
||||
assert!(matches!(t.overlays(&c).first(), Some(Overlay::Marquee(_))), "a marquee");
|
||||
}
|
||||
|
||||
+175
-11
@@ -10,15 +10,19 @@
|
||||
//! The selection keeps its bounding box and its Live Shape widgets on show with the Rectangle,
|
||||
//! Rounded Rectangle, Ellipse, Polygon and Star tools, so a shape just drawn can be adjusted at
|
||||
//! once: a drag on a live rectangle's or polygon's corner widget rounds its corners and a drag on
|
||||
//! a live ellipse's pie widget sets its pie angle, as with the Selection tool; a press anywhere
|
||||
//! else draws a new shape (#5477).
|
||||
//! a live ellipse's pie widget sets its pie angle, as with the Selection tool; a press on the
|
||||
//! selection's centre point (Show Center) moves it, and one just outside a corner of its box turns
|
||||
//! it about that centre, as the Selection tool's rotate zone does; a press anywhere else draws a
|
||||
//! new shape (#5477).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||||
|
||||
use crate::bbox::{in_rotate_zone, move_delta, rotate_for_drag};
|
||||
use crate::corners::{self, CornerDrag};
|
||||
use crate::guides::{DrawSnap, Leave, square};
|
||||
use crate::pie::{self, PieDrag};
|
||||
use crate::select::{MoveSnap, hit_center, matrix_json, shown_box};
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
|
||||
|
||||
/// The largest corner radius the Rounded Rectangle tool draws, points: the range of General ›
|
||||
@@ -26,6 +30,10 @@ use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, Tool
|
||||
/// value stop there too.
|
||||
pub const MAX_CORNER_RADIUS: f64 = 1296.0;
|
||||
|
||||
/// How far (screen points) outside a corner of the selection's box a press with a shape tool turns
|
||||
/// the selection, as the Selection tool's rotate zone does.
|
||||
const ROTATE_ZONE: f64 = 18.0;
|
||||
|
||||
/// Does tool `id` keep the selection's bounding box and Live Shape widgets on show and let its
|
||||
/// corner and pie widgets be dragged (#5477)? The shape tools that draw a box: not Line Segment.
|
||||
pub fn shows_shape_widgets(id: &str) -> bool {
|
||||
@@ -38,6 +46,16 @@ enum WidgetDrag {
|
||||
Pie(PieDrag),
|
||||
}
|
||||
|
||||
/// A drag of the selection itself, taken by a shape tool: the centre point moves it and the zone
|
||||
/// just outside a corner of its box turns it, as the Selection tool's drags do (#5477).
|
||||
enum SelectionDrag {
|
||||
/// Moving the selection by the centre point `hold`: `start` is where the press was, and the
|
||||
/// move has begun once the pointer left the drag threshold.
|
||||
Move { start: Point, hold: Point, began: bool, snap: Box<MoveSnap> },
|
||||
/// Turning the selection about `center` from where the press was.
|
||||
Rotate { center: Point, start: Point },
|
||||
}
|
||||
|
||||
pub struct ShapeTool {
|
||||
id: &'static str,
|
||||
start: Option<Point>,
|
||||
@@ -59,6 +77,12 @@ pub struct ShapeTool {
|
||||
snap: DrawSnap,
|
||||
/// A widget of the selection is being dragged instead of a shape drawn.
|
||||
widget: Option<WidgetDrag>,
|
||||
/// The selection is being moved or turned instead of a shape drawn.
|
||||
selection: Option<SelectionDrag>,
|
||||
/// The Smart Guides a move of the selection shows.
|
||||
guides: Vec<Overlay>,
|
||||
/// The measurement label of a move or a turn: where it goes and what it reads.
|
||||
measure: Option<(Point, String)>,
|
||||
}
|
||||
|
||||
/// The inner-to-outer radius ratio that keeps a star of `points` points' shoulders straight (each
|
||||
@@ -95,6 +119,9 @@ impl ShapeTool {
|
||||
line_angle: 0.0,
|
||||
fill_line: false,
|
||||
widget: None,
|
||||
selection: None,
|
||||
guides: vec![],
|
||||
measure: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +139,101 @@ impl ShapeTool {
|
||||
shows_shape_widgets(self.id) && (corners::over_widget(cx, p, false) || pie::over_widget(cx, p))
|
||||
}
|
||||
|
||||
/// The selection's box, while this tool shows it (the tools that draw a box, with a selection
|
||||
/// and no anchors picked on their own).
|
||||
fn box_of(&self, cx: &ToolContext) -> Option<vectorcraft_doc::OrientedBox> {
|
||||
shows_shape_widgets(self.id).then(|| shown_box(cx)).flatten()
|
||||
}
|
||||
|
||||
/// Is `p` just outside a corner of the selection's box, where a press turns the selection about
|
||||
/// its centre ([`ROTATE_ZONE`])? On the corner itself the Live Corners widget rounds it (#5477).
|
||||
fn over_rotate(&self, cx: &ToolContext, p: Point) -> Option<Point> {
|
||||
let bx = self.box_of(cx)?;
|
||||
in_rotate_zone(bx.rect, bx.to_local(p), cx.tol(5.0), cx.tol(ROTATE_ZONE)).map(|_| bx.center())
|
||||
}
|
||||
|
||||
/// Is `p` over the centre point of a selected object (Attributes › Show Center), where a press
|
||||
/// moves the selection? The point shows whatever View › Show Bounding Box says, so the press
|
||||
/// takes it too.
|
||||
fn over_center(&self, cx: &ToolContext, p: Point) -> bool {
|
||||
shows_shape_widgets(self.id) && hit_center(cx, p, cx.point_tol()).is_some()
|
||||
}
|
||||
|
||||
/// A press on the selection's centre point or just outside a corner of its box: the drag moves
|
||||
/// or turns the selection instead of drawing a new shape (#5477). A turn begins with the press,
|
||||
/// as the Selection tool's does; a move begins once the pointer leaves the drag threshold.
|
||||
fn press_selection(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
|
||||
if !shows_shape_widgets(self.id) {
|
||||
return None;
|
||||
}
|
||||
self.selection = match hit_center(cx, ev.pos, cx.point_tol()) {
|
||||
Some((_, hold)) => Some(SelectionDrag::Move { start: ev.pos, hold, began: false, snap: Box::new(MoveSnap::new(cx)) }),
|
||||
None => self.over_rotate(cx, ev.pos).map(|center| SelectionDrag::Rotate { center, start: ev.pos }),
|
||||
};
|
||||
self.selection.as_ref()?;
|
||||
self.snap.clear();
|
||||
self.guides.clear();
|
||||
self.measure = None;
|
||||
Some(match self.selection {
|
||||
Some(SelectionDrag::Rotate { .. }) => vec![Action::Begin("Rotate".into())],
|
||||
_ => vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// The pointer and keys while the selection is moved or turned: `None` when it is not.
|
||||
fn selection_pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
|
||||
let d = self.selection.as_mut()?;
|
||||
let out = match (ev.kind, d) {
|
||||
(PointerKind::Drag, SelectionDrag::Move { start, hold, began, snap }) => {
|
||||
let mut out = vec![];
|
||||
if !*began {
|
||||
if ev.pos.distance(*start) < cx.tol(3.0) {
|
||||
return Some(out);
|
||||
}
|
||||
*began = true;
|
||||
out.push(Action::Begin(if ev.mods.alt { "Copy".into() } else { "Move".into() }));
|
||||
}
|
||||
let shift = ev.mods.shift;
|
||||
let (delta, guides) = if shift {
|
||||
snap.snap_along(cx, *hold, move_delta(*start, ev.pos, true, cx.constrain_angle), ev.mods.alt)
|
||||
} else {
|
||||
snap.snap(cx, *hold, ev.pos - *start, ev.mods.alt)
|
||||
};
|
||||
self.guides = guides;
|
||||
self.measure = cx.measurement_labels.then(|| (ev.pos, cx.offset_label(delta.x, delta.y)));
|
||||
out.push(Action::Preview("object.transform".into(), json!({"matrix": matrix_json(Affine::translate(delta)), "copy": ev.mods.alt})));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, SelectionDrag::Rotate { center, start }) => {
|
||||
let (a, deg) = rotate_for_drag(*center, *start, ev.pos, ev.mods.shift);
|
||||
self.measure = cx.transform_tools_guides.then(|| (ev.pos, format!("{:.1}°", -deg)));
|
||||
vec![Action::Preview("object.transform".into(), json!({"matrix": matrix_json(a), "copy": false}))]
|
||||
}
|
||||
(PointerKind::Up, SelectionDrag::Move { began, .. }) => {
|
||||
let began = *began;
|
||||
self.selection = None;
|
||||
self.guides.clear();
|
||||
self.measure = None;
|
||||
if began { vec![Action::Commit] } else { vec![] }
|
||||
}
|
||||
(PointerKind::Up, SelectionDrag::Rotate { .. }) => {
|
||||
self.selection = None;
|
||||
self.measure = None;
|
||||
vec![Action::Commit]
|
||||
}
|
||||
_ => vec![],
|
||||
};
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Escape during a drag of the selection turns it back, as the Selection tool's does.
|
||||
fn cancel_selection(&mut self) -> Vec<Action> {
|
||||
self.selection = None;
|
||||
self.guides.clear();
|
||||
self.measure = None;
|
||||
vec![Action::Cancel]
|
||||
}
|
||||
|
||||
/// The pointer and keys while a widget is dragged: `None` when none is.
|
||||
fn widget_pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option<Vec<Action>> {
|
||||
let w = self.widget.as_mut()?;
|
||||
@@ -268,16 +390,19 @@ impl Tool for ShapeTool {
|
||||
self.id
|
||||
}
|
||||
fn busy(&self) -> bool {
|
||||
self.start.is_some() || self.widget.is_some()
|
||||
self.start.is_some() || self.widget.is_some() || self.selection.is_some()
|
||||
}
|
||||
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
|
||||
if let Some(out) = self.widget_pointer(cx, ev) {
|
||||
return out;
|
||||
}
|
||||
if let Some(out) = self.selection_pointer(cx, ev) {
|
||||
return out;
|
||||
}
|
||||
match ev.kind {
|
||||
PointerKind::Move => {
|
||||
// Over a widget a press would drag, no Smart Guide for a new shape's start.
|
||||
if self.over_widget(cx, ev.pos) {
|
||||
if self.over_widget(cx, ev.pos) || self.over_center(cx, ev.pos) || self.over_rotate(cx, ev.pos).is_some() {
|
||||
self.snap.clear();
|
||||
} else {
|
||||
self.snap.hover(cx, ev.pos, &[], None);
|
||||
@@ -289,6 +414,9 @@ impl Tool for ShapeTool {
|
||||
self.snap.clear();
|
||||
return vec![];
|
||||
}
|
||||
if let Some(out) = self.press_selection(cx, ev) {
|
||||
return out;
|
||||
}
|
||||
let p = self.snap.press(cx, ev.pos, &[], None);
|
||||
self.start = Some(p);
|
||||
self.last = ev.pos;
|
||||
@@ -340,6 +468,10 @@ impl Tool for ShapeTool {
|
||||
}
|
||||
}
|
||||
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
|
||||
// A drag of the selection: Escape turns it back, as with the Selection tool.
|
||||
if self.selection.is_some() {
|
||||
return if key == ToolKey::Escape { self.cancel_selection() } else { vec![] };
|
||||
}
|
||||
// A widget drag: Up and Down step a corner's kind; Escape cancels it, as with the
|
||||
// Selection tool.
|
||||
if let Some(w) = &mut self.widget {
|
||||
@@ -399,11 +531,19 @@ impl Tool for ShapeTool {
|
||||
match &self.widget {
|
||||
Some(WidgetDrag::Corner(c)) => c.adjusting(),
|
||||
Some(WidgetDrag::Pie(d)) => d.adjusting(),
|
||||
None => false,
|
||||
None => match &self.selection {
|
||||
Some(SelectionDrag::Move { began, .. }) => *began,
|
||||
Some(SelectionDrag::Rotate { .. }) => true,
|
||||
None => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
|
||||
matches!(self.widget, Some(WidgetDrag::Corner(_))) && matches!(key, ToolKey::Up | ToolKey::Down)
|
||||
match (&self.widget, &self.selection) {
|
||||
(Some(WidgetDrag::Corner(_)), _) => matches!(key, ToolKey::Up | ToolKey::Down),
|
||||
(_, Some(_)) => key == ToolKey::Escape,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match &self.widget {
|
||||
@@ -412,6 +552,10 @@ impl Tool for ShapeTool {
|
||||
None => {}
|
||||
}
|
||||
let mut o = self.snap.guides().to_vec();
|
||||
o.extend(self.guides.iter().cloned());
|
||||
if let Some((p, text)) = &self.measure {
|
||||
o.push(Overlay::Measure { p: *p, text: text.clone() });
|
||||
}
|
||||
if let Some(s) = self.start.filter(|_| self.began && cx.measurement_labels) {
|
||||
let p = self.last + Vec2::new(cx.tol(12.0), cx.tol(12.0));
|
||||
let text = if self.id == "lineSegment" {
|
||||
@@ -446,12 +590,32 @@ impl Tool for ShapeTool {
|
||||
// Over a widget a press would drag (or dragging one), the Selection tool's pointer for it.
|
||||
let idle = self.start.is_none();
|
||||
match &self.widget {
|
||||
Some(WidgetDrag::Corner(_)) => Cursor::CornerRadius,
|
||||
Some(WidgetDrag::Pie(_)) => Cursor::ArrowHollow,
|
||||
None if idle && shows_shape_widgets(self.id) && corners::over_widget(cx, p, false) => Cursor::CornerRadius,
|
||||
None if idle && self.over_widget(cx, p) => Cursor::ArrowHollow,
|
||||
None => Cursor::Crosshair,
|
||||
Some(WidgetDrag::Corner(_)) => return Cursor::CornerRadius,
|
||||
Some(WidgetDrag::Pie(_)) => return Cursor::ArrowHollow,
|
||||
None => {}
|
||||
}
|
||||
match &self.selection {
|
||||
Some(SelectionDrag::Rotate { center, .. }) => return Cursor::rotate_about_in_view(*center, p, cx.view_angle()),
|
||||
Some(_) => return Cursor::Arrow,
|
||||
None => {}
|
||||
}
|
||||
// The selection's centre point moves it and the zone just outside a corner turns it;
|
||||
// everywhere else a press draws a new shape.
|
||||
if idle {
|
||||
if self.over_center(cx, p) {
|
||||
return Cursor::Move;
|
||||
}
|
||||
if let Some(center) = self.over_rotate(cx, p) {
|
||||
return Cursor::rotate_about_in_view(center, p, cx.view_angle());
|
||||
}
|
||||
if shows_shape_widgets(self.id) && corners::over_widget(cx, p, false) {
|
||||
return Cursor::CornerRadius;
|
||||
}
|
||||
if self.over_widget(cx, p) {
|
||||
return Cursor::ArrowHollow;
|
||||
}
|
||||
}
|
||||
Cursor::Crosshair
|
||||
}
|
||||
fn options(&self) -> Value {
|
||||
json!({
|
||||
|
||||
@@ -423,3 +423,71 @@ fn a_shape_tool_drags_the_selected_shapes_widgets() {
|
||||
assert!(matches!(a.last(), Some(Action::Preview(cmd, v)) if cmd == "object.setLiveShape" && v.get("pieEnd").is_some()), "{a:?}");
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 200.0, 20.0)), vec![Action::Commit]);
|
||||
}
|
||||
|
||||
/// A selected live 100 × 100 rectangle at (100, 100)–(200, 200) with no paint: its centre point
|
||||
/// (Attributes › Show Center) is at (150, 150) and its corners at the box's.
|
||||
fn unfilled_live_rect() -> (vectorcraft_doc::Document, Selection) {
|
||||
let (d, id) = doc_with_live_rect();
|
||||
let mut s = Selection::default();
|
||||
s.add(id);
|
||||
(d, s)
|
||||
}
|
||||
|
||||
/// #5477: with a shape tool the selection's centre point moves it and the zone just outside a
|
||||
/// corner of its box turns it, as the Selection tool's do; anywhere else a press draws a new
|
||||
/// shape. The centre is where no art is painted, so without this a press there would draw one.
|
||||
#[test]
|
||||
fn a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner() {
|
||||
let (d, s) = unfilled_live_rect();
|
||||
let p = paint();
|
||||
let c = cx(&d, &s, &p);
|
||||
let centre = Point::new(150.0, 150.0);
|
||||
for id in ["rectangle", "roundedRectangle", "polygon", "star", "ellipse"] {
|
||||
let mut t = ShapeTool::new(id);
|
||||
assert_eq!(t.cursor(&c, centre, Mods::default()), Cursor::Move, "{id}");
|
||||
// No Smart Guide for a new shape's start over the centre point.
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Move, centre.x, centre.y));
|
||||
assert!(t.overlays(&c).is_empty(), "{id}");
|
||||
assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, centre.x, centre.y)).is_empty(), "{id}");
|
||||
assert!(t.busy(), "{id}");
|
||||
// A drag moves the selection by the pointer's offset.
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 160.0));
|
||||
assert_eq!(a.first(), Some(&Action::Begin("Move".into())), "{id}: {a:?}");
|
||||
assert!(t.transforming(), "{id}");
|
||||
assert_eq!(
|
||||
a.last(),
|
||||
Some(&Action::Preview("object.transform".into(), json!({"matrix": [1.0, 0.0, 0.0, 1.0, 20.0, 10.0], "copy": false}))),
|
||||
"{id}"
|
||||
);
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 170.0, 160.0)), vec![Action::Commit], "{id}");
|
||||
assert!(!t.busy() && !t.transforming(), "{id}");
|
||||
}
|
||||
// View › Hide Bounding Box hides the box, not the centre point: a press there still moves.
|
||||
let hidden = ToolContext { show_bbox: false, ..cx(&d, &s, &p) };
|
||||
let mut t = ShapeTool::new("rectangle");
|
||||
assert_eq!(t.cursor(&hidden, centre, Mods::default()), Cursor::Move);
|
||||
t.pointer(&hidden, &PointerEvent::new(PointerKind::Down, centre.x, centre.y));
|
||||
let a = t.pointer(&hidden, &PointerEvent::new(PointerKind::Drag, 170.0, 160.0));
|
||||
assert_eq!(a.first(), Some(&Action::Begin("Move".into())), "{a:?}");
|
||||
// Just outside the bottom-right corner (7.07 pt away, past the corner widget's 5): a press
|
||||
// there turns the selection about its centre.
|
||||
let outside = Point::new(205.0, 205.0);
|
||||
let mut t = ShapeTool::new("rectangle");
|
||||
assert_eq!(t.cursor(&c, outside, Mods::default()), Cursor::rotate_about(centre, outside));
|
||||
// A turn begins with the press, as the Selection tool's does (its drags preview into it).
|
||||
assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, outside.x, outside.y)), vec![Action::Begin("Rotate".into())]);
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 150.0, 205.0));
|
||||
let (want, _) = rotate_for_drag(centre, outside, Point::new(150.0, 205.0), false);
|
||||
assert_eq!(a.last(), Some(&Action::Preview("object.transform".into(), json!({"matrix": matrix_json(want), "copy": false}))), "{a:?}");
|
||||
assert!(t.transforming());
|
||||
// Escape turns it back.
|
||||
assert_eq!(t.key(&c, ToolKey::Escape, Mods::default()), vec![Action::Cancel]);
|
||||
assert!(!t.busy());
|
||||
// Away from the centre point and the corners, the tool draws a new shape.
|
||||
let away = Point::new(300.0, 300.0);
|
||||
let mut t = ShapeTool::new("rectangle");
|
||||
assert_eq!(t.cursor(&c, away, Mods::default()), Cursor::Crosshair);
|
||||
t.pointer(&c, &PointerEvent::new(PointerKind::Down, away.x, away.y));
|
||||
let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 350.0, 340.0));
|
||||
assert_eq!(a.first(), Some(&Action::Begin("Rectangle".into())), "{a:?}");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Parity with Adobe Illustrator
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Direct Selection: with Snap to Grid, a dragged straight segment lands its anchors on the grid, #3835; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -1721,6 +1721,7 @@ Edit › Copy and Cut put the selected ruler guides on the clipboard, with the s
|
||||
| 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. |
|
||||
| 2026-11-05 | minor | Compound Shapes add-on (beyond Illustrator): Object › Plug-ins › Compound Shapes lists Make, Release and Expand Compound Shape, and holds a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons make a compound shape on a click and give the permanent result on Alt-click (the default is the reverse). Selected members of a compound shape always take the clicked mode. The setting is `window.pathfinder.compoundShapes`; no existing behavior changes while it is off. Scores and estimates unchanged. |
|
||||
| 2026-11-01 | minor | Batch Trace Folder (`imageTrace.batchFolder`, #4921); evidence added; scores unchanged. |
|
||||
| 2026-11-06 | minor | Shape tools: a shape just drawn can be moved and turned with its tool still active — a press on the selection's centre point (Attributes › Show Center) moves the selection with the Selection tool's move arrow, snapping and Alt copy, and a press in the rotate zone just outside a bounding-box corner turns it about the centre with its rotate cursor, as the Selection tool's do (#5477); the Selection tool takes the centre point too, so a press there moves an unfilled shape instead of starting a marquee; evidence: `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` (`crates/tools/src/shape/tests.rs`) and `a_press_on_the_centre_point_moves_an_unfilled_shape` (`select/tests.rs`); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar (each field's scrub label was a second tab stop, invisible because only the label is drawn there, and the W/H link between W and H in the Control bar was one too, and now comes after the fields); evidence: `one_tab_moves_from_a_field_to_the_next` and `the_control_bar_link_toggles_by_click` in `crates/ui-egui/src/panels/tests_constrain.rs`; scores and estimates unchanged |
|
||||
| 2026-11-01 | minor | Copy and paste ruler guides (#3226); evidence added; scores unchanged. |
|
||||
| 2026-11-04 | minor | Area type vertical alignment buttons in the Properties panel's Paragraph section and the Control panel, off for columns, rows and threaded frames (#3759); evidence added; scores unchanged. |
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# UI and interaction parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; Transform fields: one Tab per field, the W/H link after them; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; shape tools: a press on the selection's centre point moves it and one in the rotate zone outside a box corner turns it, #5477; Transform fields: one Tab per field; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged)
|
||||
|
||||
## Feature submission evidence
|
||||
|
||||
@@ -45,6 +45,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard
|
||||
| 2026-11-06 | minor | Drop Shadow, Outer Glow and Inner Glow: spinner arrows beside Opacity, the offsets, Blur and Darkness (#5367, #5368); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Rename (`ui.rename`, no default key) opens the Layers panel's name field on the selected layer, group or object's row, as double-clicking its name does; a key can be assigned in Keyboard Shortcuts (#5324, a VectorCraft addition); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Properties panel in Edit Artboards (Shift+O): a Transform section with the shared reference point, X, Y, W and H, the preset and orientation, and Move and Scale Artwork with Artboard, above the Artboards section with the name, Artboard Options and Fill (#5219); scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Shape tools: a press on the selection's centre point (Show Center) moves the selection, also where no art is painted, and a press in the rotate zone just outside a bounding-box corner turns it about the centre, both as the Selection tool's do (#5477); the Selection tool takes its centre point too, so an unfilled shape moves instead of starting a marquee; scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Transform fields: one Tab moves from one numeric field to the next in the Transform panel, the Properties panel's Transform section and the Control bar — a field's scrub label was a second, invisible tab stop, and the W/H link between W and H is now made after the fields, so it stays a tab stop after them; scores and estimates unchanged |
|
||||
| 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged |
|
||||
| 2026-11-05 | minor | Direct Selection: Shift-click picks several points of a blend's spine and dragging one moves them all, with the key objects on them, in one undo step (#5285); scores and estimates unchanged |
|
||||
@@ -192,7 +193,7 @@ tools, never checked side by side) isn't captured row by row.
|
||||
| Live Corners widgets (radius readout, Alt-click cycles kind, double-click the Corners dialog) | done | a Direct Selection click on a widget selects its corner (#1239); a press-and-drag on a widget of a path selected whole still rounds every corner (#908), and adding widgets to the selection by Shift-click (#1232) is missing; the bounding box hides while a widget is dragged with the Selection tool (#1229); a round corner next to a curved side curves smoothly instead of sticking out of the shape (#1512); a corner rounds until its cut reaches its neighbours' cuts, so one corner of a square alone reaches the whole side (#2332) |
|
||||
| Corners between curved sides (#1144), Relative rounding | done | #1144 closed; Relative rounding in the Corners dialog (Relative / Absolute, radius as a percentage of each corner's maximum, kept as the shape is resized; `object.setLiveShape` `relativeRadii`, #3091, `tests_livecorners::relative_corner_rounding_keeps_fraction_on_resize`) |
|
||||
| Pie widgets on a live ellipse: drag one to set Pie Start or End Angle, double-click one to make the ellipse whole | done | Selection tool, with the bounding box showing |
|
||||
| A shape just drawn shows its bounding box and Live Shape widgets with the shape tool still active | done | #5477: Rectangle, Rounded Rectangle, Ellipse, Polygon and Star keep the selection's bounding box, corner widgets and pie widgets on show while they draw nothing; a drag on a corner or pie widget adjusts it (arrows step the corner kind, Escape cancels) and a press anywhere else draws. Public descriptions: [Live Shapes](https://helpx.adobe.com/illustrator/using/live-shapes.html) (live shapes adjust without switching tools) and [CreativePro, Working with Live Shapes](https://creativepro.com/?p=23921) (a widget shows in every corner on release; drag one toward the centre to round). The bounding-box handles show but scale only with the Selection tool, so a press on a corner still starts a new shape there; double-clicking a widget opens nothing with a shape tool. Test `a_shape_tool_drags_the_selected_shapes_widgets` |
|
||||
| A shape just drawn shows its bounding box and Live Shape widgets with the shape tool still active, and the selection can be moved and turned without switching tools | done | #5477: Rectangle, Rounded Rectangle, Ellipse, Polygon and Star keep the selection's bounding box, corner widgets and pie widgets on show while they draw nothing; a drag on a corner or pie widget adjusts it (arrows step the corner kind, Escape cancels), a press on the selection's centre point (Attributes › Show Center) moves the selection — the Selection tool's move arrow, snapping and Alt copy — a press just outside a corner of the box turns it about the centre (the Selection tool's rotate zone and cursor), and a press anywhere else draws. Public descriptions: [Live Shapes](https://helpx.adobe.com/illustrator/using/live-shapes.html) (live shapes adjust without switching tools) and [CreativePro, Working with Live Shapes](https://creativepro.com/?p=23921) (a widget shows in every corner on release; drag one toward the centre to round). The bounding-box handles show but scale only with the Selection tool, so a press on a corner still starts a new shape there; double-clicking a widget opens nothing with a shape tool. Tests `a_shape_tool_drags_the_selected_shapes_widgets` and `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` |
|
||||
| Isolation mode by double-click (groups and single objects), breadcrumbs, Esc to exit | done | Single paths, compound paths, images, meshes and placed files isolate too ([helpx: Isolate objects](https://helpx.adobe.com/africa/illustrator/desktop/manage-objects/select-objects/isolate-objects.html)); locking or hiding the layer (or a group) around the isolated object afterwards keeps clicks and marquees off it, as outside isolation mode (#5309) |
|
||||
| "Magnetic" moves of selected anchors with a modifier (#955 request) | missing | not a feature of the reference app as documented; under discussion |
|
||||
| Coinciding anchors of overlapping objects: the selected object's anchor is taken, also under another object | done | #1508 |
|
||||
|
||||
Reference in New Issue
Block a user