diff --git a/.github/workflows/house-rules.yml b/.github/workflows/house-rules.yml new file mode 100644 index 000000000..de64ad3ee --- /dev/null +++ b/.github/workflows/house-rules.yml @@ -0,0 +1,28 @@ +name: House rules + +on: + push: + branches: [main] + paths: + - AGENTS.md + - CLAUDE.md + - .github/workflows/house-rules.yml + pull_request: + paths: + - AGENTS.md + - CLAUDE.md + - .github/workflows/house-rules.yml + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: house-rules-${{ github.ref }} + cancel-in-progress: true + +jobs: + house-rules: + uses: storytold/craft-common-actions/.github/workflows/house-rules.yml@main + with: + rust: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07b3c6b7f..bbb0b8d79 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,6 +82,7 @@ jobs: WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} + LINUX_SIGNING_KEY: ${{ secrets.LINUX_SIGNING_KEY }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} diff --git a/AGENTS.md b/AGENTS.md index 0d33612d5..58ab2d6d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,56 @@ + +## House rules + +These apply in every Craft repository. A later rule may be stricter, not weaker. The source is `standards/house-rules-core.md` in `storytold/craft-contrib`. Propose changes there, not in a copy. + +1. **A person is accountable.** Whoever runs an agent answers for what it opens and for its review comments. +2. **Check, then claim.** Before writing code, read the issue comments, search pull requests for `#N` and the keywords, and check recent commits on `main`. Comment on the issue first. Release a claim you will not finish. +3. **Verify, don't guess.** Open a definition before using it. Confirm the cause in the code, even when someone names a location. +4. **Keep fixes narrow and tested.** Fix what the issue describes, where the bug is. Add a test that fails without the fix. +5. **Review the diff before opening a pull request.** Identifiers exist as used, the test fails without the fix, nothing unrelated changed, and the description is true. +6. **Look for a better pull request.** If another fixes the same thing, close yours when it is better or equal, and say why. +7. **Report only what ran.** Say what you tested and what you could not run. Without a local build or test, say so and rely on CI before marking ready. +8. **`main` changes through a pull request.** Do not push to it. Branch protection enforces this. There is no exception. +9. **Stop and ask only about** licensing outside the allowlist, publishing, releases, a new remote, secrets, spending money, destructive git, and anything else hard to undo. Otherwise keep working. `plan/` is a gitignored notebook, not rules. Without it, do not ask for it and do not stop. +10. **Keep secrets and personal details out** of the repository, logs and requests. +11. **CI stays shared.** Workflows call `storytold/craft-common-actions`. Do not change CI in a feature or fix pull request. If CI cannot cover a change, open an issue there. +12. **Guards say why.** A check that looks removable says what it prevents. Read that before changing it. +13. **Instructions must be readable by everyone.** No notes about one machine, and no links to files a reader cannot open. `craftrules` is retired. Do not link to it. +14. **Keep pull requests moving.** Concurrent pull requests are fine; unanswered actionable feedback is not. Respond in 7 days, a reminder at 14, closure eligible at 30, from when an actionable request waits on the author. Suggestions and unconfirmed bot findings do not start the clock. A fix, explanation or agreed plan resets it; unrelated activity does not. Waiting on a reviewer or maintainer pauses it and does not count against the author. A maintainer decides closure; it is never automatic. Reuse search results, do not poll, and stop at a rate limit. +15. **No agent attribution.** A pull request does not say an agent wrote it. A repository rule may not require that. + + +## Rust Craft repositories + +These apply to native Craft apps and to shared Rust libraries. Select them by repository type. A repository that merely contains some Rust does not take this block. Rules later in a repository's file may be stricter, but not weaker. The source is `standards/house-rules-rust.md` in [storytold/craft-contrib](https://github.com/storytold/craft-contrib). + +1. **Never crash.** Non-test code does not panic and has no `unsafe`, except where the repository file names an exception. Return errors, treat input as hostile, and bound recursion and allocations. New production crate roots copy the `deny` attribute from an existing crate: `clippy::unwrap_used`, `clippy::expect_used`, `clippy::panic`, `clippy::unimplemented`, `clippy::todo`, `clippy::unreachable`. Every crash fix comes with a test that panicked before the fix. +2. **Clean room.** Learn from observed behaviour and public specifications. Do not copy another product's code, assets, strings or artwork. Check a dependency's licence before adding it. +3. **Assets and fonts.** Every asset is original or suitably licensed, and is recorded in `ATTRIBUTION.md` in the same change. Font files live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), not in the app repository. +4. **Rust only for native Craft apps.** No handwritten JavaScript or TypeScript, and no Tauri, Electron or webview shell, in those apps. Shared tooling follows its documented technical scope and maintainer-approved exceptions. Craft PR Review's existing Tauri and TypeScript UI is one such exception. It does not relax this rule for native apps. Preserve any existing web build. +5. **Time goes through `chrono`.** Do not hand-roll calendar conversion. For dates beyond what `chrono` can represent, follow [standards/time.md](https://github.com/storytold/craft-contrib/blob/main/standards/time.md). +6. **Contributor credits.** Add or edit only your own person's entry in `contributors/people.toml`, and only after asking them. Never guess a name or hand-edit `contributors.json`. + +## Licence allowlist + +Dependencies and assets use SPDX identifiers. An expression is allowed when every identifier is in this list. A repository may name a shorter list. It may not add an identifier here by editing its own copy of these rules. + +- `ISC` +- `MIT` +- `BSD-2-Clause` +- `BSD-3-Clause` +- `Apache-2.0` +- `OFL-1.1` +- `Ubuntu-font-1.0` +- `Bitstream-Vera` +- `CC0-1.0` +- `CC-BY-4.0` +- `CC-BY-SA-4.0` +- `LicenseRef-Public-Domain` + +`LicenseRef-Storyteller-Trademark` is not in this list. It covers Storyteller brand marks only, where a repository already says so. + + # VectorCraft — instructions for agents VectorCraft is a clean-room, open-source, Rust-native vector illustration app targeting Adobe Illustrator parity (and beyond). It runs natively on macOS, Windows and Linux, and on the web via WASM. Siblings: `../photocraft` (Photoshop-class) and `../pdfcraft` (Acrobat-class); same conventions. @@ -6,10 +59,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format ## Start every session here 1. Read [`ROADMAP.md`](ROADMAP.md) (stage, numbers by dimension, what's next) and the ranked work list in [`docs/gaps.md`](docs/gaps.md). Unless the user gives you a task, pick work from that list. -2. Read `plan/STATUS.md` (local session notes, may lag the ROADMAP), then the task in `plan/execution-plan.md` §3 and the relevant `plan/architecture.md` section. Behaviour reference: public documentation only (see Clean-room below); `plan/illustrator/*.md` holds notes from it. -3. Follow the autonomous operation protocol (`plan/execution-plan.md` §7): orient → plan → implement + test → verify → record → commit. Don't stop to ask unless §7 lists the decision as the user's. - -`plan/` is gitignored (local only). +2. `plan/` is an optional gitignored notebook. Behaviour notes may be in `plan/illustrator/*.md`. Do not ask for it and do not stop when it is absent. Orient, implement, test, verify, then record in the public docs. ## Non-negotiables - **Clean-room: never use Adobe software (absolute rule, from the project owner).** Adobe's Terms of Use forbid reverse engineering, including observing a program's inputs and outputs to recreate it. So: @@ -45,7 +95,6 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format - **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`). -- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic). - **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`. - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings including the no-panic lints, tests, layers, wasm). One task id per commit (`M2.1: pen tool`). @@ -88,3 +137,5 @@ When a task lands, update in the same PR: the area row in `docs/target-app-parit - Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here. - Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time. - Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it. + +New workspace crates include `[lints] workspace = true` in `Cargo.toml`. Production crate roots declare the crate-level lint attributes before modules; crate-level `//!` documentation may precede them. diff --git a/ASSETS.md b/ASSETS.md index e4a17c764..34ff73361 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -1,5 +1,21 @@ # Asset attribution +## Indic hyphenation patterns + +| Asset | Author | Source | Licence | Notes | +|---|---|---|---|---| +| `crates/text/patterns/hyph-hi.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-hi.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-mr.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-mr.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-gu.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-gu.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-bn.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-bn.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-pa.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pa.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-ta.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-ta.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-te.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-te.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-kn.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-kn.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-ml.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-ml.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-or.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-or.tex | MIT (selected option) | Unmodified; full license in file | +| `crates/text/patterns/hyph-as.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-as.tex | MIT (selected option) | Unmodified; full license in file | + Every non-code asset in this repository (icons, images, fonts, example art, colour profiles, presets) is listed here with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an asset file is missing from this table. **Policy (mandatory):** VectorCraft contains **no Adobe iconography, images, artwork, presets, swatch/brush/symbol/pattern libraries or ICC profiles**. Every asset is either original work by VectorCraft contributors, or third-party material under an open licence (OSI open source, public domain / CC0, or Creative Commons that allows redistribution), and it is attributed below. Screenshots of Adobe software are never committed. The one exception is the ArtCraft name, wordmark and logos in `docs/brand/`: ArtCraft trademarks, not open source, usable only under [`docs/brand/LICENSE-brand.txt`](docs/brand/LICENSE-brand.txt). @@ -115,6 +131,7 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/icons/dc-center-show.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-clear.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-color-guide.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-contour.svg` | VectorCraft contributors | Original work, drawn for VectorCraft (a solid rounded square inside four corner arcs, its offset outline); not derived from any other application's icon | MIT OR Apache-2.0 | | | `assets/icons/dc-crystallize.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-cube.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/dc-dash-align.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md new file mode 100644 index 000000000..f28ed3dde --- /dev/null +++ b/ATTRIBUTION.md @@ -0,0 +1,17 @@ +# Attribution + +The complete historical asset manifest is [ASSETS.md](ASSETS.md). These hyphenation assets use the MIT option, with the complete copyright and permission notices preserved in each file. + +| Asset | Author | Source | SPDX license | Notes | +|---|---|---|---|---| +| `crates/text/patterns/hyph-hi.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-hi.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-mr.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-mr.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-gu.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-gu.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-bn.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-bn.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-pa.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-pa.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-ta.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-ta.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-te.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-te.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-kn.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-kn.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-ml.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-ml.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-or.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-or.tex | MIT | Unmodified; full license in file | +| `crates/text/patterns/hyph-as.tex` | Santhosh Thottingal | https://github.com/hyphenation/tex-hyphen/blob/5684c0f51c0b81133db2efbe60a408b4155a3ff5/hyph-utf8/tex/generic/hyph-utf8/patterns/tex/hyph-as.tex | MIT | Unmodified; full license in file | diff --git a/CLAUDE.md b/CLAUDE.md index 17783d9a1..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,76 +1 @@ -# VectorCraft — instructions for agents - -VectorCraft is a clean-room, open-source, Rust-native vector illustration app targeting Adobe Illustrator parity (and beyond). It runs natively on macOS, Windows and Linux, and on the web via WASM. Siblings: `../photocraft` (Photoshop-class) and `../pdfcraft` (Acrobat-class); same conventions. - -Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format": "drawcraft"` headers still open (`vectorcraft_format::LEGACY_EXTENSION`), and preferences migrate from the old config folder. Keep those paths working; use the new name everywhere else. - -## Start every session here -1. Read [`ROADMAP.md`](ROADMAP.md) (stage, numbers by dimension, what's next) and the ranked work list in [`docs/gaps.md`](docs/gaps.md). Unless the user gives you a task, pick work from that list. -2. Read `plan/STATUS.md` (local session notes, may lag the ROADMAP), then the task in `plan/execution-plan.md` §3 and the relevant `plan/architecture.md` section. Behaviour reference: public documentation only (see Clean-room below); `plan/illustrator/*.md` holds notes from it. -3. Follow the autonomous operation protocol (`plan/execution-plan.md` §7): orient → plan → implement + test → verify → record → commit. Don't stop to ask unless §7 lists the decision as the user's. - -`plan/` is gitignored (local only). - -## Non-negotiables -- **Clean-room: never use Adobe software (absolute rule, from the project owner).** Adobe's Terms of Use forbid reverse engineering, including observing a program's inputs and outputs to recreate it. So: - - Never launch, run, script, automate (COM, AppleScript, ExtendScript, UI automation), screenshot or measure Illustrator or any other Adobe application, and never compare our output against one. Don't make test files with them, not even "synthetic" ones. - - Never read, disassemble or copy anything inside an Adobe install (binaries, plug-ins, presets, procsets, resources, help files, fonts, ICC profiles). - - Never copy Adobe-authored code or text into this repo, tests included: no PostScript procsets or prologs (AGM, CoolType, the Illustrator prologs), no sample files. Write test inputs from scratch from the public specifications (PostScript Language Reference, PDF / ISO 32000, TIFF, EPSF/DCS). - - Behaviour comes only from public documentation (helpx, published specs, public forum posts), cited by URL, and from files users send us that they own (used locally, never committed). Never copy Adobe icons, artwork, presets or wording beyond feature names. - - If a task seems to need any of this, stop and say so instead. `cargo xtask cleanroom` (run by `cargo xtask ci`) fails on Adobe copyright notices and on fixtures made with Adobe applications. - - Never copy GPL/AGPL code (Inkscape, lib2geom…). -- **Assets: no Adobe iconography or images — ever (absolute rule, from the project owner).** - - Never add, copy, trace, redraw-from, embed or ship any icon, image, artwork, cursor, preset, swatch/brush/symbol/pattern/style library, ICC profile or screenshot from Adobe products or from any other source whose licence doesn't allow it. - - Every image, icon, font or other asset must be one of: - - original work created for VectorCraft by a contributor (who licenses it MIT OR Apache-2.0); - - OSI open source; - - public domain / CC0; - - Creative Commons with redistribution allowed. - - **Every asset file must have a row in [`ASSETS.md`](ASSETS.md)** (path, author, source URL, licence, notes). Put licence texts next to the assets (e.g. `assets/fonts/OFL-*.txt`) and summarize them in `NOTICE`. - - `cargo xtask assets` (run by `cargo xtask ci`) fails on any unattributed asset. - - **Fonts live in [storytold/craft-fonts](https://github.com/storytold/craft-fonts), never in this repo**. Never commit a font file here; add new fonts to craft-fonts. The app uses it only through the optional `CRAFT_FONTS_DIR` build option (`CRAFT_FONTS_DIR="$PWD/../craft-fonts" cargo xtask ci`; use an absolute path), read by `crates/text/build.rs` into `vectorcraft_text::CRAFT_FONTS`; never add it to a `Cargo.toml`. Everything must build, test and run with `CRAFT_FONTS` empty, and tests of its glyphs skip when it is. Details: `docs/development.md` › Fonts. - - Code and original assets are MIT OR Apache-2.0 (`LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE`). The ArtCraft logos in `docs/brand/` are trademarks, not open source, under `docs/brand/LICENSE-brand.txt`: never modify them or use them outside VectorCraft. - - Prefer art generated in code for defaults (swatches, brushes, symbols, patterns, cursors). If the provenance of an asset is unclear, don't add it. - - Reference screenshots of Illustrator stay local under the gitignored `plan/` and are never committed, published or used as assets. -- **Never crash: no panics in shipped code.** A crash loses the user's unsaved work. Files, pasted data, MCP and control messages, and command params are all untrusted input. - - Fallible work returns `Result` (or `Option`); callers handle it or propagate it with `?`. Failures reach the user as an error message (`EngineError` for commands, the crate's own error type or `String` below the engine), never as an abort. - - Workspace clippy lints ban these outside tests, so `cargo xtask ci` fails on them: `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`. Use instead: `?`, `.ok_or(…)?`, `let … else { return … }`, `if let`, `unwrap_or` / `unwrap_or_default` / `unwrap_or_else`, `.first()` / `.last()` / `.split_last()`, and a real error for a match arm you believe is impossible. - - New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit `[workspace.lints]`. Production crate roots must declare `#![forbid(unsafe_code)]` before modules or other crate items; crate-level `//!` documentation may precede it. - - Don't panic implicitly either: use `.get(i)` instead of `v[i]` or `&s[a..b]` when the index comes from data. Don't divide integers by something that can be zero, cap sizes and counts read from input (allocations, images, loops), and check that floats are finite before casting them or looping on them. - - Don't silence errors just to satisfy the lint: use `let _ =` or `.ok()` only where ignoring the failure is the right behaviour, with a comment saying why. - - Tests may unwrap and panic (`#[test]`, `#[cfg(test)]`, `tests/`, `testkit`). Integration-test files start with an `#![allow(…)]` for that. - - `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`). -- **Time goes through `chrono`** (workspace dependency). Never hand-roll UTC/calendar conversion (days-to-date algorithms, leap-year math); a crate owns only its output format string. The one exception is range extension: past the years chrono can represent, shift the date by whole 400-year Gregorian cycles (146,097 days, after which the calendar repeats), let chrono compute the month and day, and add the cycles back to the year (splitting seconds into hours, minutes and seconds is plain arithmetic). -- **Shared test corpora.** Real-file test oracles (Photoshop-authored PSDs, etc.) live in [`storytold/photocraft-corpus`](https://github.com/storytold/photocraft-corpus). Never commit large binary fixtures to this repo; fetch them pinned by commit and sha256-verified, as PhotoCraft does with `cargo xtask corpus`. -- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings including the no-panic lints, tests, layers, wasm). One task id per commit (`M2.1: pen tool`). - -## Running and looking at the app -- `cargo run --release -p vectorcraft -- --control 7979 [file.svg|file.vectorcraft]` (sibling apps' agents use the same default port: if the log says it failed to bind, pick another port — otherwise your requests reach a different app). -- Drive it: JSON lines on `127.0.0.1:7979`, e.g. `{"id":1,"method":"engine.execute","params":{"command":"shape.rectangle","params":{"x":10,"y":10,"width":100,"height":50}}}` then `{"id":2,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}`. Methods: `crates/ui-egui/src/control.rs`. -- **For UI work, look at the result** (take `ui.screenshot`, read the PNG) and compare with the public documentation (`plan/illustrator/02-ui-ux.md` notes). `ui.screenshot` needs a presented frame: if it errors (screen locked), check the art with `ui.render` / `vectorcraft-cli run … --export x.png`, and cover panels with a headless egui frame test (see `panels/transparency.rs` tests). -- **Performance:** `vectorcraft-cli perf` checks the budgets (render/pan/zoom, hit test, save/open, SVG, Pathfinder) on a synthetic 50k-path document; `vectorcraft-cli bench FILE` times one file. Run them before and after renderer, format or geometry changes, on an idle machine (the report warns when the load average makes timings noise). -- MCP: `vectorcraft-cli mcp` (see `docs/mcp.md`). -- Shell gotcha: `mv`/`cp` are aliased interactive here — use `/bin/mv -f` / `/bin/cp -f`. -- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; write manifests atomically. Each target dir grows to ~30 GB: delete yours when you finish (a full disk fails links with `errno=28`). - -## Roadmap and progress docs -The progress docs: -- [`ROADMAP.md`](ROADMAP.md): stage, headline numbers, dimensions, features, languages, upcoming, progress log. -- [`docs/target-app-parity.md`](docs/target-app-parity.md): the authoritative parity assessment (area table, methodology, calibration, evidence of what ships). -- [`docs/gaps.md`](docs/gaps.md): every known shortfall, ranked; the work list. [`docs/roadmap.md`](docs/roadmap.md): milestones and current focus. -- Checklists: [`ui-parity.md`](docs/ui-parity.md), [`file-format-parity.md`](docs/file-format-parity.md), [`hardware-parity.md`](docs/hardware-parity.md), [`localization-parity.md`](docs/localization-parity.md), [`effects-parity.md`](docs/effects-parity.md), [`type-parity.md`](docs/type-parity.md); [`architecture.md`](docs/architecture.md). - -When a task lands, update in the same PR: the area row in `docs/target-app-parity.md` (score, missing items, hours) and its evidence list, the gap entry in `docs/gaps.md` if it closed or shrank, the checklist it belongs to, the milestone row in `docs/roadmap.md`, and the ROADMAP's tables and progress log when a headline number moves. Update each touched doc's status line and revision history. -- Grade by behaviour against the public documentation (`plan/illustrator/` notes), not by whether a menu item exists. Scores are self-assessed, so err low; mark each number measured or estimated. -- Keep the README's Status section in step with the ROADMAP headline. - -## CI workflows -- Workflows in `.github/workflows/` call the shared workflows in [`storytold/craft-common-actions`](https://github.com/storytold/craft-common-actions) at `@main`, with their inputs and secrets. No other jobs: if no shared workflow does what's needed, it doesn't go here. -- Don't add or change CI in a feature or fix PR. If the existing workflows can't build or test a change, link an existing craft-common-actions issue or PR, or open a new issue, or a PR only for a small fix that is safe for every app and does not increase CI time. -- Some workflow files here still run their own jobs. Don't add to them. Moving a job into craft-common-actions is its own task, never part of another PR, and the job stays here until a shared workflow runs it. +@AGENTS.md diff --git a/Cargo.lock b/Cargo.lock index ae8a3ee6c..6f92cb310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1093,6 +1093,17 @@ dependencies = [ "thiserror 2.0.21", ] +[[package]] +name = "craft-web-api" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=fd5e2feff6ea25bed371a99cc732b3968d621505#fd5e2feff6ea25bed371a99cc732b3968d621505" +dependencies = [ + "js-sys", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "craft-window" version = "0.1.0" @@ -6100,6 +6111,7 @@ dependencies = [ "serde_json", "smithay-clipboard", "socket2", + "tempfile", "vectorcraft-engine", "vectorcraft-render", "vectorcraft-text", @@ -6163,6 +6175,7 @@ dependencies = [ "objc2-core-foundation", "objc2-core-text", "serde_json", + "tempfile", "vectorcraft-color", "vectorcraft-doc", "vectorcraft-engine", @@ -6469,6 +6482,7 @@ dependencies = [ "vectorcraft-doc", "vectorcraft-effects", "vectorcraft-geom", + "vectorcraft-pathops", "vectorcraft-testkit", "vectorcraft-text", ] @@ -6591,6 +6605,7 @@ dependencies = [ name = "vectorcraft-web" version = "0.21.0" dependencies = [ + "craft-web-api", "eframe", "egui", "js-sys", diff --git a/NOTICE b/NOTICE index 4bf18085b..ea3c311a3 100644 --- a/NOTICE +++ b/NOTICE @@ -136,3 +136,8 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of 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. + +Indic hyphenation patterns (crates/text/patterns/): +Copyright (C) 2016 Santhosh Thottingal, used under the MIT license option. +Original copyright and full license notices are preserved in each hyph-*.tex file. +Source: hyphenation/tex-hyphen, commit 5684c0f51c0b81133db2efbe60a408b4155a3ff5. diff --git a/README.md b/README.md index 8a3c453c5..79d88841e 100644 --- a/README.md +++ b/README.md @@ -253,7 +253,7 @@ 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 .ai files (layer structure in real files; proprietary editing data isn't written); +- reliable exchange of .ai files (layer structure in real files; the editing copy a save writes leaves out live effects and writes symbols and live objects as their art); - 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); - filter thumbnails and a preview area inside the Effect Gallery (the gallery itself and all 57 Photoshop-style raster filters, approximated, are in); diff --git a/ROADMAP.md b/ROADMAP.md index 6cf80d362..68b0b2d2a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,8 @@ # VectorCraft Roadmap +- Contour add-on (beyond Illustrator): drag a selected shape's edge outward or inward for a live, editable offset copy, with a floating bar for distance and Sharp, Round or Square corners; Bake turns it into paths. Scores and estimates unchanged. +- Compound Shapes add-on (beyond Illustrator): a menu for Make, Release and Expand Compound Shape and a setting, off by default, that makes the Pathfinder panel's Shape Mode buttons create compound shapes on a click. Scores and estimates unchanged. + - Paint: new gradients use the document's color model for default stops, including Gradient tool creation. Scores and estimates unchanged. - PDF.R1: editable PDF import uses the shared reader, scans page content in windows, and rejects known incomplete input before returning a document. @@ -11,7 +14,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-11-03 · **Change:** minor (new gradients follow document color mode; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Hindi catalog draft complete under the translation tests; contributor loanword terminology reviewed; wider proofreading welcome; Compound Shapes add-on, beyond Illustrator; scores and estimates unchanged; Contour add-on, beyond Illustrator; scores and estimates unchanged; new gradients follow document color mode; scores and estimates 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 @@ -20,8 +23,8 @@ and the work list in [`docs/gaps.md`](docs/gaps.md). **Why alpha:** the core workflows exist end to end (drawing and path editing, Pathfinder, paint and appearance, type, SVG/PDF/EPS, print, export) and feature depth is ~75%, but a professional can't yet switch: Illustrator's -own `.ai` files open with a wrong layer structure in real cases and can't be saved back with Illustrator's -editing data, the interaction details have never been checked side by side, and some machines fail to start the +own `.ai` files open with a wrong layer structure in real cases and are saved back with an editing copy that +leaves out live effects and writes symbols and live objects as their art, the interaction details have never been checked side by side, and some machines fail to start the app. Beta needs those closed (the beta list in [gaps.md](docs/gaps.md)). It passes the core-workflow gate: all six core workflows (path drawing and editing, paint and style, type, layers/artboards/export, print production, file exchange) work end to end on macOS with save and reopen, and only the last is partial, without @@ -104,7 +107,7 @@ Weights, evidence and the missing items per area: [target-app-parity.md](docs/ta | English | `en` | full | 100% | | Simplified Chinese | `zh-hans` | partial (messages in English) | 85% | | Spanish | `es` | full | 100% | -| Hindi | `hi` | none | 0% | +| Hindi | `hi` | catalog complete; machine-assisted draft, native review pending | complete under the scanner | | Arabic | `ar` | none (no right-to-left interface) | 0% | | French | `fr` | full | 100% | | Portuguese (Brazil) | `pt-br` | full | 100% | @@ -131,6 +134,8 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-05: Release Type on a Path joins the Type Tools add-on (Object › Plug-ins › Type Tools): each selected type on a path splits into point type with the same text and attributes, on one straight line starting where its first character sat, and the plain path it was set on, with its exact shape and place in the stacking order and no paint; or keeps only the path or only the text, optionally leaving the original. A dialog with Preview, one undo step (`addon.type.releasePath`, #5532). A VectorCraft addition: headline numbers unchanged. +- 2026-11-04: New Document: the units chosen in Preset Details now belong to the chosen document, as the pane's options are listed for the selected preset; the chosen card shows them, every other Recent and Saved card keeps the units its document or preset was made in, and the built-in tabs still follow the chosen units, so switching between inches and millimetres no longer restyles every recent template. Scores and estimates unchanged. - 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. - 2026-10-31: Pattern libraries: Window › Swatch Libraries › Patterns and the Swatches panel's library menu open Geometric Patterns, a built-in library of eleven pattern swatches (dots, stripes, checks, a grid and shapes); clicking one adds its pattern and swatch to the document, once, and applies it to the active fill or stroke (`swatch.library.add`). - 2026-11-01: Find Nearest Swatch joins the Color Effects add-on (Object › Plug-ins › Color Effects): for each colour of the selection, the closest swatches of the document, a built-in library or a library or colour book the user loaded, with their CIEDE2000 difference; Apply gives each colour its nearest swatch as one undo step, linking global and spot swatches and adding a library's to the document first, so a process colour becomes the closest spot colour (`addon.color.nearestSwatch`, `addon.color.applyNearestSwatch`, #4923). @@ -218,6 +223,8 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Hindi (`hi`): 7,877-row machine-assisted catalog, zero/one singular plurals, desktop font fallback and optional Devanagari craft-fonts fallback; all 55 translation tests pass; native review and a web font remain; scores and estimates unchanged | +| 2026-11-05 | minor | Progress log row for the Type Tools add-on's Release Type on a Path (#5532); headline numbers unchanged. | | 2026-11-03 | minor | Progress log row for the Type Tools add-on's Text Tweaker (#4924); headline numbers unchanged. | | 2026-11-02 | minor | Closed font-family fields accept wheel input for selected text in Character, Properties and Control (#5253); scores and estimates unchanged. | | 2026-11-01 | minor | Progress log row for the Color Effects add-on's Find Nearest Swatch (#4923); headline numbers unchanged. | diff --git a/apps/vectorcraft-cli/Cargo.toml b/apps/vectorcraft-cli/Cargo.toml index 8f39e3b58..f7faf7a92 100644 --- a/apps/vectorcraft-cli/Cargo.toml +++ b/apps/vectorcraft-cli/Cargo.toml @@ -45,5 +45,6 @@ objc2-core-foundation = { version = "0.3", default-features = false, features = objc2-core-text = { version = "0.3", default-features = false, features = ["std", "CTFontManager"] } [dev-dependencies] +tempfile = "3" vectorcraft-testkit = { workspace = true } serde_json = { workspace = true } diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index b7334586c..2d8df4592 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -2,9 +2,9 @@ //! //! ```text //! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] [--automation-read-root DIR]... [--automation-write-root DIR]... -//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] +//! vectorcraft-cli run [--in FILE] [--cmd id [--params '{json}'] | --commands FILE|-]... [--export out.svg]... [--scale 2] [--ppi 300] //! vectorcraft-cli commands -//! vectorcraft-cli convert IN OUT [--scale 2] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] +//! vectorcraft-cli convert IN OUT [--scale 2] [--ppi 300] [--artboard 0 | --range 1-3,5] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] //! vectorcraft-cli info FILE //! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5] //! vectorcraft-cli perf [--paths 50000] [--deep] @@ -81,11 +81,12 @@ USAGE: allowed, and a kind left out grants none of its access. They imply --headless: a running app enforces its own (vectorcraft --control PORT --automation-read-root DIR …). - vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] + vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON] | --commands FILE|-]... [--export FILE]... [--scale N] [--ppi 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; 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. + is a template, written as file.saveAsTemplate writes it). Raster exports use the document's + raster effects resolution unless --scale (pixels per point) or --ppi (pixels per inch, wins + over --scale) is given; other formats leave them out. Prints one JSON result per step. --commands reads steps from FILE, or from standard input for -, where it stands among the --cmd and --export steps: a JSON array of {\"command\": ID, \"params\": {...}} (or \"id\" for \"command\"), or one such object per line (JSON Lines), at most 64 MiB and 100000 steps. @@ -94,10 +95,10 @@ USAGE: 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] [--preserve-editing] [--ai-version 7|pdf] [--hidden-objects layer|omit|visible] [--transparent-images flatten|omit] + vectorcraft-cli convert IN OUT [--scale N] [--ppi N] [--artboard I | --range R] [--outline-text] [--decimals N] [--replace-lossy] [--editing-data] [--preserve-editing] [--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). --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 + formats). Raster resolution: --ppi, else 72 times --scale, else the document's raster effects + resolution; other formats leave them 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). @@ -279,13 +280,15 @@ fn commands() -> Result<(), String> { } fn convert(args: &[String]) -> Result<(), String> { - let valued = ["--scale", "-s", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"]; + let valued = + ["--scale", "-s", "--ppi", "--artboard", "-a", "--range", "-r", "--decimals", "--ai-version", "--hidden-objects", "--transparent-images"]; if help_requested(args, &valued) { return help(); } let mut files = vec![]; let (mut scale, mut artboard, mut range, mut outline_text, mut replace_lossy, mut editing_data, mut preserve_editing) = - (1.0f64, None::, None::, false, false, false, false); + (None::, None::, None::, false, false, false, false); + let mut ppi = None::; let mut decimals = None::; // Legacy `.ai` options, passed on as the export's own (`aiVersion`, `hiddenObjects`, // `transparentImages`); the engine checks their values. @@ -293,7 +296,15 @@ fn convert(args: &[String]) -> Result<(), String> { let mut it = args.iter(); while let Some(a) = it.next() { match a.as_str() { - "--scale" | "-s" => scale = it.next().and_then(|v| v.parse().ok()).ok_or("--scale needs a number")?, + "--scale" | "-s" => scale = Some(it.next().and_then(|v| v.parse().ok()).ok_or("--scale needs a number")?), + "--ppi" => { + ppi = Some( + it.next() + .and_then(|v| v.parse().ok()) + .filter(|n: &f64| n.is_finite() && *n > 0.0) + .ok_or("--ppi needs a positive finite number")?, + ); + } "--artboard" | "-a" => artboard = Some(it.next().and_then(|v| v.parse().ok()).ok_or("--artboard needs an index")?), "--range" | "-r" => range = Some(it.next().cloned().ok_or("--range needs artboards such as 1-3,5")?), "--outline-text" => outline_text = true, @@ -326,7 +337,7 @@ fn convert(args: &[String]) -> Result<(), String> { // 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); + let mut params = with_pixel_scale(params, &output, scale, ppi); if let Some(decimals) = decimals { params["decimals"] = json!(decimals); } @@ -372,12 +383,17 @@ 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. +/// The export `params` for `path`, with explicit raster resolution options only for raster formats. /// 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 { +fn with_pixel_scale(mut params: Value, path: &str, scale: Option, ppi: Option) -> Value { if vectorcraft_engine::cmd::fileio::writable_format(None, Some(path)).is_ok_and(|f| f.raster) { - params["scale"] = json!(scale); + if let Some(scale) = scale { + params["scale"] = json!(scale); + } + if let Some(ppi) = ppi { + params["ppi"] = json!(ppi); + } } params } @@ -485,7 +501,7 @@ fn command_step(v: Value) -> Result { } fn run(args: &[String]) -> Result<(), String> { - let valued = ["--in", "-i", "--cmd", "-c", "--params", "-p", "--commands", "--export", "-o", "--scale", "-s"]; + let valued = ["--in", "-i", "--cmd", "-c", "--params", "-p", "--commands", "--export", "-o", "--scale", "-s", "--ppi"]; if help_requested(args, &valued) { return help(); } @@ -494,7 +510,8 @@ fn run(args: &[String]) -> Result<(), String> { // The step of the `--cmd` just given, which a `--params` sets. let mut params_for: Option = None; let mut stdin_read = false; - let mut scale = 1.0; + let mut scale = None::; + let mut ppi = None::; let mut it = args.iter(); while let Some(a) = it.next() { let mut val = |what: &str| it.next().cloned().ok_or_else(|| format!("{a} needs {what}")); @@ -531,7 +548,16 @@ fn run(args: &[String]) -> Result<(), String> { } "--scale" | "-s" => { let raw = val("a number")?; - scale = raw.parse::().map_err(|_| format!("--scale {raw}: not a number"))?; + scale = Some(raw.parse::().map_err(|_| format!("--scale {raw}: not a number"))?); + } + "--ppi" => { + let raw = val("a number")?; + ppi = Some( + raw.parse::() + .ok() + .filter(|n| n.is_finite() && *n > 0.0) + .ok_or_else(|| format!("--ppi {raw}: needs a positive finite number"))?, + ); } other => return Err(format!("unknown run option `{other}`")), } @@ -576,7 +602,7 @@ fn run(args: &[String]) -> Result<(), String> { 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)) + h.call("app.export", with_pixel_scale(json!({"path": path}), &path, scale, ppi)) }; let r = r.map_err(|e| format!("step {n}: export {path}: {e}"))?; emit(json!({"step": "export", "result": r}))?; diff --git a/apps/vectorcraft-cli/src/perf.rs b/apps/vectorcraft-cli/src/perf.rs index 33815fbf3..3ff82512d 100644 --- a/apps/vectorcraft-cli/src/perf.rs +++ b/apps/vectorcraft-cli/src/perf.rs @@ -187,6 +187,48 @@ fn budget_row(name: &str, ms: f64, budget: f64, ok: bool) -> String { format!(" {name:<42} {ms:>8.2} ms {budget:>7.0} ms {}", if ok { "ok" } else { "OVER" }) } +/// Tracked rows for pointing at things in an n-path document: what hovering, clicking, marquee +/// selection and snapping cost when nothing narrows the search. +fn pointing(n: usize, rows: &mut Vec<(String, f64)>) -> Result<(), String> { + use vectorcraft_engine::tools::{Mods, PointerEvent, PointerKind}; + let err = |e: vectorcraft_engine::EngineError| e.to_string(); + let mut s = vectorcraft_engine::Session::new(); + s.add_document(synthetic(n)?, None); + let view = vectorcraft_engine::ViewInfo { zoom: 1.0, ..Default::default() }; + s.select_tool("selection", view).map_err(err)?; + // Off the art (nothing under the pointer), the case every hover over empty canvas pays. + let off = Point::new(-200.0, -200.0); + let hover = |s: &mut vectorcraft_engine::Session| { + let _ = s.pointer(&PointerEvent::new(PointerKind::Move, off.x, off.y), view); + let _ = s.cursor(off, Mods::default(), view); + }; + rows.push((format!("hover over empty canvas ({n} paths)"), median_ms(5, || hover(&mut s)))); + s.execute("select.all", &json!({})).map_err(err)?; + rows.push((format!("hover over empty canvas, all selected ({n} paths)"), median_ms(5, || hover(&mut s)))); + let doc = s.doc().map_err(err)?.doc.clone(); + rows.push((format!("hit test, a miss ({n} paths)"), median_ms(5, || drop(hit::hit_test(&doc, off, hit::HitOptions::default()))))); + let r = Rect::new(600.0, 450.0, 1000.0, 750.0); + rows.push((format!("marquee 400×300 ({n} paths)"), median_ms(5, || drop(hit::marquee(&doc, r, None, false))))); + let view_rect = Rect::new(0.0, 0.0, 1600.0, 1200.0); + rows.push(( + format!("smart guide targets in view ({n} paths)"), + median_ms(5, || drop(vectorcraft_engine::tools::guides::Targets::collect(&doc, &[], Some(view_rect)))), + )); + // Adding one more object to a selection of half the document: the command a shift-click ends + // in, without the pointer handling and hit test before it. + let ids: Vec = doc.layers[0].children().map(|c| c.iter().map(|n| n.id.0).collect()).unwrap_or_default(); + s.execute("select.set", &json!({"ids": ids[..ids.len() / 2]})).map_err(err)?; + let mut k = ids.len() / 2; + rows.push(( + format!("select.toggle in a {}-object selection", ids.len() / 2), + median_ms(5, || { + k += 1; + let _ = s.execute("select.toggle", &json!({"id": ids[k % ids.len()]})); + }), + )); + Ok(()) +} + /// `--deep`: tracked rows (no budget yet) for work the budgets above don't reach: editing a whole /// selection, live effects, every Pathfinder operation, typing in a long story, and memory, with /// the peak so far where `rss_mb` reads one. @@ -282,6 +324,8 @@ fn deep(n: usize) -> Result<(), String> { }), )); note("after typing"); + pointing(n, &mut rows)?; + note("after pointing"); // Four embedded 24 MP photos drawn at fit page by three renderers, as the app's canvas, render // worker and a panel each draw them. { diff --git a/apps/vectorcraft-cli/tests/it/fileio.rs b/apps/vectorcraft-cli/tests/it/fileio.rs index 1423b78b4..c60357b5f 100644 --- a/apps/vectorcraft-cli/tests/it/fileio.rs +++ b/apps/vectorcraft-cli/tests/it/fileio.rs @@ -105,6 +105,38 @@ fn images_open_at_their_pixel_size() { assert_eq!(&std::fs::read(&png).unwrap()[16..24], [0, 0, 0, 6, 0, 0, 0, 4], "IHDR 6×4"); } +#[test] +fn raster_exports_use_document_resolution_unless_overridden() { + let source = tmp("resolution.vectorcraft"); + ok(&["run", "--cmd", "file.new", "--params", r#"{"width":72,"height":36,"rasterEffectsPpi":300}"#, "--export", &source]); + for (flags, width, height) in [(vec![], 300u32, 150u32), (vec!["--scale", "1"], 72, 36), (vec!["--ppi", "150", "--scale", "2"], 150, 75)] { + for mode in ["run", "convert"] { + for format in ["png", "psd"] { + let path = tmp(&format!("resolution-{mode}-{width}.{format}")); + let mut args = if mode == "run" { vec!["run", "--in", &source, "--export", &path] } else { vec!["convert", &source, &path] }; + args.extend_from_slice(&flags); + ok(&args); + let bytes = std::fs::read(&path).unwrap(); + let dimensions = if format == "png" { &bytes[16..24] } else { &bytes[14..22] }; + let expected = + if format == "png" { [width.to_be_bytes(), height.to_be_bytes()] } else { [height.to_be_bytes(), width.to_be_bytes()] }; + assert_eq!(dimensions, expected.concat(), "{args:?}"); + } + } + } +} + +#[test] +fn raster_export_rejects_invalid_ppi() { + for ppi in ["0", "-150", "NaN", "inf", "high"] { + for args in [vec!["run", "--export", "unused.png", "--ppi", ppi], vec!["convert", "unused.svg", "unused.png", "--ppi", ppi]] { + let out = Command::new(BIN).args(&args).output().unwrap(); + assert!(!out.status.success(), "{args:?}"); + assert!(String::from_utf8_lossy(&out.stderr).contains("positive finite number"), "{args:?}: {out:?}"); + } + } +} + #[test] fn help_lists_readable_formats() { let out = ok(&["--help"]); diff --git a/apps/vectorcraft-web/Cargo.toml b/apps/vectorcraft-web/Cargo.toml index 8decb13d8..ff4bcef86 100644 --- a/apps/vectorcraft-web/Cargo.toml +++ b/apps/vectorcraft-web/Cargo.toml @@ -16,6 +16,8 @@ path = "src/main.rs" [dependencies] vectorcraft-engine = { workspace = true, features = ["bundled-fonts"] } vectorcraft-ui-egui = { workspace = true } +# The page command API (storytold/craft-libs#413): the request plumbing behind window.vectorcraft. +craft-web-api = { git = "https://github.com/storytold/craft-libs", rev = "fd5e2feff6ea25bed371a99cc732b3968d621505", version = "0.1.0" } vectorcraft-format = { workspace = true } vectorcraft-text = { workspace = true } egui = { workspace = true } diff --git a/apps/vectorcraft-web/src/api.rs b/apps/vectorcraft-web/src/api.rs index 526e56be1..eddd04f1b 100644 --- a/apps/vectorcraft-web/src/api.rs +++ b/apps/vectorcraft-web/src/api.rs @@ -1,14 +1,18 @@ //! `window.vectorcraft`: the control channel for the page, as promises. //! +//! `request`, `execute`, `commands`, `docInspect`, `uiInspect` and `screenshot` come from +//! `craft-web-api` (storytold/craft-libs#413), the same in every Craft app; VectorCraft adds its own +//! functions on the same object. +//! //! The same JSON methods as the desktop control server (`docs/control-protocol.md`), so an agent //! driving VectorCraft in a browser tab (or an `