85 KiB
Control protocol
vectorcraft --control <port> listens on 127.0.0.1:<port> (loopback only). The port must be a number
from 1 to 65535 (--control=<port> works too), given once; otherwise the app prints an error and exits
before it starts. One JSON request per line:
{"id": 1, "method": "ui.inspect", "params": {}} → {"id": 1, "ok": true, "result": {...}} or {"id":1,"ok":false,"error":"..."}.
Set VECTORCRAFT_CONFIG_DIR to a dedicated directory for an isolated native test profile. Preferences,
logs, recovery and user libraries then use that root, without importing the legacy DrawCraft profile.
An unset override preserves the usual platform paths and legacy migration; an explicitly empty override
disables those paths. VECTORCRAFT_NO_PREFS continues to disable preference reads and writes.
Only requests are read. Every line must be a JSON object with a string method (id and params
are optional; blank lines are skipped). Anything else gets one error reply
({"ok": false, "error": "… closing the connection"}) and the server closes the connection, so
nothing sent after it on that connection runs. That covers text that isn't JSON, a JSON array or number,
an object without method, invalid UTF-8, and a line longer than 4 MiB. An HTTP request (for example a
web page's cross-origin fetch to 127.0.0.1:<port>) therefore can't smuggle a command in its body:
its request line is rejected first. At most 16 connections are served at once; further ones get an error
line and are closed. Clients that get an error reply should reconnect. The port has no authentication,
so only enable it while you use it. Transport: apps/vectorcraft/src/control_server.rs.
Confining file access
vectorcraft --control 7979 --automation-read-root /work/project --automation-write-root /work/project/out
--automation-read-root <dir> and --automation-write-root <dir> (or VECTORCRAFT_AUTOMATION_READ_ROOT /
VECTORCRAFT_AUTOMATION_WRITE_ROOT; --flag=<dir> works too) confine what the control channel reads and
writes, with the same flags as vectorcraft-cli mcp and the other Craft apps. Every path a request reads
(open, place, relink, library and profile loads, plug-ins…) must lie inside the read root, every path it
writes (save, export, package, captures, library saves…) inside the write root; anything else is an
ordinary error reply. Paths are made absolute against the app's working directory, links are followed
(for a file that doesn't exist yet, its folder's), and the comparison is by whole folder names (/work2 is
not inside /work), ignoring case on Windows; .. below a folder that doesn't exist, a link that leads
nowhere and Windows device names are refused. A root left out grants none of its access. The roots must be
existing folders, or the app doesn't start; without --control they are ignored. See docs/mcp.md
› Confining file access for the details.
Each root flag may be given once; a command-line root overrides its environment default. Missing or
empty values, another option used as a value, and duplicate flags stop the desktop before it starts
any services, even without --control. For a folder name starting with -, use --flag=-folder
or the relative path ./-folder.
What is confined: the requests themselves, the frames that carry input they inject (ui.click, ui.key,
ui.text…, so a menu item an agent clicks is confined too), the background saves and exports they start,
and window captures. The person at the keyboard is not: files opened from the command line, Finder or a
file dialog, and everything done with the mouse and keyboard, work as usual. The preferences that move
folders the app reads or writes on its own (Additional Fonts Folder, Additional Plug-ins Folder, Data
Recovery's folder, the Templates folder) can only be pointed inside the roots by automation. The app's own folders
(preferences, Data Recovery, the library folders, VectorCraft's Fonts folder) are not confined, since no
agent names a path there. A path is checked, then opened: another program that swaps in a link between
the two can still win that race.
| Method | Params | |
|---|---|---|
engine.execute |
{command, params} |
run any engine or UI command (see engine.commands). A key of params that the command's params description doesn't name is an error naming it and the nearest key the command takes, before anything runs (unknown parameter widht; did you mean width?); see Parameter keys |
engine.commands |
every command with label, shortcut, params doc, enablement | |
document.inspect |
{depth?, childLimit?} |
layer tree, selection, the key object (key, null without one), history, paint defaults; the options slice the layer tree as document.node {summary: true} does. Each node reports opacity below 1 and blend when it isn't Normal; objects and compound paths report fill, stroke and strokeWidth, a compound path's members compoundMember: true instead |
ui.inspect |
tool, UI state, screenMode (0 normal, 1 full screen with menu bar, 2 full screen, 3 Presentation Mode), taskBar (pinned, and rect [x, y, w, h] while the Contextual Task Bar shows), freeTransformWidget ([x, y, w, h] while the Free Transform widget shows), view, canvas rect, window size, perf, background saves and exports still running, nativeMenuBar (true when the menus are in the macOS menu bar rather than the window; ui.menu.list lists the in-window menus either way), fileDialog ("open", "place", "save" or "folder" while a native file dialog is open, null otherwise; see below) |
|
ui.menu.list / ui.menu.invoke |
{command, params} |
the full menu tree / invoke an item |
ui.contextMenu.list |
the canvas context menu for the current selection, flattened like ui.menu.list (path holds its submenus). ui.click {x, y, button: "right"} on the canvas opens it, after selecting the object there unless it is already selected |
|
ui.tool.select / ui.tool.list |
{tool} |
|
ui.pointer |
`{events:[{kind: down | drag |
ui.key / ui.text |
{key, shift?, alt?, cmd?} / {text} |
synthetic keyboard input |
ui.wheel |
{x, y, dy?, dx?, unit?: "line"|"point", shift?, alt?, cmd?} |
a mouse wheel turn over screen point (x, y): dy notches up (+) or down, dx sideways. Over the canvas the wheel scrolls and Cmd- or Alt-wheel (Option on the Mac) zooms about the pointer; with the zoomWithMouseWheel preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it |
ui.set |
{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?} |
|
ui.dialog.set / .confirm / .cancel |
{field, value} |
fill and submit the open dialog |
ui.screenshot |
{path?, data?} |
capture the window (PNG), written to path; data: true sends it back as pngBase64 too. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
ui.render |
{path?, scale?, artboard?, data?} |
render an artboard headlessly (PNG; artboard: its 0-based index, default the first): written to path, else (or with data: true as well) sent back as pngBase64 |
ui.resize / ui.focus |
||
app.open / app.save / app.export / app.quit |
{path} / {path?} / {path?, format?, artboard?, range?, scale?, …} |
app.open reads every format document.open reads (see document.formats) and returns its result ({index, title, format, warnings, …}; warnings say what didn't come in as it was, such as an EPS shown as its preview image and why its PostScript couldn't be read), or null when a dialog asks first or a library loads. app.export encodes through the engine's document.export (same options; the document keeps its path) and writes path through the host; without path it returns {dataBase64, format, bytes}, as headless mode does. app.quit, file.close and file.closeAll first open a saveChanges dialog for each modified document (they return {"pending": "saveChanges"}): ui.dialog.confirm saves, ui.dialog.set {field: "discard", value: true} then confirm discards, ui.dialog.cancel cancels the whole close or quit |
app.export with useArtboards |
{path, useArtboards: true, range? | artboards?, …} |
Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a range), <path stem>-<artboard>.<ext>, and return {path, files: [path…]}; a PDF keeps them as pages of path. useArtboards: false covers the bounds of the visible art. The menu's Export As… is file.exportAs: the exportAs dialog (format, useArtboards, all, range), then a save dialog and the format's options: pngOptions / jpgOptions / webpOptions, svgOptions, or savePdf for a PDF with Use Artboards |
ui.dialog.* on gradientStop |
{field: "color" | "opacity" | "location", value} |
double-clicking a stop on the Gradient tool's annotator opens its popover (fields index, x, y, tab); set color (hex), opacity or location (percentages) and ui.dialog.confirm to apply them to the selected stop. gradient.selectStop {index} picks the stop the annotator, the panels and Delete/←/→ (ui.key) act on |
engine.execute ui.savePdfDialog |
{path?, preset?, range?, …document.exportPdf options} |
opens the Save PDF dialog (file.export.pdf with no params opens it too). Its fields are the document.exportPdf options: set compatibility with ui.dialog.set {field: "compatibility", value: "1.5"}, a whole section with {field: "compression", value: {…}}, the artboards with __allArtboards: false and range, the visible section with __section (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). ui.dialog.confirm writes path (else asks for one), returns {path, bytes, warnings} and opens the file when viewAfterSaving is set |
file.save / file.saveAs / file.saveCopy / file.saveAsTemplate (via engine.execute) |
{path?, format?, options?, svg?} |
Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's warnings say what the format loses). With path the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (vectorcraft, template, pdf, svg, svgz), then the format's options: SVG opens the svgOptions dialog (below; returns {"pending": "svgOptions", "path"}), PDF the savePdf dialog (its __save field names the save command); ui.dialog.confirm finishes the save. On the web a saveOptions dialog (fields path, format) first names the file and picks the format. file.formatOptions {format?} reads the options a save would use. file.revert opens a confirm dialog ({"pending": "confirm"}); ui.dialog.confirm reloads the saved file in the same tab |
SVG Options (engine.execute of file.export.svg, file.saveAs, file.saveCopy) |
{} / {path?, svg?: {…}} |
file.export.svg without params, and Save As or Save a Copy to an .svg/.svgz path without SVG options, open the svgOptions dialog (ui.dialog.set its fields: styling, outlineText, images, objectIds, decimals, minify, responsive, fewerTspans, metadata, preserveEditing, useArtboards, allArtboards, range, showCode, profile (svg11/tiny12), encoding (utf8/utf16/latin1), embedFonts; ui.dialog.confirm exports or saves and remembers the choices). With svg: {…} (the document.formats SVG options) they run directly. app.save {path?, svg?} takes the same options; a document saved as SVG saves as SVG again with the same options |
engine.execute file.place |
{paths?} / {path | name+dataBase64, …file.place params} |
with no file, picks files and opens the place dialog; with paths, opens it for them (fields files, link, template, replace; __replace says whether Replace applies, __info describes each file). ui.dialog.confirm places one file centred in the view (or in place of the selected object) and loads the place cursor with several (file.place.queue). With a file, it runs file.place centred in the view. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, .ai, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (.vcswatches, .gpl, .ase, .acb), graphic style libraries (.vcstyles), Libraries panel files (.vclibrary), presets (.vcflattener, .vcpdfpresets, .vcprintpresets, .vcperspective) and plug-ins (.wasm) open wherever they are dropped, as File › Open opens them. With the cursor loaded (ui.inspect → tool: "place", toolOptions: {count, current, name, names}), ui.pointer clicks and drags place files and ui.key ←/→/↑/↓ and Escape cycle and discard them |
app.export raster options |
{path?, format: png | jpg | webp, ppi?, background?, antiAlias?, interlaced?, quality?} |
ppi sets the pixel size (72 = one pixel per point) and is stored in the file; background is transparent, white, black or "#rrggbb"; antiAlias is none, art or type (text snapped to pixels); interlaced writes an Adam7 PNG. The PNG Options dialog (pngOptions; jpgOptions / webpOptions for the other raster formats) has the same fields: fill them with ui.dialog.set and press OK with ui.dialog.confirm |
app.export JPEG options |
{path?, format: jpg, quality?: 0–100, colorModel?: rgb | cmyk | gray, method?: baseline | optimized | progressive, scans?: 3–5, embedIcc?, imageMap?: none | client | server} |
cmyk writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); embedIcc (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; imageMap also writes <stem>.html (client-side <map>) or <stem>.map (NCSA) beside the image for the objects with a URL and an Image Map shape (attributes.set), returned in linked. The jpgOptions dialog has these fields too (quality is shown 0–10; a CMYK document defaults to cmyk) |
app.export PNG-8 and GIF |
{path?, format: png8 | gif, colors?: 2–256, reduction?: perceptual | selective | adaptive | web | blackWhite | gray, dither?: none | diffusion | pattern | noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options} |
a palette of at most colors entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless transparency is false, partly transparent edges are blended over matte (none keeps their colour). png8 writes an indexed .png (1–8 bits per pixel); Export for Screens rows take both. The png8Options / gifOptions dialogs have these fields |
app.export Text and WebP |
{path?, format: txt, encoding?: utf8 | utf16, lineEndings?: lf | crlf, selectionOnly?} / {format: webp, lossless?, quality?} |
Text writes the stories in stacking order (back to front; a thread once; hidden objects and template layers left out), each line ended by lineEndings; utf16 is little-endian with a byte order mark. Export As → Text opens txtOptions (fields path, encoding, lineEndings, selectionOnly; OK writes the file). WebP is always lossless: lossless: false adds a warning |
engine.execute document.exportForOffice |
{} / {path?, ppi?: 72 | 150 | 300, transparent?, artboard?} |
File → Save for Office Documents…: without params opens saveForOffice (fields ppi, transparent, artboard; OK picks the file); with params writes the artboard as a PNG (path, else a picked file) → {path}. Headless it returns {dataBase64, width, height} |
| Native file dialogs | any command with no path that needs one (file.open, file.saveAs, file.place, exports…) |
on macOS (a sheet on the window) and Linux the dialog doesn't hold the window: the call returns "cancelled" at once, ui.inspect reports fileDialog, and when the person at the computer picks a file the command runs again on the document it was asked for. While it is open another dialog is refused: that call returns "cancelled" too, and the status bar (ui.inspect → ui.status) says "A file dialog is open: choose there first"; on macOS app.quit, file.close and file.closeAll return that message and the menu bar does nothing (#592, #867). On Windows the dialog is shown in line: the call returns once it closes. Pass path to skip the dialog |
| Background Save / Export | app.save, app.export {path}, file.save, file.saveAs, file.saveCopy |
with the backgroundSave / backgroundExport preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns {path, background: true, status} at once, ui.inspect lists the jobs still running in background (labels such as "Saving a.vectorcraft"), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. With the preferences off (prefs.set), saves return once written |
engine.execute file.exportForScreens |
{} / {…document.exportForScreens params} |
without params opens exportForScreens on the settings the document last exported with (document.exportSettings). Fields: tab (artboards | assets), select (all | range | full), range ("1-3, 5"), boards (a bool per artboard; checking them sets select and range), includeBleed, folder (desktop; default the Desktop, else home), openLocation, subfolders, prefix, preset ("" | mobile | density), formats (rows {format, scale: "2x" | "100w" | "100h" | "72ppi", suffix, quality?, folder?}), settings ({png | png8 | jpg | webp | gif | svg | pdf: {…options}}, the gear's Format Settings; __settings names the format shown). ui.dialog.confirm exports (a bad range or no folder keeps it open); on the web OK reads "Download" and the files download (several as one .zip). With params it runs document.exportForScreens: no folder returns the files (or with zip: true one store-only zip) as dataBase64; with openLocation the first file is shown in the file manager |
| Export for Screens' Assets tab, Asset Export panel | ui.dialog.set tab: "assets", assets: [asset id…]; window.panel {panel: "assetExport"} |
on the Assets tab of exportForScreens the assets field lists the checked asset ids (all of them when it opens); ui.dialog.confirm runs document.exportForScreens {assets} (nothing checked keeps it open) and OK reads "Export Asset". The Asset Export panel acts through assets.* and shares the format rows with the dialog (assets.settings.set) |
engine.execute file.exportSelection |
{multiple?} |
File › Export Selection…: collects the selected objects as assets (assets.add; one per object unless multiple is false) and opens exportForScreens on its Assets tab with just them checked → {assets}. document.exportSelection still exports the selection in one go, without the dialog |
| CSS Properties panel | window.panel {panel: "cssProperties"}; engine.execute css.copy / css.exportFile |
the panel shows css.selection of the selection (after Generate CSS, while nothing is selected, css.generate); its menu holds the options the commands take (units, position, dimensions, unnamed, rasterize). css.copy {scope?, …options} copies the CSS to the clipboard → {css, rules}; css.exportFile {path?, scope?, …options} writes css.export to path (else a picked file; the web downloads it) with the pictures of rasterized art next to it → {path, rules, images} |
| ui.dialog.* on importPdf | {field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value} | opening (app.open, file.open) a PDF or .ai with several pages or a password, or placing one (file.place without page), opens the Import PDF dialog (mode: open or place; pageCount). While __locked is true, set password and ui.dialog.confirm to unlock it (a one-page file then opens or is placed). Then set allPages: false and range ("2-3, 5") to open some pages, or page to place one, and cropTo (bounding, art, crop, trim, bleed, media); ui.dialog.confirm opens or places them (placing keeps the other file.place params, such as link, template and at, in __place). document.open {pages, cropTo, password} and file.place {page, crop, password} do the same without the dialog |
Swatch editors: engine.execute with ui.swatchOptions {name} opens the swatchOptions dialog for a colour swatch
(fields name, spot, global, mode: gray/rgb/hsb/lab/cmyk/web, color: "#rrggbb" or a colour object,
preview), which previews on the canvas while open; ui.dialog.confirm applies it with swatch.edit as one undo
step and ui.dialog.cancel rolls the preview back. For a gradient swatch the dialog has name only (it shows the
gradient) and OK renames it; pattern swatches open pattern editing instead. Dropping a gradient (or colour) on a
swatch of its kind in the Swatches panel with Alt held replaces it (swatch.edit {name, paint}).
Confirmations: deleting swatches from the Swatches panel opens a confirm dialog (fields message, detail);
ui.dialog.confirm runs the command it asks about (here swatch.delete) and ui.dialog.cancel drops it.
New swatches and colour groups: ui.newSwatch {spot?, group?} opens the newSwatch dialog prefilled from the active
fill or stroke (fields as in swatchOptions plus group; a gradient or pattern only takes name) and
ui.newColorGroup {swatches?} opens newColorGroup (fields name, fromArtwork, toGlobal, includeTints,
swatches); ui.dialog.confirm runs swatch.new / swatch.newGroup.
The Color Picker is a dialog too: engine.execute {command: "ui.colorPicker", params: {stroke?, color?}} opens it for the fill (or stroke) proxy; ui.dialog.set {field: "hex", value: "00FF00"} (or color, channel, webOnly, swatches) then ui.dialog.confirm applies the colour through paint.setFill / paint.setStroke.
Graphic styles: ui.graphicStyleOptions {name} opens the graphicStyleOptions dialog (field name) for a style and
ui.dialog.confirm renames it with graphicStyle.rename (a name another style has returns an error and keeps the
dialog open); without name it names a new style made from the selection (graphicStyle.new).
ui.mergeGraphicStyles {names} opens the same dialog to name the style ui.dialog.confirm merges from them
(graphicStyle.merge). Deleting styles from the panel asks first with a confirm dialog (graphicStyle.delete).
Symbols: ui.symbolOptions opens the symbolOptions dialog (field name) that the Symbols panel's New Symbol asks
first; ui.dialog.confirm makes the symbol from the selection with that name (symbol.new).
Gradient panel: double-clicking a stop on the panel's slider opens the same gradientStop popover, with a screen
field ([x, y], screen points) in place of x/y. The panel's stop eyedropper selects the Eyedropper tool with the
tool option stop (the tool to return to; tool.setOption {key: "stop", value: "gradient"}): its next click on art
samples the colour there into the selected stop (paint.sampleColor {color, stop}) and switches back. Dragging a
swatch, a Fill/Stroke proxy or the panel's gradient thumbnail onto art runs paint.setFill / paint.setStroke (the
active proxy) with the paint's params and the object's ids; a colour dropped on the panel's ramp adds or recolours
a stop.
Freeform gradients: double-clicking a point with the Gradient tool opens the same gradientStop popover on the selected
point (paint.freeform.selectPoint); its fields are color (hex), opacity and spread (percentages), applied by
ui.dialog.confirm through paint.freeform.setPoint. ui.key Escape ends the line Lines mode is drawing.
Tool options: double-clicking a tool button runs tool.options {tool}. For gradient it opens the Gradient panel;
for eyedropper it opens Eyedropper Options, an eyedropperOptions dialog (fields sampleSize 1/3/5, pickUp and
apply, the attribute trees of eyedropper.setOptions) whose ui.dialog.confirm runs eyedropper.setOptions (what
appearance.copyFrom copies). For hand it fits the artboard in the window (view.fitArtboard) and for zoom it
shows 100% (view.actualSize). For rotate, scale, reflect and shear it opens the same dialog as Object ›
Transform (dialog kind = the tool id), or fails with nothing selected. For selection, directSelection and
groupSelection it opens the Move dialog (kind move), with the same failure. ui.key Enter with one of those seven
tools opens its dialog too (nothing happens without a selection), with the transform tools' origin at their reference
point. Gradient tool handles snap to anchors, edges and smart guides; Shift constrains them to 45° steps from the
constrainAngle preference.
Effect dialogs: engine.execute {command: "effect.dialog", params: {effect, index?, item?}} opens the effect dialog
(fields: the effect's parameters, preview). With index it edits that applied effect of item (null: the object's
effects) prefilled with its values, and ui.dialog.confirm runs effect.setParams; otherwise confirm runs
effect.apply. Choosing an effect that the list already has returns {"pending": "effectExists"} and opens the
effectExists question: ui.dialog.confirm opens the applied effect's dialog, ui.dialog.set {field: "discard", value: true} then confirm opens a fresh one that adds another, ui.dialog.cancel drops it.
The Transform effect's dialog (effect.dialog {effect: "distort.transform"}) has Transform Each's controls: sliders
for scaleH, scaleV (%) and moveH, moveV (pt), an angle dial for rotate, then copies, reflectX,
reflectY, reference (0–8, the 9-point grid) and random (each object varies its own way, the same on every redraw).
Plug-in dialogs: engine.execute {command: "plugin.dialog", params: {id}} (Object › Plug-ins) runs an object filter
plug-in at once when it takes no parameters, else opens the plugin dialog: one field per declared parameter (named
after it), __plugin (the plug-in id) and preview. It previews plugin.run on the canvas while open;
ui.dialog.confirm keeps the result as one undo step and ui.dialog.cancel rolls it back. Live-effect plug-ins open
the effect dialog (effect.dialog {effect: "plugin.<id>"}). ui.installPlugin {path?} installs a .wasm (without
a path it asks for one); see plugins.md.
Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a command parameter dialog for
edit.colors.overprintBlack (fields remove, percentage, fill, stroke, includeCmyBlacks,
includeSpotBlacks); ui.dialog.set then ui.dialog.confirm runs it on the selection.
Color Guide: color.harmony {color, rule, steps?, variation?, amount?} returns the harmony group (base first) and
its variation grid; the panel draws the same grid. ui.colorGuideOptions opens the colorGuideOptions dialog
(fields steps 1–20, amount 0–100) and ui.dialog.confirm sets the panel's options, which ui.inspect reports
as ui.color_guide (variation, steps, amount). Save Colors as Swatches runs swatch.new {colors: [...]}
(one swatch per colour, one undo step).
Color Guide Limit to Library: ui.colorGuideLimit {library} (a swatch library id or name, "document" for the
document's swatches, "" for none) limits the panel's colours; ui.inspect reports it as ui.color_guide_limit, and
color.harmony {..., limitTo} answers the limited guide (every colour snaps to the library's nearest colour, ΔE 2000).
Edit or Apply Colors runs ui.recolorDialog {colors, library?} with the harmony colours: on the selected art it
recolours as usual; with nothing selected the colours themselves are the rows and ui.dialog.confirm saves them as a
new colour group (swatch.newGroup, named by the groupName field).
Color Themes panel (window.panel {panel: "colorThemes"}): its Create tab makes a theme on a harmony wheel and its
My Themes tab lists the saved themes; every action runs a command (colorTheme.save, colorTheme.delete,
colorTheme.addToSwatches, swatch.newGroup for the theme being made), and colorTheme.list reads the library.
Edit Colors dialogs: ui.colorBalanceDialog opens Adjust Colors (colorBalance: fields mode gray/rgb/cmyk/
global, the channels r g b / c m y k / gray / tint (global mode) from −100 to 100, convert, fill, stroke,
preview) and ui.saturateDialog opens Saturate (saturate: intensity −100..100, preview). Both preview on the
canvas while open; ui.dialog.confirm keeps the result as one undo step (edit.colors.adjustBalance /
edit.colors.saturate) and ui.dialog.cancel rolls it back. Global mode answers with an error until tints of global
and spot colours exist; the dialog then stays open.
Recolor Artwork: ui.recolorDialog {colors?, library?, group?} opens the recolor dialog on the selected art
(colors: a count for an n-colour job, or colours to assign as the new colours; library: Limit to Library, a library
or "document", "" for the first library; group: a colour group, Edit or Apply Color Group). Fields: rows ([{from: [keys], to: key, exclude?}]
as recolor.reduce returns them), colors (null for Auto), method, preserveWhite, preserveBlack,
preserveGrays, limitTo, groupName, tab (assign or edit), rule (harmony id), linked, preview. Setting
colors, a preserve flag or limitTo reduces the rows again (also on ui.dialog.confirm). The dialog previews
recolor.apply on the canvas; confirm keeps it as one undo step (with a group, the group is rewritten in the same
step; with no art selected only the group changes). In the Swatches panel, double-clicking a colour group's folder,
or the options button while a group is selected, opens it.
Library panel: engine.execute {command: "window.swatchLibrary", params: {library}} opens the read-only library
panel on a swatch library (ui.inspect shows it as library_panel: {kind, id}; library: null closes it).
Clicking a swatch there runs swatch.library.add {library, names: [name], apply} (the active proxy, or the other
one with Alt), so one undo step adds and applies it; Shift/Cmd-clicks select swatches and colour groups for
Add to Swatches.
window.swatchLibrary.other {path?} loads a library file (or another document's swatches) and opens it there;
opening a .vcswatches, .gpl or .ase file with app.open does the same. A file with the same extension and
bytes as a User Defined library opens as that library. ui.saveSwatchLibrary {names?} opens the
saveSwatchLibrary dialog (fields name, format: vcswatches/gpl/ase/css, user: save to the user library
folder, selectedOnly with names); ui.dialog.confirm runs swatch.library.save (to a file it asks for a path
and suggests name with the format's extension as the file name; the web downloads the file under that name).
Graphic style libraries open in the same panel: window.graphicStyleLibrary {library} (library_panel: {kind: "graphicStyles", id}; library: null closes it). Clicking a style there runs graphicStyle.addFromLibrary {library, name, apply: true, add} (add with Alt), adding and applying it in one undo step; Shift/Cmd-clicks select styles
for Add to Graphic Styles. window.graphicStyleLibrary.other {path?} loads a .vcstyles file (or another
document's graphic styles) and opens it there; opening a .vcstyles file with app.open does the same. The User
Defined libraries are window.userGraphicStyleLibrary1…10 in Window → Graphic Style Libraries.
ui.saveGraphicStyleLibrary {names?} opens the saveGraphicStyleLibrary dialog (fields name, user: save to the
user library folder, selectedOnly with names); ui.dialog.confirm runs graphicStyle.saveLibrary (to a file it
asks for a path and suggests name with .vcstyles as the file name; the web downloads the file under that name).
Tile Edge Color: Object → Pattern → Tile Edge Color… (ui.tileEdgeColor) opens the tileEdgeColor dialog (field
color: #rrggbb or a preset name such as "Light Blue"); ui.dialog.confirm sets the preference
patternTileEdgeColor (prefs.set), the colour pattern editing mode draws the tile edge and the swatch bounds in.
Window title bar: on Windows and Linux the window has no OS decorations and the app bar is the title bar. Its
caption buttons (Minimize, Maximize/Restore, Close) sit at the bar's right end, 46 pt each; they are window chrome,
not commands, so drive them with ui.click if needed. Close runs app.quit (the same saveChanges questions for
modified documents), and empty bar space and the 5 pt window edges move and resize the window. Preferences › User
Interface › System Title Bar (systemTitleBar, read when the app starts) gives the window its OS decorations back
instead: the OS title then shows the active document (*name — VectorCraft with unsaved changes) and the app bar
drops its brand mark. On macOS the window buttons are centred on the app bar; the web build is unchanged.
Fill/Stroke chips and panel shortcuts: the Control bar's and Properties' Fill and Stroke chips bring their proxy
forward (paint.toggleActive {fill}) and open a popover with the Swatches panel (Shift-click: the Color panel's mixer);
a swatch clicked there runs paint.setFill / paint.setStroke. The Properties panel has them with nothing selected
too (they set up the next object), with the Control bar's Stroke link (the Stroke panel as a popover) and weight
spinner. Panel keys (Brushes F5, Color F6, Layers F7, Color Guide Shift+F3, Graphic Styles Shift+F5, Appearance
Shift+F6, Align Shift+F7, Transform Shift+F8, Info Cmd+F8, Gradient Cmd+F9, Pathfinder Cmd+Shift+F9, Stroke Cmd+F10,
Transparency Cmd+Shift+F10, Attributes Cmd+F11, Symbols Cmd+Shift+F11, Character Cmd+T, Paragraph Cmd+Alt+T, Tabs
Cmd+Shift+T, OpenType Cmd+Alt+Shift+T; Cmd is Ctrl on Windows and Linux) run window.panel {panel}, can be changed in
Edit › Keyboard Shortcuts (entry panel:<id>, every panel) and pressed with ui.key; ui.menu.list shows them on
the Window menu's items.
window.panel takes a panel id in any case or the panel's display label ("Layers", "Color Guide").
Collapsing the dock: window.collapseDock {collapsed?} (the » at the top of the dock; omitted toggles) hides the
Properties | Layers | Libraries group and puts its three panels as icons at the top of the icon column, under a «
that expands them again. While collapsed, those icons and window.panel {panel: "properties"|"layers"|"libraries"}
pop the panel out next to the column like the other icon panels (ui.dock_collapsed, ui.open_panel in
ui.inspect); expanding with one popped out shows its tab. The state is saved with the preferences and in user
workspaces; the built-in workspaces expand the dock.
Floating tool groups: dragging or clicking the tear-off bar down a tool group's flyout (or releasing the long press
that opened it over the bar) floats the group as a strip of tool buttons, moved by the same bar and put back in the
toolbar by the × at its top; window.floatTools {tool, floating?} does the same for the group of the current
layout holding tool (omitted toggles; a tool alone in its slot is an error). While a group floats, the presses
that open its flyout raise the strip instead. The strips (ui.floating_flyouts in ui.inspect: the group's tools
and the strip's top-left corner) are saved with the preferences and in user workspaces; the built-in workspaces
float none.
Edit Toolbar: the ••• button at the bottom of the toolbar (or window.editToolbar {open?}) opens the All Tools
drawer beside the toolbar; Escape or a click outside closes it. While it is open, ui.drag a tool from it onto the
toolbar to add it (onto a button's top or bottom edge: its own slot before or after; onto its middle: into its
group), from the toolbar into it to remove it, or within the toolbar to move it. toolbar.addTool {tool, slot?, place?}, toolbar.removeTool {tool, group?} and toolbar.reset do the same for the current Basic or Advanced
toolbar and return its slots; the edits (ui.toolbar_edits) are saved with the preferences.
The Contextual Task Bar: its handle (the grip at its left end) drags it anywhere on the canvas, which always holds
it whole, also when the window shrinks. Unpinned, the bar keeps that offset as it follows the selection, until
another document becomes active. Its More Options (…) menu has Hide Bar (window.taskBar); Pin Bar Position
(window.taskBar.pin {pinned?}, omitted toggles, checked while pinned), which holds the bar where it is across
selection changes (the handle still moves it) and, turned off, lets it follow the selection again from there; Reset
Bar Position (window.taskBar.reset), which unpins it and puts it back under the selection; and Show Properties
Panel. As in Illustrator, neither the position nor the pin is saved: each launch starts with the bar under the
selection.
The Free Transform widget: while the Free Transform tool (E) is active, a small box at the canvas's top left holds
Constrain over the tool's three modes, Free Transform, Perspective Distort and Free Distort. The buttons set the tool's
options, as tool.setOption {key: "mode", value: "free"|"perspective"|"distort"} and {key: "constrain", value} do:
Constrain acts as Shift held (proportional scaling, moves and rotations by 45°) and does nothing in the distort modes,
where it is greyed. The modifier keys still work while dragging (Cmd on a corner distorts it freely, Cmd+Alt+Shift in
perspective, Cmd on a side shears).
Ruler guides are layer objects: guide.add puts a guide on the current layer (or layer), where it shows, hides,
locks and is deleted with its layer and is listed in the Layers panel (a click on its row selects it, a drag onto
another layer's rows moves it there); guide.setLayer {index?, indexes?, layer} moves guides to another layer, and
guide.list gives each guide's layer, shown and editable. Guides on a hidden or locked layer leave the selection
and can't be picked, moved or deleted, and no guide goes onto a locked layer; Duplicate copies a layer's guides, Merge
Selected and Flatten Artwork keep them on the merged layer (Flatten discards a hidden layer's). Guides in files saved
before guides had layers go on the top visible, unlocked layer when the document opens.
Floating panels: dragging a dock tab, a panel icon or a popped-out panel's title out of the dock floats that panel
inside the window, and the strip right of the dock's tabs floats the whole Properties | Layers | Libraries group. A
floating group moves by its title bar (or the strip right of its tabs); a tab dragged out of it floats on its own;
dropped on another group's title bar or tabs it stacks with that group, and dropped on the dock (lit up while the
pointer is over it) or closed with its × its panels go back to the tabbed group or the icon column. Dropped on the upper or
lower half of another group, floating or docked (a line shows where), a group joins it in a set right above or below
it: the groups stack top to bottom, each
showing one of its panels, and move together by the top group's title bar, whose × docks them all; a group dragged by
its tab strip (a lone panel's tab or the strip right of the tabs) leaves the set. A double-click on a tab collapses a
group to its tabs, and another expands it. Dropped on the left edge of the icon column or of a dock column, a set
docks as a new dock column there: its groups stack in it as in a floating set, take groups dropped on their edges and
tabs, and leave it dragged by their tab strip; the strip along the column's top floats the whole column again, and
its × puts its panels back where they live.
window.panel.float {panel, x?, y?, onto?, below?, above?, group?, collapsed?} floats a panel (with group, its whole
group) with its top-left corner at x, y (window points; default cascaded), or stacks it as a tab with the floating
group holding onto, or puts its group in a set right below or above the group holding that panel; collapsed
collapses or expands its group;
window.panel.dock {panel, group?, column?} puts it back, or with column (true, or the place: 1 next to the icon
column, counting left) docks its group and set as a dock column; window.panel.float on a docked panel floats its
column. panel takes the window.panel ids and labels, and "tools" for
the Tools panel, which floats by its title bar and docks on the window's left edge. window.panel on a floating
panel shows its tab (expanding its group). ui.floating_panels in ui.inspect lists the groups (panel ids, the
index of the tab shown, the top-left corner, column: the set it is stacked in, top to bottom in list order,
collapsed, and docked: the dock column it stands in) and ui.toolbar_pos the Tools panel's corner (null: docked); both are saved with the
preferences and in user workspaces (the built-in workspaces dock everything), and a corner saved on a bigger window
is clamped into this one when drawn. Floating panels stay inside the app window (no separate OS windows).
Flatten Transparency: ui.flattenTransparencyDialog opens the flattenTransparency dialog for the selection
(fields preset: a preset name, setting it loads that preset's options; the option keys of
object.flattenTransparency: balance 0–100, lineArtPpi and gradientPpi 1–2400, textToOutlines,
strokesToOutlines, clipComplexRegions, antiAlias, preserveAlpha, preserveOverprints; preview, off at first).
With preview on it flattens on the canvas while open; ui.dialog.confirm keeps the result as one undo step and
ui.dialog.cancel rolls it back. Out-of-range values answer with an error and the dialog stays open.
Flattener presets: ui.flattenerPresetsDialog {selected?} opens Edit → Transparency Flattener Presets (dialog
flattenerPresets). Fields: selected (a preset name), then the selected preset's name and option keys. On a saved
preset, ui.dialog.set of an option saves it and a new name renames it (both through flattener.presets.save);
built-in presets don't change. New, Delete, Import… and Export… are flattener.presets.save {preset},
flattener.presets.delete, flattener.presets.import and flattener.presets.export; ui.dialog.confirm closes.
Opening a .vcflattener file with app.open imports its presets. In the Flatten Transparency dialog, Save Preset…
keeps the dialog's options as a saved preset.
PDF presets: ui.pdfPresetsDialog {selected?} opens Edit → PDF Presets (dialog pdfPresets, field selected: a
preset name). Built-in presets are read-only. New… and Edit… open the preset editor, which ui.pdfPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)} opens too: dialog pdfPreset, the Save PDF
dialog's option fields plus name and description; ui.dialog.confirm runs pdf.preset.save and returns to PDF
Presets (a new preset can't take a name in use). Delete, Import… and Export… are pdf.preset.delete,
pdf.preset.import and pdf.preset.export; ui.dialog.confirm closes. Opening a .vcpdfpresets file with app.open
imports its presets. In the Save PDF dialog, Save Preset… shows a name field (__savePresetAs): while it shows,
ui.dialog.confirm saves the dialog's settings as that preset and selects it instead of writing the PDF.
Flattener Preview: window.panel {panel: "flattenerPreview"} shows the panel; ui.flattenerPreview {highlight?, overprints?, preset?, options?, showOptions?} sets it (a preset loads its options, options adjust them), shows it
and refreshes its snapshot of the document, answering as flattener.preview does. ui.inspect reports the settings
as ui.flattener_preview. The panel keeps its snapshot until Refresh (it notes when the document or the settings
changed since); its preview greys the art and paints the highlighted objects or areas red.
Expand: Object → Expand… (ui.expandDialog, or object.expand without params from the menus, palette and shortcuts)
opens the expand dialog for the selection. Fields: object, fill, stroke (all on; __object, __fill,
__stroke say which the selection has something for, as object.expand.info does, and the others are disabled),
gradient (objects or mesh) and steps (1–1000, default 255). ui.dialog.confirm runs object.expand with them as
one undo step.
Attributes panel: window.panel {panel: "attributes"} (Cmd+F11) shows it; its controls run attributes.set,
path.reverse {reversed} and path.setFillRule {rule}, and its Browser button runs the UI command
attributes.openUrl {url?}, which opens the URL (default: the selection's) in the web browser and answers {url}.
Spot Colors: the Swatches panel menu's Spot Colors… (ui.spotColors) opens the spotColors dialog (field useLab:
true shows and separates spot colours defined in Lab from their Lab values, false from their working-CMYK
equivalents); ui.dialog.confirm runs swatch.spotOptions {useLab} as one undo step.
Scale options: ui.menuDialog {command} opens the dialog a command's menu item opens (object.scale → scale,
object.transformEach → transformEach, also Move, Rotate, Reflect, Shear and the Path dialogs). The Scale dialog
(scale) and Object → Transform → Transform Each… (dialog transformEach: scaleH,
scaleV %, moveH, moveV pt, rotate °, reflectX, reflectY, random, reference 0–8, copy, preview)
have the fields corners (Scale Corners) and strokes (Scale Strokes & Effects). They start from the preferences
scaleCorners and scaleStrokes, and ui.dialog.confirm keeps them as the preferences (prefs.set) before running
object.scale / object.transformEach with them. The Transform panel's checkboxes and flyout item and the Properties
panel's checkbox set the same preferences through prefs.set.
Use Preview Bounds: the Align panel flyout's Use Preview Bounds item shows and toggles the usePreviewBounds
preference (prefs.set); while it is on, the bounding box and the Transform panel, Properties and Control bar
fields measure visual bounds.
Align To: the Align panel's Align To choice is saved with the preferences, and ui.inspect reports it as
ui.align_to ("selection" or "artboard"; Align to Key Object stores no choice). The align buttons of the Align
panel, the Control bar and the Properties panel run object.align with to: "artboard" when one object is selected,
else with "key" while the selection has a key object, else with that choice. The Object → Align items send the same
to while every selected object is selected whole. With direct-selected anchor points or segments they send no to,
and the points align to their own bounds. object.align from engine.execute or ui.menu.invoke without to keeps
the engine's default: the key object while there is one, else the selection.
Width Point Edit: double-clicking a width point with the Width tool, or ui.widthPointEdit {id, index}, opens the
widthPoint dialog (fields id, index, t, side1 and side2: the left and right widths in points, linked,
adjustAdjoining). ui.dialog.confirm runs stroke.widthPoint.set with them; ui.dialog.set {field: "discard", value: true} then confirm (the Delete button) removes the point with stroke.widthPoint.remove.
Corners: double-clicking a Live Corners widget with the Selection or Direct Selection tool, or ui.corners {id?, corners?}, opens the corners dialog for a path's corners (the Direct-Selected ones, else every corner; fields id,
corners: anchor indices of the path with its corners uncut, a rectangle's 0–3 clockwise from the top-left, kind:
round, invertedRound or chamfer, radius in points; kind or radius is absent while the corners differ). ui.dialog.confirm runs object.setLiveShape with them.
Perspective plane options: double-clicking a plane widget of the perspective grid, or ui.perspectivePlane {plane},
opens the perspectivePlane dialog (fields plane: left, right or ground; location: points along the plane's
normal; objects: none, move or copy). ui.dialog.confirm runs perspective.plane.move with them.
Units: dialog distance fields (Move's dx/dy, shape sizes, Offset Path's offset, Split Into Grid's gutters and totals,
Artboard Options' sizes, effects' distances, transformEach's moveH/moveV, New Document's width/height, the
length preferences in preferences) show in the General unit (the active document's units, or unitsGeneral without a
document). Through ui.dialog.set a number is in points and a string may carry its unit ("10 mm"; a bare number in a
string is points). The preferences dialog's unitsGeneral starts as the active document's units, and OK sets them.
newDocument has units (a unit label; print presets start in unitsGeneral, screen presets in Pixels).
Document Setup: File → Document Setup… (file.documentSetup, Cmd+Alt+P) opens the documentSetup dialog. Its fields
are what document.setup reports (units, bleed [top, bottom, left, right] in pt, outlineImages,
highlightSubstitutedFonts, highlightSubstitutedGlyphs, gridSize, gridColors, simulatePaper, flattenerPreset,
discardWhiteOverprint, language, quotes, typographersQuotes, superscript, subscript, smallCapsSize,
exportText, backgroundContents) plus tab (General, Transparency or Type) and bleedLinked.
ui.dialog.confirm runs document.setup with them as one undo step; a bad value answers with an error and the dialog
stays open.
New Document: File → New… (file.newDialog, Cmd+N) opens the newDocument dialog. Its fields are file.new's params
(preset, name, width and height in pt, units, artboards, artboardLayout, bleed, backgroundContents,
colorMode, rasterEffectsPpi, previewMode) plus category (the tab: Recent, Saved, Mobile, Web, Print, Film &
Video, Art & Illustration, Branding, Social), advanced (Advanced Options open), bleedLinked and presetName
(Save Preset's name). Setting preset to a name from file.newPresets fills the other fields from that preset;
changing a field afterwards makes the settings custom (preset becomes ""). More Settings is the newDocumentMore
dialog over the same fields (its Profile is category). ui.dialog.confirm runs file.new with the fields (with
previewMode: "pixel" the app turns Pixel Preview on); on an error the dialog stays open.
Swatch Conflict: through the app, when the pasted objects' global or spot swatches conflict with the document's
(see clipboard.conflicts) and the params have no swatchConflict, every Paste command opens the swatchConflict
dialog instead and answers {dialog: "swatchConflict"}. Fields: conflicts ([{name, document, clipboard, spot}]), index (the conflict asked about), choice (merge or add), applyToAll and choices (the answers so
far). ui.dialog.confirm records the answer and asks about the next conflict (with applyToAll, answers the rest
the same); the last one runs the paste with swatchConflict. ui.dialog.cancel pastes nothing.
Paste placement: through the app, edit.paste and edit.pasteWithoutFormatting without center, dx or dy
paste at the centre of the view. The Paste menu items are enabled while the system clipboard holds something to
paste (SVG, PDF, text or a bitmap on Windows; text or a bitmap on macOS; text on Linux, whose paste keys still take a
bitmap; SVG on the web, whose paste keys also take pictures and files), even with nothing copied in the app (looked
at up to four times a second; ui.menu.list shows it). The Layers panel menu (≡ on
the dock's tab strip while Layers shows) lists the layer commands (New Layer…, Duplicate and Delete Selection,
Options for Selection…, clipping mask, isolation, Locate Object, Merge Selected, Flatten Artwork, Collect in New
Layer, Release to Layers, Reverse Order, Template, Hide/Outline/Lock Others or Show/Preview/Unlock All Layers), Paste
Remembers Layers (layer.pasteRemembersLayers, checked when on) and Panel Options…. Layer Options is the
layerOptions dialog (ui.layerOptions {ids?}, ui.newLayer {sublayer?}), Panel Options the layersPanelOptions
dialog (ui.layersPanelOptions), and ui.layersExpand {ids?, open?} opens or closes rows; see the Layers panel
section of docs/mcp.md for the row commands (layer.setCurrent, layer.highlight, layer.move…).
File Info: File → File Info… (file.info from the menu or Cmd+Alt+Shift+I; ui.fileInfoDialog for agents) opens the
fileInfo dialog. Its fields are what file.info reports (title, author, authorTitle, description,
keywords (a list: the chips), rating, copyrightStatus, copyrightNotice, copyrightUrl; created and
modified are shown read-only) plus __keyword, keywords typed but not added yet (comma-separated; OK adds them).
ui.dialog.confirm runs file.info with them as one undo step; a bad value answers with an error and the dialog
stays open.
Document Raster Effects Settings: Effect → Document Raster Effects Settings… (ui.rasterEffectsSettingsDialog for
agents) opens the rasterEffectsSettings dialog. Its fields are what document.rasterEffectsSettings reports
(resolution, colorModel, background, antiAlias, clippingMask, addAround in pt, preserveSpotColors);
ui.dialog.confirm runs it with them as one undo step; a bad value keeps the dialog open. Object → Rasterize… opens a
command dialog for object.rasterize whose fields (ppi, colorModel, background, antiAlias, clippingMask,
addAround) start from these settings.
Missing linked files: app.open of a document whose linked images' files can't be found (document.open's
missingLinks) opens the missingLinks dialog for the first (fields missing: [{name, path, ids}], modified,
applyToAll, path). ui.dialog.set {field: "path", value} then ui.dialog.confirm replaces it with that file
(links.relink; without path a file is picked), and with applyToAll the others are looked for by name in the same
folder; ui.dialog.set {field: "discard", value: true} then confirm ignores it (the images keep their preview), with
applyToAll the rest too. The next missing file is asked about after each answer; ui.dialog.cancel stops asking.
Then, with the preference updateLinks: "askWhenModified", modified linked files are offered for update in a
confirm dialog whose ui.dialog.confirm runs links.update. The same happens when the app's window comes back to
the front and a linked file of the active document changed meanwhile (asked once per change of a file); with
"automatically" the files are read again at once, with "manually" the Links panel shows them as modified.
Find Linked Files: in the missingLinks dialog, ui.dialog.set {field: "searchFolder", value} then ui.dialog.confirm
is Find in Folder…: the dialog becomes findLinks, which looks in that folder and its subfolders for every file the
dialog still asks about (links.findFiles). Fields: document (the document's uid), missing ([{name, path, ids}]),
modified, back, state (empty, searching, done, stopped or failed), search (the search's id), folder,
found (links.findFiles's links, with the files found), searched, skipped, unreadable, stopped, error,
picks (the file chosen for each row of missing, or null; with each search result, a row not chosen in the dialog
gets its only same file, else its only file found), touched (the rows whose file was chosen in the dialog; search
results leave their pick as it is), copy (Copy into the document's folder, shown for a saved document) and
searchFolder. ui.dialog.set {field: "searchFolder", value} then ui.dialog.confirm searches that folder and keeps
the dialog open. The fields change as frames draw the dialog; links.findFiles {} reports the search directly.
ui.dialog.confirm without searchFolder is Relink: each row with a file runs links.relink {ids, path, copy}, one
undo step each (copy for images only: a placed document is relinked where its file is), and it returns {relinked, failed}; the status line names the first file that failed. With back (opened from missingLinks), the rows left
without a file and those that failed go back to that dialog, then the modified files are offered for update.
ui.dialog.cancel closes it and stops its search. ui.findLinksInFolder {folder?, ids?} (Find in Folder… in the Links
panel menu) asks for a folder (or takes folder), then opens the dialog for the files of ids, else of the selected
linked images and placed documents, else the missing ones; with nothing to look for, it fails with "No linked files to
look for" and asks for no folder. On the web, ui.findLinksInFolder is disabled, links.findFiles can't start a search
and the dialog doesn't open.
Missing fonts: once the missing linked file questions are answered, or at once when there are none, app.open of a
document whose type uses fonts that aren't available (text.missingFonts) opens the missingFonts dialog, one document
at a time, and so does links.editOriginal of a placed document. When another dialog takes its place (Import PDF for a
file opened next, for example), the dialog opens again once that one closes. Fields: document (the document's uid),
fonts ([{family, style, status, resolved}]), fontsNextToDocument (the Fonts folder next to the document, or
empty), state (empty, searching, done, stopped or failed), search (the search's id), folder, found
(text.findFontFiles's fonts, with the files found), searched, skipped, unreadable, stopped, error and
chosen (the files to add; a font's first file is chosen when the search finds it). Its buttons are Find Fonts and
Close, with Search Fonts Folder, Find in Folder… and, once files are found, Add Fonts in the dialog.
ui.dialog.set {field: "discard", value: true} then ui.dialog.confirm is Find Fonts: the dialog closes and Type ›
Find Font opens (dialog findFont) with the first missing font selected. ui.dialog.set {field: "searchFolder", value} then ui.dialog.confirm searches that folder (text.findFontFiles), keeps the dialog open and clears
searchFolder, also when the search doesn't start. The fields change as frames draw the dialog; text.findFontFiles {} reports the search directly. ui.dialog.confirm without discard or searchFolder is Add Fonts: it copies the
files in chosen (text.addFontFiles) and closes the dialog. ui.dialog.cancel (Close) closes it and stops its
search. After Find Fonts or Close, the dialog doesn't open again for that open.
ui.missingFontsDialog {folder?} opens the dialog for the active document, searching folder when given.
ui.findFontsInFolder {folder?} (Find in Folder… in Type › Find Font…, shown when the document misses fonts) asks for a
folder, then opens the dialog searching it. The web shows a status line instead of the dialog, and both commands are
disabled there.
Text Import Options: placing a .txt file through the app (file.place with a file and no text options, the Place
dialog, a drop) opens the textImport dialog (fields platform: windows/mac, characterSet: unicode/ansi,
removeLineReturns, removeParagraphReturns, replaceSpaces, spaces). ui.dialog.confirm places the file as area
type with those options (file.place {text: {…}}).
System clipboard formats: through the desktop app, edit.copy and edit.cut publish what clipboard.flavours
lists to the system clipboard, and every Paste command first loads what another app copied (clipboard.importSvg,
importPdf, importEps, importText or importImage, centred in the view; the desktop app reads PDF and EPS from
the Windows clipboard and, with SVG, from the macOS pasteboard, and an .eps file copied in a file manager anywhere); an import error answers the Paste with that error
and pastes nothing. Cmd+V also pastes a bitmap or PDF with no text beside it (on the key's release, as egui sends no
Paste event for it). Screenshots paste on every platform: Windows reads PNG, else CF_DIBV5/CF_DIB, macOS the
pasteboard's PNG or TIFF, Linux image/png, and the web the picture the browser's paste event carries (#457).
File menu details: file.openRecent1…file.openRecent30 open the recent files the recentFilesCount preference
lists (0 hides them all; file.recentFiles returns the listed paths), file.reveal shows the document's file in the
system file manager (desktop only; answers {path}), and file.newFromTemplate without params opens a file picker
in the Templates folder (preference templatesFolder). Older native files open as " [Converted]" (preference
appendConverted), so their Save asks for a new .vectorcraft name. Native files reopen at the view they were saved
with.
DXF Options: Export As… with the DXF format (file.exportAs {format: "dxf"}, then the save dialog), or
ui.dxfOptionsDialog {path?, …document.exportDxf options} for agents, opens the dxfOptions dialog. Its fields are
document.export params: version, unit (a unit name such as Millimeters), scale, scaleLineweights, colors
(8, 16, 256, true), rasterFormat (png, jpeg), preserve (appearance, editability), selectedOnly,
alterPaths, outlineText, plus path and the artboard choice. ui.dialog.confirm checks them (a bad value, or
Export Selected Art Only with nothing selected, keeps the dialog open), remembers them for the next DXF Options and
writes the file (asking for a path when there is none). Export As also lists DWG, greyed out: setting its format to
dwg shows, and on confirm answers, the hint to export DXF instead.
DXF Import Options: opening a DXF drawing through the app (app.open, file.open, a drop) or placing one
(file.place with a .dxf file and no dxf options, which answers {"dialog": "dxfImport"}) opens the dxfImport
dialog (mode: open or place; layouts, units and version describe the drawing). Its fields are the
document.open dxf options: layout (one of layouts), fit, unit (a unit name such as Millimeters) and
scale (1 unit = scale drawing units; ignored with fit), scaleLineweights, center, mergeLayers.
ui.dialog.confirm opens or places the drawing (placing keeps the other file.place params in __place) and
remembers fit, scaleLineweights, center and mergeLayers for the next drawing; an error (an unknown layout,
art too large at that scale) keeps the dialog open.
Placement Options: the Links panel's flyout (or ui.placementOptionsDialog {ids?} for agents) opens the
placementOptions dialog for the selected images (fields ids, preserve: transforms/bounds/fileDimensions/
fit/fill, align: topLeft … bottomRight, clip). ui.dialog.confirm runs links.placementOptions with them as
one undo step. The Links panel (window.panel {panel: "links"}) shows links.list and the first selected image's
links.info; links.editOriginal and links.reveal (Edit › Edit Original, Show in Folder) hand the linked file to
the system and return {path}.
Package: File → Package… (ui.packageDialog) opens the package dialog for a saved document (a document never saved
gets a confirm dialog whose OK runs file.saveAs). Fields: folder (the location), name (the package folder),
copyLinks, linksFolder, relink, copyFonts, report. ui.dialog.confirm saves unsaved changes, runs
file.package and then opens a confirm dialog whose OK runs file.showPackage {folder}; the web downloads the zip
instead. Document Info's flyout picks a category and Save… runs docInfo.save {path?, selectionOnly?}.
Slices: through the app, object.slice.options and object.slice.divide with no params open their dialogs on the
selected slices. sliceOptions (fields kind: image/noImage/htmlText, name, url, target, message,
alt, text, background: ""/matte/#rrggbb, hAlign, vAlign; __html says whether HTML Text applies)
runs object.slice.options on ui.dialog.confirm; divideSlices (fields divideRows, rows, rowMode:
count/size, rowHeight, and divideColumns, columns, columnMode, columnWidth) runs object.slice.divide.
Read slice options with slice.list.
The Slice tools: ui.pointer drags with the slice tool make a user slice (Shift square, Alt from the centre; one
undo step). With sliceSelection a click selects a slice (Shift toggles), a drag moves the selected slices, a drag on
a selected user slice's handle resizes it, a double click opens sliceOptions and ui.key Delete deletes the
selected slices. Hidden or locked slices (view.slices.hide, view.slices.lock) can't be picked.
EPS Options: Export As… with the EPS format (file.exportAs {format: "eps"}, then the save dialog), or
ui.epsOptionsDialog {path?, …document.exportEps options} for agents, opens the epsOptions dialog. Its fields are
document.export params: previewFormat (none, tiffBw, tiffColor), transparentPreview, overprints
(preserve, discard), flattenerPreset (a built-in or saved preset's name), includeLinkedFiles, thumbnails,
cmykPostScript, compatibleGradients, level (2, 3), plus path and the artboard choice (useArtboards,
range). ui.dialog.confirm checks them (a bad value or an unknown preset keeps the dialog open), remembers them for
the next EPS Options and writes the file(s), asking for a path when there is none.
Data Recovery: at launch, copies left behind by a crash open the recovery dialog (never copies another running
instance keeps, so instances started side by side for screenshots don't get it). Fields: copies ([{file, title, saved}], from file.recovery.list) and discard. ui.dialog.confirm restores them all (file.recovery.restore
each, opening " [Recovered]" documents), discard: true then confirm deletes them, ui.dialog.cancel keeps them
for the next launch. While copies are written in the background ui.inspect lists them under background
("Saving recovery data for ").
Native save options: File → Save As to a native or .ai file opens the saveOptions dialog after the save panel
(heading "VectorCraft Options" or "PDF-compatible .ai Options"). Fields: separateArtboards, all (true: every
artboard; false: range, such as "1-3, 5"), includeLinked, embedProfiles, pdfCompatible, compress, and for
native files version and preview. A bad range keeps the dialog open; ui.dialog.confirm writes the file and the
artboards' files.
EMF and WMF: Export As… lists both formats (file.exportAs {format: "emf"} or "wmf"); they have no options
dialog, so ui.dialog.confirm asks for the path and writes the file (one per artboard with Use Artboards). Open and
Place read .emf and .wmf; a paste of image/emf from a system clipboard service runs clipboard.importEmf.
TIFF, BMP and Targa Options: Export As… with the TIFF, BMP or Targa format (file.exportAs {format: "tiff"}, then
the save dialog) opens tiffOptions, bmpOptions or tgaOptions: the raster rows of PNG Options (ppi,
background, antiAlias), then TIFF's colorModel (rgb, cmyk, gray; a CMYK document starts on cmyk),
byteOrder (little, big), lzw and embedIcc; BMP's colorModel (rgb, gray), fileFormat (windows,
os2), depth (1, 4, 8, 16, 24, 32), reduction and dither (4 and 8 bits), rle and flipRows; Targa's depth
(16, 24, 32). The BMP dialog keeps its choices writable together (OS/2 falls back to 24 bits and turns RLE and flipped
rows off; RLE turns flipped rows off). ui.dialog.set the fields and ui.dialog.confirm writes the file(s) with
document.export.
Save for Web: File → Export → Save for Web (Legacy)… (file.saveForWeb, Ctrl/Cmd+Alt+Shift+S) opens the saveForWeb
dialog on the remembered settings (webExport.settings). Its fields are the document.exportForWeb settings
(format, colors, reduction, dither, quality, colorTable, width, percent, output…; set preset to load
a preset) plus __view (original, optimized, 2up, 4up), __zoom (fit or a percentage), __kbps and
__color (the colour table's selected colour). ui.dialog.confirm remembers the settings and saves (a save panel for
the image, or the page with HTML output; the web downloads the files); with discard: true it is Done: it remembers
the settings and closes. file.saveForWeb with settings saves without the dialog, and file.saveForWeb.browser
writes the HTML page to a temporary folder and opens it in the default browser (desktop).
PSD Options: Export As… with the PSD format (file.exportAs {format: "psd"}, then the save dialog) opens psdOptions:
the raster rows of PNG Options (ppi, background, antiAlias), then colorModel (rgb, cmyk, gray; a CMYK
document starts on cmyk), layers (true: Write Layers; false: Flat Image, whose background list starts at White),
maxEditability and hiddenLayers (with layers) and embedIcc. ui.dialog.set the fields and ui.dialog.confirm
writes the file(s) with document.export.
Print: File → Print… (file.print with no params, Ctrl/Cmd+P) opens the print dialog on the document's print
settings. Its fields are the print.setup settings (copies, artboards, range, media, orientation,
placement, scaling, marks, bleed, output, graphics, color, margin…; a section is an object, as
{field: "marks", value: {"trim": true}}) plus printer (a name from print.printers, "" the system's default) and
toFile (save the job as a PDF), and the UI-only __section (General, Marks and Bleed, Output, Graphics, Color
Management, Advanced, Summary) and __sheet (the page the preview shows, 0-based). ui.dialog.confirm (Print) keeps
the settings with the document (print.setup, one undo step) and prints them: to the printer through the system's
print queue (the web: the browser's print dialog), else, with toFile or no printing available, a save panel for the
PDF. With discard: true it is Done: it only keeps the settings. Settings that can't print (a missing artboard, a
range of no tiles) answer with an error and keep the dialog open. file.print {settings?, printer?, toFile?, path?}
prints without the dialog; print.printers lists the printers and print.printerSetup {printer?} opens their system
settings (Setup…). Desktop submission returns {background: true, printed: false, printer, status}
while the spooler runs on a worker. ui.inspect lists it under background; completion or refusal
appears in ui.status. Quit waits for submissions. PDF/PostScript export and the browser's print
dialog retain their existing command results.
Agents still get {printers: [{name, default}], service, setup} from print.printers;
discovery failure yields an empty list. Print replies preserve pages, bytes, format and
warnings alongside the pending fields above. Validation/start failures use the existing control
reply {ok: false, error: "…"}; later spooler failures appear in ui.inspect → ui.status, with
no second reply or structured backend error object. Headless MCP keeps its engine-only file.print
result (path or dataBase64, plus the same print metadata); no spooler fields are added there.
Print presets: in the print dialog, preset names the print preset whose settings were loaded last; setting it
(ui.dialog.set {field: "preset", value: "Posters"}) loads that preset's settings, and the list reads [Custom] once
they differ. Save Preset… shows a name field (__savePresetAs): while it shows, ui.dialog.confirm saves the dialog's
settings as that preset (print.presets.save) and selects it instead of printing. ui.printPresetsDialog {selected?}
opens Edit → Print Presets (dialog printPresets, field selected); [Default] is protected. New… and Edit… open the
preset editor, which ui.printPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts from)} opens too: dialog printPreset, the Print dialog's settings fields plus name; ui.dialog.confirm runs
print.presets.save and returns to Print Presets (a new preset can't take a name in use). Delete, Import… and
Export… are print.presets.delete, print.presets.import and print.presets.export; ui.dialog.confirm closes.
Opening a .vcprintpresets file with app.open imports its presets.
Modifiers on synthetic input: ui.key, ui.click, ui.drag and ui.wheel take shift, alt, ctrl and cmd (Command on
macOS, Ctrl elsewhere), and the app holds them for the frames the input spans, as if the keys were down: a key's
press, its text and its release; a click's press and release; a drag from the press through every move to the
release. Every handler sees them (Shift+arrow nudges by the big increment, Alt+arrow nudges a copy, Shift-clicking a
Layers selection square adds to the selection); the keyboard's own modifiers apply again afterwards.
Cutting tools: Mirror & Cut (mirrorCut), Line Cut (lineCut) and Rectangle Cut (rectCut) cut the selected paths
and compound paths. ui.tool.select one, then drag with ui.pointer: the cut previews live and the release keeps it
as one undo step (ui.key Escape cancels). They run path.mirrorCut, path.lineCut and path.rectCut, which an
agent can call directly. Mirror & Cut's tool options (tool.setOption, shown in the Control bar) are axis
(free | vertical | horizontal; a constrained axis follows the pointer and a click places it) and keep
(left | right | top | bottom); Alt on release keeps the other side.
Puppet Warp (puppetWarp): with art selected, the tool shows pins at once (automatic ones at the centre and the end
of each limb until pins are placed). ui.pointer clicks on the art add pins (each new pin is selected; Shift-click
adds a pin to the selection or takes it out), dragging a pin moves every selected pin and warps the art, and
Alt-dragging near (not on) a selected pin turns the art around it. A click off the mesh adds nothing: the cursor shows
not-allowed there and the canvas says so. ui.key Delete or Backspace removes the selected pins. Adding, deleting and
each drag are one undo step each, running object.puppetWarp {rest: true, …}; the pins live in the document, so
Undo/Redo take them back with the art, they stay across tool switches while the selection is unchanged, and another
selection starts afresh. object.puppetWarp.pins reads them. Its tool options (tool.setOption, shown in the
Control bar) are expand (Expand Mesh, points, 0 allowed), showMesh (on by default) and selectAllPins (true
selects every pin, false none); ui.inspect → toolOptions lists the pins the tool last showed and the
selected ones.
Colour adjustment effects (Effect → Color Adjustments) open the effect dialog like the other effects
(effect.dialog {effect: "adjust.hueSaturation"}): its fields are the effect's parameters (sliders for the amounts,
channel for Curves and Levels, points for Curves, color for Shift to Color) and they preview live; set them with
ui.dialog.set and ui.dialog.confirm to apply (Curves also has a graph: drag its points). Object → Vector
Halftone… (engine.execute {command: "ui.menuDialog", params: {command: "object.vectorHalftone"}}) opens the
vectorHalftone dialog (fields shape, frequency, angle, mode, color, invert, clip, keepOriginal,
preview): it previews live and ui.dialog.confirm keeps the halftone as one undo step.
Tool options last: tool.setOption {key, value} or {values: {…}} (and tool to set another tool's) sets them, and
the options a tool keeps (Liquify brush and tool options, Mirror & Cut, Puppet Warp, Symbolism, drawing tools) stay
when you switch tools or documents and are saved with the preferences. The Liquify tools share their Global Brush
Dimensions (width, height, angle, intensity).
Perspective grid: View → Perspective Grid → Define Grid… (ui.perspectiveGridDialog) opens the perspectiveGrid
dialog prefilled from the grid. Its fields are those of perspective.grid.define (kind, units, scale,
gridline, angle, distance, horizonHeight, thirdVp, leftColor, rightColor, groundColor, opacity;
lengths in units, so setting units converts them). ui.dialog.confirm runs perspective.grid.define; a refused
value keeps the dialog open.
Object → Envelope Distort's dialogs open through their menu items or ui.menuDialog {command}. Make with Warp…
(object.envelope.makeWithWarp) opens envelopeWarp (fields style, horizontal, bend, h, v, preview, and
reset); while an envelope is selected the menu shows Reset with Warp… instead, and the same item, shortcut or
object.envelope.resetWithWarp opens it with reset: true and the envelope's values. Make with Mesh… / Reset with
Mesh… open envelopeMesh (rows, cols, reset, and maintainShape when resetting). Envelope Options…
(object.envelope.options) opens envelopeOptions (antiAlias, preserveShape, fidelity, distortAppearance,
distortLinearGradients, distortPatternFills) with the selected envelope's values, or with nothing selected the
defaults for new envelopes. They preview live while an envelope is involved, and ui.dialog.confirm keeps the result
as one undo step.
Liquify Tool Options: double-clicking a Liquify tool (tool.options {tool: "twirl"}) opens a liquifyOptions dialog.
Its fields are tool, the Global Brush Dimensions width, height (pt), angle, intensity (%) and usePressure,
then the tool's options detail, simplify and simplifyOn (Warp, Twirl, Pucker, Bloat), rate (Twirl),
complexity, affectAnchors, affectIn and affectOut (Scallop, Crystallize, Wrinkle), horizontal and
vertical (Wrinkle, %), and showBrush. ui.dialog.confirm runs tool.setOption {tool, values}. ui.pointer
events take pressure (0..1, default 1): it is the Liquify intensity while Use Pressure Pen is on.
Freehand Tool Options: double-clicking the Pencil, Paintbrush, Smooth, Blob Brush or Eraser tool
(tool.options {tool: "pencil"}) opens a freehandOptions dialog. Its fields are tool and the options the tool
keeps: fidelity (pt; Pencil, Paintbrush, Smooth), fill (Pencil, Paintbrush), closeWithin and editWithin (screen
pixels, 0 turns it off; Pencil, Paintbrush) and size (pt; Blob Brush, Eraser). ui.dialog.confirm runs
tool.setOption {tool, values}.
ui.pointer events also take holdMs (0..60000): the pointer then holds still that long, button down, before the
next event. Twirl, Pucker and Bloat keep applying while held (a repeat of the last point every 0.1 s), exactly as
for the same time held with the mouse. Liquify leaves type, symbols, images, graphs, meshes, envelopes, repeats and
blends as they are and says so in the status bar.
Perspective grid presets: in the perspectiveGrid dialog, name is the preset whose fields are loaded (setting the
fields of a preset, as the Preset menu does, keeps its name; other changes clear it, which reads [Custom]). Save
Preset… sets __mode: "save" and __from: "define" with name the new preset's name: ui.dialog.confirm then saves
the fields as that preset (perspective.presets.save) and returns to Define Grid on it. View → Perspective Grid →
Save Grid as Preset… (ui.savePerspectivePreset) opens the same dialog in save mode; OK saves and closes. Edit →
Perspective Grid Presets… (ui.perspectivePresetsDialog {selected?}) opens dialog perspectiveGridPresets (field
selected): New… and Edit… open the preset editor (perspectiveGrid with __mode: "edit", __original the preset
edited), whose OK runs perspective.presets.save and comes back; Delete, Import… and Export… are
perspective.presets.delete, import and export. View → Perspective Grid → One/Two/Three Point Perspective list
that type's built-in views and the saved presets (ui.perspectiveUserPreset<type>.<n>, hidden while empty). Opening a
.vcperspective file with app.open imports its presets.
Blend Options (Object › Blend › Blend Options…, ui.blendOptions, the Blend tool's double-click, Alt-click and
toolbar button) opens the blendOptions dialog on the selected blend's options: spacing (smooth | steps |
distance), steps, distance (pt), orientation (page | path) and preview; it previews live and
ui.dialog.confirm keeps the change as one undo step. With no blend selected (__target: "defaults") OK sets what
new blends start with. The Blend tool (ui.tool.select {tool: "blend"}): ui.pointer down on an object, then on
another, blends them; down on an anchor point blends from that point; each further object clicked joins the blend.
A selected blend shows its spine. With Direct Selection (directSelection) a ui.pointer drag from a spine point
moves it (object.blend.spine.moveAnchor, one undo step) and, once a point is clicked, from one of its handles
reshapes the curve, and a drag on a curved spine segment bends it (object.blend.spine.reshapeSegment); the Anchor
Point tool (anchorPoint) bends any segment of a selected blend's spine, straight ones too. Direct and Group Selection
pick a blend's key objects. With the Pen tool a click on a selected
blend's spine adds a point (on a point no key sits on, deletes it).
Perspective grid view options: View → Perspective Grid lists Show/Hide Grid, Show/Hide Rulers, Snap to Grid
(checked), Lock/Unlock Grid and Lock Station Point (checked); ui.menu.list reports the current labels. They run
perspective.grid.show, rulers, snap, lock and lockStation.
Perspective grid widgets: the Plane Switching Widget is fixed in a corner of the canvas (ui.pointer with
space: "screen" reaches it at the same pixels whatever the zoom); a press on it with any tool picks the plane while
the grid shows. ui.key with key "1"…"4" picks the left, horizontal, right or no plane. Double-clicking the
Perspective Grid tool (tool.options {tool: "perspectiveGrid"}) opens perspectiveGridOptions (fields show,
position: topLeft, topRight, bottomLeft, bottomRight); ui.dialog.confirm runs perspective.widget.options.
Envelopes on the canvas: pointer_gesture with the Mesh tool (mesh) or Direct Selection on a selected mesh envelope
drags its points (a press on a point focuses it, and a press on one of the focused point's handle ends drags that
handle); a Mesh tool click inside a mesh envelope adds a row and a column. While an envelope or its content is
selected the Control bar shows its controls (Edit Envelope / Edit Contents, warp style, orientation, bend and
distortions or mesh rows and columns, Reset, Envelope Options); each runs an object.envelope.* command.
Shared panel docking
Panel arrangements support tab grouping and reordering, horizontal and vertical splits, and movable, resizable floating groups inside the application window. Drag a panel tab, or use its context menu. Floating groups remain within the application viewport.
The shared layout begins with the existing workspace on the first docking operation. Workspace saves retain its groups, active tabs, split sizes, floating geometry, and the positions of hidden panels. Resetting a built-in workspace restores its default dock.
window.panel.move {panel, anchor, zone?, before?}moves a panel into the target group.zoneistab(default),center,left,right,top, orbottom;beforereorders tabs by panel ID. The destination must already be visible.window.panel.float {panel, x?, y?, width?, height?}detaches a panel.window.panel.dock {panel, anchor?, zone?, before?}returns a panel to its previous dock placement when possible, or to the supplied destination.window.panel.activate {panel}reveals and selects a panel, restoring its saved position when possible.window.panel.close {panel}hides it.
Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and invalid destinations return an error without changing the arrangement.
window.panel.layout {action} also accepts the shared, externally tagged action schema.
For example, {"action":{"MoveFloating":{"panel":"properties","rect":[44,66,360,450]}}}
updates a floating group's position and size; {"action":{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}}
resizes the root divider (false/true path entries select first/second children).
Move with placement: {"Tab":{"before":"swatches"}} reorders tabs, and
SetStackOpen {panel,open} / ResizeStack {panel,height} control accordion entries.
The UI dispatches these same commands. Unknown panel IDs, invalid geometry, and stale
split paths return an error without changing the workspace.