mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 23:32:14 +00:00
Merge main into fix/batch-override-export-for-screens-folder
This commit is contained in:
@@ -48,7 +48,7 @@ effects are rendered offscreen per object and cached while panning. See
|
||||
## Files
|
||||
|
||||
The native `.vectorcraft` format is versioned, compressed JSON with embedded images, saved atomically, readable
|
||||
down to v1 and from the old `.drawcraft` name. The `FORMATS` table in `crates/engine/src/cmd/fileio/mod.rs`
|
||||
down to v1 and from the old `.drawcraft` name. The `FORMATS` table in `crates/engine/src/cmd/fileio/formats.rs`
|
||||
lists every format read or written; importers are fuzzed (`crates/engine/tests/import_fuzz.rs`). See
|
||||
[file-format-parity.md](file-format-parity.md).
|
||||
|
||||
|
||||
+152
-83
@@ -1,6 +1,6 @@
|
||||
# Control protocol
|
||||
|
||||
> **Last updated:** 2026-10-13 · **Change:** 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
|
||||
@@ -51,7 +51,8 @@ A request the app hasn't answered after **60 s** gets `{"ok": false, "error": "t
|
||||
app had not started it (it was busy, or its window wasn't being drawn), the request is withdrawn
|
||||
and never runs, and the error says "will not run": retrying is safe. If the app was already
|
||||
running it, the error says "still running": it may yet complete, so check the document before
|
||||
retrying.
|
||||
retrying. On macOS the app keeps itself out of App Nap while the control channel is on, so it keeps
|
||||
answering with its window hidden, minimized or covered, or with the screen locked.
|
||||
|
||||
## Confining file access
|
||||
|
||||
@@ -62,8 +63,8 @@ vectorcraft --control 7979 --automation-read-root /work/project --automation-wri
|
||||
`--automation-read-root <dir>` and `--automation-write-root <dir>` (or `VECTORCRAFT_AUTOMATION_READ_ROOT` /
|
||||
`VECTORCRAFT_AUTOMATION_WRITE_ROOT`; `--flag=<dir>` works too) confine what the control channel reads and
|
||||
writes, with the same flags as `vectorcraft-cli mcp` and the other Craft apps. Every path a request reads
|
||||
(open, place, relink, library and profile loads, plug-ins…) must lie inside the read root, every path it
|
||||
writes (save, export, package, captures, library saves…) inside the write root; anything else is an
|
||||
(open, place, relink, library and profile loads, plug-ins…) must lie inside a read root, every path it
|
||||
writes (save, export, package, captures, library saves…) inside a write root; anything else is an
|
||||
ordinary error reply. Paths are made absolute against the app's working directory, links are followed
|
||||
(for a file that doesn't exist yet, its folder's), and the comparison is by whole folder names (`/work2` is
|
||||
not inside `/work`), ignoring case on Windows; `..` below a folder that doesn't exist, a link that leads
|
||||
@@ -71,9 +72,9 @@ nowhere and Windows device names are refused. A root left out grants none of its
|
||||
existing folders, or the app doesn't start; without `--control` they are ignored. See `docs/mcp.md`
|
||||
› Confining file access for the details.
|
||||
|
||||
Each root flag may be given once; a command-line root overrides its environment default. Missing or
|
||||
empty values, another option used as a value, and duplicate flags stop the desktop before it starts
|
||||
any services, even without `--control`. For a folder name starting with `-`, use `--flag=-folder`
|
||||
Each root flag may be given more than once, a path inside any of its folders being allowed (#3546); the
|
||||
command line's folders of a kind replace its environment default. Missing or empty values and another
|
||||
option used as a value stop the desktop before it starts any services, even without `--control`. For a folder name starting with `-`, use `--flag=-folder`
|
||||
or the relative path `./-folder`.
|
||||
|
||||
What is confined: the requests themselves, the frames that carry input they inject (`ui.click`, `ui.key`,
|
||||
@@ -90,38 +91,39 @@ the two can still win that race.
|
||||
|---|---|---|
|
||||
| `engine.execute` | `{command, params}` | run any engine or UI command (see `engine.commands`). A key of `params` that the command's params description doesn't name is an error naming it and the nearest key the command takes, before anything runs (`unknown parameter `widht`; did you mean `width`?`); see [Parameter keys](mcp.md#parameter-keys) |
|
||||
| `engine.commands` | | every command with label, shortcut, params doc, enablement |
|
||||
| `document.inspect` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | layer tree, selection, the key object (`key`, null without one), history, paint defaults; `depth` / `childLimit` slice the layer tree as `document.node {summary: true}` does; `ids`, `kind`, `limit` and `offset` list just some nodes as `nodes` (with `total`) instead of the whole tree, see [mcp.md](mcp.md). Each node reports `opacity` below 1 and `blend` when it isn't Normal; objects and compound paths report `fill`, `stroke` and `strokeWidth`, a compound path's members `compoundMember: true` instead |
|
||||
| `document.inspect` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | layer tree, selection, the key object (`key`, null without one; a key ruler guide as `keyGuide`), history, paint defaults; `depth` / `childLimit` slice the layer tree as `document.node {summary: true}` does; `ids`, `kind`, `limit` and `offset` list just some nodes as `nodes` (with `total`) instead of the whole tree, see [mcp.md](mcp.md). Each node reports `opacity` below 1 and `blend` when it isn't Normal; objects and compound paths report `fill`, `stroke` and `strokeWidth`, a compound path's members `compoundMember: true` instead |
|
||||
| `ui.inspect` | | tool, UI state, `screenMode` (0 normal, 1 full screen with menu bar, 2 full screen, 3 Presentation Mode), `taskBar` (`pinned`, and `rect` `[x, y, w, h]` while the Contextual Task Bar shows), `freeTransformWidget` (`[x, y, w, h]` while the Free Transform widget shows), view, canvas rect, window size, perf, background saves and exports still running, `nativeMenuBar` (true when the menus are in the macOS menu bar rather than the window; `ui.menu.list` lists the in-window menus either way), `fileDialog` (`"open"`, `"place"`, `"save"` or `"folder"` while a native file dialog is open, `null` otherwise; see below) |
|
||||
| `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item |
|
||||
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected. While a menu opened over locked objects is open, it has an `Unlock` submenu with one item for each locked object under the pointer, topmost first and at most 20, labeled with the object's name, which runs `layer.setProps {ids: [id], locked: false}`. A locked group gets one item, and the locked objects inside it get none. Hidden objects, objects on hidden, locked or template layers, and in isolation mode objects outside the isolated group get no item |
|
||||
| `ui.tool.select` / `ui.tool.list` | `{tool}` | |
|
||||
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool` |
|
||||
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse: `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool`. With the Shape Builder or the Live Paint Bucket and a selection of more than 128 segments, the app finds the regions off the UI thread, so a `move` replies before the region under the pointer is highlighted, and the highlight shows in the first frame after the regions are found |
|
||||
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
|
||||
| `ui.move` | `{x, y}` | screen-point pointer move |
|
||||
| `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?}` | |
|
||||
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog |
|
||||
| `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
|
||||
| `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |
|
||||
| `ui.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}` 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 |
|
||||
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
|
||||
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it; when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`, `ai`; for a Template, `template` and `ait`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. A name typed in the panel without the file type's extension gets it, and a name the panel returns with the extension of a format the panel does not list has it replaced (in a native save, `card.png` and `card.ait` are saved as `card.vectorcraft`, and `notes.v2`, whose extension names no format, is saved as `notes.v2.vectorcraft`); when a file of that name exists, which the panel did not ask about, the save (and every export or other write through a save panel) answers `cancelled` and opens a `replaceFile` dialog (fields `message`, `detail`, `path`): `ui.dialog.confirm` writes that file, `ui.dialog.cancel` writes nothing. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
|
||||
| SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`, `profile` (`svg11`/`tiny12`), `encoding` (`utf8`/`utf16`/`latin1`), `embedFonts`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centred in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). With a file, it runs `file.place` centred in the view. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centered in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). In the desktop app the dialog reads its files off the UI thread: `__info` is empty until they are read, `ui.dialog.confirm` returns `{background: true, status}`, `ui.inspect` lists the files still being read in `placing`, and the file is placed (or the cursor loaded) once it is read. With a file, it runs `file.place` centered in the view, at once. Files dropped on the window with no document open open. On the web, files dropped on the canvas are placed at the drop point (linked; Shift embeds) and files dropped off it (the tab bar) open, as in Illustrator; desktop drops carry no pointer position, so documents (native, SVG, PDF, `.ai`, EPS, DXF, EMF/WMF) open as tabs of their own and pictures and text are placed in the middle of the view. Swatch libraries and color books (`.vcswatches`, `.gpl`, `.ase`, `.acb`), graphic style libraries (`.vcstyles`), Libraries panel files (`.vclibrary`), presets (`.vcflattener`, `.vcpdfpresets`, `.vcprintpresets`, `.vcperspective`), plug-ins (`.wasm`) and shortcut sets (`.kys`, in Import Shortcut Set) open wherever they are dropped, as File › Open opens them. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files (in the desktop app each file is read off the UI thread and placed once it is read, and `ui.inspect` lists it in `placing` until then) and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
|
||||
| `app.export` JPEG options | `{path?, format: jpg, quality?: 0–100, colorModel?: rgb \| cmyk \| gray, method?: baseline \| optimized \| progressive, scans?: 3–5, embedIcc?, imageMap?: none \| client \| server}` | `cmyk` writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); `embedIcc` (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; `imageMap` also writes `<stem>.html` (client-side `<map>`) or `<stem>.map` (NCSA) beside the image for the objects with a URL and an Image Map shape (`attributes.set`), returned in `linked`. The `jpgOptions` dialog has these fields too (`quality` is shown 0–10; a CMYK document defaults to `cmyk`) |
|
||||
| `app.export` PNG-8 and GIF | `{path?, format: png8 \| gif, colors?: 2–256, reduction?: perceptual \| selective \| adaptive \| web \| blackWhite \| gray, dither?: none \| diffusion \| pattern \| noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options}` | a palette of at most `colors` entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless `transparency` is false, partly transparent edges are blended over `matte` (`none` keeps their colour). `png8` writes an indexed `.png` (1–8 bits per pixel); Export for Screens rows take both. The `png8Options` / `gifOptions` dialogs have these fields |
|
||||
| `app.export` Text and WebP | `{path?, format: txt, encoding?: utf8 \| utf16, lineEndings?: lf \| crlf, selectionOnly?}` / `{format: webp, lossless?, quality?}` | Text writes the stories in stacking order (back to front; a thread once; hidden objects and template layers left out), each line ended by `lineEndings`; `utf16` is little-endian with a byte order mark. Export As → Text opens `txtOptions` (fields `path`, `encoding`, `lineEndings`, `selectionOnly`; OK writes the file). WebP is always lossless: `lossless: false` adds a warning |
|
||||
| `engine.execute` `document.exportForOffice` | `{}` / `{path?, ppi?: 72 \| 150 \| 300, transparent?, artboard?}` | File → Save for Office Documents…: without params opens `saveForOffice` (fields `ppi`, `transparent`, `artboard`; OK picks the file); with params writes the artboard as a PNG (`path`, else a picked file) → `{path}`, and the status bar shows the first of the export's `warnings` as Save As shows a save's notes. Headless it returns `{dataBase64, width, height, warnings}` |
|
||||
| Native file dialogs | any command with no `path` that needs one (`file.open`, `file.saveAs`, `file.place`, exports…) | on macOS (a sheet on the window) and Linux the dialog doesn't hold the window: the call returns `"cancelled"` at once, `ui.inspect` reports `fileDialog`, and when the person at the computer picks a file the command runs again on the document it was asked for. While it is open another dialog is refused: that call returns `"cancelled"` too, and the status bar (`ui.inspect` → `ui.status`) says `"A file dialog is open: choose there first"`; on macOS `app.quit`, `file.close` and `file.closeAll` return that message and the menu bar does nothing (#592, #867). On Windows the dialog is shown in line: the call returns once it closes. Pass `path` to skip the dialog |
|
||||
| Background Save / Export | `app.save`, `app.export {path}`, `file.save`, `file.saveAs`, `file.saveCopy` | with the `backgroundSave` / `backgroundExport` preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns `{path, background: true, status}` at once, `ui.inspect` lists the jobs still running in `background` (labels such as `"Saving a.vectorcraft"`), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. With the preferences off (`prefs.set`), saves return once written |
|
||||
| Background Save / Export | `app.save`, `app.export {path}`, `file.save`, `file.saveAs`, `file.saveCopy`, `file.exportForScreens {folder, …}` | with the `backgroundSave` / `backgroundExport` preferences on (the default) the desktop app writes the file from a snapshot on a worker thread: the call returns `{path, background: true, status}` at once (`file.exportForScreens`: `{background: true, status}`, and the files are written into the folder from a snapshot taken at the call), `ui.inspect` lists the jobs still running in `background` (labels such as `"Saving a.vectorcraft"`), and the document counts as saved (as of the snapshot) once its file is written. Quit and Close wait for them. A job that fails reports the error in the status bar only (`ui.inspect` → `ui.status`), a path outside `--automation-write-root` included. With the preferences off (`prefs.set`), saves return once written. `app.save {path?, wait: true}` and `app.export {path, wait: true}` wait for the jobs already running, write the file before they reply whatever the preferences are set to, and return its result or the error. 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 preferences. A `wait` that is not `true`, `false` or `null` is refused with "`wait` must be true or false", and nothing is written |
|
||||
| `engine.execute` `file.exportForScreens` | `{}` / `{…document.exportForScreens params}` | without params opens `exportForScreens` on the settings the document last exported with (`document.exportSettings`). While the Artboard tool is in use, the artboards it has selected (its `selected` option, the rows the Artboards panel highlights) are checked instead, and `select` is `range`, or `all` when every artboard is selected. Fields: `tab` (`artboards` \| `assets` \| `layers` \| `selection`; OK reads Export Layer or Export Selection on those), `layers` ([layer id…] checked), `selectionMode` (`one` \| `each`), `name` (the file name pattern of `document.exportForScreens`), `counterStart`, `folders` (`none` \| `rows` \| `format` \| `item` \| `itemFormat`), `exportPreset` (the preset applied last), `__savePreset` (the name typed to save one), `select` (`all` \| `range` \| `full`), `range` ("1-3, 5"), `boards` (a bool per artboard; checking them sets `select` and `range`), `includeBleed`, `folder` (desktop; default the Desktop, else home), `openLocation`, `subfolders`, `prefix`, `preset` (`""` \| `mobile` \| `density`), `formats` (rows `{format, scale: "2x" \| "100w" \| "100h" \| "72ppi", suffix, quality?, folder?}`), `settings` (`{png \| png8 \| jpg \| webp \| gif \| svg \| pdf: {…options}}`, the gear's Format Settings; `__settings` names the format shown). `ui.dialog.confirm` exports (a bad range or no folder keeps it open); on the web OK reads "Download" and the files download (several as one `.zip`). With params it runs `document.exportForScreens`: no `folder` returns the files (or with `zip: true` one store-only zip) as `dataBase64`; with `openLocation` the first file is shown in the file manager. When `warnings` is not empty, an export into a folder or a download shows the first in the status bar as Save As shows a save's notes |
|
||||
| Export for Screens' Assets tab, Asset Export panel | `ui.dialog.set` `tab: "assets"`, `assets: [asset id…]`; `window.panel {panel: "assetExport"}` | on the Assets tab of `exportForScreens` the `assets` field lists the checked asset ids (all of them when it opens); `ui.dialog.confirm` runs `document.exportForScreens {assets}` (nothing checked keeps it open) and OK reads "Export Asset". The Asset Export panel acts through `assets.*` and shares the format rows with the dialog (`assets.settings.set`) |
|
||||
| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as assets (`assets.add`; one per object unless `multiple` is false) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog |
|
||||
| `engine.execute` `file.exportSelection` | `{multiple?}` | File › Export Selection…: collects the selected objects as one asset (`assets.add`; one per object with `multiple: true`) and opens `exportForScreens` on its Assets tab with just them checked → `{assets}`. `document.exportSelection` still exports the selection in one go, without the dialog |
|
||||
| CSS Properties panel | `window.panel {panel: "cssProperties"}`; `engine.execute` `css.copy` / `css.exportFile` | the panel shows `css.selection` of the selection (after Generate CSS, while nothing is selected, `css.generate`); its menu holds the options the commands take (`units`, `position`, `dimensions`, `unnamed`, `rasterize`). `css.copy {scope?, …options}` copies the CSS to the clipboard → `{css, rules}`; `css.exportFile {path?, scope?, …options}` writes `css.export` to `path` (else a picked file; the web downloads it) with the pictures of rasterized art next to it → `{path, rules, images, warnings}`, and the status bar shows the first of `warnings` as Export As shows an export's notes |
|
||||
|
||||
| `ui.dialog.*` on `importPdf` | `{field: "page" \| "allPages" \| "range" \| "cropTo" \| "password", value}` | opening (`app.open`, `file.open`) a PDF or `.ai` with several pages or a password, or placing one (`file.place` without `page`), opens the Import PDF dialog (`mode`: `open` or `place`; `pageCount`). While `__locked` is true, set `password` and `ui.dialog.confirm` to unlock it (a one-page file then opens or is placed). Then set `allPages: false` and `range` ("2-3, 5") to open some pages, or `page` to place one, and `cropTo` (`bounding`, `art`, `crop`, `trim`, `bleed`, `media`); `ui.dialog.confirm` opens or places them (placing keeps the other `file.place` params, such as `link`, `template` and `at`, in `__place`). `document.open {pages, cropTo, password}` and `file.place {page, crop, password}` do the same without the dialog |
|
||||
@@ -155,7 +157,8 @@ dialog open); without `name` it names a new style made from the selection (`grap
|
||||
(`graphicStyle.merge`). Deleting styles from the panel asks first with a `confirm` dialog (`graphicStyle.delete`).
|
||||
|
||||
Symbols: `ui.symbolOptions` opens the `symbolOptions` dialog (field `name`) that the Symbols panel's New Symbol asks
|
||||
first; `ui.dialog.confirm` makes the symbol from the selection with that name (`symbol.new`).
|
||||
first; `ui.dialog.confirm` makes the symbol from the selection with that name (`symbol.new`). F8 runs
|
||||
`ui.symbolOptions` without params.
|
||||
|
||||
Gradient panel: double-clicking a stop on the panel's slider opens the same `gradientStop` popover, with a `screen`
|
||||
field (`[x, y]`, screen points) in place of `x`/`y`. The panel's stop eyedropper selects the Eyedropper tool with the
|
||||
@@ -181,8 +184,13 @@ Transform (dialog kind = the tool id), or fails with `nothing selected`. For `se
|
||||
drag or Move (`dx`, `dy`) and with the `copy`, `objects` and `preview` it was last confirmed with (its `patterns` is
|
||||
the `transformPatternTiles` preference, which its OK sets). `ui.key` Enter with one of those seven
|
||||
tools opens its dialog too (nothing happens without a selection), with the transform tools' `origin` at their reference
|
||||
point. Gradient tool handles snap to anchors, edges and smart guides; Shift constrains them to 45° steps from the
|
||||
`constrainAngle` preference.
|
||||
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). 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
|
||||
(fields: the effect's parameters, `preview`). With `index` it edits that applied effect of `item` (null: the object's
|
||||
@@ -198,7 +206,9 @@ Plug-in dialogs: `engine.execute {command: "plugin.dialog", params: {id}}` (Obje
|
||||
plug-in at once when it takes no parameters, else opens the `plugin` dialog: one field per declared parameter (named
|
||||
after it), `__plugin` (the plug-in id) and `preview`. It previews `plugin.run` on the canvas while open;
|
||||
`ui.dialog.confirm` keeps the result as one undo step and `ui.dialog.cancel` rolls it back. Live-effect plug-ins open
|
||||
the `effect` dialog (`effect.dialog {effect: "plugin.<id>"}`). `ui.installPlugin {path?}` installs a `.wasm` (without
|
||||
the `effect` dialog (`effect.dialog {effect: "plugin.<id>"}`). An exporter plug-in with parameters opens the
|
||||
`pluginExport` dialog once Export As has picked the file: one field per parameter, `__plugin`, and `__export` (the
|
||||
export's own parameters); `ui.dialog.confirm` writes the file. `ui.installPlugin {path?}` installs a `.wasm` (without
|
||||
a path it asks for one); see [plugins.md](plugins.md).
|
||||
|
||||
Overprint Black: the Edit → Edit Colors → Overprint Black… menu item opens a `command` parameter dialog for
|
||||
@@ -268,6 +278,28 @@ Defined libraries are `window.userGraphicStyleLibrary1`…`10` in Window → Gra
|
||||
user library folder, `selectedOnly` with `names`); `ui.dialog.confirm` runs `graphicStyle.saveLibrary` (to a file it
|
||||
asks for a path and suggests `name` with `.vcstyles` as the file name; the web downloads the file under that name).
|
||||
|
||||
Symbol libraries open in the same panels: `window.symbolLibrary {library}` (`library_panels` kind `symbols`; `library:
|
||||
null` closes them). Clicking a symbol there runs `symbol.addFromLibrary {library, name}`, adding it to the document's
|
||||
Symbols in one undo step; Shift/Cmd-clicks select symbols for Add to Symbols, and a symbol dragged onto the canvas runs
|
||||
it with `place: true` and the drop point as `x`, `y`. `window.symbolLibrary.other {path?}` loads the symbols of any
|
||||
document VectorCraft opens (a saved symbol library is a `.vectorcraft` document) and opens them there. The User
|
||||
Defined libraries are `window.userSymbolLibrary1`…`10` in Window → Symbol Libraries. `ui.saveSymbolLibrary {names?}`
|
||||
(Save Symbols…, the Symbols panel's Save Symbol Library…) opens the `saveSymbolLibrary` dialog (fields `name`, `user`:
|
||||
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.
|
||||
@@ -292,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
|
||||
@@ -343,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
|
||||
@@ -410,6 +420,8 @@ one undo step.
|
||||
Attributes panel: `window.panel {panel: "attributes"}` (Cmd+F11) shows it; its controls run `attributes.set`,
|
||||
`path.reverse {reversed}` and `path.setFillRule {rule}`, and its Browser button runs the UI command
|
||||
`attributes.openUrl {url?}`, which opens the URL (default: the selection's) in the web browser and answers `{url}`.
|
||||
It opens `http:`, `https:` and `mailto:` URLs only, and answers a control request with an error: it hands the URL to
|
||||
another app.
|
||||
|
||||
SVG Interactivity panel: `window.panel {panel: "svgInteractivity"}` shows it; its controls run
|
||||
`svgInteractivity.setEvent`, `svgInteractivity.removeEvent` and `svgInteractivity.clearEvents`, and its Link JavaScript
|
||||
@@ -428,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
|
||||
@@ -465,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
|
||||
@@ -575,7 +590,7 @@ System clipboard formats: through the desktop app, `edit.copy` and `edit.cut` pu
|
||||
lists to the system clipboard (once they are written, the status bar shows the first of the PNG's and PDF's
|
||||
`warnings`, such as an image that couldn't be decoded), and every Paste command first loads what another app copied (`clipboard.importSvg`,
|
||||
`importPdf`, `importEps`, `importText` or `importImage`, centred in the view; the desktop app reads PDF and EPS from
|
||||
the Windows clipboard and, with SVG, from the macOS pasteboard, and an `.eps` file, or a `.ai` or `.ait` file that holds a PDF or PostScript, copied in a file manager anywhere); an import error answers the Paste with that error
|
||||
the Windows clipboard and, with SVG, from the macOS pasteboard, and an `.eps` file, or a `.ai` or `.ait` file that holds a PDF or PostScript, copied in a file manager anywhere; a copied `.ai` or `.ait` file that holds a PDF loads with `importPdf {editingData: true}`, which reads its editing data); an import error answers the Paste with that error
|
||||
and pastes nothing. Cmd+V also pastes a bitmap or PDF with no text beside it (on the key's release, as egui sends no
|
||||
Paste event for it). Screenshots paste on every platform: Windows reads `PNG`, else `CF_DIBV5`/`CF_DIB`, macOS the
|
||||
pasteboard's PNG or TIFF, Linux `image/png`, and the web the picture the browser's paste event carries (#457).
|
||||
@@ -610,7 +625,8 @@ Placement Options: the Links panel's flyout (or `ui.placementOptionsDialog {ids?
|
||||
`fit`/`fill`, `align`: `topLeft` … `bottomRight`, `clip`). `ui.dialog.confirm` runs `links.placementOptions` with them as
|
||||
one undo step. The Links panel (`window.panel {panel: "links"}`) shows `links.list` and the first selected image's
|
||||
`links.info`; `links.editOriginal` and `links.reveal` (Edit › Edit Original, Show in Folder) hand the linked file to
|
||||
the system and return `{path}`.
|
||||
the system and return `{path}`. Edit Original opens only a regular file whose extension is a format `document.open`
|
||||
reads, and answers a control request with an error unless the file is a placed document (which opens in a tab).
|
||||
|
||||
Package: File → Package… (`ui.packageDialog`) opens the `package` dialog for a saved document (a document never saved
|
||||
gets a `confirm` dialog whose OK runs `file.saveAs`). Fields: `folder` (the location), `name` (the package folder),
|
||||
@@ -636,7 +652,9 @@ EPS Options: Export As… with the EPS format (`file.exportAs {format: "eps"}`,
|
||||
(`preserve`, `discard`), `flattenerPreset` (a built-in or saved preset's name), `includeLinkedFiles`, `thumbnails`,
|
||||
`cmykPostScript`, `compatibleGradients`, `level` (`2`, `3`), plus `path` and the artboard choice (`useArtboards`,
|
||||
`range`). `ui.dialog.confirm` checks them (a bad value or an unknown preset keeps the dialog open), remembers them for
|
||||
the next EPS Options and writes the file(s), asking for a path when there is none.
|
||||
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. 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,
|
||||
@@ -785,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
|
||||
@@ -836,34 +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.
|
||||
| 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
|
||||
|
||||
@@ -877,9 +920,35 @@ split paths return an error without changing the workspace.
|
||||
- [x] Release connection slots on worker exit, including timeout and failed worker creation.
|
||||
- [x] Preserve correlated replies and persistent connections within the transport budgets.
|
||||
|
||||
## In the browser: `window.vectorcraft`
|
||||
|
||||
The web build has no TCP server (a page can't listen on a socket), so it offers the same methods to the
|
||||
page itself, as promises on `window.vectorcraft` (`apps/vectorcraft-web/src/api.rs`). An agent driving VectorCraft in a
|
||||
browser tab, or a host app showing it in an `<iframe>`, uses the commands it would send to the desktop
|
||||
app:
|
||||
|
||||
| Call | |
|
||||
|---|---|
|
||||
| `vectorcraft.request(method, params)` | any control method of this document → its `result` |
|
||||
| `vectorcraft.execute(command, params)` | `engine.execute` |
|
||||
| `vectorcraft.commands()` / `vectorcraft.inspect()` | `engine.commands` / the inspect method |
|
||||
| `vectorcraft.screenshot(params)` | `ui.screenshot` (no `path`: the PNG comes back inline) |
|
||||
| `vectorcraft.openFile(name, bytes)` | opens the bytes the way a dropped file opens (returns `true` once queued) |
|
||||
| `vectorcraft.captureDownloads(on)` | while on, a save or export is kept in the page instead of downloaded |
|
||||
| `vectorcraft.files()` / `vectorcraft.readFile(name)` | the kept files (`{path, size}`) / one of them as a `Uint8Array` |
|
||||
|
||||
Errors reject with an `Error` carrying the method's message. Requests run on the UI's next frame, so a
|
||||
hidden tab (whose frames the browser pauses) answers when it is shown again.
|
||||
|
||||
## Revision history
|
||||
|
||||
| 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). |
|
||||
| 2026-10-18 | minor | Documented `window.vectorcraft`, the web build's control channel. |
|
||||
|
||||
+157
-21
@@ -1,5 +1,9 @@
|
||||
# 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).
|
||||
> **Last updated:** 2026-10-14 · **Change:** leaving out the bundled fonts (`bundled-fonts` feature).
|
||||
> **Last updated:** 2026-10-14 · **Change:** document-font registration for all explicitly supplied faces.
|
||||
|
||||
@@ -32,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).
|
||||
@@ -104,13 +109,16 @@ On macOS the menus live in the system menu bar, not in the window's app bar (whi
|
||||
|
||||
## Linux: Wayland and X11
|
||||
|
||||
On GNOME/Wayland, enable **Preferences › User Interface › System Title Bar** and restart to use winit's font-free Adwaita window controls (`wayland-csd-adwaita-notitle`); the existing custom title bar remains the Linux default.
|
||||
|
||||
The window runs natively on Wayland (eframe's `wayland` feature) and on X11. On machines with two GPUs it renders on the one the desktop runs on unless Preferences say otherwise; see [Desktop graphics processor](#desktop-graphics-processor) (#502). The system clipboard (`apps/vectorcraft/src/clipboard.rs`) is arboard with its `wayland-data-control` feature:
|
||||
|
||||
- **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 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.
|
||||
- **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, without pressure: winit 0.30 reads pen pressure on Windows only (Windows Ink, which reaches the Liquify tools' Use Pressure Pen). Other compositors that don't emulate a pointer for tablets behave the same way.
|
||||
- **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.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.
|
||||
|
||||
## Fonts: craft-fonts (optional build input)
|
||||
|
||||
@@ -142,12 +150,15 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese-
|
||||
so they are the fallback for Japanese after the requested font; installed system fonts come after.
|
||||
Noto Sans Thai (when the checkout has it) is the fallback for Thai where no Thai font is
|
||||
installed, and the web build fetches it before the app starts (`crates/text/web-fonts.txt`).
|
||||
Noto Sans Georgian (when the checkout has it) is the same for Georgian.
|
||||
All other supplied faces follow those existing document fallbacks in manifest order, so they
|
||||
can also be selected by family name. This does not change which faces the web build embeds
|
||||
or fetches, and no craft-fonts faces are registered when the optional input is absent.
|
||||
- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first), after
|
||||
the UI fonts and before the installed fonts `ui_fonts.rs` discovers for other scripts.
|
||||
- Tests that need Japanese glyphs skip with a message when built without craft-fonts. The FreeBSD
|
||||
- UI: `theme::install_fonts` adds them at the end of every egui family (BIZ UDPGothic first, then
|
||||
Noto Sans Georgian for the Georgian interface), after the UI fonts and before the installed fonts
|
||||
`ui_fonts.rs` discovers for other scripts. On the web, Noto Sans Georgian is fetched at startup
|
||||
and reaches the UI through `ui_fonts.rs`.
|
||||
- Tests that need Japanese or Georgian glyphs skip with a message when built without craft-fonts. The FreeBSD
|
||||
CI job and every release job (`release.yml`) check craft-fonts out at a pinned commit; release
|
||||
packages carry each embedded font's `OFL-<family>.txt`.
|
||||
|
||||
@@ -155,17 +166,42 @@ CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci # also runs the Japanese-
|
||||
|
||||
The Latin fonts in `assets/fonts/` are compiled into `vectorcraft-text` by its `bundled-fonts`
|
||||
feature (3.4 MB of the binary). It is on by default, and `vectorcraft-engine`'s default
|
||||
`bundled-fonts` feature turns it on, so the apps and anything built on the engine have them. An
|
||||
`bundled-fonts` feature turns it on, so the apps and anything built on the engine with its default
|
||||
features have them. An
|
||||
embedder that brings every font itself at run time (`FontDb::add_font`, say a web app serving its
|
||||
own font catalogue) depends on the engine with `default-features = false` and doesn't ship them
|
||||
twice. Its font database starts without them: on the web and without craft-fonts it starts empty,
|
||||
and text has no glyphs until a font is added.
|
||||
|
||||
- The workspace declares `vectorcraft-text` without default features, so no library crate turns the
|
||||
fonts back on; a crate whose tests lay type out asks for `bundled-fonts` in its
|
||||
dev-dependencies (or gets it through `vectorcraft-testkit`, which uses the engine).
|
||||
An embedder that also hosts the egui panels depends on `vectorcraft-ui-egui` with
|
||||
`default-features = false` as well: its own `bundled-fonts` feature, on by default, turns the
|
||||
engine's on; `vectorcraft-mcp` has the same feature. The panels' interface faces (Source Sans 3
|
||||
Regular and Semibold, Inter Regular and SemiBold, JetBrains Mono, about 2 MB: `theme.rs`) are
|
||||
`vectorcraft-ui-egui`'s `interface-fonts` feature, also on by default; without it the panels draw in
|
||||
egui's built-in fonts, which lack some symbols they show (→ ▾ ● ✓), so such an embedder adds a
|
||||
font that has them to each of the families below (`ctx.add_font` with an `InsertFontFamily` per
|
||||
family).
|
||||
|
||||
The panels and dialogs draw in their own egui families (`theme::FONT_UI`, `FONT_UI_SEMIBOLD`,
|
||||
`FONT_MONO`), and egui panics drawing text in a family no font is bound to. `VectorcraftApp::logic`
|
||||
binds them on its first frame; an embedder that draws the panels or dialogs without it calls
|
||||
`theme::install_fonts` once on its context, with or without `interface-fonts`. It replaces the
|
||||
context's font definitions, so the embedder's own fonts are added after it.
|
||||
|
||||
- The workspace declares `vectorcraft-text` and `vectorcraft-engine` without default features, so
|
||||
each crate that wants the fonts asks for them (the apps, and `vectorcraft-testkit` for tests);
|
||||
a crate whose tests lay type out asks for `bundled-fonts` in its dev-dependencies (or gets it
|
||||
through `vectorcraft-testkit`).
|
||||
- `cargo xtask ci`'s `no-bundled-fonts` step also checks that `vectorcraft-ui-egui` and
|
||||
`vectorcraft-mcp` without their default features turn them on nowhere, for any target
|
||||
(`cargo tree -p vectorcraft-ui-egui --no-default-features --target all -e features,normal -i
|
||||
vectorcraft-text`, and the same for `vectorcraft-mcp`), and with them do; a test of either can't
|
||||
see that configuration, since their dev-dependencies turn the fonts on.
|
||||
- `cargo test -p vectorcraft-text --no-default-features --test without_bundled_fonts` checks the
|
||||
configuration without them.
|
||||
configuration without them, and `cargo test -p vectorcraft-ui-egui --no-default-features --test
|
||||
install_fonts` that the panels without their interface faces still bind every family; the same
|
||||
step lints `vectorcraft-ui-egui` without its default features. Of the crate's other tests, those
|
||||
about the interface faces need them.
|
||||
|
||||
### Arabic fonts and the web build
|
||||
|
||||
@@ -205,7 +241,7 @@ locale such as `ru-RU`, `ru-BY` or `ru-KZ` resolves to it), Ukrainian (`uk`, com
|
||||
`uk_UA.UTF-8` and every other `uk-*` locale resolve to it; integer plurals use one/few/many), Hungarian (`hu`, complete;
|
||||
`hu-HU` and every other `hu-*` locale resolve to it; a count takes the singular, so plurals have one form), Polish (`pl`, complete; `pl-PL` and every other `pl-*`
|
||||
locale resolve to it; plurals use one/few/many, with 21, 31 … taking many), Swedish (`sv`, complete; `sv-SE`, `sv-FI` and every
|
||||
other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian Portuguese
|
||||
other `sv-*` locale resolve to it), Georgian (`ka`, complete; `ka-GE` and `ka_GE.UTF-8` resolve to it; one count-neutral plural form), Czech (`cs`, every menu label) and Brazilian Portuguese
|
||||
(`pt-br`, complete; `pt-BR`, `pt_BR.UTF-8` and other tags that resolve to `pt-br` through `candidates`). Untranslated text falls back to English until its rows are added.
|
||||
|
||||
- `tl!("…")` translates a literal into the language the UI is drawn in; `i18n::t(s)` is the same for a
|
||||
@@ -225,19 +261,31 @@ other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian
|
||||
rule). The Language menu, the dropdown, locale matching and the catalog tests (well-formed, no duplicates,
|
||||
placeholders and ellipses agree, command ids exist, every row of a partial catalog is a string the UI
|
||||
shows, the UI fonts have every glyph, no Simplified characters in `zh-hant`) pick it up. Set `complete_menus`
|
||||
once every menu string and `tl!` literal is translated; `i18n::tests` then enforce it
|
||||
(`VECTORCRAFT_I18N_DUMP=strings.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists them).
|
||||
once the menus and `tl!` literals are translated; the untranslated report then lists what the catalog lacks
|
||||
(`VECTORCRAFT_I18N_DUMP=strings.txt cargo test -p vectorcraft-ui-egui dump_source_strings` lists every
|
||||
English string).
|
||||
- A missing translation never fails a test or a build: the interface shows the English text until a translator
|
||||
adds the row. `cargo xtask i18n-report` prints, per language, the strings its catalog lacks (menu strings,
|
||||
`tl!` literals, undo step names, menu labels, render-site labels, status and error messages, PostScript
|
||||
error meanings, default swatch names), and `cargo xtask ci` prints the same list in its `i18n-report` step.
|
||||
The report is the test `i18n::tests::untranslated_report`, run with `--nocapture`; it fails only when a
|
||||
scan finds too few strings to be working. Tests don't check one language's exact wording: a test that needs a
|
||||
translation compares with the catalog lookup (`tr(lang, "English")`).
|
||||
- Translations are clean-room: written from the meaning of the English text in ordinary vocabulary, never
|
||||
from another product's localisation resources. Product and technology names stay in Latin letters.
|
||||
- Status-bar messages and errors stay English where they are made (`app.ui.status`, `EngineError`: the
|
||||
control channel, MCP and tests read them) and are translated only where the status bar draws them
|
||||
(`i18n::msg`). `@msg` catalog rows hold a whole message or a template such as
|
||||
`Couldn't open {name}: {e}`; `{_1}`, `{_2}` … stand for the format string's `{}`, and the values in the
|
||||
placeholders are translated in turn (the reason after `: {e}` is often a message too). Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Swedish, Ukrainian, Vietnamese and Finnish cover every
|
||||
message literal the test scan finds (`complete_languages_translate_every_message`, languages listed in
|
||||
`COMPLETE_MESSAGES`): a new `Err("…")`, `Other(…)`, `#[error(…)]` or `status(…)` message needs a row in each of those catalogs (`VECTORCRAFT_I18N_DUMP_MESSAGES=messages.txt cargo test -p vectorcraft-ui-egui
|
||||
complete_languages_translate_every_message` lists them all). Other languages show messages in English
|
||||
until they add `@msg` rows.
|
||||
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(…)`, 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.
|
||||
- Not translated on purpose: names that are user data (layers, swatches, fonts, documents), the tab
|
||||
title's colour mode, command ids and parameter names inside messages. Not done yet: locale-aware number
|
||||
and date formats, right-to-left layout. Chinese and Japanese UI text
|
||||
@@ -251,7 +299,32 @@ other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian
|
||||
|
||||
## Vendor names gate
|
||||
|
||||
`cargo xtask brands` (part of `cargo xtask ci`) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), `Cargo.toml` descriptions and packaging files. Comments and test code are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, carries a `brand-ok` comment.
|
||||
`cargo xtask brands` (part of `cargo xtask ci`) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), `Cargo.toml` descriptions and packaging files. Comments, test files and `#[cfg(test)]` modules are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, or a name another project defines, such as a shared MIME type, carries a `brand-ok` comment. In the Linux desktop entry, whose comments take whole lines, the comment goes on the `#` line just above it; `MARK_ABOVE` in `xtask/src/brands.rs` lists the files that accept a mark there.
|
||||
|
||||
## Tests
|
||||
|
||||
- `cargo xtask test` runs the workspace's unit and integration tests with
|
||||
[cargo-nextest](https://nexte.st) when it is installed (`cargo install cargo-nextest --locked`),
|
||||
otherwise with `cargo test`, and then the doctests (`cargo test --workspace --doc`), which nextest
|
||||
doesn't run. `cargo xtask ci` runs the same two as its `test` and `doctests` steps.
|
||||
- The test build leaves out the examples (`--lib --bins --tests`); clippy (`--all-targets`) still
|
||||
checks them.
|
||||
- Each crate has one integration-test binary: its test files are modules of `tests/it/main.rs`. Run
|
||||
one file's tests with `cargo nextest run -p vectorcraft-svg --test it roundtrip::` (or `cargo test`
|
||||
with the same arguments). A new test file gets a `mod` line in `main.rs`.
|
||||
- A test file stays a binary of its own (`tests/<name>.rs`) when it changes process-wide state that
|
||||
other tests would see, because `cargo test` runs a binary's tests as threads of one process (nextest
|
||||
runs each test in its own process). In `crates/engine/tests`: the Color Settings
|
||||
(`color_settings.rs`), the font database and Fonts folder (`removed_font.rs`, `font_folder.rs`,
|
||||
`text_import_fuzz.rs`), installed plug-ins (`plugin_export.rs`, `plugin_fuzz.rs`), the working
|
||||
directory (`open_relative_path.rs`) and every command run with junk parameters
|
||||
(`command_sweep.rs`); the `system_fonts` tests of `vectorcraft-text` and `vectorcraft-pdf` too. The
|
||||
fuzz files under Robustness › Fuzzing keep their own binaries, so a deeper search runs alone, and so
|
||||
do `affinity_artboards`, `affinity_text` and `affinity_corpus`, which the Affinity corpus workflow
|
||||
names.
|
||||
- The dev profile builds the workspace crates with line tables only (`debug = "line-tables-only"`),
|
||||
so panics and backtraces still name file and line, and dependencies without debug info. For a
|
||||
debugger session, `CARGO_PROFILE_DEV_DEBUG=true cargo build` gives full debug info.
|
||||
|
||||
## Robustness: VectorCraft never crashes
|
||||
|
||||
@@ -275,7 +348,8 @@ in the code:
|
||||
| `todo!`, `unimplemented!` | don't merge unfinished paths; return an error that says what isn't supported |
|
||||
|
||||
`clippy.toml` allows `unwrap`, `expect` and `panic` inside `#[test]` and `#[cfg(test)]` code.
|
||||
Integration-test files under `tests/` and `examples/`, and the `testkit` crate, opt out with
|
||||
Integration tests (`tests/it/main.rs` and the test files that are binaries of their own), examples
|
||||
and the `testkit` crate opt out with
|
||||
`#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]`. Tests use `panic!("…")`
|
||||
instead of `unreachable!()`.
|
||||
|
||||
@@ -366,10 +440,72 @@ The original site's plain HTTP archive is no longer fetched or extracted. To ref
|
||||
review the mirror commit, original-file list, redistribution license, sizes and hashes together;
|
||||
do not replace expected hashes automatically in response to a download mismatch.
|
||||
|
||||
## Highlighted-text Eyedropper (2026-10-21)
|
||||
|
||||
Switching from Type to Eyedropper preserves the highlighted character range and its overlay.
|
||||
Sampling applies enabled font and paint attributes to that range; Shift samples color only.
|
||||
Returning to Type retains the range; deselection and document changes clear stale ranges.
|
||||
Character styles cannot represent object opacity or blend modes. With Transparency enabled,
|
||||
sampling refuses a non-neutral source or target object before changing art, paint defaults,
|
||||
selection or history. Disable Transparency in Eyedropper Options to sample supported attributes
|
||||
while preserving the target object's transparency. Whole-object sampling remains unchanged.
|
||||
Regression evidence: `tests/eyedropper.rs` covers Unicode, font/paint, Undo/Redo, tool switches,
|
||||
and atomic rejection of source and target opacity/blend modes; `text_range_and_brush_sampling_options_coexist`
|
||||
covers upstream brush-key sampling. Scores and estimates unchanged.
|
||||
|
||||
## Drag artwork between document tabs (2026-10-22)
|
||||
|
||||
Hold a Selection-tool move over another visible document tab for 400 ms to activate it,
|
||||
then release over its canvas to copy the selected artwork. The original grab point is kept
|
||||
across different zoom levels and view rotations. The source preview is rolled back before
|
||||
the switch; source artwork, source history and the internal clipboard are preserved. Returning
|
||||
to the source tab resumes a move (Alt copies). Escape or release outside the canvas cancels.
|
||||
Changing the source revision or unexpectedly changing/closing the destination cancels a stale
|
||||
transfer; focus loss, touch cancellation and multitouch cancel too. Moving tabs under a stationary
|
||||
pointer does not activate further tabs until the pointer moves. The close icon does not activate a tab during a drag. Overflow-menu hover activation and
|
||||
cross-window dragging remain outside this change.
|
||||
The cited regressions assert the internal clipboard; they do not exercise the native
|
||||
system-clipboard adapter. Tab activation resets the Selection tool through
|
||||
`Session::set_active`; gesture regressions verify it is idle after switching, dropping
|
||||
and cancelling a transfer.
|
||||
|
||||
`edit.dropSelection {sourceDocument, ids, grab, at}` uses document-scoped ids
|
||||
(`sourceDocument` is `document.inspect`’s session-unique `uid`), validates 1–10000 ids
|
||||
before editing, targets the current editable insertion context (Paste Remembers Layers does
|
||||
not reroute a drag into a matching source layer), and uses the existing
|
||||
lossless paste resource pipeline: groups, editable paths/type, images, symbols and patterns
|
||||
travel together. Same-name conflicting swatches are added under unique names to preserve
|
||||
source colours; pattern fills/strokes translate with the dropped artwork to preserve their alignment. The target receives one Undo/Redo step. Invalid or closed sources fail before
|
||||
editing. Existing artwork-to-panel drags retain their original behavior.
|
||||
|
||||
Evidence: `tests_document_drag` drives actual tab-strip/canvas pointer frames; `tests_clipboard`
|
||||
covers resource rendering, id collisions, swatch conflicts, grab-point placement, locked-target
|
||||
refusal, excessive ids, malformed numeric parameters and atomic invalid-input refusal. A private Linux/Xvfb native build also placed an original logo group
|
||||
onto a flyer through the command API; its screenshot was inspected, source preservation and
|
||||
Undo/Redo checked. Physical mouse hover on the desktop is not covered by that native smoke test.
|
||||
Public behavior reference: [Illustrator public duplication documentation](https://helpx.adobe.com/illustrator/desktop/manage-objects/edit-objects/duplicate-objects.html).
|
||||
The tab-hover workflow follows the contributor’s requested behavior. All test artwork is original geometry/text generated locally.
|
||||
The new command and validation messages are translated in all 24 language catalogs, including Turkish.
|
||||
Scores and hour estimates are unchanged.
|
||||
|
||||
## Revision history
|
||||
|
||||
| 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. |
|
||||
| 2026-10-21 | minor | Fonts › Leaving out the bundled fonts: vectorcraft-ui-egui's and vectorcraft-mcp's `bundled-fonts` features, and the engine declared without default features. |
|
||||
| 2026-10-14 | minor | Document the `bundled-fonts` feature of vectorcraft-text and vectorcraft-engine (Fonts › Leaving out the bundled fonts). |
|
||||
| 2026-10-14 | minor | Document registration of every explicitly supplied craft-fonts face after the existing script-specific prefix; empty input and web selection remain unchanged. |
|
||||
| 2026-10-13 | minor | Document pinned PngSuite files, integrity checks and download budgets (#1729). |
|
||||
|
||||
> **Tab-drag status (2026-10-22):** updated from upstream, including the Turkish catalog and all three tab-drag translations requested in #3479; Selection-tool reset regression-checked and clipboard evidence scoped to internal state; drag lifecycle, insertion and regression contracts documented; behavior, scores and estimates unchanged.
|
||||
|
||||
| Date | Change | Tab-drag history |
|
||||
|---|---|---|
|
||||
| 2026-10-22 | minor | Verify Selection-tool reset after tab activation, drop and cancellation; narrow clipboard evidence to the tested internal clipboard and correct documentation grammar; behavior, scores and estimates unchanged. |
|
||||
| 2026-10-22 | minor | Merge current main and complete the Turkish tab-drag labels and validation messages requested by the maintainer; all 24 catalogs covered; scores and estimates unchanged. |
|
||||
| 2026-10-22 | minor | Tab-drag follow-up: reconcile upstream progress updates and document drag lifecycle, insertion behavior and regression contracts; behavior, scores and estimates unchanged. |
|
||||
| 2026-10-22 | minor | Artwork dragged with Selection switches visible document tabs after a 400 ms hover, then copies editable artwork on canvas release; source and clipboard stay intact; scores and hours unchanged |
|
||||
|
||||
+3
-1
@@ -5,7 +5,9 @@ The **Dimension Tool** is in the Modify section of the toolbar. Its strip offers
|
||||
|
||||
- **Linear:** hover a path segment and drag outwards: releasing places the dimension. Or click two
|
||||
points (anchors snap, Shift constrains to 45°), then click to place. A curved segment is measured
|
||||
from end to end.
|
||||
from end to end. With Smart Guides on, both points also snap into line with other objects'
|
||||
anchors and centres, and the second onto the construction guides through the first; the guides
|
||||
show while hovering, before each click.
|
||||
- **Angular:** hover a corner where two straight segments meet and drag outwards. Or click two
|
||||
straight lines that meet (or would, extended), then click to place.
|
||||
- **Radial:** hover a circle, or a segment that is a perfect arc, and drag: outwards gives its
|
||||
|
||||
+106
-16
@@ -1,6 +1,6 @@
|
||||
# Effects parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 30/57, 27 remaining (22–38 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add an overprinting strip of the lighter colour where two solid colours meet; recounted with Hard Mix (#3061) and Soft Mix already in the catalogue: Pathfinder 13/13, Illustrator effects 49/50; previously: Transform effect: Transform Objects, Transform Patterns and Scale Strokes & Effects, #885; G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview; previously:; stacked geometry effects make at most 2 million anchors of a path and about 8 million lines to draw; Roughen, Zig Zag, Free Distort, Twist and the warps build within it, #3578; scores unchanged; G2: Effect › Sketch › Conté Crayon (Foreground Level, Background Level, Texture, Scaling, Relief, Light Direction, Invert; a black crayon in the dark areas and a white one in the light areas on textured mid-grey paper; ranges and defaults unverified VectorCraft choices); measured Photoshop-style filters 57/57, the Effect Gallery remains (4–6 h); 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; G2: Effect › Sketch › Note Paper; Halftone Pattern and Note Paper defaults documented as unverified VectorCraft choices; G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor; Sketch › Bas Relief, Charcoal, Photocopy and Stamp, #2533; Photoshop filters 46/57, 11 remaining (8–14 h); G2: Sketch › Chrome; Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap, #2972, #2967, #2976, #3034: 37/57 Photoshop effects; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; Zig Zag's Smooth points make a continuous wave, #2343; G2: Effect › Sketch › Halftone Pattern; Photoshop filters 46/57, 11 remaining (8–14 h); Round Corners also rounds corners where a curve meets a line or another curve, with Live Corners' cut; straight corners unchanged; score unchanged; Revolve: automatic smooth shading and default-on Cap with continuous rims; wide shadows and glows at high zoom, #1706; stacked Transform effects, #1705; 3D on nonzero compound paths, #1724; pattern tiles redraw, #1723; scores unchanged; Roughen's Smooth turn-backs, #1643; Scribble per region, #1648; score unchanged; Warp Arc bends in the object's proportions, #1578; Zig Zag ridges evenly spaced, #1558; effect parameters checked, #1592; score unchanged; raster effects after a pattern, radial or freeform gradient fill export at their place and size, #1536; score unchanged; 3D and Materials on outlined type and other shapes of many small curves, #1481; Twist 0° to 360°, #1524; twisted walls keep their faces, #1525; score unchanged; 3D and Materials: Inflate takes Twist, Taper and Cap; score unchanged; 3D and Materials: Perspective as the documented 0°–160° angle; 3D and Materials: the documented light settings) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
Every entry of Illustrator's Effect menu, by its public feature name, against VectorCraft's effects catalogue
|
||||
(`effect_catalog()` in `crates/effects/src/lib.rs`) and the Effect menu built from it
|
||||
@@ -11,8 +11,8 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
|
||||
|
||||
| Group | Illustrator | VectorCraft | % | Remaining (h) |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Illustrator effects (vector and stylize) | 50 | 46 | 92% | 6–10 |
|
||||
| Photoshop effects (raster, Effect Gallery) | 57 | 33 | 58% | 21–35 |
|
||||
| Illustrator effects (vector and stylize) | 50 | 49 | 98% | 4–8 |
|
||||
| Photoshop effects (raster, Effect Gallery) | 57 | 57 | 100% | 1–2 (Effect Gallery thumbnails and in-dialog preview) |
|
||||
| 3D and Materials | 4 (+3 Classic) | 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate) | ~25% | 25–45 |
|
||||
| Beyond Illustrator: Effect › Color Adjustments (6), effect plug-ins (WebAssembly) | — | 6 + plug-ins | — | — |
|
||||
|
||||
@@ -22,9 +22,9 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
|
||||
|---|---|---:|---|
|
||||
| Convert to Shape | Rectangle, Rounded Rectangle, Ellipse | 3/3 | — |
|
||||
| Crop Marks | Crop Marks | 1/1 | — |
|
||||
| Distort & Transform | Free Distort, Pucker & Bloat, Roughen, Transform, Tweak, Twist, Zig Zag | 7/7 | Free Distort's dialog has a handle on each corner of the object's bounds, moved by dragging or with the arrow keys; Transform's Transform Objects/Patterns and Scale Strokes & Effects options, invisible Reflect checkboxes (#885); Zig Zag's ridges are evenly spaced along each segment (#1558), and Smooth points make a continuous wave whose peaks and valleys have handles along the path (#2343); Roughen with Smooth keeps turn-backs smooth instead of cusps that draw as spikes (#1646) |
|
||||
| Distort & Transform | Free Distort, Pucker & Bloat, Roughen, Transform, Tweak, Twist, Zig Zag | 7/7 | Free Distort's dialog has a handle on each corner of the object's bounds, moved by dragging or with the arrow keys; Transform Patterns and Scale Strokes & Effects on a fill's or stroke's own Transform effect (on the object, Transform has all three options and visible checkboxes, #885); Zig Zag's ridges are evenly spaced along each segment (#1558), and Smooth points make a continuous wave whose peaks and valleys have handles along the path (#2343); Roughen with Smooth keeps turn-backs smooth instead of cusps that draw as spikes (#1646) |
|
||||
| Path | Offset Path, Outline Object, Outline Stroke | 3/3 | With Use Preview Bounds on, Align, Distribute and the Transform panel still measure type that carries Outline Object by its layout box (preview bounds leave out effects) |
|
||||
| Pathfinder | Add, Intersect, Exclude, Subtract, Minus Back, Divide, Trim, Merge, Crop, Outline, Hard Mix, Soft Mix, Trap | 10/13 | Hard Mix, Soft Mix, Trap |
|
||||
| Pathfinder | Add, Intersect, Exclude, Subtract, Minus Back, Divide, Trim, Merge, Crop, Outline, Hard Mix, Soft Mix, Trap | 13/13 | Trap traps only solid fills (gradients and patterns hide what is below them but don't trap) and spreads each colour's visible area as a whole; the public page gives no default values, so Trap's 0.25 pt, 100% and 40% are VectorCraft's own |
|
||||
| Rasterize | Rasterize… (as a live effect) | 1/1 | — |
|
||||
| Stylize | Drop Shadow, Feather, Inner Glow, Outer Glow, Round Corners, Scribble | 6/6 | Scribble Options' Settings presets |
|
||||
| SVG Filters | Apply SVG Filter… and the filter list | 0/1 | the whole submenu (SVG filters on import round-trip already) |
|
||||
@@ -32,28 +32,28 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md](
|
||||
|
||||
Document Raster Effects Settings exists. When the 64 million pixel cap on an effect image lowers
|
||||
its resolution, PDF export warns once per object with the requested and the lowest achieved ppi
|
||||
(`crates/engine/src/tests_pdfraster.rs`).
|
||||
(`crates/engine/src/tests/pdfraster.rs`).
|
||||
|
||||
## Photoshop effects
|
||||
|
||||
| Submenu | Items | Ours | Missing |
|
||||
|---|---|---:|---|
|
||||
| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 4/15 (approximate Cutout, Film Grain, Neon Glow, Poster Edges) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, Plastic Wrap, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor |
|
||||
| Artistic | Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor | 15/15 (approximate Colored Pencil, Cutout, Dry Brush, Film Grain, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, Rough Pastels, Smudge Stick, Sponge, Underpainting, Watercolor) | — |
|
||||
| Blur | Gaussian Blur, Radial Blur, Smart Blur | 3/3 | — |
|
||||
| Brush Strokes | Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e | 8/8 | — |
|
||||
| Distort | Diffuse Glow, Glass, Ocean Ripple | 3/3 | — |
|
||||
| Pixelate | Color Halftone, Crystallize, Mezzotint, Pointillize | 4/4 | — |
|
||||
| Sharpen | Unsharp Mask | 1/1 | — |
|
||||
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 1/14 | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper |
|
||||
| Sketch | Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper | 14/14 (approximate Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Conté Crayon, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp, Torn Edges, Water Paper; Chalk & Charcoal, Conté Crayon, Graphic Pen, Note Paper and Water Paper defaults unverified) | — |
|
||||
| Stylize | Glowing Edges | 1/1 | — |
|
||||
| Texture | Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer | 6/6 | Texturizer's Load Texture |
|
||||
| Texture | Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer | 6/6 | — |
|
||||
| Video | De-Interlace, NTSC Colors | 2/2 | — |
|
||||
| Effect Gallery | the combined browser and stack dialog | 0/1 | all |
|
||||
| Effect Gallery | the combined browser and stack dialog | 1/1 (Effect › Effect Gallery…, #2883: six categories, per-filter settings, ordered stack with add/reorder/hide/remove, live canvas preview, one undo step; `dialogs::effect_gallery::tests::stack_ok_is_one_undo_step`, `stack_preview_is_one_interaction`) | filter thumbnails; a preview area inside the dialog ([Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html)) |
|
||||
|
||||
The pixel-filter pipeline (`vectorcraft_effects::pixel`) is in place: each new filter is one function, a
|
||||
catalogue row, a menu entry and translations. Measured throughput: 0.7–1.2 agent hours per filter
|
||||
(see [calibration](target-app-parity.md#calibration)), so the 24 missing filters are ~17–29 h and the Effect
|
||||
Gallery dialog another 4–6 h. These parallelize well across agents.
|
||||
(see [calibration](target-app-parity.md#calibration)). Every Photoshop-style filter is in the catalogue, and the Effect
|
||||
Gallery dialog shipped (#2883); its filter thumbnails and in-dialog preview area are the 1–2 h that remain.
|
||||
|
||||
Film Grain (`artistic.filmGrain`, Effect › Artistic) adds stable monochrome noise to shadows and
|
||||
midtones, with Grain, Highlight Area and Intensity sliders, live preview and appearance editing.
|
||||
@@ -82,7 +82,26 @@ glow colour is the effect's own parameter. All three keep coverage, work in docu
|
||||
Document Raster Effects resolution, and use the generic effect dialog with live preview. Ranges and
|
||||
defaults are chosen for VectorCraft. Evidence: `crates/effects/src/pixel/artistic.rs`, the
|
||||
`artistic_*`, `cutout_*`, `poster_edges_*` and `neon_glow_*` tests in `crates/effects/src/pixel/tests.rs`,
|
||||
and `artistic_settings_survive_a_save_and_open` in `crates/engine/src/tests_rasterfilters.rs`.
|
||||
and `artistic_settings_survive_a_save_and_open` in `crates/engine/src/tests/rasterfilters.rs`.
|
||||
Smudge Stick (`artistic.smudgeStick`), Sponge (`artistic.sponge`) and Watercolor (`artistic.watercolor`),
|
||||
Effect › Artistic, are original visual approximations from the public descriptions in
|
||||
[Illustrator's effects reference](https://helpx.adobe.com/illustrator/using/summary-effects.html) and the
|
||||
[public Artistic filters guide](https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html), not
|
||||
pixel-identical copies. Each has live preview, appearance editing, document-unit patterns that follow the
|
||||
object, bounded work per pixel and kept coverage (they paint nothing outside the object), and bakes to an
|
||||
image in SVG and PDF. Ranges and defaults are chosen for VectorCraft, not asserted as measured.
|
||||
- Smudge Stick (Stroke Length 0–10, Highlight Area 0–20, Intensity 0–10): dark areas smudged along short
|
||||
diagonal strokes; light areas smudged with strokes twice as long and lightened towards white.
|
||||
- Sponge (Brush Size 0–10, Definition 0–25, Smoothness 1–15): the object softened, then lighter and darker
|
||||
blobs of the same hue, their contrast set by Definition, their edges by Smoothness.
|
||||
- Watercolor (Brush Detail 1–14, Shadow Intensity 0–10, Texture 1–3): detail simplified by an edge-keeping
|
||||
average whose brush widens as Brush Detail falls, colour saturated and pooled darker where tones change at
|
||||
edges, shadows deepened, a faint paper grain. Edges are found from tone only, so a change of hue at equal
|
||||
brightness is not saturated.
|
||||
|
||||
Tests: `pixel::tests_paint::*`, `tests_rasterfilters`, `dialogs::tests::raster_filter_dialogs_*`,
|
||||
`tests_labels::effect_menu_lists_the_artistic_filters_*` and the `command_sweep` structured junk.
|
||||
|
||||
Near white, highlight headroom can limit grain equally at every Intensity; highlight lift still
|
||||
depends on Intensity when Highlight Area is nonzero.
|
||||
The two rotated, variance-normalised noise layers can leave a faint diagonal weave at high
|
||||
@@ -93,7 +112,67 @@ Calibration on uniform grey at Grain 20, using 32×32 fine samples per coarse pi
|
||||
grain standard deviation within about 15% of the dense area average at pixel widths of 2, 4
|
||||
and 8 document units (ratios 1.13, 0.89, 0.85). This is evidence for the bounded approximation,
|
||||
not an accuracy guarantee for arbitrary artwork. Tests: `pixel::tests::film_grain_*`,
|
||||
`tests_rasterfilters`, `tests_adjust::film_grain_dialog_*` and `tests_labels::effect_menu_lists_film_grain_*`.
|
||||
`tests_rasterfilters`, `tests_adjust::film_grain_dialog_*` and `tests_labels::effect_menu_lists_the_artistic_filters_*`.
|
||||
|
||||
Smudge Stick (`artistic.smudgeStick`), Sponge (`artistic.sponge`) and Watercolor (`artistic.watercolor`),
|
||||
Effect › Artistic, are original visual approximations from the public descriptions in
|
||||
[Illustrator's effects reference](https://helpx.adobe.com/illustrator/using/summary-effects.html) and the
|
||||
[public Artistic filters guide](https://helpx.adobe.com/photoshop-elements/using/artistic-filters.html), not
|
||||
pixel-identical copies. Each has live preview, appearance editing, document-unit patterns that follow the
|
||||
object, bounded work per pixel and kept coverage (they paint nothing outside the object), and bakes to an
|
||||
image in SVG and PDF. Ranges and defaults are chosen for VectorCraft, not asserted as measured.
|
||||
- Smudge Stick (Stroke Length 0–10, Highlight Area 0–20, Intensity 0–10): dark areas smudged along short
|
||||
diagonal strokes; light areas smudged with strokes twice as long and lightened towards white.
|
||||
- Sponge (Brush Size 0–10, Definition 0–25, Smoothness 1–15): the object softened, then lighter and darker
|
||||
blobs of the same hue, their contrast set by Definition, their edges by Smoothness.
|
||||
- Watercolor (Brush Detail 1–14, Shadow Intensity 0–10, Texture 1–3): detail simplified by an edge-keeping
|
||||
average whose brush widens as Brush Detail falls, colour saturated and pooled darker where tones change at
|
||||
edges, shadows deepened, a faint paper grain. Edges are found from tone only, so a change of hue at equal
|
||||
brightness is not saturated.
|
||||
|
||||
Tests: `pixel::tests_paint::*`, `tests_rasterfilters`, `dialogs::tests::raster_filter_dialogs_*`,
|
||||
`tests_labels::effect_menu_lists_the_artistic_filters_*` and the `command_sweep` structured junk.
|
||||
|
||||
Bas Relief, Charcoal, Photocopy and Stamp (`sketch.basRelief`, `sketch.charcoal`, `sketch.photocopy`,
|
||||
`sketch.stamp`, Effect › Sketch; `pixel/sketch_ink.rs`) redraw the object in black ink on white paper
|
||||
and keep its coverage. Bas Relief carves low relief lit from one of eight directions (Detail, Smoothness,
|
||||
Light Direction); Charcoal draws bold edges and sketches the midtones in diagonal strokes (Thickness,
|
||||
Detail, Light/Dark Balance); Photocopy prints only where a pixel is darker than its surroundings, so large
|
||||
dark areas copy around their edges (Detail, Darkness); Stamp reduces the object to solid black and
|
||||
white (Light/Dark Balance, Smoothness). Sizes are in document points at any Document Raster Effects
|
||||
resolution, strokes and noise lie in document space around the object's centre, and all four preview
|
||||
live. Behaviour is an original visual approximation based on
|
||||
[Illustrator's public effects reference](https://helpx.adobe.com/illustrator/using/summary-effects.html)
|
||||
and the [public Sketch filters guide](https://helpx.adobe.com/photoshop-elements/using/sketch-filters.html),
|
||||
not a pixel-identical proprietary implementation; the parameter ranges and defaults are chosen for
|
||||
VectorCraft, not asserted as measured proprietary defaults. Tests: `pixel::tests_sketch::*`,
|
||||
`tests_rasterfilters`, `command_sweep`, `dialogs::tests::raster_filter_dialogs_preview_and_apply` and
|
||||
`tests_labels::effect_menu_lists_the_sketch_filters_*`.
|
||||
|
||||
Effect › Sketch › Halftone Pattern and Note Paper (`sketch.halftonePattern`, `sketch.notePaper`) are
|
||||
original black-and-white filters guided by [Illustrator's public Sketch effects summary](https://helpx.adobe.com/illustrator/using/summary-effects.html)
|
||||
and [effects overview](https://helpx.adobe.com/illustrator/using/effects.html) (Halftone Size in points at any Document Raster Effects resolution).
|
||||
The public help pages name each control but publish no numeric defaults or slider limits; VectorCraft's ranges
|
||||
follow those control names and the emboss/halftone behaviour described there, while dialog defaults are chosen
|
||||
for VectorCraft and are not verified against proprietary defaults. Evidence: `crates/effects/src/pixel/sketch.rs`,
|
||||
`halftone_*` and `note_paper_*` in `crates/effects/src/pixel/tests.rs`, `SKETCH` and
|
||||
`halftone_pattern_settings_survive_a_save_and_open` in `crates/engine/src/tests/rasterfilters.rs`, and
|
||||
`tests_labels::effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize`.
|
||||
|
||||
Effect › Sketch › Conté Crayon (`sketch.conteCrayon`; `pixel/sketch.rs`) redraws the object in a dense
|
||||
black crayon where it is dark and a pure white crayon where it is light, on mid-grey paper, as
|
||||
[Illustrator's public effects summary](https://helpx.adobe.com/illustrator/using/summary-effects.html) describes
|
||||
(black for dark areas, white for light areas). Its controls follow the public
|
||||
[Photoshop filter reference](https://helpx.adobe.com/photoshop/using/filter-effects-reference.html) and the
|
||||
[Photoshop Elements Sketch filters guide](https://helpx.adobe.com/photoshop-elements/using/sketch-filters.html)
|
||||
(foreground and background levels, texture options): Foreground Level and Background Level (0–20) set how far
|
||||
each crayon reaches into the midtones; Texture (Brick, Burlap, Canvas, Sandstone, the surfaces Texturizer
|
||||
draws), Scaling, Relief, Light Direction and Invert make the paper's tooth, which breaks the diagonal crayon
|
||||
strokes, and its relief. The public pages publish no slider ranges or defaults, so those are unverified
|
||||
VectorCraft choices; an original approximation, never compared with another application's output. Tests:
|
||||
`pixel::tests_sketch::conte_crayon_*`, `tests_rasterfilters`, `command_sweep`,
|
||||
`dialogs::tests::raster_filter_dialogs_preview_and_apply` and
|
||||
`tests_labels::effect_menu_lists_the_sketch_filters_between_sharpen_and_stylize`.
|
||||
|
||||
## 3D and Materials
|
||||
|
||||
@@ -101,20 +180,31 @@ not an accuracy guarantee for arbitrary artwork. Tests: `pixel::tests::film_grai
|
||||
|---|---|
|
||||
| Revolve | initial: live, editable profile, rotation and light gizmos, default-on Cap with continuous curved rims, smooth vector shading and round seam joins, SVG/PDF output, Expand Appearance ([revolve.md](revolve.md), #846) |
|
||||
| Extrude & Bevel, Inflate, Rotate | initial: live, depth, twist, taper, solid or hollow caps, five original bevel shapes, Inflate volume, both sides, twist, taper and cap, Rotate of every fill and stroke, Position presets, rotation and light gizmos, SVG/PDF output, Expand Appearance ([three-d.md](three-d.md)) |
|
||||
| Materials panel, lighting, ray-traced render | one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; no materials, light presets, shadows or ray tracing |
|
||||
| Materials panel, lighting, ray-traced render | one to ten directional lights with per-light Color (opaque RGB, white by default), Intensity, Rotation, Height, Softness and neutral Ambient Light, Revolve and colored Inflate use smooth vector shading; Extrude & Bevel and Rotate retain planar shading; original Standard, Diffuse, Top Left and Right lighting rigs; no materials, shadows or ray tracing |
|
||||
| 3D (Classic): Extrude & Bevel, Revolve, Rotate | none |
|
||||
| Turntable (30.0 beta) | none |
|
||||
| Export 3D objects (OBJ, USDA, glTF) | none |
|
||||
|
||||
The `three-d` crate (depends on `geom` and `pathops`, with no UI or GPU dependencies) builds, rotates, projects, shades and depth-sorts the surfaces;
|
||||
the effects crate turns them into art. What's left (materials, light presets and shadows, ray-traced rendering,
|
||||
the effects crate turns them into art. What's left (materials and shadows, ray-traced rendering,
|
||||
angular cut caps, exact visibility, 3D Classic, Turntable, 3D export) is 25–45 h.
|
||||
|
||||
## Revision history
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | minor | Pathfinder › Trap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html)): the Pathfinder panel's Trap… (`object.pathfinder.trap`, one undo step, the traps grouped above the topmost selected object and selected) and Effect › Pathfinder › Trap… (`pathfinder.trap`, live on groups) add an overprinting strip of the lighter colour inside the darker one where two solid colours meet (Reverse Traps: the darker into the lighter, but not a rich black), with Thickness, Height/Width, Tint Reduction and Traps with Process Color; the effect also has Precision and Remove Redundant Points. Recounted: Hard Mix (#3061) and Soft Mix had shipped, so Pathfinder 11/13 → 13/13 and Illustrator effects 46/50 → 49/50 (SVG Filters remains); remaining 6–10 → 4–8 h. Tests: `vectorcraft_pathops` `trap::tests`, `vectorcraft_effects` `trap::tests` and `group::tests::trap_effect_adds_an_overprinting_strip_above_the_members`, `vectorcraft_engine` `tests_trap`, `panels::pathfinder::tests::trap_opens_its_options_and_ok_traps_the_selection`. |
|
||||
| 2026-10-22 | minor | The Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, with visible checkboxes (#885); on a fill's or stroke's own Transform effect the last two are still missing. Counts unchanged. |
|
||||
| 2026-10-22 | minor | G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview. |
|
||||
| 2026-10-22 | minor | Stacked geometry effects make at most 2 million anchors of a path (of a type, symbol or image art's paths together) and about 8 million lines to draw: Roughen spaces its points further apart, Zig Zag makes fewer ridges a segment, Free Distort, Twist and the warps split into longer pieces, a result drawn with too many lines is built again with fewer points, and an effect that would still pass either is left out (#3578). Scores unchanged (estimated) |
|
||||
| 2026-10-22 | minor | G2: Effect › Sketch › Conté Crayon (Foreground Level, Background Level, Texture, Scaling, Relief, Light Direction, Invert; a black crayon in the dark areas and a white one in the light areas on textured mid-grey paper; ranges and defaults unverified VectorCraft choices); measured Photoshop-style filters 57/57, the Effect Gallery remains (4–6 h); Sketch 14/14. |
|
||||
| 2026-10-21 | minor | 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged |
|
||||
| 2026-10-20 | minor | G2: Effect › Sketch › Water Paper (Fiber Length, Brightness, Contrast; colours flow on damp fibrous paper, defaults unverified, #2955); measured Photoshop filters 47/57, Sketch 12/14. |
|
||||
| 2026-10-20 | minor | G2: Effect › Sketch › Note Paper (Image Balance, Graininess, Relief; embossed tones on fibrous paper); Halftone Pattern and Note Paper parameter ranges follow public control names, numeric defaults are unverified VectorCraft choices (public help gives no numbers); measured Photoshop filters 46/57, Sketch 11/14. |
|
||||
| 2026-10-19 | minor | Sketch › Chalk & Charcoal, Graphic Pen and Reticulation and Artistic › Plastic Wrap landed (#2972, #2967, #2976, #3034): 36/57 Photoshop effects (estimated; defaults unverified) |
|
||||
| 2026-10-16 | minor | Zig Zag with Smooth points draws a continuous wave: every peak and valley is a smooth point with handles along the path, a third of the way to the next ridge, so each span follows a half cosine through the path instead of a straight diagonal with rounded tips, also at the ends of open paths and for ridges much taller than they are apart (#2343); score unchanged (estimated) |
|
||||
| 2026-10-16 | minor | G2: Effect › Artistic › Smudge Stick, Sponge and Watercolor (live, with their documented options; original approximations); measured Photoshop filters 36/57 with Cutout, Neon Glow and Poster Edges, 21 remaining (19–31 h). |
|
||||
| 2026-10-16 | minor | G2: Effect › Sketch › Bas Relief, Charcoal, Photocopy and Stamp (black ink on white paper, sizes in points at any Document Raster Effects resolution, live preview); measured Photoshop filters 37/57 with Cutout, Neon Glow and Poster Edges, 20 remaining (18–30 h). |
|
||||
| 2026-10-16 | minor | G2: Effect › Sketch › Halftone Pattern (Size, Contrast, Pattern Type Circle, Dot or Line; black ink on white paper, Size in points at any Document Raster Effects resolution); measured Photoshop filters 30/57, 27 remaining (22–38 h). |
|
||||
| 2026-10-17 | minor | Colored Inflate: shared membrane normals and smooth RGB vector ramps replace flat polygon bands; channel groups are batched per solid-fill surface. White-light Inflate retains its original polygons, paint composition and seam joins; the 0.05 seam minimum is restored. Scores and hour estimates unchanged. |
|
||||
| 2026-10-16 | minor | 3D lighting: one to ten directional lights, a selectable light list, Add Light and Delete Light, independent settings and artboard gestures; ambient applied once and smooth RGB vector export; scores and hour estimates unchanged |
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+33
-14
File diff suppressed because one or more lines are too long
+444
-53
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
# Hardware parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** patch (web pen buttons/modifiers preserved, touch-only input left to eframe, session-only palm rejection; tool cursors painted into the window on Wayland, #1652, not run on Linux; borderless title-bar window on Windows, #1758, awaiting confirmation; a window surface larger than 8192 px keeps working, #1459 in part; Linux Display Server preference opens the window through XWayland, #1151) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-24 · **Change:** minor (pen pressure and the pen's eraser end on macOS, read from AppKit's tablet events, #852, not yet tried on a Mac; previously: patch: web pen buttons/modifiers preserved, touch-only input left to eframe, session-only palm rejection; tool cursors painted into the window on Wayland, #1652, not run on Linux; borderless title-bar window on Windows, #1758, awaiting confirmation; a window surface larger than 8192 px keeps working, #1459 in part; Linux Display Server preference opens the window through XWayland, #1151) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
|
||||
What hardware Illustrator uses, per platform, against VectorCraft. Illustrator's side is from its public
|
||||
documentation (GPU Performance, touch workspace, pen pressure pages); ours from the code
|
||||
@@ -11,7 +11,9 @@ 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. Pen input is pressure-only and Windows-first.
|
||||
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, 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 |
|
||||
|---|---|---|---|---|---|---|
|
||||
@@ -19,9 +21,10 @@ on huge documents. Pen input is pressure-only and Windows-first.
|
||||
| Multithreaded rendering off the UI thread | partial | yes | yes | yes | single-threaded | beyond Illustrator |
|
||||
| Hybrid-graphics GPU choice | automatic | power-saving GPU by default, Preferences › Graphics Processor | same; DX12 then OpenGL; no Vulkan unless asked (#545, #806) | the desktop's GPU | browser | done |
|
||||
| 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) | **no** (winit reports none, #852) | yes (pointer force via `WM_POINTER`) | **no** (winit reports none on X11 or Wayland); through XWayland (Preferences › Performance › Linux Display Server, #1151, #2028) the pen works as a mouse, and on Wayland it may not reach the window (#491, #764) | browser pointer events, with coalesced events; Apple Pencil on iPad (hover, palm rejection: fingers navigate after a pen contact until reload) | partial: 4–8 h for macOS and Wayland (winit 0.31) |
|
||||
| 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 | no | no | no | no | missing |
|
||||
| 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) |
|
||||
@@ -33,6 +36,7 @@ on huge documents. Pen input is pressure-only and Windows-first.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-24 | minor | macOS: an AppKit local event monitor reads each mouse and tablet event's pen pressure and the pen's end before the window gets it and writes them into the canvas's pen feed (#852): each pointer move of a stroke keeps its own pressure, mouse event coalescing is off, and the eraser end selects the Eraser tool. The mapping is unit-tested on every platform and the macOS code type-checks for `aarch64-apple-darwin`; not yet tried on a Mac with a tablet, so the score is unchanged (estimated) |
|
||||
| 2026-10-15 | patch | Web review: preserve pen buttons and modifiers; leave fingers to eframe before the first pen contact; remove persistent pen policy. Canvas regression tests cover intermediate moves and long-touch cancellation on native and web input. Physical-device QA pending. |
|
||||
| 2026-10-14 | minor | Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed); On Windows the custom title bar's window stays truly borderless, so the pointer lines up with what is drawn on systems where the hidden caption offset it (#1791, #1758; awaiting the reporter's confirmation); neither is confirmed on the hardware that reported it, so nothing is marked done |
|
||||
| 2026-10-13 | minor | Web: the shell reads pen and finger Pointer Events itself (pen pressure with coalesced events, Apple Pencil hover, palm rejection: after a pen contact fingers only navigate until reload; long press opens the context menu); score unchanged (estimated) |
|
||||
|
||||
@@ -32,11 +32,47 @@ 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)
|
||||
page describes the same warning for images over 1500 px). An image of more than 64 megapixels (up to
|
||||
192) is traced from a copy scaled down to 64 megapixels, and the trace says that too. A trace that would
|
||||
192) is traced from a copy scaled down to 64 megapixels, and the trace says that too. Flat Logo traces at
|
||||
most 1024 pixels on a side: a larger decoded raster is averaged down in whole-number blocks first (a 1025-pixel
|
||||
image is traced at 513, and a 16,000 × 12,000 image, decoded at 9,459 × 7,094, is traced at 946 × 710), and the trace names both sizes in `warnings`. Flat Logo gives that note in place
|
||||
of the slower-trace note for an image over 1500 pixels and in place of the 64-megapixel note. An image at most
|
||||
1024 pixels on a side is traced at its own size, also when the named `logoColors` differ slightly from the
|
||||
image's colors. Only an image with more than 150,000 different pixel values that are not exactly one traced
|
||||
color is averaged down further: the factor doubles until at most 150,000 remain or the working copy has at
|
||||
most 16,384 pixels (at most six times). A trace that would
|
||||
make more than 8,000,000 anchor points (2,000,000 on the web) is refused to protect memory, with advice:
|
||||
raise Noise, lower Paths or Colors, or pick a lower-fidelity preset. A trace keeps
|
||||
about 90 bytes an anchor; a 4000 × 3000 photo at High Fidelity Photo makes about 1.9 million.
|
||||
|
||||
@@ -57,7 +57,8 @@ nothing changes: 14, 72, 199 and 98 colours, the same paths, anchors and rendere
|
||||
|
||||
|
||||
## Large images
|
||||
Images over 1500 px on a side are traced with a warning that they trace more slowly (see
|
||||
Images over 1500 px on a side are traced with a warning that they trace more slowly (Flat Logo names
|
||||
the size it traced at instead; see
|
||||
[image-trace-adjustments.md](image-trace-adjustments.md)), and the anchor limit that guards memory is
|
||||
8,000,000 (2,000,000 on the web) instead of 2,000,000. Measured through `imageTrace.make` (release
|
||||
build, 4 cores, one trace at a time; memory is the peak allocated above the placed image, and what the
|
||||
|
||||
+33
-25
@@ -1,42 +1,45 @@
|
||||
# Localization parity
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (Bulgarian (`bg`) and Greek (`el`) catalogs complete under the tests, #1909, #2076; Finnish (`fi`) catalog added, complete under the tests; Swedish (`sv`) catalog added, complete under the tests; scores and estimates unchanged; Brazilian Portuguese (`pt-br`) catalog completed under the tests: menus, `tl!` literals and messages; 12-language score 71% → 74%; #1914) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** patch (missing translations never fail; `cargo xtask ci` prints the untranslated report)
|
||||
|
||||
Per-language status of VectorCraft's interface. How the catalogs work and how to add a language:
|
||||
[`development.md` › Localisation](development.md#localisation). Part of
|
||||
[target-app-parity.md](target-app-parity.md).
|
||||
|
||||
**How it is measured:** rows with a translation in each `crates/ui-egui/src/i18n/<code>.tsv`, counted on
|
||||
origin/main `01e165af`, except the Czech row updated against the current local catalog.
|
||||
The reference is the French catalog, which the tests hold complete: 3,416 interface
|
||||
origin/main `01e165af`, except the rows that give a count date, which were counted on `main` at `cb0a7d9` (2026-10-21).
|
||||
The reference is the French catalog, which was complete when counted: 3,416 interface
|
||||
strings (menus, command labels, `tl!` literals: panels, dialogs, tooltips, Preferences) plus 604 `@msg` status and
|
||||
error messages, 4,020 in all. A language's % is its rows over 4,020 (capped at 100% per part). The
|
||||
`complete_menus` flag and `COMPLETE_MESSAGES` list make `i18n::tests` fail if a complete language misses a
|
||||
string, so "full" here is enforced by tests. No language has help content: there is no translated (or English)
|
||||
in-app help, and the docs are English.
|
||||
error messages, 4,020 in all. A language's % is its rows over 4,020 (capped at 100% per part). A missing
|
||||
translation never fails a test or a build: the interface shows the English text until a translator adds the row.
|
||||
`cargo xtask ci` prints the untranslated report (`cargo xtask i18n-report` alone), which lists per language the
|
||||
strings missing from each catalog with the `complete_menus` flag and the messages missing from each language in
|
||||
the `COMPLETE_MESSAGES` list. Every non-English language marked "full" has the flag, and all of them except
|
||||
Bulgarian, Danish, Indonesian and Dutch are in the list. No language has help content: there is no translated (or
|
||||
English) in-app help, and the docs are English.
|
||||
|
||||
**Native review:** every catalog was written clean-room from the English meaning, most of it by agents
|
||||
(`bflatastic`), with community contributors adding or correcting (German by `dwetscher`, Japanese and Traditional
|
||||
Chinese by `imachus`). None is recorded as reviewed by a native speaker; the catalog headers ask for one.
|
||||
Chinese by `imachus`). Only Georgian is recorded as reviewed by a native speaker; the other catalog headers ask for one.
|
||||
|
||||
## The twelve key languages
|
||||
|
||||
| Language | Code | UI strings translated | Dialogs / tooltips / messages | Script support | Native review | Status | To `full` (h) |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| English | `en` | source, 100% | all | Latin | — | full | — |
|
||||
| Simplified Chinese (Mandarin) | `zh-hans` | 4,761 rows, 714 of them `@msg` (counted 2026-10-16): menus, `tl!` literals and messages complete (tests) | all | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) |
|
||||
| Simplified Chinese (Mandarin) | `zh-hans` | 5,102 rows, 791 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | all | CJK UI font from craft-fonts (BIZ UDPGothic) when embedded, else a system font; IME with marked text at the caret; vertical type partial ([type-parity.md](type-parity.md)) | no | full | — (review) |
|
||||
| Spanish | `es` | 4,009 / 4,020 (99.7%) | all (604 messages) | Latin | no | full | — (review) |
|
||||
| Hindi | `hi` | 0 | none | Devanagari: the document's text shapes (HarfRust), with tests of conjuncts, reph, pre-base matras, nukta forms and caret stops that run when an OpenType Devanagari font is installed, but the egui interface does no complex shaping | no | none | 20–30 |
|
||||
| Arabic | `ar` | 0 | none | document text: bidi layout and Paragraph Direction; interface: no right-to-left layout, no Arabic shaping in egui | no | none | 25–40 |
|
||||
| French | `fr` | 4,020 / 4,020 (100%) | all | Latin | no | full | — (review) |
|
||||
| Portuguese | `pt-br` | 4,707 catalog entries: menus, `tl!` literals and messages complete (tests); one|other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) |
|
||||
| Indonesian | `id` | 0 | none | Latin | no | none | 5–8 |
|
||||
| Portuguese | `pt-br` | 5,194 catalog entries (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals | all | Latin | contributor is a Brazilian Portuguese speaker; further native review welcome | full | — (review) |
|
||||
| Indonesian | `id` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one plural form | dialogs and tooltips all; 750 message rows (French has 796) | Latin | no | full | — (review) |
|
||||
| Japanese | `ja` | 4,004 / 4,020 (99.6%) | all | CJK UI font as above; Japanese IME; vertical type partial; Japanese crop marks and composition settings | no | full | — (review) |
|
||||
| German | `de` | 3,952 / 4,020 (98%; tests hold menus, literals and messages complete) | all (602 of 604 message rows; landed 2026-10-10) | Latin | contributor-written, not reviewed | full | — (review) |
|
||||
| Korean | `ko` | 4,151 rows: menus, `tl!` literals and messages complete (tests); one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) |
|
||||
| Vietnamese | `vi` | 4,225 rows: menus, `tl!` literals and messages complete (tests); one plural form; 3,548 plain + 13 `@plural` + 12 contextual + 652 `@msg` | all | Latin with Vietnamese diacritics (precomposed forms; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) |
|
||||
| German | `de` | 3,952 / 4,020 (98%; menus, literals and messages complete when counted) | all (602 of 604 message rows; landed 2026-10-10) | Latin | contributor-written, not reviewed | full | — (review) |
|
||||
| Korean | `ko` | 4,870 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | all | Hangul from an installed system font on the desktop (Apple SD Gothic Neo, Malgun Gothic, Noto Sans CJK KR…); the web build has no Korean UI font yet (craft-fonts); IME path exists | no (AI-drafted) | full | — (review; web font) |
|
||||
| Vietnamese | `vi` | 4,886 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form; 4,046 plain + 18 `@plural` + 28 contextual + 794 `@msg` | all | Latin with Vietnamese diacritics (precomposed forms; glyph tests pass) | contributor is a Vietnamese speaker; AI-assisted draft, further native review welcome | full | — (review) |
|
||||
|
||||
**12-language score: ~74%** (equal weights: 100 + 85 + 100 + 0 + 0 + 100 + 100 + 0 + 100 + 98 + 100 + 100, over 12).
|
||||
**12-language score: ~82%** (equal weights: 100 + 85 + 100 + 0 + 0 + 100 + 100 + 100 + 100 + 98 + 100 + 100, over 12).
|
||||
**To `full` for all twelve: ~50–85 h**, most of it Arabic (right-to-left interface) and Hindi (interface
|
||||
shaping). Each new Latin-script catalog is ~4,000 rows; the existing ones were written in a few agent hours each
|
||||
plus review. Native-speaker review is human work and isn't in the hours.
|
||||
@@ -45,19 +48,22 @@ plus review. Native-speaker review is human work and isn't in the hours.
|
||||
|
||||
| Language | Code | Translated | Status |
|
||||
|---|---|---|---|
|
||||
| Traditional Chinese (Taiwan) | `zh-hant` | 3,425 rows, menus and literals complete; messages in English (80% overall) | partial |
|
||||
| Traditional Chinese (Taiwan) | `zh-hant` | 4,892 rows, 790 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted (80% overall) | partial |
|
||||
| Italian | `it` | 4,011 / 4,020 | full |
|
||||
| Dutch | `nl` | 5,317 rows, 777 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full |
|
||||
| Danish | `da` | 5,279 rows, 750 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial; one/other plurals | full |
|
||||
| Russian | `ru` | 4,009 / 4,020 | full |
|
||||
| Ukrainian | `uk` | 4,009 / 4,020 (one/few/many plurals) | full |
|
||||
| Hungarian | `hu` | 4,524 rows: menus, `tl!` literals and messages complete (tests); one plural form | full |
|
||||
| Polish | `pl` | 4,510 rows (counted 2026-10-13): menus, `tl!` literals and messages complete (tests); one/few/many plurals | full; native review pending |
|
||||
| Finnish | `fi` | 4,770 rows: menus, `tl!` literals and messages complete (tests); one/other plurals, 668 `@msg` rows | full; AI-assisted, native proofreading pending |
|
||||
| Czech | `cs` | 4,411 catalog entries, including 636 `@msg` rows (measured locally 2026-10-11); menu, literal and message coverage enforced by tests | catalog complete; render-site gaps reduced; native review pending |
|
||||
| Swedish | `sv` | 4,770 rows, 668 of them `@msg` (counted 2026-10-15): menus, `tl!` literals and messages complete (tests); one/other plurals | full; native review welcome |
|
||||
| Bulgarian | `bg` | 4,910 rows, 703 of them `@msg` (counted 2026-10-16): menus, `tl!` literals and messages complete (tests) | full; AI-drafted, native review pending |
|
||||
| Greek | `el` | 4,880 rows, 709 of them `@msg` (counted 2026-10-16): menus, `tl!` literals and messages complete (tests) | full; native review welcome |
|
||||
| Hungarian | `hu` | 5,253 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one plural form | full |
|
||||
| Polish | `pl` | 5,130 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/few/many plurals | full; native review pending |
|
||||
| Finnish | `fi` | 5,318 rows (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals, 797 `@msg` rows | full; AI-assisted, native proofreading pending |
|
||||
| Czech | `cs` | 5,240 catalog entries, including 795 `@msg` rows (counted 2026-10-21); menus, literals and messages complete when counted | catalog complete; render-site gaps reduced; native review pending |
|
||||
| Swedish | `sv` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted; one/other plurals | full; native review welcome |
|
||||
| Bulgarian | `bg` | 5,245 rows, 763 of them `@msg` (counted 2026-10-21): menus and `tl!` literals complete when counted; its messages are partial | full; AI-drafted, native review pending |
|
||||
| Greek | `el` | 5,251 rows, 795 of them `@msg` (counted 2026-10-21): menus, `tl!` literals and messages complete when counted | full; native review welcome |
|
||||
| Georgian | `ka` | 5,200 rows (counted 2026-10-21): menus, panels, dialogs, plurals (one count-neutral form) and messages, tested complete; reviewed by a native speaker | full |
|
||||
|
||||
Eighteen non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
|
||||
Twenty-three non-English catalogs in all. Illustrator, as its public documentation lists, ships around two dozen
|
||||
interface languages (including Polish, Dutch, Turkish, the Nordic languages, Hungarian, and Middle East
|
||||
editions with Arabic and Hebrew); it doesn't ship Hindi, Indonesian or Vietnamese.
|
||||
|
||||
@@ -73,7 +79,7 @@ contextual radial adjective. PDF/Print mark styles use `mark-style`, so `Japanes
|
||||
The search placeholder, Home screen, toolbar/panel tooltips and Preferences already use lookups.
|
||||
No new `tl!("literal")` keys were needed: only `cs.tsv` adds 19 AI-draft rows and revises the gradient
|
||||
template. Every other complete catalog (`complete_menus`) has the same rows and a `Roman` row for the
|
||||
other mark style; `complete_catalogs_translate_render_site_labels` fails when one of them lacks a row.
|
||||
other mark style; the untranslated report lists a catalog that lacks one of them.
|
||||
Native review is still needed, and catalog coverage does not prove that every variable in the UI is
|
||||
translated.
|
||||
|
||||
@@ -86,6 +92,7 @@ font for the web build.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-23 | patch | A missing translation never fails a test or a build; one untranslated report, printed by `cargo xtask ci` and `cargo xtask i18n-report`, lists the missing strings per language; scores unchanged |
|
||||
| 2026-10-16 | minor | Bulgarian (`bg`, #1909) and Greek (`el`, #2076) catalogs added, complete under the tests; Swedish (#2139) and Finnish (#1994) rows for strings added since; eighteen non-English catalogs |
|
||||
| 2026-10-16 | minor | Finnish (`fi`) catalog added, complete under the tests (menus, literals, messages; one/other plurals); AI-assisted, native proofreading pending |
|
||||
| 2026-10-15 | minor | Swedish (`sv`) catalog added, complete under the tests (menus, literals, messages; one/other plurals); every `sv-*` locale resolves to it; AI-drafted, native review welcome; scores and estimates unchanged |
|
||||
@@ -99,5 +106,6 @@ font for the web build.
|
||||
| 2026-10-12 | minor | Korean (`ko`) catalog added, complete under the tests (menus, literals, messages); 12-language score 54% → 63%, hours 70–110 → 60–100 |
|
||||
| 2026-10-12 | minor | Polish (`pl`) catalog added, complete under the tests (menus, literals, messages; one/few/many plurals) |
|
||||
| 2026-10-12 | minor | Hungarian (`hu`) catalog added, complete under the tests |
|
||||
| 2026-10-12 | minor | Georgian (`ka`) catalog, complete |
|
||||
| 2026-10-11 | minor | Czech render-site follow-up: file-dialog filters, diagnostics, preset numbering, contributor tooltips/plurals, gradient grammar and contextual printer mark styles; estimates unchanged |
|
||||
| 2026-10-10 | major | First per-language table, measured from the catalog row counts against the complete French catalog |
|
||||
|
||||
+1320
-180
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,7 +1,7 @@
|
||||
# PDF backend
|
||||
|
||||
PDF writing uses `craft-pdf` from `storytold/craft-libs`, pinned to
|
||||
`ca50d63b773c99f96e75c77ce5a1d21e7c9cda0d`. Its `write` feature supplies Krilla configuration,
|
||||
`92d392cff132eaf53e13afff77f38b5c11b73aa0`. 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`
|
||||
|
||||
+147
-29
@@ -5,13 +5,16 @@ arbitrary machine code and cannot work in the browser build. Instead, plug-ins a
|
||||
run in a sandbox. One `.wasm` file works on macOS, Windows, Linux and the web. The design follows PhotoCraft's
|
||||
plug-in API (same sandbox, same manifest and parameter schema), extended for vector documents.
|
||||
|
||||
There are two kinds of plug-in:
|
||||
There are three kinds of plug-in:
|
||||
|
||||
- **Object filters** read the selected paths and compound paths (geometry, fills, strokes, gradients, opacity)
|
||||
and return the objects to keep, change or add. A run is one undo step. Use them for recolouring, path
|
||||
clean-up, halftones, generative patterns and the like.
|
||||
- **Live effects** rewrite one object's geometry each time it is drawn, from its appearance stack, like the
|
||||
built-in Distort & Transform effects. The art stays editable: move or reshape it and the effect follows.
|
||||
- **Exporters** write the whole document in a file format of their own, for pen plotters, cutting machines,
|
||||
laser cutters and the like. File › Export › Export As lists them after the built-in formats, and the host writes the
|
||||
bytes they return: a plug-in still never touches a file.
|
||||
|
||||
This page is the contract for plug-in authors and the reference for the host (`crates/plugins`, the
|
||||
`plugin.*` commands in `crates/engine/src/cmd/plugin.rs`).
|
||||
@@ -20,23 +23,33 @@ This page is the contract for plug-in authors and the reference for the host (`c
|
||||
|
||||
| Command | Params | What it does |
|
||||
|---|---|---|
|
||||
| `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, size, source}], "folder": report}` |
|
||||
| `plugin.list` | `{}` | `{"plugins": [{id, name, version, kind, author, description, params, paramsSchema, effect?, format?, extensions?, budget?, size, source}], "folder": report}` (`budget: "heavy"` for a [heavy filter](#heavy-filters)) |
|
||||
| `plugin.info` | `{"id"}` | One plug-in: the `plugin.list` entry plus `manifest`, `defaults` and `lastError` (a live effect's last failure). |
|
||||
| `plugin.run` | `{"id", "params"?: {…}, "ids"?: [...]}` (plug-in params may also be top-level keys) | Runs an object filter on the selection (or `ids`). One undo step; the result is selected. |
|
||||
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace": bool = true` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). |
|
||||
| `plugin.install` | `{"path": "/x/y.wasm"}` (desktop) or `{"dataBase64": "…", "name"?}`, `"replace": bool = true`, `"allowHeavy": bool = false` | Validates and installs a module; returns its `plugin.list` entry. The desktop app and `vectorcraft-cli` keep it in the `Plug-ins` folder next to the preferences, so it is installed again after a restart (#2541). A [heavy filter](#heavy-filters) installs only with `allowHeavy: true`, once the user agreed. |
|
||||
| `plugin.inspect` | as `plugin.install`: `{"path"}` or `{"dataBase64", "name"?}` | Validates a module without installing it; returns the `plugin.list` entry it would have. |
|
||||
| `plugin.remove` | `{"id"}` | Uninstalls, and deletes it from the `Plug-ins` folder. |
|
||||
| `plugin.reload` | `{"path"?: "folder"}` (default: the preference folder) | Installs every `*.wasm` in a folder; a bad module is reported and skipped. |
|
||||
| `plugin.reload` | `{"path"?: "folder"}` (default: the preference folder) | Installs every `*.wasm` in a folder; a bad module is reported and skipped, and so is a heavy filter (nobody was asked). |
|
||||
|
||||
Live effects are applied like any effect: `effect.apply {"effect": "plugin.<id>", "params": {…}}`, edited with
|
||||
`effect.setParams`, listed by `effect.list` (menu `Effect › Plug-ins`) and baked by Object › Expand Appearance.
|
||||
|
||||
An exporter adds a write-only format, `plugin.<id>`, which `document.formats` lists with its `plugin` id.
|
||||
`document.export {"format": "plugin.<id>", "params"?: {…}, "path"?}` writes it, and so does a `path` ending in one of
|
||||
its extensions that no built-in format has; `selectedOnly` hands it the selection, as for the built-in formats.
|
||||
|
||||
In the app, installed filters appear under **Object › Plug-ins**, next to **Install Plug-in…** and **Reload
|
||||
Plug-ins**; live effects under **Effect › Plug-ins** and in the Appearance panel's effect menu. A plug-in with
|
||||
parameters gets a dialog generated from its schema (numbers, whole numbers, checkboxes, dropdowns) with a live
|
||||
preview; without parameters it runs at once. **File › Open** of a `.wasm` file installs it too (on the web this is
|
||||
how plug-ins get in).
|
||||
parameters gets a dialog generated from its schema (numbers, whole numbers, checkboxes, dropdowns, text and lists
|
||||
of numbers, labelled, explained and sorted into sections as its manifest says) with a live preview; without
|
||||
parameters it runs at once. Parameters too many for the window's height scroll inside the dialog, which keeps its
|
||||
heading and buttons in view. Exporters are formats of **File › Export › Export As**, where Use
|
||||
Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the
|
||||
same kind once the file is picked, and its Export button writes the file. **File › Open** of a `.wasm` file installs
|
||||
a plug-in too (on the web this is how plug-ins get in). A heavy filter asks before it is installed, and runs in the
|
||||
background ([below](#heavy-filters)).
|
||||
|
||||
**Preference folder.** Edit › Preferences › Performance & Storage › *Additional Plug-ins Folder*: when set, every
|
||||
**Preference folder.** Edit › Preferences › Plug-ins & Scratch Disks › *Additional Plug-ins Folder*: when set, every
|
||||
`*.wasm` in that folder (not recursive) is installed at startup and whenever the preference changes (desktop builds;
|
||||
the web build installs from bytes).
|
||||
|
||||
@@ -56,11 +69,37 @@ The host is [`wasmi`](https://github.com/wasmi-labs/wasmi), a pure-Rust WebAssem
|
||||
| Module size | 32 MiB | rejected |
|
||||
| Linear memory | 512 MiB per instance (input and parameters: at most half) | `memory.grow` returns -1; oversized input or initial memory fails |
|
||||
| Output | 64 MiB of JSON | `Limit("output size")` |
|
||||
| Instructions (fuel) | 50 M per call + 4,000 per input byte for `vc_run` | `Limit("instruction budget")` |
|
||||
| Wall clock | 60 s per filter run, 5 s per live-effect run (desktop; checked every 20 M instructions) | `Limit("time budget")` |
|
||||
| Instructions (fuel) | 50 M per call + 4,000 per input byte for `vc_run`; a [heavy filter](#heavy-filters) 1 G + 4,000 per byte; an export 10 G + 4,000 per byte | `Limit("instruction budget")` |
|
||||
| Wall clock | 60 s per filter run, 5 s per live-effect run, 300 s per heavy filter run or export (desktop; checked every 20 M instructions) | `Limit("time budget")` |
|
||||
| Call depth | 1,024 frames | trap |
|
||||
| Module shape | wasmi's strict limits (≤ 10,000 functions, 1 memory, …) | rejected |
|
||||
|
||||
An export runs where the export runs: with Background Export on, on the export's worker, whose Cancel stops the
|
||||
plug-in between two of those 20 M-instruction slices; nothing is written then.
|
||||
|
||||
### Heavy filters
|
||||
|
||||
Some filters do heavy but legitimate work, such as routing a plotter's pen, packing shapes onto a sheet or running a
|
||||
simulation, which a minute and 50 M instructions don't cover. Such a filter declares `"budget": "heavy"` in its
|
||||
manifest (only a filter may: a live effect runs whenever the document is drawn, and an exporter has an export's
|
||||
budget). Each of its runs gets 1 G instructions (20 times a light run's) plus the 4,000 per input byte, and 300 s
|
||||
(`vectorcraft_plugins::{HEAVY_FUEL, HEAVY_WALL_TIME_MS}`). Its output can't add to the budget: the host learns how
|
||||
much there is only once the run has ended.
|
||||
|
||||
- **Installing asks first.** Install Plug-in… and File › Open show what the filter asks for, and install it once the
|
||||
user clicks OK; `plugin.install` refuses it without `allowHeavy: true`, which an agent passes once the user agreed
|
||||
(`plugin.inspect` tells it beforehand). A filter kept in the `Plug-ins` folder was agreed to and is installed again
|
||||
at startup. The Additional Plug-ins Folder skips heavy filters with a note in its report, since loading a folder
|
||||
asks nobody.
|
||||
- **It runs in the background.** Its dialog has no Preview. OK (or the menu item, without parameters) runs it on a
|
||||
thread of its own while editing goes on, shown in the status bar and, after a moment, in a progress window with
|
||||
Cancel (also Esc) and Continue in Background (`plugin.cancelRuns`, `plugin.hideRuns`). When it is done its result is
|
||||
applied as `plugin.run` with the same parameters and objects, as one undo step, unless the document or the plug-in
|
||||
changed meanwhile: then nothing changes, and the status bar says to run it again. On the web, which has no threads,
|
||||
it runs at once; the instruction budget alone bounds it there.
|
||||
- `plugin.run` itself runs a heavy filter where it is called, as it runs any filter: an MCP client waits for it, and
|
||||
its `notifications/cancelled` stops it.
|
||||
|
||||
Every run gets a **fresh instance**, so no state survives between calls. Traps, exhausted budgets, malformed
|
||||
modules, bad manifests, out-of-range pointers, negative return codes and invalid output all become errors, and the
|
||||
document is left untouched: a filter's output is checked completely before one edit applies it. A live effect that
|
||||
@@ -70,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.
|
||||
|
||||
@@ -98,21 +142,50 @@ pointers are offsets into the module's memory.
|
||||
"author": "Example",
|
||||
"description": "What it does.",
|
||||
"params": {
|
||||
"amount": {"type": "number", "min": 0, "max": 100, "default": 50},
|
||||
"steps": {"type": "int", "min": 1, "max": 16, "default": 4},
|
||||
"amount": {"type": "number", "min": 0, "max": 100, "default": 50, "unit": "%"},
|
||||
"steps": {"type": "int", "min": 1, "max": 16, "default": 4, "label": "Steps per curve"},
|
||||
"closed": {"type": "bool", "default": false},
|
||||
"mode": {"type": "choice", "options": ["soft", "hard"], "default": "soft"}
|
||||
"mode": {"type": "choice", "options": ["soft", "hard"], "default": "soft",
|
||||
"help": "Soft keeps the corners round; hard keeps them sharp."}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `id`: 1–64 characters from `A-Z a-z 0-9 . _ -` (reverse-DNS recommended). Installing a module with the same id
|
||||
replaces the old one. A live effect's effect id is `plugin.<id>`.
|
||||
- `name`: the menu label (1–64 printable characters). `kind`: `"filter"` (object filter) or `"effect"` (live effect).
|
||||
- `name`: the menu label, or an exporter's format name in Export As (1–64 printable characters). `kind`: `"filter"`
|
||||
(object filter), `"effect"` (live effect) or `"exporter"`. Exporters came after the first ABI v1 hosts, which refuse
|
||||
the kind when the plug-in is installed.
|
||||
- `budget` (filters only): `"light"` (the default) or `"heavy"`, for work that takes minutes ([heavy
|
||||
filters](#heavy-filters)). Hosts from before it ignore the key, so there the filter gets a light run's budget.
|
||||
- `extensions` (exporters only, and required): 1–8 file extensions, lower-case letters and digits without the dot
|
||||
(`["plt"]`); the first is the one Export As gives the file. A built-in format keeps an extension both list (`svg`):
|
||||
`document.export` then writes the exporter's format only when asked for by its format id.
|
||||
- `params`: in dialog order. Names are `[A-Za-z0-9_]`, not starting with `_`, and not `id`, `ids`, `params` or
|
||||
`preview`. The host validates the user's values before calling: missing values take the default, numbers are
|
||||
clamped to `min..max` (ints rounded), a wrong type or an unknown choice is an error. Numeric defaults
|
||||
are also clamped to their range, including the implicit zero default of an `int` parameter.
|
||||
are also clamped to their range, including the implicit zero default of an `int` parameter. Besides `number`,
|
||||
`int`, `bool` and `choice` there are `string` (`{"type": "string", "default": "", "maxLength": 256}`: a text field;
|
||||
longer text is cut to `maxLength`, at most 4,096 characters) and `numberList` (`{"type": "numberList", "min": 0,
|
||||
"max": 360, "default": [45, 135], "minItems": 1, "maxItems": 16}`: numbers typed separated by commas or spaces,
|
||||
each clamped to `min..max`; a list of too few or too many is an error, and `maxItems` is at most 256). A plug-in
|
||||
declares up to 256 parameters, at most 64 of them outside groups.
|
||||
|
||||
Each parameter may also say how dialogs show it. Hosts from before these keys ignore them, so a plug-in that
|
||||
declares them still installs there:
|
||||
|
||||
| Key | What the dialog does with it |
|
||||
|---|---|
|
||||
| `label` | Labels the field (1–64 characters). Without one, the label is the name spaced out (`rowSpacing` → Row Spacing). |
|
||||
| `unit` | Shows the unit the value is in after it (1–16 characters, such as `mm` or `°`). Nothing is converted: the plug-in gets the number as typed. |
|
||||
| `group` | Puts the parameter in a section that folds (1–64 characters). Parameters outside any group come first, then each group in the order it first appears. |
|
||||
| `help` | Shows what the parameter does when the pointer rests on its label (1–1,000 characters; line breaks allowed). |
|
||||
| `helpUrl` | Opens this `https://` page (at most 512 bytes) from a button after the field. |
|
||||
| `visibleWhen` | `{"pattern": ["lines", "crosshatch"], "outline": [true]}` shows the parameter only while each parameter it names (a checkbox or a choice declared before it) has one of the values listed and is shown itself. |
|
||||
|
||||
A hidden parameter still goes to the plug-in with its value: what the dialog showed never changes a run. The
|
||||
filter, live-effect and exporter dialogs use all of these keys. The Appearance panel's editor of a plug-in effect's
|
||||
parameters uses `label`, `unit`, `help` and `visibleWhen`, and lists the parameters without sections or help buttons.
|
||||
|
||||
### Input
|
||||
|
||||
@@ -120,7 +193,15 @@ pointers are offsets into the module's memory.
|
||||
order (bottom first): the selected ones and those inside selected groups and layers; hidden, locked, guide and clipping
|
||||
paths are left out, and so are other kinds of object (type, images, symbols, live blends…), which a filter never
|
||||
touches. With nothing selected the list is empty (a generator then adds its own objects). A live effect gets one
|
||||
object, holding only geometry (`type`, `path`, `fillRule`, `bounds`).
|
||||
object: its geometry (`type`, `path`, `fillRule`, `bounds`) and its `fills` and `strokes`, as a filter gets them (hosts
|
||||
from before these give geometry only). The paints are read only: an effect may draw as wide as a stroke or use a
|
||||
fill's colour to decide its shape, but it changes only the geometry.
|
||||
|
||||
An **exporter** gets something else: the document as a native `.vectorcraft` file's JSON, as `document.save` writes it
|
||||
but without indentation (the format is documented in [`crates/format/src/lib.rs`](../crates/format/src/lib.rs)). That is every layer, object,
|
||||
appearance and effect record, artboard, swatch and symbol, so an exporter reads whatever the document holds; it checks
|
||||
`version`, since a later host may write a newer one. Images of 8 KiB or more are listed in `images` with their MIME
|
||||
type and `blob` but without their data, which follows the JSON in a saved file; smaller ones keep their base64 `data`.
|
||||
|
||||
```json
|
||||
{"objects": [
|
||||
@@ -134,7 +215,9 @@ object, holding only geometry (`type`, `path`, `fillRule`, `bounds`).
|
||||
"strokes": [{"paint": {"type": "gradient", "gradient": {"kind": "Linear", "stops": [
|
||||
{"offset": 0, "color": {"model": "gray", "k": 0}},
|
||||
{"offset": 1, "color": {"model": "cmyk", "c": 0, "m": 0.5, "y": 1, "k": 0}}]}, "angle": 0},
|
||||
"width": 2}],
|
||||
"width": 2, "join": "round", "cap": "butt", "miterLimit": 10}],
|
||||
"effects": [{"id": "distort.roughen", "params": {"detail": 10, "points": "smooth", "relative": true, "seed": 0,
|
||||
"size": 5}, "visible": true}],
|
||||
"opacity": 1,
|
||||
"bounds": [10, 0, 75, 60]}
|
||||
]}
|
||||
@@ -143,12 +226,16 @@ object, holding only geometry (`type`, `path`, `fillRule`, `bounds`).
|
||||
- `type`: `"path"` or `"compound"` (a compound path: `path` holds its members' subpaths, painted as one).
|
||||
- `path`: VectorCraft's own path form. Each subpath is a run of anchors; `p` is the anchor, `in` / `out` its handles
|
||||
(absent: no handle), `kind` `"Smooth"` when the handles stay collinear. Coordinates are points, y down.
|
||||
- `fills`, `strokes`: the object's fills and strokes in paint order (bottom first); `null` is no paint. Paints use the
|
||||
- `fills`, `strokes`: the object's fills and strokes in paint order (bottom first); `null` is no paint. A stroke has its
|
||||
`paint`, `width` in points, `join` (`"miter"`, `"round"` or `"bevel"`), `cap` (`"butt"`, `"round"` or `"square"`)
|
||||
and `miterLimit`. Paints use the
|
||||
native file format's form: `{"type": "solid", "color", "swatch"?, "tint"?}`, `{"type": "gradient", "gradient":
|
||||
{"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, midpoint}]}, "geom"?, "angle",
|
||||
{"kind": "Linear"|"Radial"|"Freeform", "stops": [{offset, color, opacity, midpoint}], "dither"?, "method"?: "Perceptual"}, "geom"?, "angle",
|
||||
"freeform"?}`, `{"type": "pattern", "pattern", "xf"?}`. Colours are `{"model": "rgb", r, g, b}`, `{"model": "cmyk",
|
||||
c, m, y, k}`, `{"model": "gray", k}` (ink: 0 white, 1 black) or `{"model": "lab", l, a, b}`; components 0–1 except
|
||||
Lab's.
|
||||
- `effects` (filters): the object's own effects in order, as its effect records: `id` (as `effect.list` lists it, or
|
||||
`plugin.<id>` for a live-effect plug-in), `params` and `visible`. Its fills' and strokes' own effects aren't listed.
|
||||
- `bounds`: `[x0, y0, x1, y1]`, the geometric bounds (read only).
|
||||
|
||||
### Parameters
|
||||
@@ -157,12 +244,17 @@ object, holding only geometry (`type`, `path`, `fillRule`, `bounds`).
|
||||
|
||||
```json
|
||||
{"amount": 50, "mode": "soft",
|
||||
"_context": {"mode": "filter", "bounds": [10, 0, 75, 60], "artboard": [0, 0, 612, 792]}}
|
||||
"_context": {"mode": "filter", "bounds": [10, 0, 75, 60], "artboard": [0, 0, 612, 792], "documentData": {"pens": 4}}}
|
||||
```
|
||||
|
||||
`mode` is `"filter"` or `"effect"`; `bounds` the selection's geometric bounds (a live effect: the reference box of the
|
||||
effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard under the
|
||||
selection's centre, else the first.
|
||||
`mode` is `"filter"`, `"effect"` or `"export"`; `bounds` the selection's geometric bounds (a live effect: the reference
|
||||
box of the effect, the object's bounds or those of the previous effect's result); `artboard` (filters) the artboard
|
||||
under the selection's centre, else the first; `documentData` (filters) what the plug-in last kept with the document
|
||||
(`null` until it keeps something; see Output). A live effect's `item` says where it sits: `"object"` (one of the
|
||||
object's effects), or `{"fill": i}` / `{"stroke": i}` (in the own effects of the object's `i`th fill or stroke,
|
||||
counted from the bottom as in `fills` and `strokes`). An export's `_context` is `{"mode": "export", "extension": "plt",
|
||||
"name": "Poster"}` instead: the extension of the file being written (one the exporter lists) and its name without the
|
||||
extension (the document's name when the export is not given a path), which some formats keep in their header.
|
||||
|
||||
### Output
|
||||
|
||||
@@ -173,21 +265,43 @@ For an **object filter**, the output is the new state of the input objects:
|
||||
|
||||
- An object with the `id` of an input object updates it; keys it leaves out keep their value (`{"id": 12}` keeps the
|
||||
object as it is). `fills` and `strokes` set the object's fills and strokes in order: extra entries add fills (above
|
||||
the last one) or strokes, missing ones are removed; each stroke keeps its weight unless `width` is given (a new one: 1 pt).
|
||||
the last one) or strokes, missing ones are removed; each stroke keeps its weight, join, cap and miter limit unless
|
||||
`width`, `join`, `cap` or `miterLimit` are given (a new one: 1 pt, miter join, butt cap, miter limit 10).
|
||||
- `effects` replaces the object's own effects, in order: each `{id, params?, visible?}`. A record the object already had
|
||||
stays as it is; a new one is admitted as `effect.apply` admits it: an effect `effect.list` lists or an installed
|
||||
live-effect plug-in, with parameters it takes (filled in to their defaults; a plug-in's checked against its
|
||||
manifest), at most one 3D effect (its fills' and strokes' own effects counted), which must fit the object, and a
|
||||
Glass `textureImage` as Glass stores it (base64 of a PNG: a plug-in can't have the host read a file). A tool can
|
||||
then set up objects for a live effect in one step.
|
||||
- An object without an `id` is new and needs a `path`. It goes just above the object listed before it, or, listed
|
||||
before any input object, just below the first one listed after it. New objects take a path's defaults: no fills or
|
||||
strokes, full opacity.
|
||||
- Input objects missing from the output are deleted. The order of the input objects themselves doesn't change.
|
||||
- `type` turns a path into a compound path (one member per subpath) or back; `name: ""` clears the name.
|
||||
- A `path` returned as it was keeps a live shape live (a rectangle keeps its corner widgets); a changed one makes it a
|
||||
plain path.
|
||||
|
||||
The host validates everything before the document changes: coordinates must be finite and within ±4,000,000 pt, at
|
||||
most 100,000 objects and 4,000,000 anchors, 256 fills, strokes or gradient stops; colours, opacities and offsets are
|
||||
clamped into range, stroke weights to 0–1,000 pt; a pattern must exist in the document. A paint returned unchanged
|
||||
keeps its swatch link; any other paint is unlinked.
|
||||
clamped into range, stroke weights to 0–1,000 pt and miter limits to 1–500; a pattern must exist in the document. A
|
||||
paint returned unchanged keeps its swatch link; any other paint is unlinked. One refused effect record refuses the
|
||||
whole output.
|
||||
|
||||
A filter's output may also hold `documentData`: any JSON of at most 1 MiB, which replaces what the plug-in keeps with
|
||||
the document (`null` removes it), in the same undo step as its objects. Settings that belong to the whole document
|
||||
rather than to one object go there, such as the pens a plotter holds. A filter reads and writes only its own data. It
|
||||
is the document's foreign data, as the perspective grid is: the native file keeps it in `document.unknown.pluginData`
|
||||
under the plug-in's id, and hosts from before `documentData` keep it when they save. An exporter is given the whole
|
||||
document, so it can read what every plug-in keeps there, not only its own. A live effect isn't given it: its geometry
|
||||
is worked out from the object alone.
|
||||
|
||||
For a **live effect**, the geometry of every returned object's `path` (their subpaths, in order) replaces the
|
||||
object's; the rest is ignored (the object keeps its own appearance). An empty list removes the geometry.
|
||||
|
||||
An **exporter** returns `{"dataBase64": "…", "warnings"?: ["…"]}`: the file, in base64, which the host writes as it
|
||||
is, and what the export left out or changed, for the user (at most 100 warnings of at most 500 characters each, shown
|
||||
with the export's own). A decline (`{"error": …}`), a missing `dataBase64` or data that isn't base64 writes nothing.
|
||||
|
||||
## Writing a plug-in in Rust
|
||||
|
||||
[`crates/plugins/example`](../crates/plugins/example) is a complete object filter, **Desaturate**: it moves every
|
||||
@@ -203,18 +317,22 @@ The script also refreshes `crates/plugins/tests/fixtures/desaturate.wasm`, the t
|
||||
The crate is not a workspace member. Any language that targets core WebAssembly works (C, Zig, AssemblyScript,
|
||||
WAT…), as long as the module has no imports: in Rust, `std` on `wasm32-unknown-unknown` is fine as long as it never
|
||||
touches WASI. `vectorcraft_plugins::wat::module` is the skeleton of a plug-in in WebAssembly text, which the tests
|
||||
build their plug-ins from.
|
||||
build their plug-ins from; `wat::echo_exporter` is an exporter whose file is its input or its parameters (with a
|
||||
base64 encoder written out in WebAssembly text), which the tests read back what the host hands a plug-in with, and
|
||||
`wat::when_given` a plug-in whose output depends on whether its input or parameters hold a given text, which shows in a
|
||||
live effect's geometry.
|
||||
|
||||
## Performance
|
||||
|
||||
The interpreter costs roughly 10–100× native speed per instruction. Filters handle a selection in one call, so a
|
||||
plug-in that parses and writes JSON with `serde_json` takes milliseconds for hundreds of paths. Live effects run once
|
||||
per change of their input (results are cached), not per frame.
|
||||
per change of their input, parameters or paints (results are cached), not per frame.
|
||||
|
||||
## Not in v1
|
||||
|
||||
- Tool plug-ins (pointer events on the canvas, a guide line or rectangle, geometry back), panel plug-ins, host
|
||||
callbacks (progress, cancel), plug-in file formats.
|
||||
callbacks (a plug-in reporting progress or asking whether it was cancelled), importers (plug-in formats that open
|
||||
files).
|
||||
- Live effects that change paint (they reshape geometry; an object filter recolours), and filters on type, images or
|
||||
symbols.
|
||||
- Apply Last Effect and Last Effect work for plug-in effects; there is no "last filter" for object filters yet.
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# Print a large poster across multiple pages
|
||||
|
||||
## Overview
|
||||
|
||||
Use tiled printing to spread a large poster, sign or sewing pattern across several sheets of A4,
|
||||
Letter or other paper. VectorCraft divides the drawing into numbered tiles at the scale you choose.
|
||||
Overlap repeats part of the drawing on adjacent sheets to help you align them after printing.
|
||||
|
||||
## Print a poster on multiple sheets
|
||||
|
||||
1. Open your drawing in VectorCraft. For a full-size pattern, check that the drawing has the intended
|
||||
dimensions before printing.
|
||||
2. Choose **File → Print…** to open VectorCraft's **Print** dialog. In **Printer**, select your printer
|
||||
or **PDF File** to save the tiled job as a PDF.
|
||||
3. In **General**, under **Artboards**, choose **All** or **Range:** to select the artboards to print.
|
||||
For example, use **Range:** `1` for the first artboard. Choose **Ignore Artboards (all the art as
|
||||
one page)** only if you want to tile the bounds of all the artwork instead of individual artboards.
|
||||
4. Under **Media**, set **Size** to the paper loaded in the printer, such as **A4**, **Letter** or a Japanese paper (**B5 (JIS)**, **Japanese Postcard**, **Japanese Envelope #3**).
|
||||
Choose **Custom** to enter **Width** and **Height**. **Auto-Rotate (turn the paper to each artboard)**
|
||||
is on by default; turn it off to choose a fixed **Orientation**, such as **Portrait** or **Landscape**.
|
||||
5. In **Advanced**, set **Unprintable margin** to allow for the edges your printer cannot print.
|
||||
VectorCraft uses one value for all four edges; use a value that covers the printer's largest
|
||||
required margin. The default is zero, and VectorCraft does not read this value from the printer.
|
||||
6. Return to **General**. Under **Options**, set **Scaling** to **Tile Imageable Areas** or
|
||||
**Tile Full Pages** (see [Choose a tiling mode](#choose-a-tiling-mode)). Set **Scale**, both **W**
|
||||
and **H**, to `100%` to keep the drawing's size. Use the same percentage in both fields to keep
|
||||
its proportions; larger percentages enlarge the drawing and can require more sheets.
|
||||
7. Set **Overlap** to repeat a strip along adjoining tiles. For example, `10 mm` gives room for
|
||||
alignment. Length fields accept units such as `mm` and `in`. Overlap must be less than half
|
||||
the smaller tile dimension; increasing it can increase the sheet count.
|
||||
8. Leave **Tile Range** empty initially. Use the **Previous page** and **Next page** arrows beneath
|
||||
the preview to inspect each sheet. The label identifies its artboard and tile; the preview also
|
||||
shows the paper size and scale. Check **Summary** for warnings or invalid settings.
|
||||
9. Keep **Tile Range** empty to print every tile, or enter a range such as `1-3, 5` to print a subset.
|
||||
Check the preview again after changing the range.
|
||||
10. Click **Print**. With a printer selected, VectorCraft sends the laid-out job to the system's print
|
||||
service. With **PDF File** selected, it asks where to save the PDF, with one page per printed
|
||||
tile for each copy in composite output. **Done** keeps the print settings with the document
|
||||
without printing; save the document to retain them for next time.
|
||||
|
||||
The tiling controls belong to VectorCraft's **Print** dialog on Windows, macOS and Linux.
|
||||
**Setup…**, beside **Printer**, opens the system's printer settings, whose interface varies by
|
||||
platform. Set tiling in VectorCraft's **Scaling** list. Keep the printer's paper settings consistent
|
||||
with **Media**. In the web app, printing goes through the browser's print dialog; **PDF File**
|
||||
downloads the PDF.
|
||||
|
||||
For a paper proof, select **PDF File** and click **Print**, then open the saved PDF to inspect its
|
||||
pages before sending it to a printer. When printing that PDF, keep its page size and scale rather
|
||||
than applying another fit-to-page reduction. For a sewing pattern, measure a known length on a
|
||||
printed sheet before printing the full job.
|
||||
|
||||
## Choose a tiling mode
|
||||
|
||||
| Scaling | How it divides the drawing | When to use it |
|
||||
|---|---|---|
|
||||
| **Tile Full Pages** | Each tile uses the full paper width and height. Adjacent tiles advance by that size minus **Overlap**. | Use when you need a grid based on whole sheets, such as with a printer that supports the required borderless paper setting. On a printer with unprintable edges, artwork near tile edges can be lost unless sufficient overlap covers those gaps. |
|
||||
| **Tile Imageable Areas** | Each tile uses the paper area inside **Unprintable margin**. Adjacent tiles advance by that smaller size minus **Overlap**. | Use for printers with unprintable edges. Set the margin in **Advanced** so the tiles account for those edges; this can require more sheets than full-page tiling. |
|
||||
|
||||
With **Unprintable margin** set to zero, the two modes use the same tile dimensions. Neither mode
|
||||
makes a printer capable of borderless printing. **Fit to Page** scales an artboard onto a single
|
||||
sheet; choose a tile mode to keep a large drawing spread across sheets.
|
||||
|
||||
## Check the layout on the canvas
|
||||
|
||||
1. Set up tiling in **File → Print…**, then click **Done** to keep the settings.
|
||||
2. Choose **View → Show Print Tiling**. The canvas shows each sheet's paper edge and a dashed,
|
||||
numbered imageable area. Tiles excluded by **Tile Range** appear dimmer. The overlay uses the
|
||||
saved print settings; it does not print anything.
|
||||
3. Select the **Print Tiling Tool**. In the default toolbar, press and hold the **Hand Tool** button
|
||||
in the **Navigate** group to open its tool list, then choose **Print Tiling Tool**. The tool also
|
||||
shows the overlay while it is active.
|
||||
4. Drag on the canvas to position the grid. The pointer places the top-left corner of a page's
|
||||
imageable area and snaps near artboard edges. Tiles extend left and up as needed to cover the
|
||||
drawing, so that page may not be tile 1 after the move. Reopen **File → Print…** to check the
|
||||
resulting pages and tile numbers. Dragging the dialog's preview does not reposition tiled pages.
|
||||
|
||||
To restore the default grid position, double-click on the canvas with **Print Tiling Tool** active,
|
||||
or double-click the tool's toolbar button. Alternatively, reopen **File → Print…** and click
|
||||
**Reset** beneath **Placement** when the dialog says **The Print Tiling tool placed the pages.**
|
||||
Click **Done** to keep that change. **View → Hide Print Tiling** hides the overlay after switching
|
||||
to another tool.
|
||||
|
||||
## Print only selected tiles
|
||||
|
||||
**Tile Range** uses numbers starting at 1, across each row from left to right, then down.
|
||||
For a grid with three columns and two rows, the first row is `1, 2, 3` and the second is `4, 5, 6`.
|
||||
Enter `2, 5` to print the middle column, or `1-3, 5` to print the first row and tile 5.
|
||||
Leave the field empty to print all tiles.
|
||||
|
||||
**Range:** under **Artboards** selects drawings; **Tile Range** selects sheets within each chosen
|
||||
drawing. Tile numbering restarts for each artboard, and the same tile range applies to each one.
|
||||
To reprint one damaged sheet of one artboard, select that artboard with **Range:** and its tile
|
||||
with **Tile Range**. A tile number must exist on every selected artboard. Changing paper, scale,
|
||||
margin, overlap or grid position can change the grid and its numbering; check again before reprinting.
|
||||
|
||||
## Assemble the printed sheets
|
||||
|
||||
Lay out the sheets in tile order. Align repeated artwork in the overlap, trim unprinted edges as
|
||||
needed, and tape or glue the sheets together. Print a pair of adjacent tiles first to check the
|
||||
scale, margins and overlap before committing to a large job.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **The preview shows only one page.** It displays one sheet at a time. Use the arrows and check
|
||||
the count beneath it. If the count is one, check **Tile Range**, the chosen artboards, paper size
|
||||
and scale: the drawing may fit on one tile.
|
||||
- **The image shrinks to one sheet.** Change **Scaling** from **Fit to Page** to a tile mode and
|
||||
check both scale percentages. Also check for another fit-to-page setting when printing a saved PDF.
|
||||
- **Artwork is missing along sheet edges.** Check the printer's unprintable margins. Use
|
||||
**Tile Imageable Areas** with a suitable **Advanced → Unprintable margin**, or allow enough
|
||||
overlap in full-page tiling to cover the missing edges. Check that the physical paper matches
|
||||
**Media**; a zero margin does not remove a printer's hardware limits.
|
||||
- **No tiling boundaries appear on the canvas.** Keep the settings with **Done**, then enable
|
||||
**View → Show Print Tiling** or select **Print Tiling Tool**. If the settings are invalid, the
|
||||
overlay has no pages to draw; reopen **Print** and check **Summary**.
|
||||
- **The job has the wrong number of pages.** Check **Artboards**, **Tile Range**, **Copies** and
|
||||
**Skip Blank Artboards**. Paper size, orientation, scale, margins and overlap affect the grid.
|
||||
Under **Output**, **Mode → Separations** produces a page per enabled ink; use **Composite** for
|
||||
an ordinary poster. The dialog preview shows one copy; the saved PDF includes all copies.
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Printing and PDF file output](mcp.md#printing) — command reference.
|
||||
- [Print tiling](mcp.md#print-tiling) — overlay and positioning command reference.
|
||||
- [Print settings](mcp.md#print) — paper, scaling and output settings reference.
|
||||
+14
-3
@@ -128,11 +128,15 @@ to the target triple, so host build scripts aren't affected.
|
||||
inside the signed bundle), for an AppImage next to the `.AppImage` file (`$APPIMAGE`; the mounted
|
||||
executable's own folder is read-only). Flatpak isn't supported: `/app` is read-only and the
|
||||
sandbox has no folder "beside the app", so a Flatpak install always uses its own config folder.
|
||||
- `vectorcraft.wxs` (WiX) is a per-machine install into Program Files with a Start Menu shortcut
|
||||
- `vectorcraft.wxs` (WiX) is a dual-scope package (per-user by default into
|
||||
`%LOCALAPPDATA%\Programs\ArtCraft\VectorCraft`, per-machine into `Program Files\ArtCraft\VectorCraft` (`Program Files (x86)` for the 32-bit build),
|
||||
see `packaging/windows/INSTALLATION.md`) with a Start Menu shortcut
|
||||
and an App Paths entry (Win+R `vectorcraft`). Same-version upgrades are allowed, so release
|
||||
candidates replace each other.
|
||||
- Double-clicking the MSI opens a setup wizard (WixUI_InstallDir without the licence page):
|
||||
Welcome, install folder (remembered for upgrades in `HKLM\Software\VectorCraft\InstallDir`),
|
||||
- Double-clicking the MSI opens a setup wizard (WixUI_InstallDir without the licence page; the scope page appears only for a new full-UI install, not for
|
||||
maintenance, uninstall or reduced and silent runs):
|
||||
a scope page ("Only for me" or "Everyone on this computer"), Welcome, install folder (remembered for
|
||||
upgrades in `HKLM` or `HKCU\Software\VectorCraft\InstallDir` according to scope),
|
||||
Shortcuts (a ticked "Create a desktop shortcut" box), Ready, a progress page, and a Finish page
|
||||
that says where to find the app, with a ticked "Launch VectorCraft" box. The pages name the full
|
||||
version (`vectorcraft.en-us.wxl`). `package.ps1` draws the wizard's banner and side bitmaps from
|
||||
@@ -159,6 +163,13 @@ to the target triple, so host build scripts aren't affected.
|
||||
`.tar.gz` tree (`bin/`, `share/`). The packages install both programs, the desktop entry, the
|
||||
AppStream metainfo, the MIME type for VectorCraft documents, the icons and the licence files.
|
||||
|
||||
**Document types.** The desktop entry's `MimeType` lists every format File › Open reads but PSD,
|
||||
PSB, `.ps` PostScript and Targa, so file managers list VectorCraft under Open With for them.
|
||||
`ai.storyteller.vectorcraft.mime.xml` declares the types that shared-mime-info lacks: VectorCraft
|
||||
documents and templates, `.ait` templates and Affinity files (`application/x-affinity`). An `.af`
|
||||
file gets the Affinity type by its content, and `.afdesign` and `.afpub` files by their extension.
|
||||
A test keeps both files in step with the engine's `OPEN_EXTS`.
|
||||
|
||||
The jobs run on `ubuntu-22.04`, the oldest GitHub-hosted image, so the binaries only need
|
||||
glibc 2.35 or newer: Ubuntu 22.04+, Debian 12+, Fedora 36+ and RHEL 10. Moving the job to a newer
|
||||
image raises that floor, so do it deliberately. After packaging, the job prints the `.deb`'s
|
||||
|
||||
@@ -86,6 +86,8 @@ with the standard command.
|
||||
|
||||
## Initial limits
|
||||
|
||||
- Lighting offers Standard, Diffuse, Top Left and Right, using the [shared original rigs](three-d.md#presets).
|
||||
Other effective settings show Custom; changing the view does not change the recognized lighting preset.
|
||||
- Paths and compound paths only. Type, images and groups must be converted to paths first.
|
||||
- Open profiles have circular end caps by default; turning Cap off keeps them open. Partial-sweep angular cut sides remain open. Closed profile boundaries are swept as provided.
|
||||
- One Revolve per source object. Edit its options rather than applying another.
|
||||
|
||||
+125
-18
File diff suppressed because one or more lines are too long
@@ -13,6 +13,8 @@ Click once to select the group; click again to select its visible fill and chang
|
||||
|
||||
While the group or one of its faces is selected (Face Selection mode) and the Shaper or Selection tool is active, an arrow widget sits beside the middle of the right side of the original shapes’ bounds. Click it to enter Construction mode: the original shapes are isolated and nothing is selected, and a click with Shaper selects the group’s original shape under the pointer. Click the widget again to leave Construction mode with the group selected. The arrow points right in Face Selection mode and left in Construction mode.
|
||||
|
||||
Smart Guides and Snap to Point do not snap to the original shapes. The reference point of the transform tools snaps to an original shape only when that shape is selected, or under the pointer in Construction mode. Outside Construction mode the Lasso does not pick the anchor points of the original shapes, and the Add Anchor Point, Delete Anchor Point, Anchor Point and Scissors tools do not pick the anchor points or segments of an original shape that is not selected.
|
||||
|
||||
Object › Shaper › Release restores the original paths. Expand keeps the visible result as ordinary paths. Each scribble and source edit records one undo step. Sources in a composition must share a parent layer/group; Shaper limits a composition to 64 source paths and 4,096 segments.
|
||||
|
||||
Finer gesture heuristics remain to be implemented.
|
||||
|
||||
+575
-119
File diff suppressed because one or more lines are too long
+57
-10
@@ -52,6 +52,24 @@ middle.
|
||||
|
||||
The surface starts with a grid of 48 cells across the larger side, halved or quartered where it rises steeply near the outline. White-light Inflate retains the existing clipped polygon cells, shading, paint composition and seam joins. With colored lights, simple cells are triangulated with shared border samples, and lighting interpolates shared membrane normals through ordinary RGB vector gradients, including on a puffed back. Solid fills batch those triangles into three channel groups and one coverage clip for the whole surface. Source gradients retain per-facet vector composition and apply transparent coverage once. Seam widths retain the 0.05 to 0.75 pt bounds; colored facets use round joins. Shading is automatic, with no additional dialog option.
|
||||
|
||||
## Fills and strokes
|
||||
|
||||
Extrude & Bevel and Inflate build a solid from the path for the object's top fill, and one from the outline of
|
||||
each visible stroke (with its caps, joins, dashes, alignment and arrowheads), each in its own paint. An admin's
|
||||
reply on the reference app's bug forum says that strokes "will be treated as outlined when applying 3D"
|
||||
([August 2024](https://illustrator.uservoice.com/forums/601447-illustrator-desktop-bugs/suggestions/48755201-expanding-a-3d-extruded-object-with-a-stroke-as-a)).
|
||||
The rest is our reading:
|
||||
|
||||
- When the fill and every stroke are opaque with Normal blending, the faces of all the solids are drawn in one
|
||||
back-to-front order, and the caps turned toward the viewer are drawn last, the lower item's first. Otherwise
|
||||
each solid is drawn on its own with its opacity and blending, the lower item's first. With a light that is not
|
||||
white, each solid is also drawn on its own when one of them is a puffed, shaded Inflate surface in a solid color
|
||||
(drawn as one composite) or is painted with a gradient that has transparent stops (drawn with an opacity mask).
|
||||
- The solids turn, twist and taper about the center of the bounds of all their shapes, Inflate's grid covers
|
||||
those bounds, and with Perspective one eye sees every solid. A stroke too narrow to hold a point of Inflate's
|
||||
grid stays flat.
|
||||
- A stroke whose outline takes more than 2,048 points is left out, and the fill's solid still draws.
|
||||
|
||||
## Rotate
|
||||
|
||||
The flat art turned in 3D about the centre of its bounds. Every fill and stroke is turned with its own paint (a
|
||||
@@ -83,6 +101,34 @@ saves the angle.
|
||||
|
||||
## Lighting
|
||||
|
||||
### Presets
|
||||
|
||||
The Lighting tab has **Standard**, **Diffuse**, **Top Left** and **Right** choices. Choosing one
|
||||
replaces the whole light rig with one white front light at 80% intensity and sets Ambient Light.
|
||||
It leaves Shading, the object's view, geometry and other effect options unchanged. Manual edits
|
||||
remain available; the list shows **Custom** whenever the effective lighting matches none of the
|
||||
presets, including when there is more than one light. An equivalent explicit single light is
|
||||
recognized too. Reset all returns to Standard. Preview, Cancel and Undo work as for other options.
|
||||
|
||||
These are original VectorCraft settings based on the public preset names and directions. Adobe's
|
||||
documentation does not publish numeric rigs; the values are not copied from its software or files
|
||||
and are not claimed to reproduce its presets exactly. Standard retains the existing default.
|
||||
|
||||
| Preset | Rotation | Height | Softness | Ambient Light |
|
||||
|---|---:|---:|---:|---:|
|
||||
| Standard | 125.26438968275465 degrees | 30 degrees | 0% | 25% |
|
||||
| Diffuse | 125.26438968275465 degrees | 30 degrees | 100% | 35% |
|
||||
| Top Left | 135 degrees | 45 degrees | 0% | 25% |
|
||||
| Right | 0 degrees | 30 degrees | 0% | 25% |
|
||||
|
||||
`effect.lightingPreset {index: 0, item: null, preset: "standard" | "diffuse" | "topLeft" | "right", ids?, target?}`
|
||||
applies the same settings through commands, CLI and MCP. The index and item address an already
|
||||
applied 3D effect. Every target must have a 3D effect at that position; validation is atomic, and
|
||||
the whole operation is one undo step. The numeric light settings are saved, not a preset name.
|
||||
Existing documents keep their appearance unless a preset is chosen.
|
||||
|
||||
### Light settings
|
||||
|
||||
One to ten directional lights. **Lights** selects the light whose settings and artboard gizmo are edited. **Add Light** adds and selects a white front light; **Delete Light** removes the selected light while retaining at least one. Ambient Light is shared by all lights. The public panel options' Light Settings are kept in their order:
|
||||
|
||||
| Option | |
|
||||
@@ -120,20 +166,20 @@ and [Create 3D objects](https://helpx.adobe.com/illustrator/desktop/special-effe
|
||||
pages, and the archived Extrude & Bevel option list (depth 0–2,000, cap on/off, bevel height, bevel extent in or
|
||||
out, perspective). No Adobe software, assets, presets or bevel shapes were used.
|
||||
|
||||
- **Materials** (base, Adobe Substance and graphics), light presets, **shadows**
|
||||
and **ray-traced rendering** are not built: shading uses directional lights, painted with the
|
||||
object's top fill (else its top stroke) for Extrude & Bevel and Inflate.
|
||||
- **Materials** (base, Adobe Substance and graphics), **shadows**
|
||||
and **ray-traced rendering** are not built: shading uses directional lights, in the paint of the fill or
|
||||
stroke the solid is built for ([Fills and strokes](#fills-and-strokes)).
|
||||
- The bevel shapes and their names are our own; custom bevel paths are not supported.
|
||||
- Visibility is a painter's algorithm: faces of a closed solid turned away are left out, the rest drawn back to
|
||||
front. Concave solids seen at steep angles can overlap wrongly; open (hollow) solids show both sides.
|
||||
Twisted walls and bevels are drawn in pieces about as long as a wall slice is wide (fewer near the limit of
|
||||
32,768 facets), and a cap or flat back turned toward the viewer is drawn over them. The piece drawn on top can
|
||||
still lie a few points behind another nearby piece.
|
||||
- Visibility is a painter's algorithm: faces of a closed solid turned away are left out, and the rest are drawn
|
||||
back to front, with a flat cap or flat back turned toward the viewer drawn after all of them. Concave solids
|
||||
seen at steep angles can overlap wrongly; open (hollow) solids show both sides. Twisted walls and bevels are
|
||||
drawn in pieces about as long as a wall slice is wide (fewer near the limit of 32,768 facets). The piece drawn
|
||||
on top can still lie a few points behind another nearby piece.
|
||||
- Extrude & Bevel and Rotate retain planar shading. Colored Inflate uses smooth vector lighting; white-light Inflate keeps its original polygon shading; bounded cell triangulation leaves a cell flat when it contains a complete hole or an unusually complex/degenerate contour. Its geometric silhouette is still a polygon approximation.
|
||||
- Paths and compound paths only: convert type, images and groups to outlines first. One 3D effect per object.
|
||||
- Curves are followed within 1/2000 of the shape's size (at least 0.05 pt), so a small curve takes a few points
|
||||
and a large one up to 32. Extrude & Bevel and Inflate take up to 2,048 outline points, Rotate (which only turns
|
||||
the outline) 65,536.
|
||||
and a large one up to 32. Extrude & Bevel and Inflate take up to 2,048 outline points (a stroke's outline is
|
||||
counted on its own), Rotate (which only turns the outline) 65,536.
|
||||
- **3D (Classic)** (the track cube, surface types, blend steps, map art), Turntable and 3D export are separate,
|
||||
unbuilt items.
|
||||
|
||||
@@ -143,4 +189,5 @@ Release measurements and a repeatable CLI scene are in [3D lighting performance]
|
||||
|
||||
| Date | Change |
|
||||
|---|---|
|
||||
| 2026-10-21 | Four original lighting presets, shared by all four 3D effects and commands, with effective-setting recognition and existing preview/history behavior. |
|
||||
| 2026-10-17 | Smooth colored Inflate lighting and surface batching; legacy white-light appearance and the 0.05 seam minimum preserved; shared light Color and multiple lights remain vector output. |
|
||||
|
||||
+40
-12
File diff suppressed because one or more lines are too long
@@ -0,0 +1,114 @@
|
||||
# Typography and Custom Lettering in VectorCraft
|
||||
|
||||
## What you can do
|
||||
|
||||
- **Typesetting:** use an available font to create editable text, then adjust its size, style and spacing.
|
||||
- **Custom lettering:** convert text to vector outlines and reshape the letters for a title or logo.
|
||||
- **Reusable font files:** VectorCraft does not currently create or export installable TTF or OTF fonts.
|
||||
|
||||
This guide covers the first two workflows. The steps apply to the v0.13.1 release; the desktop app
|
||||
can use installed system fonts, while the web app uses the fonts supplied by its deployment.
|
||||
|
||||
## Getting started with the Type Tool
|
||||
|
||||
1. Create a document with **File → New…**, or open an existing one.
|
||||
2. Select **Type Tool** in the toolbar, or press **T** when you are not typing in a text field.
|
||||
3. Click an empty part of the artboard and type a short heading, such as `MOON`. A click creates
|
||||
point text, which grows as you type. To create text that wraps inside a frame, drag a rectangle
|
||||
instead. If placeholder text appears selected, typing replaces it.
|
||||
4. Press **Escape** to finish typing. Select **Selection Tool** (**V**) and click the text object.
|
||||
This lets you format the whole heading rather than just characters selected while typing.
|
||||
5. Open **Window → Type → Character**. Use the font family menu at the top of the panel to search
|
||||
for a font and click its name to apply it. The menu below it chooses an available style, such as
|
||||
Regular, Bold or Italic; the choices depend on the font. **Type → Font** also lists available fonts.
|
||||
6. Adjust **Font Size** (the field beside **T**) and **Tracking** (beside **VA↔**) in the Character
|
||||
panel. Hover a field's label to see its name. For example, enter `72 pt` for a large heading and
|
||||
try tracking `50` to spread its letters slightly. Positive tracking adds space; negative tracking
|
||||
tightens it. **Leading** controls the spacing between lines; **Kerning (0 = Auto)** adjusts pairs.
|
||||
7. To change only part of the text, select **Type Tool**, click into the text and drag across the
|
||||
characters before using the Character panel. Press **Escape** when finished. Keep the text
|
||||
editable until its wording, font and spacing are settled.
|
||||
8. Choose **File → Save As…**, select the **VectorCraft** format, and save the document as
|
||||
`lettering-editable.vectorcraft`.
|
||||
|
||||
The desktop app scans system fonts and loads faces as needed. If you install a font while it is
|
||||
open, use **Refresh Font List** in the Character panel's menu. On the web, installing a font on your
|
||||
computer does not add it to VectorCraft's font menu; the available fonts depend on the hosted build.
|
||||
|
||||
## Turning text into editable outlines
|
||||
|
||||
Create Outlines replaces text with vector paths. You can reshape those paths, but you can no longer
|
||||
edit the words or choose a different font for them with the Type Tool.
|
||||
|
||||
1. Keep the editable file saved above. Choose **File → Save As…** again, select **VectorCraft**,
|
||||
and save as `lettering-outlines.vectorcraft`. Continue in this second file so later saves do not
|
||||
overwrite your editable original.
|
||||
2. With **Selection Tool** (**V**), select the text object you want to convert.
|
||||
3. Choose **Type → Create Outlines**. The default shortcut is **Ctrl+Shift+O** on Windows and Linux,
|
||||
or **Cmd+Shift+O** on macOS.
|
||||
4. Select **Direct Selection Tool** (**A**) and click a letter's edge to inspect its paths and anchors.
|
||||
The text has become a group of paths, one compound path per glyph. A compound path can include
|
||||
both a letter's outer edge and its inner holes, such as the middle of an `O`.
|
||||
|
||||
Conversion uses the font currently drawn. Resolve missing or substituted fonts before outlining,
|
||||
or the paths will capture the substitute's shapes.
|
||||
|
||||
## Customizing individual letters
|
||||
|
||||
Try making the final `N` in `MOON` taller to give the heading a distinctive ending.
|
||||
|
||||
1. Select **Direct Selection Tool** (**A**). Click empty canvas to clear the current selection,
|
||||
then click an anchor on the outlined `N`. Anchors are the points that define a path's shape;
|
||||
Direct Selection can reach paths inside the group without ungrouping the letters.
|
||||
2. Drag the anchor a short distance to change that part of the letter. To move several anchors
|
||||
together, Shift-click them before dragging, or drag a selection rectangle around them from
|
||||
empty canvas. For the taller `N`, select its top anchors and move them up slightly.
|
||||
3. On a curved letter such as `O`, select an anchor on a curve. Drag the end of a visible direction
|
||||
handle to change the curve's bend. These Bézier handles control how the curve leaves or enters
|
||||
the anchor. Straight corners may have no handles to drag.
|
||||
4. Switch to **Selection Tool** (**V**) to inspect the whole heading. Use **Edit → Undo** if a change
|
||||
distorts the letter or closes an inner hole, then try a smaller adjustment.
|
||||
|
||||
These changes affect the artwork in this document. They do not change the installed font or other
|
||||
text using it. A glyph is not always one typed character: for example, a font may join letters into
|
||||
a ligature, which becomes a single glyph outline.
|
||||
|
||||
## Saving and exporting your artwork
|
||||
|
||||
- **Keep the project:** use **File → Save** to save `lettering-outlines.vectorcraft`. Retain
|
||||
`lettering-editable.vectorcraft` separately for future wording or font changes. A native file keeps
|
||||
editable text only where you have left text objects unconverted.
|
||||
- **Export vector artwork:** choose **File → Export → Export As…**, select **SVG** or **PDF**, and
|
||||
continue through the export options. The outlined letters are vector shapes in the output, not
|
||||
keyboard-editable text. Check **Use Artboards** if you want the artboard bounds rather than the
|
||||
bounds of the visible artwork.
|
||||
- **Create an installable font:** SVG and PDF exports are artwork, not TTF/OTF files. Renaming their
|
||||
extension does not turn them into fonts.
|
||||
|
||||
## Can VectorCraft create a font file?
|
||||
|
||||
No. VectorCraft does not currently provide a complete font-authoring workflow or TTF/OTF export.
|
||||
It does not turn your letter paths into a reusable font with character encoding, glyph metrics and
|
||||
font-wide kerning definitions. Character panel spacing changes format the text in your drawing;
|
||||
they do not edit a font's kerning tables.
|
||||
|
||||
To make an installable typeface, use a dedicated font editor to assign glyphs to characters, set
|
||||
metrics and spacing, and generate the font file. VectorCraft's outline tools are useful for custom
|
||||
lettering artwork, but exporting that artwork is only one part of making a font.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **I cannot find my font.** On desktop, check that it is installed, clear the font menu's search
|
||||
and filters, then choose **Refresh Font List** from the Character panel menu. **Type → Find Font…**
|
||||
lists fonts used by the document and marks missing ones; it can replace them with available fonts.
|
||||
A missing family, style or character may use a fallback, so check the result before outlining.
|
||||
On the web, the site must supply the font; system-font scanning and the desktop refresh option
|
||||
are unavailable there.
|
||||
- **I cannot change the words after Create Outlines.** The result is paths, not text. Use
|
||||
**Edit → Undo** if the conversion is still in the undo history, or reopen your editable original.
|
||||
- **I cannot install my modified letters as a font.** They are drawing objects. SVG/PDF export
|
||||
does not add the structure required by an installable font; see [Can VectorCraft create a font
|
||||
file?](#can-vectorcraft-create-a-font-file).
|
||||
- **How do I keep text editable?** Save the text in a native `.vectorcraft` file before outlining,
|
||||
then use a different file name for the outlined version. Saving paths in the native format does
|
||||
not turn them back into text.
|
||||
+216
-38
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user