diff --git a/crates/tools/src/shape.rs b/crates/tools/src/shape.rs index 163117085..1e6396647 100644 --- a/crates/tools/src/shape.rs +++ b/crates/tools/src/shape.rs @@ -11,19 +11,20 @@ //! 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 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). +//! a live ellipse's pie widget sets its pie angle, as with the Selection tool; a drag on one of the +//! box's eight handles scales the selection, 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 handles, centre point and rotate zone do; a press anywhere else draws a new shape (#5477). use serde_json::{Value, json}; +use vectorcraft_doc::OrientedBox; use vectorcraft_geom::{Affine, Point, Rect, Vec2}; -use crate::bbox::{move_delta, rotate_for_drag}; +use crate::bbox::{Handle, hit_handle, move_delta, rotate_for_drag, scale_for_drag}; use crate::corners::{self, CornerDrag}; -use crate::guides::{DrawSnap, Leave, square}; +use crate::guides::{DrawSnap, Leave, PointSnap, Targets, square}; use crate::pie::{self, PieDrag}; -use crate::select::{MoveSnap, hit_center, matrix_json, rotate_zone, shown_box}; +use crate::select::{MoveSnap, handle_cursor, hit_center, is_area_type, matrix_json, rotate_zone, 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 › @@ -43,9 +44,15 @@ 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). +/// A drag of the selection itself, taken by a shape tool: a handle of its box scales it, 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 { + /// Scaling the selection by `handle` of its box `bx`, as it was when the press came. `areas`: + /// area type is selected, and its frame resizes while its type keeps its size. `snap` holds + /// what the moved handle snaps to. `last`: where the last drag was, and whether Backtick was + /// down then. + Scale { handle: Handle, bx: OrientedBox, areas: bool, snap: Box, last: Option<(Point, bool)> }, /// 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 }, @@ -178,6 +185,13 @@ impl ShapeTool { rotate_zone(cx, &self.box_of(cx)?, p) } + /// The handle of the selection's box under `p`, with the box: a press there scales the + /// selection, as with the Selection tool, whose handles take the same 5 pt square. + fn over_handle(&self, cx: &ToolContext, p: Point) -> Option<(Handle, OrientedBox)> { + let bx = self.box_of(cx)?; + hit_handle(bx.rect, bx.to_local(p), cx.tol(5.0)).map(|h| (h, bx)) + } + /// 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. @@ -185,22 +199,35 @@ impl ShapeTool { 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. + /// A press on a handle of the selection's box, on its centre point or just outside a corner of + /// its box: the drag scales, moves or turns the selection instead of drawing a new shape + /// (#5477). The handles come first, as with the Selection tool. A scale or a turn begins with + /// the press, as the Selection tool's do; a move begins once the pointer leaves the drag + /// threshold. fn press_selection(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Option> { 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 = if let Some((handle, bx)) = self.over_handle(cx, ev.pos) { + let is_area = |id: &vectorcraft_doc::NodeId| cx.doc.node(*id).is_some_and(is_area_type); + let areas = cx.selection.objects.iter().any(is_area); + Some(SelectionDrag::Scale { handle, bx, areas, snap: Box::new(PointSnap::new(cx, || Targets::for_scale(cx))), last: None }) + } else { + 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 { + // Only area type: the step resizes type areas; anything else scales. + Some(SelectionDrag::Scale { areas, .. }) => { + let only_areas = areas && cx.selection.objects.iter().all(|id| cx.doc.node(*id).is_some_and(is_area_type)); + vec![Action::Begin(if only_areas { "Resize Type Area" } else { "Scale" }.into())] + } Some(SelectionDrag::Rotate { .. }) => vec![Action::Begin("Rotate".into())], _ => vec![], }) @@ -235,6 +262,32 @@ impl ShapeTool { 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::Drag, SelectionDrag::Scale { handle, bx, areas, snap, last }) => { + // As the Selection tool scales: in the box's own frame, the moved handle snapped, + // and with Use Preview Bounds by the visual box's size unless the drag flips it. + *last = Some((ev.pos, ev.backquote)); + let m = ev.mods; + let a = scale_for_drag(bx.rect, *handle, bx.to_local(ev.pos), m.shift, m.alt); + let (a, guides) = snap.snap_scale(cx, bx, *handle, a, m.shift, m.alt); + self.guides = guides; + let nr = a.transform_rect_bbox(bx.rect); + self.measure = cx.transform_tools_guides.then(|| (ev.pos, cx.size_label(nr.width(), nr.height()))); + let [sx, _, _, sy, _, _] = a.as_coeffs(); + let (cmd, mut params) = if !ev.backquote && cx.preview_bounds && sx > 0.0 && sy > 0.0 { + let reference = if m.alt { 4 } else { handle.opposite().reference() }; + ("object.setBounds", json!({ "width": nr.width(), "height": nr.height(), "reference": reference, "copy": false })) + } else { + ("object.transform", json!({ "matrix": matrix_json(bx.conjugate(a)), "copy": false })) + }; + if *areas { + params["typeAreas"] = json!(true); + } + if ev.backquote { + params["objects"] = json!(false); + params["patterns"] = json!(true); + } + vec![Action::Preview(cmd.into(), params)] + } (PointerKind::Up, SelectionDrag::Move { began, .. }) => { let began = *began; self.selection = None; @@ -243,8 +296,22 @@ impl ShapeTool { self.snapped = false; if began { vec![Action::Commit] } else { vec![] } } + (PointerKind::Up, SelectionDrag::Scale { last, .. }) => { + // Backtick pressed or let go after the last drag: the scale is previewed again at + // that drag's position with the key as it is at the release, as the Selection tool + // does. + let again = last.filter(|(_, backquote)| *backquote != ev.backquote).map(|(pos, _)| pos); + let mut out = + again.and_then(|pos| self.selection_pointer(cx, &PointerEvent { kind: PointerKind::Drag, pos, ..*ev })).unwrap_or_default(); + self.selection = None; + self.guides.clear(); + self.measure = None; + out.push(Action::Commit); + out + } (PointerKind::Up, SelectionDrag::Rotate { .. }) => { self.selection = None; + self.guides.clear(); self.measure = None; vec![Action::Commit] } @@ -422,8 +489,9 @@ impl Tool for ShapeTool { fn busy(&self) -> bool { self.start.is_some() || self.widget.is_some() || self.selection.is_some() } - /// A widget or selection drag takes the pointer; otherwise a press draws, unless it lands on - /// the selection's centre point or in the rotate zone just outside its box's corner. + /// A widget or selection drag takes the pointer; otherwise a press draws, unless it lands on a + /// handle of the selection's box, its centre point or the rotate zone just outside its box's + /// corner. fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { if let Some(out) = self.widget_pointer(cx, ev) { return out; @@ -434,7 +502,11 @@ impl Tool for ShapeTool { 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) || self.over_center(cx, ev.pos) || self.over_rotate(cx, ev.pos).is_some() { + if self.over_widget(cx, ev.pos) + || self.over_handle(cx, ev.pos).is_some() + || 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); @@ -566,6 +638,11 @@ impl Tool for ShapeTool { vec![] } } + /// A drag of a handle of the selection's box: the canvas keeps drawing the box at the changing + /// bounds, as for the Selection tool. + fn resizing(&self) -> bool { + matches!(self.selection, Some(SelectionDrag::Scale { .. })) + } /// Adjusting a widget (the canvas hides the bounding box meanwhile, as for the Selection tool). fn transforming(&self) -> bool { match &self.widget { @@ -573,7 +650,7 @@ impl Tool for ShapeTool { Some(WidgetDrag::Pie(d)) => d.adjusting(), None => match &self.selection { Some(SelectionDrag::Move { began, .. }) => *began, - Some(SelectionDrag::Rotate { .. }) => true, + Some(SelectionDrag::Rotate { .. } | SelectionDrag::Scale { .. }) => true, None => false, }, } @@ -630,9 +707,9 @@ impl Tool for ShapeTool { } o } - /// The Selection tool's pointer over a Live Shape widget, the selection's centre point or the - /// rotate zone just outside its box's corner; the crosshair everywhere else, where a press - /// draws. + /// The Selection tool's pointer over a Live Shape widget, a handle of the selection's box, its + /// centre point or the rotate zone just outside its box's corner; the crosshair everywhere + /// else, where a press draws. fn cursor(&self, cx: &ToolContext, p: Point, m: Mods) -> Cursor { // Over a widget a press would drag (or dragging one), the Selection tool's pointer for it. let idle = self.start.is_none(); @@ -645,13 +722,14 @@ impl Tool for ShapeTool { // move snaps, as the Selection tool's. match &self.selection { Some(SelectionDrag::Rotate { center, .. }) => return Cursor::rotate_about_in_view(*center, p, cx.view_angle()), + Some(SelectionDrag::Scale { handle, bx, .. }) => return handle_cursor(*handle, bx.angle - cx.view_angle()), Some(SelectionDrag::Move { began: true, .. }) if m.alt => return Cursor::SelectionCopy, Some(SelectionDrag::Move { began: true, .. }) => return if self.snapped { Cursor::ArrowHollow } else { Cursor::Arrow }, Some(_) => return Cursor::Arrow, None => {} } - // A press takes the corner and pie widgets first, then the centre point, then the zone - // just outside a corner; everywhere else it draws a new shape. + // A press takes the corner and pie widgets first, then the box's handles, then the centre + // point, then the zone just outside a corner; everywhere else it draws a new shape. if idle { if shows_shape_widgets(self.id) && corners::over_widget(cx, p, false) { return Cursor::CornerRadius; @@ -659,6 +737,9 @@ impl Tool for ShapeTool { if self.over_widget(cx, p) { return Cursor::ArrowHollow; } + if let Some((handle, bx)) = self.over_handle(cx, p) { + return handle_cursor(handle, bx.angle - cx.view_angle()); + } if self.over_center(cx, p) { return Cursor::Move; } diff --git a/crates/tools/src/shape/tests.rs b/crates/tools/src/shape/tests.rs index 580f15c16..27654f965 100644 --- a/crates/tools/src/shape/tests.rs +++ b/crates/tools/src/shape/tests.rs @@ -583,6 +583,62 @@ fn a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corn assert_eq!(a.first(), Some(&Action::Begin("Rectangle".into())), "{a:?}"); } +/// With a shape tool, a drag on one of the eight handles of the selection's bounding box scales the +/// selection, as with the Selection tool, instead of drawing a new shape from the handle. +#[test] +fn a_shape_tool_scales_the_selection_by_its_bounding_box_handles() { + let (d, s) = unfilled_live_rect(); + let p = paint(); + let c = cx(&d, &s, &p); + let corner = Point::new(200.0, 200.0); + for id in ["rectangle", "roundedRectangle", "polygon", "star", "ellipse"] { + let mut t = ShapeTool::new(id); + assert_eq!(t.cursor(&c, corner, Mods::default()), handle_cursor(Handle::BottomRight, 0.0), "{id}"); + // No Smart Guide for a new shape's start over a handle. + t.pointer(&c, &PointerEvent::new(PointerKind::Move, corner.x, corner.y)); + assert!(t.overlays(&c).is_empty(), "{id}"); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, corner.x, corner.y)), vec![Action::Begin("Scale".into())], "{id}"); + // A resize: the canvas keeps the box on show at the changing bounds. + assert!(t.busy() && t.transforming() && t.resizing(), "{id}"); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 220.0, 230.0)); + let want = scale_for_drag(Rect::new(100.0, 100.0, 200.0, 200.0), Handle::BottomRight, Point::new(220.0, 230.0), false, false); + assert_eq!(a, vec![Action::Preview("object.transform".into(), json!({"matrix": matrix_json(want), "copy": false}))], "{id}"); + assert_eq!(t.cursor(&c, Point::new(220.0, 230.0), Mods::default()), handle_cursor(Handle::BottomRight, 0.0), "{id}"); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 220.0, 230.0)), vec![Action::Commit], "{id}"); + assert!(!t.busy() && !t.transforming() && !t.resizing(), "{id}"); + } + // The side handles scale along one axis; Escape turns the scale back. + let mut t = ShapeTool::new("rectangle"); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 100.0)), vec![Action::Begin("Scale".into())]); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 150.0, 80.0)); + let want = scale_for_drag(Rect::new(100.0, 100.0, 200.0, 200.0), Handle::Top, Point::new(150.0, 80.0), false, false); + assert_eq!(a, vec![Action::Preview("object.transform".into(), json!({"matrix": matrix_json(want), "copy": false}))]); + assert_eq!(t.key(&c, ToolKey::Escape, Mods::default()), vec![Action::Cancel]); + assert!(!t.busy()); + // Backtick pressed after the last drag: the release previews the scale again for the patterns + // only, then commits. + let mut t = ShapeTool::new("rectangle"); + t.pointer(&c, &PointerEvent::new(PointerKind::Down, corner.x, corner.y)); + t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 220.0, 230.0)); + let a = t.pointer(&c, &PointerEvent { backquote: true, ..PointerEvent::new(PointerKind::Up, 220.0, 230.0) }); + let want = scale_for_drag(Rect::new(100.0, 100.0, 200.0, 200.0), Handle::BottomRight, Point::new(220.0, 230.0), false, false); + let patterns = json!({"matrix": matrix_json(want), "copy": false, "objects": false, "patterns": true}); + assert_eq!(a, vec![Action::Preview("object.transform".into(), patterns), Action::Commit]); + assert!(!t.busy()); + // View › Hide Bounding Box hides the handles, and a press there draws a new shape. + let hidden = ToolContext { show_bbox: false, ..cx(&d, &s, &p) }; + let mut t = ShapeTool::new("rectangle"); + assert_eq!(t.cursor(&hidden, corner, Mods::default()), Cursor::Crosshair); + t.pointer(&hidden, &PointerEvent::new(PointerKind::Down, corner.x, corner.y)); + let a = t.pointer(&hidden, &PointerEvent::new(PointerKind::Drag, 240.0, 240.0)); + assert_eq!(a.first(), Some(&Action::Begin("Rectangle".into())), "{a:?}"); + // The Line Segment tool shows no box, so its press draws. + let mut t = ShapeTool::new("lineSegment"); + t.pointer(&c, &PointerEvent::new(PointerKind::Down, corner.x, corner.y)); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 240.0, 240.0)); + assert_eq!(a.first(), Some(&Action::Begin("Line".into())), "{a:?}"); +} + /// Two selected live rectangles, the second placed so that its top-left Live Corners widget (10 pt /// in along the corner's bisector) lands on the first one's centre point. fn two_live_rects() -> (vectorcraft_doc::Document, Selection) { diff --git a/crates/ui-egui/src/canvas/overlays.rs b/crates/ui-egui/src/canvas/overlays.rs index fcda53def..055263297 100644 --- a/crates/ui-egui/src/canvas/overlays.rs +++ b/crates/ui-egui/src/canvas/overlays.rs @@ -123,7 +123,7 @@ pub(super) fn selection_overlay(app: &mut VectorcraftApp, p: &egui::Painter, xf: // shows while the key is down, where its handles take that press. let held = super::drag::mods(p.ctx().input(|i| i.modifiers), false); let lends_selection = !app.session.tool_busy() && app.session.lent_tool_id(&held) == Some("selection"); - let show_box = (app.session.tool_id() == "selection" || lends_selection || (shape_tool && !app.session.tool_busy())) + let show_box = (app.session.tool_id() == "selection" || lends_selection || (shape_tool && (!app.session.tool_busy() || resizing))) && (resizing || !app.session.tool_transforming() || moved.is_some()) && app.ui.view.bounding_box && app.session.active().is_some_and(|st| !st.selection.is_empty() && st.selection.anchors.is_empty()); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index efe59c127..0f61369d8 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -1173,6 +1173,56 @@ fn a_click_just_outside_the_canvas_does_not_reach_the_tool() { assert_eq!(selected(&app), vec![vectorcraft_doc::NodeId(id)]); } +/// The box-drawing shape tools show the bounding box of the shape just drawn (#5477). A drag of +/// its bottom-right handle with the tool still active scales that shape, as with the Selection +/// tool, rather than drawing a second shape from the handle. +#[test] +fn a_shape_tool_scales_the_shape_just_drawn_by_its_bounding_box_handle() { + let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + for tool in ["rectangle", "roundedRectangle", "ellipse", "polygon", "star"] { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.select_tool(tool); + let ctx = egui::Context::default(); + frame(&mut app, &ctx, vec![]); + let drag = |app: &mut VectorcraftApp, a: Pos2, b: Pos2| { + for ev in [ + egui::Event::PointerMoved(a), + button(a, true), + egui::Event::PointerMoved(a.lerp(b, 0.5)), + egui::Event::PointerMoved(b), + button(b, false), + ] { + frame(app, &ctx, vec![ev]); + } + frame(app, &ctx, vec![]); + }; + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + drag(&mut app, xf.to_screen(Point::new(150.0, 120.0)), xf.to_screen(Point::new(210.0, 170.0))); + let objects = |app: &VectorcraftApp| { + let mut n = 0; + app.session.active().unwrap().doc.walk(|node| n += usize::from(node.path_data().is_some())); + n + }; + assert_eq!(objects(&app), 1, "{tool}: the drag draws one shape"); + let before = app.selection_box().unwrap_or_else(|| panic!("{tool}: the shape just drawn shows its box")); + // The bottom-right handle, dragged 40 pt along the box's width and 30 pt along its height + // (a polygon's or star's box turns with the shape). + let (corner, to) = (Point::new(before.rect.x1, before.rect.y1), Point::new(before.rect.x1 + 40.0, before.rect.y1 + 30.0)); + drag(&mut app, xf.to_screen(before.to_doc() * corner), xf.to_screen(before.to_doc() * to)); + let after = app.selection_box().unwrap_or_else(|| panic!("{tool}: the shape stays selected")); + let label = app.session.active().and_then(|st| st.history.undo.last().map(|e| e.label.clone())); + assert_eq!(objects(&app), 1, "{tool}: no second shape, last step {label:?}"); + assert!( + (after.rect.width() - before.rect.width() - 40.0).abs() < 0.5 && (after.rect.height() - before.rect.height() - 30.0).abs() < 0.5, + "{tool}: {:?} -> {:?}, last step {label:?}", + before.rect, + after.rect + ); + assert_eq!(label.as_deref(), Some("Scale"), "{tool}"); + } +} + #[test] fn a_drag_from_a_ruler_onto_the_canvas_makes_a_guide() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b4c949f74..a7ae301a0 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -936,7 +936,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Curvature point markers are circular, with no direction handles. While drawing, its own point feedback replaces generic selection markers; existing paths use the selection overlay, including partial selections and Hide Edges. Anchor Size applies, and Direct Selection and Pen retain their handles. The real egui-frame regressions in `crates/ui-egui/src/canvas.rs` verify marker positions, counts and unchanged documents (#1236, in part); the existing-path interpolation request remains open. The curve through the points (`curvature_spline` in `crates/tools/src/draw2.rs`) makes each stretch of three points follow the circle through them: a smooth point's handles lie along that circle's tangent, each `chord / (3 cos²(α/2))` long (α between handle and chord, capped at 90°), and the ends of an open path mirror their neighbour's tangent in their chord, so a J or a U curves evenly, a turn tighter than 30° comes to a point, and uneven spacing doesn't loop; the rubber band, a dragged or inserted point and `path.convertAnchor` to smooth use the same rule, while the Pencil's fitting keeps its own; `tests/draw2.rs` checks that the rubber band is the path the click makes (#1236, in part). The Smooth tool smooths only the stretch of path under the drag (#1547, fixes #1512). While Alt is held during a Paintbrush stroke or before a press with no path selected that the Smooth tool can act on, the Paintbrush shows a closing cursor (a crosshair with a ring), and so does the Pencil with Alt key toggles to Smooth Tool off when Alt is pressed after the drag starts; releasing the mouse button with Alt still held closes the path. Before a Paintbrush press with such a path selected, Alt shows the Smooth cursor, and holding Alt at the press smooths that path with the Smooth tool's radius and Smooth Tool Options' Fidelity. A Pencil drag started with Shift or Alt held draws a straight line instead, which stays open. With Shift held at the press, the Pencil draws a straight line from the press point to the pointer at 45° steps from the Constrain Angle, as [Draw straight lines with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/draw-straight-lines-with-the-pencil-tool.html) describes; pressed on an end of a selected open path, the line continues the path from that end (part of #3799). With Option/Alt held at the press and Alt key toggles to Smooth Tool off, the Pencil draws a straight line at any angle, whose ends snap to Smart Guides, and it stays open when Alt is still held at the release; Shift and Alt together keep the 45° steps (#3799). While Shift or Alt is held before the press, the Pencil shows a straight-line cursor (a crosshair with a short line), and over an end of a selected open path, where the line continues the path, a path-continuation cursor (the same line leaving a small square), as the page describes. - - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. A shape just drawn stays adjustable with its tool: its Live Shape widgets drag, a press on its centre point (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 the Selection tool's rotate cursor (#5477; tests `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` and, through the engine, `shapetools.rs`). The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). + - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. A shape just drawn stays adjustable with its tool: its Live Shape widgets drag, a press on its centre point (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 the Selection tool's rotate cursor (#5477; tests `a_shape_tool_moves_the_selection_by_its_centre_point_and_turns_it_from_a_corner` and, through the engine, `shapetools.rs`). A drag on one of its eight bounding-box handles scales it as the Selection tool's handles do, with their cursors and snapping, instead of drawing a new shape from the handle (tests `a_shape_tool_scales_the_selection_by_its_bounding_box_handles` and `a_shape_tool_scales_the_shape_just_drawn_by_its_bounding_box_handle`). The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays by 0.1 radian at each drag update in which the pointer's document y grows, and back by as much at each one in which it falls, Shift held keeps their turn, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation (unturned, or turned by a nonzero General › Constrain Angle), and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812). - **Shapes:** all shape tools (including Flare) and the line, arc, spiral and grid tools. Space held while dragging out a shape, line, arc, spiral or grid moves it at its current size. The Flare tool's center drag turns the rays with the pointer, Shift keeps them in their original orientation, and Space held during either of its drags moves the flare. While a grid is dragged out, Up/Down change its rows (concentric dividers) and Left/Right its columns (radial dividers). A click with a grid tool asks for the size, the divider counts and the grid options (#1377, in part): a skew for each set of dividers, Use Outside Rectangle As Frame or Create Compound Path From Ellipses, and Fill Grid; they are parameters of `shape.rectangularGrid` and `shape.polarGrid`, stay as the tool's options and apply to drags too. The curve a skew percentage follows and the direction of the radial skew are our own (the public documentation gives neither), and the dialogs have no reference point locator. An ellipse's Pie Start and End Angle and Invert Pie (Transform and Properties panels' Ellipse Properties, `object.setLiveShape {pieStart, pieEnd, invertPie}`) cut it as a pie (#812). With the bounding box showing, the Selection tool shows a selected ellipse's two pie widgets just outside it; dragging one sets its angle in one undo step and double-clicking one makes the ellipse whole again. A live polygon's Polygon Properties there: side count, angle, corner type and radius, radius, side length and Make Sides Equal once an uneven scale has made its sides differ (`object.setLiveShape {sides, polygonAngle, polygonRadius, sideLength, makeSidesEqual}`, part of #812); a star is a plain path, as in Illustrator. The Flare Tool Options last as the tool's options: the next flare dragged out uses them, a click's dialog shows them and double-clicking the tool opens them (part of #812).