Merge main into feat/jxl-import

This commit is contained in:
dotCooCoo
2026-10-26 11:59:56 -07:00
400 changed files with 35996 additions and 8793 deletions
+89 -69
View File
@@ -1,6 +1,6 @@
# Control protocol
> **Last updated:** 2026-10-23 · **Change:** symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); previously: synthetic input budgets (#1709); bounded transport waits (#1726).
> **Last updated:** 2026-10-25 · **Change:** brush library panels (`window.brushLibrary`, `window.brushLibrary.other`, `ui.saveBrushLibrary`); previously: Transform Each opens on the values it was last confirmed with; its Copy button confirms with `copy: true` (#1732); previously: panel docking on craft-ui's layout: `ui.docking`, the parameters of `window.panel.float` and `window.panel.dock`; previously: `ui.screenshot`'s 64 MiB inline-PNG cap and its path-plus-data downgrade; symbol library panels (`window.symbolLibrary`, `window.symbolLibrary.other`, `ui.saveSymbolLibrary`); synthetic input budgets (#1709); bounded transport waits (#1726).
`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
@@ -102,8 +102,9 @@ the two can still win that race.
| `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above |
| `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?, imageTracePreview?}` | `imageTracePreview`: the Image Trace panel's Preview (`ui.inspect` shows it as `ui.image_trace_preview`) |
| `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.dialog.set` / `.confirm` / `.cancel` | `{field, value}` or `{fields: [{field, value}…]}` | fill and submit the open dialog. `ui.dialog.set` takes the fields the dialog has (`ui.inspect`), `__` fields, `discard` on a dialog with a discard button, `preview` on one with a live preview, the fields that the dialog reads but adds only when a control changes (such as `searchFolder`, or `copy` in the Transform dialogs), and the fields it passes to the command it runs: every parameter of the `command` dialog's command (`ui.paramDialog`), and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects; any other field is an error that lists the dialog's fields, and the dialog stays as it was `fields` sets the list (at most 256 items) in order (a missing `value` is `null`) and checks every item first: when one is refused, the call is an error and no field changes |
| `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. The inline PNG is capped at 64 MiB: with a `path` the file is still written and `pngBase64` is omitted when the image is larger, while a path-less `data: true` request fails if either the raw RGBA image or the encoded PNG exceeds the cap. 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?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `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 |
@@ -186,7 +187,9 @@ tools opens its dialog too (nothing happens without a selection), with the trans
point. For `lineSegment`, `arc`, `rectangularGrid` and `polarGrid` it opens the tool's Tool Options: the dialog its
click opens (kind = the tool id), without `x` and `y` and with `__toolOptions: true`, whose `ui.dialog.confirm` runs
`tool.setOption {tool, values}` and draws nothing; the next drag with the tool and its click dialog use the values
(the line's `fillLine`, the grids' Default Size `width`, `height` and `reference` too). Gradient tool handles snap to
(the line's `fillLine`, the grids' Default Size `width`, `height` and `reference` too). For `typeOnPath` and
`verticalTypeOnPath` it opens Type on a Path Options for the selected type on a path (a `command` dialog running
`type.pathOptions`), or fails when no type on a path is selected. 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
@@ -285,6 +288,18 @@ Defined libraries are `window.userSymbolLibrary1`…`10` in Window → Symbol Li
save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `symbol.saveLibrary` (to a
file it asks for a path and suggests `name` with `.vectorcraft`).
Brush libraries open in the same panels: `window.brushLibrary {library}` (`library_panels` kind `brushes`; `library:
null` closes them). Clicking a brush there runs `brush.addFromLibrary {library, name}`, adding it to the document's
Brushes in one undo step, with `apply: true` when paths are selected (they take the brush in the same step; the step is
Apply Brush when the document has the brush already and the click only applies it);
Shift/Cmd-clicks select brushes for Add to Brushes. A brush dragged onto a path runs it with `apply: true` and that
path as `ids`, and one dragged onto the Brushes panel runs it without. `window.brushLibrary.other {path?}` loads the
brushes of any document VectorCraft opens (a saved brush library is a `.vectorcraft` document) and opens them there.
The User Defined libraries are `window.userBrushLibrary1`…`10` in Window → Brush Libraries. `ui.saveBrushLibrary
{names?}` (Save Brushes…, the Brushes panel's Save Brush Library…) opens the `saveBrushLibrary` dialog (fields
`name`, `user`: save to the user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs
`brush.saveLibrary` (to a file it asks for a path and suggests `name` with `.vectorcraft`).
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.
@@ -309,12 +324,10 @@ Edit › Keyboard Shortcuts (entry `panel:<id>`, every panel) and pressed with `
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.
Collapsing the dock: `window.collapseDock {collapsed?}` (the » / « on the dock's column bar; omitted toggles the
outer column) collapses every expanded dock column to icons (`true`) or expands the outer column, Properties |
Layers | Libraries (`false`). A panel in a collapsed column opens as a flyout from its icon, as `window.panel` does
(`ui.open_panel` in `ui.inspect`, `flyout` in `ui.docking`); the state is `ui.dock_collapsed`.
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
@@ -360,32 +373,12 @@ and can't be picked, moved or deleted, and no guide goes onto a locked layer; Du
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).
Floating panels: the window's panels and the canvas are one craft-ui docking layout (see Shared panel docking
below). Dragging a tab, a group's drag bar or an icon out of the dock and dropping it on the canvas or on no target
floats it; dropping it on a group's tabs, on a group's upper or lower edge or beside a column docks it there. On
the desktop each floating window is an OS window of its own; on the web and on Wayland it floats inside the app
window. `ui.toolbar_pos` in `ui.inspect` is the Tools panel's corner (null: docked); `window.panel.float` and
`window.panel.dock` with `panel: "tools"` float it at `x`, `y` or dock it on the window's left edge.
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
@@ -447,7 +440,10 @@ Scale options: `ui.menuDialog {command}` opens the dialog a command's menu item
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`.
panel's checkbox set the same preferences through `prefs.set`. Transform Each opens on the values it was last
confirmed with (scale, move, rotate, reflect, random, reference, `objects` and `preview`; never `copy`), kept with the
UI state (#1732). Its Copy button, left of Cancel, sets `copy: true` and confirms; `ui.dialog.set {field: "copy",
value: true}` then `ui.dialog.confirm` does the same.
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
@@ -484,7 +480,7 @@ string is points). The `preferences` dialog's `unitsGeneral` starts as the activ
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`,
`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
@@ -658,7 +654,7 @@ EPS Options: Export As… with the EPS format (`file.exportAs {format: "eps"}`,
`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. With Use Artboards and two or more
artboards, it writes a central file of the whole document under the chosen name and one `{stem}_{artboard}.eps` per
artboard.
artboard. When no art shows, it writes only the artboard files.
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,
@@ -807,7 +803,8 @@ keeps: `fidelity` (pt; Pencil, Paintbrush, Smooth), `fill` (Pencil, Paintbrush),
Blob Brush; on by default for the Pencil and Paintbrush and off for the Blob Brush, and while it is off the path a
stroke draws, continues or merges into is not selected afterwards), `mergeOnlySelection` (Blob Brush; off by default,
and while it is on a stroke merges only with selected blobs), `closeWithin` and `editWithin` (screen pixels, 0 turns
it off; Pencil, Paintbrush) and `size` (pt; Blob Brush, Eraser). `ui.dialog.confirm` runs
it off; Pencil, Paintbrush), `size` (pt; Blob Brush, Eraser), and `angle` (°, −180 to 180; 0) and `roundness`
(%, 1 to 100; 100) of the Eraser's tip (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
@@ -858,39 +855,58 @@ distortions or mesh rows and columns, Reset, Envelope Options); each runs an `ob
## 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 canvas and the panels form one craft-ui docking layout: the protected `canvas` panel beside a dock at the
window's right edge. The dock's columns, from the window edge inwards, are the Properties | Layers | Libraries
column, the icon column (one group per row of icon panels) and any columns the user adds. Floating windows hold
groups stacked top to bottom or split side by side. Workspaces save the layout (`ui.docking` in `ui.inspect`;
absent while the workspace was never customized and its layout follows `ui.dock_tab`, `ui.dock_collapsed` and
`ui.dock_width`) and where each hidden panel was (`ui.docking_hidden`). `ui.inspect` has no `floating_panels`.
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.
Commands (`panel` and every panel parameter take a panel id in any case or its display label):
- `window.panel.move {panel, anchor, zone?, before?}` moves a panel into the target group.
`zone` is `tab` (default), `center`, `left`, `right`, `top`, or `bottom`; `before` reorders
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.
- `window.panel.collapse {panel, collapsed?}` shows the dock group holding a panel as its tabs
only, as a double-click on a tab does (without `collapsed` it toggles), and makes the panel the
group's shown one; showing another of its panels expands the group. Only a group that is the
whole dock or sits above or below another collapses (one beside another is refused). Floating
groups collapse with `window.panel.float {panel, collapsed}`. Returns `{panel, collapsed}`.
| Command | Params (`?` optional) | Does |
|---|---|---|
| `window.panel.move` | `{panel, anchor, zone?, before?, group?, placement?}` | into the group holding `anchor`: `zone` is `tab` (default), `center`, `left`, `right`, `top`, `bottom`, `above` or `below` (a group of its own above or below it); `before` reorders tabs |
| `window.panel.float` | `{panel, x?, y?, width?, height?, group?, column?, onto?, above?, below?, collapsed?}` | floats the panel, its group (`group`) or its dock column or floating window (`column`; a number counts as `true`) at the rect given, else inside the dock area at the size a drag floats it at |
| `window.panel.dock` | `{panel, anchor?, zone?, before?, group?, column?, side?, onto?, above?, below?, icon?, collapsed?}` | without a destination, a floating panel goes back where it floated from (else joins the first dock group) and a hidden one shows where it was; `anchor` and `zone` as for `move`; `side` (`Start`, `End`) makes it a new innermost column of that dock |
| `window.panel.activate`, `.close`, `.closeGroup`, `.closeOthers` | `{panel}` | shows and selects, or hides |
| `window.panel.minimize` | `{panel, minimized?}` | minimizes a dock group to its tabs, or a floating window to its title bar; omitted toggles |
| `window.panel.collapse` | `{panel, collapsed?}` | `window.panel.minimize`; returns `{panel, collapsed, layout}` |
| `window.panel.iconic` | `{panel, iconic?}` | collapses the panel's dock column to icons, or expands it |
| `window.panel.layout`, `ui.dock` | `{action}` | one craft-ui `Action`, externally tagged, e.g. `{"MoveFloating":{"panel":"swatches","rect":[44,66,360,450]}}` or `{"ResizeSplit":{"path":[],"size":{"Ratio":0.35}}}`; `ui.dock` also takes craft-ui's `{operation, …}` |
Panel IDs are stable across themes. Invalid IDs, non-finite or invalid geometry, and
invalid destinations return an error without changing the arrangement.
Parameters from VectorCraft's own docking, on `window.panel.float` and `window.panel.dock`:
`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.
- `onto`: as a tab of the group holding that panel. On `float` that group must float; on `dock` it is `anchor`.
- `above`, `below`: as a group of its own above or below the group holding that panel, in its floating window or
dock column.
- `column: n` on `dock` (with no other destination): its group (or the panel, when `group` is false and its group
holds others) as a new dock column at place `n`, 1 next to the icon column, counting inwards. A panel whose group
floats takes its whole floating window along. `column: true` with no `anchor` or `side` docks at the innermost place.
- `icon: true` on `dock`: in a row of its own at the end of the icon column.
- `collapsed`: then minimizes (`true`) or restores (`false`) its group or floating window.
Give at most one of `onto`, `above`, `below`, `icon` and a column place, and none of them with `anchor`, `side`,
`zone`, `before`, `x`, `y`, `width` or `height`. The commands return `{layout, closed, floating}`: the layout,
the panels the command closed, and the floating panels. A hidden panel is shown first. Unknown panels, invalid
geometry or destinations, and stale split paths return an error and leave the layout unchanged.
`ui.docking` (a control method, read only) returns:
- `layout`: the layout in use; `customized`: whether the workspace has a saved layout; `hidden`: the hidden panels
with a remembered place.
- `flyout`: the panel open as a flyout from an icon column; `focused`: the panel the user last clicked or pressed
in; `autoCollapse`: flyouts close on a press outside them; `panelsShown`: false while Tab or a full screen mode
hides the panels.
- `hitRects`: `[{hit, rect}]`, every docking control the main window drew last frame (tabs, drag bars, borders,
column bars, » buttons, icons, icon grips, floating title bars and close buttons, resize edges, dividers) as a
craft-ui `Hit`, e.g. `{"Tab":"layers"}`, with `[x, y, width, height]` in window points. Groups, columns and
windows are named by their first panel.
Floating windows in OS windows of their own (the desktop): `hitRects` leaves out their controls, and `ui.pointer`,
`ui.click` and `ui.drag` reach the main window only, so drive them with commands. Such a window's place on screen is
`floating[].native` in the layout (`rect` is where it would float inside the app window). Keys typed in one run the
main window's shortcuts; modal dialogs block them.
## Synthetic input hardening checklist
@@ -928,6 +944,10 @@ hidden tab (whose frames the browser pauses) answers when it is shown again.
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | Panel docking on craft-ui's layout: `ui.docking`, the floating and dock parameters, native floating windows. |
| 2026-10-24 | minor | Documented the brush library panel commands (`window.brushLibrary`, `.other`, `ui.saveBrushLibrary`, `window.userBrushLibrary1`…`10`). |
| 2026-10-24 | minor | Documented `ui.screenshot`'s 64 MiB inline-PNG reply cap: with `path` the file is still written and `pngBase64` is omitted when over the cap; a path-less `data: true` request fails before encoding. |
| 2026-10-25 | minor | Documented that Transform Each opens on the values it was last confirmed with and that its Copy button confirms with `copy: true` (#1732). |
| 2026-10-23 | minor | Documented the symbol library panel commands (`window.symbolLibrary`, `.other`, `ui.saveSymbolLibrary`, `window.userSymbolLibrary1`…`10`). |
| 2026-10-13 | minor | Documented count, pending-event and per-frame request budgets (#1709). |
| 2026-10-13 | minor | Documented transport deadlines and connection-slot recovery (#1726). |
+7 -3
View File
@@ -1,5 +1,6 @@
# Development
> **Last updated:** 2026-10-25 · **Change:** Linux Wayland drag-and-drop note names eframe 0.37 (still on winit 0.30).
> **Last updated:** 2026-10-23 · **Change:** Tests: cargo xtask test, one integration-test binary per crate, the dev profile's debug info.
> **Last updated:** 2026-10-21 · **Change:** the interface faces as vectorcraft-ui-egui's `interface-fonts` feature, and `theme::install_fonts` for an embedder that draws the panels itself.
> **Last updated:** 2026-10-21 · **Change:** leaving out the bundled fonts with the egui panels (vectorcraft-ui-egui's and vectorcraft-mcp's `bundled-fonts` features).
@@ -35,6 +36,7 @@ How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop:
- **Save / Save As / Export** go through `Services::download`: a Blob, an object URL and a temporary `<a download>`, all created from Rust. There is no save dialog, so the suggested name becomes the download name.
- **Drag-and-drop:** `WebShell` takes the frame's `dropped_files` before the app sees them, reads each with `DroppedFile::bytes_async` and pushes the bytes into the inbox (files to be placed on a canvas go to `Services::place_inbox` with their `place::DropAt`: the document they were dropped on, by uid, and the point; `VectorcraftApp::drop_target` decides from the position the page's drag events give). Desktop drags carry no position (winit 0.30 reports none, and the pointer position egui last had is stale), so the desktop app decides by kind: documents open, pictures and text are placed in the middle of the view. On the web and the desktop, swatch and graphic style libraries, Libraries panel files, presets, plug-ins and shortcut sets open wherever they are dropped, as File › Open opens them (`io::opens_as`). A file that arrives after another document became active still lands in its own document, which becomes active again; if that document closed meanwhile, the file is not placed and the status bar says so. (The app's synchronous drop path is compiled out on wasm32.)
- **Pen and touch input** (`apps/vectorcraft-web/src/pencil.rs`): the shell reads pen and finger Pointer Events itself, in the capture phase on the window, before eframe's touch and pointer handlers (which can't tell a pen from a finger and see each touch twice, as a pointer and as a touch event). Mouse and trackpad events stay eframe's, as do finger events until the first pen contact, preserving eframe's touch-only keyboard path. Pen buttons and event modifiers follow eframe's mappings. A pen drives egui's pointer and is a touch contact whose force is the pen's pressure; its coalesced events (an Apple Pencil reports up to 240 a second) reach the tool one by one with their pressure (`canvas::frame_moves`); holding it still opens the context menu (egui's long touch) and drops the tool's press. Fingers are touch contacts (pinch, two-finger pan, the undo and redo taps); the first one also drives the pointer, so it taps panels and menus. Once a pen has been seen in this page session (reset by reloading; no persistent storage), a finger never presses a tool on the canvas (`touch::finger_navigates_only`), and a pen landing cancels the fingers already down (palm rejection). The page itself doesn't scroll, zoom or select under touch (`touch-action: none`, `overscroll-behavior`), and the canvas stays inside the safe areas.
- **Shortcuts the browser also has** (`apps/vectorcraft-web/src/keys.rs`): many of the app's Cmd and Ctrl chords are the browser's too (`Cmd+=`, `Cmd+-` and `Cmd+0` zoom the page, `Cmd+Z` undoes in the browser, in Safari Undo Close Tab, `Cmd+[` goes back, `Cmd+F` finds), and eframe lets most of them through. A capture listener on the window cancels the browser's action for a press the keymap in effect binds, the user's own shortcuts included (`shortcuts::runs_a_shortcut`, matched as the dispatcher matches it); egui still gets the key and runs the command. It acts only while the canvas or eframe's text input has the keyboard, and never on a plain or Shift press, an AltGr press (Windows reports it as Ctrl and Alt) or an IME's keys, which type; Cut, Copy and Paste stay the browser's, whose clipboard events carry the text, and so does Reload. Browsers keep a few chords from every page, which never reach it (Safari's `Cmd+L`, so New Layer's shortcut does nothing there; Chrome's `Cmd+T`, `Cmd+W`, `Cmd+N`): those still go to the browser. While the app has the keyboard, the page-zoom keys zoom the canvas instead; Preferences › User Interface › UI Scaling and the browser's View menu still scale the interface.
- **Data Recovery** keeps its copies in `localStorage` (`vectorcraft-recovery/<area>/<name>`, shared by the site's tabs), so tabs can't lock their areas the way desktop apps lock a folder (`RecoveryStore::lock`). Each tab holds its area with a heartbeat it refreshes every minute and, where the browser has Web Locks (secure pages: `https://` or localhost), with a Web Lock named after the area (`apps/vectorcraft-web/src/locks.rs`, through `js_sys::Reflect` since web-sys has them only as an unstable API). The browser releases the lock only when the tab is gone, not while its timers are paused in the background, which a heartbeat can't tell apart (#367). `navigator.locks.query()` is asynchronous, so the shell keeps a snapshot refreshed every 10 seconds (taken once before the first frame); a snapshot older than a minute counts every area as held. An area is offered as a crash's leftovers only when no tab holds its lock and its heartbeat is older than three intervals. Without Web Locks a paused tab can still be taken for gone; it repairs that when it resumes: its next heartbeat (or recovery save) writes again the copies missing from its area, and a heartbeat newer than the one the other tab wrote taking the area over makes the area running again for that tab.
- **Losing the graphics (#369):** the browser can drop the page's WebGPU device or WebGL context (a GPU reset, a driver update, too many tabs on the GPU; or a script calling `device.destroy()` / `WEBGL_lose_context.loseContext()`). The app goes on without it, so the shell watches for it: wgpu's device-lost callback for WebGPU, the canvas's `webglcontextlost` event for WebGL2. Both report to a `graphics::GraphicsLoss`, and the next frame of `WebShell::logic` handles it:
1. `VectorcraftApp::graphics_lost` writes Data Recovery copies of the modified documents at once (to browser storage, also when Data Recovery is turned off).
@@ -114,7 +116,7 @@ The window runs natively on Wayland (eframe's `wayland` feature) and on X11. On
- **Clipboard:** under Wayland arboard talks to the compositor through the data-control protocol (wlroots compositors such as Sway and Hyprland, KDE Plasma), so bitmaps (`image/png`), SVG markup and text copied in any app paste (#398). Where the compositor lacks the protocol (GNOME's Mutter, [arboard#223](https://github.com/1Password/arboard/issues/223)), arboard falls back to the X11 clipboard through XWayland, which only holds what X11 apps copied. egui's own text paste into fields goes through smithay-clipboard and works either way.
- **Clipboard owners that never answer (#510):** reading the clipboard waits on the app that owns it: arboard gives up on an X11 owner after 4 s, and a Wayland read waits for the owner to send its data. So on Linux the check that enables the Paste menu items runs on a background thread with a second clipboard handle (`crates/ui-egui/src/clipboard_probe.rs`, installed as `Services::clipboard_probe`), only while nothing is copied in the app and a document is open, and the window keeps drawing. A paste itself still reads on the UI thread, so pasting from such an owner waits for it once. Windows and macOS keep the check on the UI thread: Windows only asks which formats the clipboard holds, macOS asks the pasteboard server.
- **Copied files:** files copied in a file manager (`text/uri-list`; `CF_HDROP` on Windows, file URLs on macOS) paste as the first one that is art (SVG, PDF, EPS, EMF/WMF or a bitmap; a `.ai` or `.ait` file from the editing data it holds, else as the PDF or PostScript it holds), before the clipboard's other formats (which include the files' paths as text).
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.36 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.37 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
- **Pens and pen displays under Wayland (#491, #2028):** winit 0.30 doesn't bind the Wayland tablet protocol (`zwp_tablet_v2`; tablet input arrives in winit 0.31, [winit#4318](https://github.com/rust-windowing/winit/pull/4318), [egui#7731](https://github.com/emilk/egui/pull/7731)). Since Plasma 6.3, KWin no longer turns tablet input into pointer input for such apps ([kwin!6336](https://invent.kde.org/plasma/kwin/-/merge_requests/6336), commit [25c65abd](https://invent.kde.org/plasma/kwin/-/commit/25c65abddd95eb87fb892a8594faccbf509ff62f)), and GNOME's Mutter does not turn it into pointer input either (that commit's message): on KWin the pen moves the compositor's cursor over the window, but the app gets no motion or presses and shows none of its own cursors. Until eframe moves to winit 0.31, either run the app under XWayland, which supports the tablet protocol and hands the pen to X11 apps as a pointer (Preferences › Performance › Linux Display Server › X11 (XWayland); X11 (XWayland) When a Pen Is Attached (`x11WithPen`), which opens the window through XWayland only when `/proc/bus/input/devices` lists a pen as the app starts, a device with the `BTN_TOOL_PEN` key and the `ABS_X` and `ABS_Y` axes, and logs `pen found: opening the window through XWayland`; `WAYLAND_DISPLAY= vectorcraft`, or `env -u WAYLAND_DISPLAY vectorcraft`; for a desktop launcher, put `env WAYLAND_DISPLAY=` before the command in its `Exec=` line), or turn KWin's deprecated emulation back on for the whole session by setting `KWIN_WAYLAND_EMULATE_TABLET=1` in KWin's environment (for example in a file under `~/.config/environment.d/` or in `/etc/environment`) and logging in again. The pen then works as a mouse. Under XWayland the app also reads the pen's pressure (next item); with KWin's emulation on native Wayland the pen has no pressure. Other compositors that don't emulate a pointer for tablets behave the same way.
- **Pen pressure under X11 and XWayland (#764):** winit 0.30 hands a pen to an X11 window as a mouse, without its pressure. When the window is on X11, XWayland included, the desktop app opens a second X connection (x11rb's `RustConnection`) and reads XInput2 raw events for every device (`apps/vectorcraft/src/tablet.rs`, from PhotoCraft's `crates/tablet`). A device with an `Abs Pressure` valuator is a pen, unless its XInput type is `TOUCHPAD` or `TOUCHSCREEN`. While a pen is the pointer that moved last, its pressure, scaled from the valuator's range to 0..1, reaches the tools that use pressure: the Paintbrush's pressure-sensitive brushes and the Liquify tools' Use Pressure Pen. A pen device whose name or XInput type contains `eraser` (in any case) is the pen's eraser end: while the pointer is over the window, the canvas selects the Eraser tool when the eraser end moves, and it selects the tool used before when the tip moves again (`crates/ui-egui/src/stylus.rs`). The X server needs XInput 2.1 or later; otherwise the log has a `tablet pressure:` warning with the reason, and the pen draws at full pressure.
@@ -278,8 +280,9 @@ other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Georgian, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian, Vietnamese, Finnish and Turkish
translate status and error messages (`COMPLETE_MESSAGES` in `i18n::tests`); the untranslated report lists the
message literals the scan finds that one of them has no row for. The scan reads `Err("…")`, `Other(…)`,
`#[error(…)]` and `status(…)` messages, messages given to an error enum's variant (`PsError::Limit(…)`), to the
engine's `bad(cmd, …)`, `err(…)` or `error(…)`, and to the PDF writer's `bad(…)` or `failed(…)`
`#[error(…)]` and `status(…)` messages, messages given to an error enum's variant (`PsError::Limit(…)`, the reason
in `EngineError::Disabled(id, …)`), to the engine's `bad(cmd, …)`, `err(…)` or `error(…)`, and to the PDF writer's
`bad(…)` or `failed(…)`, and skips calls inside comments
(`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them
all). A missing row shows the English message. Other languages show messages in English until they add `@msg`
rows.
@@ -489,6 +492,7 @@ Scores and hour estimates are unchanged.
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | Linux Wayland drag-and-drop note names eframe 0.37 (still on winit 0.30); behavior unchanged. |
| 2026-10-23 | minor | Tests: `cargo xtask test` (nextest, then doctests), one integration-test binary per crate and the files that keep their own, the dev profile's debug info. |
| 2026-10-21 | minor | Fonts › Leaving out the bundled fonts: vectorcraft-ui-egui's `interface-fonts` feature, and `theme::install_fonts` for an embedder that draws the panels without `VectorcraftApp::logic`. |
| 2026-10-21 | minor | M3: highlighted-text Eyedropper sampling preserves ranges and rejects unsupported source/target transparency atomically; regression-tested; scores and estimates unchanged. |
File diff suppressed because it is too large Load Diff
+2
View File
@@ -131,6 +131,8 @@ the app's own editing copy of a file (an EPS after its `%%EOF`, a `.ai`'s `AIPri
| Date | Change | Summary |
|---|---|---|
| 2026-10-25 | minor | `.ai` import: the editing data is read up to 512 MiB on 64-bit builds (the limit was 256 MiB), so a file whose embedded rasters unpack to 300 MiB or more keeps its layers, groups and text instead of opening as its PDF part (part of #3510); 32-bit and web builds keep 256 MiB. Own-fixture test `import::ai::tests::editing_data_past_256_mib_is_read`; scores and hours unchanged |
| 2026-10-25 | minor | `.ai` import: the `--` placeholder of a radial gradient's highlight (`-- -- angle length Bh`, a gradient without one) and the `XC` operator after a linked image's file name no longer send the whole file to its PDF part, so files with them keep their layers, groups and live text (#4176). A `--` anywhere else is still an operator the reader doesn't know. Own-fixture tests in `import::ai::tests`; scores and hours unchanged |
| 2026-10-24 | minor | EPS, EMF/WMF and legacy `.ai`: type with ruby (ふりがな) is written with the reading's glyphs where the canvas draws them (they were dropped); legacy `.ai` writes such type as outlines, since a text object can't hold ruby. Tests `eps::tests::ruby_is_written_with_its_type`, `tests_legacy_ai::type_with_ruby_is_outlines_with_the_reading`, `metafile::tests::ruby_is_written_with_its_type`; scores and hours unchanged |
| 2026-10-24 | minor | `.ai` import: an image larger than the editing data's reader takes (2^25 pixels, a halftone texture at print resolution) no longer leaves its layer blank: the file comes in as its PDF part, which reads larger images, with a note saying why (#1479). Own-fixture test; scores and hours unchanged |
| 2026-10-24 | minor | PDF export: a pattern fill or stroke is written as a PDF tiling pattern (ISO 32000-1, 8.7.3.1) of one period of the tiling, as the canvas repeats it, so grid, brick and hex tilings and overlapping tiles keep their look in a few kilobytes, and PDF import reads it back as one object with a pattern swatch instead of hundreds of tile paths (#3849). Patterns nested more than four deep, or with tiles beyond a billion points, are still written as their tiles, now with a warning. Tests: the issue's hatch round trip, a brick tiling read back against the canvas, the nesting warning; scores and hours unchanged |
+60 -21
View File
File diff suppressed because one or more lines are too long
+4 -2
View File
@@ -12,7 +12,8 @@ documentation (GPU Performance, touch workspace, pen pressure pages); ours from
**~35% (estimated), 25–45 h.** VectorCraft rasterizes the canvas on the CPU (multithreaded `vello_cpu`) and uses
the GPU only to composite, so any GPU that can show a window works, at the cost of GPU-accelerated preview
on huge documents. VectorCraft reads pen pressure on Windows, on the web, on macOS, and on Linux under X11 and XWayland, and it
reads the pen's eraser end on macOS and on Linux under X11 and XWayland.
reads the pen's eraser end on macOS, on Linux under X11 and XWayland, and on the web when it presses.
reads the pen's eraser end on Windows, on macOS and on Linux under X11 and XWayland.
| Feature | Illustrator | VectorCraft macOS | Windows | Linux | Web | Status / hours |
|---|---|---|---|---|---|---|
@@ -22,7 +23,8 @@ reads the pen's eraser end on macOS and on Linux under X11 and XWayland.
| Restart on another GPU when the first fails to present | — | yes (#502, #964) | yes | yes | restart on lost device (#369) | done |
| Pen pressure | yes (Wacom, Windows Ink, macOS tablet events) | yes, read from AppKit's tablet events before the window gets them (winit reports none), each move of a stroke with its own pressure, mouse event coalescing off (#852; not yet tried on a Mac) | yes (pointer force via `WM_POINTER`) | under X11 and XWayland (Preferences › Performance › Linux Display Server, #1151, #2028), read from XInput2 raw events (#764); **no** on native Wayland, where winit reports none and the pen may not reach the window (#491) | browser pointer events, with coalesced events; Apple Pencil on iPad (hover, palm rejection: fingers navigate after a pen contact until reload) | partial: 2–4 h for Wayland (winit 0.31), plus a check on a Mac |
| Pen tilt, bearing, barrel rotation (6D Art Pen) | yes, drives brushes | no | no | no | no | missing: 4–8 h after the input layer (#372) |
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | no | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | no | partial |
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | no | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | a press of the eraser end (Pointer Events eraser button, `button` 5) selects the Eraser tool, and that press erases; a press of the tip brings back the tool used before; Pointer Events report no end while the pen hovers | partial |
| Pen eraser end | yes | the eraser end (AppKit's tablet proximity events) selects the Eraser tool and the tip brings back the tool used before, as on Linux (#852; not yet tried on a Mac) | with Windows Ink, an inverted pen or its eraser button (`PEN_FLAG_INVERTED`, `PEN_FLAG_ERASER` in the pen's pointer messages) selects the Eraser tool and the tip brings back the tool used before, as on Linux (not yet tried with a pen) | under X11 and XWayland, the eraser end selects the Eraser tool and the tip brings back the tool used before (#764); **no** on native Wayland | no | partial |
| Touch gestures (pinch zoom, two-finger pan, rotate view, Touch workspace) | yes (Windows touch, Surface) | trackpad pinch, scroll and rotate | pinch, pan; two-finger tap undo, three-finger redo (#585) | same where winit reports touch | eframe until the first pen contact, then Pointer Events (pen and fingers told apart); long press opens the context menu | partial: touch workspace and touch-screen rotation missing, 3–5 h |
| Trackpad (smooth scroll, pinch) | yes | yes | precision touchpads | yes | yes | done |
| HiDPI / Retina, per-monitor scale | yes | yes | yes | yes | yes | done; a window dragged between monitors of different scale can still grow on Windows (#1459, open); the custom title bar's window on Windows stays truly borderless so the pointer lines up with what is drawn (#1791, #1758; awaiting the reporter's confirmation) |
+29
View File
@@ -32,6 +32,35 @@ white beside a cream), at most six; scattered specks of a colour add none. Adobe
its own palettes are chosen; this is VectorCraft's own method (see
[image-trace-quality.md](image-trace-quality.md#auto-color-keeps-rare-colours)).
**Document Library color groups.** With Palette › Document Library and Library › Document Swatches,
the panel's **Colors** menu traces with every document swatch (**All**) or with the colors of one
color group from the Swatches panel, as the public
[Image Trace panel options](https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html)
page ([archived](https://web.archive.org/web/20251230195658/https://helpx.adobe.com/illustrator/desktop/manage-objects/traces-mockups-symbols/image-trace-panel-options.html))
describes Document Library. In the params of `imageTrace.make` or `imageTrace.makeAndExpand`,
`"colorGroup": "<name>"` names the group. It needs `"mode": "color"`, `"palette": "documentLibrary"` and
`"library": "document"`; a `colorGroup` with a nonempty name and any other mode, palette or library is refused. An
empty name names no group and is accepted with any mode, palette or library; with Document Library and
`"library": "document"` it traces with every document swatch. A group whose name matches exactly is used first, then one whose name differs only in case. At
most `colors` of its colors are used. A missing group and a group with no solid colors are refused. Choosing another
mode, palette or library in the panel clears the group. A group the active document does not have shows as All
and traces as All.
**Document Library** traces with the library's colors as they are. Each pixel takes the library
color nearest to it in lightness and hue, with a difference in hue or chroma counted three times
as much as one in lightness, so a gray or a muted brown takes a black, white or other neutral
library color rather than a saturated color of the same lightness.
**Abutting and Overlapping.** Abutting is the default Method, and the Default, Auto Color, 3 Colors,
6 Colors and 16 Colors presets use it. With Abutting, two neighboring colors share one exact edge. The
canvas smooths the edges of each path on its own, so each of the two paths covers only part of the
pixels along that edge, and a thin line of the background can show between the colors. Web browsers
and PDF viewers usually draw an exported SVG or PDF file the same way and show the same line. Raster
exports (PNG, JPEG, TIFF and the other bitmap formats) with None anti-aliasing do not show it at any
size. Raster exports with Art Optimized anti-aliasing, the default, do not show it up to 10 million
pixels and 16,384 pixels on a side. A raster export shows the line as the canvas does when it uses
Type Optimized anti-aliasing, or when it uses Art Optimized and is larger than either limit.
Overlapping stacks the colors instead: a lower color continues under the edge of the color above it,
and no background shows along that edge.
**Large images.** An image of more than 1500 pixels on a side is traced as any other, only more
slowly, and the trace says so: `imageTrace.make` returns the note in `warnings` and the status bar
shows it (the [results optimization](https://helpx.adobe.com/illustrator/using/image-trace-results-optimization.html)
+189 -28
View File
@@ -9,7 +9,7 @@ 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` or `ui_inspect`, `type_text`, `open_panel`, `screenshot {window:true}` or `ui_screenshot`). |
| **Headless** | `vectorcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui` or `ui_inspect`, `type_text`, `answer_dialog`, `open_panel`, `screenshot {window:true}` or `ui_screenshot`). |
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.
@@ -105,7 +105,7 @@ The two flags (PhotoCraft and the other Craft apps take the same ones, so one cl
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,
updating links, the images an SVG links to, swatch library, graphic style library, symbol library, brush 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
@@ -233,8 +233,10 @@ nothing truncated the reply is the plain summary. Both must be non-negative inte
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"`).
node (layers and groups too) reports its opacity when it is below 1, in both scales: `opacity` is the 0–1 fraction
the document stores and `opacityPct` the 0–100 % that `object.setProps`, `transparency.set`, `draw_path` and
`draw_shape` take (so an agent passes `opacityPct` back, never `opacity`); 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
@@ -356,7 +358,7 @@ The older names below remain listed because existing workflows use them.
| `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)) |
| `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, drawn as `export` draws a PNG with `background: "white"` at the scale that fits `max_side` (at most 1). Artboards too large to render within the allocation bound return a tool error |
| `render_preview` | Optional `max_side` (1–4096, default 1024), `artboard` (0-based, default 0) and `traceViews`; inline PNG of that artboard without editing it, drawn as `export` draws a PNG with `background: "white"` at the scale that fits `max_side` (at most 1). With `traceViews: true`, Image Trace objects draw in the view set with `imageTrace.setView` (outlines, the source image…), as in the app's window; by default they draw their tracing result. 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. `ui_inspect` is the same call as `inspect_ui`, and `ui_screenshot` is `screenshot` with `window: true`. `ui_screenshot` takes an optional `max_side` (1–4096), as `screenshot {window: true}` does: a capture whose longer side is larger is scaled down to `max_side` pixels on that side, and the reply's text gives the returned size as `width`/`height` and the capture's as `capturedWidth`/`capturedHeight`. Without `max_side` the full capture comes back |
Every tool declares read-only, destructive, idempotent and open-world hints. File-writing
@@ -413,15 +415,16 @@ objects' fills or strokes differ (`fillMixed` / `strokeMixed`, drawn as a "?" pr
| `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. Headless, a `move` replies after the tool's highlight under the pointer is ready, including the Shape Builder's and the Live Paint Bucket's regions of a large selection, which the desktop app finds off the UI thread and highlights after the reply. |
| `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. |
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. Returns `{applied, objects}`: `objects` is the number of objects painted, as the `objects` of the `paint.setFill`, `paint.setStroke` and `stroke.set` replies (the largest of them): a selected group counts the objects inside it. When it is 0, a `note` says that only the paint for new art changed. |
| `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. |
| `answer_dialog` | `{set?: [{field, value?}], action?}` | Remote only. Sets fields of the app's open dialog in the order given with one `ui.dialog.set {fields}` (a missing `value` is `null`). The app stores all of them or none. When the app refuses the `ui.dialog.set`, for example because no dialog is open, the call is an error and `action` is not sent. Then `action` `confirm` (OK, `ui.dialog.confirm`) or `cancel` (`ui.dialog.cancel`). Give `set`, `action` or both; `set` holds at most 256 fields. `inspect_ui` shows the open dialog and its fields as `ui.dialog`. Returns `{dialog, result}`: the dialog after the fields were set (or `null`) and the reply to `confirm` or `cancel` (or `null`). After `invoke_menu` `app.quit`, `file.close` or `file.closeAll` replies `{"pending": "saveChanges"}`, `{action: "confirm"}` saves, `{set: [{field: "discard", value: true}], action: "confirm"}` closes without saving and `{action: "cancel"}` cancels the close or quit. |
| `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?, max_side?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. |
| `screenshot` | `{path?, scale?, artboard?, window?, max_side?, traceViews?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard with the pixels of a PNG `export` with `background: "white"` at the same scale; `traceViews:true` draws Image Trace objects in the view set with `imageTrace.setView`, as in the app's window; `window:true` captures the app window (remote only), as `ui_screenshot` does, and `max_side` (1–4096, window captures only) scales the returned image down as described for `ui_screenshot`. With `path` the backend (the headless session or the app) writes the PNG there too; a window capture written there keeps its full size with `max_side`. |
| `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, JPEG XL (an image opens as a document of its pixel size; a Photoshop file as its merged image; a JPEG XL file as its first frame). 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?, compress?, version?, preview?, options?}` | 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). For a native file, `compress: true` gzips it (default: the `useCompression` preference, Preferences › File Handling › Use Compression), `version` (1 to 4) writes it for an older VectorCraft (1 and 2 are never compressed, and `version: 2` with `compress: true` is an error), and `preview: true` embeds a PNG of the first artboard. `options` passes the format's save options as `run_command document.formats` lists them. When `compress`, `version` or `preview` is given both at the top level and in `options` with different values, the call is an error (`` `compress: true` and `options.compress: false` disagree: give the option once ``) and nothing is written. The save drops each option the written format doesn't read. The reply's `warnings` then name each such option given (for example `` `compress` was not used: SVG files don't read it `` for an `.svg` path, and `` `options.preview` was not used: … `` for a key in `options`; a `.ai` file reads `compress` but not `version` or `preview`). `run_command document.save` and the other save commands refuse any option given beside the path and in `options` with different values, and give the same warning for a top-level `compress`, `version` or `preview`. The headless session and the app receive the same `app.save` params, and the app is also sent `wait: true`: the call returns once the file is written, or with the error (such as a path outside the write root), even with Background Save on. When the save asks first (the question before replacing a file that opened with losses, or on macOS and Linux the file dialog for an untitled document without `path`), the reply is `{pending}` or `"cancelled"` as before, and the save that runs after the answer uses the Background Save preference. |
| `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. |
| `export` | `{path?, format?, scale?, artboard?, range?, selection?, outlineText?, options?}` | `svg`, `svgz`, `pdf`, `ai` (the PDF-compatible `.ai` that Save writes, which carries the document when every artboard is written in order), `eps`, `dxf`, `emf`, `wmf`, `png`, `jpg`, `webp`, `gif`, `png8` (an indexed `.png`), `tiff`, `bmp`, `tga`, `psd` (layered), `txt` (the document's text), `vectorcraft` or `template` (a native template). The tool's `format` enum and `document.formats` list them, generated from the engine's format table. When `format` is omitted, it comes from the path's extension. PDF and `.ai` write one page per artboard: all of them, or `artboard` (0-based) / `range` (`"1-3, 5"`, 1-based). SVG and SVGZ write one file per chosen artboard. EPS writes one file of the visible art; with `artboard`, `range`, `options.artboards` or `options: {"useArtboards": true}` it writes one file per chosen artboard, after a central file of the visible art when there are two or more, and `useArtboards: false` keeps the one file. Rasters, DXF, EMF and WMF write one artboard, or one file per chosen artboard with `options: {"useArtboards": true}`. Rasters also take `options: {"rect": [x, y, width, height]}` (page points, y down): that area of the page at the given `ppi`/`scale`, without an artboard and without changing the document; it is refused with `artboard`, `artboards`, `range` or `useArtboards`, with a non-positive or non-finite size, and for non-raster formats. `options` carries more format options (e.g. `{"quality": 80}` for JPEG). `selection: true` exports the selected objects cropped to their bounds (the reply adds their `bounds`, and reports `format` and the encoder's `warnings` as a whole-document export does); `outlineText: true` writes SVG text as paths. Template layers are left out, live effects are kept, and exporting `vectorcraft` never changes the document's path. Without `path` the bytes come back as `dataBase64`. Both backends run the same `document.export` call. |
| `add_text` | `{text, x?, y?, width?, height?, fit?, fitMinPercent?, path?, mode?, pathEffect?, size?, font?, color?, justify?, vertical?}` | Point type at (x, y) (`justify`: `left`, `center` or `right` starts, centres or ends each line on x); area type with `width`/`height` (`fit`: `none`, `autoHeight`, `shrinkText`, as `text.areaOptions`); or `path` + `mode` (`area`/`onPath`) to flow text in or along a path, with `pathEffect` (`rainbow`, `skew`, `3dRibbon`, `stairStep`, `gravity`); `justify` (also `auto`, `justifyAll`) aligns the paragraphs of every kind, as `text.setStyle` does later. `vertical: true` uses the existing vertical writing mode for every kind (default `false`, horizontal). Returns `{id}`, with `overflow: true` when the text doesn't fit its frame or path. |
| `apply_effect` | `{effect?, params?, ids?}` | Appends a live effect. Without `effect`, returns the effect catalogue with parameters and defaults. |
| `pathfinder` | `{operation, ids?}` | `unite`, `minusFront`, `intersect`, `exclude`, `divide`, `trim`, `merge`, `crop`, `outline`, `minusBack`. For a live version, apply the `pathfinder.*` effect to a group (`effect.apply` groups several loose selected objects first). |
@@ -1030,6 +1033,14 @@ of the format they write (one object can carry several formats' options). A comm
(`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.
An unknown command id is matched the same way as an unknown parameter key: the error names the nearest command id when
one is near, ignoring case. A step of `command.batch` or `action.batch` gets the same suggestion in its step error:
```json
{"id": "select.al"}
→ unknown command `select.al`; did you mean `select.all`?
```
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
@@ -1114,7 +1125,7 @@ naming the key, and change nothing: `radius` in `shape.rectangle`; `sides` and `
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
`text.editRange` and `text.getRange`; `at` in `text.startList`; `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
@@ -1374,6 +1385,19 @@ names must stay unique. In the desktop app the saved selections are listed at th
(`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.
## Saved views
View → New View… keeps a view under a name, in the document: `view.saved.new {name?, center: [x, y], zoom,
rotation?}` (the desktop app fills in its current view; default the first free "View N"; at most 25 per document,
names up to 255 characters, each name once) → `{name, index}`, one undo step. `view.saved.list` →
`{views: [{name, center, zoom, rotation}…]}` in menu order. Edit Views… renames and deletes:
`view.saved.edit {name, newName?, delete?}`, or several at once as `{edits: [{name, newName?, delete?}…]}` in one
undo step (→ `{views}`), where every `name` is the name before the edit and the names must stay unique; it is
enabled while the document has a saved view. In the desktop app the views are listed at the bottom of the View menu
(`view.goto1` … `view.goto25`); choosing one sets the current window's zoom, centre and rotation. A file's views are
checked when it opens: past 25, blank or repeated names, and views without a finite centre and a positive zoom 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
@@ -1849,18 +1873,44 @@ document holding only them and what they use; without `path` it returns `{dataBa
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.
## Brush libraries
Brush libraries are read-only sets of brushes: four generated in code (Art Ribbons, Calligraphic Nibs, Pattern
Borders, Scatter Shapes), the `.vectorcraft` files in the user library folder of the desktop app (User Defined) and
the brushes of any document loaded with `brush.loadLibrary` (a document without brushes of its own has the default
ones). `brush.libraries` lists them (`id`, `name`, `category`: `builtIn`, `user` or `loaded`, `count`) and
`brush.library {library}` returns one's brushes (`name`, `type`; `library` is an id or a name).
`brush.addFromLibrary {library, name? | names?}` copies brushes into the document's Brushes as one undo step (no
names: the whole library), with the resources their art uses: patterns, symbols, swatches (`swatchConflict` as for
`edit.paste`), graphic styles and images. Brushes the document already has (same name and definition) are reported
under `existing`; a name another brush has gets a number, and `names` gives each one's name in the document. The
first one becomes the current brush. `apply: true` also applies it to the paths `ids` (default: the selected ones)
in the same step, as `brush.apply` does: what clicking a library brush with paths selected, or dragging one onto a
path, does.
```json
{"name":"run_command","arguments":{"command":"brush.addFromLibrary","params":{"library":"art-ribbons","name":"Striped Ribbon","apply":true}}}
```
`brush.saveLibrary {path?, names?, name?, user?}` writes the document's brushes as a brush library: a `.vectorcraft`
document holding only them and what their art uses; without `path` it returns `{dataBase64}`, and `user: true`
saves into the user library folder (category `user`, User Defined). `brush.loadLibrary {path? | data? |
dataBase64?, name?}` loads the brushes 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}`
Each swatch, graphic style, symbol or brush 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}`,
`window.symbolLibrary {library}` and `window.brushLibrary {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:<kind>:<library id>` (`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.
`window.panel.close {panel}` closes it. `window.swatchLibrary.other {path}`, `window.graphicStyleLibrary.other
{path}`, `window.symbolLibrary.other {path}` and `window.brushLibrary.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
@@ -2014,6 +2064,20 @@ old name keeps it. A blank name gives "New Pattern N" in `object.pattern.make` a
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"patternTileEdgeColor","value":"#ff4f4f"}}}
```
Pathfinder Options: `object.pathfinder.options {precision?, removeRedundantPoints?, removeUnpainted?}` (or
`prefs.get`/`prefs.set` with key `pathfinder`) reads and sets the options every later Shape Mode and Pathfinder
operation uses: `precision` (0.001–10 pt, default 0.01; a string with a unit also reads) is how closely results follow
the paths (they are refit to within it and pieces thinner than it are dropped), `removeRedundantPoints` takes the
anchors that don't change a result's shape off it, and `removeUnpainted` makes Divide and Outline leave out the pieces
of objects with no fill (or only hidden fills). Both default to false; `{}` reads the options. Each Shape Mode and
Pathfinder command (`object.pathfinder.unite` … `minusBack`) also takes `precision`, `removeRedundantPoints` and
`removeUnpainted` for one run; the values it used join its journal entry, so a replay or an action gives the same art
whatever the options are then.
```json
{"name":"run_command","arguments":{"command":"object.pathfinder.options","params":{"precision":0.1,"removeUnpainted":true}}}
```
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:
@@ -2378,7 +2442,8 @@ These commands refuse a string or another value that is not a number with `` `dy
naming the key; the document does not change and no undo step is recorded: `t`, `dx` and `dy` in
`path.reshapeSegment` and `object.blend.spine.reshapeSegment`; `x`, `y` and `t` in `path.curvatureEdit`; `x` and `y`
in `path.convertAnchor`; `t` in `path.insertAnchor`; `fidelity` and the `within` of `redraw` in `path.freehand` (and in
`brush.freehand`, which runs it); `size` in `path.eraseRegion` and `path.blob`; `width` in `path.eraseSegments`;
`brush.freehand`, which runs it); `size`, `angle` and `roundness` in `path.eraseRegion`; `size` in `path.blob`;
`width` in `path.eraseSegments`;
`radius` and `fidelity` in `path.smoothRegion`; `tolerance` in `path.joinScrub`; `dx` and `dy` in `path.moveAnchors`;
and `dx`, `dy`, `tol` and `radius` in `path.reshape`. A null is read as a key left out. `path.convertAnchor` refuses
an `x` without a `y`, or a `y` without an `x`, with "x and y must be given together". `path.freehand`,
@@ -2772,8 +2837,9 @@ off, an empty window, and the Home button or `app.home` still shows the screen),
default: on, a click away from a panel popped out of the icon column puts it away), `displayPrintSize` (off by
default: View › Actual Size / `view.actualSize` is one document point per screen point; on, one document inch fills
96 screen points, an inch at the system's reference density whatever the display scaling). `highlightAlternateGlyphs` (off by default: on, the canvas paints a pink cell behind glyphs OpenType features
substituted — ligatures, contextual forms — on screen; Document Setup's Highlight Substituted Fonts / Glyphs stay
separate). `blackOnScreen`
substituted — ligatures, contextual forms — on screen; Document Setup's Highlight Substituted Glyphs stays
separate). `highlightSubstitutedFonts` (on by default: the canvas paints pink behind text drawn in a substitute for a
missing font, in every document). `blackOnScreen`
(Appearance of Black › On Screen, `accurate` by default) sets how the canvas shows pure CMYK black, black ink alone
at 100%: `accurate` shows it through the CMYK profile, a dark grey, and `rich` shows it as RGB black. Rich blacks,
black tints and images show the same either way, and exports are unchanged. `blackOutput` (Appearance of Black ›
@@ -2889,8 +2955,13 @@ Pen's second anchor at (150, 431), in line with the square's centre.
matched without regard to case: a word with no hyphens is never hyphenated (`typography`), and a word with hyphens
breaks only where they are (`hap-pen`). `prefs.set` pushes the list into the hyphenator and reflows open documents.
- `highlightAlternateGlyphs` (off by default): Preferences › Type › Highlight Alternate Glyphs. On, the canvas paints
pink behind OpenType substitutions (ligatures, contextual forms). Document Setup's Highlight Substituted Fonts /
Glyphs stay separate. Agents set it with `prefs.set`; the desktop canvas reads it on the next paint.
pink behind OpenType substitutions (ligatures, contextual forms). Document Setup's Highlight Substituted Glyphs
stays separate. Agents set it with `prefs.set`; the desktop canvas reads it on the next paint.
- `highlightSubstitutedFonts` (on by default): Preferences › Type › Highlight Substituted Fonts. On, the canvas paints
pink behind text drawn in a substitute for a missing font, in every open document. Off, no document shows that
highlight. `document.setup` ignores this key and adds a `warnings` entry that names `prefs.set` to its reply. A file
saved with the key in its Document Setup still opens and keeps the value it was saved with, and the preference
decides whether that file shows the highlight.
```json
{"name":"run_command","arguments":{"command":"prefs.set","params":{"values":{"typeSizeIncrement":4,"trackingIncrement":50}}}}
@@ -3154,6 +3225,13 @@ selected artboards keeps its size.
{"name":"run_command","arguments":{"command":"artboard.setProps","params":{"artboards":[{"index":0,"width":300},{"index":2,"width":300}],"scaleArt":true}}}
```
Delete Empty Artboards: `artboard.deleteEmpty {}` deletes every artboard with no artwork on it, with its guides, in
one undo step, and returns the indices it deleted (numbered as before) as `{deleted: [index…]}`. An artboard has
artwork when an object (hidden, locked and template art included; guides not) has visual bounds that meet or touch
it; a group counts by its members, a clip group or clipping layer by its members inside the clip. Locked artboards stay; when every artboard is empty and none is locked, artboard 1 stays (with a locked
artboard left, artboard 1 can go). With
nothing to delete the command is disabled and fails with `no empty artboards to delete`.
Locked artboards: `artboard.setProps {index, locked: true}` locks an artboard and `{index, locked: false}` unlocks it,
each in one undo step. The Artboards panel's lock column, left of a row's options icon, shows a lock on a locked
artboard, and a click there runs this command. A locked artboard keeps its place and size. `artboard.setProps` with a
@@ -3267,7 +3345,9 @@ images, symbol instances, live objects, and those in opacity masks, symbols and
go in an image under the untouched vector art, other effects replace the object with its image, and a path whose
fills or strokes carry effects is written as one piece per fill and stroke, so only the affected ones become images.
When an image in the art can't be decoded, `object.rasterize` and `object.cropImage` (without `trim`) make their image
without it, and their results carry the `warning` that Flatten Transparency gives.
without it, and their results carry the `warning` that Flatten Transparency gives. `object.rasterize` takes each frame
of threaded text that it rasterizes out of its thread with the text it shows, as `type.createOutlines` does (see
Threading text from the in and out ports), and the other frames keep the text they hold.
```json
{"name":"run_command","arguments":{"command":"document.rasterEffectsSettings","params":{"resolution":"high","background":"white","addAround":36}}}
@@ -3519,8 +3599,10 @@ listed in `warnings`. Binary DXF and DWG files answer an error saying to save th
`links.list {show?: all|missing|modified|embedded, sort?: name|kind|status}` lists every image in the layers, top
first, as the Links panel does: `{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth,
pixelHeight, path?, found?, page?, preview?, note?}` (`note`: the image is too large to decode and isn't drawn;
`document.open` and `file.place` say the same in their `warnings`). `links.info {id?}` is the Link Info: `image.info`'s fields plus
pixelHeight, path?, found?, page?, preview?, note?}` (`note`: the image takes more than the decode allowance, 2 GiB
or 512 MiB in the web app, as RGBA or as decoded, and it is drawn at the largest size within the allowance ("shown at
reduced size"; the document keeps the whole file) or, when it can't be read that way or has more pixels than the
allowance has bytes, drawn as a crossed gray box on the canvas and left out of exports ("too large to draw"); `document.open` and `file.place` say the same in their `warnings`). `links.info {id?}` is the Link Info: `image.info`'s fields plus
`status`, `format`, the file's `ppi` and the `effectivePpi`, `scale` (% of 100%), `rotation` (degrees,
counter-clockwise), `placement` and, for a linked file, `fileName`, `location`, `fileSize`, `created`, `modified`.
`links.goTo {id}` selects an image; `links.embed {ids?}` keeps linked files' pixels in the document (an image whose
@@ -3628,8 +3710,9 @@ fallback font…), each note once; the export still writes or returns every file
Batch exports (#1935):
- **What:** `layers: [id…]` (each layer's art alone) or `selection: "one" | "each"` instead of artboards
(`assets`, `fullDocument` as before).
- **Names:** `name` is a pattern for every file: `<filename>` (the document), `<name>` (the artboard, asset, layer
or object), `<index>`, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"`
- **Names:** `name` is a pattern for every file: `<filename>` (the document's file name without its extension, or
its title before it is saved; the Full Document file and the `.zip` use the same name), `<name>` (the artboard,
asset, layer or object), `<index>`, `####` (a counter zero-padded to the run of `#`, from `counterStart`; `counterPer: "item"`
or `"file"`), `YYYYMMDD`, `YYYY-MM-DD`, `<date>`, `<time>`, `<format>`, `<scale>` and `<suffix>` (else the row's
suffix ends the name; `date` sets the day the pattern uses). Different files it names alike get `-2`, `-3`….
- **Folders:** `folders: "none" | "rows" | "format" | "item" | "itemFormat"` (all in one, a folder per row, per
@@ -4127,6 +4210,9 @@ text at its size, through its thread too. `text.reshapeArea {id, anchors: [[subp
frame anchors with their handles (Direct Selection dragging a corner, or an edge's two ends), and
`text.areaOptions {width?, height?}` sizes the area from its top-left corner (the query returns both). Each is one
undo step. Object › Transform › Scale, the Scale tool and the Transform panel still scale the type.
`text.reshapePath {id, anchors: [[subpath, anchor]…], dx, dy}` does the same for the path of type on a path
(Direct Selection dragging one of its anchors): the start and end brackets keep their fractions of the path and the
text reflows along it.
```json
{"name":"run_command","arguments":{"command":"text.reshapeArea","params":{"id":42,"anchors":[[0,2]],"dx":40,"dy":60}}}
@@ -4215,6 +4301,13 @@ that doesn't exist, a `from` that is point type, a `to` that is an open path, `f
or already in a thread (`from` may be threaded already: the frame goes in beside it), coordinates that aren't finite
numbers and a `before` that isn't true or false.
A double-click on a port where the frame is threaded breaks the thread there (one undo step, `text.thread.break`):
`text.thread.break {id, port: "in" | "out"}` splits the thread of area type or type on a path `id` before it (`in`) or
after it (`out`). The whole story flows into the frames before the break; the frames after it are left empty, still
threaded with each other when two or more remain, and the first of them starts a paragraph. It refuses an `id` that
doesn't exist, a `port` other than `in` or `out`, and a side where no frame of a thread comes before or after `id`.
In every thread, a paragraph that continues from one frame into the next has its first line, first-line indent and
bullet or number in the frame where it starts. In the next frame its first line starts where its wrapped lines start,
and it has no bullet or number. In a frame that is no longer threaded (after `text.thread.releaseSelection` or
@@ -4233,6 +4326,9 @@ thread. When `effect.expandAppearance` keeps a frame as type in a group (type wi
effects are shadows, outer glows or Crop Marks), the type stays in the thread in the frame's place. Object › Expand
(`object.expand`) outlines type with these two commands. `object.flattenTransparency` takes each frame of threaded text
that it outlines (`textToOutlines`) or turns into flat regions or an image out of its thread in the same way.
`symbol.new` (New Symbol) and `symbol.update` (Redefine Symbol) take each frame of threaded text in the selection out
of its thread in the same way, and the symbol's art holds a copy of the frame with the text it shows. When every frame
of a thread goes into the symbol, the copy of the last frame keeps the text it has no room for.
## Converting between point type and area type
@@ -4444,10 +4540,15 @@ after Remove Threading or Release Selection, in a frame converted to point type,
duplicated, copied in the Layers panel, or taken out of a symbol or a placed file). A symbol made of a thread keeps the
numbers in the symbol's art.
The canvas, EPS, PDF and Type › Create Outlines draw the marker; in PDF it is a path, not text. Live SVG text writes the
marker's characters before the paragraph's text, except type on a path with a list, which it writes as outlines with a
warning. DXF writes the marker as a TEXT entity of its own, its characters in the order they are drawn. Legacy `.ai`
writes the paragraph's text without the marker and warns that bulleted and numbered lists are left out.
The canvas, EPS, PDF and Type › Create Outlines draw the marker. PDF writes the marker as text of its own, where the
canvas draws it, and a PDF viewer selects, copies and finds its characters. Each of its glyphs stands for the marker
character that the glyph's font maps to that glyph in its character map. Glyphs that an OpenType feature such as
oldstyle figures replaced stand for the characters left over, in order, when there are as many of those glyphs as
characters left over. A marker glyph that stands for no character, such as the missing-glyph box when no font has a
glyph for it, is drawn as a path. Live SVG text writes the marker's characters before the paragraph's text, except
type on a path with a list, which it writes as outlines with a warning. DXF writes the marker as a TEXT entity of its
own, its characters in the order they are drawn. Legacy `.ai` writes the paragraph's text without the marker and warns
that bulleted and numbered lists are left out.
Type › Bullets and Numbering › Apply Bullets and the canvas context menu's Bullets and Numbering › Apply Bullets run
`type.applyBullets {bullet?}` on the selected type's paragraphs, or on those the Type tool's selection touches. Without
@@ -4455,9 +4556,50 @@ Type › Bullets and Numbering › Apply Bullets and the canvas context menu's B
same menus runs `type.applyNumbers {number?}`. Without `number` it runs `text.setFormat {list: "number"}`, and with it
it runs `text.setFormat {number}`.
Type › Bullets and Numbering › Convert to Text runs `type.convertListToText` on the selected type's paragraphs, or on
those the Type tool's selection touches, and that runs `text.convertListToText {ids?, start?, end?}`. The marker of
each list paragraph becomes characters at the start of the paragraph: the bullet, or the number and its period as the
paragraph showed it, then a tab. They are set in the marker's font, size and color: the style of the paragraph's first
character without the attributes the marker leaves out. The paragraph leaves its list. When the marker's tab went to
the left indent, the paragraph gets a left-aligned tab stop without a leader there, in place of any stop there, so that
the text after the tab starts where it started after the marker. When the marker's tab went to a tab stop, the
converted tab goes to the same place when that stop is left-aligned and no stop there or past it has a leader. When a
stop there or past it has a leader or is not left-aligned, or a right, center or decimal stop lies between the marker
and the place its tab ended, the paragraph gets a left-aligned stop without a leader where the marker's tab ended, in
place of any stop there and of those right, center and decimal stops before it. A paragraph that holds a tab of its
own keeps its stops past the left indent as they are. The characters are text like any other: they can be selected,
styled, edited and deleted, and copied text includes them. In threaded type the numbers are the ones the story counted
through its frames, and a paragraph that continues into the next frame gets its characters once, where its text
starts. An empty list paragraph has no marker and only leaves the list. A numbered paragraph after the converted ones
that stays in its list counts from 1 again, as after any paragraph in no list. A paragraph with no direction set runs
the way its first strong character does; when a letter or a roman numeral put before right-to-left text would make it
run left to right, the paragraph gets the Right to Left direction. A right-to-left paragraph that leaves its list no
longer has its indents mirrored, and on a path the tab takes its own width, as every tab on a path does. With the
Every-line Composer, a paragraph that holds a tab is broken line by line, so a converted paragraph of several lines
can break at other places than its list paragraph did. The change is one undo step, and the reply gives the `ids` of
the type it changed, how many paragraphs were `converted`, and the characters `inserted`, each with the type object's
`id`, the byte `at` which they went in (counted in the text before the change) and their `length` in bytes. While the
Type tool edits, `text.convertListToText` ends its typing first and moves its anchor and caret past the characters
inserted at or before them, whether the call comes from the menu, MCP or the control channel.
In threaded type, when the caret's character moved to another frame, the Type tool edits that frame with the caret
next to the same character, and that frame becomes the selection when it was not selected.
Typing starts a list too. When the Type tool types a space on its own (one key press, or `type_text {text: " "}`) right
after `1.` or `1)`, `A.` or `A)`, or one of `*`, `#`, `>` and `+` at the start of a paragraph that is in no list, the
typed characters and the space are removed and the paragraph becomes a numbered list in 1, 2, 3 or A, B, C, or a
bulleted list with the filled dot. A list started with `1)` or `A)` also writes its numbers with a period. A paragraph
whose left and first-line indents are both 0 gets 18 pt and -18 pt. The tool runs `text.startList {id, at}`, with `at`
the byte after the space, as an undo step of its own after the typing: one Undo puts the characters back as text with no
list, and typing goes on after them; Redo removes them again, and typing goes on before the paragraph's text. The same
characters later in a paragraph start nothing, and so do list characters that arrive in one piece with the space: a
paste, an IME commit, or `type_text` of a longer string. From a script, `text.startList {id, at?}` (`at` defaults to the end of the text) does
the same with the text before `at` and returns `started` and `caret`. When the list starts, `caret` is the byte where
the paragraph's text now starts; when the text before `at` starts no list, nothing changes, `started` is false and
`caret` is `at`.
```json
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"bullet":"square"}}}
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"number":"upperRoman"}}}
{"name":"run_command","arguments":{"command":"text.convertListToText","params":{}}}
```
## Small Caps, Superscript and Subscript
@@ -4655,6 +4797,25 @@ to `object.setBounds`, which scales the other dimension by the same factor; agen
{"name":"run_command","arguments":{"command":"object.setBounds","params":{"width":200,"proportional":true}}}
```
## Transform panel menu
The Transform panel menu's Transform Object Only, Transform Pattern Only and Transform Both are the
`transformPanelPatterns` preference (`object`, `pattern`, `both`). Once one is chosen, the fields, flips and menu
flips of the Transform panel and the Properties panel's Transform section send it as `objects` (false for `pattern`)
and `patterns` (true for `pattern` and `both`) to `object.setBounds`, `object.rotate`, `object.shear` and
`object.reflect`; agents pass those switches themselves. Until then (`default`, also after `prefs.reset`) they send
neither, so pattern tiles follow `transformPatternTiles`, and the menu checks Transform Both while it is on and
Transform Object Only while it is off. Use Registration Point for Symbol is the
`useRegistrationPointForSymbol` preference: with it on and one symbol instance selected, those panels show and set X
and Y at the symbol's registration point (`symbol.options` `registration`) where the instance's transform puts it
(after a flip the art's top-left corner is the box's top-right one), give `reference` as the box's point nearest it
and turn, shear and flip about it (`origin`).
```json
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"transformPanelPatterns","value":"pattern"}}}
{"name":"run_command","arguments":{"command":"object.setBounds","params":{"x":170,"reference":4,"objects":false,"patterns":true}}}
```
## Plug-ins
WebAssembly plug-ins ([plugins.md](plugins.md)) are reached through commands; the desktop app and an unconfined
+4 -4
View File
@@ -1,7 +1,7 @@
# PDF backend
PDF writing uses `craft-pdf` from `storytold/craft-libs`, pinned to
`2eca9b3a53a98ac52efed2a27353c6a6927ead10`. Its `write` feature supplies Krilla configuration,
`a0916808f7d78bed8c7b90cdf937176015454a7f`. Its `write` feature supplies Krilla configuration,
metadata construction, fallible font preparation and serialization, and native unwind guards.
The dependency disables default features and enables `write` and `interpret`.
The production writer retains Krilla’s raster-image support without its optional text shaper.
@@ -51,10 +51,10 @@ warnings also become errors before a document is returned. Whitespace and commen
EOF remain valid. The typed iterator does not classify every malformed operator, so this
is known-incomplete rejection rather than a general PDF validity certificate.
The three patched Hayro crates and Krilla are selected in the root `[patch.crates-io]`
The five patched Hayro crates and Krilla are selected in the root `[patch.crates-io]`
from the same immutable revision as `craft-pdf`. `cargo xtask pdf-deps` rejects missing,
duplicate, local-path, registry and mismatched revisions for those five packages.
`craft-print` and `craft-ui` keep their existing independent revisions.
duplicate, local-path, registry and mismatched revisions for those seven packages.
`craft-print`, `craft-ui` and the other craft-libs crates use the same revision.
Dependency asset hashes and sources are in `ASSETS.md`; redistributed license texts are
in `licenses/pdf-backend`. Package staging carries those texts together with `NOTICE`
+5
View File
@@ -109,6 +109,11 @@ wasmi is built with its *portable* (loop) dispatcher: the default tail-call disp
sibling-call optimisation, and when that doesn't happen a long-running module overflows the host stack, which aborts
the process instead of returning an error.
wasmi compiles each function when it is first called, and the instruction budget doesn't pay for that, so a run
takes as many instructions the first time as the next. Charged to the run, as wasmi charges by default, compiling
could fail it: when the 20 M-instruction slice the run was in ran out while a function compiled, wasmi ended the run
with an error rather than a pause the host resumes, and the run stopped with most of its budget left.
Drawing evaluates effects often, so live-effect results are cached by plug-in installation, parameters and input
geometry: a plug-in runs again only when one of them changes.
+5 -3
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long
+30 -12
View File
File diff suppressed because one or more lines are too long