diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 000000000..2408084cf --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +reviews: + auto_review: + # Dependabot bumps are checked by CI; skip CodeRabbit reviews on them. + ignore_usernames: + - "dependabot[bot]" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..c9de87acf --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,33 @@ +version: 2 +updates: + # Maintain Rust / Cargo dependencies + - package-ecosystem: "cargo" + directories: + - "/" + - "/crates/affinity/fuzz" + - "/crates/plugins/example" + schedule: + interval: "daily" + time: "06:00" + timezone: "America/New_York" + # Consolidate updates into a single PR rather than one PR per crate + groups: + cargo-dependencies: + patterns: + - "*" + commit-message: + prefix: "chore(deps)" + + # Maintain GitHub Actions workflows + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + time: "06:00" + timezone: "America/New_York" + groups: + actions: + patterns: + - "*" + commit-message: + prefix: "ci(deps)" diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml index 37cebf6c3..6ee868152 100644 --- a/.github/workflows/freebsd.yml +++ b/.github/workflows/freebsd.yml @@ -35,5 +35,5 @@ jobs: uses: storytold/craft-common-actions/.github/workflows/freebsd.yml@main with: require-craft-fonts: true - craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 + craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc run-package-check: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9161aee6e..54a8e53fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,8 @@ jobs: with: app-name: vectorcraft app-title: VectorCraft - craft-fonts-ref: 3d9ab3a132ed0eff457e424226a435a901da30d3 + craft-fonts-ref: a71c20e30af613287c504f7e685ca8fad77752bc + web-craft-fonts: true version: ${{ inputs.version }} secrets: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} diff --git a/AGENTS.md b/AGENTS.md index 1731a4d39..0b6171cd3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,6 +41,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format - `vectorcraft_engine::guard` is the last line of defence. Every entry point (commands, tool events, MCP requests, the UI frame, control requests) catches a panic, rolls the document back and reports an internal error. It exists for bugs and doesn't replace `Result`: wasm aborts on panic, so the web app has no safety net. - Untrusted input is fuzzed (`engine/tests/import_fuzz.rs`, `engine/tests/command_sweep.rs`, `format/tests/prop_format.rs`, `mcp/tests/protocol_props.rs`). Extend these when you add an importer, a parser or a protocol method. - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them. +- **Adding a cursor.** The app decides which cursors exist; whoever draws them works from the list. For a new tool state with a cursor of its own: add the `Cursor` variant (`crates/tools/src/lib.rs`, with a doc comment), return it from the tool's `cursor()` with a tool test, cover it in `cursor_icon` (`crates/ui-egui/src/canvas.rs`), draw it in `crates/ui-egui/src/cursors.rs` or give it a stand-in in `Cursor::stand_in()` until it's drawn, and add its `[[cursor]]` entry to `crates/tools/src/cursors.toml` (when it shows, in plain behaviour terms, and its hotspot; never another app's look). A missing entry only warns (`cargo xtask cursors`): the cursor still works. - **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. - **The UI is thin**: panels read engine state and act through `app.run(id, params)`. Colours come from `theme::Tokens`. - **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`). diff --git a/ASSETS.md b/ASSETS.md index 81b0bd630..f8121c35b 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -167,6 +167,7 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/icons/dc-new-fill.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-new-item.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-new-stroke.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-objects-on-path.svg` | VectorCraft contributors | Original work, drawn for VectorCraft: three small squares along a curve | MIT OR Apache-2.0 | | | `assets/icons/dc-options.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-para-center.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-para-justify-all.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | diff --git a/CLAUDE.md b/CLAUDE.md index ca12182b7..899f22e39 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,6 +41,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format - `vectorcraft_engine::guard` is the last line of defence. Every entry point (commands, tool events, MCP requests, the UI frame, control requests) catches a panic, rolls the document back and reports an internal error. It exists for bugs and doesn't replace `Result`: wasm aborts on panic, so the web app has no safety net. - Untrusted input is fuzzed (`engine/tests/import_fuzz.rs`, `engine/tests/command_sweep.rs`, `format/tests/prop_format.rs`, `mcp/tests/protocol_props.rs`). Extend these when you add an importer, a parser or a protocol method. - **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*` (id, label, menu path, shortcut, params doc, `enabled`, `run`) + tests. Tools emit commands (Begin/Preview/Commit). UI-only commands live in `crates/ui-egui/src/menus.rs` (`UI_COMMANDS`). The control channel and MCP reach all of them. +- **Adding a cursor.** The app decides which cursors exist; whoever draws them works from the list. For a new tool state with a cursor of its own: add the `Cursor` variant (`crates/tools/src/lib.rs`, with a doc comment), return it from the tool's `cursor()` with a tool test, cover it in `cursor_icon` (`crates/ui-egui/src/canvas.rs`), draw it in `crates/ui-egui/src/cursors.rs` or give it a stand-in in `Cursor::stand_in()` until it's drawn, and add its `[[cursor]]` entry to `crates/tools/src/cursors.toml` (when it shows, in plain behaviour terms, and its hotspot; never another app's look). A missing entry only warns (`cargo xtask cursors`): the cursor still works. - **Layering** is enforced by `cargo xtask layers`. Nothing below L6 depends on egui/eframe/winit/rfd. - **The UI is thin**: panels read engine state and act through `app.run(id, params)`. Colours come from `theme::Tokens`. - **Rust only** (no handwritten JS/TS). **Never break wasm** (`cargo xtask wasm`). diff --git a/Cargo.lock b/Cargo.lock index eca47581b..ffd1b3d45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,6 +806,11 @@ dependencies = [ "libc", ] +[[package]] +name = "craft-docpath" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=82a24b006eefa036b43c09a67fb9a3fbd17ffe63#82a24b006eefa036b43c09a67fb9a3fbd17ffe63" + [[package]] name = "craft-pdf" version = "0.1.0" @@ -3671,6 +3676,17 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "log", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + [[package]] name = "self_cell" version = "1.3.0" @@ -4507,7 +4523,7 @@ dependencies = [ [[package]] name = "vectorcraft" -version = "0.12.0" +version = "0.14.0" dependencies = [ "arboard", "block2 0.6.2", @@ -4531,6 +4547,7 @@ dependencies = [ "vectorcraft-engine", "vectorcraft-text", "vectorcraft-ui-egui", + "wayland-backend", "wayland-client", "wayland-protocols", "webbrowser", @@ -4538,11 +4555,12 @@ dependencies = [ "winit", "winresource", "winsafe", + "zbus", ] [[package]] name = "vectorcraft-affinity" -version = "0.12.0" +version = "0.14.0" dependencies = [ "crc32fast", "flate2", @@ -4553,7 +4571,7 @@ dependencies = [ [[package]] name = "vectorcraft-brush" -version = "0.12.0" +version = "0.14.0" dependencies = [ "kurbo 0.13.1", "serde", @@ -4565,7 +4583,7 @@ dependencies = [ [[package]] name = "vectorcraft-cad" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "kurbo 0.13.1", @@ -4581,7 +4599,7 @@ dependencies = [ [[package]] name = "vectorcraft-cli" -version = "0.12.0" +version = "0.14.0" dependencies = [ "log", "objc2-core-foundation", @@ -4603,7 +4621,7 @@ dependencies = [ [[package]] name = "vectorcraft-color" -version = "0.12.0" +version = "0.14.0" dependencies = [ "kurbo 0.13.1", "moxcms", @@ -4615,8 +4633,9 @@ dependencies = [ [[package]] name = "vectorcraft-doc" -version = "0.12.0" +version = "0.14.0" dependencies = [ + "hayro-ccitt", "image", "jpeg-encoder", "kurbo 0.13.1", @@ -4632,7 +4651,7 @@ dependencies = [ [[package]] name = "vectorcraft-effects" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "kurbo 0.13.1", @@ -4648,14 +4667,16 @@ dependencies = [ [[package]] name = "vectorcraft-engine" -version = "0.12.0" +version = "0.14.0" dependencies = [ + "craft-docpath", "crc32fast", "flate2", "image", "kurbo 0.13.1", "log", "proptest", + "roxmltree 0.21.1", "ruzstd", "serde", "serde_json", @@ -4684,9 +4705,11 @@ dependencies = [ [[package]] name = "vectorcraft-eps" -version = "0.12.0" +version = "0.14.0" dependencies = [ + "fax", "flate2", + "hayro-ccitt", "image", "kurbo 0.13.1", "ruzstd", @@ -4705,7 +4728,7 @@ dependencies = [ [[package]] name = "vectorcraft-format" -version = "0.12.0" +version = "0.14.0" dependencies = [ "flate2", "proptest", @@ -4720,7 +4743,7 @@ dependencies = [ [[package]] name = "vectorcraft-geom" -version = "0.12.0" +version = "0.14.0" dependencies = [ "kurbo 0.13.1", "proptest", @@ -4732,7 +4755,7 @@ dependencies = [ [[package]] name = "vectorcraft-mcp" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "log", @@ -4751,7 +4774,7 @@ dependencies = [ [[package]] name = "vectorcraft-metafile" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "kurbo 0.13.1", @@ -4765,7 +4788,7 @@ dependencies = [ [[package]] name = "vectorcraft-pathops" -version = "0.12.0" +version = "0.14.0" dependencies = [ "kurbo 0.13.1", "linesweeper", @@ -4777,7 +4800,7 @@ dependencies = [ [[package]] name = "vectorcraft-pdf" -version = "0.12.0" +version = "0.14.0" dependencies = [ "aes", "craft-pdf", @@ -4807,7 +4830,7 @@ dependencies = [ [[package]] name = "vectorcraft-plugins" -version = "0.12.0" +version = "0.14.0" dependencies = [ "serde", "serde_json", @@ -4821,7 +4844,7 @@ dependencies = [ [[package]] name = "vectorcraft-render" -version = "0.12.0" +version = "0.14.0" dependencies = [ "flate2", "gif", @@ -4848,15 +4871,16 @@ dependencies = [ [[package]] name = "vectorcraft-scriptcheck" -version = "0.12.0" +version = "0.14.0" dependencies = [ "proptest", ] [[package]] name = "vectorcraft-svg" -version = "0.12.0" +version = "0.14.0" dependencies = [ + "craft-docpath", "flate2", "image", "kurbo 0.13.1", @@ -4879,8 +4903,9 @@ dependencies = [ [[package]] name = "vectorcraft-testkit" -version = "0.12.0" +version = "0.14.0" dependencies = [ + "fax", "image", "kurbo 0.13.1", "proptest", @@ -4896,7 +4921,7 @@ dependencies = [ [[package]] name = "vectorcraft-text" -version = "0.12.0" +version = "0.14.0" dependencies = [ "harfrust 0.13.3", "icu_segmenter", @@ -4916,7 +4941,7 @@ dependencies = [ [[package]] name = "vectorcraft-three-d" -version = "0.12.0" +version = "0.14.0" dependencies = [ "vectorcraft-geom", "vectorcraft-pathops", @@ -4924,7 +4949,7 @@ dependencies = [ [[package]] name = "vectorcraft-tools" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "kurbo 0.13.1", @@ -4940,7 +4965,7 @@ dependencies = [ [[package]] name = "vectorcraft-trace" -version = "0.12.0" +version = "0.14.0" dependencies = [ "image", "kurbo 0.13.1", @@ -4952,7 +4977,7 @@ dependencies = [ [[package]] name = "vectorcraft-ui-egui" -version = "0.12.0" +version = "0.14.0" dependencies = [ "craft-ui", "egui", @@ -4986,13 +5011,14 @@ dependencies = [ [[package]] name = "vectorcraft-web" -version = "0.12.0" +version = "0.14.0" dependencies = [ "eframe", "egui", "js-sys", "log", "rfd", + "serde_json", "vectorcraft-engine", "vectorcraft-format", "vectorcraft-text", @@ -5933,6 +5959,7 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.4.1", "rustix 0.38.44", + "sctk-adwaita", "smithay-client-toolkit 0.19.2", "smol_str", "tracing", @@ -6096,7 +6123,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" [[package]] name = "xtask" -version = "0.12.0" +version = "0.14.0" dependencies = [ "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 1c10d551b..30eff51df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"] default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"] [workspace.package] -version = "0.12.0" +version = "0.14.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.95" @@ -13,6 +13,7 @@ repository = "https://github.com/storytold/vectorcraft" [workspace.dependencies] craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "ca50d63b773c99f96e75c77ce5a1d21e7c9cda0d", default-features = false, features = ["interpret", "write"] } craft-print = { git = "https://github.com/storytold/craft-libs", rev = "d991bf6792f8ade99c51358bf3c1d902c8cd96b5" } +craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "82a24b006eefa036b43c09a67fb9a3fbd17ffe63" } craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675", features = ["serde"] } # internal vectorcraft-geom = { path = "crates/geom" } @@ -64,7 +65,7 @@ wat = "1.228" # Without `text` (glyph runs, through glifo and a second skrifa): the renderer draws the outlines # vectorcraft-text gives it. The other default features stay. vello_cpu = { version = "0.2", default-features = false, features = ["std", "png", "u8_pipeline", "f32_pipeline"] } -image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp"] } +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif", "tiff", "bmp", "tga"] } # Without `text` and the system fonts (fontdb, harfrust and skrifa, at other versions than ours): # SVG import reads `` from the XML itself and never gives usvg a font. A test of the SVG # crate turns `text` on. @@ -79,6 +80,9 @@ flate2 = { version = "1", default-features = false, features = ["rust_backend"] rustix = { version = "1.1", default-features = false } # Zstandard decoder (pure Rust, builds for wasm32 too): the editing copy an EPS carries of its art. ruzstd = { version = "0.9.1", default-features = false, features = ["std"] } +# CCITT Group 3 and Group 4 decoder (ITU-T T.4 and T.6): EPS images read through +# `CCITTFaxDecode`. The PDF reader's hayro-syntax uses the same version. +hayro-ccitt = "0.3" jpeg-encoder = { version = "0.6", default-features = false, features = ["std"] } gif = { version = "0.14", default-features = false, features = ["std", "raii_no_panic"] } # CMYK image samples (vectorcraft-doc's cmyk module): the image crate's own TIFF and JPEG diff --git a/NOTICE b/NOTICE index bcd3055b9..5cf3c8952 100644 --- a/NOTICE +++ b/NOTICE @@ -92,3 +92,34 @@ Assets already bundled through Hayro's default features retain their licences: non-visual character-mapping data; see licenses/pdf-backend/hayro-cmap/LICENSE.txt. Adobe and contributor names do not endorse or promote this product. Per-file hashes and sources are recorded in ASSETS.md. + +Linux Wayland window decorations +------------------------------- +sctk-adwaita 0.10.1 (https://github.com/PolyMeilex/sctk-adwaita, +commit d5f343c45c04d4a026db57f76760819b7325db00) is used by winit on Linux +for vector window controls. Its title-font features are disabled: no Cantarell +font is embedded by this dependency and no font file is added to this repo. + +sctk-adwaita's MIT licence: + +MIT License + +Copyright (c) 2022 Bartłomiej Maryńczak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 36fe11fe9..294824164 100644 --- a/README.md +++ b/README.md @@ -11,20 +11,23 @@

