Merge main into fix/outline-stroke-keeps-effects-on-outline

This commit is contained in:
dotCooCoo
2026-10-20 20:01:02 -07:00
702 changed files with 102038 additions and 5871 deletions
+75
View File
@@ -0,0 +1,75 @@
# 3D lighting performance
Measured 2026-10-17 for PR #2706 on one macOS machine.
## Method
- Apple M1 Pro, 10 cores, 16 GiB RAM, macOS 26.5.1 (arm64).
- `cargo build --release -p vectorcraft-cli`, the workspace release profile (thin LTO, one codegen unit), without bundled fonts.
- Before: published PR head `f63fafdb`. After: the reviewed implementation rebased on main `f7de3d6a`, with legacy white-light Inflate restored and colored solid-fill channel groups batched per surface. The main update is included in this comparison.
- Both executables were retained separately. No compilation ran during measurement. Three paired repetitions alternate before/after for each fixture. Each `bench` invocation warms its renderer once, then averages five frames for each thread setting. The table reports the median of the three reported averages.
- `bench` renders a 400 x 400 pt artboard at 1200 x 1200 pixels, fitting it to 95% of the viewport. The scene contains one white-filled ellipse, centered at (200, 200), with radii 110 and 140 pt, no stroke, Inflate volume 50%, depth 0, solid cap, one side, view rotation (-20, 25, 0), no perspective, and 15% neutral ambient.
- Two lights: orange `#ff8040`, intensity 70%, rotation 135 degrees, height 35 degrees, softness 15%; blue `#4080ff`, intensity 65%, rotation -35 degrees, height 25 degrees, softness 10%. Both are in front.
- The one-light case uses the legacy fields: `#ff8040`, intensity 80%, rotation 125.26438968275465 degrees, height 30 degrees, softness 0%. The white case changes its color to `#ffffff`.
- The source-gradient case uses the same two lights with a linear source gradient from (90, 200) to (310, 200), orange at opacity 0.45 and blue at opacity 0.7.
- `perf` covers the standard 50,000-path synthetic budget scene. It was also run three times per executable, alternating before/after. Its table reports medians.
## Reproduce
Create the two-light scene using the updated CLI, then use the same saved file with both executables:
```sh
vectorcraft-cli run \
--cmd file.new --params '{"width":400,"height":400}' \
--cmd document.setup --params '{"backgroundContents":"white"}' \
--cmd path.create --params '{"d":"M310.0 200.0C310.0 277.3198649763111 260.7513224813873 340.0 200.0 340.0C139.2486775186127 340.0 90.0 277.3198649763111 90.0 200.0C90.0 122.6801350236889 139.2486775186127 60.0 200.0 60.0C260.7513224813873 60.0 310.0 122.6801350236889 310.0 200.0Z"}' \
--cmd paint.setFill --params '{"color":"#ffffff"}' \
--cmd paint.setStroke --params '{"none":true}' \
--cmd effect.apply --params '{"effect":"threeD.inflate","item":null,"params":{"rotationX":-20,"rotationY":25,"rotationZ":0,"ambient":15,"volume":50,"depth":0,"cap":true,"bothSides":false,"lights":[{"color":"#ff8040","intensity":70,"rotation":135,"height":35,"softness":15},{"color":"#4080ff","intensity":65,"rotation":-35,"height":25,"softness":10}]}}' \
--cmd document.save --params '{"path":"/tmp/inflate-two-lights.vectorcraft"}'
vectorcraft-cli bench /tmp/inflate-two-lights.vectorcraft --size 1200x1200 --iters 5
vectorcraft-cli perf
```
## Measurements
Milliseconds per frame, medians of three five-frame averages. `8` is the CLI default thread count on this machine; `0` selects the single-thread renderer.
| Inflate scene | Before, 8 | After, 8 | Before, 0 | After, 0 |
|---|---:|---:|---:|---:|
| Two colored lights, solid fill | 136.2 | 96.8 | 147.2 | 100.6 |
| One colored light, solid fill | 94.4 | 96.7 | 107.0 | 100.0 |
| Legacy white light, restored polygon shading | 12.3 | 4.1 | 27.3 | 9.5 |
| Two colored lights, transparent source gradient | 145.6 | 146.8 | 156.9 | 157.0 |
The two-light solid-fill case takes 28.9% less frame time at the default thread count and 31.7% less in the single-thread renderer. The one-light case is 2.4% higher with eight threads and 6.5% lower single-threaded. The source-gradient case retains its previous per-facet compositor and changes by less than 1% in these medians. Restoring legacy white-light polygons also removes the extra smooth triangles from that path.
All six `perf` runs exited successfully, with no busy-machine warning and every individual budget row passing. The reported one-minute load averages were 4.5 to 5.9 on ten cores. Median milliseconds:
| Standard 50,000-path workload | Before | After | Budget |
|---|---:|---:|---:|
| render: fit page, 2880×1800 | 47.15 | 47.80 | 100 |
| render: background refresh after a pan | 48.18 | 46.71 | 100 |
| render: 400% zoom, 2880×1800 | 13.45 | 13.68 | 16 |
| hit test (per click) | 0.43 | 0.42 | 2 |
| save .vectorcraft | 52.78 | 52.15 | 300 |
| open .vectorcraft | 143.84 | 142.55 | 300 |
| export SVG | 207.37 | 206.25 | 500 |
| Pathfinder Unite, 1,000 paths | 43.18 | 43.29 | 150 |
| handle drag, 5,000-anchor path (per move) | 0.94 | 0.84 | 8 |
| segment drag, 5,000-anchor path (per move) | 0.87 | 0.92 | 8 |
The colored fixture uses the same projected mesh before and after. The solid-fill batch reduces compositor groups and clips; it retains three sets of channel facet paths. Smooth vector export still has many gradient elements.
## Rendering structure
For a smooth colored solid fill, all facets share one compound coverage clip and three isolated channel groups. Each channel has an opaque black backdrop; Screen combines the disjoint RGB channels and Multiply applies the source color. Face order is retained in each channel. Surface opacity and outer coverage apply once. The group/clip counts are independent of triangle count, and export remains vector artwork.
Source gradients retain their independently placed per-facet vector compositor. They remain a more expensive case. Patterns retain their neutral shading approximation. White-light Inflate uses the original polygon shading, paint composition and default seam joins; the seam width retains its 0.05 to 0.75 pt bounds.
Geometry/effects tests cover legacy polygons and seam bounds, shared smooth normals, finite ramps, holes and constant group/clip counts. Engine tests cover the batched SVG round trip, vector PDF, expansion, undo, compound holes and surface opacity.
## Legacy white-light comparison
Four original synthetic scenes were exported at 1200 x 1200 from unchanged main `f7de3d6a` and the updated implementation. The PNG files match byte for byte in all four cases: the legacy white-lit ellipse above; the same ellipse with both sides, hollow cap, twist 60 degrees, taper 70%, perspective 80 degrees and view Y rotation 145 degrees; an unshaded ellipse; and the transparent source gradient above under the legacy white light. The command recipe was also checked against the measured colored fixture's image pixels.
+53 -14
View File
@@ -90,7 +90,7 @@ 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?}` | layer tree, selection, the key object (`key`, null without one), history, paint defaults; the options slice the layer tree as `document.node {summary: true}` does. Each node reports `opacity` below 1 and `blend` when it isn't Normal; objects and compound paths report `fill`, `stroke` and `strokeWidth`, a compound path's members `compoundMember: true` instead |
| `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 |
@@ -109,20 +109,20 @@ the two can still win that race.
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, a converted older file, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
| `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 |
| 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 |
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
| `app.export` JPEG options | `{path?, format: jpg, quality?: 0–100, colorModel?: rgb \| cmyk \| gray, method?: baseline \| optimized \| progressive, scans?: 3–5, embedIcc?, imageMap?: none \| client \| server}` | `cmyk` writes ink amounts in the working CMYK space (CMYK colours keep their inks; RGB ones are separated with the colour settings); `embedIcc` (default true) embeds sRGB, the working CMYK profile or a grey profile, generated by the CMS; `imageMap` also writes `<stem>.html` (client-side `<map>`) or `<stem>.map` (NCSA) beside the image for the objects with a URL and an Image Map shape (`attributes.set`), returned in `linked`. The `jpgOptions` dialog has these fields too (`quality` is shown 0–10; a CMYK document defaults to `cmyk`) |
| `app.export` PNG-8 and GIF | `{path?, format: png8 \| gif, colors?: 2–256, reduction?: perceptual \| selective \| adaptive \| web \| blackWhite \| gray, dither?: none \| diffusion \| pattern \| noise, ditherAmount?: 0–100, transparency?, matte?, interlaced?, …raster options}` | a palette of at most `colors` entries (art with that many colours or fewer keeps them exactly); pixels under half opacity become one transparent entry unless `transparency` is false, partly transparent edges are blended over `matte` (`none` keeps their colour). `png8` writes an indexed `.png` (1–8 bits per pixel); Export for Screens rows take both. The `png8Options` / `gifOptions` dialogs have these fields |
| `app.export` Text and WebP | `{path?, format: txt, encoding?: utf8 \| utf16, lineEndings?: lf \| crlf, selectionOnly?}` / `{format: webp, lossless?, quality?}` | Text writes the stories in stacking order (back to front; a thread once; hidden objects and template layers left out), each line ended by `lineEndings`; `utf16` is little-endian with a byte order mark. Export As → Text opens `txtOptions` (fields `path`, `encoding`, `lineEndings`, `selectionOnly`; OK writes the file). WebP is always lossless: `lossless: false` adds a warning |
| `engine.execute` `document.exportForOffice` | `{}` / `{path?, ppi?: 72 \| 150 \| 300, transparent?, artboard?}` | File → Save for Office Documents…: without params opens `saveForOffice` (fields `ppi`, `transparent`, `artboard`; OK picks the file); with params writes the artboard as a PNG (`path`, else a picked file) → `{path}`. Headless it returns `{dataBase64, width, height}` |
| `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 |
| `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 |
| `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 |
| 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}` |
| `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 |
@@ -163,7 +163,9 @@ tool option `stop` (the tool to return to; `tool.setOption {key: "stop", value:
samples the colour there into the selected stop (`paint.sampleColor {color, stop}`) and switches back. Dragging a
swatch, a Fill/Stroke proxy or the panel's gradient thumbnail onto art runs `paint.setFill` / `paint.setStroke` (the
active proxy) with the paint's params and the object's `ids`; a colour dropped on the panel's ramp adds or recolours
a stop.
a stop. Alt-clicking a color swatch in the Swatches panel recolors the selected stop as a drop on it does
(`paint.editGradient {stops}`), or the selected point of a freeform gradient (`paint.freeform.setPoint`); with no stop
or point selected, it paints the inactive Fill or Stroke.
Freeform gradients: double-clicking a point with the Gradient tool opens the same `gradientStop` popover on the selected
point (`paint.freeform.selectPoint`); its fields are `color` (hex), `opacity` and `spread` (percentages), applied by
@@ -175,7 +177,9 @@ for `eyedropper` it opens Eyedropper Options, an `eyedropperOptions` dialog (fie
`appearance.copyFrom` copies). For `hand` it fits the artboard in the window (`view.fitArtboard`) and for `zoom` it
shows 100% (`view.actualSize`). For `rotate`, `scale`, `reflect` and `shear` it opens the same dialog as Object ›
Transform (dialog kind = the tool id), or fails with `nothing selected`. For `selection`, `directSelection` and
`groupSelection` it opens the Move dialog (kind `move`), with the same failure. `ui.key` Enter with one of those seven
`groupSelection` it opens the Move dialog (kind `move`), with the same failure; Move opens on the offset of the last
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.
@@ -194,7 +198,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
@@ -399,12 +405,21 @@ changed since); its preview greys the art and paints the highlighted objects or
Expand: Object → Expand… (`ui.expandDialog`, or `object.expand` without params from the menus, palette and shortcuts)
opens the `expand` dialog for the selection. Fields: `object`, `fill`, `stroke` (all on; `__object`, `__fill`,
`__stroke` say which the selection has something for, as `object.expand.info` does, and the others are disabled),
`gradient` (`objects` or `mesh`) and `steps` (1–1000, default 255). `ui.dialog.confirm` runs `object.expand` with them as
`gradient` (`objects` or `mesh`) and `steps` (1–1000, default 255), which are enabled when `fill` is on and
`__gradient` says the selection has a gradient fill (`object.expand.info`'s `gradient`). `ui.dialog.confirm` runs `object.expand` with them as
one undo step.
Attributes panel: `window.panel {panel: "attributes"}` (Cmd+F11) shows it; its controls run `attributes.set`,
`path.reverse {reversed}` and `path.setFillRule {rule}`, and its Browser button runs the UI command
`attributes.openUrl {url?}`, which opens the URL (default: the selection's) in the web browser and answers `{url}`.
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
Files… button runs the UI command `ui.javascriptFilesDialog`, which opens the `javascriptFiles` dialog (fields `file`
and `selected`). The dialog's Add and Remove buttons run `svgInteractivity.addFile {file}` and
`svgInteractivity.removeFile {file}` at once, and `ui.dialog.confirm` closes it.
Spot Colors: the Swatches panel menu's Spot Colors… (`ui.spotColors`) opens the `spotColors` dialog (field `useLab`:
true shows and separates spot colours defined in Lab from their Lab values, false from their working-CMYK
@@ -561,9 +576,10 @@ dialog, a drop) opens the `textImport` dialog (fields `platform`: `windows`/`mac
type with those options (`file.place {text: {…}}`).
System clipboard formats: through the desktop app, `edit.copy` and `edit.cut` publish what `clipboard.flavours`
lists to the system clipboard, and every Paste command first loads what another app copied (`clipboard.importSvg`,
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).
@@ -598,7 +614,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),
@@ -663,7 +680,8 @@ a preset) plus `__view` (`original`, `optimized`, `2up`, `4up`), `__zoom` (`fit`
`__color` (the colour table's selected colour). `ui.dialog.confirm` remembers the settings and saves (a save panel for
the image, or the page with HTML output; the web downloads the files); with `discard: true` it is Done: it remembers
the settings and closes. `file.saveForWeb` with settings saves without the dialog, and `file.saveForWeb.browser`
writes the HTML page to a temporary folder and opens it in the default browser (desktop).
writes the HTML page to a temporary folder and opens it in the default browser (desktop). Both return the export's
`warnings`, and the status bar shows the first one as Save As shows a save's notes.
PSD Options: Export As… with the PSD format (`file.exportAs {format: "psd"}`, then the save dialog) opens `psdOptions`:
the raster rows of PNG Options (`ppi`, `background`, `antiAlias`), then `colorModel` (`rgb`, `cmyk`, `gray`; a CMYK
@@ -864,9 +882,30 @@ 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-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. |
+19 -9
View File
@@ -85,7 +85,8 @@ By default VectorCraft's own crates log at `info` and everything else at `warn`.
|---|---|
| `VECTORCRAFT_CONTROL_PORT` | Same as `--control <port>`, which overrides it; a value that isn't a port from 1 to 65535 stops the app |
| `VECTORCRAFT_IN_WINDOW_MENUS` | Any value but empty or `0`: same as `--in-window-menus` (see [macOS: the menu bar](#macos-the-menu-bar)) |
| `VECTORCRAFT_NO_PREFS` | No preferences read or written, no default Data Recovery folder and no log file (agents' test runs) |
| `VECTORCRAFT_NO_PREFS` | No preferences read or written, no default Data Recovery folder and no log file, and no saved PDF presets or loaded ICC profiles kept (agents' test runs; also for `vectorcraft-cli`) |
| `VECTORCRAFT_CONFIG_DIR` | The folder for the preferences and the folders next to them (`PDF Presets`, `Color Profiles`, …), for the app and `vectorcraft-cli` (see `docs/control-protocol.md`) |
| `WGPU_POWER_PREF` | Graphics adapter: `low`, `high` or `none` (see [Desktop graphics processor](#desktop-graphics-processor)) |
| `WGPU_ADAPTER_NAME` | Graphics adapter by (part of) its name, any case (see [Desktop graphics processor](#desktop-graphics-processor)) |
| `WGPU_BACKEND` | Graphics backends: `vulkan`, `dx12`, `metal`, `gl` |
@@ -103,13 +104,15 @@ 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).
- **Copied files:** files copied in a file manager (`text/uri-list`; `CF_HDROP` on Windows, file URLs on macOS) paste as the first one that is art (SVG, PDF, EPS, EMF/WMF or a bitmap; a `.ai` or `.ait` file from the editing data it holds, else as the PDF or PostScript it holds), before the clipboard's other formats (which include the files' paths as text).
- **Dropping files on the window does not work under Wayland:** winit 0.30, which eframe 0.36 runs on, has no Wayland drag and drop ([winit#1881](https://github.com/rust-windowing/winit/issues/1881), added in winit 0.31; [egui#1563](https://github.com/emilk/egui/issues/1563)), so no `dropped_files` arrive. Until eframe moves to winit 0.31: copy the files in the file manager and paste them, use File › Place or Open, or open the window through XWayland, where drops work: set **Preferences › Performance › Linux Display Server** to **X11 (XWayland)** and restart (#1151), or start the app with `WAYLAND_DISPLAY= vectorcraft`. The preference (`linuxDisplayServer`, read at startup in `apps/vectorcraft/src/main.rs`, shown on Linux only) has winit open the window on X11 (`EventLoopBuilderExtX11::with_x11`) when the session is Wayland, `DISPLAY` is set and the X11 libraries winit loads are installed (`apps/vectorcraft/src/xwayland.rs` reads `ldconfig -p` and the usual library directories); otherwise the window opens on Wayland and the log says why. Unlike clearing `WAYLAND_DISPLAY`, it leaves the clipboard on the compositor's own.
- **Pens and pen displays under Wayland (#491):** 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)): 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), `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.
- **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.
## Fonts: craft-fonts (optional build input)
@@ -141,12 +144,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`.
@@ -204,7 +210,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
@@ -232,7 +238,7 @@ other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian
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, Spanish, French, Hungarian, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Swedish, Ukrainian, Vietnamese and Finnish cover every
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 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
@@ -244,9 +250,13 @@ other `sv-*` locale resolve to it), Czech (`cs`, every menu label) and Brazilian
system font; the glyph test checks Latin and Cyrillic catalogs always and the CJK ones only with craft-fonts.
A Traditional Chinese UI font is still to be added to craft-fonts for the web build.
## Cursors
`crates/tools/src/cursors.toml` lists every `Cursor` value: when it shows, in plain behaviour terms, and where its hotspot is, so whoever draws the cursors knows what each one is for without reading the tools (CLAUDE.md › Adding a cursor). `cargo xtask cursors` (part of `cargo xtask ci`) warns about a `Cursor` value with no entry, an entry with no `Cursor` value, a repeated id or a missing field; it never fails the build. A cursor not drawn yet borrows another's look (`Cursor::stand_in`).
## 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.
## Robustness: VectorCraft never crashes
+98 -11
View File
@@ -1,6 +1,6 @@
# Effects parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (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-20 · **Change:** minor (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 | 45 | 90% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 30 | 53% | 22–38 |
| Illustrator effects (vector and stylize) | 50 | 46 | 92% | 6–10 |
| Photoshop effects (raster, Effect Gallery) | 57 | 46 | 81% | 8–14 |
| 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,10 +22,10 @@ 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); 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'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) |
| 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 |
| Rasterize | Rasterize… (as a live effect) | 0/1 | the live effect (Object › Rasterize exists) |
| 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) |
| Warp | Arc, Arc Lower, Arc Upper, Arch, Bulge, Shell Lower, Shell Upper, Flag, Wave, Fish, Rise, Fisheye, Inflate, Squeeze, Twist | 15/15 | warp point editing in envelopes; Arc is a circular arc in the object's own proportions (#1578) |
@@ -38,13 +38,13 @@ its resolution, PDF export warns once per object with the requested and the lowe
| 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 | 1/15 (approximate Film Grain) | Colored Pencil, Cutout, Dry Brush, Fresco, Neon Glow, Paint Daubs, Palette Knife, Plastic Wrap, Poster Edges, 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 | 8/15 (approximate Cutout, Film Grain, Neon Glow, Plastic Wrap, Poster Edges, Smudge Stick, Sponge, Watercolor) | Colored Pencil, Dry Brush, Fresco, Paint Daubs, Palette Knife, Rough Pastels, Underpainting |
| 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 | 11/14 (approximate Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation, Stamp; Chalk & Charcoal, Graphic Pen and Note Paper defaults unverified) | Conté Crayon, Torn Edges, Water Paper |
| Stylize | Glowing Edges | 1/1 | — |
| Texture | Craquelure, Grain, Mosaic Tiles, Patchwork, Stained Glass, Texturizer | 6/6 | Texturizer's Load Texture |
| Video | De-Interlace, NTSC Colors | 2/2 | — |
@@ -52,7 +52,7 @@ its resolution, PDF export warns once per object with the requested and the lowe
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 27 missing filters are ~18–33 h and the Effect
(see [calibration](target-app-parity.md#calibration)), so the 12 missing filters are ~8–14 h and the Effect
Gallery dialog another 4–6 h. These parallelize well across agents.
Film Grain (`artistic.filmGrain`, Effect › Artistic) adds stable monochrome noise to shadows and
@@ -68,6 +68,40 @@ mid-grey to a quarter-strength floor at full Intensity before highlight headroom
soft grain, black takes light speckles and white stays clean. The tonal curve and endpoint behaviour
are interpretations, not verified proprietary behaviour. Saturated colour channels may clip. The parameter
ranges and defaults are chosen for VectorCraft, not asserted as measured proprietary defaults.
Cutout, Neon Glow and Poster Edges (`artistic.cutout`, `artistic.neonGlow`, `artistic.posterEdges`)
follow the same two public references, with original algorithms. Cutout softens the object's colours (Edge Simplicity, less with Edge
Fidelity), splits each colour channel into Number of Levels steps, and paints each resulting piece
in the average colour that falls into it, so hues survive. Poster Edges
posterizes each colour channel to Posterization + 2 levels and draws black lines on the edges of
any channel and of the object's outline, wider with Edge Thickness and darker, on fainter edges
too, with Edge Intensity. Neon Glow redraws the object in dimmed greys screened with a glow colour
from its light areas (positive Glow Size) or its dark ones (negative), spread by the size and as
strong as Glow Brightness; with no foreground and background colours in Illustrator's model, the
glow colour is the effect's own parameter. All three keep coverage, work in document units at any
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`.
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
@@ -78,7 +112,52 @@ 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`.
## 3D and Materials
@@ -86,20 +165,28 @@ 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 directional light with the documented Intensity, Rotation, Height, Softness and Ambient Light, Revolve uses smooth vector shading, the other types use flat facets; no materials, several lights, light colour or 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; no materials, light presets, 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, several lights and shadows, ray-traced rendering,
the effects crate turns them into art. What's left (materials, light presets 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-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 |
| 2026-10-16 | minor | 3D lighting: opaque RGB Color for the shared directional light, with neutral ambient, preview/reset controls and smooth vector Revolve shading; scores and hour estimates unchanged |
| 2026-10-15 | minor | Round Corners rounds corners whose anchor carries a handle (where a curve meets a line, or two curves meet) with the round cut Live Corners uses (#1144), reaching `radius` along each side; before, they were left sharp. Corners between handle-less anchors are rounded as before; score unchanged (estimated) |
| 2026-10-14 | minor | Revolve: automatic smooth vector shading, default-on Cap with continuous curved rims, and bounded seam joins; angular cut sides remain open. Scores and hour estimates unchanged at this rounding |
| 2026-10-14 | minor | Wide drop shadows and glows at high zoom render within the raster budget (#1706); stacked Transform effects are bounded (#1705); 3D effects keep the area a nonzero compound path fills (#1724); pattern tiles redraw when a symbol, pattern or image their art shows changes (#1723) (all #1864). Scores unchanged (estimated) |
File diff suppressed because one or more lines are too long
+89 -36
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -19,10 +19,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`) | X11 only via XWayland (Preferences › Performance › Linux Display Server, #1151); none on Wayland (#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) | **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 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 |
| Touch gestures (pinch zoom, two-finger pan, rotate view, Touch workspace) | yes (Windows touch, Surface) | trackpad pinch and scroll | 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 rotate gesture missing, 3–5 h |
| 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) |
| Multiple monitors: panels and document windows on another screen | yes | no (one OS window) | no | no | — | missing: 6–10 h (with multiple windows, gap G7) |
+14
View File
@@ -26,6 +26,20 @@ so Undo also restores the panel values before its next frame. View can show the
Use **Expand** when finished to turn the result into ordinary editable vector objects;
after expanding, the tracing settings are no longer available. **Release** restores the source.
**Auto Color** (the Automatic palette) counts flat art's colours by area, and also keeps colours too
rare for that count when they are distinct and drawn as regions of their own (a stem, a highlight, a
white beside a cream), at most six; scattered specks of a colour add none. Adobe does not document how
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)).
**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
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.
These controls use VectorCraft's existing tracing algorithm. Parameter effects follow its
implementation; they do not claim pixel-identical results with another application.
The terminology and general control behavior are described in the
+277 -1
View File
@@ -1,12 +1,144 @@
# Image Trace quality
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-17 · **Change:** minor (Image Trace: Auto Color keeps rare but distinct colours; earlier: Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-17 · **Change:** minor (Image Trace: large images trace with a warning; anchor limit raised to 8,000,000; earlier: Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-17 · **Change:** minor (Image Trace: Raster::new returns an error for a buffer of the wrong length; earlier: Gradients: grown over noise and soft rims)
> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (Create Strokes: maximal-disc pruning and junction merging; centre-lines rerun)
> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (Create Strokes: exact crop distances and Guo–Hall thinning; centre-lines rerun)
> **Last reviewed:** 2026-10-17 · **Last updated:** 2026-10-17 · **Change:** minor (centre-lines baseline; no tracing behaviour change)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: a higher Paths never gives fewer segments on collapsed loops)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: the notch rule reads only its own piece)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: an edge's sub-pixel shift is the same from either side)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-16 · **Change:** minor (Image Trace: a simpler contour walk with no precomputed direction table)
What Image Trace (`crates/trace`) makes of images whose right answer is known, measured by a harness and
compared with the numbers before each change. Nothing here is a judgement by eye, and nothing is compared
with any other product except the open tracer named under [Yardstick](#4-yardstick).
## Auto Color keeps rare colours
The Automatic palette (Auto Color) counts flat art's colours by area: a colour needs about 1% of the
picture (0.5% at Colors 100) to be counted, and the count is then fitted by median cut (Heckbert 1982)
and k-means (Lloyd 1982), weighted by area. A stem, a highlight or a cream close to a pale background
was averaged into a large neighbour, and Noise could remove what was left of it. Now, on the flat route
only, after the count is fitted, the colours too rare or too close for it are found and kept when they
are:
- distinct: at least 2.5 (Oklab × 100; Ottosson 2020) from every palette colour and from the main
colour each one stands for;
- flat: mostly one histogram bin;
- drawn: a 4-connected region of their own (not blended) pixels of at least 8 pixels, or three of at
least 4 (repeated highlights); a colour lying between two others, as an edge's anti-aliasing mixes
them, must also have an inside (pixels whose four neighbours are all of it).
At most six are kept, largest region first. They are fixed while the other colours are refitted around
them, mixed-colour merging leaves them alone, and Noise removal keeps their regions of 4 pixels or more
(smaller specks of them still go). This follows the idea of salient-colour preservation in palette
reduction (Fukuda et al. 2019, doi:10.1117/12.2521607); the rule and its numbers are VectorCraft's own.
Full Tone, Limited and the continuous-tone route of Automatic are unchanged.
Tests (code-drawn): `automatic_keeps_a_rare_distinct_stem_or_patch` (2, 3, 5 and 8 flat colours and a
contrasting stem 1–4 px wide or a patch, about 0.05% to 0.5% of the picture, hard and anti-aliased at
1× and 2×: the stem's colour is kept and no other is added), `automatic_keeps_near_white_highlights_on_a_pale_background`
(ten white islands of about 12 px on #FFF4E0: kept, each still a path at Noise 16),
`automatic_keeps_a_thin_low_contrast_stem` (#909090 on #808080, 3 px), `scattered_noise_adds_no_colour`
(the negative control: the same areas as single scattered pixels add no colour) and
`noise_keeps_a_kept_rare_colours_regions_but_not_its_specks` (Noise 0 to 32).
Measured on 22 synthetic flat-colour logos drawn from known palettes, each hard 1×, anti-aliased 1×
and 2× (66 inputs, used locally and not committed; a key colour is missing when no traced colour is
within ΔE00 5): every key colour and no spurious one in **66 of 66** (was 39); missing key colours 0 (was 43),
spurious 0 (was 2), key colours sharing one traced colour 0 pairs (was 46); mean rendered ΔE00 against
the input 0.138 (was 0.348); paths 2547 → 2586, anchors 27,204 → 24,896, trace time 2.85 → 3.02 s in
all. On four public-domain scan crops (1024 × 1024) Auto Color takes the continuous-tone route and
nothing changes: 14, 72, 199 and 98 colours, the same paths, anchors and rendered error (2.11, 5.66,
9.15 and 1.00 ΔE00), time within run-to-run spread.
## Large images
Images over 1500 px on a side are traced with a warning that they trace more slowly (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
document keeps after the trace) on code-drawn photo-like pictures (40 soft-edged shaded discs on a
gradient, texture, sensor noise):
| size | preset | time | anchors | paths | peak | kept |
|:--|:--|--:|--:|--:|--:|--:|
| 2000 × 2000 | High Fidelity Photo | 4.1 s | 737,478 | 16,064 | 152 MB | 52 MB |
| 2000 × 2000 | Low Fidelity Photo | 1.6 s | 114,008 | 1,486 | 142 MB | 7 MB |
| 2000 × 2000 | Default | 0.9 s | 103,084 | 1,001 | 117 MB | 6 MB |
| 3000 × 2000 | High Fidelity Photo | 6.4 s | 1,013,114 | 25,256 | 236 MB | 75 MB |
| 3000 × 2000 | Default | 1.2 s | 145,756 | 1,574 | 162 MB | 9 MB |
| 4000 × 3000 | High Fidelity Photo | 12.8 s | 1,914,018 | 58,638 | 471 MB | 150 MB |
| 4000 × 3000 | Low Fidelity Photo | 5.1 s | 258,826 | 4,833 | 428 MB | 18 MB |
| 4000 × 3000 | Default | 2.4 s | 273,392 | 3,568 | 317 MB | 17 MB |
| 4000 × 3000 | 16 Colors | 3.0 s | 103,222 | 2,434 | 364 MB | 7 MB |
| 4000 × 3000 | Full Tone, Colors 85, Paths 90, Noise 4 | 29.6 s | 5,230,847 | 164,687 | 684 MB | 412 MB |
| 4000 × 3000 | Full Tone, Colors 100, Paths 100, Noise 1 | 37.2 s | 7,885,709 | 338,561 | 876 MB | 685 MB |
| 6000 × 4000 | Full Tone, Colors 85, Paths 90, Noise 4 | 106.1 s | 10,445,409 | 398,772 | 1169 MB | 872 MB |
| 6000 × 4000 | Full Tone, Colors 100, Paths 100, Noise 1 | 116.3 s | 13,508,871 | 658,133 | 1456 MB | 1230 MB |
(The last two rows were measured without a limit; they are refused now.) The 4000 × 3000 High Fidelity
Photo trace was 86,000 anchors under the old limit; a noisier scan of the same size would have been
refused. At 8,000,000 anchors a trace keeps about 0.7 GB, with a peak of about 0.9 GB while tracing.
## Collapsed loops keep few segments
A small closed loop (or a piece that returns to its own junction) whose fit collapsed to a single
cubic cannot stay as one, so it kept every positioned point as a straight segment. A looser Paths
collapses more loops, so it could give more segments than a tighter one. Such a loop now tries the
fewest segments that still hold it, each only if the result keeps an area (more than 0.25 px²) and
stays within 1.5 × max(tolerance, 0.5 px) of the points both ways: the fit at up to three somewhat
tighter tolerances (what a slightly higher Paths gives); for a closed loop then two cubics split at
its start and its farthest point, then the outline simplified at the tolerance and finer; the
positioned points last, as before.
Test on code-drawn images: `a_higher_paths_never_gives_fewer_segments` (32 seeded 48 px images of
soft blobs in four colours plus pixel noise, Color and Black and White, Noise 1 and 3, segments at
Paths 0, 25, 50, 75 and 100 must not fall). Before, six runs fell, e.g. 125 → 79 segments from
Paths 0 to 25 (seed 21, Color, Noise 1) and 71 → 63 → 52 from Paths 0 to 50 (seed 3, Black and
White, Noise 3); now they are 75 → 79 and 41 → 48 → 52. One image (seed 16 in Black and White) still
falls from 140 to 136 segments between Paths 0 and 25, before and after this change; that fall has
another cause, still open, and the image is left out of the test. The crate's other tests pass unchanged.
## Notches read only their own piece
Where the pixel at a sharp corner falls to the other colour, the outline cuts the corner in two
one-pixel steps, and between two straight edges of three cracks or more those two steps become one
point, where the two edges meet (a notch). Whether the edges were long enough was measured on the
whole loop. An edge can run on past a junction in one region's loop while the region across it,
whose loop turns at that junction, sees a shorter edge: one side made the notch and the other did
not, the shared piece got different points on each side, and the abutting curves left a gap. The
rule in `subpixel::outline` now also requires the three cracks of each edge next to the notch to lie
in the piece (no junction among them), as the corner-pixel correction above it already did.
Test on code-drawn images: `noisy_colour_images_leave_no_sample_uncovered` (20 seeded 36 px images
of soft overlapping blobs in four colours plus pixel noise; Color, 5 colours, Noise 1 and 2, Paths
0, 50 and 100; five sample points per pixel must each lie in some path). Without the condition 93
sample points were uncovered (seeds 9, 11 and 13); with it none are. The crate's other tests pass
## Same sub-pixel shift from either side of an edge
Each crack between two regions is moved by a sub-pixel shift read from the image. The reading
looked for a thin run (one to three pixels) on the crack's inside before one on its outside, so where
both sides were thin (a one- or two-pixel sliver beside another, as in soft gradients and noisy
scans) the two regions of the crack got different shifts, their shared piece different points, and
the abutting curves left slivers that no path covers. `Layers::shift` now averages the reading from
either side, `(s(c) - s(c flipped)) / 2`, which is exactly antisymmetric in floating point, so both
regions get the same points.
Tests on code-drawn images: `a_crack_between_thin_slivers_has_the_same_shift_from_either_side` (a
noisy ripple of 1–2 px columns at three thresholds: every crack's shift is the exact negative of its
flipped crack's, and the image does have cracks the one-sided reading gets wrong) and, in
`tests/shared_boundaries.rs`, `thin_slivers_on_both_sides_of_an_edge_leave_nothing_uncovered` (a
48 px noisy ripple in Black and White at thresholds 140, 157, 170 and Paths 0, 50, 100, sampled on
the 4 × 4 grid per pixel the shared-boundary check uses). With the one-sided reading 482–1,228
sample points per trace were uncovered; now none are. The crate's other tests pass unchanged.
## A simpler contour walk
`contour::trace_mask` follows each region's outline along the pixel cracks with the inside on the
right. It used to build a table of outgoing directions for every pixel corner in a raster pass and
then follow it. It now works out each turn during the walk from the two pixels ahead of the current
corner (right if the one ahead on the right is outside, straight if the one ahead on the left is,
else left; so at a saddle it turns right and diagonal pixels stay apart, as before). Loop starts are
found by scanning the pixels for a top edge on the outline, with one flag per pixel for a top edge
already walked. A mask of the wrong length now gives no loops instead of failing an assertion.
The output is identical: `the_walk_equals_the_stored_direction_walk` compares it with a test-only
copy of the previous walk on all 65,536 masks of 4 × 4 pixels and on 400 seeded 23 × 17 masks.
Time (best of 7, release build, 4 cores): the walk alone on 2000 × 2000 masks 19.1 ms vs 26.6 ms
(a smooth pattern), 81.3 vs 93.2 ms (12% random pixels), 223.1 vs 238.1 ms (50% random pixels);
whole traces of 1000 × 1000 images 73.1 vs 77.1 ms (Black and White), 249.6 vs 251.7 ms (Color, 6
colours) and 214.1 vs 218.6 ms (Color, 6 colours, Noise 1), with the same segment counts.
## Noise threshold stability (local development, 2026-10-14)
Noise must merge whole 4-connected regions, including pixels absorbed by an earlier merge.
@@ -1006,10 +1138,154 @@ One trace at a time on this machine (all cores); memory is the peak allocated ab
| Outline, enhanced | photo-like | 16 | 1.64 s, 632 paths, 34884 anchors, 256 MB |
| Outline, enhanced | four-colour badge | 16 | 0.73 s, 1 paths, 49 anchors, 176 MB |
## Centre lines
Measured 2026-10-17 with maximal-disc pruning and junction merging, against base `3e44478f`.
Earlier preset and cost tables retain their previous measurements. Regenerate only this class:
```text
TRACE_QUALITY_CLASS=centre-lines TRACE_QUALITY_REPORT=centre-lines.md cargo test --release -p vectorcraft-trace --test quality quality_report -- --ignored --nocapture
```
Junction clusters use their deepest pixel and a disc radius of `distance − 0.5` px. Terminal
branches shorter than `r + max(2, 0.5 × min(2r, Stroke))` are pruned, shortest first. Tiny
self-loops inside the junction disc are removed; overlapping junctions and geometrically split
crossings merge, and runs join through degree-two nodes. Simplification is bounded to 32 rounds;
nodes and branches are each capped at 200,000. An over-budget blob stays with the fill path.
Whole-blob selection, ink refinement, cap detection and curve fitting retain their current rules.
Code-drawn scenes only: 23 scenes, each anti-aliased (8×8 coverage samples) and hard (that coverage thresholded at 0.5). Coordinates, widths and scene Stroke values are fixed; junction/letter/crossing coordinates include the 0.27/0.41 px offset. Create Fills and Strokes are on, Stroke is the scene's limit (13 px, mix 6 px, varying 12 px), Black and White threshold 128, Noise 4, Ignore White on, other settings at their defaults. Today's tracer has no minimum-length setting (the dashed scenes specify 0 for future use).
Distances use dense exact centre samples (≤0.25 px), emitted curves sampled at ≤0.5 px control-polygon steps, and nearest-point searches capped at 30 px. Found is the fraction of true samples within 1.5 px; width errors use output samples within 2 px of a true stroke. Samples inside exact fills are excluded. Topology is recovered from emitted subpath endpoints and intersections, merging coincident positions within 0.05 px; an endpoint contributes one arm, an interior two. Junction matching radius is local true width + 3 px; ends use half-width + 4 px. Each match is used once. Free ends exclude true junctions, corners, other strokes and fills; unmatched emitted ends are spurs. Junctions/ends within 2 px of fills are excluded. End error is distance, with signed error along the outward tangent in parentheses. Coverage is a union of painted output at pixel centres (0.02 px flattening tolerance, actual emitted caps), compared with the exact swept shape at pixel centres, for both input variants. Area error is signed relative to exact area. Anchors counts stroke anchors, including all compound subpaths; paths counts stroked + filled paths. Time covers tracing only, one scene at a time. Profile-dependent columns are unavailable (–).
### Centre line and width (px)
| scene | input | paths | out→true mean / p95 | true→out mean / p95 | found % | width err uniform mean / p95 | variable mean / p95 | profile mean |
|---|---|---|---|---|---|---|---|---|
| lines w1 | aa | 93+0 | 0.09 / 0.16 | 2.64 / 30.00 | 90.9 | 0.05 / 0.13 | – | – |
| lines w1 | hard | 92+0 | 0.18 / 0.38 | 1.93 / 5.01 | 82.3 | 0.14 / 1.00 | – | – |
| lines w2 | aa | 24+0 | 0.11 / 0.27 | 0.16 / 0.29 | 100.0 | 0.03 / 0.06 | – | – |
| lines w2 | hard | 24+0 | 0.22 / 0.38 | 0.25 / 0.42 | 100.0 | 0.18 / 0.59 | – | – |
| lines w3 | aa | 24+0 | 0.09 / 0.21 | 0.15 / 0.27 | 100.0 | 0.04 / 0.09 | – | – |
| lines w3 | hard | 24+0 | 0.20 / 0.42 | 0.24 / 0.46 | 100.0 | 0.22 / 0.54 | – | – |
| lines w4 | aa | 24+0 | 0.09 / 0.20 | 0.15 / 0.26 | 100.0 | 0.06 / 0.12 | – | – |
| lines w4 | hard | 24+0 | 0.20 / 0.42 | 0.24 / 0.46 | 100.0 | 0.12 / 0.46 | – | – |
| lines w6 | aa | 24+0 | 0.10 / 0.22 | 0.15 / 0.26 | 100.0 | 0.09 / 0.19 | – | – |
| lines w6 | hard | 24+0 | 0.20 / 0.40 | 0.24 / 0.43 | 100.0 | 0.24 / 0.40 | – | – |
| lines w8 | aa | 24+0 | 0.11 / 0.26 | 0.16 / 0.29 | 100.0 | 0.13 / 0.26 | – | – |
| lines w8 | hard | 24+0 | 0.23 / 0.56 | 0.26 / 0.59 | 100.0 | 0.13 / 0.49 | – | – |
| lines w10 | aa | 24+0 | 0.11 / 0.24 | 0.16 / 0.29 | 100.0 | 0.16 / 0.32 | – | – |
| lines w10 | hard | 24+0 | 0.24 / 0.64 | 0.27 / 0.67 | 99.9 | 0.24 / 0.61 | – | – |
| lines w12 | aa | 24+0 | 0.12 / 0.30 | 0.17 / 0.32 | 100.0 | 0.20 / 0.39 | – | – |
| lines w12 | hard | 24+0 | 0.24 / 0.60 | 0.27 / 0.61 | 99.9 | 0.25 / 0.52 | – | – |
| curves w2 | aa | 4+0 | 0.11 / 0.22 | 0.15 / 0.26 | 100.0 | 0.01 / 0.01 | – | – |
| curves w2 | hard | 4+0 | 0.22 / 0.55 | 0.25 / 0.57 | 100.0 | 0.05 / 0.06 | – | – |
| curves w4 | aa | 4+0 | 0.12 / 0.25 | 0.16 / 0.28 | 99.9 | 0.01 / 0.03 | – | – |
| curves w4 | hard | 4+0 | 0.24 / 0.57 | 0.26 / 0.56 | 99.9 | 0.00 / 0.00 | – | – |
| curves w8 | aa | 4+0 | 0.16 / 0.30 | 0.22 / 0.34 | 98.7 | 0.02 / 0.06 | – | – |
| curves w8 | hard | 4+0 | 0.26 / 0.61 | 0.29 / 0.63 | 98.8 | 0.00 / 0.00 | – | – |
| circles | aa | 25+0 | 0.09 / 0.23 | 0.24 / 0.30 | 97.6 | 0.00 / 0.02 | – | – |
| circles | hard | 23+0 | 0.27 / 0.64 | 0.45 / 0.75 | 96.5 | 0.02 / 0.10 | – | – |
| junctions w2 | aa | 7+0 | 0.14 / 0.34 | 0.18 / 0.35 | 100.0 | 0.12 / 0.03 | – | – |
| junctions w2 | hard | 7+0 | 0.36 / 0.82 | 0.40 / 0.85 | 100.0 | 0.14 / 0.12 | – | – |
| junctions w4 | aa | 7+0 | 0.17 / 0.72 | 0.20 / 0.49 | 99.5 | 0.27 / 5.00 | – | – |
| junctions w4 | hard | 7+0 | 0.42 / 1.11 | 0.42 / 0.98 | 99.2 | 0.30 / 5.00 | – | – |
| junctions w8 | aa | 7+0 | 0.28 / 1.49 | 0.31 / 1.45 | 95.1 | 0.30 / 0.04 | – | – |
| junctions w8 | hard | 7+0 | 0.47 / 1.36 | 0.50 / 1.47 | 95.1 | 0.33 / 0.22 | – | – |
| crossings w3 | aa | 5+0 | 0.14 / 0.34 | 0.18 / 0.31 | 100.0 | 0.05 / 0.09 | – | – |
| crossings w3 | hard | 5+0 | 0.39 / 1.14 | 0.40 / 0.93 | 99.9 | 0.20 / 0.32 | – | – |
| crossings w6 | aa | 5+0 | 0.42 / 2.16 | 0.38 / 1.79 | 92.3 | 0.10 / 0.19 | – | – |
| crossings w6 | hard | 5+0 | 0.57 / 2.27 | 0.54 / 1.95 | 90.9 | 0.25 / 0.36 | – | – |
| dashed w2 | aa | 31+0 | 0.10 / 0.22 | 0.15 / 0.25 | 100.0 | 0.02 / 0.05 | – | – |
| dashed w2 | hard | 31+0 | 0.14 / 0.36 | 0.18 / 0.38 | 100.0 | 0.09 / 0.34 | – | – |
| dashed w4 | aa | 29+0 | 0.11 / 0.28 | 0.15 / 0.31 | 100.0 | 0.01 / 0.03 | – | – |
| dashed w4 | hard | 29+0 | 0.15 / 0.42 | 0.19 / 0.41 | 100.0 | 0.11 / 0.46 | – | – |
| letters w3 | aa | 7+0 | 0.19 / 0.86 | 0.22 / 0.62 | 98.7 | 0.01 / 0.13 | – | – |
| letters w3 | hard | 7+0 | 0.33 / 0.92 | 0.34 / 0.71 | 99.0 | 0.06 / 0.20 | – | – |
| letters w5 | aa | 7+0 | 0.27 / 1.49 | 0.29 / 1.31 | 95.9 | 0.02 / 0.20 | – | – |
| letters w5 | hard | 7+0 | 0.40 / 1.48 | 0.41 / 1.29 | 96.2 | 0.05 / 0.20 | – | – |
| mix | aa | 3+1 | 0.10 / 0.21 | 12.89 / 30.00 | 57.3 | 0.03 / 0.14 | – | – |
| mix | hard | 3+1 | 0.22 / 0.43 | 12.95 / 30.00 | 57.3 | 0.08 / 0.15 | – | – |
| varying | aa | 5+0 | 0.13 / 0.28 | 0.19 / 0.31 | 99.5 | 1.89 / 4.13 | – | – |
| varying | hard | 5+0 | 0.20 / 0.51 | 0.24 / 0.52 | 99.7 | 1.90 / 4.22 | – | – |
### Topology, ends, coverage
| scene | input | junctions ok / wrong deg / missed / extra | junction pos err | ends found | end err (signed) | spurs | IoU uniform / profile | area err uniform / profile | fill→fill % | anchors | width pts | ms |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| lines w1 | aa | 0 / 0 / 0 / 76 | – | 43/48 | 0.71 (-0.69) | 88 | 0.790 / – | -6.2% / – | – | 186 | – | 4.6 |
| lines w1 | hard | 0 / 0 / 0 / 0 | – | 42/48 | 1.50 (-1.42) | 142 | 0.644 / – | -25.8% / – | – | 184 | – | 3.6 |
| lines w2 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.32 (-0.20) | 0 | 0.914 / – | -0.3% / – | – | 71 | – | 4.3 |
| lines w2 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.38 (-0.10) | 0 | 0.808 / – | -2.0% / – | – | 58 | – | 4.2 |
| lines w3 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.26 (-0.17) | 0 | 0.931 / – | -3.0% / – | – | 64 | – | 4.4 |
| lines w3 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.24) | 0 | 0.881 / – | -2.5% / – | – | 50 | – | 4.3 |
| lines w4 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.25 (-0.18) | 0 | 0.964 / – | -0.6% / – | – | 70 | – | 4.6 |
| lines w4 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.15) | 0 | 0.906 / – | -0.1% / – | – | 54 | – | 4.5 |
| lines w6 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.30 (-0.18) | 0 | 0.965 / – | -0.3% / – | – | 65 | – | 5.1 |
| lines w6 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.45 (-0.12) | 0 | 0.923 / – | 1.4% / – | – | 55 | – | 4.9 |
| lines w8 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.37 (-0.20) | 0 | 0.967 / – | -1.0% / – | – | 66 | – | 5.4 |
| lines w8 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.46 (-0.18) | 0 | 0.934 / – | -0.3% / – | – | 53 | – | 5.4 |
| lines w10 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.35 (-0.16) | 0 | 0.969 / – | -0.7% / – | – | 66 | – | 5.9 |
| lines w10 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.50 (-0.12) | 0 | 0.942 / – | -0.1% / – | – | 52 | – | 5.9 |
| lines w12 | aa | 0 / 0 / 0 / 0 | – | 48/48 | 0.40 (-0.16) | 0 | 0.969 / – | -0.6% / – | – | 70 | – | 6.4 |
| lines w12 | hard | 0 / 0 / 0 / 0 | – | 48/48 | 0.54 (-0.11) | 0 | 0.949 / – | -0.2% / – | – | 53 | – | 6.3 |
| curves w2 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.18 (-0.13) | 0 | 0.921 / – | -0.6% / – | – | 34 | – | 4.7 |
| curves w2 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.22 (-0.05) | 0 | 0.821 / – | -2.4% / – | – | 29 | – | 4.7 |
| curves w4 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 0.56 (-0.42) | 0 | 0.949 / – | -0.8% / – | – | 35 | – | 5.1 |
| curves w4 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 0.66 (-0.38) | 0 | 0.904 / – | -0.5% / – | – | 30 | – | 5.2 |
| curves w8 | aa | 0 / 0 / 0 / 0 | – | 8/8 | 2.24 (-2.01) | 0 | 0.956 / – | -0.9% / – | – | 42 | – | 5.9 |
| curves w8 | hard | 0 / 0 / 0 / 0 | – | 8/8 | 1.97 (-1.67) | 0 | 0.936 / – | -0.5% / – | – | 31 | – | 5.9 |
| circles | aa | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 32 | 0.964 / – | -0.9% / – | – | 71 | – | 7.4 |
| circles | hard | 0 / 0 / 0 / 0 | – | 0/0 | – (–) | 28 | 0.888 / – | -0.7% / – | – | 52 | – | 8.0 |
| junctions w2 | aa | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.32 (-0.16) | 0 | 0.889 / – | 6.1% / – | – | 84 | – | 3.7 |
| junctions w2 | hard | 7 / 0 / 0 / 0 | 1.17 | 23/23 | 0.49 (-0.15) | 0 | 0.771 / – | 4.9% / – | – | 59 | – | 3.6 |
| junctions w4 | aa | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.33 (-0.26) | 0 | 0.919 / – | 5.0% / – | – | 100 | – | 4.1 |
| junctions w4 | hard | 7 / 0 / 0 / 1 | 1.62 | 23/23 | 0.65 (-0.32) | 0 | 0.850 / – | 7.0% / – | – | 81 | – | 4.0 |
| junctions w8 | aa | 7 / 0 / 0 / 2 | 2.76 | 23/23 | 0.43 (-0.27) | 0 | 0.937 / – | 2.7% / – | – | 93 | – | 4.7 |
| junctions w8 | hard | 7 / 0 / 0 / 1 | 2.76 | 23/23 | 0.80 (-0.35) | 0 | 0.913 / – | 2.5% / – | – | 77 | – | 4.7 |
| crossings w3 | aa | 5 / 0 / 0 / 2 | 0.25 | 20/20 | 0.27 (-0.18) | 0 | 0.935 / – | -1.1% / – | – | 74 | – | 2.8 |
| crossings w3 | hard | 5 / 0 / 0 / 5 | 0.25 | 20/20 | 0.41 (-0.17) | 0 | 0.791 / – | -7.6% / – | – | 69 | – | 2.8 |
| crossings w6 | aa | 5 / 0 / 0 / 1 | 0.25 | 20/20 | 0.35 (-0.17) | 0 | 0.925 / – | -3.6% / – | – | 112 | – | 3.3 |
| crossings w6 | hard | 5 / 0 / 0 / 4 | 0.25 | 20/20 | 0.51 (-0.20) | 0 | 0.872 / – | -2.0% / – | – | 81 | – | 3.4 |
| dashed w2 | aa | 0 / 0 / 0 / 0 | – | 62/62 | 0.28 (-0.20) | 0 | 0.927 / – | -2.6% / – | – | 64 | – | 2.0 |
| dashed w2 | hard | 0 / 0 / 0 / 0 | – | 62/62 | 0.33 (-0.11) | 0 | 0.926 / – | -3.6% / – | – | 67 | – | 1.9 |
| dashed w4 | aa | 0 / 0 / 0 / 0 | – | 58/58 | 0.30 (-0.20) | 0 | 0.952 / – | -0.1% / – | – | 61 | – | 2.1 |
| dashed w4 | hard | 0 / 0 / 0 / 0 | – | 58/58 | 0.37 (-0.08) | 0 | 0.944 / – | -1.5% / – | – | 60 | – | 2.1 |
| letters w3 | aa | 7 / 0 / 0 / 6 | 1.47 | 17/17 | 0.62 (-0.47) | 1 | 0.923 / – | -0.9% / – | – | 114 | – | 3.3 |
| letters w3 | hard | 7 / 0 / 0 / 3 | 1.47 | 17/17 | 0.38 (-0.21) | 1 | 0.859 / – | -0.6% / – | – | 81 | – | 3.3 |
| letters w5 | aa | 6 / 1 / 0 / 6 | 2.10 | 17/17 | 0.99 (-0.83) | 0 | 0.929 / – | -1.7% / – | – | 124 | – | 3.7 |
| letters w5 | hard | 6 / 1 / 0 / 8 | 2.10 | 17/17 | 1.05 (-0.79) | 0 | 0.887 / – | -2.0% / – | – | 100 | – | 3.7 |
| mix | aa | 0 / 0 / 0 / 0 | – | 6/7 | 0.24 (-0.17) | 0 | 0.986 / – | 0.1% / – | 99.7 | 11 | – | 6.5 |
| mix | hard | 0 / 0 / 0 / 0 | – | 6/7 | 0.57 (-0.46) | 0 | 0.969 / – | -0.0% / – | 99.1 | 11 | – | 6.4 |
| varying | aa | 0 / 0 / 0 / 0 | – | 10/10 | 2.03 (-1.08) | 0 | 0.735 / – | 4.8% / – | – | 22 | – | 5.2 |
| varying | hard | 0 / 0 / 0 / 0 | – | 10/10 | 1.92 (-0.94) | 0 | 0.736 / – | 5.2% / – | – | 22 | – | 5.3 |
**aa totals**: junctions 44 correct, 1 wrong degree, 0 missed, 94 extra (of 45); ends 682 of 688; spurs 121.
**hard totals**: junctions 44 correct, 1 wrong degree, 0 missed, 22 extra (of 45); ends 681 of 688; spurs 171.
**Scope of the improvement:** correct junction degrees rise from 30/45 to 44/45 in each variant;
missed junctions fall from two to zero. Extra junctions fall from 104 to 94 aa and 27 to 22 hard.
Spur totals remain 121 aa / 171 hard: the one-pixel-line and circle scenes contribute 120 / 170
of those, with unchanged path counts (one-pixel lines: 93 aa / 92 hard; circles: 25 aa / 23 hard).
The graph stage does not reconnect separate blobs. Raw graph tests cover straight lines at 24
angles, junctions and 20–90° crossings; these do not replace the emitted-path measurements above.
The remaining spur/extra-junction target is not met by this graph-only change.
## Revision history
| date | change |
|:--|:--|
| 2026-10-17 | Auto Color keeps rare but distinct colours (stems, highlights, near-white on pale backgrounds): 66 of 66 flat test logos keep every key colour (was 39); scans unchanged |
| 2026-10-17 | Images over 1500 px on a side trace with a warning; the anchor limit is 8,000,000 (2,000,000 on the web); time and memory of 2000 × 2000 to 6000 × 4000 code-drawn photos |
| 2026-10-17 | `Raster::new` returns an error for RGBA bytes of the wrong length (it failed an assertion) and `Raster::pixel` reads nothing past the raster; traces of valid images unchanged |
| 2026-10-17 | M12: maximal-disc graph pruning, split-crossing merges and degree-two joins; correct junction degrees 30→44/45 aa and hard, none missed; remaining spurs unchanged. Centre-lines rerun, scores and earlier tables unchanged. |
| 2026-10-17 | M12: exact per-blob Euclidean distances and Guo–Hall thinning; centre-lines rerun on 46 inputs. Ends found: aa 638→682/688, hard 631→681/688. Whole-blob stroke/fill choice retained; preset/cost tables unchanged. |
| 2026-10-17 | M12: add the ignored centre-lines class and measure the existing Create Strokes output on 23 code-drawn scenes, aa and hard; centre/width/topology/end/coverage metrics, no new images, dependencies or behaviour |
| 2026-10-16 | Image Trace: a loop or junction-returning piece whose fit collapsed to one cubic keeps the fewest segments that hold its area and course instead of every positioned point, so a higher Paths no longer gives fewer segments on code-drawn noisy images (six falling runs to none; one other cause remains). |
| 2026-10-16 | Image Trace: a notch corner is cut only when the straight edges beside it lie in the shared piece, so both regions of the piece get the same points (code-drawn noisy colour images: 93 uncovered sample points to 0). |
| 2026-10-16 | Image Trace: a crack's sub-pixel shift is the mean of the readings from its two sides, exactly antisymmetric, so abutting regions get the same points on thin slivers (a code-drawn noisy ripple went from 482–1,228 uncovered sample points per trace to 0). |
| 2026-10-16 | Image Trace: the contour walk works out each turn from the pixels as it goes instead of following a precomputed table of per-corner directions; identical loops on all 65,536 4 × 4 masks and 400 seeded masks, and no slower (whole 1 MP traces 1–5% faster). |
| 2026-10-14 | Closed fitting retains the positioned boundary when straightening collapses a nonzero-area loop; four generated regressions; preset scores unchanged |
| 2026-10-14 | Tiny closed fits retain their positioned outlines instead of losing islands/holes; three generated regression tests; quality-harness baseline unchanged |
| 2026-10-14 | Gradients: a gradient that noise or a soft rim broke into pieces is found in its largest piece and grown over the rest (blurred and noisy linear gradients: 3 to 11 paths beside the gradient down to 0.5 with 6 Colors, 24 to 56 down to 0.5 to 1.5 with High Fidelity Photo) |
+13 -9
View File
@@ -1,6 +1,6 @@
# 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-16 · **Change:** minor (Georgian (`ka`) interface catalog; 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)
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
@@ -17,16 +17,16 @@ 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` | 3,647 rows; menus, panels, dialogs complete (tests); 85% overall | messages in English (1 of 604) | 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 | partial | 2–3 |
| 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) |
| 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), untested for Devanagari, but the egui interface does no complex shaping | no | none | 20–30 |
| 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) |
@@ -45,19 +45,20 @@ 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,741 rows, 742 of them `@msg` (counted 2026-10-17): menus, `tl!` literals and messages complete under the tests (80% overall) | partial |
| Italian | `it` | 4,011 / 4,020 | 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 |
| Finnish | `fi` | 5,218 rows: menus, `tl!` literals and messages complete (tests); one/other plurals, 769 `@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 |
| Georgian | `ka` | 5,066 rows (counted 2026-10-18): 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
Nineteen 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.
@@ -72,8 +73,10 @@ 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. Other catalogs fall back to English for new variable/template lookups. Native review is
still needed, and catalog coverage does not prove that every variable in the UI is translated.
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.
Native review is still needed, and catalog coverage does not prove that every variable in the UI is
translated.
## Not done in any language
@@ -97,5 +100,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 |
+858 -155
View File
File diff suppressed because it is too large Load Diff
+82 -21
View File
@@ -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,21 +23,29 @@ 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?, size, source}], "folder": report}` |
| `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. |
| `plugin.remove` | `{"id"}` | Uninstalls. |
| `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.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. |
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).
**Preference folder.** Edit › Preferences › Performance & Storage › *Additional Plug-ins Folder*: when set, every
`*.wasm` in that folder (not recursive) is installed at startup and whenever the preference changes (desktop builds;
@@ -56,11 +67,14 @@ 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`; 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 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.
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
@@ -98,21 +112,48 @@ 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.
- `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 +161,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": [
@@ -160,9 +209,13 @@ object, holding only geometry (`type`, `path`, `fillRule`, `bounds`).
"_context": {"mode": "filter", "bounds": [10, 0, 75, 60], "artboard": [0, 0, 612, 792]}}
```
`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. 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
@@ -188,6 +241,10 @@ keeps its swatch link; any other paint is unlinked.
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 +260,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.
+125
View File
@@ -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** or **Letter**.
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.
+7
View File
@@ -159,6 +159,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
+5 -4
View File
@@ -10,8 +10,9 @@ line, give it a nonzero offset. A vertical line at zero offset lies on the axis
**Revolve angle** sweeps 0 to 360 degrees. **Rotation X/Y/Z** rotates the viewing direction, with all three axes defaulting to 0 degrees. **Perspective**
runs 0° to 160°, as documented: 0° is an orthographic view, and wider angles bring the eye nearer (see
[three-d.md](three-d.md#perspective)). **Segments** controls radial resolution (8 to 128 for a full turn).
Lighting is one directional light with the documented Light Settings (see [three-d.md](three-d.md#lighting)):
**Intensity**, **Rotation**, **Height**, **Softness** and **Ambient Light**, and **Light behind the object**.
Lighting supports one to ten directional lights with the documented Light Settings (see [three-d.md](three-d.md#lighting)):
**Color**, **Intensity**, **Rotation**, **Height**, **Softness** and **Ambient Light**, and **Light behind the object**.
Color is white by default, with a picker and a reset dot. Colored light preserves neutral ambient illumination.
Turn **Shading** off to keep the source paint without lighting.
Shaded Revolve surfaces automatically use smooth lighting from the profile's tangents,
@@ -44,7 +45,7 @@ horizontally to turn the light round the object; drag the **Height** line or its
raise or lower it. The labels remain usable when the ring shrinks to a point (Height 90°).
Hold Shift to snap to 15-degree increments. Light gestures update their matching fields without
rotating the object, and keep the same temporary preview, Escape, Cancel and undo behaviour.
Intensity, Softness and Ambient Light are draggable fields and sliders.
Intensity, Softness and Ambient Light are draggable fields and sliders. The Lights list selects which light the gizmo and settings edit; Add Light and Delete Light manage up to ten lights, keeping at least one. Ambient is shared and applied once. Preview, Cancel, Reset all and undo include the whole list.
**Shading** off hides the light gizmo and disables lighting controls. Switch to **Rotation** to return
to the object rings. **Show axis** in **Advanced** displays the source's vertical axis on the object.
@@ -89,7 +90,7 @@ with the standard command.
- 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.
- Object-level Revolve uses the top visible fill, falling back to a stroke. Per-item Revolve uses that item's paint.
- Smooth diffuse shading, one directional light, no textures, cast shadows or physical materials. Smooth shading interpolates lighting in projected triangles; it does not smooth the silhouette or add specular highlights.
- Smooth diffuse shading, one to ten directional lights, no textures, cast shadows or physical materials. Smooth shading interpolates lighting in projected triangles; it does not smooth the silhouette or add specular highlights.
- Faces use back-to-front depth sorting. Caps facing the camera cover their rim seams; caps facing away sit behind the body. Intersecting profiles can have incorrect visibility.
- Curves use sixteen samples per segment; profiles are limited to 1,024 samples and surfaces to 32,768 faces. Smooth shaded side quads become two triangles and share that same limit. A continuous cap retains its original radial mesh cost within this limit. Projected ring curves have a 0.001-point fitting tolerance and a shared 32,768-curve limit.
- Axis, bounds and ordinary selection handles continue to refer to the editable 2D source profile.
+44 -12
View File
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -9,10 +9,12 @@ Scribble over overlapping paths to make an editable Shaper Group:
- End the scribble outside the artwork to remove the touched fills and exposed boundary strokes. Shared edges along surviving filled areas remain, closing their contours. Compatible stroke pieces join at the new corners, so the selected stroke’s join style applies there.
- Scribble over an unfilled stroke segment to remove that piece. A line passing through a shape is split at the intersections, so its protruding ends can be removed separately.
Click once to select the group; click again to select its visible fill and change its color. Double-click with Shaper to enter construction editing and select an original shape. The Selection tool then moves or resizes it normally, and the visible composition updates. Escape leaves construction editing. The original live shapes and edit recipes survive native save/reopen. Outside construction editing, selection highlights and bounds show only the visible result.
Click once to select the group; click again to select its visible fill and change its color. Double-click with Shaper to enter construction editing and select an original shape. The Selection tool then moves or resizes it normally, and the visible composition updates. Escape leaves construction editing. The original live shapes and edit recipes survive native save/reopen. Outside construction editing, selection highlights and bounds show only the visible result. Outside construction editing, a marquee selects the group or its faces only where it reaches the visible result, and Direct Selection's clicks and marquee select only the anchor points of the visible result.
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.
+260 -49
View File
File diff suppressed because one or more lines are too long
+48 -17
View File
@@ -50,7 +50,25 @@ middle.
| Volume | 0–100 %. The surface is a membrane under even pressure, held along the outline (and around holes): it rises vertically from the edge and rounds smoothly inside, without creases along the shape's middle. At 50 % a circle becomes a half sphere; 100 % puffs twice as high, 0 % is flat. |
| Both sides | The back puffs out too; otherwise it is flat. |
The surface is cut into a grid of 48 facets across the larger side, halved or quartered where it rises steeply near the outline (the cells meet without cracks), each clipped to the shape and flat shaded.
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
@@ -83,18 +101,23 @@ saves the angle.
## Lighting
One white directional light, with the public panel options' Light Settings in their order (Color aside):
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 | |
|---|---|
| Color | Opaque RGB light color, white by default. The picker previews changes; its dot resets to white. |
| Intensity | 0–100 %, default 80 %: the light's brightness. |
| Rotation | −180° to 180°: round the object on the page. Our reading: 0° is on the object's right, counterclockwise, as Illustrator's other angles. |
| Height | 0° to 90°: from beside the object (0°) to straight in front of it (90°). |
| Softness | 0–100 %, default 0 %: how far round the object the light reaches. Our reading: at 0 % the light falls off with the angle to nothing at 90° (as before), at 100 % it reaches nothing only straight away from the light. |
| Ambient Light | 0–200 %, default 25 %: light from everywhere. Faces are never brighter than the art's own colour, so past 100 % (less the light's share) it adds nothing more. |
| Ambient Light | 0–200 %, default 25 %: neutral light from everywhere. Each channel is clamped to the art's own color; at 100 % ambient every channel is fully lit, so higher values add nothing. |
| Light behind the object | The documented "Move selected light to the back of object": the light shines from behind. |
The documentation gives no defaults; ours keep the earlier light: Rotation 125.26° and Height 30° are the direction
Colored illumination multiplies each source RGB channel by `clamp(ambient + sum(intensity * diffuse * color), 0, 1)`. Each light has its own direction and softness. Ambient is added once, and channel saturation happens after the sum. Ambient light stays neutral. Softness supplies the same diffuse falloff as before. White supplies neutral illumination; Shading off ignores the lights. Solid fills and gradient stops (including freeform points) receive the color. Pattern fills retain the existing neutral shadow approximation because their referenced tiles are not evaluated per face. SVG/PDF and Expand Appearance keep the shaded solids and gradients as vectors.
Revolve interpolates the directional illumination before clipping each RGB channel, adding vector stops where a channel saturates. Its continuous caps keep flat lighting. A source gradient and the light ramp are combined with isolated vector Multiply groups; a vector clip applies the curved face boundary once, avoiding antialiasing seams, and a vector opacity mask preserves transparent stops when needed. With multiple lights, each RGB channel has its own affine vector gradient; disjoint channel ramps combine through Screen before multiplying the source paint. This preserves differently colored directions and clips each channel after interpolation. No raster image is needed.
The documentation gives no numeric defaults; ours keep the earlier light: Rotation 125.26° and Height 30° are the direction
the default light had (45° left of the eye and 45° up). Effects saved before (and scripts) give the light by
`lightAzimuth` and `lightElevation`; those are read as the Rotation, Height and side of the same direction, so the
shading is exactly as before.
@@ -104,9 +127,9 @@ shading is exactly as before.
`threeD.extrude`: `depth`, `twist`, `taper` (%), `cap`, `bevel` (`none`, `classic`, `rounded`, `cove`, `step`,
`ogee`), `bevelWidth`, `bevelHeight`, `bevelRepeat`, `bevelSpace` (%), `bevelInside`, `bevelBothSides`.
`threeD.inflate`: `depth`, `volume` (%), `bothSides`. All three take the view and lighting parameters Revolve
uses: `rotationX`, `rotationY`, `rotationZ`, `perspectiveAngle` (degrees), `shade`, `lightIntensity` (%), `lightRotation`,
uses: `rotationX`, `rotationY`, `rotationZ`, `perspectiveAngle` (degrees), `shade`, `lightColor` (`#rrggbb`, default `#ffffff`), `lightIntensity` (%), `lightRotation`,
`lightHeight` (degrees), `lightSoftness` (%), `ambient` (%, to 200), `lightBehind`. Lengths are points and scale with the object (Scale Strokes & Effects).
Commands validate the shape and the parameters before the document changes.
Commands validate the shape and the parameters before the document changes. All four also take `lights`: null (the default, use the earlier single-light fields), or an array of one to ten objects. Each object takes `color` (opaque `#rrggbb`, white), `intensity` (0..100%, 80), `rotation` (-180..180 degrees, 125.26), `height` (0..90 degrees, 30), `softness` (0..100%, 0), and `behind` (boolean, false). Omitted settings use those defaults. An explicit list replaces the earlier single light; `ambient` and `shade` remain shared. Set `lights` to null to return to the single-light fields. Invalid lists, counts, keys and values are rejected before editing; malformed loaded lists fall back to the earlier light safely.
## What differs from the public documentation
@@ -115,19 +138,27 @@ 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), **several lights**, light colour, light presets, **shadows**
and **ray-traced rendering** are not built: shading is one directional light, flat per face, painted with the
object's top fill (else its top stroke) for Extrude & Bevel and Inflate.
- **Materials** (base, Adobe Substance and graphics), light presets, **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.
- Shading is flat per facet: an inflated rim seen almost edge-on shows faint banding where the facets get finer.
- 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.
Release measurements and a repeatable CLI scene are in [3D lighting performance](3d-lighting-performance.md).
## Revision history
| Date | Change |
|---|---|
| 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. |
+17 -8
View File
@@ -1,6 +1,6 @@
# Type parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-17 · **Change:** minor (a click on the out port threads the text into a closed path, a new frame at the click or a dragged rectangle, #2572 in part; scores unchanged; native family labels fall back to localized legacy names when typographic names are English-only, #2208; scores unchanged; Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)
Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in
[target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the
@@ -18,28 +18,32 @@ against Illustrator's public documentation; **estimated** unless noted.
| Feature | Status | Notes |
|---|---|---|
| Native font-family labels | partial | With Show Font Names in English off, localized legacy names are used when typographic names contain no native label (#2208); loaded-font and catalog-only regressions. Choosing among multiple native languages is unchanged. |
| Point, area and type-on-a-path type; vertical variants | done / partial | vertical point, area and path type have initial support; fullwidth `!` / `?` keep the nominal glyph upright when no vertical alternate exists (Unicode Tu); in a non-rectangular area a word too wide for the top lines moves down to the first line that holds it (#1615); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802); `text.create` and `text.createInPath` take `justify` (#1752); MCP `add_text` forwards `vertical` to both commands |
| Area Type Options (rows, columns, inset, first baseline, Auto Size, Shrink Text to Fit) | done | the commands that make or restyle type say when it does not fit its frame; MCP `add_text` takes Auto Size and Shrink Text to Fit (#1471) |
| Threaded text, Fit Headline, Text Wrap | done | Fit Headline sees wrapping in a frame one line tall (#1444) |
| Threading from the out port: a click on the red "+" loads the text; a click on a closed path or area type threads into it, a click on empty canvas makes a frame of the same shape and size there, a drag draws a rectangular frame; Escape or another tool cancels | partial | #2572 in part: the Selection tool, `text.thread.add {from, to?, rect?, at?}`; only the red port of overflowing area type is clickable (empty ports and in ports are not drawn, so they can't be clicked yet), type on a path doesn't thread, and the loaded cursor borrows the I-beam until it's drawn |
| Type on a Path effects and options; brackets | done | #429 |
| Character and Paragraph panels, styles with overrides | done | Character-style Auto-leading resets, partial definitions, local leading overrides, override markers, Undo/Redo and native serialization have regression coverage; differing paragraph alignments leave the shared alignment buttons unselected (headless regressions, full integration CI and visual review pass); mixed-value display for other paragraph fields remains missing |
| Character size and horizontal scale after object/group scaling | done | #1034: fields and setters compensate the stored affine; point size follows the transformed em-height axis, not the glyph ink bounds. Range edits, keyboard increments, leading and baseline shift use document points; rotation, reflection and shear remain editable. |
| Preserve font sizes while scaling a group | missing | Separate enhancement; scaling still scales text with the other artwork. |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference unused (#394); stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607) |
| OpenType panel, Glyphs panel, alternates | done | Highlight Alternate Glyphs preference (#394, off by default) paints pink behind OpenType substitutions on screen when on; stylistic sets ss01–ss20, stylistic and titling alternates, proportional and lining figures, superscript and subscript are character-style features (`text.setStyle {features}`, #1607) |
| Font menu: samples, live preview, filters, favourites; font matching by any name; Find Font | done | #339; script filter: Japanese, Latin, Thai; the font scan on Linux also reads the folders fontconfig's configuration lists, such as TeX fonts under `/usr/share/texmf/fonts` (#1548, fixes #1529; not verified on Linux) |
| Missing glyphs set in an installed font made for their script (Thai, Lao, Khmer, Myanmar first; then any font that has them) | done | Thai: Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai, TLWG |
| Full font-name matching (OpenType name ID 4) | done | #1258: loaded/cataloged faces and Missing Fonts searches; localized names and variable default faces; Windows font-service QA pending |
| Variable fonts | partial | named instances as styles; axis sliders missing |
| Variable fonts | partial | named instances as styles; Character panel › Variable Font sets each axis (`fontAxes`); the font menu's Variable filter is missing |
| Fallback glyphs keep the text's weight and slant | done | #2491: a character from a fallback face is set in that family's face nearest the run's weight and slant; Bold Italic of a missing family is drawn Bold (Source Sans 3 has no Bold Italic) rather than Italic |
| Colour fonts (OpenType SVG / COLR) | partial | |
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
| Hyphenation… and Justification… options | missing | |
| Optical Margin Alignment | missing | menu stub |
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right paragraphs; type on a path and right-to-left paragraphs do not hang |
| Roman Hanging Punctuation | done | Paragraph panel menu › Roman Hanging Punctuation, `text.setFormat {romanHangingPunctuation}` (#2756): per paragraph, Optical Margin Alignment's punctuation and amounts without the letters; paragraph styles carry it. Evidence: `tests::roman_hanging_punctuation_hangs_punctuation_but_not_letters`, `typecmd::area_tests::roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on` |
| Hyphenation… and Justification… options | done | Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310). Justification… (`ui.justification`, `text.setFormat`, #2755) sets Word Spacing, Letter Spacing and Glyph Scaling (Minimum, Desired, Maximum), Auto Leading and Single Word Justification; Desired applies to every line, justified lines and the Every-line Composer use the ranges. Vertical type isn't glyph scaled; type on a path doesn't use them. Evidence: `crates/text/src/tests_justify.rs`, `dialogs::justification::tests` |
| Tab stops and leaders; Tabs panel | partial | Tabs panel and stops with leaders in the model (`text.tabs.set`); a stop drags from where the press landed (#1458); leaders fill the tab's gap, repeated on a grid so they line up from line to line (#1877) |
| Hidden characters (Show Hidden Characters) | done | `type.hiddenCharacters` |
| Spell check (Check Spelling, Auto Spell Check, custom dictionary) | missing | menu stubs; needs an openly licensed dictionary |
| Smart Punctuation, Change Case, Find & Replace | done | |
| Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | |
| Touch Type tool | missing | listed in the toolbar, not implemented |
| Caret: the arrows and Delete step over whole clusters (Thai consonant with its marks, combining accents, emoji); Backspace removes one character | done | in text with right-to-left characters the arrows move in visual order and stop on both sides of a character drawn with no width, such as a zero width non-joiner |
| Touch Type tool | done | moves, scales and rotates one character through its kerning, baseline shift, scales and rotation (`text.touchType`); the first character of a line and right-to-left characters move only up and down |
| Retype (identify fonts in outlines/images) | missing | Illustrator's uses a cloud model |
| Snap to Glyph | missing | menu stub |
| Thai, Lao, Khmer, Myanmar: shaping (marks, reordering), lines broken between words, word-by-word caret and double-click | done | words found by ICU4X's line segmenter: dictionaries on the desktop, LSTM models on the web |
@@ -59,8 +63,11 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
| Date | Change | Summary |
|---|---|---|
| 2026-10-17 | minor | Roman Hanging Punctuation (#2756): per-paragraph hanging of Latin punctuation outside the margin; scores unchanged |
| 2026-10-17 | minor | Justification… (#2755): word and letter spacing, glyph scaling, Auto Leading and Single Word Justification per paragraph; the default word spacing range is 80/100/133%, so justified lines break differently than with the old fixed 80–150%; scores unchanged |
| 2026-10-17 | minor | A click on area type's red out port loads the Selection tool with the overflow text; the next click threads it into a closed path or a new frame of the same shape there, a drag into a new rectangle (`text.thread.add`, #2572 in part). The out port moved up the frame's right side, clear of the corner handle. Scores and hours unchanged. |
| 2026-10-16 | minor | Native font-family labels fall back from English-only typographic names to localized legacy names (#2208). English labels and native typographic precedence are preserved; synthetic loaded/catalog tests. Scores and estimates unchanged. |
| 2026-10-16 | minor | Align Stroke on live type: the characters' stroke goes inside or outside the glyphs without creating outlines, for every run or a range; kept in the native file and in canvas, PDF, EPS and SVG output (#2294); beyond Illustrator, scores and hours unchanged |
| 2026-10-14 | minor | Cmd+Shift+C (Align center) works on Windows and Linux too, where egui reported it as a copy (#1940); scores and hours unchanged |
| 2026-10-14 | minor | Cmd+Shift+C (Align center) works on Windows and Linux too, where egui reported it as a copy; a double-click on area type's bottom middle handle turns on Auto Size (#1940); scores and hours unchanged |
| 2026-10-14 | minor | MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged. |
| 2026-10-14 | minor | Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. |
@@ -75,6 +82,8 @@ Character Styles menu New/Redefine and footer New capture the first selected cha
| 2026-10-13 | minor | Fit Headline no longer pushes the tracking to its ceiling and loses glyphs when the frame holds one line (#1445, fixes #1444); text frames, wrap shapes and type paths with coordinates beyond a billion points hold no text, and layout limits its search for room, instead of hanging (#1451); scores and hours unchanged |
| 2026-10-12 | minor | Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged |
| 2026-10-12 | minor | A character no installed font had is looked for again after the fonts are rescanned, so installing a font fixes it without a restart (#1376, from #992); scores and hours unchanged |
| 2026-10-16 | minor | Fallback glyphs keep the text's weight and slant; Bold Italic of a missing family keeps the bold (#2491); scores and hours unchanged |
| 2026-10-16 | minor | A variable font whose name table says Light but whose default instance is Regular (Noto Sans Hebrew) lists and weighs its default face as Regular (#2521); scores and hours unchanged |
| 2026-10-12 | minor | Full-name font matching and Missing Fonts searches (#1258): synthetic static/variable font regressions; Windows font-service QA pending; scores and hours unchanged |
| 2026-10-11 | minor | Middle Eastern: digit types done (Character panel menu › Digits: Default, Arabic, Hindi, Farsi; `text.setFormat` / `text.setRangeStyle {digits}`); Character Direction, kashidas, composers and the split caret still missing; scores unchanged at this rounding |
| 2026-10-11 | minor | Thai, Lao, Khmer and Myanmar words for the caret (Ctrl/Alt+arrows) and double-click; combining marks count as part of a word; score unchanged (estimated) |
+114
View File
@@ -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.
+64 -15
View File
File diff suppressed because one or more lines are too long