# VectorCraft MCP server `vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0; see [Protocol](#protocol) for version negotiation). Agents use it to draw, inspect and look at VectorCraft documents. It has two backends: | Mode | How | What works | |---|---|---| | **Remote** | `vectorcraft-cli mcp --connect 127.0.0.1:7979` (the app must run with `vectorcraft --control 7979`) | Everything. Tool calls are forwarded over the [control protocol](control-protocol.md), so you watch the app change live. | | **Headless** | `vectorcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui`, `type_text`, `open_panel`, `screenshot {window:true}`). | With no flag, the server tries `127.0.0.1:7979` and falls back to headless. Logs go to stderr; stdout carries only protocol messages. ## Install and register From the repository root, with Rust 1.95+ and the platform's source-build prerequisites installed: ```sh cargo install --path apps/vectorcraft-cli --locked # POSIX shells, for Cargo's default install location: export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH" vectorcraft-cli --help claude mcp add vectorcraft -- vectorcraft-cli mcp # or pin a mode: claude mcp add vectorcraft-headless -- vectorcraft-cli mcp --headless claude mcp add vectorcraft-app -- vectorcraft-cli mcp --connect 127.0.0.1:7979 ``` Cargo installs a binary, not a symlink into `target/`, so it survives `cargo clean`. Re-run the install command after updating the checkout. By default the executable goes in `$CARGO_HOME/bin` (`~/.cargo/bin` when unset), or `%USERPROFILE%\.cargo\bin` on Windows. If you set `--root`, `CARGO_INSTALL_ROOT`, or Cargo's `install.root`, use that installation root's `bin` directory instead. Add it to the `PATH` inherited by the MCP client, not just an interactive shell, and restart the client after changing its environment. Installing does not start or register a server. Other clients use the same command in their JSON config: ```json {"mcpServers": {"vectorcraft": {"command": "vectorcraft-cli", "args": ["mcp"]}}} ``` If a client reports `ENOENT` or "Executable not found in $PATH", verify the install with `command -v vectorcraft-cli` (PowerShell: `Get-Command vectorcraft-cli`) in the client's environment. If that environment cannot be changed, set JSON `command` to the absolute installed binary path, not a literal `~` or `$HOME` expression. Avoid symlinks into `target/release`: cleaning the build directory breaks them. A source build alone does not put the CLI on `PATH`. For a live session, start the app first: `cargo run --release -p vectorcraft -- --control 7979`. ### From an installed release The release packages ship `vectorcraft-cli` alongside the desktop app, so no build is needed: | Install | CLI | |---|---| | Windows (MSI) | `C:\Program Files\VectorCraft\vectorcraft-cli.exe` by default (wherever you installed it otherwise), not on `PATH` | | Linux (deb, rpm) | `/usr/bin/vectorcraft-cli` | | macOS | the separate `vectorcraft-cli--macos-.zip` release asset (the `.app` holds only the desktop app) | ```sh # Windows, default install folder claude mcp add vectorcraft -- "C:\Program Files\VectorCraft\vectorcraft-cli.exe" mcp # Linux, or macOS with the CLI unzipped onto PATH claude mcp add vectorcraft -- vectorcraft-cli mcp ``` ## Confining file access ```sh vectorcraft-cli mcp --automation-read-root /work/project --automation-write-root /work/project/out vectorcraft-cli mcp --automation-read-root /work/project --automation-read-root /work/shared-assets \ --automation-write-root /work/project/out --automation-write-root /tmp/scratch ``` ```json {"mcpServers": {"vectorcraft": {"command": "vectorcraft-cli", "args": ["mcp", "--automation-read-root", "/work/project", "--automation-write-root", "/work/project/out"]}}} ``` The two flags (PhotoCraft and the other Craft apps take the same ones, so one client config works across the suite) keep the agent to the folders you give it: - Every file a command reads must lie inside a **read root**: `open_file`, `file.place`, relinking and updating links, the images an SVG links to, swatch library, graphic style library, symbol library and colour-profile loads, plug-ins, `file.newFromTemplate`, preset imports, shortcut sets (`shortcutSet.read`), the folders `text.findFontFiles` and `links.findFiles` search, the scripts `script.check` reads and the files they include. - Every file a command writes must lie inside a **write root**: `save_file`, `export`, the save and export commands, Export for Screens, Package, Export for Web, print to file, `screenshot {path}`, library and preset saves to a path, the copies `links.relink {copy}` makes. - `file.list {path}` (through `run_command`) lists a folder's files and folders, held to the read roots like any read, so an agent can see what it was given (#1163). - Each flag may be given more than once (#3546): a path inside any of the read roots may be read, one inside any of the write roots written, so one session can read a project folder and a shared assets folder, or write to an output folder and a scratch folder, without widening a root to their common parent. A folder given twice counts once. - Read and write access are separate. Give the same folder to both flags for both; a kind left out grants none of its access (with only `--automation-read-root`, nothing can be written to disk, though `export` and the save commands without a path still return the bytes). Without either flag nothing changes. - A path is made absolute against the server's working directory, then its links are followed: a file (or folder) link that leads out of a root is outside it. For a file that doesn't exist yet, its nearest existing folder's links are followed and the rest must be plain names, so `..` below a folder that doesn't exist is refused, as is a link that leads nowhere (writing through it would create its target). The result is compared with the root by whole folder names: `/work2/x` is not inside `/work`. On Windows case is ignored, UNC paths (`\\server\share\…`) are resolved like drive-letter ones, and device names (`CON`, `NUL`, `COM1`…) are refused. - A refused path is an ordinary tool error the agent can read and correct, for example `automation path rejected: outside the write root /work/project/out: /etc/x.png` (with several, `outside the write roots /work/project/out, /tmp/scratch: …`) or `automation filesystem access is not granted: write authority is absent: /work/project/out/x.png`. Links in an opened document that point outside the read roots show as missing. - The preferences that move folders the app reads or writes on its own (`fontsFolder`, `pluginsFolder`, `recoveryFolder`, `templatesFolder`) can only be set inside the roots. The app's own folders (preferences, Data Recovery, the User Defined library folders, VectorCraft's Fonts folder) are not confined: no agent names a path there. - The roots must be existing folders; a missing one stops the server with an error. Each flag takes one folder per use (`--flag ` or `--flag=`) and may be used again for more. The flags confine the headless server and imply `--headless`; `--connect` with them is an error, since the server would otherwise talk to an app that isn't confined. A running app confines itself: start it with the same flags, `vectorcraft --control 7979 --automation-read-root --automation-write-root `, and connect as usual (see [Control protocol](control-protocol.md#confining-file-access)). The server then writes nothing itself: `screenshot {path}` asks the app to write the file. The checks hold for every thread of the server process. A path is checked, then opened: another program that swaps a link in between can still win that race, so the roots keep an agent to the files it was given rather than guarding against other software on the computer. ## Protocol Newline-delimited JSON-RPC 2.0 on stdio. The revision is **`2025-06-18`**; `2025-03-26` and `2024-11-05` are accepted too. Every request and notification must carry `"jsonrpc": "2.0"`: anything else is refused with `-32600` (requests) or ignored (notifications, which have no id to address a reply to) before it is dispatched. When a tool call reaches a running app, it is one control-channel request, and the app echoes its `id` in the reply: a reply with any other `id` is rejected, the connection dropped, and the next call starts a fresh one — the call reports uncertainty like any reply that never arrived whole. A malformed JSON reply also drops the connection and reports that uncertainty; the failed operation is never replayed automatically. The app's refusals carry no `id` (a connection past its 16, a line it would not read): the call reports the app's reason and says nothing was run. Before each call the server checks that the app still holds the connection and opens a new one when it doesn't (the app closes a connection left idle for 5 minutes, and a restarted app is a new one), so a request is never written to a closed socket; when the app can't be reached, the call says that nothing was sent. Clients requesting `2026-07-28` through `initialize`, or through a request's `_meta["io.modelcontextprotocol/protocolVersion"]`, receive `resultType: "complete"`, `ttlMs` and `cacheScope: "private"` on tool/resource/template/prompt lists and resource/prompt reads. List TTL is 600000 ms; live reads use zero. Older clients keep the original response shapes. This does not add streaming or input requests — see [Not implemented](#not-implemented). One message is one line of at most 16 MiB: a longer line is answered with `-32600` and skipped, and the server keeps serving what follows. A batch holds at most 64 messages. `command_batch` holds at most 256 steps, and the step results it keeps (for later steps' `"$N.path"` references and the reply) total at most 8 MiB — past that the batch stops with the reason, closing its atomic undo group as usual. Replies from a connected app are refused past 256 MiB and the connection dropped; the call reports uncertainty like any reply that never arrived whole. `initialize` advertises: | Capability | What it covers | |---|---| | `tools` | The tools below | | `resources` | Three fixed resources and four templates | | `prompts` | Five ready-made workflows | | `completions` | `completion/complete` for prompt arguments and template variables | | `logging` | `logging/setLevel` and `notifications/message` | ### Prompts `prompts/list` offers five workflows; `prompts/get` returns one templated user message with its arguments filled in. A missing required argument is `-32602`. | Prompt | Arguments (required first) | |---|---| | `poster` | `brief`, `palette`, `text` | | `icon-set` | `subject`, `count`, `detail` | | `recolor` | `palette`, `method` | | `trace-and-style` | `path`, `preset`, `effect` | | `export-set` | `formats`, `directory`, `scale` | ```sh echo '{"jsonrpc":"2.0","id":1,"method":"prompts/get","params":{"name":"poster","arguments":{"brief":"a jazz festival"}}}' \ | vectorcraft-cli mcp --headless ``` ### Resource templates `vectorcraft://document` (summary), `vectorcraft://document/json` (the whole model), and `vectorcraft://commands` (the live command catalog with enabled state) are the fixed resources. The templates read one thing at a time, which matters on a real document: the full model is thousands of lines an agent pays for again on every change. | `uriTemplate` | Reads | |---|---| | `vectorcraft://object/{id}` | One layer or object with its children, bounds and paint: `document.node {id, summary: true}`, the node as `document.inspect` lists it. A large container can be sliced with `run_command document.node {id, summary: true, depth?, childLimit?}` (a level that shows fewer children reports `childCount`) | | `vectorcraft://command/{id}` | One command: label, menu path, shortcut, parameter description, enablement | | `vectorcraft://effect/{id}` | One live effect with its parameters and defaults | | `vectorcraft://swatch/{name}` | One swatch, colour, gradient or pattern swatch (percent-encode spaces) | An unknown URI is `-32002`; a template with no value, or a value that names nothing, is `-32602`. The same reads work as query commands through `run_command`: `document.node {id}` returns the object's full model JSON (geometry, appearance, every attribute) and `document.node {id, summary: true}` the compact summary `document.inspect` gives for it (id, name, kind, bounds, paint labels, children), without summarizing the whole document. A summary read slices a large container: `depth` is how many child levels it includes (`0`: the node alone) and `childLimit` how many children each node shows, top of the stack first (both default to all). A level that shows fewer children than it has reports their total as `childCount`, so truncation is never silent; with nothing truncated the reply is the plain summary. Both must be non-negative integers and need `summary: true` (the full object JSON is never truncated); anything else is an error. In the summary an object reports its paint as `fill`, `stroke` (labels) and `strokeWidth`. So does a compound path, whose paint is its own: its member paths report `compoundMember: true` instead of a paint they don't have. Every node (layers and groups too) reports `opacity` when it is below 1 and `blend` when it isn't Normal, as the label `object.setProps {blend}` takes (`"Multiply"`, `"Color Burn"`). On a large document, drill instead of dumping: `inspect_document {depth: 0}` (or `run_command document.inspect {depth?, childLimit?}`, same rules) is the skeleton (artboards with top layers and counts), `run_command document.find {name?, kind?, text?, limit?}` locates nodes across the whole tree, and `document.node` reads the interesting ones sliced. `document.inspect` and the `doc_inspect` / `inspect_document` tools also scope the reply themselves: `ids: [id…]` lists just those nodes (any level, in the order given, an unknown id is an error, at most 10000), `kind` lists every node of one panel label (ignoring case; within the subtrees of `ids` when both are given) with its ancestor `path`, and `limit` / `offset` page the list. A scoped reply carries the nodes as `nodes` instead of `layers` (each the summary the full tree gives it, sliced by `depth` / `childLimit`) plus `total`, the entries before paging; artboards, selection and history stay whole. `limit` / `offset` alone page the top layers (`layers` and `total`). Without these options the reply is the whole layer tree as before. `document.find` needs at least one filter and all given filters must match; matching ignores case, `name` is a substring of the Layers panel name, `text` a substring of type content and `kind` the exact panel label (`Layer`, `Group`, `Clip Group`, `Path`, `Compound Path`, `Type`, `Image`, `Rectangle`, ...). It answers `{matches: [{id, name, kind, path}], total}` top of the stack first, where `path` is the ancestor chain (layer first) and `limit` caps `matches` (default 100; `0` only counts). `document.json` stays whole: it is the fidelity path, not the way to look around. ### Completions `completion/complete` answers for a prompt argument (`ref/prompt`) or a template variable (`ref/resource`), ranked by prefix and then by substring, capped at 100 values with `total` and `hasMore`. The values are read from the live catalogues, so they follow the app: `formats` suggests what the engine writes, `preset` the Image Trace presets, `effect` the effect ids, `{id}` the layer and object ids in the document, `{name}` its swatch names. An unknown reference comes back empty rather than as an error, since a completion is asked for mid-typing. ### Logging `logging/setLevel` accepts any RFC 5424 severity — `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency` — and an unknown one is `-32602` with the previous level left in place. From then on the engine's own `log` records arrive as `notifications/message` with `level`, `logger` and `data`. A `null` level turns it back off. Nothing is sent before the client asks, and records queued at the old level are dropped rather than replayed at the new one. Installing the logger that receives those records is `vectorcraft-cli`'s job, not the library's: a crate that installed one would override whatever logger an embedder had already set up. Embedding `vectorcraft-mcp` directly means calling `vectorcraft_mcp::logging::install()` yourself if you want log records to reach your client. ### Long calls: cancellation, progress and a busy engine A call runs to the end before its reply, and the engine runs one call at a time, in the order the lines arrived. A thread of its own reads the stream meanwhile, so the server still hears the client while an export runs (#2165, #2171, #2172): - **Cancellation.** `notifications/cancelled {requestId}` ([spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/cancellation)) takes a request out of the queue, or stops the one that is running, and **that request gets no reply**. A running export, flatten, bake or render notices at its next checkpoint (between pages and artboards, between flattened groups, while drawing objects), returns, and writes nothing: the document is exactly as it was and the engine is free for the next call. An unknown, finished or malformed `requestId` is ignored, and so is a cancel of `initialize`. A call without checkpoints (every ordinary edit is over in milliseconds) runs to its end, but its reply is dropped all the same. Against a running app (`--connect`) the server stops waiting and drops the connection, but the app cannot be told to stop what it started. - **Progress.** A request with `_meta.progressToken` gets `notifications/progress` ([spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/utilities/progress)) before its reply: the PDF writer reports "Writing page 2 of 5" with `progress` 2 and `total` 5, raster export reports each artboard, `document.exportForScreens` and `assets.export` report each file ("Exporting file 3 of 250", in place of their encoders' steps), and the PDF flattening stage reports a message without a total. `progress` only grows, updates come at most every 200 ms (the last page always), and none follows the reply. A client that does not read fast enough misses updates, never the reply. - **A busy engine.** `ping`, `tools/list`, `resources/list`, `resources/templates/list`, `prompts/list` and `prompts/get` need no engine and are answered at once, whatever is running. Anything else that arrives while a call has already run for two seconds is not queued behind it: it is answered with error `-32003` (`data.busy` names the running call, its request id and how long it has run) and was **not run**, so the client can retry later or cancel the running call. Requests that arrive sooner than that queue and are answered in order, as before, so a client that sends a few quick calls together sees no difference. At most 64 lines wait; past that the server stops reading until the engine catches up. `vectorcraft_mcp::Server::serve` takes the input and the output on separate threads (`Send`), and `Server::with_busy_after` changes the two seconds. Calling `handle_line` directly is as before: one line in, one reply out, no threads. ### Not implemented - **`resources/subscribe` / `notifications/resources/updated`.** It would also have to be honest about *whose* edits it sees: comparing the undo history's length misses edits made in the app window, which is the main reason to run against a live document. `document.inspect`'s `revision` is the right signal — it is monotonic and already bumped when a document is replaced — but noticing an edit still means someone asking. - **Reading the document during a long call.** Requests that need the engine (`doc_inspect`, `resources/read`, `command_list`) are refused as busy while an export runs, not answered from a snapshot. - **`sampling/createMessage` and `elicitation/create`.** Both are server→client requests. Supporting them means being able to suspend a tool call mid-flight, which is what the `2026-07-28` revision's input-requests mechanism exists for. - **Streamable HTTP / SSE.** stdio only. `completion/complete` covers the "what could I pass here" half of the same problem, which is why it is here and subscriptions are not. ## Tools ### Common command tools These names match the conventions in [FilmCraft #28](https://github.com/storytold/filmcraft/pull/28). The older names below remain listed because existing workflows use them. | Tool | Arguments and result | |---|---| | `command_list` | Optional `filter` (or `query`), `menu` (a menu path substring, levels joined with ` > `), `enabled_only`, `brief` (no `params` text); returns the command array | | `command_run` | `id`, optional `params`; runs the command through the existing backend. An unknown key in `params` is an error that names it and the nearest key the command takes (`invalid parameters for `file.new`: unknown parameter `widht`; did you mean `width`?`), so a misspelt key no longer runs the command with its default (see [Parameter keys](#parameter-keys)) | | `command_batch` | `steps: [{id, params?}]` (at most 256), optional `stop_on_error` (default true), `atomic` and `label`; returns `completed`, `failed`, `results: [{ok, result\|error}]` and `created` (the ids the `draw_path` / `draw_shape` steps made, in order). A step's `id` is a command id, or `draw_path` / `draw_shape` with that tool's arguments as `params` (checked as `tools/call` checks them; the step's result is the tool's, so `"$N.id"` is the new object). Each edit has its own undo step, or with `atomic: true` the batch's edits are one undo step named `label` (else after its first edit; `edit.undoGroup.begin`/`end` underneath); failures set `isError`. A param string `"$N.path"` or `"$last.path"` takes a value from an earlier step's result (see `run` below). Kept step results total at most 8 MiB; past that the batch stops with the reason. This is the MCP tool, not the engine command `command.batch` (that one takes `commands: [{command, params}]` and is what the Actions panel plays); use `command_run` with `command.batch` when replaying a recorded action | | `doc_inspect` | Optional `depth`, `childLimit`, `ids`, `kind`, `limit`, `offset`; same summary as `inspect_document` | | `render_preview` | Optional `max_side` (1–4096, default 1024) and `artboard` (0-based, default 0); inline PNG of that artboard without editing it. Artboards too large to render within the allocation bound return a tool error | | `ui_inspect`, `ui_screenshot` | Connected desktop state/window capture; tool errors in headless mode | Every tool declares read-only, destructive, idempotent and open-world hints. File-writing `screenshot`, `save_file` and `export` are conservatively marked as mutations. Unknown top-level argument keys return JSON-RPC `-32602`; command failures and caught tool panics return `isError` content, and the server continues serving. Command `params` are passed through: the registry currently describes them in prose, so MCP does not guess schemas or silently remove keys. Coordinates are points in document space: y points down, the origin is the first artboard's top-left, and a new document is 612 × 792 (US Letter). New objects become the selection. Most commands act on the selection or on explicit `ids`. A command that leaves out an object selected inside a selected group also leaves it out when `ids` list it together with a group that holds it. Paint values (`fill`, `stroke`) accept `"#rrggbb"`, `"none"`, `[r,g,b]` (0..1), `{"c","m","y","k"}`, `{"gray"}`, or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name"}`). For `draw_shape` and `draw_path`, omitted paint fields inherit the current [new-art appearance](#new-art). Both also style the new object in the same call: `dash` (lengths, or null for solid), `cap` (butt|round|square) and `join` (miter|round|bevel) go to `stroke.set` with `strokeWidth`; `opacity` (0–100 %) and `name` to `object.setProps`; `effects: [{effect, params?}]` are applied in order as `apply_effect` does, to the whole object. They are checked before anything is drawn (effect ids against `effect.list`), so a bad one draws nothing. The object and its styling are one `command.batch`, so the call is one undo step (named as the drawing command names its own, e.g. Pen or Rectangle) and a paint the engine refuses (a colour it can't read, a swatch the document lacks) fails the call and leaves the document as it was. The reply adds `name`, `opacity` and `effects` (each `effect.apply` result) when given; `paint` lists the stroke options applied. A gradient given to several objects fits each one's own bounds; `paint.setFill` / `paint.setStroke` with `span: "selection"` place a linear or radial gradient once on the union of the targets' bounds instead, so one ramp runs across them. For `draw_shape` and `draw_path`, omitted paint fields inherit the current [new-art appearance](#new-art). For a filled shape or path without an outline, pass an explicit `fill` and `stroke: "none"`. Lines start with no fill and a visible stroke. The Fill/Stroke proxy commands run through `run_command`: `paint.invert` and `paint.complement` recolour the active proxy keeping each colour's model, `paint.lastColor` / `paint.lastGradient` re-apply the last solid colour or gradient, and `paint.recent` returns the recent colours that every paint command (and the eyedropper) feeds. `paint.proxies` returns what the proxies show: the fill and stroke, which one is active, and whether the selected objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" proxy). | Tool | Arguments | Notes | |---|---|---| | `list_commands` | `{filter?, query?, menu?, enabledOnly?, brief?}` | The command catalogue: id, label, menu, shortcut, params doc (left out with `brief`), enablement. | | `run_command` | `{command, params?}` | Runs any command. Use it for everything without a dedicated tool. | | `inspect_document` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | Artboards (with `color`, `#rrggbb`, for one with a background color), layer tree (ids, kinds, bounds, paint), selection, history, tool. A type node's `fill`, `stroke` and `strokeWidth` are the paint its characters show (its first run's); fills and strokes of the type object itself come as `objectFill`, `objectStroke` and `objectStrokeWidth`. Paint comes as a label (`"#ff0000"`, `"None"`, `"G1 40% (#ff9999)"` for a swatch at a tint); `paint.proxies` gives it as an object. | | `inspect_ui` | `{}` | UI state. Remote mode only. | | `select_tool` | `{tool}` | `selection`, `directSelection`, `pen`, `rectangle`, `ellipse`, `polygon`, `star`, `lineSegment`, … | | `pointer_gesture` | `{events:[{kind,x,y,mods?}], tool?, mods?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. Events go through the same path as the mouse. | | `draw_path` | `{points \| d, closed?, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `points` is `[[x,y],…]` or `[{x,y,in?,out?,smooth?},…]`. `d` is SVG path data. The options are below. | | `draw_shape` | `{shape, …geometry, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `rectangle`/`ellipse`: `x,y,width,height` (plus `radius` for corners). `polygon`: `cx,cy,radius,sides`. `star`: `cx,cy,radius1,radius2,points`. `line`: `x1,y1,x2,y2`. | | `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. | | `press_key` | `{key, mods?}` | Remote: a real key event. Headless: runs the command or tool bound to that shortcut, or sends the key to the busy tool (digits too: `5` while dragging with the Perspective Selection tool; `e` while dragging a Selection or Group Selection marquee). | | `type_text` | `{text}` | Remote only. | | `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. | | `open_panel` | `{panel}` | Remote only. `panel` is a panel id (`layers`, `swatches`, `colorGuide`, …, as `window.panel` takes) or its display label (`"Color Guide"`), in any case. | | `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). With `path` the backend (the headless session or the app) writes the PNG there too. | | `open_file` | `{path}` | Opens any readable file as a new active document: `.vectorcraft`/`.drawcraft`, `.vctemplate`, `.svg`/`.svgz`, `.pdf`/`.ai`, `.ait`, `.eps`/`.epsf`, `.ps`, `.dxf`, `.emf`, `.wmf`, PNG, JPEG, GIF, WebP, TIFF, BMP, Targa (`.tga`/`.vda`/`.icb`/`.vst`), PSD/PSB (an image opens as a document of its pixel size; a Photoshop file as its merged image). Templates (`.vctemplate`, `.ait`) open as a new untitled document. PDF, `.ai` and SVG files saved with Preserve Editing reopen as the document they carry. The reply is `document.open`'s: its `warnings` say what didn't come in as it was (an EPS whose PostScript can't be read opens as its preview, and the warning names the PostScript error, the operator and the procedure). `run_command document.formats` lists the formats. | | `save_file` | `{path?}` | Runs `document.save`: the document's own file in its own format (native `.vectorcraft` unless it was opened from or saved as SVG, PDF or a restorable `.ai`; then `warnings` say what that format loses). A path's extension picks the format (`.vectorcraft`, `.vctemplate`, `.pdf`, `.svg`, `.svgz`, `.ai`: a PDF carrying the native document, which reopens editable). | | `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `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 writes one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based); the other formats write one artboard. `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. | | `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, SVGZ and EPS write one file per chosen artboard. 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`, `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?}` | 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. 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). | | `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. | | `undo` / `redo` | `{}` | Many calls that change nothing record no undo step, for example `object.align` with nothing to move, `object.move` by 0 without `copy` and `object.showAll` with nothing hidden. `undo` (`edit.undo`) after such a call undoes the step before it. `inspect_document` lists the undo steps as `history`. `run_command` with `edit.clearHistory` removes every undo and redo step of the active document and returns `{cleared}`, the number of steps removed; the document stays as it is, and the command records no undo step. | For example, create editable Japanese area type with the engine's vertical writing mode: ```json {"name":"add_text","arguments":{"text":"こんにちは","x":40,"y":40,"width":160,"height":240,"size":20,"vertical":true}} ``` `vertical` also applies to point type and to `path` + `mode` (`area` or `onPath`). Omit it or pass `false` for horizontal writing. This is the same option as `text.create` / `text.createInPath`; it does not change the engine's existing glyph orientation or CJK composition. For SVG/SVGZ, `export`'s top-level `outlineText: true` writes text as paths; DXF takes it too (its type is outlined anyway unless `preserve: editability`). PDF instead uses `options: {"advanced": {"outlineText": true}}`; its default is false (live text). With `mods.shift`, a marquee dragged with the Selection tool toggles the objects it reaches (the selected ones leave the selection, the others join it, the rest stays), and one dragged with Direct or Group Selection toggles the anchors inside it (a path left with none leaves the selection, one with all of them is selected whole). The Lasso adds anchors with `mods.shift` and takes them away with `mods.alt`. They run `select.toggle {ids}` and `select.anchorsMany {items, mode: "set"|"add"|"toggle"|"subtract"}`: ```json {"name":"pointer_gesture","arguments":{"tool":"selection","mods":{"shift":true},"events":[{"kind":"down","x":180,"y":80},{"kind":"drag","x":300,"y":200},{"kind":"up","x":380,"y":200}]}} ``` `press_key {key: "e"}` while a Selection or Group Selection marquee is being dragged makes the marquee select only the objects that lie entirely inside it, measured by their geometric bounds, and `e` pressed again switches it back to the default mode. The Selection tool selects a group only when the whole group lies inside; Group Selection selects the objects inside groups, each as a whole object. Both run `select.set {ids}` (`select.toggle {ids}` with `mods.shift`). The marquee also selects the artboard guides that lie entirely inside it, and no canvas guide. Each marquee starts in the default mode. To press the key during a drag, end one `pointer_gesture` after the `drag` event and send the rest in another with the same `tool`, which keeps the drag: ```json {"name":"pointer_gesture","arguments":{"tool":"selection","events":[{"kind":"down","x":80,"y":80},{"kind":"drag","x":220,"y":170}]}} {"name":"press_key","arguments":{"key":"e"}} {"name":"pointer_gesture","arguments":{"tool":"selection","events":[{"kind":"drag","x":230,"y":170},{"kind":"up","x":230,"y":170}]}} ``` With nothing being dragged, `e` chooses the tool bound to E. Appearance stacks: an object can carry several fills and strokes (`appearance.addFill`, `appearance.addStroke`), indexed in paint order (0 is painted first, the bottom row of the Appearance panel). Cmd+/ runs `appearance.addFill` and Cmd+Alt+/ runs `appearance.addStroke` (`press_key {key: "/", mods: {cmd: true, alt: true}}`). `paint.setFill`, `paint.setStroke`, `stroke.set`, `stroke.setAdvanced`, `paint.editGradient`, `paint.setGradientGeom` and `transparency.set` take `item` to edit one of them; `run_command appearance.setActiveItem {"index": n}` makes that row the target of later calls that omit `item` (as clicking the row in the Appearance panel does) until the selection changes; the proxy of its kind (`paint.proxies`) and the Gradient tool's annotator then show that row. `inspect_document` reports it as `paint.appearanceItem`. Live effects take the same `item` to apply to one fill or stroke instead of the whole object: `run_command effect.apply {"effect": "path.offsetPath", "item": 0}`, and `effect.remove`, `effect.setParams` (`visible` toggles one) and `effect.duplicate` address that item's effects; `apply_effect` uses the active item. `effect.list` reports each object's item effects under `applied[].items`. `effect.apply {"effect": "path.outlineObject"}` (Effect › Path › Outline Object) takes no parameters. Type that carries it is drawn, exported and expanded as its glyph outlines and stays editable type, and the effects after it reshape those outlines. On paths, compound paths and images it does not change what is drawn. Errors (unknown tool, bad arguments, a disabled or failing command) come back as a normal result with `isError: true` and a message. The model can read the message and retry. Swatches go through `run_command`: `swatch.list` lists every swatch with its colour group, kind and colour; `swatch.edit {name, newName?, color?, mode?, global?, spot?}` edits one (fills, strokes and text linked to a global or spot swatch follow its colour and name, as one undo step); `swatch.delete {names, unlink?}` deletes swatches and colour groups in one undo step (art using a deleted global swatch keeps its colour, unlinked). `swatch.new {color?, mode?, spot?, global?, group?}` saves a colour (into a colour group with `group`), gradient or pattern; `swatch.newGroup {fromArtwork: true, toGlobal?, includeTints?}` makes a colour group of the selected art's colours (by default as global swatches the art links to). `swatch.move {names, to?, group?}` reorders swatches or moves them into or out of a colour group (as dragging them in the Swatches panel does); give colour group names instead to reorder the groups. Dropping a swatch on art in the app runs `paint.setFill` (or `paint.setStroke`, the active proxy) with `ids: [the object under the pointer]`. The Swatches panel menu's commands: `swatch.addUsedColors {selection?, global?}`, `swatch.unused` (a query: the names Select All Unused selects), `swatch.merge {names}` (the first is kept), `swatch.ungroup {name}` and `swatch.sortByKind`. `brush.move {name | names, to?, type?}` reorders brushes among the brushes of their type, as dragging one onto another of its type in the Brushes panel does. `to` is an index among the brushes of that type in `brush.list` order: the brushes go before the first brush of that type at or after `to` that is not moving, and after the last brush of that type when there is none or `to` is left out. A move that keeps the order records no undo step. Brushes of different types, or a `type` other than theirs, are refused with "brushes move only within their type". Document colour mode: `file.new {colorMode: "cmyk"}` starts a CMYK document with CMYK default swatches, and RGB colors applied to it (`paint.setFill`/`setStroke` colors, gradient stops and freeform points, `paint.editGradient` stops, `paint.freeform.addPoint`/`setPoint`, `paint.sampleColor`, `object.mesh.setPointColor`/`addLine`, the `text.create` and `text.createInPath` color, the `text.setStyle` fill, `text.setRangeStyle` fills and strokes, a `shapeBuilder.merge` hex fill (its `fill: {color, keepModel: true}` form keeps RGB), the `symbol.adjust` stain color, `swatch.new {color}` and `{colors}`, `swatch.newGroup`, `swatch.editGroup` and `swatch.edit`) are stored as CMYK, converted through the color settings (`edit.colorSettings`: the CMYK working space and intent, as Document Color Mode converts them); Gray stays Gray, and `keepModel: true` keeps a color as given. A color that `swatch.edit` or `swatch.editGroup` is given and the swatch already has keeps its model, as does a stop color that `paint.editGradient` is given and the gradient already has; a stop that names a swatch takes the swatch's color as it is, and `swatch.new` or `swatch.edit` with `mode` converts a color into that mode only. RGB documents keep colors as given. Harmonies, Edit Colors blends, inversions and Recolor Artwork keep each colour's model (a blend between models takes the document's). `swatch.new {colors: [...]}` saves several colours as swatches in one undo step. `color.harmony {color, rule, steps?, variation?, amount?}` answers what the Color Guide panel shows: the harmony rule's colours, base first, and per colour its row of `2·steps+1` variations (shades, cool or muted on the left, tints, warm or vivid on the right). With `limitTo` (a swatch library id or name, or `"document"` for the document's swatches) every colour snaps to the library's nearest colour (ΔE 2000), as the panel's Limit to Library does (`ui.colorGuideLimit`); Recolor Artwork's `limitTo` takes the same values. Color Themes (local only: no online service) keeps five-colour themes in the preferences. `colorTheme.save {colors: [...]}` saves up to five colours as they are, `colorTheme.save {color, rule}` saves the five-colour theme a harmony rule makes from a base colour (base first; `name?` defaults to "Theme N", `replace: ""` overwrites that theme in place). `colorTheme.list` answers `{themes: [{name, colors: [hex], keys: [colour keys], rule?}]}`, `colorTheme.delete {name}` removes one and `colorTheme.addToSwatches {name}` adds one to the Swatches panel as a colour group (one undo step). ```json {"name":"run_command","arguments":{"command":"colorTheme.save","params":{"color":"#2266aa","rule":"splitComplementary","name":"Harbor"}}} {"name":"run_command","arguments":{"command":"colorTheme.addToSwatches","params":{"name":"Harbor"}}} ``` SVG Options: `export` to SVG takes them in `options`, flat or as `{"svg": {…}}`: `styling` (`presentation`, `style`, `entities`, `css`), `outlineText`, `images` (`embed`, or `link`: embedded images are written next to the SVG, or returned as `linked`), `objectIds` (`layerNames`, `minimal`, `unique`), `decimals` (1–7), `minify`, `responsive`, `useArtboards`, `range: "all"` (one SVG per artboard, listed in `files`, each holding only the art over its artboard), `preserveEditing` (the SVG reopens as the full document), `metadata` and `fewerTspans` (one `` per line of type). Unknown keys inside `svg` are rejected; `run_command document.formats` lists every option with its default. `encoding` is `utf8`, `utf16` (big-endian after a byte order mark) or `latin1` (ISO 8859-1, other characters as `&#x…;` references); `document.serialize` still answers `text`, plus `dataBase64` (the file) when it isn't UTF-8, and such files open again. `profile: "tiny12"` writes a simplified SVG Tiny 1.2 (presentation attributes only; filters, masks, blend modes and embedded fonts left out, symbols as art, each with a warning). `embedFonts: true` embeds the fonts type uses as `@font-face` rules subset to the characters used; a font whose licence (OS/2 `fsType`) forbids subsetting is embedded whole, one that forbids embedding is left out with a warning. Type names its faces by numeric `font-weight` (600 for Semibold and for a Japanese face's W6, 300 for Light, `bold` for 700), embedded or not. `svgz` takes the same options and writes the SVG gzipped (`.svgz` files also open, place and paste). `run_command document.save {path: "x.svg", svg: {…}}` saves as SVG (or `.svgz`). Symbols export as one `` with a `` per instance. An instance the def can't stand for is written as its own art: one stained by its fill; one scaled while the symbol has strokes (their weight doesn't scale), or rotated or scaled while it has effects, brushes or live objects; and every instance of a symbol with pattern paints or unlinked opacity masks (those stay on the page). `hiddenLayers: true` keeps hidden layers and objects, not displayed (`display="none"`); `document.save` and the CLI's `convert` keep them unless told otherwise, other exports leave them out, and they reopen hidden. An object's own data (`object.setProps {data: {pivot: "100,180"}}`) is written as `data-*` attributes (`data-pivot="100,180"`), and an SVG's `data-*` attributes come in as it. Attributes in other applications' XML namespaces (`plotter:pen="2"`) come in on their objects too and are written back, each namespace declared once on the root. A `preserveEditing` SVG carries the native document (CDATA in ``) and a hash of the markup around it: if another app changed the SVG since, `document.open` reads it as plain SVG and says so in `warnings`. Saving beyond `save_file` goes through `run_command`: `file.saveAs {path?, format?, options?, svg?}` (the document takes on the new file and format; for `format: "ait"`, or a `.ait` path without `format`, it fails with an error that names `file.saveAsTemplate`), `file.saveCopy` (the document keeps its path, title and modified state), `file.saveAsTemplate {path?, format?}` (it writes a `.vctemplate`, or, for `format: "ait"` or a `.ait` path, a PDF-compatible `.ait` written as a `.ai` file is, and with `aiVersion: "7"` it fails with an error; both open untitled; a path whose extension names another format has it replaced by the template's extension, and any other path that does not end in that extension has it added, so `card.png` is saved as `card.vctemplate` and `card` as `card.vctemplate`; the suggested name and the Templates folder come back without a path), `file.newFromTemplate {path}`, `file.revert` and `file.formatOptions {format?}` (a format's options with the values a save would use). Without a path the save commands return `{dataBase64, name, folder?, warnings}`. A document saved as SVG or PDF remembers its options, so the next save reuses them. A PDF opened with `pages`, `cropTo` or `password` is only part of the file, so Save asks for a new name instead of writing it back. `file.documentColorMode {mode, convert?, intent?, grays?}` switches the document colour mode, converting the colours of the art, symbols, pattern tiles and swatches through the colour settings (`object.convertDocumentColorMode` is an alias kept for older scripts). Gray colours stay Gray and print on the black plate only. RGB greys are colours like any other: to CMYK they separate through the profile into four-colour greys and a rich black, as Illustrator converts them. `grays: "black"` puts RGB greys with R = G = B on the black plate instead, K = their grey value (the inks Edit Colors › Convert to Grayscale before the switch would give); other colours convert as usual. SVG import keeps what the canvas edits live. `` becomes a pattern swatch. A `` with `` becomes a symbol (named after its `data-name`, else its id) with an instance per ``; a `` that shows it differently (paint inherited from the ``, a viewport that cuts the art) or that the canvas would draw otherwise (a scaled symbol with strokes) stays art. `feGaussianBlur`, `feDropShadow` and the usual shadow chains (offset, blur, flood or colour matrix, composite, merge) become Gaussian Blur, Drop Shadow, Outer Glow, Inner Glow and Feather effects; other filters are listed in `warnings`. Undisplayed objects (`display:none`) come back hidden, nested clip paths intersect, and reflected or repeated gradients (`spreadMethod`) are expanded into stops. `` files (relative links are found in the SVG's folder; only files on this computer or the SVG's network share are read, and pasted SVG reads only absolute paths) stay linked images (see Linked images); SVG files and `data:` SVGs become art; a file that can't be read becomes a placeholder in its box, named in `warnings` and `missingLinks`. Native files: `run_command document.save {compress: true}` writes a gzip-compressed `.vectorcraft` (the `useCompression` preference makes that the default for saves; compressed files open like any other, told apart by their content). `version: 2` or `1` writes a file older VectorCraft versions open (format name `drawcraft`, never compressed), and `preview: true` embeds a PNG of the first artboard, at most 256 px on its longer side. A save writes a temporary file and renames it over the old one, so a failed save never damages the file; only the images the document uses are written, and document keys written by a newer version are kept. ## Resources Fixed resources and the four templates (same as [Resource templates](#resource-templates) under Protocol): | URI | Content | |---|---| | `vectorcraft://document` | `document.inspect` summary (JSON) | | `vectorcraft://document/json` | The complete document model (JSON) | | `vectorcraft://commands` | Live command catalog with enabled state | | `vectorcraft://object/{id}` | One layer or object (`document.node {id, summary: true}`) | | `vectorcraft://command/{id}` | One command's metadata and enablement | | `vectorcraft://effect/{id}` | One live effect with parameters and defaults | | `vectorcraft://swatch/{name}` | One swatch (percent-encode spaces) | ## Examples Draw, look, export: ```json {"name":"draw_shape","arguments":{"shape":"rectangle","x":72,"y":72,"width":200,"height":120,"radius":12,"fill":"#1e88e5","stroke":"none"}} {"name":"draw_shape","arguments":{"shape":"star","cx":400,"cy":300,"radius1":90,"radius2":40,"points":5,"fill":"#ffc107","stroke":"#000000","strokeWidth":2}} {"name":"draw_path","arguments":{"d":"M72 400 C 150 300 250 500 330 400","stroke":"#e53935","strokeWidth":4,"fill":"none"}} {"name":"screenshot","arguments":{"scale":0.5}} {"name":"export","arguments":{"path":"/tmp/art.svg"}} {"name":"export","arguments":{"path":"/tmp/art.png","options":{"ppi":144,"background":"white","antiAlias":"type"}}} {"name":"export","arguments":{"path":"/tmp/icon.png","range":"1-3","options":{"useArtboards":true}}} ``` Export As goes through `options` too: for PNG, JPEG and WebP `useArtboards: true` writes one file per artboard (all, or `range`), named `-.`, and returns `{path, files}` (a PDF keeps them as pages of one file; an SVG writes one file per artboard of a `range`); `useArtboards: false` exports the bounds of the visible art with what its effects paint, the effects of groups, type and images included. `artboard.fitToArt` measures the art the same way. Raster exports take more options in `options`: `ppi` (72, 150, 300…; stored in the file), `background` (`transparent`, `white`, `black` or `"#rrggbb"`), `antiAlias` (`none`, `art`, `type`: text snapped to pixels) and, for PNG, `interlaced`. Long-tail commands: ```json {"name":"list_commands","arguments":{"filter":"align"}} {"name":"run_command","arguments":{"command":"select.all"}} {"name":"run_command","arguments":{"command":"object.align","params":{"horizontal":"left"}}} {"name":"run_command","arguments":{"command":"object.group"}} {"name":"run_command","arguments":{"command":"document.exportPdf","params":{"path":"/tmp/art.pdf","range":"1, 3","compatibility":"1.5"}}} {"name":"run_command","arguments":{"command":"document.pdfSettings","params":{"marks":{"trim":true},"includeDocument":true}}} ``` PDF files take the Save PDF dialog's options: `preset`, `standard`, `compatibility`, the General toggles and the `compression`, `marks`, `bleed`, `output`, `advanced` and `security` sections (`list_commands` with filter `exportPdf` documents every field). `document.exportPdf` and `export` (format `pdf`, the options in `options`) return `warnings`: options accepted but not applied yet, and features approximated or left out. An image whose pixel data can't be decoded (a PNG cut short) is left out of the pages, and `warnings` holds the note "an image couldn't be decoded and is missing from the picture" once, as raster exports give it; `.ai` saves and `file.print` do the same. A standard with a PDF version it doesn't allow is refused (PDF/A-2b at 1.3 or 2.0, PDF/X-4 above 1.6, PDF/X-1a and PDF/X-3 above 1.4); choosing a standard without giving `compatibility` sets its version (PDF 1.3 for PDF/X-1a and PDF/X-3). A `security` password encrypts the file (RC4 40-bit at PDF 1.3, RC4 128-bit at 1.4–1.5, AES-128 at 1.6, AES-256 at 1.7 and 2.0) with its permissions; either password opens it (`document.open {password}`), and a password with PDF/A or PDF/X is refused. Pattern fills and strokes are written as their tiles clipped to the area they paint (a stroke's outline, with its dashes, caps, profile, arrowheads and alignment), and freeform gradients as an image of their colour field at the document's raster effects resolution, clipped the same way. `createLayers` writes each layer and sublayer (template layers are left out) as a PDF layer, an optional content group named as the layer and listed under its parent layer's: hidden layers are off (their art is written, for the reader to show), non-printing ones have `/PrintState /OFF` and locked ones are locked. It needs PDF 1.5 or later (at 1.4 it warns), and the file reopens with those layers and sublayers. A `.ai` file writes them unless `createLayers` is false, so apps that read its PDF part find the layers, hidden ones included; hidden objects aren't in the PDF part. Images follow the `compression` settings of their kind (`color`, `gray`, or `mono` for black-and-white images): above `abovePpi` as placed they are resampled (`downsample`: `average`, `subsample` or `bicubic`) to `ppi`, and compressed with `zip`, `jpeg` (at `quality`; images with transparency stay lossless) or `auto` (JPEGs stay JPEG, the others lossless). `none`, `jpeg2000`, CCITT and `runLength` are written as ZIP, with a warning when an image needs them. A 16-bit PNG that is not resampled, recompressed or converted keeps its 16 bits a sample in PDF 1.5 and later files, and is written at 8 bits in PDF 1.3 and 1.4 files. CMYK images stay CMYK (DeviceCMYK, or ICC-based with the CMYK profile when colours are tagged, as in PDF/X-3 and PDF/X-4): a CMYK JPEG neither resampled nor recompressed is written unchanged, and the others' ink amounts are resampled and compressed again (CMYK JPEG or ZIP). `output.conversion` converts them like CMYK colours: `destination` to another CMYK profile or to RGB, `preserveNumbers` (and PDF/X-1a) keeps their numbers in a CMYK destination. `document.pdfSettings` lists the options that differ from the preset and the warnings without writing a file. `thumbnails: true` embeds each page drawn small (106 px on its long side, without the layers the page leaves out) as its `/Thumb` image. `fastWebView: true` writes a linearised file (the linearization dictionary first, then the first page with hint tables saying where every other page's objects are), which stays linearised when a password encrypts it. `compatibility: "1.3"` writes a PDF 1.3 file, which has no transparency: a copy of the document is flattened first with `flattenerPreset` (empty: High Resolution; `high`, `medium`, `low` or a saved preset, `flattener.presets.list`) and `flattener` options over it (as `object.flattenTransparency` takes them); images with see-through pixels count as transparency and rasterized areas are clipped to their regions. A file that would still have transparency fails the export. PDF/X-1a and PDF/X-3 files are PDF 1.3 files too. A page with a side longer than 14,400 pt is written with a `UserUnit` (PDF 1.6): its longer side divided by 14,400, rounded up to the next thousandth, with the page's sizes and coordinates in those units, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2). In PDF 1.3 to 1.5 files and PDF/X files, such a page is written in points. An Illustrator EPS (version 9 on) or `.ai` file opens from the editing data it carries (after the EPS page; in the `.ai` file's PDF private data): layers and sublayers (name, order, visibility, lock, printing, preview, dimming, colour), groups as they were nested, compound paths, clipping groups, object names, hidden and locked objects, fills and strokes (spot colours as spot swatches), linear and radial gradients, opacity, blend modes, isolation and knockout, embedded images with their alpha channel, guides, and every artboard where it is. An object with several fills or strokes, effects or a brush comes in as its drawn look (a group named after it). Type is made from the file's text document, whole and editable: point type, area type (a story in several frames as threaded type) and type on a path, with its fonts, size, leading, tracking, scaling, baseline shift, fill and stroke (grey, RGB or CMYK), opacity, blend mode, alignment, indents and paragraph spacing. Where the text document can't say, type that shows is the page's, in its text object's place. A text object on a layer that shows and doesn't print keeps the type the text document makes for it, on that layer, even where the page doesn't draw it. A file whose editing data can't be decompressed or has symbols, pattern fills, placed files or anything else the reader doesn't read on a layer that shows, or whose layers look different from its page, opens as before: an EPS as its printed page, a `.ai` file from its PDF content (where plain groups open ungrouped), with a warning saying why. Layers look different when they draw an object the page doesn't (or miss one it draws), and, for an EPS whose page is the box of its art, when they print art outside it. A file whose editing data is for a page of another size opens the same way, and its warning gives both sizes in points, with the scale of the editing data's page to the file's page (1:10 for a tenth of its size) when their sides are in proportion. With `textAs: "outlines"` a `.ai` file whose type shows opens from its PDF content, which has the type's outlines. A PDF saved with Illustrator's editing data holds it in `AIPDFPrivateData` streams and opens from it as a `.ai` file does. The document then has what the file's page doesn't print: hidden objects and layers, layers that don't print, guides and the art outside the artboards, as the app that wrote the file shows them. Exports and `artboard.fitToArt` leave out the hidden art, the guides and template layers; an EPS's artboard is the file's artboard, not the page's bounding box. A print pipeline that wants only what the page prints opens the file with `editingData: false` (default `true`): an EPS as its page, a `.ai` file from its PDF content (a `.ai` saved without PDF compatibility then can't be opened). ```json {"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/label.eps","editingData":false}}} ``` Opening a PDF (or `.ai`) imports every page as an artboard and layer; `document.open` takes `pages` ("2-3, 5", 1-based), `cropTo` (`bounding` (the art's bounds), `art`, `crop` (default), `trim`, `bleed`, `media`: the box each artboard gets; with `trim` the bleed box round it becomes the document's bleed, and a PDF-compatible `.ai` or a PDF saved with its editing data opens on its trim boxes for `crop`, its crop box taking the bleed in) and `password` for an encrypted file. `document.pdfInfo` reads a file without opening it: the page count, each page's size and boxes, `needsPassword`, and with `thumbnail: n` a PNG of page n. It reads a PostScript file (an EPS, a `.ps` file, a `.ai` in PostScript form) as one page the size of its bounding box (a US Letter page without one), with an empty `boxes`, and `thumbnail: 1` shows what that page prints. A page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, the file's numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size. Imported colours keep their model: DeviceCMYK and CMYK ICC colours stay CMYK (a file painted mostly in CMYK opens as a CMYK document), DeviceGray is Gray, and Separation and DeviceN inks become spot swatches the art links to at its tint (gradient stops too). CMYK images (DeviceCMYK, or ICC-based with four components) keep their ink amounts: a CMYK JPEG as it is, any other as a CMYK TIFF (masked CMYK images, and ones with 1, 2 or 4 bits per sample, open in RGB with a warning). Placing a CMYK TIFF keeps it CMYK too. `colorMode: "rgb" | "cmyk"` opens any file in that mode instead, its colours converted as `file.documentColorMode` does (`grays` too): ```json {"name":"run_command","arguments":{"command":"document.pdfInfo","params":{"path":"/tmp/brochure.pdf","thumbnail":2,"cropTo":"trim"}}} {"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/brochure.pdf","pages":"2-3","cropTo":"trim"}}} ``` PostScript files (`.eps`, `.epsf`, `.ps`, and `.ai` files saved in older formats or without PDF compatibility) open through the EPS reader (see EPS and PostScript import). An `.ai` saved without PDF compatibility (its PDF part is only a placeholder page) opens from its editing data alone: its type is made from the file's text document where it can be (point type, area type and type on a path), and what can't be is left out with a warning. Non-native art (the content of a placed PDF, which Illustrator shows but doesn't edit and keeps as a PDF inside the editing data) is drawn by the PDF importer, as a group named "Non-native art" fitted to the object's box; one whose PDF can't be read is left out with a warning. Without editing data it says it can't be opened. `pages` that name every page of a file saved without PDF compatibility also open it from its editing data. Without editing data it says it can't be opened. `pages` that name every page of such a file also open it from its editing data. `file.place` reads it from its editing data too and places the art of the layers that show on artboard `page`, clipped to that artboard unless `crop` is `bounding`. `clipboard.importPdf` takes every object of its editing What a PDF holds comes in as editable art: soft masks become opacity masks (an alpha mask as a white copy of its art; the backdrop colour gives Clip, an inverting transfer function Invert), transparency groups keep isolation and knockout, tiling patterns become pattern swatches, patch and triangle mesh shadings become gradient meshes, and gradients keep their stop opacity and stop where the shading doesn't extend. Text becomes point type, one object per run of a line in the file's font (by name; fonts that aren't available are listed in `warnings` and show in the fallback font, and every export that draws that type — PDF, EPS, EMF/WMF, raster images, SVG with outlined or embedded fonts — says in its `warnings` that it wrote the fallback font) — `textAs: "outlines"` keeps the glyph outlines the file draws instead (its embedded fonts, installed or not). Lines wrapped in a frame (each wrapped line ending in a space, sharing a left edge, font and leading) come back as one area type object, left aligned or justified, where laying it out breaks the lines where the file does; a stroke written as each glyph's outline stroked is the type object's stroke; glyphs set apart by gaps wider than a space keep their places. Strokes stay live strokes (width, cap, join, miter limit, dash and paint), and an object written as a fill and then a stroke of the same outline is one path with both. Optional content groups (the layers of PDF and PDF-compatible `.ai` files) become layers with their name, visibility (the default configuration's, or a view state that is off), print state and lock, nested as sublayers the way the file's layer order nests them, stacked as the pages mark them (layers with no art come in empty); art that is off comes in as a hidden layer. Art outside them goes to a layer per page (except the opaque white page a `.ai` paints under its layers, which isn't art). `layers: false` gives one layer per page of only what shows: ```json {"name":"run_command","arguments":{"command":"document.open","params":{"path":"/tmp/map.pdf","textAs":"outlines","layers":false}}} ``` Preserve Editing (`preserveEditing`, on in the `VectorCraft Default` preset) embeds the native document in the PDF as an embedded file (`vectorcraft-editing.vectorcraft`; files named `drawcraft-editing.drawcraft` are read too) with a hash of the pages it was written with. `open_file` / `document.open` of such a PDF, `.ai` or `.ait` (no `pages` picked) restores the document exactly (`restored: true`); when another app changed the pages, or the data can't be read, the artwork is imported instead and the first warning says why. Choosing a standard turns it off (PDF/A refuses it). Save to a `.ai` path (`document.save {path: "art.ai"}` or `{format: "ai"}`) writes a PDF-compatible file that always carries the document, takes the PDF options and keeps its path when reopened, so Save writes `.ai` again; a `.ait` opens untitled. `document.export` / `serialize` to `.ai` (and the CLI's `run --export x.ai` and `convert in.svg out.ai`) write the same file without giving the document a path. `options: {aiVersion: "7"}` writes legacy `.ai` files of version 7 instead (the PostScript form, which keeps layers, groups, compound paths and clipping groups in apps that read it), one per artboard of `range` (default all; `{name}-{artboard}.ai` when several, listed in the result's `files`), with `transparentImages: "flatten"` (default) or `"omit"` for images with see-through pixels, opacity or a blending mode, `hiddenObjects: "layer"` (default: a hidden layer of each layer's hidden objects), `"omit"` or `"visible"`, and `outlineText: true` to write type as outlines instead of text objects. An image that can't be decoded is left out of the file and does not show in art that is flattened or whose raster effects are expanded; `warnings` carries "an image couldn't be decoded and is missing from the picture" once for all such images. From the CLI: `vectorcraft-cli convert in.svg out.ai --ai-version 7 [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] [--outline-text] [--range R]`. `document.export`, `serialize`, `exportSelection` and `exportForScreens` refuse a `.ait` path or `format: "ait"` with an error that names `file.saveAsTemplate`, which writes it. The CLI's `run --export x.ait` and `convert in.svg out.ait` write it as `file.saveAsTemplate` does, with every artboard; `convert` refuses `--artboard` and `--range` for a `.ait`. Drive a tool like a mouse: ```json {"name":"pointer_gesture","arguments":{"tool":"ellipse","events":[ {"kind":"down","x":100,"y":100},{"kind":"drag","x":150,"y":140},{"kind":"up","x":200,"y":180}]}} ``` Edit a gradient with the Gradient tool's annotator (the bar runs along the vector; stop chips sit 10 px under it, midpoint diamonds 7 px above): a click on the bar adds a stop, dragging a chip moves it (drag it off the bar to delete it, hold Alt to copy it), dragging the end handle changes the vector. The selected stop (`gradient.selectStop`) takes Delete and ←/→: ```json {"name":"run_command","arguments":{"command":"paint.setFill","params":{"gradient":{"start":[100,150],"end":[200,150]}}}} {"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":130,"y":150},{"kind":"up","x":130,"y":150}]}} {"name":"pointer_gesture","arguments":{"events":[{"kind":"down","x":130,"y":160},{"kind":"drag","x":160,"y":160},{"kind":"up","x":160,"y":160}]}} {"name":"press_key","arguments":{"key":"Delete"}} ``` A radial gradient's annotator also draws its extent: a dashed ellipse around the centre (the start, drawn as a ring) with a dot on it across the bar (drag it to change the aspect ratio); dragging the ellipse elsewhere rotates it. The dot inside the centre ring is the focal point, where the first stop sits: drag it for an off-centre radial, back onto the centre to centre it. `paint.setGradientGeom` sets the same things directly (`aspect` in %, `focal` in document coordinates or `null`; `start`/`end` may be left out), and they export as SVG `fx`/`fy` and PDF two-point radial shadings: ```json {"name":"run_command","arguments":{"command":"paint.setGradientGeom","params":{"aspect":60,"focal":[130,140]}}} ``` Dither (the Gradient panel's box) draws a linear or radial gradient fill with fine noise that hides banding; SVG and PDF exports write such a fill as an image at the Document Raster Effects resolution. It applies to fills (`stroke: true` is refused): ```json {"name":"run_command","arguments":{"command":"paint.editGradient","params":{"dither":true}}} ``` Method (the Gradient panel's menu): `classic` (the default) mixes the colours between stops in display RGB, `perceptual` mixes a linear or radial gradient's colours in OKLab so the steps look even. SVG and PDF exports write a Perceptual gradient as an image at the Document Raster Effects resolution (a dithered Perceptual fill with its noise). `paint.setFill`/`paint.setStroke` take it in `gradient` too: ```json {"name":"run_command","arguments":{"command":"paint.editGradient","params":{"method":"perceptual"}}} ``` Freeform gradients: `paint.editGradient {kind: "freeform"}` places four or more colour points inside each selected object (coloured along the stops; `mode: "points"|"lines"` is the Draw toggle). `paint.freeform.get` lists the points (document coordinates), lines and the selected point; `paint.freeform.addPoint {at, color?, opacity?, spread?, line?}`, `setPoint {index?, …}`, `deletePoint {index?}`, `addLine {points}`, `splitLine {line, segment, t?}` and `selectPoint {index|null}` edit them (`index` defaults to the selected point; each edit is one undo step). The color of a point colored along the stops, of a point `splitLine` adds and of a point `addPoint` adds without `color` is a mix of the colors around it (the two stops, the line's two points, or all the points) in the color model those colors share, so CMYK stops and points give CMYK points. With the Gradient tool on a freeform gradient a click on a point selects it (drag to move it), a click on a line adds a point on it, a click elsewhere on the art adds a point (in Lines mode joined to the selected one), and Delete removes the selected point: ```json {"name":"run_command","arguments":{"command":"paint.editGradient","params":{"kind":"freeform","mode":"lines"}}} {"name":"run_command","arguments":{"command":"paint.freeform.addPoint","params":{"at":[150,150],"color":"#ff3366","spread":20}}} {"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":180,"y":170},{"kind":"up","x":180,"y":170}]}} ``` On the canvas the selected point also shows its spread as a dashed ring with a handle 16 px (or the spread, if larger) to its right: drag the ring or handle to change the spread. Dragging a point out of the object removes it, and double-clicking one opens its popover (dialog `gradientStop`, which on a freeform gradient edits the selected point: set `color`, `opacity` and `spread` and confirm). In Lines mode successive clicks on the art draw one smooth line through the points they add; a click on an existing point first continues the line from it, and `press_key` Escape (or a click off the art) ends it: ```json {"name":"pointer_gesture","arguments":{"tool":"gradient","events":[{"kind":"down","x":110,"y":190},{"kind":"up","x":110,"y":190},{"kind":"down","x":150,"y":170},{"kind":"up","x":150,"y":170}]}} {"name":"press_key","arguments":{"key":"Escape"}} ``` Raw protocol (for debugging): ```sh printf '%s\n' \ '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"sh","version":"0"}}}' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"ellipse","x":10,"y":10,"width":100,"height":80,"fill":"#ff0000"}}}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inspect_document","arguments":{}}}' \ | target/release/vectorcraft-cli mcp --headless ``` ## Headless batch CLI ```sh vectorcraft-cli commands # command catalogue (JSON) vectorcraft-cli run --in in.svg \ --cmd select.all --cmd paint.setFill --params '{"color":"#ff0000"}' \ --export out.svg --export out.png --scale 2 vectorcraft-cli run --cmd file.new --params '{"width":800,"height":600}' \ --cmd shape.star --params '{"cx":400,"cy":300,"radius1":200,"radius2":90}' \ --export star.vectorcraft ``` `run` prints one JSON line per step (`open`, `cmd`, `export`) and exits non-zero on the first failure, naming the step (counting from 0, as `"$N"` references do) and its command. `--params` applies to the `--cmd` just before it. `run` also accepts the host commands `file.open`, `file.save`, `file.export`, `file.exportForScreens` and `tool.select`. `run --in`, `convert` and `info` read every format `document.open` reads (`vectorcraft-cli --help` lists them). ### Steps from a file or standard input A command line has a length limit (32,767 characters on Windows), which long parameters such as a placed file's `dataBase64` or a long list of ids soon reach (#3147). `--commands FILE` reads the steps from a file instead, and `--commands -` from standard input: ```sh vectorcraft-cli run --in sheet.vectorcraft --commands steps.json --export out.pdf ``` ```json [ {"command": "file.place", "params": {"name": "code.svg", "dataBase64": "PHN2ZyB4bWxucz0i…", "rect": [20, 40, 16, 57]}}, {"command": "object.setProps", "params": {"id": "$0.ids.0", "name": "barcode"}} ] ``` The text is a JSON array of steps, or one step per line (JSON Lines; blank lines are skipped). A step is an object with `command`, the command id (or `id`, as `command_batch` names it), and optional `params`, a JSON object; any other key is refused. The steps run where `--commands` stands among the `--cmd` and `--export` options, in order, and count with them for `"$N"`, so a file can refer to the ids an earlier `--cmd` made and a later `--cmd` to the file's results. `--commands` may be given more than once (standard input once). The text is untrusted input: it must be UTF-8 (a byte order mark is skipped), at most 64 MiB and 100,000 steps, and a malformed step stops the run before anything runs, with the step's number and its entry (`entry 3`) or line (`line 3`) in the message. ### What a headless session remembers `vectorcraft-cli run` and the headless `mcp` server keep two things between runs, in folders next to the preferences (the folder the desktop app uses, so both see the same ones; `VECTORCRAFT_CONFIG_DIR` moves it): - **Saved PDF presets** (`pdf.preset.save`, `.delete`, `.import`): one `.vcpdfpresets` file per preset in `PDF Presets`, written as the command runs. Presets saved earlier are there at the next start and work as `preset` in the PDF options. - **ICC profiles** (`color.loadProfile`): a copy of the file goes into `Color Profiles` (the result says `remembered: true`), and every profile there is registered again at the next start, before the first command, so `edit.colorSettings {"cmyk": "…"}` or a preset's output intent can name it in a later run. The copy is what counts: the original may move or be deleted. `color.removeProfile {name}` forgets a profile and its copy (not while it is a working space). Not remembered: Color Settings, the document, anything else. Nothing is read or written when `VECTORCRAFT_NO_PREFS` is set, and a server confined with `--automation-read-root` or `--automation-write-root` keeps nothing either (the folders are outside its roots). A file that is damaged, too large (a profile over 32 MB, a presets file over 1 MB) or not a valid preset or profile is skipped and logged. When the desktop app starts, the presets it kept in its preferences file before this folder existed are moved into it; after that the folder is the one place they live, so a preset deleted by `vectorcraft-cli` is gone in the app too. ### Reproducible output PNG, PDF, EPS, legacy `.ai`, packaged zips and saved `.vectorcraft` files record the time they were made in (PDF `CreationDate` and `ModDate`, the PNG `Creation Time` entry, a zip entry's date, a document's `created` and `modified`), so the same commands run twice gave different bytes though the pixels matched (#2880). Setting **`SOURCE_DATE_EPOCH`** to a non-negative whole number of seconds since the epoch records that instant instead of the clock, and the bytes repeat (): ```sh SOURCE_DATE_EPOCH=1791208980 vectorcraft-cli run --cmd file.new --params '{}' --export out.pdf ``` A command that takes its own date (`file.new {created}`, a save's `modified`) still wins over the variable. A blank, negative or non-numeric value is ignored and the clock decides, so an export is never refused over the environment. SVG output carries no time and was already reproducible. Two things stay on the real clock and are not pinned by the variable: the recovery heartbeat, and the random seed PDF encryption draws on. **A PDF exported with a password therefore still differs between runs** whatever `SOURCE_DATE_EPOCH` says, because that seed supplies its AES initialisation vectors and its file `/ID`. Reproducible output means an unencrypted export. ### Parameter keys `command_run`, each step of `command_batch`, `run --cmd … --params` and the control channel's `engine.execute` check the keys of `params` against the command's params description (the `params` text `command_list` returns): the object it starts with, `{x, width?: pt, name?|title?: …}`, names the keys; `?` marks an optional one. A key it doesn't name is refused before the command runs: ```json {"id": "file.new", "params": {"widht": 300}} → invalid parameters for `file.new`: unknown parameter `widht`; did you mean `width`? ``` Only the top level is checked: the keys of an object inside a value (`artboardLayout: {…}`) are the command's to read. The one exception is an effect's `params`: `effect.apply` (and `appearance.addEffect`) and `effect.setParams` check them against the effect's own entry in `effect.list`, its `defaults` and the keys its `params` text names. A key the effect doesn't take, or a value of another type than its default (a string for a number, a number for a switch), is refused the same way: ```json {"id": "effect.apply", "params": {"effect": "blur.gaussian", "params": {"blurr": 3}}} → invalid parameters for `effect.apply`: unknown parameter `blurr`; the command takes: radius ``` The `color` of Drop Shadow, Inner Glow, Outer Glow and Shift to Color must be one the effect can read: a hex color (`#rrggbb`, `#rgb` or `#rrggbbaa`, with or without the `#`), `black`, `white`, `red`, `green` or `blue`, `[r, g, b]` (three numbers, each read on a 0 to 1 scale), or a color object (`{"model": "rgb", "r": 1, "g": 0, "b": 0}`). `effect.apply` and `effect.setParams` refuse any other value themselves (where the parameter check runs first, a number or a boolean is refused as not a string), so the app's effect dialogs and Appearance panel, a step of `command.batch` and a replayed journal get the same error, and the document and its undo history stay as they were: ```json {"id": "effect.setParams", "params": {"index": 0, "params": {"color": "#ff00"}}} → invalid parameters for `effect.setParams`: `color` must be a #rrggbb color ``` A document saved with such a color opens as before, and the effect draws its default color (an array of three or more entries reads an entry that isn't a number as 0). The `channel` of Levels and Curves must be `rgb`, `red`, `green` or `blue` (`r`, `g`, `b` and any letter case are read too). `effect.apply` and `effect.setParams` refuse any other value themselves in the same way, and a document saved with one opens as before and draws it as `rgb`: ```json {"id": "effect.setParams", "params": {"index": 0, "params": {"channel": "alpha"}}} → invalid parameters for `effect.setParams`: `channel` must be one of rgb, red, green, blue ``` A number outside the range the effect documents is still stored as given and clamped when the effect draws. A plug-in effect's parameters are resolved against its manifest instead, and `effect.setParams` as a step of `command.batch` isn't checked before the batch runs (the effect it edits may only exist once the steps before it ran); the command still refuses an effect `color` it can't read. A description with a `…` entry takes more keys than it lists: those of the command it names (`…settings as file.new`), or any key when it names none, as `document.export` and `document.save` do for the options of the format they write (one object can carry several formats' options). A command that takes its own reply back (`document.setup`, `text.areaOptions`) says so the same way. The app's own journal isn't checked: an action or journal recorded by an older version replays as it did, except for an effect `color` that the effect can't read. These Object menu commands refuse a string or another value that is not a number with `` `scaleH` must be a number, not "50" ``, naming the key, and change nothing: `scaleH`, `scaleV`, `moveH`, `moveV` and `rotate` in `object.transformEach`; `ppi` in `object.rasterize`; `width`, `height`, `spacingX` and `spacingY` in `object.createObjectMosaic`; `offset`, `length`, `bleed` and `weight` in `object.createTrimMarks`; and `spacing` in `artboard.rearrange`. A null is read as a key left out. `object.cropImage` refuses a `rect` that is not an array of four numbers, such as a string, with `rect must be [x, y, width, height]`; a null `rect` crops to the artboard, as a missing one does. `object.createObjectMosaic` (`columns`, `rows`), `object.transformEach` (`reference`, `seed`) and `artboard.rearrange` (`columns`) take whole numbers, written `4` or `4.0`. A fraction or a negative number is refused with `` `columns` must be a whole number ``. A string or another value that is not a number is refused with `` `columns` must be a number, not "4" ``. Each message names the key that was given, and the command then changes nothing. These Object menu commands refuse a string, a number or another value that is not true or false with `` `copy` must be true or false ``, naming the key, and change nothing: `reflectX`, `reflectY`, `random`, `copy`, `strokes`, `corners`, `objects` and `patterns` in `object.transformEach`; `gray` and `deleteRaster` in `object.createObjectMosaic`; `trim` in `object.cropImage`; `allArtboards` in `object.createTrimMarks`; and `byColumn`, `moveArtwork` and `patterns` in `artboard.rearrange`. A null is read as a key left out. `object.createTrimMarks` refuses a `style` that is not a string, such as `5`, with `` `style` must be roman or japanese ``, the message it gives for a string that names no style. A whole number that is larger than the build's `usize` can hold (4294967295 on a 32-bit build, such as the `wasm32-unknown-unknown` web build) counts as larger than any index, place, byte offset or count. An index, such as a `subpath`, `anchor`, `segment`, document `index`, `artboard`, appearance `item` or `index`, gradient `stop` or freeform point `index`, names nothing, and the command does what it does for any other index past the end: most commands fail and change nothing, and `appearance.removeItem` removes nothing. A place (`to` in `swatch.move`, `artboard.reorder`, `graphicStyle.move`, `appearance.moveItem`, `appearance.duplicateItem` and `effect.move`) puts the item last. A text `start` or `end` is past the end of the text. `object.slice.divide` refuses such a `rows` or `columns`, `recolor.reduce` keeps each color in its own row for such a `colors`, and `layer.setProps` refuses such a `color`. `object.setBounds` reads a `reference` past 8 as 8, as `object.transformEach` does. The blend, envelope and gradient mesh commands refuse a string or another value that is not a number with the same message, as in `` `steps` must be a number, not "8" ``, and change nothing: `steps`, `distance` and `value` in `object.blend.make` and `object.blend.options`; `bend`, `h`, `v`, `horizontalDistortion` and `verticalDistortion` in `object.envelope.makeWithWarp`, `resetWithWarp` and `options`; `rows`, `cols` and `columns` in `object.envelope.makeWithMesh`, `resetWithMesh` and `object.mesh.create`; `fidelity` in `object.envelope.options`; `highlight` in `object.mesh.create`; and `opacity` in `object.mesh.setPointColor`. A null is read as a key left out. `object.mesh.create` refuses an `at` that is not two numbers with `` `at` must be [x, y] ``. The transform, Align, Object Properties and Live Shape Properties commands refuse a string, a number or another value that is not true or false with `` `strokes` must be true or false ``, naming the key, and leave the document and its undo history as they were: the options `copy`, `strokes`, `corners`, `patterns`, `objects` and `typeAreas` in `object.transform`, `object.move`, `object.nudge`, `object.rotate`, `object.scale`, `object.reflect`, `object.shear`, `object.transformAgain` and `object.setBounds`; `anchors` in `object.transform`; `big` in `object.nudge`; `absolute` in `object.rotate`; `proportional` in `object.setBounds`; `patterns` in `object.align`, `object.distribute` and `object.distributeSpacing`; `visible`, `locked`, `isolate` and `knockoutShape` in `object.setProps`; and `invertPie`, `makeSidesEqual` and `relativeRadii` in `object.setLiveShape`. A null is read as a key left out. They also refuse a string or another value that is not a number with `` `opacity` must be a number, not "50" ``, naming the key, and leave the document and its undo history as they were: `opacity` in `object.setProps`; `radius` and `sides` in `object.setLiveShape`; `reference` in `object.setBounds`; and `artboard` in `object.align`, `object.distribute` and `object.distributeSpacing`. A null is read as a key left out. The blend and envelope commands refuse a string, a number or another value that is not true or false with `` `smooth` must be true or false ``, naming the key, and leave the document, its undo history and the options new blends and envelopes start with as they were: `smooth` in `object.blend.make` and `object.blend.options`; `independent` in `object.blend.spine.moveAnchor`; `perpendicular` in `object.blend.spine.reshapeSegment`; `horizontal` in `object.envelope.makeWithWarp`, `resetWithWarp` and `options`, also when an `orientation` is given; `maintainShape` in `object.envelope.resetWithMesh`; `antiAlias`, `distortAppearance`, `distortLinearGradients` and `distortPatternFills` in `object.envelope.options`; and `editing` in `object.envelope.editContents`. A null is read as a key left out. These commands refuse a string, a number or another value that is not true or false with `` `closed` must be true or false ``, naming the key, and leave the document and its undo history as they were: `clockwise` in `shape.spiral`; `closed` in `shape.arc` and `path.create`; `vertical` and `placeholder` in `text.create` and `text.createInPath`; `underline`, `strikethrough`, `allCaps`, `proportionalMetrics`, `tateChuYoko` and `noBreak` in `text.setRangeStyle`; `effective` in `text.getRange`; `on` in `type.opticalMarginAlignment`, `type.systemLayout` and `type.fractionalWidths`; `remove` and `overhang` in `type.ruby`; `expandInstances` in `symbol.delete`; `alt` in `symbol.spray` and `symbol.adjust`; `spot` and `global` in `swatch.new` and `swatch.edit`; `unlink` in `swatch.delete`; `fromArtwork`, `toGlobal` and `includeTints` in `swatch.newGroup`; `global` and `selection` in `swatch.addUsedColors`; `visible` in `appearance.setItem`; `copy` in `appearance.transfer`; `reverse` and `append` in `appearance.copyFrom`; `reverse` and `stroke` in `paint.editGradient`; `stroke` in `paint.setGradientGeom` and in `paint.freeform.addPoint`, `setPoint`, `deletePoint`, `addLine`, `splitLine` and `get`; `erase`, `openFilledAsClosed` and `splitStroke` in `shapeBuilder.merge`; `openFilledAsClosed` in `shapeBuilder.regions`; `closeGaps` in `livePaint.gapOptions`; and `enhanced` in `imageTrace.make` and `imageTrace.makeAndExpand`. A null is read as a key left out. These commands refuse a string or another value that is not a number with `` `radius` must be a number, not "2" ``, naming the key, and change nothing: `radius` in `shape.rectangle`; `sides` and `rotation` in `shape.polygon`; `radius2`, `points` and `rotation` in `shape.star`; `diameter`, `opacity`, `brightness`, `growth`, `fuzziness`, `rays`, `longest`, `rayFuzziness`, `rings`, `largest`, `x2`, `y2`, `direction`, `pathLength` and `seed` in `shape.flare`; `decay` and `segments` in `shape.spiral`; `rows` and `columns` in `shape.rectangularGrid`; `concentric` and `radial` in `shape.polarGrid`; `size` and the `width`, `height` and `fitMinPercent` of `area` in `text.create`; `size` and `fitMinPercent` in `text.createInPath`; `start`, `end`, `size`, `tracking`, `baselineShift`, `hScale`, `vScale`, `rotation` and `strokeWidth` in `text.setRangeStyle`; `start` and `end` in `text.editRange` and `text.getRange`; `start`, `hScale`, `vScale` and `rotation` in `text.touchType`; `by`, `start` and `end` in `type.step`; `by` in `type.size.increase` and `type.size.decrease`; `size` in `type.ruby`; `width`, `height` and `fitMinPercent` in `text.areaOptions`; `x` and `y` in `symbol.place`; `radius`, `density` and `scale` in `symbol.spray`; `radius` and `intensity` in `symbol.adjust`; `to` in `swatch.move`; `weight` and `opacity` in `appearance.setItem`; `angle` in `paint.editGradient` and in the `gradient` of a command that takes a paint; `t` in `paint.freeform.splitLine`; `tolerance` and `gap` in `shapeBuilder.merge`; `gap` in `shapeBuilder.regions`; and `width` and `tolerance` in `livePaint.strokeEdge`. A null is read as a key left out. A gradient stop that names a swatch refuses a `tint` that is not a number with `` stop 0: `tint` must be a number ``. A step can use an earlier step's result, in `run` as in the `command_batch` tool. A parameter string that is exactly `$N` or `$N.path` takes that value from the result of step `N` (counting from 0, in the order they ran; `run --in`'s open isn't a step), and `$last…` from the step before. A path goes into objects by key and arrays by index: `"$1.id"`, `"$2.ids.0"`, `"$last.ids[0]"`. A string starting with `$$` stands for one `$` (`"$$1.99"` is `$1.99`). A reference that names no value fails the step, so a command never runs with one left in it: ```sh vectorcraft-cli run --cmd file.new --params '{"width":600,"height":200}' --cmd text.create --params '{"x":10,"y":60,"text":"plain and bold words","size":30}' --cmd text.setRangeStyle --params '{"id":"$1.id","start":10,"end":14,"style":"Bold"}' ``` ## Checking scripts written for the reference app `script.check {path? | data? | dataBase64?, name?, includes?}` reads a script written for the reference app (`.jsx`, `.jsxinc`, `.js`, or a compiled `.jsxbin`) and reports what it uses. The check does not run any of the script, and it does not change the document or the preferences. The reply's `kind` is `script`, `compiled` (a `.jsxbin` file, or text that starts with `@JSXBIN@`, which the check doesn't read), `module` (a file with a top-level `import` or `export`, such as a React `.jsx` file), `notText` (a NUL byte, or more than one byte in ten a control character, in the first 8 KiB) or `tooLarge` (over 8 MiB). For a script, the reply holds these lists: - `needs` lists what the script needs from the app that runs it: `language` (the script language's own objects and directives, such as `$`, `alert`, `UnitValue` and `#target`), `dom` (the reference app's document model), `files`, `menuCommands`, `dialogs`, and `hostOnly` (features only the reference app has: palettes, `#targetengine`, BridgeTalk, sockets, native code, XMP, the XML object and E4X, operator overloading, reflection, actions and `selectTool`). - `directives` lists the `#include`, `#includepath`, `#script`, `#strict`, `#target` and `#targetengine` lines, and `targets` splits each `#target` line's application specifier into `app`, `instance`, `version` and `locale`. A directive is read where it starts a line, written `#name` or `//@name`. - `features` lists the platform features the script uses by id (such as `file.write`, `scriptui.dialog`, `menuCommand` or `e4x`), each with a count and its lines. - `dom.members` lists the document-model member chains as the script writes them (`app.activeDocument.layers[].name`, with `[]` for an index and `()` for a call); a variable assigned from a chain is replaced by that chain, and a name written with `\u` escapes is read as the name it spells. `dom.types` lists the type names the script compares with `.typename` or creates with `new`, apart from the language's, the platform's and the script's own functions. - `menuCommands.strings` lists the string literals the script passes to `executeMenuCommand` (`app.executeMenuCommand('…')`), each with a count and its lines, and `menuCommands.dynamic` counts the calls whose argument isn't a string literal. - `outside` lists what the script does outside the document: `filesWritten` (with the open modes used), `filesOpened` (files opened in other apps), `network`, `nativeCode`, `otherApps` (messages to other apps), `environment` (environment variables), `unseenCode` (`eval`, `new Function` and `$.evalFile`) and `preferences` (the reference app's preferences changed). - `problems` lists what would stop the script loading as written (strings, comments, regular expressions, template literals and XML literals that aren't closed, brackets that don't match, syntax newer than ES3 and unknown directives) and notes on its text (bytes that aren't valid UTF-8, mixed line endings, and nesting deeper than the check follows: more than 4,096 open brackets, or XML literals and the code in their braces nested more than 4,096 levels deep). Each entry of `targets`, `directives`, `includes` and `problems` has the `line` it is on (a problem with the whole text has none) and, when it is in an included file, that file's path as `file`. `features`, `menuCommands.strings` and `outside` count the script and its included files together and list up to 20 of the script's own lines. With a path and `includes: true`, the check also reads the files the `#include` lines name, and the files their own `#include` lines name. When the script or one of these files assigns a member chain or a File object to a variable, the check treats the variable as holding it in every file. The check looks for a name in the folder of the file that holds the `#include` line, then in each folder that the last `#includepath` line before it names, taken from that same folder. It reads only files inside the script's folder: it doesn't follow an absolute name, a name, folder or link that leads out of the script's folder, a name or folder longer than 4,096 bytes, a file outside the automation read root, or the `JSINCLUDE` environment variable, and it reads each file once. An include's `status` is `checked`, `missing`, `notFollowed`, `cycle` (the file it names includes the file the line is in, directly or through other files), `notRead` (includes weren't read: no `includes`, no path, or the web app), `limit` (one of the limits below was reached) or `unreadable` (a compiled file, or not text), and its `path` is the file found, relative to the script's folder. The check reads at most 8 MiB of a file and 32 MiB in all, follows 64 included files to a depth of 16 and looks up at most 1,024 paths for them, keeps 1,000 entries in each list, follows at most 1,000 variables that hold member chains and 1,000 that hold File objects (`truncated` is true when an entry or a variable was left out), and cuts the text it echoes from the script to 120 characters. File › Scripts › Check Script… picks a `.jsx`, `.jsxinc` or `.jsxbin` file and shows its report, and File › Open and drag-and-drop show the report for those files too; the web app checks the script without its included files. Over the control channel, `ui.checkScript {path? | data? | dataBase64?, name?}` does the same, and with a path it checks the included files too. ```json {"name":"run_command","arguments":{"command":"script.check","params":{"path":"/Users/me/Scripts/Tidy Layers.jsx","includes":true}}} ``` ```sh vectorcraft-cli run --cmd script.check --params '{"path": "Tidy Layers.jsx"}' ``` ## Transparency and opacity masks Transparency and opacity-mask commands take `ids` (or `id`), so they need no selection. Opacity is a percentage (0..100) in every command (`transparency.set`, `object.setProps`, `appearance.setItem`). `transparency.info` returns the Transparency panel's values, with `null` where the objects differ. Making a mask from one object gives it an empty mask and enters mask editing: art drawn then becomes the mask, and the mask commands act on the masked object until `transparency.stopEditingOpacityMask`. Saves and exports never include the editing layer. ```json {"name":"run_command","arguments":{"command":"transparency.set","params":{"ids":[12],"opacity":40,"blend":"Multiply"}}} {"name":"run_command","arguments":{"command":"transparency.makeOpacityMask","params":{"ids":[12,15],"invert":true}}} {"name":"run_command","arguments":{"command":"transparency.setOpacityMask","params":{"id":12,"clip":false}}} {"name":"run_command","arguments":{"command":"transparency.info","params":{"ids":[12,20]}}} ``` Knockout Group has three states: `transparency.set {knockout: "on"|"off"|"neutral"}` (`true` = on, `false` = neutral, the default). In a knockout group each child hides what it covers of the children below it; neutral groups pass the enclosing group's setting to their children, off groups never knock out. `knockoutShape: true` makes the object's opacity and opacity mask scale how much it knocks out. `transparency.togglePageKnockoutGroup` and `transparency.togglePageIsolatedBlending` (`{value?}`) treat the whole page as a knockout or isolated group; they are saved with the document and undoable, and `transparency.info` reports them. PDF and SVG write knockout groups as soft-masked groups with the same look (the PDF export reports a warning). Groups are not isolated unless Isolate Blending is on: a blend mode inside a group with opacity, a blend mode, an opacity mask, knockout or a clip reaches the art below the group, on screen and in PDF (a knockout group's elements then composite against the art below it). SVG has no non-isolated groups, so there such groups stay isolated. `transparency.viewOpacityMask {on?, id?}` is Alt-clicking the mask thumbnail: the canvas shows only the mask of `id` (default: the mask being edited, else the first selected masked object) as greyscale coverage (white = opaque) and the mask is edited; called again it shows the artwork, still editing. It is view state of the open document: the canvas (and `ui.screenshot`) shows it, exports never do, and it ends when mask editing ends. `view.transparencyGrid {on?}` (View → Show Transparency Grid; default: toggle) is view state of each open document, like the mask view: the grid shows behind the artboards of the documents that turn it on, on the canvas and in `ui.screenshot`, never in exports. It returns `{on}`, isn't undoable and doesn't mark the document changed. `view.globalRulers {global?}` (View → Rulers → Change to Global / Artboard Rulers, Cmd+Alt+R; default: toggle) is saved with the document and undoable. With artboard rulers (a new document's) the rulers count from the active artboard's top-left corner, or the zero point set for it; with global rulers, from one document zero point. Command coordinates stay absolute either way. `document.rulerOrigin {artboard?}` returns the zero point the rulers use with that artboard active; `document.setRulerOrigin {x, y, artboard?}` and `document.resetRulerOrigin {artboard?}` set or reset the document's (global rulers) or that artboard's (artboard rulers, kept relative to it so it follows the artboard). ```json {"name":"run_command","arguments":{"command":"view.globalRulers","params":{"global":true}}} ``` `view.videoRulers {}` (View → Rulers → Show / Hide Video Rulers) is a UI view flag, not saved with the document and not undoable. Show Video Rulers shows the rulers and has them count the active artboard in video pixels from its top-left corner, whatever the ruler origin: a pixel is as many points wide as the artboard's `videoRulerPixelAspectRatio` (`artboard.setProps`) and one point tall, so a 720 × 480 pt artboard at 0.9 reads 800 × 480. Hide Video Rulers puts the rulers back in the General unit from the ruler origin; they stay shown. It returns whether video rulers show. ## Clipping masks `object.clippingMask.make` clips the selected objects by the topmost one, which may be a path, a compound path or a text object (it loses its paint). Compound holes, even-odd fills, glyph outlines and the union of a group's members clip alike on screen, in raster export and in SVG and PDF. `object.clippingMask.release` turns the clip group into a plain group and keeps the clipping path with whatever paint it has. A clipping path can be painted after Make (`paint.setFill`, `paint.setStroke`, `stroke.set` with its `ids`): its fill paints behind the clipped art and its stroke over it, not clipped, on screen and in SVG and PDF. ```json {"name":"run_command","arguments":{"command":"select.set","params":{"ids":[12,15]}}} {"name":"run_command","arguments":{"command":"object.clippingMask.make","params":{}}} ``` `layer.clippingMask.toggle {id?}` is the Layers panel's clipping mask button: the top object of the layer `id` (default: the one highlighted layer or group row, else the one selected group, else the current layer) becomes its clipping path (unpainted, moved to the bottom of the layer, so art added later is clipped too); called again it releases the mask. It returns `{clip}`, and the Layers panel underlines clipping-path names. ## Object selection `select.set {ids}` replaces the selection, `select.add {ids}` adds to it, and `select.toggle {id}` or `select.toggle {ids}` toggles each target. Every id must be a non-negative integer naming an existing object. A malformed or unknown id is an error naming that value; the entire selection is left unchanged. All three commands return `{count, ids}` for the resulting selection, in selection order. An empty array clears the selection for `select.set` and leaves it alone for Add and Toggle. `select.key {id}` makes `id` the key object that Align aligns to and Distribute Spacing spaces from (`object.align` without `to`, `object.distributeSpacing` with `spacing`), and `select.key {}` cancels it. The id must name a selected top-level object. Layers, compound-path members and objects inside another selected object are not top-level objects. Any other id is an error naming that value, and the key is left unchanged. A null `id` is an error too. A click with the Selection tool on a selected top-level object, while two or more objects are selected or ruler guides are selected with it, runs `select.key {id}`, and a click on the key object again runs `select.key {}`. On a single selected object with no ruler guide selected, or on a group selected with only its own members, `select.key` sets no key. With a key object (or a key guide), `object.align` moves the other selected ruler guides too: a vertical guide goes to the key's left edge, centre or right edge, a horizontal one to its top, centre or bottom (#2143). Aligning to the selection or the artboard leaves guides where they are. `select.key {guide}` makes a selected ruler guide (its `guide.list` index) the key instead, while one or more objects are selected (#2143). `object.align` then lines the objects up on the guide and the guide stays put: a vertical guide sets where their left edges, centres or right edges go and vertical alignment leaves them where they are, a horizontal guide the other way round. Selected anchor points align to it the same way. `object.distribute {spacing}` and `object.distributeSpacing {spacing}` space the objects from it (see [Align to Artboard](#align-to-artboard)). A click with the Selection tool on a guide selected with objects runs `select.key {guide}`, and a click on the key guide again runs `select.key {}`. The key guide goes when it leaves the selection or no object is left selected, and an object key replaces it (and the other way round). It returns `{key}`, the key object's id or null, with `guide`, the key guide's index, when a guide is the key. `document.inspect` (the `inspect_document` tool) reports the key object as `key`, null without one, and the key guide as `keyGuide`. The `ui.pointer` and `pointer_gesture` replies list it as `key` beside `selection`. The command palette lists `select.key` as Cancel Key Object and runs it without an id. `edit.clear {ids?}` deletes the exact selected objects, including individual compound-path members; their unselected siblings remain in the compound. Supplying `ids` works without a selection and overrides selected anchors or guides. Every explicit id is validated before any deletion; a malformed or unknown id leaves the document unchanged. An empty `ids` array does nothing. Layers themselves are kept, and selecting an ancestor together with its descendants deletes that subtree once. Without `ids`, direct-selected anchors or selected ruler guides retain their usual Clear behavior. Cut still removes the objects it copied, including a whole compound when a member is selected. `object.setProps` with `visible: false` or `locked: true` deselects every selected object that is hidden or locked after the call: the selected objects it hides or locks, selected objects inside them, and selected objects that were already hidden or locked. The key object is cleared when it is one of them, and `edit.undo` selects them again with the key object. When the call changes nothing, it records no undo step and still deselects them. Other `object.setProps` calls leave the selection unchanged. `layer.move`, `node.move`, `layer.merge`, `layer.flatten`, `layer.collectInNew`, `dataset.select`, `dataset.next` and `dataset.prev` also deselect every selected object that is hidden or locked after the call, such as objects moved into a hidden or locked layer or group, or objects that a data set hides. The key object is cleared when it is one of them, and `edit.undo` selects them again with the key object. These Select menu commands select only the objects that `select.all` selects and the objects inside them: every `select.same.*` and `select.object.*` command except `select.object.slices`, `select.nextAbove`, `select.nextBelow` and `select.recall`. They leave out hidden and locked objects with the objects inside them, the objects on hidden, locked or template layers and sublayers and, in isolation mode, everything outside the isolated group or layer. When the isolated group or layer, or a group or layer it is in, is hidden, locked or a template layer, they also leave out everything in it, and `select.all`, `select.allOnArtboard` and `select.inverse` select nothing. `select.object.bristleBrushStrokes` selects the objects whose topmost stroke has a bristle brush (type `bristle` in `brush.list`), whatever the brush is named; a brush of another type with "Bristle" in its name does not count. `select.nextAbove` and `select.nextBelow` select the nearest such object above or below the first selected object among the objects beside it in its group or layer, passing over hidden and locked objects and sublayers. When there is no such object, the selection stays as it is. ## Global Edit Select › Start Global Edit is `select.globalEdit {appearance?, size?, artboards?, includeCanvas?}`. With one path, compound path or symbol instance selected, it finds the objects of the same shape (moved or scaled copies; not rotated ones), among those `select.all` would take and the objects in their groups, and returns `{active: true, count, ids}`, the selected object first. `appearance` also asks for the same fill and stroke, `size` for the same width and height; `artboards` is `"all"` or a range such as `"1, 3-5"`, and `includeCanvas: false` leaves out the objects on no artboard. Missing params come from `select.globalEditOptions` (the Global Edit Options, kept for the session; it returns them with `count`, the objects a start would take now). The selection stays the one object. Until the next `select.globalEdit` (Stop Global Edit, `{active: false}`), what an edit of a selected match changed is given to the other matches, in the same undo step, and nothing else. A changed outline or anchors go to each match in proportion to its size. A changed opacity or blending, and each changed setting of a fill or stroke (paint, weight, opacity, blending, effects…), are copied as they are to the match's fill or stroke of the same kind and rank; its other fills and strokes, and their unchanged settings, stay its own (a stroke weight change leaves the fills). A stroke weight set on the selected match is given as it is, and resizing the outline in proportion never scales a match's stroke weight. Hidden or locked matches are left as they are. A fill or stroke added or removed is added or removed on the matches whose fills and strokes lay in the same order; the others, and any other change to the list, take the whole list. Whenever the document is put back (Undo, Redo, a cancelled drag or scrub), the matches it no longer has leave the Global Edit, which ends when fewer than two are left. An edit that would take any match past the canvas fails with the coordinates-out-of-range error and changes nothing. Nothing of it is saved. ## Saved selections Select → Save Selection… keeps the selected objects under a name, in the document: `select.save {name?}` (default the first free "Selection N"; an existing name is replaced with the current selection; at most 25 per document, names up to 255 characters) → `{name}`, one undo step. `select.savedList` lists the names in menu order, and `select.recall {name}` selects those objects again, except the ones deleted since and the ones hidden, locked or outside the isolated group or layer (see Object selection) → `{count}`. Update Selection: `select.update {}` replaces the objects of the saved selection last recalled in the document with the selected objects → `{name}`, one undo step. The command is enabled while objects are selected and the document holds that saved selection, under its own name or a name `select.editSaved` gave it; Undo and Redo do not change which saved selection is recalled. Edit Selection… renames and deletes: `select.editSaved {name, newName?, delete?}`, or several at once as `{edits: [{name, newName?, delete?}…]}` in one undo step, where every `name` is the name before the edit and the names must stay unique. In the desktop app the saved selections are listed at the bottom of the Select menu (`select.recall1` … `select.recall25`) and in the Layers panel menu's Save Selection submenu (`select.recall {name}`). A file's saved selections are checked when it opens: past 25, blank or repeated names, and ids the document doesn't have are dropped. ## Ruler guides Ruler guides are numbered in the order they were made. `guide.add {vertical, pos, artboard?}` makes one (the x of a vertical guide, the y of a horizontal one, in points) → `{index}`. With `artboard` (an artboard's index) it is an artboard guide: it runs across that artboard only, moves with it (`artboard.move`, a pure move in `artboard.setProps`, `artboard.rearrange`), scales with it (`artboard.setProps {scaleArt}`), is copied with it (`artboard.duplicate`, `artboard.move {copy}`) and is deleted with it (`artboard.delete`); without, a canvas guide runs across the whole canvas. `guide.list` → `[{index, vertical, pos, selected, artboard?}…]` (`artboard` only for artboard guides). `guide.select {indexes: [index…], toggle?}` selects guides on their own (the art is deselected; `toggle` adds or removes them) → `{selected}`. `guide.move {index, pos}` puts one guide somewhere, `guide.move {dx?, dy?, copy?}` moves the selected ones (vertical guides by `dx`, horizontal ones by `dy`; `copy` leaves them and selects the moved copies), and `guide.remove {index?}` deletes one guide or the selected ones → `{count}`; each is one undo step. With guides selected, `edit.clear` (Delete) deletes them and `object.nudge` (the arrow keys) nudges them. View › Guides › Lock Guides (`view.guides.lock`) deselects them and the paths made into guides, in every open document, and refuses these commands until unlocked. While it is on, a command that selects art, such as `edit.undo`, `edit.redo`, `object.exitIsolation`, `object.unlockAll`, `object.showAll` or `edit.paste`, leaves the guides out. `view.guides.release {index?, ids?}` (View › Guides › Release Guides) turns guides into paths → `{count}`. With `index` it releases that ruler guide, and with `ids` it releases those guide paths (and the guide paths inside them). Otherwise it releases the selected guide paths and ruler guides, and with no guide selected it releases every guide path. A released ruler guide becomes a straight path with no fill or stroke, at the top of the guide's layer (or where new art goes, for a guide on no layer). An artboard guide becomes a line across its artboard, and a canvas guide becomes a line across the bounds of all the artboards. `index` and `ids` are refused while guides are locked. The release is one undo step, and the released paths are selected. With the Selection, Direct Selection or Group Selection tool, a double-click with Cmd/Ctrl and Shift held releases the ruler guide or guide path under the pointer (`pointer_gesture` with a `doubleclick` event and `mods: {cmd: true, shift: true}`), unless guides are hidden or locked. The Selection, Direct Selection and Group Selection tools pick a guide within the selection tolerance, over the art (an anchor on it comes first with Direct Selection), and drag the selected guides: `mods.alt` copies them, `mods.shift` snaps the dragged guide to the ruler's ticks, and otherwise it snaps to whole pixels, the grid or, with Smart Guides, the edges, centres (so the sides' midpoints) and anchors of the art and of the artboards; with Smart Guides off, View › Snap to Point pulls it onto anchors. In the desktop app a guide dragged out of a ruler snaps the same way and is made where it is released over the canvas (with the Artboard tool, as an artboard guide of the active artboard), a guide dropped off the canvas onto its ruler is deleted, and hidden guides (View › Guides › Hide Guides) can't be picked. An artboard guide is picked, drawn and snapped to across its artboard only. ```json {"name":"run_command","arguments":{"command":"guide.add","params":{"vertical":true,"pos":100}}} {"name":"run_command","arguments":{"command":"guide.add","params":{"vertical":false,"pos":200,"artboard":0}}} {"name":"pointer_gesture","arguments":{"tool":"selection","events":[{"kind":"down","x":100,"y":50},{"kind":"drag","x":140,"y":50},{"kind":"up","x":140,"y":50}]}} {"name":"run_command","arguments":{"command":"guide.list","params":{}}} ``` ## Symbols, brushes, variables and Image Trace These families are in the live catalog (`command_list` / `vectorcraft://commands`); the prompt `trace-and-style` already walks Image Trace. Short agent entry points: - Symbols: `symbol.list` (each row includes `registration` 0..8 and `exportType`: `movieClip`|`graphic`), `symbol.new {name?, registration?, exportType?, nineSlice?, nineSliceGuides?, ids?}`, `symbol.options {name?, newName?, registration?, exportType?, nineSlice?, nineSliceGuides?}` (`nineSlice` bool turns 9-slice guides on or off; `nineSliceGuides` is `[left, top, right, bottom]`, fractions 0..1 of the symbol size; rows of `symbol.list` carry `nineSlice`: the guides or `null`), `symbol.place {name?, x?, y?}`, `symbol.breakLink {ids?}`, `symbol.replace`, `symbol.edit`, `symbol.duplicate`, `symbol.selectInstances {name}`. `symbol.new` and `symbol.duplicate {name, newName?}` trim the white space around the name they are given, as `symbol.options` trims `newName`; a blank one gives "New Symbol" or " copy". - The Brushes panel menu's Select All Unused: `brush.unused` (a query) lists the brushes no stroke paints with, in library order, and the panel selects them. Strokes in the art (opacity masks included), symbols, graph designs, pattern tiles and graphic styles count, and so do the strokes in the art of a brush that is used. The art of a brush that is not used does not count, so a brush whose own art is the only art that paints with it is listed. - Brushes: `brush.list`, `brush.get {name}`, `brush.apply {name, ids?}`, `brush.new {type, name?, params?, ids?}`, `brush.setCurrent {name}`, `brush.options {name, params?, newName?}`, `brush.delete {name} | {names}`, `brush.duplicate {name, newName?} | {names}`, `brush.freehand`, `object.expandBrush {ids?}`. `brush.new`, `brush.duplicate` and `brush.options` trim the white space around the name they are given. A blank `name` gives the type's default name ("New Calligraphic Brush" for a calligraphic brush), and a blank `newName` gives "`` copy" in `brush.duplicate` and keeps the name in `brush.options`. A Hue Shift `colorization` given to `brush.new` or `brush.options` without a `key` takes the color that the art's fills and strokes use most (on a tie, the one found first), or black when they use no solid color. `brush.options` changes only the fields of `colorization` and of an art brush's `scale` that `params` gives, so `{colorization: {method: "hueShift"}}` keeps a Hue Shift brush's key. `brush.new` and `brush.options` take a bristle brush name of at most 31 characters: `brush.new` refuses a longer one, counting the number it adds to a taken name, and `brush.options` refuses a longer `newName`. `brush.duplicate` does not check the length. A bristle brush that already has a longer name keeps it, and `brush.options` without a new name, or with a `newName` that only trims the white space around the name, does not refuse. Other brush types take a name of any length. - Variables: `variable.list`, `variable.define {name, kind: text|visibility|image|graph}`, `variable.bind {variable, ids?}`, `variable.makeTextDynamic`, `variable.makeVisibilityDynamic`, `variable.makeImageDynamic`, `variable.makeGraphDynamic`, `variable.unbind`, `variable.rename`, `variable.delete`, `variable.highlight` (image values are file paths, graph values CSV as `graph.setData`). `variable.lock {locked?}` (Lock Variables) locks or unlocks the variables; without `locked` it toggles, and the lock is saved with the document. While the variables are locked, `variable.define`, `variable.delete`, `variable.rename`, the four `variable.make…Dynamic` commands and a `variable.loadLibrary` that would delete or define a variable (below) fail with "variables are locked"; `variable.bind`, `variable.unbind` and the `dataset.*` commands still run (except a `dataset.import` whose header would define a variable, below), and `variable.list` reports `locked`. - Data sets: `dataset.list`, `dataset.new {name, values?}`, `dataset.set {name, values?}`, `dataset.rename`, `dataset.delete`, `dataset.capture`, `dataset.update`, `dataset.select {name}`, `dataset.next` / `dataset.prev` (each `dataset.select` / next / prev is one undo step over every bound object), `dataset.import {path|data|dataBase64|csv}` / `dataset.export {path?}` (CSV: `name`, then one column per variable). `dataset.import` also reads tab-separated files, and files whose first header cell isn't `name`: every header cell is then a variable, and the rows take the first free names `Data Set 1`, `Data Set 2` and so on. An import error about a row's values or name gives the file line the row starts on. A header cell that names no variable defines one, unbound, in the same undo step as the rows. `#Shown` defines a visibility variable `Shown`, `@Photo` defines an image variable `Photo`, and a cell without a prefix defines a text variable. A prefixed cell whose name is already a variable of the prefix's kind names it; one whose name is a variable of another kind fails the import. A `%` cell that isn't a variable's exact name (graph data files) is refused. The reply's `defined` lists the variables the import defined. While the variables are locked, an import whose header would define a variable fails with "variables are locked". - Variable libraries: `variable.loadLibrary {path|xml|dataBase64}` (the Variables panel menu's Load Variable Library…) replaces the document's variables and data sets with a variable library's in one undo step. The library is XML whose `variableSets` element holds `variableSet` elements, each with a `variables` element of `variable` elements (`varName`, `trait`) and a `sampleDataSets` element of `sampleDataSet` elements (`dataSetName`). Every variable of the document is deleted with its bindings and values, and every data set is deleted. Then the library's variables whose `trait` is `textcontent` are defined as text variables, unbound. In each data set, the element named after a text variable gives its value: the text of its `p` elements, one paragraph each. An element without a `p` element gives no value. Variables of other kinds are left out with their values: the reply lists them in `skipped` (`name`, `trait`), and its `warning` names each `trait` with its count. The reply's `deleted` and `deletedDataSets` list the variables and data sets the load deleted. While the variables are locked, a library that would delete or define a variable fails with "variables are locked". The reply is `{variables, deleted, deletedDataSets, names, skipped, warning?}`. A library larger than 8 MiB, one whose elements nest more than 256 deep, and one with more than 256 variables or 1,000 data sets are refused. Its document type declaration may hold an external identifier, comments, processing instructions and up to 64 entity declarations whose values are plain text (no `<`, `&` or `%`). A declaration that holds anything else, and entity references that expand the library past 8 MiB, are refused. So are a variable or data set declared twice and a value for a variable the library doesn't declare. - Batch: `action.batch {commands, source?: folder|datasets, folder?, destination?: none|saveAndClose|folder, destinationFolder?, overrideOpen?, overrideSave?, overrideExport?, includeSubdirs?, stopOnError?}` plays an action on every file in a folder or every data set (the Actions panel menu's Batch…). Override Save and Override Export end the file name of a step with `format` in that `format`, in lower case and without a leading dot, when it is one of the format's extensions (`.svg` and `SVG` give `.svg`, `.JPEG` gives `.jpeg`), and otherwise in the format's first extension (`png8` gives `.png`, `template` gives `.vctemplate`). A step without `format` keeps the source file's extension (`.vectorcraft` over data sets). - Image Trace: `imageTrace.presets`, `imageTrace.make {id?, preset?, params?, view?}` (or `imageTrace.makeAndExpand`), `imageTrace.setView`, `imageTrace.expand`, `imageTrace.release`. - Live Paint: `livePaint.make`, `livePaint.merge`, `livePaint.fill {group?, point}`, `livePaint.strokeEdge`, `livePaint.info {group}`, `livePaint.release`, `livePaint.expand`, and `livePaint.gapOptions {ids?, detect?, size?, color?, closeGaps?}` (Object › Live Paint › Gap Options). With `detect: true`, paint stops at the gaps that run from a loose end of a path to the nearest path, up to `size` wide (`small` 3 pt, `medium` 6 pt, `large` 12 pt, or a number of points up to 72); `closeGaps: true` adds an unpainted path across each of them. The options are saved with the group, and the reply and `livePaint.info` list the gaps up to `size` wide that no path closes, as `[x0, y0, x1, y1]`. `livePaint.merge` keeps the bottom group's options when its Gap Detection is on, and otherwise turns Gap Detection on with `small` when another merged group has it on. When Gap Detection is off in every merged group, it stays off. `view.livePaintGaps` (View › Show Live Paint Gaps, or Hide Live Paint Gaps while it is on) turns the drawing of those gaps on or off. While it is on and edges show, the canvas draws the gaps of each selected Live Paint group in that group's Gap Preview Color. The reply is the new state (`true` when on). The Live Paint Selection tool's options (`tool.setOption {tool: "livePaintSelection", values}`; double-clicking the tool opens them, dialog `livePaintSelectionOptions`) are `selectFills` and `selectStrokes` (both on), `color` (the highlight color, `#rrggbb` or a layer color's name; `#ff2a2a`) and `width` (the highlight's width, 1 to 20 screen pixels; 4). With `selectStrokes` off, Alt-click selects the face under the pointer as a click does; with `selectFills` off, a click selects the edge nearest the pointer as Alt-click does; with both off, a click selects no face or edge. `livePaint.merge` keeps the bottom Live Paint group's id, name, lock, visibility, transparency, appearance, opacity mask and graphic style link. With a Live Paint group among the objects, `livePaint.make` adds the others to it as `livePaint.merge` does, and so does `livePaint.fill {ids}` when `point` is outside that group's faces. `livePaint.fill` does not fill a face that is hidden on its own. With `group`, a point on a hidden face is an error. With `ids`, at a point on a hidden face it fills the face there of another Live Paint group among the objects; otherwise it returns an error and does not merge the objects. `brush.selectionArt {ids?|id?}` (a query) replies `{art}`: the object that `brush.new` given the same `ids` or `id`, or the same selection, takes as a new scatter or art brush's art and as a new pattern brush's side tile. Several objects become one group, and `art` is `null` when nothing is selected or `ids` name no object. In the app, the Brushes panel's New Brush button, its menu's New Brush… and art dropped on the panel open New Brush (`ui.newBrush {ids?}`, dialog `newBrush`, field `type`). Scatter, art and pattern need art. OK opens that type's Brush Options for the new brush; for a scatter, art or pattern brush they preview the art from `brush.selectionArt`. Their OK runs `brush.new {type, name, params, ids?}`, and Cancel adds no brush. `dataset.select`, `dataset.next` and `dataset.prev` reply `{name, applied, skipped}`. They add `overflow: [id…]` when area type or type on a path whose text they rewrote doesn't fit, as `text.setText` does (see [Fitting area type](#fitting-area-type-auto-size-shrink-text-to-fit-overflow)). They skip an object that is locked or inside a locked layer or group, and count it in `skipped`. They rewrite hidden objects and objects inside a hidden layer or group, which stay hidden unless one of the data set's visibility values shows them. When such a layer is shown again, its type holds the data set's text. They apply the values in the order the variables are defined. Of two variables of the same kind on one object, the one defined last wins. `dataset.list`, `dataset.capture` and `dataset.update` read hidden objects and objects inside a hidden layer or group as well. ```json {"name":"run_command","arguments":{"command":"symbol.list","params":{}}} {"name":"run_command","arguments":{"command":"brush.list","params":{}}} {"name":"run_command","arguments":{"command":"variable.list","params":{}}} {"name":"run_command","arguments":{"command":"dataset.list","params":{}}} {"name":"run_command","arguments":{"command":"dataset.capture","params":{}}} {"name":"run_command","arguments":{"command":"dataset.select","params":{"name":"Data Set 1"}}} {"name":"run_command","arguments":{"command":"imageTrace.presets","params":{}}} {"name":"run_command","arguments":{"command":"command.batch","params":{"label":"Offset","commands":[{"command":"object.move","params":{"dx":10,"dy":10,"copy":true}}]}}} ``` When no object is bound to a variable that the data set has a value for, for example after `dataset.import` defines new variables without binding them, `dataset.select`, `dataset.next` and `dataset.prev` apply nothing and add `warning`, which the app's status bar shows: "nothing in data set A was applied: no object is bound to a variable it has a value for". A data set with a value for a bound variable gives no warning even when every object bound to it is locked; `skipped` counts those objects. `dataset.update {all?}` writes what the bound objects hold now into the active data set, for each variable that the data set holds. A variable whose objects report no value, because they are locked, inside a locked layer or group, or deleted, keeps the data set's value. With `all: true` it also adds every other variable that the objects report a value for. The reply gives `values`, the number of values it wrote, and `left`, the variables it did not write. ## Graphic styles A graphic style holds an appearance (fills, strokes, effects) plus opacity, blend mode, isolate and knockout. `graphicStyle.new {name?, id?}` captures an object (a group without its own fills or strokes lends its topmost object's, type its characters'). `graphicStyle.apply {name, ids?}` gives the objects the style and links them; `add: true` adds the style on top of the existing appearance instead. Linked objects stay linked while they keep the style's look: editing their appearance or transparency breaks the link, and `graphicStyle.redefine {name?, id?}` updates only the objects still linked. `graphicStyle.list` returns each style with the ids linked to it and the style of the first selected object; `select.same.graphicStyle` selects an object's fellow users. Styles keep placed gradients relative to the bounds of the object they were made from, and each object a style is applied to gets them at the same place relative to its own bounds. ```json {"name":"run_command","arguments":{"command":"graphicStyle.new","params":{"id":12,"name":"Glow"}}} {"name":"run_command","arguments":{"command":"graphicStyle.apply","params":{"name":"Glow","ids":[15,20]}}} {"name":"run_command","arguments":{"command":"graphicStyle.redefine","params":{"id":12}}} {"name":"run_command","arguments":{"command":"graphicStyle.list","params":{}}} ``` `graphicStyle.merge {names, name?}` adds a style stacking the fills, strokes and effects of two or more styles (each on top of the ones before it) with the first one's transparency; `graphicStyle.move {name, to}` reorders the list (what dragging a style in the panel does). Override Character Color (`graphicStyle.setOptions {overrideCharColor}`, the preference `overrideCharColor`, on by default) makes a style applied to type replace its characters' fill and stroke with the style's fills and strokes; off, the characters keep their colour under them. ## Editing appearance stacks `effect.move {from, to, fromItem?, toItem?, copy?}` reorders an effect or moves it between the object's effects (`null`) and a fill's or stroke's (an item index), as dragging its row in the Appearance panel does; `copy: true` copies it (Alt-drag). `appearance.duplicateItem {index, to?}` / `{indices}` and `appearance.removeItem {index | indices}` act on several fills/strokes at once, and `appearance.showAllHidden` makes every hidden fill, stroke and effect visible again. In remote mode, `effect.dialog {effect, index, item}` opens an applied effect's dialog prefilled (`ui.dialog.confirm` runs `effect.setParams`); see the control protocol for the "already applied" question. ## Overprint Each fill and stroke (and each character's fill and stroke) can overprint: its inks print over the inks below instead of knocking them out. `object.setOverprint {fill?, stroke?, item?, ids?}` sets it (groups set their contents, type its characters too; `item` aims at one appearance item) and `attributes.info {ids?}` reads it back (`null` where the objects differ). Overprint Preview (`view.overprintPreview`) and Separations Preview show it; rendering approximates it by darkening (the stronger ink of each plate wins, so a colour overprinting itself stays that colour). Older files that listed Overprint Black objects in the document get the flags on load. PDF export writes them with `advanced.overprint: "preserve"` (the default): each overprinting fill and stroke sets a graphics state with `/OP true /op true /OPM 1` (`/OPM 0` in PDF/A). With `discardWhiteOverprint` (`document.setup`, on by default) white ones knock out instead; `"discard"` writes none. ```json {"name":"run_command","arguments":{"command":"object.setOverprint","params":{"ids":[12],"stroke":true}}} {"name":"run_command","arguments":{"command":"attributes.info","params":{"ids":[12]}}} {"name":"run_command","arguments":{"command":"view.overprintPreview","params":{"on":true}}} ``` ## Revolve (initial 3D effect) `effect.apply {effect: "threeD.revolve", params: {...}, ids?, item?}` applies a live Revolve to a path or compound path. `effect.dialog` opens compact options with artboard rotation and light gizmos, `effect.setParams` edits it, and `effect.expandAppearance` bakes it into shaded vector faces. Native saves keep the editable source; SVG/PDF exports bake the same faces the canvas draws. Only one Revolve is supported per profile. Parameters: `angle` (degrees, 0..360, default 360), `offset` (points, 0..100000, default 0), `edge` (`"left"` or `"right"`), `rotationX/Y/Z` (degrees, defaults 0/0/0), `perspectiveAngle` (degrees, 0..160, default 0; an earlier `perspective`, 0..100%, is read as the angle that shows the same view), `segments` (8..128, default 64), `shade` (default true), `lightColor` (opaque `#rrggbb`, default `#ffffff`), `lightIntensity` (0..100%, default 80), `lightRotation` (degrees, -180..180, default 125.26; 0 on the object's right, counterclockwise), `lightHeight` (degrees, 0..90, default 30; 90 straight in front), `lightBehind` (default false), `lightSoftness` (0..100%, default 0), `ambient` (Ambient Light, 0..200%, default 25; an earlier `lightAzimuth` and `lightElevation` are read as the same light), `cap` (boolean, default true), and `expandVisibleOnly` (boolean, default true). A vertical-line profile needs a positive offset to have a radius. `cap` closes each circular endpoint of an open profile with one curved, uniformly shaded vector path. Body faces share that curved rim. Partial sweeps use sectors and leave their angular cut sides open. Closed or coincident-end profiles are unchanged. Missing `cap` enables caps; set it to false for open endpoints. Shaded Revolve surfaces automatically interpolate curved-surface lighting with vector gradients while preserving profile corners and flat caps. There is no separate smoothing parameter. `shade: false` disables all lighting. Visibility of intersecting surfaces is approximate. `lights` is null by default, retaining the earlier single-light controls. Set an array of one to ten objects to replace that light: `{color: "#rrggbb", intensity: 0..100, rotation: -180..180, height: 0..90, softness: 0..100, behind: bool}`. Defaults per light are white, 80%, 125.26 degrees, 30 degrees, 0%, false. Omitted fields use defaults; unknown nested keys and malformed lists are refused before mutation. `ambient` is shared and counted once. Set `lights: null` to restore the earlier controls. The list is saved in the native file and survives preview, undo and expansion. ```json {"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"lights":[{"color":"#ff8040","rotation":135,"height":35,"intensity":70},{"color":"#4080ff","rotation":-45,"height":20,"intensity":40}],"ambient":20}}}} ``` `lightColor` changes only the earlier directional light; ambient stays neutral. Invalid command colors are refused before editing, and invalid loaded colors fall back to white. Colored smooth faces use ordinary RGB vector gradients; source gradients keep their placement and opacity with vector blending/masks. Pattern tiles retain neutral shading. The dialog's Lighting tab manipulates the same `lightRotation` and `lightHeight` parameters; its guide visibility and tab choice are UI state, excluded from saved effect parameters. `expandVisibleOnly` is saved with the effect. It trims covered portions of opaque solid surfaces only when running `effect.expandAppearance`, preserving visible interiors through openings. Set it to false to expand all faces. Live preview and SVG/PDF baking keep the full surface. Transparent/unknown paint, non-normal blending, later geometry effects or bounded visibility failures preserve all faces. See [Revolve](revolve.md) for the scope and limitations. ```json {"name":"run_command","arguments":{"command":"effect.apply","params":{"effect":"threeD.revolve","item":null,"params":{"angle":360,"offset":20,"edge":"left","rotationX":0,"rotationY":0,"rotationZ":0}}}} {"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"angle":180,"lightRotation":30,"lightColor":"#ff8040","cap":true}}}} {"name":"run_command","arguments":{"command":"effect.setParams","params":{"index":0,"item":null,"params":{"expandVisibleOnly":true}}}} {"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{}}} ``` ## 3D lighting presets `effect.lightingPreset {index: int, preset: "standard" | "diffuse" | "topLeft" | "right", item?, ids?, target?}` replaces the lighting rig and ambient of an existing Revolve, Extrude & Bevel, Inflate or Rotate effect. `item: null` addresses object-level effects; an item index addresses a fill/stroke's effects. All targets must have a 3D effect at `index`, otherwise nothing changes. The operation is one undo step and preserves Shading, geometry, source paint and the view. The four original rigs use one white front light at 80% intensity. Standard keeps the existing default; Diffuse uses 100% Softness and 35% Ambient Light, Top Left uses Rotation 135 and Height 45, and Right uses Rotation 0 and Height 30. Full values and the public behavior reference are in [3D lighting presets](three-d.md#presets). These are original settings, not imported preset files. The document stores their numeric settings; manual edits remain possible through `effect.setParams`. ```json {"name":"run_command","arguments":{"command":"effect.lightingPreset","params":{"index":0,"item":null,"preset":"topLeft"}}} ``` ## Extrude & Bevel, Inflate and Rotate `effect.apply {effect: "threeD.extrude" | "threeD.inflate" | "threeD.rotate", params: {...}, ids?, item?}` applies the other 3D types to a path or compound path, live like Revolve (the source stays editable; SVG/PDF exports and `effect.expandAppearance` use the same shaded faces). An object takes one 3D effect, Revolve included. `threeD.extrude`: `depth` (points, 0..2000, default 50), `twist` (degrees, 0..360, 0), `taper` (1..100%, 100), `cap` (true solid, false hollow), `bevel` (`none`, `classic`, `rounded`, `cove`, `step`, `ogee`), `bevelWidth` and `bevelHeight` (points, default 4), `bevelRepeat` (1..32), `bevelSpace` (0..90%), `bevelInside`, `bevelBothSides`. `threeD.inflate`: `depth` (points, default 0), `twist` (degrees, 0..360, 0), `taper` (1..100%, 100), `cap` (true solid, false open behind), `volume` (0..100%, default 50), `bothSides`. All three take `rotationX/Y/Z` (degrees, -180..180, default the Off-Axis Front position -21.88/-23.4/9.06), `perspectiveAngle` and the lighting parameters as Revolve. See [three-d.md](three-d.md) for the Position presets, the scope and what differs from the public documentation. ## Edit Colors and Recolor Artwork `edit.colors.invert`, `edit.colors.toCMYK`, `edit.colors.toGrayscale`, `edit.colors.toRGB`, `edit.colors.saturate`, `edit.colors.adjustBalance` and `recolor.apply` recolour everything inside the selection (or `ids`) in one undo step: fills, strokes, text, gradient stops, gradient-mesh points, embedded images (a recoloured copy of the image; linked images are left alone) and the tiles of pattern fills and strokes (a recoloured copy saved as a new pattern swatch, e.g. "Dots 2"; the original pattern is untouched). `includeImages: false` / `includePatterns: false` leave images or patterns out. The Blend commands also grade gradient meshes, which keep their shading. ```json {"name":"run_command","arguments":{"command":"recolor.colors","params":{}}} {"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":{"#ff0000":"#0055ff"},"includeImages":false}}} {"name":"run_command","arguments":{"command":"edit.colors.adjustBalance","params":{"mode":"cmyk","m":-20,"k":10}}} ``` Recolor Artwork keys colours by identity, not hex: `recolor.colors` lists each colour's `key` (`"rgb 255 0 0"`, `"cmyk 0 100 100 0"`, `"gray 40"`, `"lab 55 60 -40"`; RGB in 0–255 levels, CMYK and Gray in percent), so CMYK, Lab and RGB colours that look alike stay apart, and every colour parameter also accepts a key. `recolor.reduce {colors?, method?, preserve?, limitTo?}` groups the selection's colours into rows of similar colours (k-means in Lab, weighted by use; tints of a global swatch share its row; White and Black are preserved by default) and returns a map of rows `[{from: [keys], to: key}]` to edit and pass to `recolor.apply {map, method?, model?, limitTo?, group?, groupColors?, rename?}`. For scripts and MCP, `model` selects the replacement color model: `keep` (default) retains each original color's model, `new` takes the replacement's model, and `document` converts replacements and edited color groups to the document's model. Invalid values fail before editing. This parameter is not a control in the Recolor Artwork dialog. A returned `note` reports a model conversion in `keep` mode. `method` says how a row's colours take its new colour: `exact`, `preserveTints` (tints of the row's darkest colour stay tints), `scaleTints` (every colour becomes a tint as light, relative to the darkest), `tintsShades` (lighter and darker than the row's average become tints and shades) or `hueShift` (the most saturated colour takes the new colour, the others turn by the same hue). `limitTo` snaps new colours to a swatch library's nearest colour; `recolor.randomize {map, order?, saturationBrightness?, seed?}` shuffles or varies new colours; a row with `exclude: true` keeps its colours. `swatch.editGroup {group, colors, rename?}` rewrites a colour group in one undo step (art linked to its global swatches follows), and `recolor.apply` with `group` recolours the art and rewrites the group together. ```json {"name":"run_command","arguments":{"command":"recolor.reduce","params":{"colors":2,"preserve":{"grays":true}}}} {"name":"run_command","arguments":{"command":"recolor.apply","params":{"map":[{"from":["cmyk 0 100 100 0","cmyk 0 40 40 0"],"to":"cmyk 100 50 0 0"}],"method":"scaleTints"}}} {"name":"run_command","arguments":{"command":"swatch.editGroup","params":{"group":"Brights","colors":["#ff0000","cmyk 0 0 100 0"]}}} ``` ## Group, layer and type appearance Groups and layers carry fills, strokes and effects of their own, as in the reference app: their fills and strokes paint every member's geometry, and their effects apply to the members as one piece (one combined drop shadow; a Transform or Warp moves or bends the whole group). Their Contents row (type: Characters) is a slot in the stack: fills and strokes above it paint over the members (characters), those below under them; new items go above it (on an image, below it). `appearance.moveItem {"from": "contents", "to": n}` puts the row above the bottom `n` items, and an item move takes `contents` to say where the row ends up in the same undo step; `document.node` shows the slot as `appearance.contents_index`. The `appearance.*`, `effect.*` and `graphicStyle.apply` commands edit the selected objects themselves (a group's own stack; layers through `ids`) or, with `target: "contents"`, the objects inside the groups and layers. `appearance.targetContents` selects a group's members (double-clicking the Contents row). SVG and PDF export bake a group's own fills, strokes and geometry effects into paths; its raster effects stay a filter on the whole group in SVG and become an image of the whole group in PDF. A geometry effect on one of type's own fills or strokes (`effect.apply` with `item`) reshapes the whole outline that the fill or stroke paints, and SVG, PDF, EPS and the other vector exports write such type as paths. On type that holds inline graphics, those effects are drawn and exported only when the type has a geometry effect or perspective of its own. An image's own fills and strokes draw only when they carry a visible Convert to Shape or Outline Object effect: Convert to Shape sizes its shape from the image's box, Outline Object gives the image's frame, the effects after it reshape that shape, and the effects before it are left out. Its Contents row stands for the image; `appearance.addFill` and `appearance.addStroke` put a new item right below that row, so it paints under the image. SVG, PDF, EPS and the other vector exports write those items as paths, and `effect.expandAppearance` turns them into objects in a group with the image. Use Preview Bounds measures an image by its frame. ```json {"name":"run_command","arguments":{"command":"appearance.addFill","params":{"ids":[7]}}} {"name":"run_command","arguments":{"command":"appearance.moveItem","params":{"ids":[7],"from":"contents","to":1}}} {"name":"run_command","arguments":{"command":"effect.apply","params":{"ids":[2],"effect":"stylize.dropShadow"}}} {"name":"run_command","arguments":{"command":"appearance.addStroke","params":{"ids":[7],"target":"contents"}}} ``` ## Swatch libraries Swatch libraries are read-only sets of swatches computed in code (Web Safe 216, Grays and Neutrals, Earth Tones, Skin Tone Ramps, Pastels, Brights, Metallic Gradients, Perceptual Scales, Harmony Sets). `swatch.library.list` lists them (`id`, `name`, `category`, `count`) and `swatch.library.get {library}` returns one's swatches and colour groups in the `swatch.list` shape (`library` is an id or a name). `swatch.library.add {library, names?}` copies swatches into the document as one undo step: a colour group's name brings the whole group, a swatch's name the swatch alone, no names the whole library; swatches the document already has (same name and paint) are reported under `existing` and not added again. `apply: "fill"|"stroke"` also applies the first one, in the same undo step (what clicking a swatch in the library panel does). `swatch.resetDefaults {replace?}` brings back the missing default swatches. ```json {"name":"run_command","arguments":{"command":"swatch.library.get","params":{"library":"earth-tones"}}} {"name":"run_command","arguments":{"command":"swatch.library.add","params":{"library":"earth-tones","names":["Clay"]}}} ``` `swatch.library.save {path?, format?: "vcswatches"|"gpl"|"ase"|"css", names?, name?, user?}` writes the document's swatches as a library (`.vcswatches` keeps colour models, global, spot, gradients and colour groups; `.gpl` is 8-bit RGB; a swatch exchange `.ase` file keeps solid colors in their own model (RGB, CMYK, Lab or Gray) as global, spot or process colors, and color groups, writes a tint swatch as the color it shows and leaves gradients out; CSS writes custom properties); without `path` it returns `{data}`, or `{dataBase64}` for `.ase`, and `user: true` saves into the user library folder of the desktop app (listed as category `user`, User Defined). The reply's `count` is the number of swatches written: `.gpl` and `.ase` hold solid colors only, and CSS leaves freeform gradients out. `swatch.library.load {path? | data? | dataBase64?, name?}` loads a `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or color book (`.acb`) file, or another document's swatches, as a library to add from. From an `.ase` file it reads RGB, CMYK, Lab and Gray colors as global, spot or process swatches and keeps their color groups. A color book (such as a Pantone book you own) gives its RGB, CMYK or Lab colors named with the book's prefix and suffix ("PANTONE 185 C"), as spot colors unless the book marks them as process colors; `.acb` files are read, never written. A file in the user library folder, or a file with the same extension and bytes as one there, loads as that User Defined library (category `user`). `swatch.library.copyToUser {library}` copies a loaded library into the user library folder of the desktop app (a library file as it is; a document's swatches or a library loaded from `data` or `dataBase64` as `.vcswatches`) under a name no file there has, and lists it as User Defined from then on; until the app quits, commands given its loaded id use the copy → `{library, name, count, path, copied}` (`copied: false` when the folder already held the same file). Without a user library folder the command is disabled. ## Graphic style libraries Graphic style libraries are read-only sets of graphic styles generated in code (Shadows and Glows, Outlines and Rules, Hand-Drawn, Gradient Finishes, Shape Effects, Blends and Transparency). `graphicStyle.libraries` lists them (`id`, `name`, `category`: `builtIn`, `user` or `loaded`, `count`) and `graphicStyle.library {library}` returns one's styles in the `graphicStyle.list` shape (`library` is an id or a name). `graphicStyle.addFromLibrary {library, name? | names?}` copies styles into the document's Graphic Styles as one undo step (no names: the whole library); styles the document already has (same name and look) are reported under `existing`, a name another look has gets a number, and the patterns the styles paint with come along. `apply: true` also applies the first one to `ids` or the selection (`add: true` adds its appearance on top instead), in the same undo step: what clicking a style in the library panel does. Library styles keep placed gradients in unit-box space, so each object gets them at the same place relative to its bounds. ```json {"name":"run_command","arguments":{"command":"graphicStyle.library","params":{"library":"hand-drawn"}}} {"name":"run_command","arguments":{"command":"graphicStyle.addFromLibrary","params":{"library":"hand-drawn","name":"Crosshatch","apply":true}}} ``` `graphicStyle.saveLibrary {path?, names?, name?, user?}` writes the document's styles as a `.vcstyles` library (JSON: the styles unlinked from swatches, with their opacity, blend mode, isolate and knockout, and the patterns they paint with); without `path` it returns `{data}`, and `user: true` saves into the user library folder of the desktop app (category `user`, User Defined). `graphicStyle.loadLibrary {path? | data? | dataBase64?, name?}` loads a `.vcstyles` file, or another document's graphic styles, as a library to add from; a file in the user library folder, or a file with the same extension and bytes as one there, loads as that User Defined library. ## Symbol libraries Symbol libraries are read-only sets of symbols: one generated in code (Basic Shapes), the `.vectorcraft` files in the user library folder of the desktop app (User Defined) and the symbols of any document loaded with `symbol.loadLibrary`. `symbol.libraries` lists them (`id`, `name`, `category`: `builtIn`, `user` or `loaded`, `count`) and `symbol.library {library}` returns one's symbols (`name`, `size`, `registration`, `exportType`; `library` is an id or a name). `symbol.addFromLibrary {library, name? | names?}` copies symbols into the document's Symbols as one undo step (no names: the whole library), with their Symbol Options and the resources they use: the symbols they nest, patterns, swatches (`swatchConflict` as for `edit.paste`), brushes, graphic styles and images. Symbols the document already has (same name and art) are reported under `existing`; a name another symbol has gets a number, and `names` gives each one's name in the document. The first one becomes the current symbol. `place: true` also places an instance of it with its registration point at `x`, `y` (default: the first artboard's centre) in the same step: what dragging a symbol out of the library panel onto the canvas does. ```json {"name":"run_command","arguments":{"command":"symbol.addFromLibrary","params":{"library":"basic-shapes","name":"Star","place":true,"x":120,"y":80}}} ``` `symbol.saveLibrary {path?, names?, name?, user?}` writes the document's symbols as a symbol library: a `.vectorcraft` document holding only them and what they use; without `path` it returns `{dataBase64}`, and `user: true` saves into the user library folder (category `user`, User Defined). `symbol.loadLibrary {path? | data? | dataBase64?, name?}` loads the symbols of any document VectorCraft opens as a library to add from; a file it can't read is an error. ## Library panels Each swatch, graphic style or symbol library opens in a read-only panel of its own (#2603), as in Illustrator, and several can be open at once. `window.swatchLibrary {library}`, `window.graphicStyleLibrary {library}` and `window.symbolLibrary {library}` (remote mode, `invoke_menu`) open one floating, or bring its panel forward when it is already open, and answer `{open, panel, name, count}`; `library: null` closes every panel of that kind. The panel id is `library::` (`library:swatches:pastels`, `library:graphicStyles:hand-drawn`, `library:swatches:loaded/C:/Palettes/Brand.ase`), and the panel commands take it like any other panel's: `window.panel.float {panel, onto?, x?, y?}` groups or moves it, `window.panel.dock {panel}` docks it, `window.panel.close {panel}` closes it. `window.swatchLibrary.other {path}` and `window.graphicStyleLibrary.other {path}` load a file and open it the same way. `ui.libraryPanel {panel, library?, view?, showFind?, persistent?}` sets one panel: `library` shows another library of its kind in that panel (what Previous Library and Next Library do; a library open in a panel of its own is brought forward instead), `view` is `smallThumb`, `mediumThumb`, `largeThumb`, `smallList` or `largeList`, `showFind` shows or hides the find field, and `persistent: true` is the panel menu's Persistent: the panel reopens when the app starts, with its view and find field, where the saved layout had it (or floating). A library loaded from a file is read again from its path. When the file is gone, the panel still reopens and stays in the preferences: its tab shows the file's name, its body shows `Missing:` with the file's path and a Locate Library… button, and the status bar names the file. A persistent panel whose User Defined library's file is deleted while the app runs stays open with the same tab and `Missing:` body; any other panel closes. A closed persistent panel can still be set, so `persistent: false` stops it reopening. The reply is `{panel, kind, library, view, showFind, persistent, open}`, and `ui.inspect` lists every panel's settings under `ui.library_panels`. `ui.libraryPanel.locate {panel, path?}` is Locate Library…: it loads the library at `path` (without one, it asks for a file) as Other Library… does and shows it in the open panel `panel`, which keeps its view, find field and Persistent, and answers as `ui.libraryPanel` does. When the file's library already has a panel open, `panel` closes and is forgotten, and that panel is brought forward, made persistent when `panel` was. It runs on any open panel whose library is missing, with Persistent on or off. On a panel whose library isn't missing, it answers an error without asking for or loading a file, and the panel keeps its library. `window.swatchLibrary` and `window.graphicStyleLibrary` also take a missing library's id, so a closed missing panel opens again, and an open panel whose library is missing is brought forward with Persistent on or off; they answer its file's name without the extension and `count: 0`. ```json {"name":"invoke_menu","arguments":{"command":"window.swatchLibrary","params":{"library":"pastels"}}} {"name":"invoke_menu","arguments":{"command":"ui.libraryPanel","params":{"panel":"library:swatches:pastels","view":"smallList","persistent":true}}} {"name":"invoke_menu","arguments":{"command":"window.panel.close","params":{"panel":"library:swatches:pastels"}}} ``` ## Libraries The Libraries panel's libraries hold graphics, colours and character and paragraph styles that any document can use. They are kept on this machine: each is a `.vclibrary` file (JSON) named after the library (`CON-.vclibrary` for a library named after a Windows device such as `CON`) in the `Libraries` folder next to the preferences (none in the web app and headless sessions, where they last for the session). `library.list` lists them (`id`, `name`, the count of each kind, `current`, `folder`); `library.get {library?}` returns one's items (graphics with `id`, `name`, size and a PNG `thumbnail` in base64; colours with `name`, `hex` and `color`; styles with `name` and `attrs`). `library.create {name?}`, `library.rename {library?, name}`, `library.delete {library?}` and `library.setCurrent {library}` manage them; `library` is an id or a name, and without it the current library (the one the panel shows) is meant. `library.add {library?, kind, ids?, name?, color?}` adds from the selection: `graphic` (a copy of the objects with the images, symbols, patterns, swatches, styles and brushes they use, as Copy takes them), `fillColor` / `strokeColor` (the first selected object's solid colour, else the default one, or `color`), `charStyle` / `paraStyle` (the selected text's attributes). An item the library already has is reported as `existing`. `library.use {library?, kind, item, center?, to?}` uses one: a graphic is placed centred on `center` (pasted with its resources; a swatch name the document gives another colour merges into the document's), a colour paints the selection's fill or stroke, and a style is added to the document under its name without the white space around it (numbered when that name is taken by other attributes) and applied to the selected text, each as one undo step. `library.removeItem {library?, kind, item}` removes one. Items can be put in user-named groups, which the panel shows as collapsible sections after the ungrouped items (its search field filters every item by name; `library.get` lists them all to filter). `library.createGroup {library?, name?, items?: [{kind, item}]}` makes one (a name another group has, ignoring case, gets a number) with those items moved into it; `library.renameGroup {library?, group, name}` renames one; `library.deleteGroup {library?, group}` deletes one, its items staying in the library ungrouped; and `library.moveItem {library?, kind, item, group?, before? | after?}` moves an item into a group (without `group`, out of its group) and, with `before` or `after` (another item of its kind), just before or after that item in the order its kind is listed and shown in. `library.moveGroup {library?, group, to}` moves a group to place `to` of the list (0: first; past the end: last). Dragging items and group headers in the panel runs these. An item is in one group at most; `library.get` lists the groups as `groups: [{name, items: [{kind: "color"|"charStyle"|"paraStyle"|"graphic", item}]}]` (a graphic by its id), and each kind's items in the order the panel shows them. `library.export {library?, path?}` writes a library (its colours, styles, graphics with their art and thumbnails, and its groups) as a `.vclibrary` file, the JSON the library folder keeps (without `path`: `{data}`, the file's text). `library.import {path? | data? | dataBase64?, name?}` adds the library of such a file as a new library and makes it the current one; a name another library has gets a number (`Brand (2)`). Imported files are checked: at most 10,000 items of each kind and 1,000 groups, names clipped and made unique, thumbnails kept only when they are small PNGs, and groups naming items that aren't there dropped. Files written before groups load as they are. ```json {"name":"run_command","arguments":{"command":"library.add","params":{"kind":"graphic","name":"Logo"}}} {"name":"run_command","arguments":{"command":"library.use","params":{"kind":"graphic","item":"Logo","center":[300,200]}}} {"name":"run_command","arguments":{"command":"library.createGroup","params":{"name":"Logos","items":[{"kind":"graphic","item":"Logo"}]}}} {"name":"run_command","arguments":{"command":"library.export","params":{"path":"/tmp/Brand.vclibrary"}}} ``` ## Strokes on type `stroke.set` without an `item` gives type its characters' stroke: weight, cap, join, miter limit, `align` and the dash options go to every run (the object keeps no stroke of its own); an `item` still edits an object-level stroke added with `appearance.addStroke`, which takes every stroke option (dashes, profile, opacity, blend) like a path's and paints under or over the characters as its place relative to the Characters row says. To stroke some characters, use `text.setRangeStyle {id, start, end, strokeOptions: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?}}`. `align: "inside"|"outside"` keeps the characters' stroke on that side of the glyph outlines, drawn at twice the weight and clipped, while the type stays editable (beyond Illustrator); SVG export writes the text live and that stroke as glyph outlines over it. `inspect_document` reports each object's stroke as `strokeOptions` (type: its first run's) in `stroke.set` terms. `stroke.set` on a group leaves the images and symbol instances in it alone. ## Inline graphics in type A document symbol can sit in a line of type like a character (beyond Illustrator; like InDesign's inline anchored objects), e.g. mana symbols in card rules text. `text.insertInline {id?, at?, symbol?, scale?: 1, shift?: 0}` inserts one at byte offset `at` (default: the Type tool's selection, which it replaces) of text `id` (default: the text being edited), showing `symbol` (default: the Symbols panel's current symbol) → `{id, caret}`; Type → Insert Inline Symbol does the same at the caret. The graphic is one character, U+FFFC, in a run of its own: `text.getRange` reports it as `{text: "\uFFFC", style, inline: {symbol, scale, baseline_shift}}`, plain text (and copied text) shows U+FFFC, and `text.editRange` with styled `runs` can insert one. Typing over it, Backspace and Delete remove it whole; neighbouring text never merges into it. Layout: the art is scaled uniformly to `scale` × the run's font size tall, its left edge on the pen and its vertical centre on the middle of the cap height raised by `shift` points. Its advance is the scaled art width plus the run's tracking, so it breaks like a word (no break between it and punctuation stuck to it) and justifies like a glyph; a graphic taller than the font's ascent or descent opens up its line. Its art follows the symbol (Redefine Symbol resizes it); a missing or deleted symbol leaves an empty slot one em square that draws nothing. Its em box is its run's, whatever its scale: Character Alignment moves it with its run's text (it is one of the line's largest characters only when its run's size is), and Top-to-Top leading spaces its line by its run's em box (Roman leading by its run's leading). In right-to-left text it is a bidi neutral (U+FFFC): it takes the direction of the text around it and the line's reordering places it. The canvas draws the art inside the type's transparency (its opacity and blend mode apply). SVG keeps the text live (the text after a graphic is positioned after it) and writes each graphic as a `` of the symbol's `` def (or a copy of its art); type on a path with graphics is written as outlines. PDF draws the art as vector paths where the layout puts it. ## Flatten Transparency `object.flattenTransparency {ids?, preset?, …options}` turns transparent art into opaque art that looks the same, in one undo step. The targets split into groups of overlapping objects; groups without transparency stay as they are. Each other group becomes one group of atomic regions: paths filled with the flat colour the art showed there (over white; over nothing, keeping alpha, with `preserveAlpha: true`), plus one image where gradients, patterns, images, opacity masks or raster effects reach (clipped to those regions with `clipComplexRegions`, a rectangle without). `preset` is `high`, `medium` (the default) or `low`; option keys (`balance` 0–100, `lineArtPpi`, `gradientPpi`, `textToOutlines`, `strokesToOutlines`, `clipComplexRegions`, `antiAlias`, `preserveAlpha`, `preserveOverprints`) override it, at the top level or in `options`. `balance: 0` rasterizes everything; lower balances rasterize groups that split into many regions. The result reports `{ids, vector, rasterized, options}`. When an image in the art can't be decoded, the image that replaces the art does not show it, and the result also carries `warning`: "an image couldn't be decoded and is missing from the picture", the note raster exports give. ```json {"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"ids":[12,15],"preset":"high"}}} {"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"balance":0,"lineArtPpi":150}}} ``` ## Pattern editing display In pattern editing mode the tile edge (`pattern.options {showTileEdge}`) and the swatch bounds (`pattern.options {showSwatchBounds}`: the part of the tiling the swatch repeats, dashed) are drawn in the preference `patternTileEdgeColor`. `object.pattern.make {name?}`, `object.pattern.saveCopy {name?}` and `pattern.options {newName?}` trim the white space around the name they are given. The first two number a name that a pattern, a swatch or a color group has ("Dots 2"), and `pattern.options` refuses a name that another pattern, a swatch or a color group has. It takes a name that only the pattern's own swatches have (a name Swatch Options gave the pattern's swatch, for example) when none of them still has the old name. A rename gives the new name to the pattern's swatches that had the old one; any other swatch that has the old name keeps it. A blank name gives "New Pattern N" in `object.pattern.make` and "`` copy" in `object.pattern.saveCopy`, and `pattern.options` keeps the name. ```json {"name":"run_command","arguments":{"command":"pattern.options","params":{"showSwatchBounds":true}}} {"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"patternTileEdgeColor","value":"#ff4f4f"}}} ``` The Eyedropper: `appearance.copyFrom {source}` copies what the Eyedropper Options pick up and apply from `source` to the selection (`ids`); `reverse: true` copies the selection's attributes onto `source` (Alt-click) and `append: true` adds the source's fills and strokes on top of each target's stack (Shift+Alt-click). The options live in the preferences: `eyedropper.setOptions {sampleSize?, pickUp?, apply?}` (or `prefs.get`/`prefs.set` with key `eyedropper`) reads and sets them; a tree names flags under `appearance` (`transparency`, `fill` `{color, transparency, overprint}`, `stroke` `{color, transparency, overprint, weight, cap, join, miter, dash}`), `character` and `paragraph`, and a bool for a branch sets all of it. `paint.sampleColor {color}` puts a sampled color into the active proxy (in a CMYK document an RGB color is stored as CMYK, as `paint.setFill` stores it). Clicking an image samples its pixels: a CMYK image (a four-component JPEG, or an 8- or 16-bit chunky CMYK TIFF) gives its ink amounts as they are, and an RGB image's sRGB pixel is taken into the working RGB space. ```json {"name":"run_command","arguments":{"command":"eyedropper.setOptions","params":{"pickUp":{"appearance":{"stroke":{"weight":false}}}}}} {"name":"run_command","arguments":{"command":"appearance.copyFrom","params":{"source":12,"ids":[7,8]}}} ``` ## The Layers panel: rows, layers and sublayers Every layer, sublayer, group and object is a row of the Layers panel. Sublayers are layers inside layers: they are not objects, so clicking or marquee-selecting art in a sublayer, Select All and the other Select commands take the art itself, and each sublayer has its own colour for the selection highlight. `document.inspect` lists the tree (layers report `color`, `template`, `printable`, `preview`, `dimImages` and `clip`), the `currentLayer` (where new art goes) and `layerRows`, the rows highlighted in the panel. - **Clicking rows** (not undo steps): `layer.setCurrent {id}` is a plain click on any row: it alone is highlighted and its layer (the row itself when it is a layer or sublayer) becomes current. `layer.highlight {ids, mode?: set|add|toggle}` is Shift-click (a range) and Ctrl/Cmd-click (toggle). Selecting art makes its layer current. Without `ids`, the panel commands below act on the highlighted rows, else on the current layer. - **Selection column**: `layer.selectAll {id, add?}` selects a row's visible, unlocked art (a layer's sublayers' too); `add` (Shift) adds it, or removes it when it's all selected. - **Eye and lock**: `layer.setProps {ids?, visible?, locked?, name?}` for any rows, plus Layer Options for layers: `template` (also locks the layer and dims its images to 50% unless given), `printable`, `preview` (off: the layer draws and is clicked in outline, Ctrl/Cmd-click its eye), `dimImages` (0–100, or false) and `color` (an index 0–26, `#rrggbb` or a preset name). Several rows change in one undo step (a drag down the eye or lock column). - **Dragging rows**: `layer.move {ids, target, place?: above|below|inside, copy?}` moves rows beside a row or into a layer or group (on top of its contents), keeping their stacking order; `copy` (Alt-drag) moves copies. Layers go only in layers or at the top level and objects in layers and groups (an object placed beside a top-level layer goes inside it); a row never goes into itself or a row inside it, and a locked layer or group takes nothing. Dragging the selected-art square is `layer.move` with the selected objects. `node.move {id, parent?, index}` is the low-level form with the same nesting rules; unlike `layer.move`, it also moves a row into a locked layer or group. - **Buttons**: `layer.new {name?, top?, …options}` (above the current layer at its level; `top` with Ctrl/Cmd), `layer.newSublayer {parent?, name?, …options}` (on top of the parent's contents), `layer.delete {ids?}` (rows with what they hold; the last layer stays), `layer.locate {id?}` (highlights the selected object's row; the panel opens the rows around it) and `layer.clippingMask.toggle`. - **Panel menu**: `layer.duplicate {ids?}`, `layer.merge {ids?}` (into the layer highlighted last, keeping the stacking order), `layer.flatten {id?}` (every other visible layer's art into one layer; hidden layers are deleted, templates stay), `layer.collectInNew {ids?}`, `layer.releaseToLayers {id?, build?}` and `layer.releaseToLayersBuild` (each object of a layer or group in a sublayer of its own; Build adds up copies), `layer.reverse {ids?}`, `layer.template {ids?, on?}`, `layer.hideOthers` / `layer.showAll`, `layer.outlineOthers` / `layer.previewAll` (Ctrl/Cmd+Alt-clicking an eye runs Outline Others for the row's layer, or Preview All Layers when the others are in Outline already), `layer.lockOthers` / `layer.unlockAll` (Alt-clicking an eye or a lock runs Hide or Lock Others for the row's layer, or Show or Unlock All when the others already are), `layer.pasteRemembersLayers`, `object.isolate {id?}` (a group, a layer or a single object, by default the first selected object; not type, not a hidden or locked object or one in a hidden or locked group or layer, and not a guide path while Lock Guides is on) / `object.exitIsolation`. - **Dialogs and panel state** (control channel and the app's MCP): `ui.layerOptions {ids?}` and `ui.newLayer {sublayer?}` open Layer Options (dialog `layerOptions`: name, color, template, locked, visible, printable, preview, dimImages, dimPercent; OK is one undo step; Cmd/Ctrl+Alt+L runs `ui.newLayer` for a new layer), `ui.layersPanelOptions` opens Panel Options (`layersPanelOptions`: layersOnly, locateSelected, rowSize small|medium|large|other, otherSize, thumbLayers, thumbGroups, thumbObjects), and `ui.layersExpand {ids?, open?}` opens or closes rows as their triangles do (Alt-click: everything inside). With `locateSelected` on (Locate Selected Objects, off by default), when the last selected object changes on the canvas or by a command, the panel opens the layers and groups around it and scrolls to its row, as after `layer.locate`, but does not highlight the row. When Show Layers Only, a search or the filter leaves the object without a row, the panel opens no rows for it and scrolls as with the option off. A change made with the panel's own rows, buttons or menu is not located, and a change made during a drag is located when the drag ends. A document opens with only its top-level layers open; the open rows are saved in the native file and reopen that way. - **Search and filter** (control channel and the app's MCP): the Search All field keeps the rows whose names hold the text typed, and `ui.layersFilter {objects?, layers?}` sets the choices of the Filter button beside it. `objects` (By Object) takes `paths` (paths and compound paths), `shapes` (live rectangles, ellipses, polygons and lines), `text`, `images`, `effects` (rows with an effect on themselves or on a fill or stroke, shown or hidden), `groups`, `linkedFiles` (linked images and placed documents) and `guides` (paths made into guides, and ruler guides); `layers` (By Layer) takes `locked` and `hidden` (the row's own lock and eye). A row stays when it is one of those kinds and in one of those states (an empty list takes every row) and its name holds the search text, or when a row inside it stays. A key left out keeps its list. The filter lasts the session and does not change the document, its open rows or the selection. `ui.layersFilter` answers `{objects, layers}`. ```json {"name":"run_command","arguments":{"command":"layer.newSublayer","params":{"name":"Shadows"}}} {"name":"run_command","arguments":{"command":"layer.move","params":{"ids":[12,15],"target":7,"place":"inside"}}} {"name":"run_command","arguments":{"command":"layer.setProps","params":{"ids":[7],"preview":false,"color":"Orange"}}} {"name":"run_command","arguments":{"command":"layer.highlight","params":{"ids":[3,7]}}} {"name":"run_command","arguments":{"command":"layer.merge","params":{}}} ``` ## Targeting layers and moving appearances `layer.target {id}` is the Layers panel's target circle: a layer gets its visible, unlocked art (its sublayers' too) selected and is itself the target, so `appearance.*`, `effect.*`, `transparency.*` and the opacity-mask commands without `ids` act on the layer (its opacity, its own fills and effects, an opacity mask on the whole layer); a group or object is simply selected. `document.inspect` reports it as `target`, and any other selection change ends it. `appearance.transfer {source, target, copy?}` is dragging a target circle onto another: the target gets the source's fills, strokes, effects and transparency, and the source is cleared unless `copy` (Alt-drag). Dropping a circle on the panel's trash is `appearance.clear {ids: [id]}`. Masked objects' names have a dashed underline; while an opacity mask is edited the panel lists only an `` entry and the document tab says `(/Opacity Mask)`. ```json {"name":"run_command","arguments":{"command":"layer.target","params":{"id":2}}} {"name":"run_command","arguments":{"command":"transparency.set","params":{"opacity":50}}} {"name":"run_command","arguments":{"command":"appearance.transfer","params":{"source":12,"target":2,"copy":true}}} ``` ## Expand Appearance `effect.expandAppearance {ids?, target?}` (Object → Expand Appearance, enabled when a selected or targeted object's appearance isn't basic) turns appearances into plain objects in one undo step: each visible fill becomes a copy of the path painted by that fill alone and each stroke its outline filled with its paint (a brushed stroke: its brush art), grouped in paint order under the object's id; each piece takes its fill's or stroke's opacity and blend mode, and the group keeps the object's transparency and opacity mask. Geometry effects are baked; raster effects become an embedded image at the document's raster effects resolution (shadows and outer glows under the art; a blur, feather or inner glow replaces the object with the image). Type with a geometry effect (Outline Object included) or with fills and strokes of its own is outlined; type that has no fills or strokes of its own and whose only effects are shadows or outer glows stays type, with the image under it. A group's or layer's own fills and strokes become objects among its members, and its members are expanded too. The result is `{ids}`; when an image in the art can't be decoded, the images made from raster effects do not show it, and the result also carries the `warning` that Flatten Transparency gives. ```json {"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{"ids":[12]}}} ``` ## Tints of global and spot colours A colour linked to a global or spot swatch has a tint (the reference app's T slider): `paint.setFill {swatch, tint?: 0..100}` (and `paint.setStroke`) applies the swatch at that percentage, linked, so `swatch.edit` recolours it at its own tint. `paint.proxies` shows the paint as `{type: "solid", color, swatch, tint}` (`tint` 0..1, left out at 100 %); `document.inspect` reports it as a label, the swatch name, the tint below 100 % and the colour it gives (`"G1 40% (#ff9999)"`, `"G1 (#ff0000)"`). `swatch.new {tint?}` with the current fill (or `swatch`) saves a tint swatch, "Name 40%": `swatch.list` reports it with `tintOf` and `tint`, applying it links to its base at that tint, and it follows edits of its base. A spot tint prints that percentage of its plate (Separations Preview, PDF Separation value). `edit.colors.adjustBalance {tint: -100..100}` (Global mode) shifts the tints of the selection's linked colours and leaves the rest alone. ```json {"name":"run_command","arguments":{"command":"paint.setFill","params":{"swatch":"Ink","tint":40}}} {"name":"run_command","arguments":{"command":"edit.colors.adjustBalance","params":{"mode":"global","tint":-20}}} ``` ## Linked gradient stops Applying a gradient swatch (`paint.setFill {swatch}`) records it as the gradient's `swatch` (the Swatches panel highlights it; new stops drop the link). A gradient stop can link to a global or spot swatch like a solid colour: give it `swatch` (and `tint` %, default 100 or a tint swatch's own) instead of `color` in `paint.editGradient {stops}` or a `gradient` paint's stops. Editing or deleting the swatch then recolours or unlinks the stop, in art and in gradient swatches. A spot stop separates on its plate; a gradient whose stops are all tints of one spot ink (or paper white, 0 %) exports to PDF as a Separation shading (the writer has no DeviceN, so a gradient mixing a spot ink with other colours is written in process colours, with a warning). ```json {"name":"run_command","arguments":{"command":"paint.editGradient","params":{"stops":[{"offset":0,"swatch":"Ink"},{"offset":1,"swatch":"Ink","tint":20}]}}} ``` ## Transparency flattener presets `flattener.presets.list` lists the built-in presets (High, Medium and Low Resolution, `builtIn: true`) and the saved ones, each with its `options`. `flattener.presets.save {name?, newName?, preset?, …options}` creates or changes a saved preset (a change starts from the preset's own options; `newName` renames it; built-in presets can't change); `flattener.presets.delete {name}` deletes one. `flattener.presets.export {names?, path?}` writes them as a `.vcflattener` JSON file (without `path` it returns `data`), and `flattener.presets.import {path? | data? | dataBase64?, replace?}` adds a file's presets (names in use get a number unless `replace`). Saved presets live with the preferences and work as `preset` in `object.flattenTransparency` (and anywhere else flattener options are taken). ```json {"name":"run_command","arguments":{"command":"flattener.presets.save","params":{"name":"Press","preset":"high","balance":60}}} {"name":"run_command","arguments":{"command":"object.flattenTransparency","params":{"preset":"Press"}}} ``` ## Flattener Preview `flattener.preview {highlight?, overprints?, preset?, …options, ids?}` reports what flattening the document (or `ids`) would do without changing it: `counts` of transparent objects, all affected objects, patterns, outlined strokes and type, rasterized complex regions (areas the raster/vector balance rasterizes whole), all rasterized areas and flat-colour regions, plus `regions` (bounds, and `id` for objects) for the `highlight` asked for. `overprints: "discard"` or `"simulate"` flatten without preserving overprints. ```json {"name":"run_command","arguments":{"command":"flattener.preview","params":{"highlight":"allRasterized","preset":"low"}}} ``` ## Width profiles The Profile list holds the built-in variable-width profiles (`uniform`, `lens`, `taperStart`, `taperEnd`, `pinch`, `teardrop`, `wave`) and profiles saved from strokes; saved ones are kept with the preferences, not in the document (no undo step). `stroke.widthProfile.list` returns every row (`id`, `label`, `builtIn`, `points` as `[t, left, right]`) and `current`, the selected stroke's profile (`"custom"` when it isn't listed). `stroke.widthProfile.add {name?}` saves the selected stroke's variable width (default name "Width Profile N"), `stroke.widthProfile.delete {name?}` removes a saved one (default: the selected stroke's; built-ins can't be deleted) and `stroke.widthProfile.reset` removes every saved one. `stroke.set {profile}` takes a built-in id or a saved name. ```json {"name":"run_command","arguments":{"command":"stroke.widthProfile.add","params":{"name":"Ribbon"}}} {"name":"run_command","arguments":{"command":"stroke.set","params":{"ids":[9],"profile":"Ribbon"}}} ``` ## Arrowheads `stroke.set {startArrow, endArrow}` takes any of 40 generated heads by name (the params doc lists them; `null` for none): arrows (`Arrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, `Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide. ## Stroke bounds and clicks Visual bounds (Fit to Selected Art, Rasterize, exporting all art) and clicks on the canvas take in the whole stroke as drawn: arrowheads, inside/outside alignment (an outside stroke is hit outside the path, an inside one inside it), the width profile's width where you click, projecting caps and miter spikes up to the miter limit. A rectangle's right-angle miters stay within half the weight of its edges. A symbol instance is measured by its symbol's art, placed by the instance's transform. The bounds in `document.inspect` and `document.node`, the Align commands, `object.setBounds`, the bounding box, clicks, marquees, Fit to Artwork Bounds and exports of the art use that box. So do `artboard.rearrange` and `artboard.move` (which artboard the art moves with), `artboard.convertToArtboards`, `select.allOnArtboard`, `object.transformEach`, `object.lock.above` and `object.hide.above`. `edit.copy` and `edit.cut` record the artboard under the center of that box as the one the copy comes from, and `edit.pasteInPlace`, `edit.pasteInFront`, `edit.pasteInBack` and `edit.pasteOnAllArtboards` keep the copy's offset to it. `edit.paste {center}`, `library.use {center}` and the `clipboard.import*` commands center that box on the point, and `library.add` gives a graphic the size of that box. `clipboard.exportPng`, `clipboard.exportPdf` and the PNG and PDF that Copy offers other apps are cropped to that box. Visual bounds (Use Preview Bounds, Fit to Selected Art) take in the strokes in the art at their own weight, as the canvas draws them when the instance scales the art. An instance is measured as the 20 × 20 square around the origin, which symbols made in the app keep their art in, when its symbol is missing, when its symbol or a symbol nested in its art holds an instance of itself, and when symbols nest 32 deep in its art. PDF and EPS export and the SVG of an artboard leave out art outside the page or artboard, and Skip Blank Artboards skips an artboard with no art on it. For both, the box of an instance whose art holds brushed strokes grows by 16 times the widest stroke (at least 1 pt) of the objects in its art that have a brushed stroke, plus 8 pt. The SVG of an artboard also keeps an object whose effects paint onto the artboard, and an object that holds an instance when the effects in the symbol's art, or the instance's own, paint onto it. ## Expand `object.expand {object?, fill?, stroke?, gradient?, steps?}` (Object → Expand…) turns the selection into plain art in one undo step. `object` (on by default) outlines type, turns live shapes into paths and bakes effects; `stroke` (on) outlines strokes into filled paths; `fill` (on) expands gradient fills: with `gradient: "objects"` (the default) into `steps` (1–1000, default 255) solid objects (rectangles across a linear gradient, each from its band to the far end so no seams show; concentric ellipses for a radial one, largest first; just the bands when stops are translucent), with `gradient: "mesh"` into a gradient mesh that paints the gradient (columns or rings where its colour changes, sharp at coincident stops), each inside a clip group shaped like the object. A freeform gradient becomes a mesh shaped like the object either way. Expanded colours keep the colour model their stops share. Expand mixes a freeform gradient's point colors in the model they share to color the mesh points; it mixes C, M, Y and K as inks. When the models differ, the colors mix in display RGB and are stored in the working RGB space, and a CMYK document stores them as CMYK through the color settings. `fill` also expands pattern fills: each becomes the pattern's tiles that reach the object's box, placed by the pattern's transform, in a clip group shaped like the object that isolates their blending, with the fill's opacity, blend mode and effects; the object's other fills and strokes stay on copies of it in paint order. The tiles' stroke weights and effect distances scale with the pattern, and opacity masks in the tile art, unlinked ones included, move with each tile. When the pattern is rotated, reflected, stretched or sheared, or scaled while its art has geometry effects (Roughen's detail is per inch), the strokes (brushed ones included) and geometry effects of the tiles' paths become filled art and their live shapes become plain paths, in their opacity masks too. A path in the tile art with a Transform effect set to Random, or with a brush that varies at random (a calligraphic or scatter brush with Random variation, or a bristle brush), becomes filled art first, so every tile shows the one result the canvas draws. A pattern fill stays as it is when it lays no tiles there (as when the box would take more than 250,000 tile positions), when a group or type in its art has a Transform effect set to Random, when the pattern is scaled, rotated, reflected, stretched or sheared and its art holds a symbol instance (the canvas scales the instance's strokes with the tile), and when the pattern is rotated, reflected, stretched or sheared, or scaled while its art has geometry effects, and its art has raster or 3D effects, or a geometry effect on a group or type, which the canvas draws turned or scaled with each tile. The geometry effects of a gradient or pattern fill (such as Offset Path) and those of the object shape the outline of the clip group, the gradient fits that outline and the tiles reach its box, as the canvas paints the fill; the expanded art does not keep them. With `fill: false` gradient and pattern fills stay live. `object.expand.info` answers which options have something to expand in the selection (`{object, fill, stroke, gradient}`; `gradient` says whether Fill has a gradient fill, which Expand Gradient To applies to), and `object.mesh.create` on a gradient-filled object colours the mesh points as the gradient paints them, with the colors Object › Expand gives them. ```json {"name":"run_command","arguments":{"command":"object.expand","params":{"stroke":false,"gradient":"mesh"}}} {"name":"run_command","arguments":{"command":"object.expand","params":{"steps":16}}} ``` ## Lab spot colours Colours can be CIE Lab (D50): give `{"l": 55, "a": 60, "b": 40}` (L 0–100, a and b about −128–127) wherever a colour is taken; documents store them as `{"model": "lab", …}`. `swatch.new {color, spot: true}` or `swatch.edit {name, mode: "lab"}` defines a spot colour in Lab (Swatch Options' Lab mode). `swatch.spotOptions {useLab}` sets the document's Spot Colors options: with `true` (the default) Lab spot colours show and print from their Lab values and PDF export writes their Separation spaces with a Lab alternate; with `false` art linked to them takes their working-CMYK equivalents and PDF uses a DeviceCMYK alternate. Without `useLab` it answers the current setting. ```json {"name":"run_command","arguments":{"command":"swatch.new","params":{"name":"Lab Ink","color":{"l":55,"a":60,"b":40},"spot":true}}} {"name":"run_command","arguments":{"command":"swatch.spotOptions","params":{"useLab":false}}} ``` ## Attributes, URLs and image maps The Attributes panel (`window.panel {panel: "attributes"}`, Cmd+F11) reads `attributes.info {ids?}`: `overprintFill`, `overprintStroke`, `showCenter`, `imageMap`, `url`, `note`, `fillRule` and `reversed` (null where the objects differ) and `recentUrls`. `attributes.set {overprintFill?, overprintStroke?, showCenter?, imageMap?, url?, note?, ids?}` sets them in one undo step; `path.setFillRule {rule: "nonZero"|"evenOdd"}` sets the fill rule of the paths and compound paths, and `path.reverse {reversed?}` makes subpaths run counter-clockwise (true) or clockwise (false). SVG export wraps an object with a URL in ``, and SVG import reads `` links back. ```json {"name":"run_command","arguments":{"command":"attributes.set","params":{"ids":[12],"url":"https://example.com","imageMap":"rectangle"}}} {"name":"run_command","arguments":{"command":"path.setFillRule","params":{"rule":"evenOdd"}}} ``` ## SVG interactivity The SVG Interactivity panel (`window.panel {panel: "svgInteractivity"}`) reads `svgInteractivity.info {ids?}`: `events` (`[{event, script}]` in the panel's order, null where the objects differ), `files` (the linked JavaScript files) and `eventNames` (the 19 events the panel lists, from `onfocusin` to `onscroll`). `svgInteractivity.setEvent {event, script, ids?}` sets the JavaScript an event runs on the objects, replacing the event's script where they have one (an empty `script` removes it); `svgInteractivity.removeEvent {event, ids?}` and `svgInteractivity.clearEvents {ids?}` remove one event or all of them. Each is one undo step. `svgInteractivity.addFile {file}` and `svgInteractivity.removeFile {file}` link and unlink a JavaScript file (a path or a URL, kept as given without surrounding spaces); a document links at most 64 files. `ui.javascriptFilesDialog {}` opens the JavaScript Files dialog, whose Choose… puts a picked file in as a `file:///` URL. SVG export writes each event as an event attribute on the object's element (`onclick="…"`; with the SVG Tiny 1.2 profile it leaves them out with a warning) and each file as `