VectorCraft

- Vector illustration; an open-source, clean-room reimplementation of Adobe Illustrator, rebuilt in pure Rust. + Vector illustration; an open-source, modern vector art application, built in pure Rust.

- A fast, open-source, clean-room take on the Adobe Illustrator workflow. It runs natively on + A fast, open-source vector illustration workflow. It runs natively on macOS, Windows, Linux and FreeBSD, and in the browser via WebAssembly. Built by the ArtCraft team.

- Status: in active development - Written in pure Rust - Runs on macOS, Windows, Linux, FreeBSD and the web - MCP server for agents - License: MIT OR Apache-2.0 + Status: in active development + Written in pure Rust + Runs on macOS, Windows, Linux, FreeBSD and the web + MCP server for agents + License: MIT OR Apache-2.0 +
+ Scanned by SonarCloud + Reviewed by CodeRabbit

@@ -147,7 +150,7 @@ exposes to agents, and exported by VectorCraft's own renderer. The source files ## Why VectorCraft -- **Familiar.** Illustrator's layout, tools, menus, panels and shortcuts: the Pen, Direct Selection, +- **Familiar.** A familiar layout, tools, menus, panels and shortcuts: the Pen, Direct Selection, Pathfinder, Smart Guides, Appearance, Swatches, Layers and more. You already know how to use it. - **Fast.** Multithreaded SIMD rendering off the UI thread. 20,000 shapes render in about 27 ms at full retina resolution while the interface stays at 120 fps. @@ -163,6 +166,8 @@ exposes to agents, and exported by VectorCraft's own renderer. The source files ## Quick start +For posters and sewing patterns, see [Print a large poster across multiple pages](docs/print-tiling.md). + ```sh git clone https://github.com/storytold/vectorcraft cd vectorcraft @@ -178,6 +183,8 @@ cd apps/vectorcraft-web && trunk build --release # web build → dist cargo xtask ci # fmt, clippy, tests, layering, wasm, vendor names ``` +For text and custom letter shapes, see [Typography and Custom Lettering](docs/typography-lettering.md). + Japanese fonts come from [storytold/craft-fonts](https://github.com/storytold/craft-fonts), an optional build input (releases always use it): `CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo run --release -p vectorcraft` (an absolute path). Without it, Japanese text uses the installed system fonts. See [`docs/development.md`](docs/development.md#fonts-craft-fonts-optional-build-input). @@ -194,8 +201,10 @@ drag-and-drop, set **Preferences › Performance › Linux Display Server** to * the app with `WAYLAND_DISPLAY= vectorcraft`: the window then opens through XWayland, where drops work (see [`docs/development.md`](docs/development.md#linux-wayland-and-x11)). -On Linux under KDE Plasma 6.3 or later with Wayland, a drawing tablet's pen moves the cursor but VectorCraft doesn't -respond to it yet (#491). Open the window through XWayland instead, with the same preference or `WAYLAND_DISPLAY= vectorcraft` (see +On Linux with Wayland, under KDE Plasma 6.3 or later and under GNOME, VectorCraft doesn't respond to a drawing tablet's +pen yet (#491, #2028). Open the window through XWayland instead, where the pen works as a mouse: set the same +preference to **X11 (XWayland)**, or to **X11 (XWayland) When a Pen Is Attached** to use XWayland only when a pen is +attached as VectorCraft starts, and restart; or start the app with `WAYLAND_DISPLAY= vectorcraft` (see [`docs/development.md`](docs/development.md#linux-wayland-and-x11)). ### Use it from Claude Code and other agents @@ -222,9 +231,8 @@ VectorCraft is under active development. [**ROADMAP.md**](ROADMAP.md) has the st and what's next; [`docs/target-app-parity.md`](docs/target-app-parity.md) the full parity assessment and [`docs/gaps.md`](docs/gaps.md) the ranked list of what's missing. -**Where we are (2026-10-10): alpha.** About 88% of Illustrator 2026's menu items, tools, effects and formats exist -(92.5% of menu items do something), feature depth is ~75%, and we estimate ~65% "ready for real work" for a mainstream illustrator (~62% against -all of Illustrator): everyday +**Where we are (2026-10-10): alpha.** About 88% of core menu items, tools, effects and formats exist +(92.5% of menu items do something), feature depth is ~75%, and we estimate ~65% ready for real work for a mainstream illustrator (~62% across all tracked features): everyday vector illustration is close to usable: drawing and path tools, Pathfinder and Shape Builder, paint, gradients, appearance and transparency, type with styles, threading and Hebrew/Arabic bidirectional layout, and files (SVG, PDF and PDF-compatible `.ai` with PDF/X, legacy version 7 `.ai` keeping layers, groups and type, EPS, DXF, EMF/WMF, raster formats, PSD out and PSD/PSB in as their merged image, Print, Package). @@ -236,22 +244,22 @@ doesn't write Affinity files. Current `.af` validation includes 33 pinned files, and every-board SVG/PDF/PSD export, with fixes for Affinity 3 artboards, source-backed JPEGs and text runs. [Scope and limits](crates/affinity/README.md); [source audit and remaining gaps](docs/affinity-validation.md). The interface -speaks English, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Polish, Brazilian Portuguese, Russian, Swedish, Ukrainian, Finnish, Bulgarian, Greek and Czech; +speaks English, German, Japanese, Korean, Vietnamese, Traditional and Simplified Chinese, Spanish, French, Hungarian, Italian, Polish, Brazilian Portuguese, Russian, Swedish, Ukrainian, Finnish, Bulgarian, Greek, Czech and Georgian; [localization-parity.md](docs/localization-parity.md) has the details. -The scores are self-assessed from our code against Illustrator's public documentation; +The scores are self-assessed from our code against public documentation; [target-app-parity.md](docs/target-app-parity.md#how-this-was-measured-2026-10-10) explains how far to trust them. **What's missing:** -- reliable exchange of Illustrator's own `.ai` files (layer structure in real files; Illustrator's editing data isn't written); +- reliable exchange of .ai files (layer structure in real files; proprietary editing data isn't written); - an interaction-fidelity pass covering every tool's modifiers and small behaviours; - 3D and Materials beyond the initial live Revolve (Extrude & Bevel, Inflate, Rotate, materials); -- about half of the Photoshop-style raster effects (27 of 57 still missing; Film Grain is available under Artistic, Halftone Pattern under Sketch) and the Effect Gallery; +- most of the raster effects (11 of 57 still missing; Cutout, Film Grain, Neon Glow, Plastic Wrap, Poster Edges, Smudge Stick, Sponge and Watercolor are available under Artistic, Bas Relief, Chalk & Charcoal, Charcoal, Chrome, Graphic Pen, Halftone Pattern, Note Paper, Photocopy, Plaster, Reticulation and Stamp under Sketch) and the Effect Gallery; - CJK composition for vertical type (vertical type itself has initial support, with a Japanese interface); - scripting, and the rest of Variables (image and graph kinds, dataset import); - generative AI features. **Where we're going:** next is the interaction-fidelity pass, `.ai` files from real users and start-up stability, -alongside the raster-effects package; then type, the Illustrator 2026 additions and 3D. The ranked list is in +alongside the raster-effects package; then type, recent additions and 3D. The ranked list is in [`docs/gaps.md`](docs/gaps.md). **Workspace:** `crates/{geom, color, doc, pathops, text, effects, three-d, trace, brush, render, svg, pdf, eps, cad, metafile, format, tools, engine, ui-egui, mcp, testkit}` @@ -370,8 +378,6 @@ ArtCraft Team and are not covered by this license. They may be used only unmodif part of this repository and VectorCraft, under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). Forks and modified versions must remove them. -Adobe, Photoshop, Illustrator, Premiere Pro, Lightroom, Acrobat, After Effects and InDesign are trademarks or registered trademarks of Adobe Inc. in the United States and/or other countries. VectorCraft is an independent, open-source project and is not affiliated with, sponsored by or endorsed by Adobe Inc.; these names are used only to describe the workflows it is compatible with. -

ArtCraft
Made by the ArtCraft team and community. diff --git a/ROADMAP.md b/ROADMAP.md index 522d5ffc9..8ae1cf14a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** minor (about 160 community PRs of 2026-10-15/16: PDF import crash fixes, Dimension tool, legacy `.ai` save, Intertwine, web pen input, Image Trace fixes, `.ai`/EPS/PDF import fidelity, Bulgarian, Greek, Swedish and Finnish catalogs; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-19 · **Change:** minor (about 90 community PRs of 2026-10-19: Objects on Path, the Simplify slider, Pencil Edit Selected Paths in the middle of a path, Georgian, and more; Background Export progress dialog with Cancel, #2564; about 85 community PRs of 2026-10-18: Justification…, Word .docx placed as area type, CCITT Group 3 TIFF, Roman Hanging Punctuation, blend spines edited with the Anchor Point tool, shadows and glows in stack order, coloured 3D lights, a Dutch catalog completed; 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; about 145 community PRs of 2026-10-17: image-decode overflow guards, opaque floating panels, MCP export cancel and progress, editable Illustrator EPS import, scoped `doc_inspect`, Dutch, Danish and Indonesian, Simplified Chinese messages, screen-reader names; headline numbers unchanged; about 160 community PRs of 2026-10-15/16: PDF import crash fixes, Dimension tool, legacy `.ai` save, Intertwine, web pen input, Image Trace fixes, `.ai`/EPS/PDF import fidelity, Bulgarian, Greek, Swedish and Finnish catalogs; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -29,7 +29,7 @@ blocking it ([Alpha gate](docs/roadmap.md#alpha-gate)). | | Value | Kind | |---|---|---| -| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 44/50, Photoshop effects 30/57, 3D 4/4 (initial types, no materials) | +| **Feature breadth** (Illustrator's menu items, tools, effects, formats exist) | **~88%** | measured in part: menu items 348/375 wired (92.8%), tools 89/92, Illustrator effects 46/50, Photoshop effects 46/57, 3D 4/4 (initial types, no materials) | | **Feature depth** (weighted by use, scored by behaviour) | **~75%** | estimated | | **To beta** | **~170–270 h** one agent · ~50–80 h with 4–6 agents | estimated | | **To full parity** | **~360–590 h** one agent · ~95–165 h with 4–6 agents | estimated | @@ -76,7 +76,7 @@ The dimension hours overlap (feature rows hold some UI, format and AI work), so | Strokes, brushes, width profiles | 88% | 8–15 | | Appearance, transparency, graphic styles, masks | 97% | 1–2 | | Live vector effects | 85% | 6–10 | -| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 49% | 23–39 | +| Raster effects (Effect Gallery) — [effects-parity.md](docs/effects-parity.md) | 81% | 8–14 | | 3D and Materials | ~25% | 30–55 | | Type core — [type-parity.md](docs/type-parity.md) | 80% | 14–19 | | Type advanced (CJK, spelling, Touch Type) | 50% | 20–28 | @@ -112,8 +112,8 @@ Weights, evidence and the missing items per area: [target-app-parity.md](docs/ta | Korean | `ko` | full | 100% | | Vietnamese | `vi` | full | 100% | -Also shipped: Traditional Chinese (partial, 80%), Bulgarian, Finnish, Greek, Hungarian, Italian, Polish, Russian, Swedish and Ukrainian (full), Czech (catalog complete; render-site gaps reduced; native review pending). -No catalog has had a native speaker's review. Details: [localization-parity.md](docs/localization-parity.md). +Also shipped: Traditional Chinese (partial, 80%), Bulgarian, Finnish, Greek, Hungarian, Italian, Polish, Russian, Swedish, Ukrainian and Georgian (full), Czech (catalog complete; render-site gaps reduced; native review pending). +The Georgian catalog has had a native speaker's review; no other catalog has. Details: [localization-parity.md](docs/localization-parity.md). ## Upcoming @@ -123,7 +123,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap 2. **`.ai` files from real users** (G9): layer structure, guides, speed: 10–20 h. 3. **Launch and stability on real machines** (G10): 20–35 h. 4. **Core-tool correctness** (G11: Shape Builder regions, Asset Export borders): 8–15 h. -5. **Raster effects** (G2) alongside: 28 filters and the Effect Gallery, 23–39 h. +5. **Raster effects** (G2) alongside: the 11 remaining filters (7 Artistic, 3 Sketch, 1 more) and the Effect Gallery, 8–14 h. 6. **Then:** type core and advanced type (G4), the Illustrator 2026 additions (G13), 3D (G3). ## Progress log @@ -134,7 +134,14 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | Date | What landed | |---|---| +| 2026-10-18 | Inch rulers divide the inch in half, not in ten. Zooming in stepped an inch ruler through 0.2, 0.1 and 0.05 of an inch, marks an inch ruler does not carry; below an inch the steps are binary fractions now, 1/2 down to 1/16, with the unlabelled ticks between them binary too (down to 1/128, so the 32nds, 64ths and 128ths appear as the report expects), while the metric units, points, pixels and picas keep their tenths. A label carries the decimals its step needs, so a sixteenth reads 0.0625 rather than the rounded 0.06, with trailing zeros dropped (#2827). Headline numbers unchanged. | +| 2026-10-18 | Exported and saved files can repeat their bytes: PNG, PDF, EPS, legacy `.ai`, packaged zips and `.vectorcraft` saves recorded the wall-clock time they were made in, so the same commands run twice gave different bytes though the pixels matched. `SOURCE_DATE_EPOCH` now stands in for the clock in everything a file records, as the reproducible-builds specification defines it; a date a command is given still wins, and a malformed value is ignored rather than refusing the export. The recovery heartbeat and the seed PDF encryption draws on keep the real clock, so a password-protected PDF still differs between runs (#2880). Headline numbers unchanged. | +| 2026-10-18 | About 85 community PRs landed in two batches through the full gate, with our fixes on top. New: Word `.docx` files place as area type with their formatting (#2766), CCITT Group 3 TIFF files open (#2770), Paragraph panel menu › Roman Hanging Punctuation (#2775), the Anchor Point tool edits blend spine points and handles (#2643, fixes #2544), shadows and glows each draw from the art with the effects before them, without the old exponential cost (#2395), one to ten coloured lights in 3D effects (#2706), Artboard Options X and Y with a reference point (#2739), more Contextual Task Bar actions (#2471), Optical Margin Alignment for a whole thread (#2773), History panel Step Backward/Forward and Set History Limit (#2764), Edit › Undo and Redo name the step as the History panel does (#2774), bounding-box handles snap to the grid, pixels and guides (#2715), Ctrl/Cmd+A in the Artboards panel (#2761), Tabs panel menu items (#2742), Import Data Sets defines variables from header cells (#2736), a Linux option to use X11 when a pen is attached (#2717). Fidelity and robustness: variable fonts named by their default weight and fallback glyphs keep bold and italic (#2733, #2732), legacy `.ai` layer names in Windows Latin-1 (#2722), WMF window mapping (#2721), Magic Wand inside isolation (#2720), type values out of range in files are clamped (#2728, #2757), raster effects stay responsive in drags (#2745), exports and Save for Web report undecodable images (#2762, #2737, #2684), Windows-safe exported file names (#2738), a marquee skips a Shaper Group's originals (#2772). Second batch: an export progress dialog with Cancel on the shared cancel token (#2564), CMYK gradient meshes drawn several times faster within a 16 MB cap (#2802), Paragraph panel menu › Justification… with file values clamped (#2771), Edit › Clear History (#2843), SVG image links keep their spelling (#2837, fixes main's Windows test failures), the Linux save dialog through the XDG portal (#2783), RGB input stored as CMYK in CMYK documents (#2812), legacy `.ai` export of images and radial highlights (#2801, #2807), `.ai` import notes for damaged editing data, page sizes and variable libraries (#2799, #2800, #2842), PDF and `.ai` keep their creation date (#2838), undecodable images left out of PDF exports with a note (#2790), MCP input waits for queue room (#2776), slice, snapping, selection, type and accessibility fixes (#2794–#2797, #2805, #2806, #2809, #2815, #2844–#2846), dropdowns close after a keyboard pick (#2824), plug-in dialogs scroll (#2840). Languages: the Dutch catalog completed (#2730). Headline numbers unchanged (estimated). | +| 2026-10-19 | About 90 community PRs landed in batches through the full gate, with our fixes on top (formatting, catalog rows, small correctness fixes). New: Objects on Path (#3066), Object › Path › Simplify's slider with Auto-Simplify and More Options (#2555), Pencil Edit Selected Paths redraws the middle of a selected path (#2905), Pencil Tool Options' Alt key toggles to Smooth Tool (#2685), 3D solids built from each stroke's outline (#3049), Live Paint Selection Tool Options (#3052), Locate Selected Objects in the Layers panel (#3033), Snap to Unit for tab stops (#3039), DXF circles and arcs as native entities (#2962), Targa files (#2765), a Georgian interface (#2045), Traditional Chinese status and error messages (#2784), the web page's `window.vectorcraft` control channel (#2943), `select.color` (#2939), reproducible export bytes with `SOURCE_DATE_EPOCH` (#2947), Spacing Guides on resizes (#2966) and measurement labels on more tools. Fixed: number fields step to the next whole unit (#2916), a click away from edited type makes no new text (#2922), inch rulers in halves (#2951), Recent cards in New Document (#2904), Japanese W-weights in SVG (#2896), the point-type baseline marker (#2897) and the Reshape tool's falloff (#2900). Headline numbers unchanged. | +| 2026-10-17 | Background Export shows a progress dialog: the file being exported, the artboard count and a bar, Cancel (Esc) and Continue in Background. It runs on the shared cancel token of #2546, so a cancelled export writes no file and keeps no half-drawn pattern tile or effect raster; translated in every language with complete menus (#2564). Headline numbers unchanged. | +| 2026-10-17 | About 145 community PRs landed in six batches, each through the full gate, plus our fixes on top. Crashes and hostile input: an image's RGBA size is checked from its header before decoding, without overflowing, in the renderer, sampling, recoloring and Crop Image (#2442, #2468, #2407), PDF Add Around and huge art are clamped (#2440), graph axis labels past i64, `+` in hex colours and XML ids are handled (#2610, #2470, #2447), Document Raster Effects resolutions open in range (#2579), Batch names stay in their folder (#2504). Regressions: floating panel groups are opaque again while the docked card keeps its rounded corners (#2499), the Wayland pointer returns over panels (#2502). New: MCP cancels long exports, answers while one runs and reports progress (#2546), `doc_inspect` takes ids, kind and paging (#2543), raster export of a page rectangle (#2511), `text.create` returns bounds (#2478), PDF presets and ICC profiles persist (#2547), screen-reader names for widgets (#2611), Expand on Intertwine (#2622, fixes #2540), Duplicate/Copy/Cut of several artboards (#2567), print presets by name (#2607), TSV data sets (#2608), planar TIFF (#2598), Alt-click a swatch onto a gradient stop (#2623), Shift-click ranges in Links, Graphic Styles and Libraries (#2612, #2613, #2618), Space+Shift rotates the view (#2601), Presentation Mode keys (#2583), Small Caps and super/subscript buttons (#2508). Fidelity: editable vector art from Illustrator EPS (#2571), `.ai` Drop Shadow Darkness (#2513), overprint preview with Darken (#2514), Generic CMYK black round-trips (#2503), PNG sRGB chunk (#2519), kerning at a caret (#2518), tab widths in justified area type (#2487), Transform copies keep their own gradients (#2486), smooth Zig Zag (#2475), Live Corners round up to their neighbours (#2484), kerning across style runs (#2554), SVG letter spacing on justified lines (#2574), Dimension tool snapping (#2482), gradient chips (#2495), Transform panel flips and percentages (#2400, #2404). Later the same day: PostScript `.ps` and `.epsf` files open (#2461), EPS images with CCITTFax compression draw (#2445), Illustrator 3 EPS without layers (#2436), `.ai` type opacity and blend modes (#2420), envelope meshes and compound holes in `.ai` (#2410, #2444), CMYK image soft masks in PDF export (#2448), Show Transparency Grid saved with the document (#2488), Save Selection in the Layers panel menu (#2483), Character and Paragraph Reset Panel (#2479, #2438), Cmd/Ctrl+Shift double-click releases a guide (#2467), Shift+Enter keeps a field active (#2431), Shift keeps reshaped handles perpendicular (#2425), emptied type keeps its style (#2464), Smart Blur radius (#2443), freeform gradients recolour (#2397), mesh slivers (#2456), Recolor wheel handles in CMYK (#2454), and six Image Trace fixes (#2387, #2584, #2586–#2589). Fifth batch: Optical Margin Alignment (#2563), Window › Blend panel (#2573), graph column designs (#2406), Live Paint Gap Options (#2424), the SVG Interactivity panel (#2437), RTF placed as area type (#2524), the Touch Type tool (#2469), pattern fills expand into clipped tiles (#2616), Save and Export ask before replacing a file an added extension names (#2619), the Artboard tool's Control bar and Contextual Task Bar (#2559, #2556), Alt-click New Artboard for its options (#2496), Shift-click in Brushes (#2562), the Move dialog remembers the last move (#2542), PDF export uses the document's assigned profiles (#2536), Printer Setup opens the selected printer's preferences on Windows (#2577). Sixth batch: Effect › Artistic › Cutout, Neon Glow and Poster Edges (#2453), View › Rulers › Change to Global / Artboard Rulers (#2416), Symbol Options 9-slice guides (#2512), Create Gradient Mesh on an embedded image (#2538), Rotate View cursors and the Mac trackpad rotation gesture (#2594, #2644), choice parameters as dropdowns in the inline effect editor (#2582), Group Selection picks and moves one path of a compound path (part of #2441), Shift-click ranges in the Artboards panel (#2626), Lock Variables (#2631), a layer dropped under the last row goes to the bottom (#2632), and robustness work: huge selections draw bounded overlays (#2635, #2645, part of #2460), values out of range in presets and files are clamped (#2656), whole-number checks for counts (#2638, #2659), exports warn about overset type (#2628), symbol instances measured by their art (#2614), colour-managed Eyedropper in CMYK documents (#2664). Later: Effect › Rasterize (#2409), variable-font axis sliders in the Character panel (#2710), Image Trace Create Strokes on an exact distance transform with Guo–Hall thinning and a skeleton graph (#2703–#2705), Auto Color keeps rare colours (#2702), large images trace with a warning (#2700), a 1e9 pt input bound on Reshape Segment, Convert Anchor Point and the Pencil (#2709), task-bar Connect, Corner and Smooth for selected anchors (#2697), PDF 0 w strokes import as hairlines (#2696), threading text from the out port (#2713, part of #2572), a panel per swatch library with Persistent (#2714, #2603), installed plug-ins kept across restarts (#2695, #2541). Languages: Dutch, Danish and Indonesian complete (#2393), Simplified Chinese status and error messages (#2578), translated file filters and presets (#2590). Headline numbers unchanged (estimated). | | 2026-10-16 | Symbol Options › Export Type (Graphic or Movie Clip, default Movie Clip) with Registration; `symbol.options` / `symbol.list` (G5). +| 2026-10-15 | Cursors: 36 more tool states get cursors of their own. Node editing follows the public manual (Direct Selection's anchor squares, Reshape Segment, Group Selection's plus, the arrow hollow on a snap, the Alt-copy arrowheads), the Anchor Point caret, the Curvature pen and Free Transform's side-shear arrows are drawn; Lasso, the freehand, symbolism and other tools borrow a drawn cursor until theirs is drawn. `crates/tools/src/cursors.toml` lists every cursor for whoever draws them, and `cargo xtask cursors` warns about one missing from it. Scores unchanged. | | 2026-10-16 | About 160 community PRs landed, each through the full gate. Crashes and hostile files: damaged PDF dictionaries and arrays no longer crash import (#2133, #2240, #2244, #2271), area type with huge row or column counts opens (#2109), hostile mesh edges are bounded (#2051). New: Dimension tool (#1788), legacy version 7 `.ai` save (#1794), Object › Intertwine (#2095), Apple Pencil and touch input on the web (#1740), Sample Color from Screen (#2257), Drop Shadow Darkness (#2252), Smart Blur edge modes (#2154), Scribble variations (#2180), Free Distort corners in its dialog (#2134), Rotate View presets (#2106), tritanopia proofing (#2234), Layers panel filters (#2087), Hide Template (#2117), File › Automate Batch through the Actions panel (#1938), dataset and graph CSV (#1929, #1941), Reset Tracking and Kerning (#2274), Alt+Enter copies in Transform fields (#2236), Shift+Tab panels (#2254), Control bar isolation, Recolor and image Replace (#2258, #2079, #2081). Revolve renders smooth with Cap on by default (#1193); Image Trace keeps tiny parts, thin outlines and shared corners, and Grayscale gradients are grey (#2012, #2025, #2263, #2264, #2265, #1879). Import: many `.ai`, EPS and PDF fidelity fixes (#2014–#2016, #2029, #2030, #2044, #2046, #2050–#2052, #2058, #2129, #2199, #2202, #2218), images up to 1 GB decoded (#2060), `.ai`/`.ait`/`.svgz` files paste from a file manager (#2148, #2201). Fixes across fields (arithmetic #2069, #2209, #2022; caret #1403), New Document (#2158, #2176), menus (#2232), shortcuts (#2235, #2146), Transform Each axes (#2227), Paste in Place (#1926), Color Picker tab order (#1937, #1939) and more. Languages: Bulgarian, Greek, Swedish and Finnish complete under the tests, pt-br completed. Details per PR in the PRs; headline numbers unchanged (estimated). | | 2026-10-14 | Brazilian Portuguese (`pt-br`) covers the whole interface and status/error messages under the completeness tests (#1914); Localization 71% → 74% (measured), hours 55–90 → 50–85. | | 2026-10-14 | Direct Selection: deleting an interior anchor of an open path leaves separate surviving runs. Delete/Backspace and history regressions cover the gap; explicit Remove Anchors remains connected. Headline numbers unchanged. | @@ -160,6 +167,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap | 2026-10-12 | A `.ai` file with placed (linked) pictures opens from its editing data, with its guides, sublayers and hidden objects: each picture is a linked image in its place, showing the low-resolution preview the file keeps until its file is found or relinked (#1264). Scores unchanged. | | 2026-10-12 | Desktop printing adopts the shared `craft-print` spooler with exact physical sheet sizes. Windows 7 retains its WMI/viewer transport and excludes the shared backend; compatibility CI checks the dependency tree and PE imports. Scores unchanged. | | 2026-10-12 | Graphs: Object › Graph › Design… and Marker… (designs with a preview, New Design, Rename, Delete Design, Paste Design, Select Unused, and markers drawn with a design or None) (#1092); Graph Data opens as a worksheet with Import Data…, column width and decimals (#1138), and takes Transpose, Switch x/y, `|` line breaks in labels and the pie Legend, Position and Sort options (#1115); Graph Type adds Add Drop Shadow and Add Legend Across Top (#1119), widths past 100%, First Row and First Column in Front, and Draw Filled Lines (#1136); graphs keep their label formatting when drawn again (#1137). Align: a Horizontal & Vertical Align Center button (#1073); Align to Key Object makes the topmost selected object the key (#1221), and the palette's key entry reads Cancel Key Object (#1224). Pen: the nearest end wins over snapping, Alt on the last anchor edits it, and Enter, Escape or a tool switch finish a handle preview as one undo step (#821). Direct Selection: a click on an anchor of an already selected path selects that anchor alone (#1237). Make Compound Path keeps the backmost object's paint, and a selected member reads and changes the compound's fill rule (#1225). Shape Builder and Live Paint no longer turn a line's loose end past a crossing into a piece that runs out and back (#1230). Art moved or duplicated with an artboard follows Transform Pattern Tiles (#1219). A click with the Rectangular Grid tool opens its options dialog (#1169). Commands that change nothing (Lock, Hide, Transform Each, Rearrange Artboards and others) add no undo step (#1233). Saving with missing placed files keeps their relink data, and .ai export honours the chosen artboards (#718). SVG export keeps Unicode data-attribute names, and SVG Options refuses an invalid artboard range (#989). CMYK colours given in percent with a component under 1 keep their scale (#1249). Colour Image Trace keeps close dark tones apart (#1247). macOS: the Save sheet has a Format menu (#1235). The Hand and Zoom tools show drawn cursors on every platform (#1238). Czech: the remaining strings are translated, and Pathfinder's Add reads "Sjednotit" (#1226). MCP: a lost reply reports that the edit may have happened instead of replaying it (#1246); `select.key` checks its id and agents can read the key object (#1222). Tests: the command sweeps and import fuzz tests fail on a panic caught by the engine's guard (#1218), and the envelope export test ignores PDF dates and file IDs (#1220). Effect › Artistic › Film Grain (Grain, Highlight Area, Intensity; Photoshop-style filters 29/57) (#1282), and Craquelure, Mosaic Tiles and Stained Glass redraw 25–30% faster with identical pixels (#1265). Direct Selection: clicking a segment selects only that segment, and Delete removes just it (#1300); Shift-click deselects one anchor of a path selected whole (#1263). Pen + Alt converts anchors of paths inside a selected group or compound path (#1306). The Curvature tool shows circular point markers without direction handles (#1284). Snap to Point: pressing near an anchor drags by that anchor, with Smart Guides on or off (#1313). Transform Again repeats the options of the transform it repeats (#1303). Aligning anchors that are already aligned adds no undo step (#1276), and type commands that change nothing record none either (#1316). Simplify keeps the path selected with its new anchors, and its dialog previews and shows the anchor count (#1305). Adding a stroke colour to an unpainted zero-width stroke gives it a visible 1 pt width (#1256). The Eyedropper samples locked objects, template layers and art outside isolation mode (#1260), and shows a pipette cursor (#1288). Fonts named by their full name resolve to the right face (#1267), and Proportional Metrics brings the matching kerning (`apkn`, `vkrn`, `vapk`) (#1291). Artboards: Escape leaves Artboard editing (#1296), Document Setup's Edit Artboards switches to the Artboard tool (#1294), and the Artboard tool starts on the active artboard (#1309). New and imported documents use the Gridline every and Subdivisions preferences (#1314). Send to Current Layer refuses a hidden or locked current layer (#1301), and layer row commands check explicit ids (#1255). Links: Find in Folder… finds missing linked files in a folder and its subfolders and can copy them into the document's folder (#1287). Image Trace traces images over 64 megapixels scaled down instead of refusing them (#1315). DXF export marks closed curves as closed SPLINE entities (#1320). EPS import: closepath respects the path size limit (#1311). The interface is available in Hungarian (#1302), and more Czech labels are translated (#1259). Linux RPMs declare shared-library capabilities (#1290). CI pins every third-party action to a verified commit (#1321). Select menu commands skip hidden and locked art and stay inside the isolated group in isolation mode (#1325). Fill, stroke, gradient and Appearance panel commands that change nothing add no undo step (#1331). EPS import no longer hangs on shading functions fanned out through shared arrays (#1330), looped huge arcs, fine dashes and rectangle arrays (#1329), or very long dash patterns (#1332). Plug-in whole-number fields keep values past 2^53 exactly as typed (#1327). Large documents use far less memory: decoded images are shared and capped, undo history is bounded by memory, native files open in the background and save as a stream, and Data Recovery writes images once (#1245). Opacity mask commands (#1336), Simplify and Add Anchor Points (#1337) add no undo step when they change nothing. EPS files with thousands of tiling patterns, spot inks or clipped shadings open several times faster with the same result (#1341). A `.ai` or PDF saved from a CMYK document with transparency no longer opens blank in applications that read soft masks by luminosity (#1359, part of #1286). Gradient-filled shapes in a PDF or `.ai` open as one path with the gradient instead of a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on their outlines (#1351). A `.ai` saved with a bleed opens on its artboard with the bleed round it as the document's bleed, from its layers, and Crop To › Trim does the same for a PDF (#1349); a PDF saved with its editing data opens from it, with its layers, groups and art past the page, as a `.ai` does (#1345). Art moved into a hidden or locked layer (row drag, Merge Selected, Flatten Artwork, Collect in New Layer) or hidden by a data set is deselected (#1355), and hiding or locking a layer with a large selection deselects its objects in linear time (200,000 objects: 3.9 s to 24 ms) (#1353). Objects moved whole together with direct-selected anchors follow Preferences › Transform Pattern Tiles (#1354). Preferences › Appearance of Black › On Screen works: Display All Blacks as Rich Black shows 100K black as RGB black on the canvas (#1350). DXF: a closed control-point SPLINE opens as a closed path (#1318). Numeric fields: dragging over part of a value in an unfocused field selects that part instead of the whole value (#1339). Pathfinder Divide: an open line or curve across a filled shape cuts it into separate pieces (#1335); Unite, Minus Front, Minus Back, Intersect and Exclude results keep the source object's name, so exported SVG keeps its id (#1308). Object › Align follows the Align To choice (one object aligns to the artboard), and the choice is kept across restarts (#1347). Artboard tool: a click on an object creates an artboard fitted to it (#1252). Rulers: drag the ruler corner to move the zero point, double-click it to reset; the X/Y fields measure from it (#1250). Live Corners: with Direct Selection, a click on a corner widget selects that corner, so the next drag rounds it alone (#1239). A click with the Polar Grid tool opens a dialog for its size and divider counts, as the Rectangular Grid tool does (#1192). A `.ai` whose editing data holds an image on hidden lines is no longer misread (#1364, part of #1264). PDF export leaves out art placed beyond 1,000,000,000 pt instead of aborting debug and test builds (#1360). Affinity import: rectangles keep per-corner radii and straight (cut) corners (#1369, part of #666). Tools panel: the tool buttons are centred inside the panel border, in one and two columns (#1363). Spanish: 371 more interface strings translated (presets, shortcut editor, print and PDF options, Save for Web and more) (#1371). The Properties panel no longer widens and cuts off its labels when an Image Trace object is selected (#1374, part of #1358). Art drawn flush with an artboard (a background a rounding error outside it) moves, copies and scales with the artboard again (#1375, from #1295). 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). A PDF saved with its editing data and a bleed opens on its artboard with the bleed round it, as a `.ai` does (#1379, follow-up to #1349 and #1345). Dialogs opened from a menu focus their first field every time they open, not only the first time, so typing, Tab and Enter work (#1382, fixes #1367); a dialog whose first field is greyed out (Blend Options on Smooth Color, Layers Panel Options) gives the keyboard focus to that field once it is enabled (#1395). In a `.ai` opened from its editing data, a gradient with stops in the middle draws across its whole shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392) (follow-ups to #1264). `--control` without a valid port (or a bad `VECTORCRAFT_CONTROL_PORT`) stops the desktop app with an error instead of starting without a control server, and `--control=` works too (#1383). Documents with many symbol instances or inline graphics redraw faster and use less memory: the art placed for each instance is kept between frames (#1386). PDF and `.ai` import and placed PDFs keep overprint (`/OP`, `/op`), so overprinting spot colours still overprint after export (#1413, fixes #1388). A `.ai` whose PDF part draws only text opens from that part when its editing data can't be read, instead of being refused (#1414, fixes #1387). In a `.ai` opened from its editing data, a compound path takes the paint of its last path, so its gradient no longer comes in black, and paths grouped inside a compound path stay its paths (#1426, fixes #1410). With anchors selected with Direct Selection, the Scale, Rotate, Reflect and Shear tools, their dialogs and Move transform only those anchors (#1427, fixes #1408). The Move, Rotate, Scale, Reflect and Shear dialogs have a live Preview, and the arrow keys step their angles and percentages by 1 (Shift: 10) (#1428, fixes #1412). A stroke wider than its closed shape (a small ellipse or rectangle with a heavy stroke) paints solid on the canvas, as in exports, instead of showing a hole (#1409, fixes #1396). The bounding box hides while a corner radius is dragged with the Selection tool (#1402, fixes #1229). Pasting a picture works when the clipboard also holds a blank text item beside it (#1404). `document.inspect` reports a compound path's fill, stroke and weight and every node's blend mode when it isn't Normal (#1397, fixes #1389); `object.isolate` with an id that doesn't exist says there is no such object (#1415); the MCP guide covers Spacing Guides and four more preferences, all resources, and the symbol, brush, variable and Image Trace commands (#1418). PDF and `.ai` import keep a stroke opaque and unmasked when the fill under it was faded or masked by its group, and a `.ai` read as its PDF part keeps its RGB numbers (#1434). SVG export writes knockout groups of opaque art without masks (smaller files, no edge seams), and `vectorcraft-cli convert` takes `--decimals` (#1435). Delete, Cut, Group, Ungroup, Make and Release Compound Path, Make Clipping Mask, Send to Current Layer, Duplicate and the copying transforms of thousands of objects take a fraction of a second instead of seconds (#1430, part of #1429), and an Alt-drag with the selection and transform tools copies the selection once per drag instead of on every move (#1464, part of #1429). An Affinity text frame too short for its first line (a 64 pt title in a 54 pt frame) is made taller on import, with a note, instead of showing nothing (#1449, fixes #1423). 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 (from damaged files) hold no text instead of hanging the layout (#1451). Czech: 128 more strings translated (#1461). Plug-ins: a numeric parameter's default stays inside its declared range (#1463). Split Into Grid has the documented options: number, height or width, gutter and total for rows and columns, Add Guides and Preview; several selected paths become one grid painted as the topmost one (#1488, fixes #1462). Grayscale TIFFs with an alpha channel or other extra channels can be placed, relinked and opened, the alpha as the image's transparency (#1489, part of #1466). Selected text in numeric and dialog fields is drawn in white or black instead of blue on the blue highlight (#1490, part of #1465). The Pathfinder panel works on a group selected alone (its members are combined), and a greyed-out button says what it needs (#1493, for #1450). Transform Objects joins Transform Patterns in the Move, Rotate, Scale, Reflect and Shear dialogs and in Transform Each (off: only the pattern tiles transform), and Reflect takes an angle for its axis (#1494, fixes #1448 and #1460). A window larger than 8192 px (several monitors, a display-scale change) keeps its surface instead of closing the app (#1495, part of #1459). Commands run through MCP, the control channel and the CLI refuse a parameter key they don't take, naming it and the nearest accepted key (#1496, fixes #1442). A focused number field follows its value when a slider or preview changes it, so the next key no longer puts the old value back (#1497, fixes #1457). Effect › 3D and Materials: outlined words and other shapes of many small curves take Rotate, Extrude & Bevel and Inflate (curves are sampled by a tolerance; Rotate takes up to 65,536 outline points; #1481), Extrude & Bevel's Twist runs 0° to 360°, as the public panel reference gives it (#1524), and a twisted extrusion keeps its walls (from about 45° they were dropped; #1525). Text commands and the MCP `add_text` tool say when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). A tab stop in the Tabs panel drags from where the press landed, including one just added (#1487, fixes #1458). `graph.setData`'s CSV keeps each value exactly instead of rounding it to two decimals, so applying the returned CSV changes nothing (#1485). In Outline view a template layer keeps drawing in Preview, dimmed, so a picture placed to be traced stays visible (#1505, fixes #1486). Sliders in Save for Web, the raster export options, Preferences and the opacity popup show their rail, filled up to the handle (#1506, fixes #1483). Direct Selection picks the anchor of the selected object where anchors of overlapping objects coincide, also under another object (#1509, fixes #1508). Print-size images (past 512 MB decoded, up to 2 GB) draw in the canvas and in raster exports and can be trimmed with Crop Image; an image whose side is past 65,535 px is shrunk to fit, a raster export says when an image could not be decoded, and Crop Image's trim leaves CMYK images alone (#1517). On Windows, `vectorcraft-cli perf` reports the cores busy and, with `--deep`, its working set, and warns on a busy machine as on macOS and Linux (#1502). French: 423 missing strings added and 188 entries reworded to the feature names French users know (#1511). Hungarian: one message uses the informal address like the rest of the interface (#1474). | +| 2026-10-12 | Georgian interface (`ka`, ქართული): the whole catalog, plurals and messages, tested complete; Georgian letters come from an installed font (SF Georgian, Sylfaen, Noto Sans Georgian, DejaVu Sans) from the first frame; Type › Change Case › Title Case and Sentence case leave Georgian lower case instead of turning it into Mtavruli. | | 2026-10-11 | Effect › Brush Strokes: Accented Edges, Angled Strokes, Crosshatch, Dark Strokes, Ink Outlines, Spatter, Sprayed Strokes, Sumi-e (G2). Command parameters from agents and files checked before anything changes (paint, place, cuts, dashes, crop, asset ids, anchors, paths, distort; #984–#999); command dialogs keep whole numbers whole, so Create Object Mosaic takes the tiles typed (#1000); stepper arrows take Shift (×10) and Ctrl/Cmd (÷10) like the arrow keys (#991). Exports and Rasterize supersample Art Optimized edges, so shapes meeting edge to edge leave no seam (#983). Character and paragraph styles take leading, kerning, mojikumi and the other attributes left out at their defaults (#1001). Drawing, typing and pasting with no layer shown and unlocked is refused instead of adding art to a hidden or locked layer (#1002). MCP `add_text` gives type on or in a path the colour asked for (#1003). MCP `screenshot` of a connected app renders the artboard asked for (#1004). `vectorcraft-cli run` ends quietly when its reader closes early (#1005). Select › All on Active Artboard selects on the artboard made active, not always the first (#1006). Reverse Gradient keeps off-centre midpoints, mirrored (#1007). A polar grid with no radial dividers draws none (#1008). Use Precise Cursors also covers the Pen's join pointer (#1009). SVGs with one absolute and one unitless root side import at their size, unstretched (#1010). DXF US survey inches, yards and miles open at their size and name (#1011). A file placed into a document with no other links follows its first change even while the app sits idle (#1031). PDF import keeps the style a font name says when the file doesn't embed the font (#1023). Shift selects ruler guides and art together, and Delete removes both (#1022). Open filled paths are hit by their fill on every side, the rotate cursor bows around the corner it is at, the flip icons match their flips, the Color Picker previews on the selection, and Properties › Transform reaches Scale Corners and Scale Strokes & Effects (#1036). Offset Path and the other booleans finish on outlines where the sweep used to loop until memory ran out (linesweeper 0.5, #1028). Floating panel groups stack into sets that move together, collapse to their tabs and dock as extra dock columns that scroll (#1020). Ruler guides live on layers: listed in the Layers panel, shown, hidden, locked, duplicated, merged and deleted with their layer, and moved between layers (#1021). A `.ai` whose editing data uses global process colours (`Xk`/`XK`) or pattern fills keeps its sublayers: the colours come in as global swatches at their tint, the patterns as pattern swatches (#1025). A `.ai` in PostScript form opens through its layers, read from the program itself, also when it names the prolog defining its operators without including it, as Rhino and other CAD apps write it (#1027). A fill or stroke chosen while the Type tool has characters selected colours just those characters (#1063). Scaled type shows its scaled font size, leading, baseline shift and horizontal scale in the Character panel, and a size typed there is the size you get (#1034). Character panel menu › Proportional Metrics sets Japanese and Chinese full-width glyphs on the font's proportional widths in horizontal type (#966). Align, Distribute, Move, Arrange, Lock, Hide, Ungroup and other Object commands that would change nothing no longer record an undo step or mark the document changed (#1060). Spot colours defined in Lab open with their Lab values from `.ai` and PDF files, instead of as wrong RGB colours or their CMYK equivalents (#1032). Build scripts leave a generated source they wouldn't change untouched, so the next build doesn't compile most of the workspace again where file times lose their sub-second part (#1051). A fill colour recolours the gradient mesh point last clicked with the Mesh tool or Direct Selection, and a Mesh tool click on a mesh's point selects that mesh (#1052). The All Tools drawer stays two tools wide instead of filling a wide window (#1054). Text Wrap's Make, Release and Options refuse a list of object ids with a malformed or unknown id instead of skipping it (#1055). A freeform gradient line with a malformed point index is refused instead of being shortened to the indices that are valid (#1056). The Corners dialog (`ui.corners`) refuses a malformed object id or a listed anchor that isn't a corner instead of editing the selection or the other corners (#1057). New View refuses a centre that isn't exactly [x, y] and a rotation that isn't a number, instead of trimming the one and saving the other as 0° (#1058). The command sweeps try junk scale, spacing and artboard values too, which they had skipped since before those bugs were fixed (#1059). EPS and PostScript `arc` and `arcn` with huge angles no longer hang the import, and an arc whose angles are whole turns apart is empty, as the PostScript reference defines it (#1062). Align and Distribute Spacing keep the key object still also when it is inside a selected group or compound path (#1074). A `.ai` whose layers can't be read yet still opens with its artboards where they were (in a grid, say), not in a row (#1068). Layer Options (`ui.layerOptions`) refuses a malformed or unknown row id instead of skipping it or opening on the highlighted rows (#1077). Shaper scribbles given a list of object ids refuse a malformed or unknown id instead of skipping it and editing the shapes that remain (#1078). Delete Width Point (`stroke.widthPoint.remove`) refuses a list of indices with a malformed entry instead of deleting the points that remain, and no longer wraps a huge index round on the web (#1079). Preferences' checkboxes have an outline when the pointer isn't over them, in every interface brightness, as the app's other dialogs' checkboxes do (#1080). The last plain checkboxes (Properties panel snapping and Scale Strokes & Effects, Revolve, Save options, Export for Screens, Variables, plug-in forms) keep a visible box at rest in every theme too (#885). Setting or copying a width point with a malformed index is refused instead of adding a new point or wrapping a huge index round (follow-up to #1079). An Illustrator EPS opened through its layers keeps its own page size and the red outline of text stacked over a dark one, as 0.8.0 did (#1102). The macOS DMG is built so the app in it passes `codesign --verify --strict --deep`, and the release checks that it does (#1085). Swatch and graphic style libraries, Libraries panel files, presets and plug-ins dropped on the window open as File › Open opens them instead of being placed on the canvas (#1083). A key object needs another selected object outside it: a change that leaves one object selected lets the key go, and one selected object aligns to the artboard whatever Align To says (#1088). Align to Artboard aligns to the active artboard, and actions play their Align to Artboard, All on Active Artboard and Paste in Place, in Front and in Back steps on the artboard active when they play (#1105). Align refuses a call with neither direction, and Align, Distribute, Reflect and Shear refuse a direction, axis, target or bounds they don't take instead of falling back to a default (#1106). `object.setProps` deselects the objects it hides or locks, the key object included, as the Layers panel's eye and lock do (#1107). Align, Distribute and Distribute Spacing move pattern tiles with the art when General › Transform Pattern Tiles is on (#1108). Object › Lock › All Artwork Above and Object › Hide › All Artwork Above deselect the selected objects they lock or hide, the key object included (#1109). Save Swatch Library's `count` is the number of swatches the file holds: `.gpl` and `.ase` hold solid colors only, and CSS leaves freeform gradients out (#1110). In a rotated view (Rotate View), an artboard's name stays at its own top-left corner (#1095). Ctrl+Shift+L, C, R, J and F align or justify the paragraphs of the selected type, or those the Type tool's selection touches (#1111). On macOS, file dialogs are sheets on the document window, so resizing one no longer crashes the app; while one is open the menus, Quit and Close wait, and its answer goes to the document that asked (#867). Shape Builder merges regions that crossing lines cut along the lines' own edges, so a strip merged across a grid of lines is one outline with no seams left inside (#1081). Proportional Metrics also works in vertical type: upright glyphs take the font's proportional heights (`vpal`), and Line-end Punctuation Half Width takes nothing more off the punctuation it re-spaced (#1082). Plug-in whole-number parameters keep their exact value however large, and stay in the range the manifest gives (#1091). PDF and `.ai` import keeps type spaced glyph by glyph (character tightening, optical kerning) with each glyph in its place, giving each character its own tracking when the file's font is installed, and CJK punctuation closed up by its proportional width continues its line (#1101). Direct Selection: pressing an anchor of a path selected whole keeps all its anchors selected, so the drag moves them all; Alt-dragging by an anchor copies the whole path, held by that anchor, which snaps onto other anchors and onto the original the copy leaves behind; Alt pressed or released during the drag turns the copy on and off, as with the Selection tool, whose copies snap to their original too, and the undo step is named Copy or Move for what the drag did (#1104). The toolbar's Fill/Stroke proxy and Color, Gradient and None buttons grow in two columns, where the three drawing modes sit side by side (#1129). An image copied in a browser that puts its `` markup on the clipboard as text pastes as the picture, not the markup (#1089). `file.place` of an SVG with `crop: "crop"` frames and clips it by its viewBox, so art drawn on a shared canvas keeps its place and scale (#788). While a star is dragged, Ctrl/Cmd keeps its inner radius as the outer one grows and Alt keeps its shoulders straight (#812). Edit Toolbar opens the All Tools drawer beside the toolbar, dims the tools already in it, and adds, removes and moves tools by drag and drop while it is open (#1018). The Selection tool's arrow is black and Direct Selection's white in every theme, as users know them: in the dark themes they looked swapped. The font menu keeps its highlight on the same font when the list changes under it (a family's styles listed above it, fonts that finish loading, a favourite unstarred), and ends the live preview when that font leaves the list (#1116). The Windows installer has a Shortcuts page with a ticked "Create a desktop shortcut" box, and a silent install adds the shortcut unless `INSTALLDESKTOPSHORTCUT=""` is passed (#1128). Image Trace's Noise merges specks until none under its size is left beside another colour, so a higher Noise never gives more paths (#1130). `cargo clippy -D warnings` passes for the wasm32 web build (#1132). Gradient stops with opacity blend colour and opacity apart on the canvas, as Illustrator and the PDF and SVG exports do: a transparent stop's colour still tints the fade (#1142). An SVG mask whose art is all hidden imports as an empty opacity mask, so its object stays hidden as browsers draw it (#1143). Shift-click with the Artboard tool, or in the Artboards panel, selects several artboards, which then move, duplicate (Alt-drag) and delete together (#1139). A shape far beyond any page (from a malformed file) is left undrawn instead of making the renderer abort the app. Live Corners round corners where curved segments meet too, trimming each curve to what the cut leaves of it (#1144). Effect › Distort & Transform › Zig Zag's dialog has sliders for Size and Ridges per segment, and Relative / Absolute and Smooth / Corner as radio buttons (#1133). Object › Path › Offset Path opens on the offset, joins and miter limit it was last applied with (#1152). A marquee selects only the objects whose paths it touches: one drawn over a background, in Preview or Outline, no longer selects the background too (#1103). On Linux, Preferences › Performance › Linux Display Server › X11 (XWayland) opens the window through XWayland on a Wayland session, where files dropped from a file manager arrive (#1151). A selected gradient mesh shows its mesh lines and points with the Selection and Direct Selection tools too, not only with the Mesh tool (#1111). A blend's spine bends: the Anchor Point tool drags any of its segments into a curve, straight ones too, and Direct Selection reshapes its curved segments (`object.blend.spine.reshapeSegment`, #1111). Alt-click on a hidden layer's eye shows that layer as it hides the others, and on a locked layer's lock unlocks it as it locks the others: the layer clicked is the one left in view (#1158). Object › Compound Path › Release splits a path of several subpaths (an SVG `d` with several `M`s, a path drawn in one go over MCP) into one path per subpath, with its paint and stacking order, as it does a compound path (#1163). MCP `render_preview` takes an `artboard` index, so an agent can look at every page of a multi-artboard document (#1163). The MCP drawing tools say that omitted paint follows the new-art appearance (a white fill and 1 pt black stroke at first), and `export` says which formats take `outlineText` (#1148). The desktop app refuses a malformed `--automation-read-root` / `--automation-write-root` (no folder, an empty one, another option or the flag twice) before it starts, instead of dropping the root its environment variable set and leaving the control channel unconfined (#1157). PDF export warns, once per object, when the 64 million pixel cap renders its raster effects below the document's raster effects resolution, with the requested and the achieved ppi (#1147). MCP `command_batch` with `atomic: true` makes the whole batch one undo step, named by `label` (`edit.undoGroup.begin`/`end`, #1163). `file.list {path}` lists a folder's files and folders for agents, held to the automation read root (#1163). Thai typed in a font without Thai glyphs is set in an installed font made for Thai (Leelawadee UI, Tahoma, Thonburi, Sukhumvit Set, Noto Sans Thai…) instead of whichever face with Thai glyphs loaded first, and Lao, Khmer and Myanmar likewise. With the Type tool, the Left and Right arrows and Delete step over whole clusters (a Thai consonant with its vowel and tone marks, a letter with a combining accent, an emoji sequence), so the caret never lands inside one; Backspace still takes back one character at a time, a mistyped tone mark say. Thai, Lao, Khmer and Myanmar area type breaks its lines between words, which those scripts don't separate with spaces, instead of running on to the next space; the words come from ICU4X's line segmenter (its dictionaries on the desktop, its smaller LSTM models on the web). The font menu's script filter has Thai beside Japanese and Latin: the families whose character map has Thai. With craft-fonts' Noto Sans Thai, Thai is set in it where no Thai font is installed, and the web build fetches it before the app starts, so Thai no longer shows as empty boxes there. With the Type tool, Ctrl/Alt+arrows and double-click take Thai, Lao, Khmer and Myanmar text word by word (ภาษา, ไทย…) instead of a whole run without spaces, and a word's vowel and tone marks (and combining accents) stay part of it. Releases embed the current craft-fonts (Noto Sans Thai, Noto Sans Arabic and Noto Sans CJK SC beside the Japanese fonts), as the other craft apps' releases do. Object › Compound Path › Make cuts holes where the paths in front overlap the backmost one, also when they were drawn running the same way (#1197). On Windows, Direct3D 12 shaders compile with FXC, part of Windows, so a dxcompiler.dll another program left on the search path without its dxil.dll no longer loses the graphics device at start-up and drops the app to OpenGL (#1194). With anchor points selected by Direct Selection, the Align buttons align those points (to their own bounds or the artboard) instead of moving the whole path (#1180). With Smart Guides, an object grabbed by an anchor and dropped near another object's anchor lands exactly on it, instead of its bounds lining up a fraction of a point beside it (#955). .ai import: a copied area type or type-on-a-path object now sits where its own art puts it, not on top of the original (#1208). EPS/.ai import: a font is found by its exact PostScript name before the name is split, so families with hyphens keep their name (#1207). EPS/.ai import: type from the text document keeps its line spacing across paragraphs instead of applying the paragraph spacing twice (#1206). Brazilian Portuguese translates the remaining UI labels and engine messages (#1205). Czech joins the complete languages: panels, dialogs, plurals and status and error messages (#1189). Type: Digits (Character panel menu, with the Middle Eastern options) draws every digit in the Default, Arabic, Hindi or Farsi numeral system (#1201). Linux: under Wayland, Automatic draws on the GPU the compositor says it draws with, so a hybrid laptop whose compositor runs on the discrete GPU no longer opens an empty window (#1200). Large documents: editing a big selection takes time linear in the document (a 25,000-path nudge 3.5 s → 48 ms), live geometry effects are kept across frames, Pathfinder Trim, Merge, Crop and Outline are 9–20× faster with identical output, and undo history holds 41% less (#1173). | | 2026-10-10 | Initial live Revolve (Effect › 3D and Materials, #846, #605), in its own `three-d` crate. Progress docs re-measured and restructured to the craftrules standard (this page, `docs/target-app-parity.md`, `gaps.md`, `ui-parity.md`, the format, hardware, localization, effects and type checklists). Same day: v0.8.0; Effect › Distort, Pixelate and Texture filters (13); German interface; Variables (data merge); Graph Type value axes and tick marks; restart when the first frame never reaches the screen (#964) | | 2026-10-09 | Radial Blur, Smart Blur, Unsharp Mask (gap 2 begins); 340 commits, the busiest day | @@ -177,6 +185,10 @@ The full pre-standard record of what landed is kept in [docs/roadmap.md](docs/ro | Date | Change | Summary | |---|---|---| +| 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-17 | minor | Progress log row for the Background Export progress dialog (#2564); headline numbers unchanged. | | 2026-10-14 | minor | G11: open-path anchor Delete removes adjoining segments without bridging the gap; explicit Remove Anchors keeps its reconnect/refit behavior. Scores and hours unchanged. | | 2026-10-14 | minor | Document fonts register every explicitly supplied craft-fonts family after the existing Japanese, Arabic and Thai prefix; optional empty input remains supported. Headline scores and estimates are unchanged. | | 2026-10-14 | minor | Progress log rows for the later merges of 2026-10-14 (#1630–#1875 as listed there); headline numbers unchanged. | diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index bb3879b47..96cc2d0fb 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -78,16 +78,20 @@ USAGE: vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N] Headless batch: open FILE (any readable format) or start a new document, run commands in - order, export each FILE in the format its extension picks (see Writable formats). Prints one - JSON result per step. + order, export each FILE in the format its extension picks (see Writable formats; a .ait FILE + is a template, written as file.saveAsTemplate writes it). --scale sizes raster exports + (pixels per point, default 1); other formats leave it out. Prints one JSON result per step. vectorcraft-cli commands Print the command catalogue as JSON. vectorcraft-cli convert IN OUT [--scale N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] Open IN (any readable format) and export OUT in the format its extension picks (see Writable - formats). --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard + formats). --scale sizes a raster OUT (pixels per point, default 1); other formats leave it + out. --artboard is 0-based, --range 1-based (\"1-3,5\"); a PDF gets every artboard unless one of them is given, EPS the bounds of the art, the other formats the first artboard. + A .ait OUT is a template of every artboard, written as file.saveAsTemplate writes it (not with + --artboard or --range). Live effects are kept, and hidden layers and objects (written hidden in SVG and PSD), with SVG's data-* attributes; --outline-text writes SVG text as paths, --decimals sets SVG's coordinate precision (1–7, default 3). OUT may not be IN when reading @@ -159,6 +163,16 @@ fn main() -> ExitCode { } } +/// A headless session that remembers what the user keeps between runs: the PDF presets saved, the +/// ICC profiles loaded and the plug-ins installed (#2168, #2174, #2541), in the folders next to the preferences that the desktop +/// app uses too. `VECTORCRAFT_NO_PREFS` and `VECTORCRAFT_CONFIG_DIR` apply as they do there. +fn with_stores(mut h: Headless) -> Headless { + prefs_dir::attach_profile_store(&mut h.session); + prefs_dir::attach_plugin_store(&mut h.session); + prefs_dir::attach_pdf_preset_store(&mut h.session); + h +} + fn mcp(args: &[String]) -> Result<(), String> { let mut connect: Option = None; let mut headless = false; @@ -198,14 +212,14 @@ fn mcp(args: &[String]) -> Result<(), String> { eprintln!("vectorcraft-cli: files confined: read root {}, write root {}", show(roots.read_root()), show(roots.write_root())); Box::new(Headless::with_document().with_automation_roots(Some(roots))) } else if headless { - Box::new(Headless::with_document()) + Box::new(with_stores(Headless::with_document())) } else if let Some(addr) = connect { // Explicit address: fail loudly if the app isn't there. Box::new(Remote::connect(&addr).map_err(|e| format!("cannot connect to {addr}: {e}"))?) } else { match Remote::connect(DEFAULT_ADDR) { Ok(r) => Box::new(r), - Err(_) => Box::new(Headless::with_document()), + Err(_) => Box::new(with_stores(Headless::with_document())), } }; eprintln!("vectorcraft-cli: MCP server on stdio ({})", backend.describe()); @@ -213,8 +227,8 @@ fn mcp(args: &[String]) -> Result<(), String> { // uses `vectorcraft_mcp` free to bring its own. Silent until a client sends logging/setLevel. vectorcraft_mcp::logging::install(); let stdin = std::io::stdin(); - let stdout = std::io::stdout(); - Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string()) + // The server reads and writes from threads of its own, so it takes the handles, not locks held here. + Server::new(backend).serve(std::io::BufReader::new(stdin), std::io::stdout()).map_err(|e| e.to_string()) } fn commands() -> Result<(), String> { @@ -249,6 +263,10 @@ fn convert(args: &[String]) -> Result<(), String> { } } let [input, output] = <[String; 2]>::try_from(files).map_err(|_| "convert needs IN and OUT")?; + let template = is_template(&output); + if template && (artboard.is_some() || range.is_some()) { + return Err("a .ait template holds every artboard: --artboard and --range don't apply".into()); + } let mut h = Headless::new(); let opened = if editing_data { let params = json!({"path": input, "editingData": "always"}); @@ -256,16 +274,23 @@ fn convert(args: &[String]) -> Result<(), String> { } else { h.call("app.open", json!({"path": input})).map_err(|e| format!("open {input}: {e}"))? }; - // A conversion keeps hidden layers and objects, written hidden, where the format can (SVG, - // PSD). - let mut params = json!({"path": output, "scale": scale, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy}); - if let Some(decimals) = decimals { - params["decimals"] = json!(decimals); - } + let (command, mut params) = if template { + // Save as Template writes every artboard, with the .ai options. + ("file.saveAsTemplate", json!({"path": output, "format": "ait", "outlineText": outline_text, "acknowledgeLoss": replace_lossy})) + } else { + // A conversion keeps hidden layers and objects, written hidden, where the format can (SVG, + // PSD). + let params = json!({"path": output, "artboard": artboard, "range": range, "outlineText": outline_text, "hiddenLayers": true, "acknowledgeLoss": replace_lossy}); + let mut params = with_pixel_scale(params, &output, scale); + if let Some(decimals) = decimals { + params["decimals"] = json!(decimals); + } + ("document.export", params) + }; for (key, value) in legacy { params[key] = json!(value); } - let mut r = h.call("engine.execute", json!({"command": "document.export", "params": params})).map_err(|e| format!("export {output}: {e}"))?; + let mut r = h.call("engine.execute", json!({"command": command, "params": params})).map_err(|e| format!("export {output}: {e}"))?; // What reading the input left out (a missing linked file, an unsupported filter…) matters as // much as what writing the output did. if let (Some(w), Some(out)) = (opened.get("warnings").filter(|w| w.as_array().is_some_and(|a| !a.is_empty())), r.as_object_mut()) { @@ -275,6 +300,21 @@ fn convert(args: &[String]) -> Result<(), String> { Ok(()) } +/// Is `path` a `.ait` template? `file.saveAsTemplate` writes it; exports don't. +fn is_template(path: &str) -> bool { + vectorcraft_engine::cmd::fileio::format_for_name(path).is_some_and(|f| f.id == "ait") +} + +/// The export `params` for `path`, with `--scale` (pixels per point) when its format is raster. +/// DXF has a `scale` of its own (drawing units per unit), which would resize the drawing and leave +/// its units unset. +fn with_pixel_scale(mut params: Value, path: &str, scale: f64) -> Value { + if vectorcraft_engine::cmd::fileio::writable_format(None, Some(path)).is_ok_and(|f| f.raster) { + params["scale"] = json!(scale); + } + params +} + fn info(args: &[String]) -> Result<(), String> { let file = args.first().ok_or("info needs a FILE")?; let mut h = Headless::new(); @@ -327,7 +367,7 @@ fn run(args: &[String]) -> Result<(), String> { } } - let mut h = Headless::new(); + let mut h = with_stores(Headless::new()); let mut out = std::io::stdout().lock(); // A reader that stopped early (a closed pipe) isn't an error, and the batch goes on: its later // steps (an export…) still run, unprinted. @@ -361,7 +401,12 @@ fn run(args: &[String]) -> Result<(), String> { r } Step::Export(path) => { - let r = h.call("app.export", json!({"path": path, "scale": scale})).map_err(|e| format!("export {path}: {e}"))?; + let r = if is_template(&path) { + h.call("engine.execute", json!({"command": "file.saveAsTemplate", "params": {"path": path, "format": "ait"}})) + } else { + h.call("app.export", with_pixel_scale(json!({"path": path}), &path, scale)) + }; + let r = r.map_err(|e| format!("export {path}: {e}"))?; emit(json!({"step": "export", "result": r}))?; r } diff --git a/apps/vectorcraft-cli/tests/cli.rs b/apps/vectorcraft-cli/tests/cli.rs index 37bb28c22..b376a139f 100644 --- a/apps/vectorcraft-cli/tests/cli.rs +++ b/apps/vectorcraft-cli/tests/cli.rs @@ -7,10 +7,18 @@ use serde_json::{Value, json}; const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); +/// Returns the path of `name` in `vectorcraft-cli-test-` in the temp folder. The first call removes +/// that folder and creates it again empty. fn tmp(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("vectorcraft-cli-test-{}", std::process::id())); + static DIR: std::sync::OnceLock = std::sync::OnceLock::new(); + DIR.get_or_init(|| empty_dir(std::env::temp_dir().join(format!("vectorcraft-cli-test-{}", std::process::id())))).join(name) +} + +/// Removes `dir` and everything in it, creates it again empty and returns it. +fn empty_dir(dir: std::path::PathBuf) -> std::path::PathBuf { + let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - dir.join(name) + dir } #[test] @@ -50,6 +58,28 @@ fn run_batch_exports() { assert_eq!(last["result"]["artboards"][0]["width"], 200.0); } +#[test] +fn scale_sizes_raster_exports_only() { + // --scale is pixels per point. DXF has a `scale` of its own (drawing units per mm) that would + // resize the drawing and leave its units unset. + let svg = tmp("scale.svg"); + std::fs::write(&svg, r#""#) + .unwrap(); + let svg = svg.to_str().unwrap(); + let (plain, scaled, png) = (tmp("plain.dxf"), tmp("scaled.dxf"), tmp("scaled.png")); + let ok = |args: &[&str]| { + let out = Command::new(BIN).args(args).output().unwrap(); + assert!(out.status.success(), "{args:?}: {}", String::from_utf8_lossy(&out.stderr)); + }; + ok(&["convert", svg, plain.to_str().unwrap()]); + ok(&["convert", svg, scaled.to_str().unwrap(), "--scale", "2"]); + assert!(std::fs::read(&scaled).unwrap() == std::fs::read(&plain).unwrap(), "convert --scale 2 changed the DXF"); + ok(&["run", "--in", svg, "--export", scaled.to_str().unwrap(), "--export", png.to_str().unwrap(), "--scale", "2"]); + assert!(std::fs::read(&scaled).unwrap() == std::fs::read(&plain).unwrap(), "run --scale 2 changed the DXF"); + // 100 pt at scale 2 → 200 px (IHDR width). + assert_eq!(std::fs::read(&png).unwrap()[16..20], 200u32.to_be_bytes()); +} + #[test] fn run_reports_errors() { let out = Command::new(BIN).args(["run", "--cmd", "nope.nothing"]).output().unwrap(); @@ -87,7 +117,20 @@ fn mcp_headless_over_stdio() { /// server starts, and the process then reads and writes only inside its roots. #[test] fn mcp_automation_roots() { - let base = tmp("roots"); + automation_roots(&tmp("roots")); +} + +/// A run that has the process id of an earlier run finds that run's files and links in its folder. +/// `empty_dir`, which `tmp` uses, removes them, so the run can make them again instead of failing +/// with "File exists". +#[test] +fn mcp_automation_roots_in_a_folder_an_earlier_run_left() { + let dir = tmp("earlier-run"); + automation_roots(&dir.join("roots")); + automation_roots(&empty_dir(dir).join("roots")); +} + +fn automation_roots(base: &std::path::Path) { for d in ["in", "out", "outside"] { std::fs::create_dir_all(base.join(d)).unwrap(); } @@ -346,3 +389,97 @@ fn run_with_a_closed_stdout_ends_quietly_and_finishes_the_batch() { let out = Command::new(BIN).args(["run", "--cmd", "not-a-command"]).stdout(Stdio::null()).output().unwrap(); assert!(!out.status.success()); } + +/// A fresh settings folder for one test (`VECTORCRAFT_CONFIG_DIR`), so no run reads or writes the +/// developer's real profile. +fn config_dir(name: &str) -> std::path::PathBuf { + let dir = tmp(&format!("config-{name}")); + let _ = std::fs::remove_dir_all(&dir); + dir +} + +/// One `vectorcraft-cli run` with `steps` ((command, params)) against the settings folder `config`; +/// the result of each step. +fn run_in(config: &std::path::Path, steps: &[(&str, Value)]) -> Vec { + let mut cmd = Command::new(BIN); + cmd.arg("run").env("VECTORCRAFT_CONFIG_DIR", config).env_remove("VECTORCRAFT_NO_PREFS"); + for (id, params) in steps { + cmd.args(["--cmd", id, "--params", ¶ms.to_string()]); + } + let out = cmd.output().unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + String::from_utf8(out.stdout).unwrap().lines().map(|l| serde_json::from_str::(l).unwrap()["result"].clone()).collect() +} + +/// An ICC file of an RGB profile named `name` (a built-in one with its description replaced by a +/// name of the same length). +fn icc_file_named(path: &std::path::Path, name: &str) { + let builtin = "Display P3"; + assert_eq!(builtin.len(), name.len()); + let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::>(); + let mut bytes = vectorcraft_color::cms::icc_bytes(builtin).unwrap().to_vec(); + let (from, to) = (utf16(builtin), utf16(name)); + let at = bytes.windows(from.len()).position(|w| w == from).expect("the description in the profile"); + bytes[at..at + to.len()].copy_from_slice(&to); + std::fs::write(path, bytes).unwrap(); +} + +fn profile_names(settings: &Value) -> Vec<&str> { + settings["profiles"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap()).collect() +} + +/// #2168, #2174: the PDF presets saved and the ICC profiles loaded in one headless run are there in +/// the next run, and gone again once deleted. +#[test] +fn saved_pdf_presets_and_loaded_profiles_survive_a_restart() { + let config = config_dir("restart"); + let icc = tmp("studio-p3.icc"); + icc_file_named(&icc, "StudioRGB3"); + let r = run_in( + &config, + &[ + ("pdf.preset.save", json!({"name": "Press", "description": "for the printer"})), + ("color.loadProfile", json!({"path": icc.to_str().unwrap()})), + ], + ); + assert_eq!(r[1]["name"], "StudioRGB3"); + + let r = run_in(&config, &[("pdf.preset.list", json!({})), ("edit.colorSettings", json!({}))]); + let presets: Vec<&str> = + r[0]["presets"].as_array().unwrap().iter().filter(|p| p["builtIn"] == false).map(|p| p["name"].as_str().unwrap()).collect(); + assert_eq!(presets, ["Press"]); + assert_eq!(r[0]["presets"].as_array().unwrap().iter().find(|p| p["name"] == "Press").unwrap()["description"], "for the printer"); + assert!(profile_names(&r[1]).contains(&"StudioRGB3"), "{:?}", profile_names(&r[1])); + // The profile works as a working space, and the file it came from can go. + std::fs::remove_file(&icc).unwrap(); + let steps = [ + ("edit.colorSettings", json!({"rgb": "StudioRGB3"})), + ("pdf.preset.delete", json!({"name": "Press"})), + ("edit.colorSettings", json!({"rgb": vectorcraft_color::cms::SRGB})), + ("color.removeProfile", json!({"name": "StudioRGB3"})), + ]; + let r = run_in(&config, &steps); + assert_eq!(r[0]["rgb"], "StudioRGB3"); + assert_eq!(r[3]["removed"], "StudioRGB3"); + + let r = run_in(&config, &[("pdf.preset.list", json!({})), ("edit.colorSettings", json!({}))]); + assert!(r[0]["presets"].as_array().unwrap().iter().all(|p| p["builtIn"] == true)); + assert!(!profile_names(&r[1]).contains(&"StudioRGB3")); +} + +/// Runs without preferences (`VECTORCRAFT_NO_PREFS`) keep what they save for the run only, and +/// leave nothing in the settings folder. +#[test] +fn runs_without_preferences_remember_nothing() { + let config = config_dir("noprefs"); + let out = Command::new(BIN) + .args(["run", "--cmd", "pdf.preset.save", "--params", r#"{"name":"Press"}"#]) + .env("VECTORCRAFT_CONFIG_DIR", &config) + .env("VECTORCRAFT_NO_PREFS", "1") + .output() + .unwrap(); + assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr)); + assert!(!config.exists(), "nothing written"); + let r = run_in(&config, &[("pdf.preset.list", json!({}))]); + assert!(r[0]["presets"].as_array().unwrap().iter().all(|p| p["builtIn"] == true)); +} diff --git a/apps/vectorcraft-cli/tests/fileio.rs b/apps/vectorcraft-cli/tests/fileio.rs index db1174188..4de10d932 100644 --- a/apps/vectorcraft-cli/tests/fileio.rs +++ b/apps/vectorcraft-cli/tests/fileio.rs @@ -8,9 +8,16 @@ use serde_json::Value; const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); +/// Returns the path of `name` in `vectorcraft-cli-fileio-` in the temp folder. The first call +/// removes that folder and creates it again empty. fn tmp(name: &str) -> String { - let dir = std::env::temp_dir().join(format!("vectorcraft-cli-fileio-{}", std::process::id())); - std::fs::create_dir_all(&dir).unwrap(); + static DIR: std::sync::OnceLock = std::sync::OnceLock::new(); + let dir = DIR.get_or_init(|| { + let dir = std::env::temp_dir().join(format!("vectorcraft-cli-fileio-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + dir + }); dir.join(name).to_string_lossy().to_string() } @@ -172,3 +179,28 @@ fn convert_writes_legacy_ai_with_its_options() { assert!(!bad.status.success()); assert!(String::from_utf8_lossy(&bad.stderr).contains("hiddenObjects"), "{}", String::from_utf8_lossy(&bad.stderr)); } + +/// convert and `run --export` write a `.ait` as Save as Template does: a PDF-compatible template of +/// every artboard that opens as a new untitled document. +#[test] +fn convert_and_run_write_ait_templates() { + let pdf = tmp("template-in.pdf"); + make(&pdf, 120, 80); + let (converted, exported) = (tmp("converted.ait"), tmp("exported.ait")); + let r: Value = serde_json::from_str(&ok(&["convert", &pdf, &converted])).unwrap(); + assert_eq!((&r["format"], &r["path"]), (&Value::from("ait"), &Value::from(converted.as_str())), "{r}"); + let r: Value = serde_json::from_str(ok(&["run", "--in", &pdf, "--export", &exported]).lines().nth(1).unwrap()).unwrap(); + assert_eq!(r["result"]["format"], "ait", "{r}"); + for ait in [&converted, &exported] { + assert!(std::fs::read(ait).unwrap().starts_with(b"%PDF"), "{ait}"); + let opened: Value = serde_json::from_str(ok(&["run", "--in", ait]).lines().next().unwrap()).unwrap(); + let opened = &opened["result"]; + assert_eq!((&opened["format"], &opened["restored"]), (&Value::from("ait"), &Value::from(true)), "{opened}"); + assert!(opened["title"].as_str().is_some_and(|t| t.starts_with("Untitled-")), "{opened}"); + assert_eq!(info(ait)["artboards"].as_array().unwrap().len(), 2, "{ait}"); + } + // A template holds every artboard: one can't be picked. + let one = Command::new(BIN).args(["convert", &pdf, &tmp("one.ait"), "--artboard", "0"]).output().unwrap(); + assert!(!one.status.success()); + assert!(String::from_utf8_lossy(&one.stderr).contains("every artboard"), "{}", String::from_utf8_lossy(&one.stderr)); +} diff --git a/apps/vectorcraft-web/Cargo.toml b/apps/vectorcraft-web/Cargo.toml index 2e9218dc3..f67a377ef 100644 --- a/apps/vectorcraft-web/Cargo.toml +++ b/apps/vectorcraft-web/Cargo.toml @@ -26,6 +26,7 @@ log = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] } rfd = { workspace = true } wasm-bindgen = "0.2" +serde_json = { workspace = true } wasm-bindgen-futures = "0.4" js-sys = "0.3" web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlIFrameElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "ClipboardEvent", "DataTransfer", "File", "FileList", "Url", "Location", "CssStyleDeclaration", "Event", "EventTarget", "MouseEvent", "PointerEvent", "TouchEvent", "AddEventListenerOptions", "DomRect", "DragEvent", "Storage", "Navigator", "RequestInit", "Response", "AbortController", "AbortSignal"] } diff --git a/apps/vectorcraft-web/src/api.rs b/apps/vectorcraft-web/src/api.rs new file mode 100644 index 000000000..526e56be1 --- /dev/null +++ b/apps/vectorcraft-web/src/api.rs @@ -0,0 +1,184 @@ +//! `window.vectorcraft`: the control channel for the page, as promises. +//! +//! The same JSON methods as the desktop control server (`docs/control-protocol.md`), so an agent +//! driving VectorCraft in a browser tab (or an `