Shippori Mincho (assets/fonts, from #145) moves to storytold/craft-fonts. crates/text/build.rs embeds the fonts of a craft-fonts checkout when CRAFT_FONTS_DIR is set (web: BIZ UDPGothic Regular only) as vectorcraft_text::CRAFT_FONTS; unset, it is empty and Japanese falls back to the installed fonts. Document text gets the Japanese faces after the bundled fonts (Mincho first); the UI adds them at the end of every egui family (BIZ UDPGothic first), before ui_fonts' system-font discovery. Release and FreeBSD CI builds check craft-fonts out at a pinned commit; packages carry the fonts' OFL texts. Co-Authored-By: Claude Opus 5.5 <[email protected]>
8.1 KiB
Development
Web build
apps/vectorcraft-web runs the same VectorcraftApp in the browser through eframe's web runner. The renderer is wgpu: WebGPU where the browser has it, WebGL2 otherwise. It is Rust only; the only JavaScript is the glue wasm-bindgen generates.
brew install trunk # or: cargo install trunk --locked
rustup target add wasm32-unknown-unknown
cd apps/vectorcraft-web
trunk build --release # writes ../../dist/web (index.html, .js glue, .wasm)
trunk serve --release # dev server on http://127.0.0.1:8766
Any static file server works for dist/web, for example python3 -m http.server 8766 inside that directory. The release .wasm is about 17.5 MB, or 7.1 MB gzipped, so serve it with compression.
URL flag: ?webgl forces the WebGL2 backend.
How the web shell (apps/vectorcraft-web/src/web.rs) differs from desktop:
- Open sets
Services::open_async, which showsrfd::AsyncFileDialog. The bytes arrive inServices::inbox, which the app drains every frame. - Save / Save As / Export go through
Services::download: a Blob, an object URL and a temporary<a download>, all created from Rust. There is no save dialog, so the suggested name becomes the download name. - Drag-and-drop:
WebShelltakes the frame'sdropped_filesbefore the app sees them, reads each withDroppedFile::bytes_asyncand pushes the bytes into the inbox. (The app's synchronous drop path is compiled out on wasm32.) - No control server: browsers can't listen on TCP. To automate the web build, drive headless Chrome with
--remote-debugging-port. - Quick smoke test:
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --enable-unsafe-webgpu --screenshot=web.png --window-size=1440,900 --virtual-time-budget=15000 http://127.0.0.1:8766/(headless Chrome on macOS gets a real WebGPU adapter).
Fonts: craft-fonts (optional build input)
Font files are never committed to this repository. Fonts shared by the Crafting Apps live in
storytold/craft-fonts; the rules are in craftrules
standards/fonts.md. To add
a font, add it there. (The Latin fonts in assets/fonts/ predate the rule and stay.)
VectorCraft builds, tests and runs without craft-fonts. To embed its Japanese fonts (BIZ UDPGothic
for UI text, Shippori Mincho and BIZ UDMincho for document text), point the CRAFT_FONTS_DIR build
option at a checkout:
git clone https://github.com/storytold/craft-fonts ../craft-fonts
CRAFT_FONTS_DIR=../craft-fonts cargo run --release -p vectorcraft # relative to the workspace root
CRAFT_FONTS_DIR=../craft-fonts cargo xtask ci # also runs the Japanese-glyph tests
crates/text/build.rsreads the checkout'sfonts/manifest.txtand embeds every font it lists asvectorcraft_text::CRAFT_FONTS(empty withoutCRAFT_FONTS_DIR). Nothing is downloaded, and craft-fonts is never aCargo.tomldependency. A bad path is a build warning, or an error withCRAFT_FONTS_REQUIRED=1(release builds).- Web (wasm32) builds embed only BIZ UDPGothic Regular, to keep the
.wasmsmall enough for static hosts. - Document text: the Japanese faces join the font database after the bundled fonts (Mincho first), so they are the fallback for Japanese after the requested font; installed system fonts come after.
- UI:
theme::install_fontsadds them at the end of every egui family (BIZ UDPGothic first), after the UI fonts and before the installed fontsui_fonts.rsdiscovers for other scripts. - Tests that need Japanese glyphs skip with a message when built without craft-fonts. The FreeBSD
CI job and every release job (
release.yml) check craft-fonts out at a pinned commit; release packages carry each embedded font'sOFL-<family>.txt.
Vendor names gate
cargo xtask brands (part of cargo xtask ci) fails when user-visible text names another vendor's products or company: string literals in Rust sources (command labels and params docs, menus, panels, MCP tool definitions), Cargo.toml descriptions and packaging files. Comments and test code are not checked. Say "the reference app" or name the feature itself. A line that must keep an old name, such as an alias that files or preferences from earlier versions still use, carries a brand-ok comment.
Robustness: VectorCraft never crashes
A crash takes the user's unsaved work with it, and much of what the app reads is untrusted: SVG,
PDF, .ai and .vectorcraft files, pasted data, MCP and control-channel messages, command
parameters. Shipped code therefore never panics. Anything that can fail returns Result (or
Option), the caller handles or propagates it, and the user sees an error message.
Enforced by lints
Cargo.toml denies these clippy lints for the whole workspace, and cargo xtask ci runs clippy with
-D warnings:
| Banned in shipped code | Use instead |
|---|---|
x.unwrap(), x.expect("…") |
x?, x.ok_or(err)?, let Some(v) = x else { return … }, if let, unwrap_or / unwrap_or_default / unwrap_or_else |
v.last().unwrap(), v.pop().unwrap() |
if let Some(l) = v.last_mut(), let Some((last, rest)) = v.split_last() else { … } |
panic!, unreachable!() in a match arm |
return an error (EngineError::Other, bad(C, "…")) or a sensible default |
todo!, unimplemented! |
don't merge unfinished paths; return an error that says what isn't supported |
clippy.toml allows unwrap, expect and panic inside #[test] and #[cfg(test)] code.
Integration-test files under tests/ and examples/, and the testkit crate, opt out with
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]. Tests use panic!("…")
instead of unreachable!().
Not caught by lints: avoid implicit panics too
- Indexing and slicing:
v[i]and&s[a..b]panic when out of range. When the index comes from data (a file, a parameter, another document), usev.get(i)/s.get(a..b). Slicing a&stralso panics off a UTF-8 boundary. - Arithmetic: integer division or
%by zero,usizesubtraction that can go below zero (writei + 1 == ninstead ofi == n - 1, or usesaturating_sub), andascasts from non-finite floats. - Unbounded work: cap counts, sizes and allocations read from input. Image dimensions, repeat counts, line breaks and recursion depth all need a limit.
- Encoders and decoders: propagate their errors. Writing an empty file is a silent failure, not a fix.
Don't silence the lint by discarding errors. let _ = … and .ok() are for failures that really
don't matter, with a comment saying why.
The safety net
vectorcraft_engine::guard::catch_panic wraps every entry point:
Session::executerestores the active document, selection and interaction as they were before the top-level command and returnsEngineError::Internal.- Tool events (
Session::pointer,tool_key,tool_text) reset the tool and cancel its drag. - The MCP server answers the request with a JSON-RPC internal error and keeps serving.
VectorcraftApp::logicanduilose one frame and show the error in the status bar; each control-channel request is guarded on its own.
The net exists for bugs, not as a substitute for Result. On wasm a panic aborts the app, so the
web build has no net at all.
Fuzzing
Untrusted input has property tests that must never panic:
crates/engine/tests/import_fuzz.rs: garbage, hostile and mutated SVG and PDF, and swatch (.vcswatches,.gpl), graphic style (.vcstyles) and flattener preset (.vcflattener) libraries, loaded and used, then rendered and exported.crates/engine/tests/command_sweep.rs: every command with junk parameters.crates/format/tests/prop_format.rs: garbage and mutated.vectorcraftfiles.crates/mcp/tests/protocol_props.rs: malformed MCP messages.
CI runs a few dozen cases each. Before touching an importer, run a deeper search, e.g.
PROPTEST_CASES=20000 cargo test --release -p vectorcraft-engine --test import_fuzz. When it finds a
panic, fix the code and add the input as a regular test.