diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index c27466935..c519b2e56 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -8,7 +8,9 @@ //! Group Selection's clicks do), drag a direction handle to //! reshape (Shift keeps it at 45° steps round its anchor, Alt moves it alone; smart guides snap //! it), marquee to select anchors (Shift-drag toggles them: the selected ones inside are -//! deselected, the others selected), drag a corner widget of any path (a star, a pen path) to round its corners +//! deselected, the others selected; with Alt held at the press, as Group Selection for the drag, +//! it selects every object it touches, whole), drag a corner widget of any path (a star, a pen +//! path) to round its corners //! (the selected ones when anchors are selected; Alt-click cycles their kind, double-click opens //! the Corners dialog). //! Group Selection: click selects the leaf; each further click on it adds the next enclosing group; @@ -36,6 +38,8 @@ //! them all, and the arrow keys nudge them; otherwise the type is selected whole. Dragging an anchor of type on //! a path's path reshapes the path (`text.reshapePath`): the text reflows along it. //! Dragging the brackets of selected type on a path moves or flips it ([`crate::pathtype`]). +//! A press on a selected object's centre point (Attributes panel › Show Center) selects the object +//! whole, and a drag from it moves the object. //! //! A press on the stroke of a path that isn't selected as a whole selects that segment's two anchors //! (the fill, or Alt, selects the whole path); dragging the segment bends it if it's curved, else @@ -103,6 +107,9 @@ enum State { /// Group Selection, E pressed an odd number of times during the drag: the marquee selects /// only the objects that lie entirely inside it, whole. enclosed: bool, + /// Direct Selection with Alt held at the press, which makes it the Group Selection tool + /// for the drag: the marquee selects every object it touches, whole (#4890). + whole: bool, }, Corner(CornerDrag), /// Dragging a bracket of type on a path. @@ -508,6 +515,16 @@ 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)> { anchor_owners(cx, is_area_type).into_iter().find_map(|id| { @@ -737,6 +754,9 @@ impl Tool for DirectSelectionTool { if let Some((id, si, ai, grab)) = hit_anchor(cx, p, point) { return self.press_anchor(cx, ev, p, (id, si, ai, grab)); } + if let Some((id, center)) = hit_center(cx, p, point) { + return self.press_center(cx, ev, p, id, center); + } if let Some((id, anchors)) = hit_frame_edge(cx, p, tol) { self.frame.clear(); return self.press_type_area(cx, id, vec![(id, anchors)], p, ev.mods.shift); @@ -756,7 +776,7 @@ impl Tool for DirectSelectionTool { if let Some(out) = self.press_hidden(cx, ev, p) { return out; } - self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false }; + self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false, whole: ev.mods.alt }; vec![] } (PointerKind::Drag, State::MoveAnchors { start, grab, began }) => self.drag_move_anchors(cx, ev, p, (start, grab, began)), @@ -839,8 +859,8 @@ impl Tool for DirectSelectionTool { self.state = State::Idle; self.mesh.release(cx).unwrap_or_default() } - (PointerKind::Drag, State::Marquee { start, toggle, enclosed, .. }) => { - self.state = State::Marquee { start, cur: p, toggle, enclosed }; + (PointerKind::Drag, State::Marquee { start, toggle, enclosed, whole, .. }) => { + self.state = State::Marquee { start, cur: p, toggle, enclosed, whole }; vec![] } (PointerKind::Drag, State::Corner(mut c)) => { @@ -875,7 +895,7 @@ impl Tool for DirectSelectionTool { self.guides.clear(); vec![Action::Commit] } - (PointerKind::Up, State::Marquee { start, toggle, enclosed, .. }) => self.release_marquee(cx, p, (start, toggle, enclosed)), + (PointerKind::Up, State::Marquee { start, toggle, enclosed, whole, .. }) => self.release_marquee(cx, p, (start, toggle, enclosed, whole)), _ => vec![], } } @@ -913,6 +933,8 @@ impl Tool for DirectSelectionTool { match self.state { // Alt-dragging leaves a copy behind. State::MoveObject { began: true, .. } if m.alt => return Cursor::SelectionCopy, + // An Alt-marquee is Group Selection's until it is released. + State::Marquee { whole: true, .. } => return Cursor::GroupSelection, // Bending a segment keeps the cursor that offered it. State::Segment { began: true, .. } | State::SpineSegment { began: true, .. } => return Cursor::ReshapeSegment, // A drag shows the filled arrow, hollow while it snaps to a point or a guide. @@ -1102,7 +1124,7 @@ impl DirectSelectionTool { if let Some(out) = self.press_hidden(cx, ev, p) { return out; } - self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false }; + self.state = State::Marquee { start: p, cur: p, toggle: ev.mods.shift, enclosed: false, whole: false }; return vec![]; }; // A compound path's paths are picked one at a time, below the compound path (#2441). @@ -1191,6 +1213,19 @@ impl DirectSelectionTool { } vec![] } + /// Press on the centre point of object `id`: it selects the object whole (Shift toggles it), + /// as a press on its fill does, and a drag moves it held by that point (#4891). + fn press_center(&mut self, cx: &ToolContext, ev: &PointerEvent, p: Point, id: NodeId, center: Point) -> Vec { + self.state = State::MoveObject { start: p, grab: None, hold: center, began: false, deselect: None }; + self.frame.clear(); + if ev.mods.shift { + return vec![Action::Exec("select.toggle".into(), json!({"id": id.0}))]; + } + if cx.selection.partial(id).is_some() || cx.selection.segments.contains_key(&id) { + return vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))]; + } + vec![] + } fn press_object(&mut self, cx: &ToolContext, ev: &PointerEvent, p: Point, tol: f64, mut h: vectorcraft_doc::hit::Hit) -> Vec { if let Some(part) = cx.doc.node(h.leaf).and_then(|n| compound_part_at(n, p, tol)) { h.ancestry.push(part); @@ -1381,7 +1416,7 @@ impl DirectSelectionTool { (false, None, None, None) => group.map(|g| Action::Exec("select.add".into(), json!({"ids": [g.0]}))).into_iter().collect(), } } - fn release_marquee(&mut self, cx: &ToolContext, p: Point, (start, toggle, enclosed): (Point, bool, bool)) -> Vec { + fn release_marquee(&mut self, cx: &ToolContext, p: Point, (start, toggle, enclosed, whole): (Point, bool, bool, bool)) -> Vec { self.state = State::Idle; let r = Rect::from_points(start, p); if r.width() < cx.tol(3.0) && r.height() < cx.tol(3.0) { @@ -1392,14 +1427,16 @@ impl DirectSelectionTool { return vec![Action::Exec("select.none".into(), json!({}))]; } // With Group Selection's enclosed marquee, the tool selects the objects inside it - // (members of groups too), each whole, as a click selects one. - if enclosed { + // (members of groups too), each whole, as a click selects one; with an Alt-marquee, every + // object it touches, each whole. + if enclosed || whole { let mut touched = vectorcraft_doc::hit::marquee(cx.doc, r, cx.isolation, true); let editable = cx.doc.editable(); touched.retain(|id| editable(*id)); - let ids: Vec = crate::select::inside(cx, r, touched).into_iter().map(|id| json!(id.0)).collect(); + let picked = if enclosed { crate::select::inside(cx, r, touched) } else { touched }; + let ids: Vec = picked.into_iter().map(|id| json!(id.0)).collect(); let art = Action::Exec(if toggle { "select.toggle" } else { "select.set" }.into(), json!({ "ids": ids })); - return std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r, true)).collect(); + return std::iter::once(art).chain(crate::rulerguide::marquee_select(cx, r, enclosed)).collect(); } // Anchors inside the rect, on editable paths inside isolation mode. let mut sel: Vec<(NodeId, Vec)> = vec![]; diff --git a/crates/tools/src/direct/tests.rs b/crates/tools/src/direct/tests.rs index cb852c553..b7892cc2d 100644 --- a/crates/tools/src/direct/tests.rs +++ b/crates/tools/src/direct/tests.rs @@ -922,6 +922,61 @@ fn shift_marquee_toggles_anchors() { } } +/// #4890: Alt held at the press makes a Direct Selection marquee Group Selection's for the drag: +/// it selects each path it touches whole (with Shift, toggles it), not the anchors inside it, and +/// leaves the paths it misses; the Group Selection cursor stays until the release. +#[test] +fn an_alt_marquee_selects_the_paths_it_touches_whole() { + let (d, a, _) = two_squares(); + let (s, p) = (Selection::default(), paint()); + let cx = cx(&d, &s, &p); + let alt = Mods { alt: true, ..Default::default() }; + let alt_shift = Mods { shift: true, ..alt }; + for (mods, cmd) in [(alt, "select.set"), (alt_shift, "select.toggle")] { + let mut t = DirectSelectionTool::new(false); + // Round A's top-right corner only; B (300..400) is left out. + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 150.0, 50.0).with_mods(mods)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 250.0, 150.0).with_mods(mods)); + assert_eq!(t.cursor(&cx, Point::new(250.0, 150.0), mods), Cursor::GroupSelection, "{mods:?}"); + let up = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 250.0, 150.0).with_mods(mods)); + assert_eq!(up, vec![Action::Exec(cmd.into(), json!({"ids": [a.0]}))], "{mods:?}"); + } +} + +/// #4891: a press on the centre point a selected object shows (Show Center) selects it whole, and +/// a drag from there moves it by the drag as one Move; the square is unfilled, so the press finds +/// no fill there. +#[test] +fn the_centre_point_selects_and_moves_the_object_whole() { + let (mut d, id) = doc_with_rect(); + let n = d.node_mut(id).unwrap(); + n.appearance.items.retain(|i| !matches!(i, vectorcraft_doc::AppearanceItem::Fill(_))); + n.edit_attrs(|a| a.show_center = Some(true)); + let p = paint(); + // One anchor selected: the square shows its centre, (150, 150). + let one = anchors_of(id, &[(0, 0)]); + let mut t = DirectSelectionTool::new(false); + let c = cx(&d, &one, &p); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 151.0, 149.0)), vec![Action::Exec("select.set".into(), json!({"ids": [id.0]}))]); + assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 151.0, 149.0)).is_empty()); + // Selected whole: the press keeps the selection and the drag moves the square. + let mut whole = Selection::default(); + whole.set([id]); + let c = ToolContext { smart_guides: false, ..cx(&d, &whole, &p) }; + assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0)).is_empty()); + let a = t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 140.0)); + let moved = json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(Vec2::new(20.0, -10.0))), "copy": false}); + assert_eq!(a, vec![Action::Begin("Move".into()), Action::Preview("object.transform".into(), moved)]); + assert_eq!(t.pointer(&c, &PointerEvent::new(PointerKind::Up, 170.0, 140.0)), vec![Action::Commit]); + // Unselected, the centre isn't shown and a press there starts a marquee. + let none = Selection::default(); + let mut t = DirectSelectionTool::new(false); + let c = cx(&d, &none, &p); + assert!(t.pointer(&c, &PointerEvent::new(PointerKind::Down, 150.0, 150.0)).is_empty()); + t.pointer(&c, &PointerEvent::new(PointerKind::Drag, 170.0, 140.0)); + assert!(t.overlays(&c).iter().any(|o| matches!(o, Overlay::Marquee(_)))); +} + /// Group Selection: E pressed during a marquee drag makes it select the objects entirely inside /// it whole, a group's member without its group. Direct Selection does not take E. #[test] diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 639e28693..340854a0d 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Arrange Tools add-on; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Direct Selection: Alt-marquee selects whole paths, #4890; the centre point selects and moves its object, #4891; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -704,8 +704,10 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur `path.reshapeSegment`, `object.blend.spine.reshapeSegment`, `path.convertAnchor`, `path.curvatureEdit`, `path.insertAnchor`, `path.moveAnchors`, `path.reshape`, `path.close` and the Pencil, Curvature, Knife, Eraser, Path Eraser, Smooth, Join and Blob Brush commands refuse a number, point or handle given as text, such as "10", and leave the document unchanged; `path.convertAnchor` refuses an `x` without a `y`, or a `y` without an `x`. `path.close` and `path.setHandle` (`independent`), `path.moveAnchors` (`patterns`), the Pencil (`closed`, `keepSelected`, `fill`), Curvature (`closed` and a point's `corner`), `path.curvatureEdit` (`corner`), `path.reshapeSegment` (`perpendicular`) and the Blob Brush (`keepSelected`, `mergeOnlySelection`, `merge`) refuse a value other than true, false or null, such as "true", and leave the document unchanged. Direct Selection's marquee selects the frame anchors of area type that lie inside it, and a click on a frame anchor selects that one; Shift toggles them. The selected frame anchors show filled. A drag of one of them moves all of them, the arrow keys nudge them, and the type reflows at its size (`text.reshapeArea`, which takes `items` to move the anchors of several frames in one step), as [Create text](http://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html) and [Selecting objects](http://web.archive.org/web/20211230015528/https://helpx.adobe.com/illustrator/using/selecting-objects.html) describe. When the selection also holds other objects, path anchors or ruler guides, or when another change to the document or the selection follows the pick, the type is selected whole. + A press on a selected object's centre point (Attributes panel › Show Center) selects the object whole, and a drag from it moves the object as one Move, held by that point (#4891; test `the_centre_point_selects_and_moves_the_object_whole`). - **Group Selection (#2441):** a click picks one path of a compound path (its outline within the pick tolerance, else the innermost path whose area holds the click: the outer shape or a hole), Shift-click adds another, and a click on a selected path adds the compound path, as a click on a group member climbs to its group. A path selected this way moves, rotates and scales on its own; paint still goes to the compound path. With Direct Selection, an Alt-click on a path selected whole adds its group, and each further Alt-click adds the next group up, as Group Selection's clicks do; the climb stops below the layer and its sublayers and at the isolated object. An Alt-drag still copies the selection, and Alt+Shift-click on a segment or the fill still adds or removes the path. Public references: [Select objects](https://web.archive.org/web/20250904012025/https://helpx.adobe.com/illustrator/using/selecting-objects.html) and [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) (archived copies). + With Alt held at the press, a Direct Selection marquee is Group Selection's for the drag: it selects every path it touches, whole (Shift toggles them), not the anchors inside it, and the Group Selection pointer stays until the release (#4890; test `an_alt_marquee_selects_the_paths_it_touches_whole`). With Direct Selection, Shift pressed during the drag of a curved segment keeps both of its handles at right angles to the line between its anchors, on the same side and at the same length (`path.reshapeSegment {perpendicular}`). Shift held while any segment is dragged with the Anchor Point tool or the Pen with Alt does the same. With the Anchor Point tool, Shift held at the press on an anchor (or a selected blend's spine point) moves it, handles and all, instead of pulling out handles; Shift kept down keeps the move at 45° steps, and a Shift-click that doesn't move leaves the anchor as it was (`path.moveAnchor`). A selected blend spine's segment dragged with the Anchor Point tool, or a curved one dragged with Direct Selection, bends the same way (`object.blend.spine.reshapeSegment`). - **Live Corners:** drag a live rectangle's corner widgets (Selection or Direct Selection) to round all corners, with a radius readout; with Direct Selection, select corner anchors to show and round just those corners (#381). Each corner keeps its own radius and kind (Round, Inverted Round, Chamfer): Alt-click a widget to cycle the kind, double-click it for the Corners dialog, and the Properties and Transform radius fields set the selected corners (`object.setLiveShape {corners, radius, kind}`). A link toggle behind Corner Radius (the `singleCornerRadius` preference) switches the panels to a field for each corner of a live rectangle: Top Left, Top Right, Bottom Right and Bottom Left. Corners stay circular when the rectangle is scaled unevenly or along its own sides when rotated (the radius keeps its size, or scales by the mean scale with Scale Corners on); only a shear distorts them. On a rectangle that isn't square every corner is the same circle: a radius past half the shorter side stops there for all of them, the widgets sit on the corners as drawn and turn the edited corners red at that limit, CSS export writes the drawn radii, and rectangles saved stretched by older versions get circular corners when their corners are next set (#442). Live Corners work on any path's corners too, as on a rectangle's (#511): a star's, a live polygon's (which also shows them to the Selection tool, has a Corner Radius field in the Properties and Transform panels and keeps its radius as its sides change) and a pen path's, wherever two sides meet at an angle at an anchor that isn't smooth: curved sides too, trimmed to what the cut leaves of them (#1144). Direct Selection shows the widgets of every corner, or of the corners holding selected anchors; each corner is a circle tangent to both sides, drawn no larger than takes its cut halfway along its shorter side; a path keeps its uncut outline so its corners stay editable, and the same command, dialog, angle preference and View › Hide Corner Widget apply. With Direct Selection a click on a widget (no drag) selects its corner, as a click on the corner's anchor does, so the next drag on it rounds that corner alone; Shift-click toggles it (#1239). While a corner widget is dragged with the Selection tool the bounding box hides, so the rounding shows against the path alone (#1229). A round corner next to a curved side curves smoothly instead of sticking out of the shape (#1547, fixes #1512). View › Hide Edges hides the Live Corners widgets, the bounding box and the type widget, and the selection tools then do not answer a press on them or change the cursor over them. @@ -1382,6 +1384,7 @@ dialogs, Preview and Cancel, Zoom to Selection) and `command_sweep`. Commands: ` | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | Selection tools: with Alt held at the press, a Direct Selection marquee selects every path it touches whole and keeps the Group Selection pointer (#4890); a press on a selected object's centre point selects the object whole and a drag from it moves it (#4891); regression tests in `direct/tests.rs`; scores and estimates unchanged. | | 2026-10-31 | minor | Arrange Tools add-on (a VectorCraft addition): evidence added; scores unchanged. | | 2026-10-31 | minor | Guides and snapping: Snap to Perpendicular and Snap to Tangent apply to an existing line's end dragged with Direct Selection, as the public pages describe for editing existing lines (the end anchor of a straight segment, not mid-line anchors or curved segments); a regression test covers both snaps, both toggles and non-snapping controls (#2530); scores and estimates unchanged. | | 2026-10-30 | minor | File formats: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index add5e8fd7..2ad33fd86 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Arrange Tools add-on; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-31 · **Change:** minor (Direct Selection: an Alt-marquee selects the paths it touches whole, #4890; a click on a selected object's centre point selects it whole and a drag moves it, #4891; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -129,6 +129,8 @@ tools, never checked side by side) isn't captured row by row. | Direct Selection and Group Selection pick only inside the isolated object in isolation mode | done | #1666 | | Group Selection picks one path of a compound path (the outer shape or a hole) and moves it on its own; a second click adds the compound path | done | #2441; Shift-click on a selected member deselects it on release | | Direct Selection, Group Selection and the Selection tool take a clipping path (a clip group's, or a layer's clipping mask) on its outline, within the selection tolerance, even where the art it clips lies over its edge | done | `HitOptions::clip_outlines`; Select Behind lists the same; the eyedropper and the tools that act on what is drawn take the art | +| Alt with Direct Selection makes a marquee Group Selection's for the drag: it selects every path it touches, whole (Shift toggles them), and the Group Selection pointer stays until the release | done | #4890 | +| Direct Selection: a click on a selected object's centre point (Attributes › Show Center) selects the object whole, and a drag from it moves the object in one undo step | done | #4891 | | Escape cancels a Direct Selection drag | done | #1803 (#1743) | | 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`) | @@ -377,6 +379,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-10-31 | minor | Direct Selection: with Alt held at the press, a marquee selects every path it touches whole and keeps the Group Selection pointer while dragging (#4890); a click on a selected object's centre point (Show Center) selects the object whole, and a drag from it moves the object (#4891); scores unchanged | | 2026-10-31 | minor | Arrange Tools add-on (a VectorCraft addition): Object › Plug-ins › Arrange Tools; scores and estimates unchanged | | 2026-10-31 | minor | Export for Screens is laid out as Illustrator's: Artboards and Assets tabs with a grid or list of thumbnails, Clear Selection and Prefix under them, Create Sub-folders by Scale or Format (on by default), Export PDFs As Single File or Multiple Files (`pdfs`), iOS and Android rows, the format presets in a ☰ menu, and a Selected / files-to-export count; the naming pattern, Layers and Selection pages and folders list stay in `document.exportForScreens` for scripts and MCP; scores unchanged | | 2026-10-31 | minor | Properties: Quick Actions has Blend Options for one selected blend with any tool, not only the Blend tool, as the blending page describes (#4817); scores unchanged |