Pathfinder: direct commands set the operation Cmd+4 repeats

Every successful Shape Mode or Pathfinder operation now sets the
operation that Repeat Pathfinder runs, including direct
object.pathfinder.* commands, not only the panel's buttons. Trap,
compound-shape edits and live effects leave it unchanged, and a failed
operation keeps the earlier one. The panel menu names the remembered
operation, and Repeat runs it on the current selection with the current
Pathfinder Options.

With too few objects selected, Cmd/Ctrl+4 leaves the art as it is and
shows the panel's selection message in the status bar. Adds the
"no Pathfinder operation applied yet" rows to the Catalan and Turkish
catalogs, and tests for direct and failed operations, Trap, compound
shapes, current options, a one-path selection and the default keys.
This commit is contained in:
dotCooCoo
2026-10-30 23:52:26 +07:00
committed by bflatastic
parent 9b3e2d873b
commit bad34b7115
10 changed files with 148 additions and 29 deletions
+6
View File
@@ -88,6 +88,12 @@ impl VectorcraftApp {
{
crate::ime::note_application_edit(self, &ctx, ime_view, ime_target, ime_before);
}
if r.is_ok()
&& let Some(op) = id.strip_prefix("object.pathfinder.")
&& panels::pathfinder::operation_label(op).is_some()
{
self.last_pathfinder = Some(op.to_string());
}
if r.is_ok() && matches!(id, "edit.copy" | "edit.cut") {
self.publish_clipboard();
}
+1
View File
@@ -5105,6 +5105,7 @@
@msg no document no hi ha document
@msg no document open no hi ha cap document obert
@msg no effect applied yet encara no s’ha aplicat cap efecte
@msg no Pathfinder operation applied yet encara no s’ha aplicat cap operació del Cercador de traçats
@msg no effect at index {k} no hi ha cap efecte a l’índex {k}
@msg no empty artboards to delete no hi ha taulers buits per suprimir
@msg no end record no hi ha registre final
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3931,6 +3931,7 @@ axis Both Her İkisi
@msg no document belge yok
@msg no document open hiçbir belge açık değil
@msg no effect applied yet henüz hiçbir efekt uygulanmadı
@msg no Pathfinder operation applied yet henüz hiçbir Yol Bulucu işlemi uygulanmadı
@msg no effect at index {k} {k} endeksinde etkisi yok
@msg no end record son kayıt yok
@msg no file manager here burada dosya yöneticisi yok
Can't render this file because it contains an unexpected character in line 5 and column 25.
+2 -3
View File
@@ -134,9 +134,8 @@ pub struct VectorcraftApp {
pub titlebar_inset: f32,
/// Last applied effect (Effect → Apply Last Effect).
pub last_effect: Option<(String, serde_json::Value)>,
/// The operation a Pathfinder panel button ran last (`unite`, `divide`, …) and the button's
/// label, which Repeat Pathfinder runs again ([`panels::pathfinder::repeat`]).
pub last_pathfinder: Option<(String, String)>,
/// Last successful destructive Pathfinder operation, repeated on the current selection.
pub last_pathfinder: Option<String>,
/// Last confirmed Warp Options (bend, distortion, orientation) for the next new warp dialog.
pub last_warp: Option<serde_json::Value>,
/// The Character panel field that takes the keyboard focus when the panel next draws it
+4 -1
View File
@@ -217,7 +217,10 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
"effect.dialog" | "ui.recolorDialog" | "ui.effectGalleryDialog" => app.session.active().is_some_and(|d| !d.selection.is_empty()),
"ui.cropImage" => selected_image(app, |_| true),
"effect.applyLast" | "effect.last" => app.last_effect.is_some() && app.session.active().is_some_and(|d| !d.selection.is_empty()),
"pathfinder.repeat" => app.last_pathfinder.as_ref().is_some_and(|(op, _)| enabled(app, &format!("object.pathfinder.{op}"))),
"pathfinder.repeat" => app.last_pathfinder.as_ref().is_some_and(|op| {
let min = crate::panels::pathfinder::minimum_operands(op);
crate::panels::pathfinder::operands(app) >= min
}),
"file.export.pdf" | "ui.savePdfDialog" | "ui.fileInfoDialog" | "ui.rasterEffectsSettingsDialog" => app.session.active().is_some(),
"ui.missingFontsDialog" => !cfg!(target_arch = "wasm32") && app.session.active().is_some(),
"ui.findFontsInFolder" => crate::picks::can(app, &crate::picks::PickRequest::Folder) && app.session.active().is_some(),
+1 -1
View File
@@ -579,7 +579,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"pathfinder.repeat",
"Repeat Pathfinder",
"Cmd+4",
"{} repeat the last Pathfinder operation run from the Pathfinder panel on the selection; an error until a button has run",
"{} repeat the last successful Shape Mode or Pathfinder operation on the selection with the current Pathfinder Options; an error until one has run",
),
(
"file.export.pdf",
+129 -19
View File
@@ -26,24 +26,38 @@ pub const PATHFINDERS: [(&str, &str, &str); 6] = [
("dc-pf-minus-back", "Minus Back", "minusBack"),
];
fn run(app: &mut VectorcraftApp, _ui: &Ui, op: &str, label: &str) {
if app.run(&format!("object.pathfinder.{op}"), json!({})).is_ok() {
app.last_pathfinder = Some((op.to_string(), label.to_string()));
}
/// Returns the minimum number of operands required by a Pathfinder operation.
pub(crate) fn minimum_operands(op: &str) -> usize {
if matches!(op, "outline" | "divide") { 1 } else { 2 }
}
/// Repeat Pathfinder (`pathfinder.repeat`, Cmd/Ctrl+4, the panel menu's Repeat item): the
/// operation a button of the panel ran last, on the selection.
/// The panel name for one of the ten repeatable Shape Mode or Pathfinder operations.
pub fn operation_label(op: &str) -> Option<&'static str> {
SHAPE_MODES.into_iter().chain(PATHFINDERS).find(|(_, _, id)| *id == op).map(|(_, label, _)| label)
}
fn run(app: &mut VectorcraftApp, op: &str) {
// The command path reports a failed operation in the status bar.
let _ = app.run(&format!("object.pathfinder.{op}"), json!({}));
}
/// Repeat the last successful Shape Mode or Pathfinder operation on the current selection.
pub fn repeat(app: &mut VectorcraftApp) -> Result<Value, String> {
match app.last_pathfinder.clone() {
Some((op, _)) => app.run(&format!("object.pathfinder.{op}"), json!({})),
Some(op) => {
let min = minimum_operands(&op);
if operands(app) < min {
return Err("Select two or more objects, or a group".into());
}
app.run(&format!("object.pathfinder.{op}"), json!({}))
}
None => Err("no Pathfinder operation applied yet".into()),
}
}
/// How many objects the selection gives an operation: a group selected alone counts as the shapes
/// the operation takes from it, nested groups included (#1450, #1635).
fn operands(app: &VectorcraftApp) -> usize {
pub(crate) fn operands(app: &VectorcraftApp) -> usize {
let alone = app.session.active().filter(|d| d.selection.len() == 1).and_then(|d| d.doc.node(*d.selection.objects.first()?));
match alone.filter(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. })) {
Some(group) => vectorcraft_engine::cmd::shape_count(group),
@@ -64,13 +78,13 @@ fn button(ui: &mut Ui, icon: &str, tip: &str, selected: bool, n: usize, min: usi
/// A Shape Mode button clicked: Alt makes a compound shape of the selection in `mode`; with
/// members of compound shapes selected they take `mode`; otherwise operation `op` runs.
fn shape_mode(app: &mut VectorcraftApp, ui: &Ui, op: &str, label: &str, mode: ShapeMode, members: bool) {
fn shape_mode(app: &mut VectorcraftApp, ui: &Ui, op: &str, mode: ShapeMode, members: bool) {
if ui.input(|i| i.modifiers.alt) {
crate::menus::invoke(app, "object.compoundShape.make", json!({"mode": mode.name()}));
} else if members {
crate::menus::invoke(app, "object.compoundShape.setMode", json!({"mode": mode.name()}));
} else {
run(app, ui, op, label);
run(app, op);
}
}
@@ -88,7 +102,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
let count = if modes.is_empty() { n } else { n.max(2) };
for ((icon, tip, op), mode) in SHAPE_MODES.into_iter().zip(ShapeMode::ALL) {
if button(ui, icon, tip, shared == Some(mode), count, 2) {
shape_mode(app, ui, op, tip, mode, !modes.is_empty());
shape_mode(app, ui, op, mode, !modes.is_empty());
}
}
let resp = ui.add_enabled_ui(expand, |ui| widgets::flat_button_fit(ui, "Expand", 90.0)).inner;
@@ -100,9 +114,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 5.0;
for (icon, tip, op) in PATHFINDERS {
let min = if op == "outline" || op == "divide" { 1 } else { 2 };
let min = minimum_operands(op);
if button(ui, icon, tip, false, n, min) {
run(app, ui, op, tip);
run(app, op);
}
}
});
@@ -122,7 +136,8 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
let label = app
.last_pathfinder
.as_ref()
.map(|(_, l)| crate::i18n::fmt(tl!("Repeat {name}"), &[("name", tl!(l))]))
.and_then(|op| operation_label(op))
.map(|label| crate::i18n::fmt(tl!("Repeat {name}"), &[("name", tl!(label))]))
.unwrap_or_else(|| tl!("Repeat Pathfinder").into());
if menu_item(ui, &label, crate::menus::enabled(app, "pathfinder.repeat"), false) {
crate::menus::invoke(app, "pathfinder.repeat", json!({}));
@@ -325,6 +340,11 @@ mod tests {
/// A click on the panel button that screen readers name `label`. egui finds what the pointer
/// is over from the frame before, so the move gets a frame of its own.
fn click_button(app: &mut VectorcraftApp, label: &str) {
click_button_with_modifiers(app, label, egui::Modifiers::NONE);
}
/// Click a panel button with a modifier held across the input frames.
fn click_button_with_modifiers(app: &mut VectorcraftApp, label: &str, modifiers: egui::Modifiers) {
crate::i18n::set_current(crate::i18n::Lang::EN);
let ctx = egui::Context::default();
ctx.enable_accesskit();
@@ -333,6 +353,8 @@ mod tests {
let mut frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
time += 0.1;
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(300.0, 200.0));
let mut events = events;
events.insert(0, egui::Event::ModifiersChanged(modifiers));
let input = egui::RawInput { events, time: Some(time), screen_rect: Some(screen), ..Default::default() };
let mut out = ctx.run_ui(input, |ui| show(app, ui));
out.textures_delta.clear();
@@ -344,8 +366,7 @@ mod tests {
let node = nodes.iter().find(|(_, n)| n.label() == Some(label) && n.role() == egui::accesskit::Role::Button);
let b = node.unwrap_or_else(|| panic!("no button named {label:?}")).1.bounds().expect("bounds");
let at = egui::pos2((b.x0 + b.x1) as f32 * 0.5, (b.y0 + b.y1) as f32 * 0.5);
let button =
|pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE };
let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers };
frame(app, vec![egui::Event::PointerMoved(at)]);
frame(app, vec![button(true)]);
frame(app, vec![button(false)]);
@@ -375,6 +396,7 @@ mod tests {
fn cmd_4_is_repeat_pathfinder() {
use crate::shortcut_editor::{command_for_key, default_command_shortcut};
assert_eq!(default_command_shortcut("pathfinder.repeat"), Some("Cmd+4"));
assert_eq!(default_command_shortcut("effect.applyLast"), Some("Cmd+Shift+E"));
assert_eq!(command_for_key("Cmd+4"), Some("pathfinder.repeat"));
let chord = crate::shortcuts::parse("Cmd+4").expect("parses");
assert_eq!((chord.modifiers, chord.logical_key), (egui::Modifiers::COMMAND, egui::Key::Num4));
@@ -384,6 +406,8 @@ mod tests {
let rows: Vec<_> = listed.as_array().expect("rows").iter().map(|r| (r["label"].clone(), r["default"].clone())).collect();
assert_eq!(rows, [(json!("Repeat Pathfinder"), json!("Cmd+4"))], "Keyboard Shortcuts lists it");
assert_eq!(app.run("shortcuts.conflicts", json!({})).unwrap(), json!([]), "no other default has the key");
assert_eq!(vectorcraft_engine::command_specs().iter().filter(|c| c.shortcut == Some("Cmd+4")).count(), 0);
assert_eq!(crate::menus::UI_COMMANDS.iter().filter(|c| c.2 == "Cmd+4").map(|c| c.0).collect::<Vec<_>>(), ["pathfinder.repeat"]);
}
/// After Unite from the panel, Cmd/Ctrl+4 unites the two squares selected next, and the panel
@@ -404,11 +428,11 @@ mod tests {
assert_eq!(objects(&app), [first, second, ("path", [210.0, 10.0, 290.0, 90.0])], "the menu item merged the third pair");
}
/// Before a button of the panel has run, Repeat Pathfinder is disabled and Cmd/Ctrl+4 leaves
/// the selected squares as they are, with a message in the status bar. After a button has
/// Before a repeatable operation has run, Repeat Pathfinder is disabled and Cmd/Ctrl+4 leaves
/// the selected squares as they are, with a message in the status bar. After an operation has
/// run, the command is disabled again while nothing is selected.
#[test]
fn cmd_4_does_nothing_before_a_panel_button_has_run() {
fn cmd_4_does_nothing_before_an_operation_has_run() {
let mut app = two_squares();
let before = objects(&app);
assert_eq!(before.len(), 2);
@@ -419,5 +443,91 @@ mod tests {
click_button(&mut app, "Unite");
app.run("select.none", json!({})).unwrap();
assert!(!crate::menus::enabled(&app, "pathfinder.repeat"), "nothing selected");
assert!(app.run("pathfinder.repeat", json!({})).is_err());
}
/// One selected path cannot supply the operands for Unite; the shortcut reports the failure.
#[test]
fn repeat_with_one_path_keeps_art_and_history() {
let mut app = two_squares();
click_button(&mut app, "Unite");
let before = objects(&app);
assert_eq!(before.len(), 1);
assert!(!crate::menus::enabled(&app, "pathfinder.repeat"));
press_cmd_4(&mut app);
assert_eq!(objects(&app), before);
assert_eq!(app.ui.status, "Select two or more objects, or a group");
assert_eq!(app.last_pathfinder.as_deref(), Some("unite"));
}
/// A direct command replaces the panel's previous operation and supplies the menu name.
#[test]
fn direct_operation_becomes_the_one_repeated() {
let mut app = two_squares();
click_button(&mut app, "Unite");
select_two_squares(&mut app, 110.0);
app.run("object.pathfinder.intersect", json!({})).unwrap();
assert_eq!(app.last_pathfinder.as_deref(), Some("intersect"));
select_two_squares(&mut app, 210.0);
click_menu_item(&mut app, "Repeat Intersect");
assert_eq!(objects(&app).last(), Some(&("path", [240.0, 40.0, 260.0, 60.0])));
assert_eq!(app.last_pathfinder.as_deref(), Some("intersect"));
}
/// A rejected operation leaves the prior successful operation available to repeat.
#[test]
fn failed_operation_keeps_history() {
let mut app = two_squares();
click_button(&mut app, "Unite");
assert!(app.run("object.pathfinder.intersect", json!({"precision": -1.0})).is_err());
assert_eq!(app.last_pathfinder.as_deref(), Some("unite"));
select_two_squares(&mut app, 110.0);
click_menu_item(&mut app, "Repeat Unite");
assert_eq!(objects(&app).last(), Some(&("path", [110.0, 10.0, 190.0, 90.0])));
}
/// A successful Trap keeps the last repeatable operation in history.
#[test]
fn successful_trap_keeps_history() {
let mut app = two_squares();
click_button(&mut app, "Unite");
let mut rect = |x: f64, color: &str| {
let id = app.run("shape.rectangle", json!({"x": x, "y": 10, "width": 50, "height": 50})).unwrap()["id"].clone();
app.run("paint.setFill", json!({"color": color})).unwrap();
id
};
let (a, b) = (rect(110.0, "#ffff00"), rect(160.0, "#0000ff"));
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("object.pathfinder.trap", json!({"thickness": 2, "tintReduction": 30})).unwrap();
assert_eq!(app.last_pathfinder.as_deref(), Some("unite"));
}
/// Repeat reads the current options instead of replaying the earlier command's settings.
#[test]
fn repeat_uses_current_pathfinder_options() {
let mut app = two_squares();
app.run("object.pathfinder.divide", json!({})).unwrap();
assert_eq!(app.last_pathfinder.as_deref(), Some("divide"));
app.run("object.pathfinder.options", json!({"precision": 1.0})).unwrap();
let a = app.run("shape.rectangle", json!({"x": 0, "y": 0, "width": 100, "height": 100})).unwrap()["id"].clone();
let b = app.run("shape.rectangle", json!({"x": 0, "y": 99.5, "width": 100, "height": 100})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
let id = app.run("pathfinder.repeat", json!({})).unwrap()["ids"][0].as_u64().unwrap();
let group = app.session.active().unwrap().doc.node(NodeId(id)).unwrap();
assert_eq!(group.children().unwrap().len(), 2);
assert_eq!(app.last_pathfinder.as_deref(), Some("divide"));
}
/// Compound-shape creation and member edits leave destructive Pathfinder history alone.
#[test]
fn compound_shape_commands_do_not_set_history() {
let mut app = two_squares();
let a = app.session.active().unwrap().selection.objects[0];
click_button_with_modifiers(&mut app, "Minus Front", egui::Modifiers::ALT);
assert_eq!(app.last_pathfinder, None);
app.run("select.set", json!({"ids": [a.0]})).unwrap();
click_button(&mut app, "Unite");
assert_eq!(app.last_pathfinder, None);
assert!(!crate::menus::enabled(&app, "pathfinder.repeat"));
}
}
+1 -1
View File
@@ -431,7 +431,7 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
| `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. |
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group (`effect.apply` groups several loose selected objects first). In the desktop app, the UI command `pathfinder.repeat` (Repeat Pathfinder, Cmd/Ctrl+4) takes no params and runs the operation a Pathfinder panel button ran last on the selection; before a button has run it returns the error "no Pathfinder operation applied yet". |
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group (`effect.apply` groups several loose selected objects first). In remote desktop mode, `run_command` and `invoke_menu` can both call `pathfinder.repeat` with `{}`. Every successful Shape Mode or Pathfinder operation, including direct `object.pathfinder.*` commands, sets its history. Repeat applies it to the current selection using current Pathfinder Options. Trap, compound-shape edits and live effects do not set history. With no history it returns "no Pathfinder operation applied yet". |
| `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. With `copy`, the first step duplicates. |
| `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | The nine Illustrator graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. An empty CSV cell or a `null` in `rows` is a blank value; a number in straight quotes is a label. Edit later with `graph.setData` / `graph.setType` via `run_command`. `graph.column {design, columnType: vertical\|uniform\|repeating\|sliding, each, fractions: chop\|scale}` draws the columns or bars of the series picked with Group Selection (or `seriesIndexes`, or all of them) with a graph design saved by `graph.design`; with no `design` it returns the series' current design and options. Type in the design that holds a `%` followed by two digits shows each column's value, with at least the first digit's places before the decimal point and the second digit's places after it. |
| `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. |
+1 -1
View File
@@ -615,7 +615,6 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command.
- With the Artboard tool, Cmd/Ctrl+A after a click on a row of the Artboards panel selects every artboard, as the macOS menu bar's Select All does then; the active artboard stays active, and the art's selection does not change. After a press outside the panel's list, Cmd/Ctrl+A selects every object again. It used to select every object after a click in the panel too. Public reference: [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html).
- Cmd/Ctrl+/ (`appearance.addFill`) and Cmd/Ctrl+Alt+/ (`appearance.addStroke`) add a new fill and a new stroke to the selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do, on the keys the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for adding a new fill and a new stroke. Keyboard Shortcuts lists both under Window › Appearance.
- Cmd/Ctrl+4 (`pathfinder.repeat`, Repeat Pathfinder) runs the operation a Pathfinder panel button ran last on the selected objects, as the panel menu's Repeat item does. Until a button has run, the command is disabled and the key leaves the art as it is, with "no Pathfinder operation applied yet" in the status bar.
- Cmd/Ctrl+Alt+Shift+. and , (`type.size.increaseByFive`, `type.size.decreaseByFive`) step the font size of the selected type, or of the text the Type tool has selected, by five times the Size/Leading increment (Preferences › Type); Cmd/Ctrl+Shift+. and , still step it by one increment.
- Cmd/Ctrl+Alt+Q (`type.resetTracking`) sets the tracking of the selected type, or of the text the Type tool has selected, to 0 and its kerning to Auto (0 in the Character panel), in one undo step; at a caret it resets the kerning of the character before it.
- Cmd/Ctrl+Alt+K (`type.highlightKerning`) and Cmd/Ctrl+Alt+Shift+F (`type.highlightFontName`) show the Character panel and give its Kerning field the keyboard focus with the value selected, or open its font family menu with the search field focused, on the keys the reference app's [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page lists for highlighting kerning and the font name field. The page lists the same key for selecting the tracking value; `type.highlightTracking` focuses the Tracking field the same way and has no default key in Keyboard Shortcuts.
@@ -795,6 +794,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Extrude & Bevel, Inflate and Rotate (Effect › 3D and Materials): live effects in the `three-d` crate with depth, twist, taper, solid or hollow caps, five original bevel shapes (width, height, repeat, spacing, inside, both sides), Inflate's volume, both sides, twist, taper and cap, Rotate's turned fills and strokes, Position presets, the rotation and light gizmos, SVG/PDF vector output and Expand Appearance; one 3D effect per object; covered by geometry, effect, canvas-versus-export, command and dialog tests ([three-d.md](three-d.md)).
- Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends have continuous caps by default, shading is smooth with one to ten colored lights, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)).
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). On the canvas, in Expand Appearance and in exports, Effect → Pathfinder uses the glyph outlines of type in the group, and results painted like the type keep its opacity and blend mode.
- Cmd/Ctrl+4 (`pathfinder.repeat`) repeats the last successful Shape Mode or Pathfinder operation, including direct `object.pathfinder.*` commands, on the current selection; the panel menu names it. Trap, compound-shape edits and live effects do not set history. Public references: [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), [Pathfinder panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html).
- Pathfinder Options ([Pathfinder panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html)): the panel menu's Pathfinder Options… sets Precision (0.001–10 pt: results refit to within it, thinner pieces dropped), Remove Redundant Points and Divide and Outline Will Remove Unpainted Artwork (`object.pathfinder.options`, kept in the preferences) for every Shape Mode and Pathfinder operation; the defaults keep the earlier output (`pathops` `precision_drops_pieces_thinner_than_it`, engine `tests_pathops` Pathfinder Options tests, `panels::pathfinder` dialog test).
- Pathfinder › Trap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html), [Pathfinder panel](https://helpx.adobe.com/illustrator/using/pathfinder-panel-overview.html)): the Pathfinder panel menu's Trap… (`object.pathfinder.trap`) and Effect › Pathfinder › Trap… (`pathfinder.trap`, live on groups) add, where two objects' visible areas of different solid colours meet, a strip of the darker one's area along the edge, filled with the lighter colour at the Tint Reduction percentage and set to overprint (Reverse Traps: the darker colour spreads instead, but not a rich black); Thickness 0.01–5000 pt across vertical edges, Height/Width scales it across horizontal ones, Traps with Process Color paints a swatch colour's trap with its CMYK equivalent; the effect also has Precision and Remove Redundant Points. The panel's Trap is one undo step and groups the traps above the topmost selected object, selected; with no adjoining edges of different colours it says so and changes nothing. Tests: `crates/pathops/src/trap.rs`, `crates/effects/src/trap.rs`, `group::tests::trap_effect_adds_an_overprinting_strip_above_the_members`, `crates/engine/src/tests/trap.rs`, `panels::pathfinder::tests::trap_opens_its_options_and_ok_traps_the_selection`.
- Compound shapes ([Create compound shapes with Pathfinder](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-shapes-with-pathfinder.html), [Pathfinder panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html)): the Pathfinder panel menu's Make Compound Shape (every member in Add) and Alt-clicking a Shape Mode (every member in that mode) make the two or more selected paths, compound paths, groups, compound shapes or type a live compound shape (`object.compoundShape.make`): a group whose members each keep a shape mode and stay editable, drawn, printed and exported (SVG, PDF and the rest through the baked document) as the area the modes make from the backmost member up, open paths closed, painted with its own appearance, taken from the topmost member not subtracting. With members selected (Direct Selection, Layers panel, isolation mode) a Shape Mode button sets theirs (`object.compoundShape.setMode`) and is lit for the mode they share; paint given to a member goes to the compound shape. Release Compound Shape gives the members back as they were (`object.compoundShape.release`); Expand Compound Shape and the panel's Expand button turn it into the path or compound path of its area (`object.compoundShape.expand`). Shape Modes and Pathfinders take a compound shape as its area. Saved files keep it (`compound_shape`, `shape_mode`; an unknown mode reads as Add). Tests: `crates/engine/src/tests/compound_shape.rs`, `group::tests::compound_shapes_draw_the_area_their_shape_modes_make`, `group::tests::nested_compound_shapes_take_part_as_their_area`, `panels::pathfinder::tests::compound_shape_items_follow_the_selection`, the `CompoundShape` op of the property-test strategies. Not yet: blends, envelopes and warps as members, the question marks on mixed member modes, hit-testing by the area (a click hits the members).
+2 -3
View File
@@ -244,9 +244,7 @@ tools, never checked side by side) isn't captured row by row.
## Keyboard shortcuts
Cmd/Ctrl+/ (`appearance.addFill`) and Cmd/Ctrl+Alt+/ (`appearance.addStroke`) add a new fill and a new stroke to the
selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do. Cmd/Ctrl+4
(`pathfinder.repeat`, Repeat Pathfinder) runs the operation a Pathfinder panel button ran last on the selected
objects, as the panel menu's Repeat item does.
selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do.
~110 distinct modified shortcuts on menu commands plus single-key tool shortcuts for the default toolbar
(`crates/tools/src/catalog.rs`), a shortcut editor with chords, and saved sets. Cmd/Ctrl+Alt+L (`ui.newLayer`) opens Layer Options for a new layer, as an Alt-click on the Layers panel's New Layer button does, and Cmd/Ctrl+L still adds a layer without the dialog. F8 (`ui.symbolOptions`) opens Symbol Options for a new symbol made from the selection, as the Symbols panel's New Symbol button does. Not yet diffed item by item
@@ -308,6 +306,7 @@ next unit in the order a right-click on a ruler lists them, from Feet back to Po
| Pathfinder panel on one group selected alone; a greyed-out button says what it needs | partial | #1450; the group's members are combined; one plain path selected alone stays disabled (the public documentation doesn't say what the operations do to a single path) |
| Pathfinder Divide: an open cutting line splits a filled path | done | #1307; straight, crossed, bent and curved cutters, holes and frontmost appearance regression-tested; native automation: independent piece movement, one Undo/Redo, saved-document round trip and window capture; existing bounded-map fallback remains |
| Pathfinder panel › Pathfinder Options…: Precision, Remove Redundant Points, Divide and Outline Will Remove Unpainted Artwork | done | #3426; `object.pathfinder.options`, kept in the preferences; defaults (0.01 pt, both off) keep the earlier results; [spec](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html) |
| Cmd/Ctrl+4 repeats the last successful Shape Mode or Pathfinder operation on the selection | done | Direct `object.pathfinder.*` commands set history too; Trap, compound-shape edits and live effects do not. Repeat uses current Pathfinder Options; [key spec](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html), [menu spec](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/pathfinder-panel-overview.html) |
| Pathfinder panel: compound shapes (Make, Release and Expand Compound Shape in the panel menu, Alt-click a Shape Mode, a Shape Mode for selected members, the Expand button) | partial | #4522; `object.compoundShape.make`/`setMode`/`release`/`expand`; members stay editable, the area draws, exports and saves; the button of the members' shared mode is lit; [spec](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/create-compound-shapes-with-pathfinder.html); missing: blends, envelopes and warps as members, question marks for mixed modes, hit-testing by the area |
| Canvas context menu | done | Layers rows, panels and type have none (gap) |
| SVG Interactivity panel: JavaScript for each object's events and the document's linked JavaScript files, written to SVG and read back as text | done | `svgInteractivity.*`, `ui.javascriptFilesDialog` |