diff --git a/AGENTS.md b/AGENTS.md index f6592c386..b20f2b87e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,8 @@ -# DrawCraft — instructions for agents +# VectorCraft — instructions for agents -DrawCraft 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 `../printcraft` (Acrobat-class); same conventions. +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 `../printcraft` (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 `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` §3 and the relevant `plan/architecture.md` section. Behaviour reference: `plan/illustrator/*.md`. @@ -13,7 +15,7 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ - **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 DrawCraft by a contributor (who licenses it MIT OR Apache-2.0); + - 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. @@ -28,11 +30,11 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, wasm). One task id per commit (`M2.1: pen tool`). ## Running and looking at the app -- `cargo run --release -p drawcraft -- --control 7979 [file.svg|file.drawcraft]` (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). +- `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 `plan/illustrator/02-ui-ux.md` / `10-observed-ui.md`. `ui.screenshot` needs a presented frame: if it errors (screen locked), check the art with `ui.render` / `drawcraft-cli run … --export x.png`, and cover panels with a headless egui frame test (see `panels/transparency.rs` tests). -- **Performance:** `drawcraft-cli perf` checks the budgets (render/pan/zoom, hit test, save/open, SVG, Pathfinder) on a synthetic 50k-path document; `drawcraft-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: `drawcraft-cli mcp` (see `docs/mcp.md`). +- **For UI work, look at the result** (take `ui.screenshot`, read the PNG) and compare with `plan/illustrator/02-ui-ux.md` / `10-observed-ui.md`. `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`). diff --git a/ASSETS.md b/ASSETS.md index 448f9b34f..3e66903da 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -2,7 +2,7 @@ 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):** DrawCraft contains **no Adobe iconography, images, artwork, presets, swatch/brush/symbol/pattern libraries or ICC profiles**. Every asset is either original work by DrawCraft 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. +**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. Generated-in-code art is original and has no file to list. This covers the default swatches, brushes, symbols, patterns, graphic styles, image-trace presets, the app icon (`cargo xtask bundle`) and the vector tool cursors (`crates/ui-egui/src/cursors.rs`). @@ -49,137 +49,137 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/icons/cloud.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/combine.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/copy.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | -| `assets/icons/dc-actions.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-bottom.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-hcenter.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-left.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-right.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-top.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-al-vcenter.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-align.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-alignto-artboard.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-alignto-key.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-alignto-selection.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-anchor.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-appearance.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-arc.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-arrow-down.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-arrow-up.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-artboard-options.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-artboards.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-blend.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-bloat.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-cap-butt.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-cap-round.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-cap-square.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-clear.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-color-guide.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-crystallize.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-cube.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dash-align.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dash-exact.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-direct.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-bottom.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-hcenter.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-hspace.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-left.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-right.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-top.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-vcenter.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-dist-vspace.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-draw-behind.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-draw-inside.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-draw-normal.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-ellipse.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-fill-none.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-flare.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-folder.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-free-transform.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-fx.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-gamut.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-grad-freeform.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-grad-linear.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-grad-radial.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-gradient.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-graphic-styles.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-grid-view.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-group-select.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-join-bevel.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-join-miter.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-join-round.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-join.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-knife.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-line.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-list-view.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-live-bucket.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-live-select.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-mask-none.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-measure.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-mesh.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-new-fill.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-new-item.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-new-stroke.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-options.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-center.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-justify-all.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-justify-center.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-justify-left.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-justify-right.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-left.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-para-right.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-path-eraser.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pathfinder.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pen-add.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pen-delete.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-perspective.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-crop.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-divide.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-exclude.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-intersect.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-merge.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-minus-back.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-minus-front.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-outline.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-trim.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pf-unite.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-place-symbol.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-polar-grid.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-polygon.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-pucker.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-puppet.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-rearrange.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-rect-grid.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-reference-point.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-remove-brush.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-reshape.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-reverse.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-rotate-view.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-rounded-rect.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-scallop.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-screen-mode.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-selection.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-shape-builder.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-shear.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-smooth.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-stroke-center.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-stroke-inside.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-stroke-outside.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-stroke.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-swap.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-swatch-kinds.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-symbol-sprayer.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-touch-type.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-transform-panel.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-transparency.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-twirl.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-type-area.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-type-path.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-type-vertical.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-width-profile.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-width.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-wrinkle.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-zoom-large.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | -| `assets/icons/dc-zoom-small.svg` | DrawCraft contributors | Original work, drawn for DrawCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-actions.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-bottom.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-hcenter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-left.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-right.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-top.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-al-vcenter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-align.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-alignto-artboard.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-alignto-key.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-alignto-selection.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-anchor.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-appearance.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-arc.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-arrow-down.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-arrow-up.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-artboard-options.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-artboards.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-blend.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-bloat.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-cap-butt.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-cap-round.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-cap-square.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-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 | | +| `assets/icons/dc-dash-exact.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-direct.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-bottom.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-hcenter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-hspace.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-left.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-right.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-top.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-vcenter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-dist-vspace.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-draw-behind.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-draw-inside.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-draw-normal.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-ellipse.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-fill-none.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-flare.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-folder.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-free-transform.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-fx.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-gamut.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-grad-freeform.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-grad-linear.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-grad-radial.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-gradient.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-graphic-styles.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-grid-view.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-group-select.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-join-bevel.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-join-miter.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-join-round.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-join.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-knife.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-line.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-list-view.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-live-bucket.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-live-select.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-mask-none.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-measure.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-mesh.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `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-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 | | +| `assets/icons/dc-para-justify-center.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-para-justify-left.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-para-justify-right.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-para-left.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-para-right.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-path-eraser.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pathfinder.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pen-add.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pen-delete.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-perspective.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-crop.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-divide.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-exclude.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-intersect.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-merge.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-minus-back.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-minus-front.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-outline.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-trim.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pf-unite.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-place-symbol.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-polar-grid.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-polygon.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-pucker.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-puppet.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-rearrange.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-rect-grid.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-reference-point.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-remove-brush.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-reshape.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-reverse.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-rotate-view.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-rounded-rect.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-scallop.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-screen-mode.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-selection.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-shape-builder.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-shear.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-smooth.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-stroke-center.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-stroke-inside.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-stroke-outside.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-stroke.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-swap.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-swatch-kinds.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-symbol-sprayer.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-touch-type.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-transform-panel.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-transparency.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-twirl.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-type-area.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-type-path.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-type-vertical.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-width-profile.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-width.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-wrinkle.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-zoom-large.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | +| `assets/icons/dc-zoom-small.svg` | VectorCraft contributors | Original work, drawn for VectorCraft | MIT OR Apache-2.0 | | | `assets/icons/ellipsis.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/eraser.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/eye-off.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | @@ -260,19 +260,19 @@ Generated-in-code art is original and has no file to list. This covers the defau | `assets/icons/waves.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/x.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | | `assets/icons/zoom-in.svg` | Lucide Icons and Contributors | https://lucide.dev (lucide-static v1.49.0; header kept in file) | ISC (`assets/icons/LICENSE-lucide.txt`) | | -| `docs/images/dusk-poster.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/dusk-poster.drawcraft` | MIT OR Apache-2.0 | | -| `examples/dusk-poster.drawcraft` | DrawCraft contributors | Original artwork built through the DrawCraft command API | MIT OR Apache-2.0 | | -| `examples/dusk-poster.svg` | DrawCraft contributors | SVG export of the above | MIT OR Apache-2.0 | | -| `docs/images/shot-1-neon.png` | DrawCraft contributors | Screenshot of DrawCraft itself (DrawCraft/Lucide UI icons only), editing `examples/neon-drive.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/shot-2-ribbons.png` | DrawCraft contributors | Screenshot of DrawCraft itself, editing `examples/ribbons.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/shot-3-sheet.png` | DrawCraft contributors | Screenshot of DrawCraft itself, editing `examples/feature-sheet.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/shot-4-bezier.png` | DrawCraft contributors | Screenshot of DrawCraft itself, editing `examples/feature-sheet.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/art-neon-drive.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/neon-drive.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/art-ribbons.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/ribbons.drawcraft` | MIT OR Apache-2.0 | | -| `docs/images/art-pathfinder.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/feature-sheet.drawcraft` (artboard 1) | MIT OR Apache-2.0 | | -| `docs/images/art-mesh.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/feature-sheet.drawcraft` (artboard 2) | MIT OR Apache-2.0 | | -| `docs/images/art-repeat.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/feature-sheet.drawcraft` (artboard 3) | MIT OR Apache-2.0 | | -| `docs/images/art-envelope.png` | DrawCraft contributors | Rendered by DrawCraft from `examples/feature-sheet.drawcraft` (artboard 4) | MIT OR Apache-2.0 | | -| `examples/neon-drive.drawcraft` | DrawCraft contributors | Original artwork built through the DrawCraft command API | MIT OR Apache-2.0 | | -| `examples/ribbons.drawcraft` | DrawCraft contributors | Original artwork built through the DrawCraft command API | MIT OR Apache-2.0 | | -| `examples/feature-sheet.drawcraft` | DrawCraft contributors | Original artwork built through the DrawCraft command API | MIT OR Apache-2.0 | | +| `docs/images/dusk-poster.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/dusk-poster.vectorcraft` | MIT OR Apache-2.0 | | +| `examples/dusk-poster.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | +| `examples/dusk-poster.svg` | VectorCraft contributors | SVG export of the above | MIT OR Apache-2.0 | | +| `docs/images/shot-1-neon.png` | VectorCraft contributors | Screenshot of VectorCraft itself (VectorCraft/Lucide UI icons only), editing `examples/neon-drive.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/shot-2-ribbons.png` | VectorCraft contributors | Screenshot of VectorCraft itself, editing `examples/ribbons.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/shot-3-sheet.png` | VectorCraft contributors | Screenshot of VectorCraft itself, editing `examples/feature-sheet.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/shot-4-bezier.png` | VectorCraft contributors | Screenshot of VectorCraft itself, editing `examples/feature-sheet.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/art-neon-drive.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/neon-drive.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/art-ribbons.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/ribbons.vectorcraft` | MIT OR Apache-2.0 | | +| `docs/images/art-pathfinder.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/feature-sheet.vectorcraft` (artboard 1) | MIT OR Apache-2.0 | | +| `docs/images/art-mesh.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/feature-sheet.vectorcraft` (artboard 2) | MIT OR Apache-2.0 | | +| `docs/images/art-repeat.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/feature-sheet.vectorcraft` (artboard 3) | MIT OR Apache-2.0 | | +| `docs/images/art-envelope.png` | VectorCraft contributors | Rendered by VectorCraft from `examples/feature-sheet.vectorcraft` (artboard 4) | MIT OR Apache-2.0 | | +| `examples/neon-drive.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | +| `examples/ribbons.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | +| `examples/feature-sheet.vectorcraft` | VectorCraft contributors | Original artwork built through the VectorCraft command API | MIT OR Apache-2.0 | | diff --git a/CLAUDE.md b/CLAUDE.md index f6592c386..b20f2b87e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,8 @@ -# DrawCraft — instructions for agents +# VectorCraft — instructions for agents -DrawCraft 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 `../printcraft` (Acrobat-class); same conventions. +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 `../printcraft` (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 `plan/STATUS.md` (current milestone, next task), then the task in `plan/execution-plan.md` §3 and the relevant `plan/architecture.md` section. Behaviour reference: `plan/illustrator/*.md`. @@ -13,7 +15,7 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ - **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 DrawCraft by a contributor (who licenses it MIT OR Apache-2.0); + - 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. @@ -28,11 +30,11 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ - **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests, layers, wasm). One task id per commit (`M2.1: pen tool`). ## Running and looking at the app -- `cargo run --release -p drawcraft -- --control 7979 [file.svg|file.drawcraft]` (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). +- `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 `plan/illustrator/02-ui-ux.md` / `10-observed-ui.md`. `ui.screenshot` needs a presented frame: if it errors (screen locked), check the art with `ui.render` / `drawcraft-cli run … --export x.png`, and cover panels with a headless egui frame test (see `panels/transparency.rs` tests). -- **Performance:** `drawcraft-cli perf` checks the budgets (render/pan/zoom, hit test, save/open, SVG, Pathfinder) on a synthetic 50k-path document; `drawcraft-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: `drawcraft-cli mcp` (see `docs/mcp.md`). +- **For UI work, look at the result** (take `ui.screenshot`, read the PNG) and compare with `plan/illustrator/02-ui-ux.md` / `10-observed-ui.md`. `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`). diff --git a/Cargo.lock b/Cargo.lock index d9ba616bb..00fb280e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -823,311 +823,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "drawcraft" -version = "0.1.0" -dependencies = [ - "arboard", - "drawcraft-engine", - "drawcraft-ui-egui", - "eframe", - "egui", - "log", - "muda", - "rfd", - "serde_json", -] - -[[package]] -name = "drawcraft-brush" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-geom", - "kurbo 0.13.1", - "serde", - "serde_json", -] - -[[package]] -name = "drawcraft-cli" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-engine", - "drawcraft-format", - "drawcraft-geom", - "drawcraft-mcp", - "drawcraft-render", - "drawcraft-svg", - "drawcraft-testkit", - "serde_json", -] - -[[package]] -name = "drawcraft-color" -version = "0.1.0" -dependencies = [ - "kurbo 0.13.1", - "moxcms", - "proptest", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-doc" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-geom", - "kurbo 0.13.1", - "proptest", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-effects" -version = "0.1.0" -dependencies = [ - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-pathops", - "kurbo 0.13.1", - "serde_json", -] - -[[package]] -name = "drawcraft-engine" -version = "0.1.0" -dependencies = [ - "drawcraft-brush", - "drawcraft-color", - "drawcraft-doc", - "drawcraft-format", - "drawcraft-geom", - "drawcraft-pathops", - "drawcraft-pdf", - "drawcraft-render", - "drawcraft-svg", - "drawcraft-testkit", - "drawcraft-text", - "drawcraft-tools", - "drawcraft-trace", - "kurbo 0.13.1", - "log", - "proptest", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-format" -version = "0.1.0" -dependencies = [ - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-testkit", - "proptest", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-geom" -version = "0.1.0" -dependencies = [ - "drawcraft-testkit", - "kurbo 0.13.1", - "proptest", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-mcp" -version = "0.1.0" -dependencies = [ - "drawcraft-doc", - "drawcraft-engine", - "drawcraft-format", - "drawcraft-geom", - "drawcraft-render", - "drawcraft-svg", - "drawcraft-testkit", - "drawcraft-tools", - "log", - "proptest", - "serde_json", -] - -[[package]] -name = "drawcraft-pathops" -version = "0.1.0" -dependencies = [ - "drawcraft-geom", - "drawcraft-testkit", - "kurbo 0.13.1", - "linesweeper", - "proptest", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-pdf" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-text", - "hayro-interpret", - "hayro-syntax", - "image", - "krilla", - "kurbo 0.13.1", - "log", - "smallvec", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-render" -version = "0.1.0" -dependencies = [ - "drawcraft-brush", - "drawcraft-color", - "drawcraft-doc", - "drawcraft-effects", - "drawcraft-geom", - "drawcraft-testkit", - "drawcraft-text", - "image", - "log", - "proptest", - "serde_json", - "vello_common 0.2.0", - "vello_cpu 0.2.0", -] - -[[package]] -name = "drawcraft-svg" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-testkit", - "image", - "kurbo 0.13.1", - "proptest", - "svgtypes 0.16.1", - "thiserror 2.0.21", - "usvg 0.48.1", -] - -[[package]] -name = "drawcraft-testkit" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-engine", - "drawcraft-format", - "drawcraft-geom", - "drawcraft-render", - "drawcraft-svg", - "image", - "kurbo 0.13.1", - "proptest", - "serde_json", -] - -[[package]] -name = "drawcraft-text" -version = "0.1.0" -dependencies = [ - "drawcraft-doc", - "drawcraft-geom", - "harfrust 0.13.3", - "kurbo 0.13.1", - "log", - "skrifa 0.47.0", -] - -[[package]] -name = "drawcraft-tools" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-pathops", - "drawcraft-text", - "kurbo 0.13.1", - "serde", - "serde_json", -] - -[[package]] -name = "drawcraft-trace" -version = "0.1.0" -dependencies = [ - "drawcraft-geom", - "drawcraft-pathops", - "image", - "kurbo 0.13.1", - "serde", - "serde_json", - "thiserror 2.0.21", -] - -[[package]] -name = "drawcraft-ui-egui" -version = "0.1.0" -dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-effects", - "drawcraft-engine", - "drawcraft-format", - "drawcraft-geom", - "drawcraft-pdf", - "drawcraft-render", - "drawcraft-svg", - "drawcraft-text", - "drawcraft-tools", - "egui", - "egui_extras", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "drawcraft-web" -version = "0.1.0" -dependencies = [ - "drawcraft-engine", - "drawcraft-ui-egui", - "eframe", - "egui", - "js-sys", - "log", - "rfd", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "ecolor" version = "0.36.2" @@ -4150,6 +3845,311 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vectorcraft" +version = "0.1.0" +dependencies = [ + "arboard", + "eframe", + "egui", + "log", + "muda", + "rfd", + "serde_json", + "vectorcraft-engine", + "vectorcraft-ui-egui", +] + +[[package]] +name = "vectorcraft-brush" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "serde", + "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-geom", +] + +[[package]] +name = "vectorcraft-cli" +version = "0.1.0" +dependencies = [ + "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-engine", + "vectorcraft-format", + "vectorcraft-geom", + "vectorcraft-mcp", + "vectorcraft-render", + "vectorcraft-svg", + "vectorcraft-testkit", +] + +[[package]] +name = "vectorcraft-color" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "moxcms", + "proptest", + "serde", + "serde_json", + "thiserror 2.0.21", +] + +[[package]] +name = "vectorcraft-doc" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "proptest", + "serde", + "serde_json", + "thiserror 2.0.21", + "vectorcraft-color", + "vectorcraft-geom", +] + +[[package]] +name = "vectorcraft-effects" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "serde_json", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-pathops", +] + +[[package]] +name = "vectorcraft-engine" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "log", + "proptest", + "serde", + "serde_json", + "thiserror 2.0.21", + "vectorcraft-brush", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-format", + "vectorcraft-geom", + "vectorcraft-pathops", + "vectorcraft-pdf", + "vectorcraft-render", + "vectorcraft-svg", + "vectorcraft-testkit", + "vectorcraft-text", + "vectorcraft-tools", + "vectorcraft-trace", +] + +[[package]] +name = "vectorcraft-format" +version = "0.1.0" +dependencies = [ + "proptest", + "serde", + "serde_json", + "thiserror 2.0.21", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-testkit", +] + +[[package]] +name = "vectorcraft-geom" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "proptest", + "serde", + "serde_json", + "thiserror 2.0.21", + "vectorcraft-testkit", +] + +[[package]] +name = "vectorcraft-mcp" +version = "0.1.0" +dependencies = [ + "log", + "proptest", + "serde_json", + "vectorcraft-doc", + "vectorcraft-engine", + "vectorcraft-format", + "vectorcraft-geom", + "vectorcraft-render", + "vectorcraft-svg", + "vectorcraft-testkit", + "vectorcraft-tools", +] + +[[package]] +name = "vectorcraft-pathops" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "linesweeper", + "proptest", + "thiserror 2.0.21", + "vectorcraft-geom", + "vectorcraft-testkit", +] + +[[package]] +name = "vectorcraft-pdf" +version = "0.1.0" +dependencies = [ + "hayro-interpret", + "hayro-syntax", + "image", + "krilla", + "kurbo 0.13.1", + "log", + "smallvec", + "thiserror 2.0.21", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-text", +] + +[[package]] +name = "vectorcraft-render" +version = "0.1.0" +dependencies = [ + "image", + "log", + "proptest", + "serde_json", + "vectorcraft-brush", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-effects", + "vectorcraft-geom", + "vectorcraft-testkit", + "vectorcraft-text", + "vello_common 0.2.0", + "vello_cpu 0.2.0", +] + +[[package]] +name = "vectorcraft-svg" +version = "0.1.0" +dependencies = [ + "image", + "kurbo 0.13.1", + "proptest", + "svgtypes 0.16.1", + "thiserror 2.0.21", + "usvg 0.48.1", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-testkit", +] + +[[package]] +name = "vectorcraft-testkit" +version = "0.1.0" +dependencies = [ + "image", + "kurbo 0.13.1", + "proptest", + "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-engine", + "vectorcraft-format", + "vectorcraft-geom", + "vectorcraft-render", + "vectorcraft-svg", +] + +[[package]] +name = "vectorcraft-text" +version = "0.1.0" +dependencies = [ + "harfrust 0.13.3", + "kurbo 0.13.1", + "log", + "skrifa 0.47.0", + "vectorcraft-doc", + "vectorcraft-geom", +] + +[[package]] +name = "vectorcraft-tools" +version = "0.1.0" +dependencies = [ + "kurbo 0.13.1", + "serde", + "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-pathops", + "vectorcraft-text", +] + +[[package]] +name = "vectorcraft-trace" +version = "0.1.0" +dependencies = [ + "image", + "kurbo 0.13.1", + "serde", + "serde_json", + "thiserror 2.0.21", + "vectorcraft-geom", + "vectorcraft-pathops", +] + +[[package]] +name = "vectorcraft-ui-egui" +version = "0.1.0" +dependencies = [ + "egui", + "egui_extras", + "log", + "serde", + "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-effects", + "vectorcraft-engine", + "vectorcraft-format", + "vectorcraft-geom", + "vectorcraft-pdf", + "vectorcraft-render", + "vectorcraft-svg", + "vectorcraft-text", + "vectorcraft-tools", +] + +[[package]] +name = "vectorcraft-web" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "js-sys", + "log", + "rfd", + "vectorcraft-engine", + "vectorcraft-ui-egui", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "vello_common" version = "0.1.0" @@ -5086,11 +5086,11 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee" name = "xtask" version = "0.1.0" dependencies = [ - "drawcraft-color", - "drawcraft-doc", - "drawcraft-geom", - "drawcraft-render", "serde_json", + "vectorcraft-color", + "vectorcraft-doc", + "vectorcraft-geom", + "vectorcraft-render", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7ed99e469..b61689453 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,34 +1,34 @@ [workspace] resolver = "3" members = ["crates/*", "apps/*", "xtask"] -default-members = ["crates/*", "apps/drawcraft", "apps/drawcraft-cli", "xtask"] +default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"] [workspace.package] version = "0.1.0" edition = "2024" license = "MIT OR Apache-2.0" rust-version = "1.90" -repository = "https://github.com/storytold/drawcraft" +repository = "https://github.com/storytold/vectorcraft" [workspace.dependencies] # internal -drawcraft-geom = { path = "crates/geom" } -drawcraft-color = { path = "crates/color" } -drawcraft-doc = { path = "crates/doc" } -drawcraft-pathops = { path = "crates/pathops" } -drawcraft-brush = { path = "crates/brush" } -drawcraft-trace = { path = "crates/trace" } -drawcraft-text = { path = "crates/text" } -drawcraft-effects = { path = "crates/effects" } -drawcraft-render = { path = "crates/render" } -drawcraft-svg = { path = "crates/svg" } -drawcraft-pdf = { path = "crates/pdf" } -drawcraft-format = { path = "crates/format" } -drawcraft-tools = { path = "crates/tools" } -drawcraft-engine = { path = "crates/engine" } -drawcraft-ui-egui = { path = "crates/ui-egui" } -drawcraft-mcp = { path = "crates/mcp" } -drawcraft-testkit = { path = "crates/testkit" } +vectorcraft-geom = { path = "crates/geom" } +vectorcraft-color = { path = "crates/color" } +vectorcraft-doc = { path = "crates/doc" } +vectorcraft-pathops = { path = "crates/pathops" } +vectorcraft-brush = { path = "crates/brush" } +vectorcraft-trace = { path = "crates/trace" } +vectorcraft-text = { path = "crates/text" } +vectorcraft-effects = { path = "crates/effects" } +vectorcraft-render = { path = "crates/render" } +vectorcraft-svg = { path = "crates/svg" } +vectorcraft-pdf = { path = "crates/pdf" } +vectorcraft-format = { path = "crates/format" } +vectorcraft-tools = { path = "crates/tools" } +vectorcraft-engine = { path = "crates/engine" } +vectorcraft-ui-egui = { path = "crates/ui-egui" } +vectorcraft-mcp = { path = "crates/mcp" } +vectorcraft-testkit = { path = "crates/testkit" } # foundation kurbo = { version = "0.13", features = ["serde"] } diff --git a/NOTICE b/NOTICE index 875266646..ca8586499 100644 --- a/NOTICE +++ b/NOTICE @@ -1,11 +1,11 @@ -DrawCraft -Copyright the DrawCraft contributors. Licensed under MIT OR Apache-2.0. +VectorCraft +Copyright the VectorCraft contributors. Licensed under MIT OR Apache-2.0. This product bundles third-party material under the following licences. Per-file attribution (author, source, licence) for every asset is in ASSETS.md. No Adobe icons, images or artwork are included. - Lucide icons (assets/icons/*.svg except dc-*.svg) — ISC License. See assets/icons/LICENSE-lucide.txt. -- DrawCraft icons (assets/icons/dc-*.svg) — original work, MIT OR Apache-2.0. +- VectorCraft icons (assets/icons/dc-*.svg) — original work, MIT OR Apache-2.0. - Source Sans 3 and Source Serif 4 (assets/fonts/Source*) — SIL Open Font License 1.1. See assets/fonts/OFL-SourceSans3.txt, OFL-SourceSerif4.txt. - Inter (assets/fonts/Inter-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-Inter.txt. - JetBrains Mono (assets/fonts/JetBrainsMono-*.ttf) — SIL Open Font License 1.1. See assets/fonts/OFL-JetBrainsMono.txt. diff --git a/ROADMAP.md b/ROADMAP.md index e15b33457..67e3f576f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ -# DrawCraft Roadmap +# VectorCraft Roadmap -DrawCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux and the web (WASM), and agents can drive it fully over a JSON control channel and MCP. +VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux and the web (WASM), and agents can drive it fully over a JSON control channel and MCP. This file tracks **how far we are and what's left**. Estimates are wall-clock hours of continuous agent work (Claude Opus 5.5 lead plus 4–6 parallel agents). They were made by counting the remaining work, not measured, and are updated as work lands. @@ -22,7 +22,7 @@ _Last updated: 2026-10-01._ - Actions panel (record/playback, persisted), generic parameter dialogs for every "…" command. - JSON-lines control channel with real egui pointer and keyboard injection. - MCP server with 19 tools, which can attach to the running app or run headless. - - Headless CLI (`drawcraft-cli`). + - Headless CLI (`vectorcraft-cli`). - Actions panel that records and plays back commands. - **UI:** Illustrator 2026 layout restyled to measured values: - Medium Dark theme, categorized and Advanced toolbars, 35 pt document tabs, 33 pt panel tabs. @@ -45,7 +45,7 @@ _Last updated: 2026-10-01._ - **Advanced art:** live Blends (steps/distance/smooth colour, spine), Envelope Distort (warp/mesh/top object), Gradient Mesh, Shape Builder, Live Paint, Image Trace (12 presets), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). - **Colour, type and file workflows:** Recolor Artwork (dialog with harmonies), Edit Colors, Find & Replace, Change Case, Smart Punctuation, Guides, Lock/Hide Above, Transform Each, Rasterize. - **Formats:** - - `.drawcraft` (lossless JSON), SVG import/export, PDF export/import (including PDF-compatible `.ai`). + - `.vectorcraft` (lossless JSON), SVG import/export, PDF export/import (including PDF-compatible `.ai`). - PNG, JPEG and WebP export, Export for Screens, Place. - **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. - **Tests:** ~700 automated tests: model-based property tests, a junk-parameter sweep over every command, golden renders, and MCP end-to-end tests over stdio. @@ -59,7 +59,7 @@ _Last updated: 2026-10-01._ | M2 | Drawing tools + smart guides | ✅ mostly done (Shaper, Pen modifier nuances) | 5–10 | | M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | 🟡 panels done; opacity masks done (make/release, clip, invert, disable, link; render + SVG `` in/out + PDF soft mask); mask-editing mode, freeform gradients pending | 6–10 | | M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | 🟡 Export for Screens (PNG/JPG/WebP/SVG/PDF × scales) done; headless CLI/MCP export every format; system clipboard: copy puts SVG markup on it, paste takes SVG from other apps (Ctrl/Cmd+C/X/V now also work off macOS); PNG/PDF clipboard flavours, EPS/DXF pending | 8–12 | -| M5 | Performance | 🟡 background render + caches + MT done; `drawcraft-cli bench` and `drawcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 10–20 | +| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 10–20 | | M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (Shape Builder edge erase, large-offset bug open) | 3–6 | | M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | 🟡 Character/Paragraph Styles, Area Type Options, threaded text, Fit Headline, Glyphs done; OpenType/Tabs panels, text wrap, Find Font, spell check pending | 12–20 | | M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends, Envelopes (warp/mesh/top object), Width tool, Liquify tools, Puppet Warp and Perspective Grid landed; fidelity pass pending | 8–12 | diff --git a/apps/drawcraft-cli/Cargo.toml b/apps/drawcraft-cli/Cargo.toml deleted file mode 100644 index b2908fa98..000000000 --- a/apps/drawcraft-cli/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "drawcraft-cli" -version.workspace = true -edition.workspace = true -license.workspace = true -publish = false -description = "DrawCraft command line: MCP server (stdio) and headless batch runs" - -[[bin]] -name = "drawcraft-cli" -path = "src/main.rs" - -[lints] -workspace = true - -[dependencies] -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-engine = { workspace = true } -drawcraft-format = { workspace = true } -drawcraft-geom = { workspace = true } -drawcraft-svg = { workspace = true } -drawcraft-mcp = { workspace = true } -drawcraft-render = { workspace = true } -serde_json = { workspace = true } - -[dev-dependencies] -drawcraft-testkit = { workspace = true } -serde_json = { workspace = true } diff --git a/apps/drawcraft-web/index.html b/apps/drawcraft-web/index.html deleted file mode 100644 index beb2938ab..000000000 --- a/apps/drawcraft-web/index.html +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - DrawCraft - - - - - -
Loading DrawCraft…
- - diff --git a/apps/vectorcraft-cli/Cargo.toml b/apps/vectorcraft-cli/Cargo.toml new file mode 100644 index 000000000..0fcd8674e --- /dev/null +++ b/apps/vectorcraft-cli/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "vectorcraft-cli" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false +description = "VectorCraft command line: MCP server (stdio) and headless batch runs" + +[[bin]] +name = "vectorcraft-cli" +path = "src/main.rs" + +[lints] +workspace = true + +[dependencies] +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-format = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-svg = { workspace = true } +vectorcraft-mcp = { workspace = true } +vectorcraft-render = { workspace = true } +serde_json = { workspace = true } + +[dev-dependencies] +vectorcraft-testkit = { workspace = true } +serde_json = { workspace = true } diff --git a/apps/drawcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs similarity index 79% rename from apps/drawcraft-cli/src/main.rs rename to apps/vectorcraft-cli/src/main.rs index 2deab0b7c..4a6a3c525 100644 --- a/apps/drawcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -1,11 +1,11 @@ -//! `drawcraft-cli`: DrawCraft from the command line. +//! `vectorcraft-cli`: VectorCraft from the command line. //! //! ```text -//! drawcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] -//! drawcraft-cli run [--in file.drawcraft|file.svg] [--cmd id [--params '{json}']]... [--export out.svg|.png|.pdf|.jpg|.webp|.drawcraft]... [--scale 2] -//! drawcraft-cli commands -//! drawcraft-cli bench FILE [--size 2880x1800] [--iters 5] -//! drawcraft-cli perf [--paths 50000] +//! vectorcraft-cli mcp [--connect 127.0.0.1:7979 | --headless] +//! vectorcraft-cli run [--in file.vectorcraft|file.svg] [--cmd id [--params '{json}']]... [--export out.svg|.png|.pdf|.jpg|.webp|.vectorcraft]... [--scale 2] +//! vectorcraft-cli commands +//! vectorcraft-cli bench FILE [--size 2880x1800] [--iters 5] +//! vectorcraft-cli perf [--paths 50000] //! ``` #![forbid(unsafe_code)] @@ -14,29 +14,29 @@ use std::process::ExitCode; mod perf; -use drawcraft_mcp::{Backend, DEFAULT_ADDR, Headless, Remote, Server}; use serde_json::{Value, json}; +use vectorcraft_mcp::{Backend, DEFAULT_ADDR, Headless, Remote, Server}; const USAGE: &str = "\ -drawcraft-cli — DrawCraft automation +vectorcraft-cli — VectorCraft automation USAGE: - drawcraft-cli mcp [--connect ADDR | --headless] + vectorcraft-cli mcp [--connect ADDR | --headless] Run the MCP server on stdio. Default: connect to a running app at 127.0.0.1:7979 - (drawcraft --control 7979), falling back to a headless in-process session. + (vectorcraft --control 7979), falling back to a headless in-process session. - drawcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N] - Headless batch: open FILE (.drawcraft/.svg) or start a new document, run commands in - order, export (.svg, .png, .pdf, .jpg, .webp, .drawcraft by extension). Prints one JSON result per step. + vectorcraft-cli run [--in FILE] [--cmd ID [--params JSON]]... [--export FILE]... [--scale N] + Headless batch: open FILE (.vectorcraft/.svg) or start a new document, run commands in + order, export (.svg, .png, .pdf, .jpg, .webp, .vectorcraft by extension). Prints one JSON result per step. - drawcraft-cli commands + vectorcraft-cli commands Print the command catalogue as JSON. - drawcraft-cli bench FILE [--size WxH] [--iters N] - Render FILE (.drawcraft/.svg) fitted to WxH (default 2880x1800) and print ms per frame + vectorcraft-cli bench FILE [--size WxH] [--iters N] + Render FILE (.vectorcraft/.svg) fitted to WxH (default 2880x1800) and print ms per frame (warm), multithreaded and single-threaded. - drawcraft-cli perf [--paths N] + vectorcraft-cli perf [--paths N] Check the performance budgets (render, pan, hit test, save/load, SVG, Pathfinder) on a synthetic N-path document (default 50000). Exits non-zero if a budget is exceeded. "; @@ -54,7 +54,7 @@ fn main() -> ExitCode { Ok(()) } Some("-V" | "--version") => { - println!("drawcraft-cli {}", env!("CARGO_PKG_VERSION")); + println!("vectorcraft-cli {}", env!("CARGO_PKG_VERSION")); Ok(()) } Some(other) => Err(format!("unknown subcommand `{other}`\n\n{USAGE}")), @@ -62,7 +62,7 @@ fn main() -> ExitCode { match r { Ok(()) => ExitCode::SUCCESS, Err(e) => { - eprintln!("drawcraft-cli: {e}"); + eprintln!("vectorcraft-cli: {e}"); ExitCode::FAILURE } } @@ -93,7 +93,7 @@ fn mcp(args: &[String]) -> Result<(), String> { Err(_) => Box::new(Headless::with_document()), } }; - eprintln!("drawcraft-cli: MCP server on stdio ({})", backend.describe()); + eprintln!("vectorcraft-cli: MCP server on stdio ({})", backend.describe()); let stdin = std::io::stdin(); let stdout = std::io::stdout(); Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string()) @@ -188,13 +188,13 @@ fn bench(args: &[String]) -> Result<(), String> { let doc = hl.session.doc().map_err(|e| e.to_string())?.doc.clone(); let b = doc.artboards.first().map(|a| a.rect).ok_or("document has no artboard")?; let z = (w as f64 / b.width()).min(h as f64 / b.height()) * 0.95; - let view = drawcraft_geom::Affine::translate((w as f64 / 2.0, h as f64 / 2.0)) - * drawcraft_geom::Affine::scale(z) - * drawcraft_geom::Affine::translate(-b.center().to_vec2()); - let opts = drawcraft_render::RenderOptions::default(); + let view = vectorcraft_geom::Affine::translate((w as f64 / 2.0, h as f64 / 2.0)) + * vectorcraft_geom::Affine::scale(z) + * vectorcraft_geom::Affine::translate(-b.center().to_vec2()); + let opts = vectorcraft_render::RenderOptions::default(); println!("{file}: {} nodes, {w}x{h}", doc.layers.iter().map(|l| l.count()).sum::()); - for threads in [drawcraft_render::default_threads(), 0] { - let mut r = drawcraft_render::Renderer::new(); + for threads in [vectorcraft_render::default_threads(), 0] { + let mut r = vectorcraft_render::Renderer::new(); r.threads = threads; r.render(&doc, w, h, view, &opts); let t = std::time::Instant::now(); diff --git a/apps/drawcraft-cli/src/perf.rs b/apps/vectorcraft-cli/src/perf.rs similarity index 86% rename from apps/drawcraft-cli/src/perf.rs rename to apps/vectorcraft-cli/src/perf.rs index 1d29db5a3..3ab22a119 100644 --- a/apps/drawcraft-cli/src/perf.rs +++ b/apps/vectorcraft-cli/src/perf.rs @@ -1,15 +1,15 @@ -//! `drawcraft-cli perf`: the performance budgets (plan §4) measured on a synthetic document. +//! `vectorcraft-cli perf`: the performance budgets (plan §4) measured on a synthetic document. //! //! Each row is the median of several runs. Timings are wall-clock: on a busy machine (load //! average above the core count) they are noise, and the report says so. use std::time::Instant; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, Document, Node, hit}; -use drawcraft_geom::{Affine, Point, Rect, shapes}; -use drawcraft_render::{RenderOptions, Renderer}; use serde_json::json; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, Node, hit}; +use vectorcraft_geom::{Affine, Point, Rect, shapes}; +use vectorcraft_render::{RenderOptions, Renderer}; /// Deterministic xorshift in 0..1. struct Rng(u64); @@ -81,7 +81,7 @@ pub fn run(args: &[String]) -> Result<(), String> { } let cores = std::thread::available_parallelism().map_or(1, |c| c.get()); let load = load_average(); - println!("DrawCraft performance budgets — {n} paths, {cores} cores, load average {}", load.map_or("?".into(), |l| format!("{l:.1}"))); + println!("VectorCraft performance budgets — {n} paths, {cores} cores, load average {}", load.map_or("?".into(), |l| format!("{l:.1}"))); let noisy = load.is_some_and(|l| l > cores as f64 * 0.75); if noisy { println!("WARNING: the machine is busy; wall-clock timings below are not trustworthy."); @@ -122,14 +122,14 @@ pub fn run(args: &[String]) -> Result<(), String> { rows.push(("hit test (per click)", hits, 2.0)); let mut bytes = vec![]; - rows.push(("save .drawcraft", median_ms(3, || bytes = drawcraft_format::save_file(&doc)), 300.0)); - rows.push(("open .drawcraft", median_ms(3, || drop(drawcraft_format::load(&bytes).expect("load"))), 300.0)); - let svg_opts = drawcraft_svg::ExportOptions::default(); - rows.push(("export SVG", median_ms(3, || drop(drawcraft_svg::export(&doc, &svg_opts))), 500.0)); + rows.push(("save .vectorcraft", median_ms(3, || bytes = vectorcraft_format::save_file(&doc)), 300.0)); + rows.push(("open .vectorcraft", median_ms(3, || drop(vectorcraft_format::load(&bytes).expect("load"))), 300.0)); + let svg_opts = vectorcraft_svg::ExportOptions::default(); + rows.push(("export SVG", median_ms(3, || drop(vectorcraft_svg::export(&doc, &svg_opts))), 500.0)); // Pathfinder Unite on 1,000 overlapping paths. let unite = median_ms(3, || { - let mut s = drawcraft_engine::Session::new(); + let mut s = vectorcraft_engine::Session::new(); s.add_document(synthetic(1000), None); s.execute("select.all", &json!({})).expect("select"); s.execute("object.pathfinder.unite", &json!({})).expect("unite"); diff --git a/apps/drawcraft-cli/tests/cli.rs b/apps/vectorcraft-cli/tests/cli.rs similarity index 93% rename from apps/drawcraft-cli/tests/cli.rs rename to apps/vectorcraft-cli/tests/cli.rs index 7d8d32a46..0bd7cafd7 100644 --- a/apps/drawcraft-cli/tests/cli.rs +++ b/apps/vectorcraft-cli/tests/cli.rs @@ -3,10 +3,10 @@ use std::process::{Command, Stdio}; use serde_json::{Value, json}; -const BIN: &str = env!("CARGO_BIN_EXE_drawcraft-cli"); +const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); fn tmp(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("drawcraft-cli-test-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("vectorcraft-cli-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); dir.join(name) } @@ -24,7 +24,7 @@ fn commands_prints_catalogue() { fn run_batch_exports() { let svg = tmp("batch.svg"); let png = tmp("batch.png"); - let dc = tmp("batch.drawcraft"); + let dc = tmp("batch.vectorcraft"); let out = Command::new(BIN) .args(["run", "--cmd", "file.new", "--params", r#"{"width":200,"height":100}"#]) .args(["--cmd", "shape.ellipse", "--params", r#"{"x":10,"y":10,"width":80,"height":60}"#]) @@ -76,7 +76,7 @@ fn mcp_headless_over_stdio() { assert!(out.status.success()); let replies: Vec = String::from_utf8(out.stdout).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect(); assert_eq!(replies.iter().map(|r| r["id"].as_u64().unwrap()).collect::>(), [1, 2, 3, 4]); - assert_eq!(replies[0]["result"]["serverInfo"]["name"], "drawcraft"); + assert_eq!(replies[0]["result"]["serverInfo"]["name"], "vectorcraft"); assert_eq!(replies[2]["result"]["isError"], false); assert_eq!(replies[3]["result"]["content"][0]["type"], "image"); } diff --git a/apps/drawcraft-cli/tests/mcp_e2e.rs b/apps/vectorcraft-cli/tests/mcp_e2e.rs similarity index 87% rename from apps/drawcraft-cli/tests/mcp_e2e.rs rename to apps/vectorcraft-cli/tests/mcp_e2e.rs index 54bbc97b5..5a353070a 100644 --- a/apps/drawcraft-cli/tests/mcp_e2e.rs +++ b/apps/vectorcraft-cli/tests/mcp_e2e.rs @@ -1,12 +1,12 @@ -//! End-to-end MCP over stdio against the built `drawcraft-cli mcp --headless` binary. +//! End-to-end MCP over stdio against the built `vectorcraft-cli mcp --headless` binary. use std::io::{BufRead, BufReader, Write}; use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; -use drawcraft_testkit::raster::Image; use serde_json::{Value, json}; +use vectorcraft_testkit::raster::Image; -const BIN: &str = env!("CARGO_BIN_EXE_drawcraft-cli"); +const BIN: &str = env!("CARGO_BIN_EXE_vectorcraft-cli"); struct Mcp { child: Child, @@ -23,7 +23,7 @@ impl Mcp { .stdout(Stdio::piped()) .stderr(Stdio::null()) .spawn() - .expect("spawn drawcraft-cli"); + .expect("spawn vectorcraft-cli"); let stdin = child.stdin.take(); let stdout = BufReader::new(child.stdout.take().unwrap()); Self { child, stdin, stdout, next: 1 } @@ -69,18 +69,18 @@ impl Mcp { self.tool_json("run_command", json!({"command": command, "params": params})) } fn document(&mut self) -> Value { - let v = self.request("resources/read", json!({"uri": "drawcraft://document/json"})); + let v = self.request("resources/read", json!({"uri": "vectorcraft://document/json"})); serde_json::from_str(v["result"]["contents"][0]["text"].as_str().unwrap()).unwrap() } fn art_count(&mut self) -> usize { - let d: drawcraft_testkit::doc::Document = serde_json::from_value(self.document()).unwrap(); + let d: vectorcraft_testkit::doc::Document = serde_json::from_value(self.document()).unwrap(); d.node_count() - d.layers.len() } fn initialize(&mut self) { let v = self.request("initialize", json!({"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "e2e", "version": "0"}})); let r = &v["result"]; - assert_eq!(r["serverInfo"]["name"], "drawcraft"); + assert_eq!(r["serverInfo"]["name"], "vectorcraft"); assert!(r["protocolVersion"].is_string()); assert!(r["capabilities"]["tools"].is_object()); self.notify("notifications/initialized"); @@ -99,7 +99,7 @@ impl Drop for Mcp { } fn tmp(name: &str) -> String { - drawcraft_testkit::temp_dir("cli-mcp-e2e").join(name).to_string_lossy().to_string() + vectorcraft_testkit::temp_dir("cli-mcp-e2e").join(name).to_string_lossy().to_string() } #[test] @@ -161,9 +161,9 @@ fn draw_inspect_batch_undo_redo() { let after = m.document(); assert_ne!(before, after); m.tool("undo", json!({})); - assert!(drawcraft_testkit::invariants::json_approx_eq(&m.document(), &before, 1e-12)); + assert!(vectorcraft_testkit::invariants::json_approx_eq(&m.document(), &before, 1e-12)); m.tool("redo", json!({})); - assert!(drawcraft_testkit::invariants::json_approx_eq(&m.document(), &after, 1e-12)); + assert!(vectorcraft_testkit::invariants::json_approx_eq(&m.document(), &after, 1e-12)); // Tool errors are reported in-band, and the server keeps working. let v = m.request("tools/call", json!({"name": "run_command", "arguments": {"command": "no.such.command"}})); @@ -183,9 +183,9 @@ fn screenshot_is_a_decodable_png() { let r = m.tool("screenshot", json!({"scale": 0.5, "path": path})); let img = r["content"].as_array().unwrap().iter().find(|c| c["type"] == "image").expect("image content").clone(); assert_eq!(img["mimeType"], "image/png"); - let png = drawcraft_testkit::format::base64_decode(img["data"].as_str().unwrap()).expect("base64"); + let png = vectorcraft_testkit::format::base64_decode(img["data"].as_str().unwrap()).expect("base64"); let decoded = Image::from_png(&png).expect("decodable PNG"); - let doc: drawcraft_testkit::doc::Document = serde_json::from_value(m.document()).unwrap(); + let doc: vectorcraft_testkit::doc::Document = serde_json::from_value(m.document()).unwrap(); let ab = doc.artboards[0].rect; assert_eq!(decoded.width, (ab.width() * 0.5).round() as u32); assert_eq!(decoded.height, (ab.height() * 0.5).round() as u32); @@ -208,11 +208,11 @@ fn export_formats_and_reopen() { let original = m.document(); let n = m.art_count(); - let dc = tmp("e2e.drawcraft"); + let dc = tmp("e2e.vectorcraft"); let svg = tmp("e2e.svg"); let pdf = tmp("e2e.pdf"); let png = tmp("e2e.png"); - m.tool("export", json!({"format": "drawcraft", "path": dc})); + m.tool("export", json!({"format": "vectorcraft", "path": dc})); m.tool("export", json!({"path": svg})); m.tool("export", json!({"path": png, "scale": 0.25})); m.run("document.export", json!({"format": "pdf", "path": pdf})); @@ -220,9 +220,9 @@ fn export_formats_and_reopen() { assert!(std::fs::read_to_string(&svg).unwrap().contains("= n, "svg re-open lost art"); m.run("document.open", json!({"path": pdf})); @@ -231,10 +231,10 @@ fn export_formats_and_reopen() { assert!(docs.is_object()); // save_file writes the native format to a new path and it loads. - let saved = tmp("saved.drawcraft"); + let saved = tmp("saved.vectorcraft"); m.tool("save_file", json!({"path": saved})); - let d = drawcraft_testkit::format::load(&std::fs::read(&saved).unwrap()).unwrap(); - drawcraft_testkit::invariants::check_document(&d).unwrap(); + let d = vectorcraft_testkit::format::load(&std::fs::read(&saved).unwrap()).unwrap(); + vectorcraft_testkit::invariants::check_document(&d).unwrap(); m.close(); } diff --git a/apps/drawcraft-web/Cargo.toml b/apps/vectorcraft-web/Cargo.toml similarity index 76% rename from apps/drawcraft-web/Cargo.toml rename to apps/vectorcraft-web/Cargo.toml index 1b97ae73d..3bf8c7665 100644 --- a/apps/drawcraft-web/Cargo.toml +++ b/apps/vectorcraft-web/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "drawcraft-web" -description = "DrawCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" +name = "vectorcraft-web" +description = "VectorCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)" version.workspace = true edition.workspace = true license.workspace = true @@ -10,12 +10,12 @@ publish = false workspace = true [[bin]] -name = "drawcraft-web" +name = "vectorcraft-web" path = "src/main.rs" [dependencies] -drawcraft-engine = { workspace = true } -drawcraft-ui-egui = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-ui-egui = { workspace = true } egui = { workspace = true } log = { workspace = true } diff --git a/apps/drawcraft-web/Trunk.toml b/apps/vectorcraft-web/Trunk.toml similarity index 67% rename from apps/drawcraft-web/Trunk.toml rename to apps/vectorcraft-web/Trunk.toml index 5a4fff4ea..5683fc5ad 100644 --- a/apps/drawcraft-web/Trunk.toml +++ b/apps/vectorcraft-web/Trunk.toml @@ -1,4 +1,4 @@ -# Build with: trunk build --release (from apps/drawcraft-web) +# Build with: trunk build --release (from apps/vectorcraft-web) [build] target = "index.html" dist = "../../dist/web" diff --git a/apps/vectorcraft-web/index.html b/apps/vectorcraft-web/index.html new file mode 100644 index 000000000..05490806f --- /dev/null +++ b/apps/vectorcraft-web/index.html @@ -0,0 +1,19 @@ + + + + + + VectorCraft + + + + + +
Loading VectorCraft…
+ + diff --git a/apps/drawcraft-web/src/main.rs b/apps/vectorcraft-web/src/main.rs similarity index 75% rename from apps/drawcraft-web/src/main.rs rename to apps/vectorcraft-web/src/main.rs index b0f087d45..945f83d49 100644 --- a/apps/drawcraft-web/src/main.rs +++ b/apps/vectorcraft-web/src/main.rs @@ -1,6 +1,6 @@ -//! DrawCraft in the browser. +//! VectorCraft in the browser. //! -//! Runs the same [`drawcraft_ui_egui::DrawcraftApp`] as the desktop app through eframe's web +//! Runs the same [`vectorcraft_ui_egui::VectorcraftApp`] as the desktop app through eframe's web //! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` //! from this directory; see `docs/development.md` ("Web build"). //! @@ -23,5 +23,5 @@ fn main() { #[cfg(not(target_arch = "wasm32"))] fn main() { - eprintln!("drawcraft-web only runs in the browser: build it with `trunk build --release` in apps/drawcraft-web"); + eprintln!("vectorcraft-web only runs in the browser: build it with `trunk build --release` in apps/vectorcraft-web"); } diff --git a/apps/drawcraft-web/src/web.rs b/apps/vectorcraft-web/src/web.rs similarity index 87% rename from apps/drawcraft-web/src/web.rs rename to apps/vectorcraft-web/src/web.rs index 68c065dc1..cb8bdae5c 100644 --- a/apps/drawcraft-web/src/web.rs +++ b/apps/vectorcraft-web/src/web.rs @@ -2,15 +2,15 @@ use std::sync::{Arc, Mutex}; -use drawcraft_engine::Session; -use drawcraft_ui_egui::{DrawcraftApp, Services}; +use vectorcraft_engine::Session; +use vectorcraft_ui_egui::{Services, VectorcraftApp}; use wasm_bindgen::JsCast as _; type Inbox = Arc)>>>; -const OPEN_EXTS: &[&str] = &["drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"]; -const CANVAS_ID: &str = "drawcraft_canvas"; -const LOADING_ID: &str = "drawcraft_loading"; +const OPEN_EXTS: &[&str] = &["vectorcraft", "drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"]; +const CANVAS_ID: &str = "vectorcraft_canvas"; +const LOADING_ID: &str = "vectorcraft_loading"; pub fn start() { eframe::WebLogger::init(log::LevelFilter::Info).ok(); @@ -35,10 +35,10 @@ pub fn start() { options, Box::new(move |cc| { if let Some(rs) = &cc.wgpu_render_state { - log::info!("drawcraft-web: wgpu backend {:?}", rs.adapter.get_info().backend); + log::info!("vectorcraft-web: wgpu backend {:?}", rs.adapter.get_info().backend); } let inbox: Inbox = Arc::default(); - let app = DrawcraftApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); + let app = VectorcraftApp::new(Session::new(), services(inbox.clone(), cc.egui_ctx.clone())); Ok(Box::new(WebShell { app, inbox })) }), ) @@ -46,7 +46,7 @@ pub fn start() { if let Some(el) = document.get_element_by_id(LOADING_ID) { match result { Ok(()) => el.remove(), - Err(e) => el.set_inner_html(&format!("

DrawCraft failed to start: {e:?}

A browser with WebGPU or WebGL2 is required.

")), + Err(e) => el.set_inner_html(&format!("

VectorCraft failed to start: {e:?}

A browser with WebGPU or WebGL2 is required.

")), } } }); @@ -59,7 +59,7 @@ fn query() -> String { /// Wraps the app to read dropped files asynchronously (browsers can't read them synchronously) /// and feed them through the inbox. struct WebShell { - app: DrawcraftApp, + app: VectorcraftApp, inbox: Inbox, } @@ -120,7 +120,7 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services { /// Trigger a browser download of `bytes` named after the last component of `path`. fn download(path: &str, bytes: &[u8]) -> Result<(), String> { let js = |e: wasm_bindgen::JsValue| format!("{e:?}"); - let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "drawcraft".into()); + let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "vectorcraft".into()); let window = web_sys::window().ok_or("no window")?; let document = window.document().ok_or("no document")?; let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes)); @@ -150,7 +150,7 @@ fn mime_for(name: &str) -> &'static str { Some("png") => "image/png", Some("jpg" | "jpeg") => "image/jpeg", Some("pdf") => "application/pdf", - Some("drawcraft") => "application/json", + Some("vectorcraft" | "drawcraft") => "application/json", _ => "application/octet-stream", } } diff --git a/apps/drawcraft/Cargo.toml b/apps/vectorcraft/Cargo.toml similarity index 80% rename from apps/drawcraft/Cargo.toml rename to apps/vectorcraft/Cargo.toml index 4facecf52..c8de7ec44 100644 --- a/apps/drawcraft/Cargo.toml +++ b/apps/vectorcraft/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "drawcraft" +name = "vectorcraft" version.workspace = true edition.workspace = true license.workspace = true publish = false -default-run = "drawcraft" +default-run = "vectorcraft" [lints] workspace = true [dependencies] -drawcraft-engine = { workspace = true } -drawcraft-ui-egui = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-ui-egui = { workspace = true } eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] } egui = { workspace = true } serde_json = { workspace = true } diff --git a/apps/drawcraft/src/control_server.rs b/apps/vectorcraft/src/control_server.rs similarity index 87% rename from apps/drawcraft/src/control_server.rs rename to apps/vectorcraft/src/control_server.rs index 81df35b44..f4066b5e2 100644 --- a/apps/drawcraft/src/control_server.rs +++ b/apps/vectorcraft/src/control_server.rs @@ -1,24 +1,24 @@ //! Loopback JSON-lines control server: one request per line, one reply per line. -//! This is the transport the MCP server (`drawcraft mcp --connect`) wraps. +//! This is the transport the MCP server (`vectorcraft mcp --connect`) wraps. use std::io::{BufRead, BufReader, Write}; use std::net::{TcpListener, TcpStream}; use std::sync::mpsc::{Receiver, Sender, channel}; use std::time::Duration; -use drawcraft_ui_egui::ControlRequest; use serde_json::{Value, json}; +use vectorcraft_ui_egui::ControlRequest; pub fn start(port: u16, ctx: egui::Context) -> Receiver { let (tx, rx) = channel::(); let listener = match TcpListener::bind(("127.0.0.1", port)) { Ok(l) => l, Err(e) => { - eprintln!("drawcraft: control server failed to bind 127.0.0.1:{port}: {e}"); + eprintln!("vectorcraft: control server failed to bind 127.0.0.1:{port}: {e}"); return rx; } }; - eprintln!("drawcraft: control server listening on 127.0.0.1:{port}"); + eprintln!("vectorcraft: control server listening on 127.0.0.1:{port}"); std::thread::spawn(move || { for stream in listener.incoming().flatten() { let tx = tx.clone(); diff --git a/apps/drawcraft/src/main.rs b/apps/vectorcraft/src/main.rs similarity index 63% rename from apps/drawcraft/src/main.rs rename to apps/vectorcraft/src/main.rs index 9169fbb88..bc887d026 100644 --- a/apps/drawcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -1,25 +1,25 @@ -//! DrawCraft desktop app. +//! VectorCraft desktop app. //! -//! Usage: `drawcraft [--control ] [files…]` +//! Usage: `vectorcraft [--control ] [files…]` //! -//! `--control ` (or `DRAWCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: +//! `--control ` (or `VECTORCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server: //! `{"id":1,"method":"ui.inspect","params":{}}` → `{"id":1,"ok":true,"result":…}`. -//! See `drawcraft_ui_egui::control` for the methods. +//! See `vectorcraft_ui_egui::control` for the methods. mod control_server; #[cfg(target_os = "macos")] mod native_menu; -use drawcraft_engine::Session; -use drawcraft_ui_egui::{DrawcraftApp, Services}; +use vectorcraft_engine::Session; +use vectorcraft_ui_egui::{Services, VectorcraftApp}; -struct App(DrawcraftApp, #[cfg(target_os = "macos")] Option); +struct App(VectorcraftApp, #[cfg(target_os = "macos")] Option); impl eframe::App for App { fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { #[cfg(target_os = "macos")] { - if self.1.is_none() && std::env::var_os("DRAWCRAFT_NO_NATIVE_MENU").is_none() { + if self.1.is_none() && std::env::var_os("VECTORCRAFT_NO_NATIVE_MENU").is_none() { self.1 = Some(native_menu::NativeMenu::install(&mut self.0)); } if let Some(m) = &mut self.1 { @@ -42,37 +42,47 @@ impl eframe::App for App { } } -/// Where UI preferences live: ~/Library/Application Support/DrawCraft (macOS), -/// %APPDATA%\DrawCraft (Windows), $XDG_CONFIG_HOME or ~/.config/drawcraft (Linux). +/// Where UI preferences live: ~/Library/Application Support/VectorCraft (macOS), +/// %APPDATA%\VectorCraft (Windows), $XDG_CONFIG_HOME or ~/.config/vectorcraft (Linux). fn prefs_path() -> Option { + prefs_path_for("VectorCraft", "vectorcraft") +} + +/// The same place under the project's former name (DrawCraft): read once if there are no +/// VectorCraft preferences yet, so settings survive the rename. +fn legacy_prefs_path() -> Option { + prefs_path_for("DrawCraft", "drawcraft") +} + +fn prefs_path_for(name: &str, lower: &str) -> Option { let base = if cfg!(target_os = "macos") { - std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support/DrawCraft")) + std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join("Library/Application Support").join(name)) } else if cfg!(windows) { - std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join("DrawCraft")) + std::env::var_os("APPDATA").map(|a| std::path::PathBuf::from(a).join(name)) } else { std::env::var_os("XDG_CONFIG_HOME") .map(std::path::PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| std::path::PathBuf::from(h).join(".config"))) - .map(|c| c.join("drawcraft")) + .map(|c| c.join(lower)) }; base.map(|b| b.join("ui.json")) } -fn load_prefs(app: &mut DrawcraftApp) { - if std::env::var_os("DRAWCRAFT_NO_PREFS").is_some() { +fn load_prefs(app: &mut VectorcraftApp) { + if std::env::var_os("VECTORCRAFT_NO_PREFS").is_some() { return; } - if let Some(p) = prefs_path() - && let Ok(bytes) = std::fs::read(&p) - && let Ok(ui) = serde_json::from_slice::(&bytes) + let bytes = prefs_path().and_then(|p| std::fs::read(p).ok()).or_else(|| legacy_prefs_path().and_then(|p| std::fs::read(p).ok())); + if let Some(bytes) = bytes + && let Ok(ui) = serde_json::from_slice::(&bytes) { app.ui = ui.sanitized(); } - drawcraft_ui_egui::prefs_dialog::restore(app); + vectorcraft_ui_egui::prefs_dialog::restore(app); } -fn save_prefs(app: &DrawcraftApp) { - if std::env::var_os("DRAWCRAFT_NO_PREFS").is_some() { +fn save_prefs(app: &VectorcraftApp) { + if std::env::var_os("VECTORCRAFT_NO_PREFS").is_some() { return; } if let Some(p) = prefs_path() { @@ -89,7 +99,7 @@ fn services() -> Services { Services { pick_open: Some(Box::new(|| { rfd::FileDialog::new() - .add_filter("All supported", &["drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"]) + .add_filter("All supported", &["vectorcraft", "drawcraft", "svg", "png", "jpg", "jpeg", "gif", "webp"]) .pick_file() .map(|p| p.to_string_lossy().to_string()) })), @@ -103,14 +113,14 @@ fn services() -> Services { } fn main() -> eframe::Result { - let mut control_port: Option = std::env::var("DRAWCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); + let mut control_port: Option = std::env::var("VECTORCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok()); let mut files = Vec::new(); let mut args = std::env::args().skip(1); while let Some(a) = args.next() { match a.as_str() { "--control" => control_port = args.next().and_then(|p| p.parse().ok()), "--version" => { - println!("drawcraft {}", env!("CARGO_PKG_VERSION")); + println!("vectorcraft {}", env!("CARGO_PKG_VERSION")); return Ok(()); } _ => files.push(a), @@ -118,7 +128,7 @@ fn main() -> eframe::Result { } let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_title("DrawCraft") + .with_title("VectorCraft") .with_inner_size([1440.0, 900.0]) .with_min_inner_size([800.0, 500.0]) .with_drag_and_drop(true) @@ -128,10 +138,10 @@ fn main() -> eframe::Result { ..Default::default() }; eframe::run_native( - "DrawCraft", + "VectorCraft", options, Box::new(move |cc| { - let mut app = DrawcraftApp::new(Session::new(), services()); + let mut app = VectorcraftApp::new(Session::new(), services()); load_prefs(&mut app); app.integrated_titlebar = cfg!(target_os = "macos"); if let Some(port) = control_port { @@ -139,8 +149,8 @@ fn main() -> eframe::Result { app = app.with_control(rx); } for f in files { - if let Err(e) = drawcraft_ui_egui::io::open_path(&mut app, &f) { - eprintln!("drawcraft: {f}: {e}"); + if let Err(e) = vectorcraft_ui_egui::io::open_path(&mut app, &f) { + eprintln!("vectorcraft: {f}: {e}"); } } Ok(Box::new(App( diff --git a/apps/drawcraft/src/native_menu.rs b/apps/vectorcraft/src/native_menu.rs similarity index 86% rename from apps/drawcraft/src/native_menu.rs rename to apps/vectorcraft/src/native_menu.rs index 7c25b418a..2f9cd23c0 100644 --- a/apps/drawcraft/src/native_menu.rs +++ b/apps/vectorcraft/src/native_menu.rs @@ -1,15 +1,15 @@ -//! macOS: DrawCraft's menu tree as the native system menu bar (like Illustrator on the Mac). +//! macOS: VectorCraft's menu tree as the native system menu bar (like Illustrator on the Mac). //! Items dispatch through the same command path as the in-window menus; enablement, check marks //! and dynamic labels ("Undo Move") are refreshed a few times per second. use std::collections::HashMap; use std::str::FromStr; -use drawcraft_ui_egui::DrawcraftApp; -use drawcraft_ui_egui::menus::{self, Item}; use muda::accelerator::Accelerator; use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu}; use serde_json::Value; +use vectorcraft_ui_egui::VectorcraftApp; +use vectorcraft_ui_egui::menus::{self, Item}; enum Handle { Plain(MenuItem), @@ -34,10 +34,10 @@ fn accel(sc: &str) -> Option { } impl NativeMenu { - pub fn install(app: &mut DrawcraftApp) -> Self { + pub fn install(app: &mut VectorcraftApp) -> Self { // Accelerators come from `shortcut_of`, which honours the user's overrides. - drawcraft_ui_egui::shortcut_editor::sync(&app.ui); - let generation = drawcraft_ui_egui::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed); + vectorcraft_ui_egui::shortcut_editor::sync(&app.ui); + let generation = vectorcraft_ui_egui::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed); let menu = Menu::new(); let mut items = HashMap::new(); let mut counter = 0usize; @@ -57,20 +57,20 @@ impl NativeMenu { } /// Dispatch clicked items and refresh state. - pub fn poll(&mut self, app: &mut DrawcraftApp) { + pub fn poll(&mut self, app: &mut VectorcraftApp) { while let Ok(ev) = MenuEvent::receiver().try_recv() { if let Some((cmd, params, _, _)) = self.items.get(ev.id.as_ref()) { let p = if params.is_null() { serde_json::json!({}) } else { params.clone() }; menus::invoke(app, cmd, p); } } - let now = drawcraft_ui_egui::now_ms(); + let now = vectorcraft_ui_egui::now_ms(); if now - self.last_refresh < 250.0 { return; } self.last_refresh = now; // Shortcuts edited or workspaces added: rebuild so accelerators and lists are current. - if drawcraft_ui_egui::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed) != self.generation { + if vectorcraft_ui_egui::shortcut_editor::GENERATION.load(std::sync::atomic::Ordering::Relaxed) != self.generation { *self = NativeMenu::install(app); return; } @@ -108,7 +108,13 @@ impl NativeMenu { } } -fn build(app: &DrawcraftApp, parent: &Submenu, entries: &[Item], items: &mut HashMap, counter: &mut usize) { +fn build( + app: &VectorcraftApp, + parent: &Submenu, + entries: &[Item], + items: &mut HashMap, + counter: &mut usize, +) { for e in entries { match e { Item::Sep => { diff --git a/crates/brush/Cargo.toml b/crates/brush/Cargo.toml index 784232aa7..3610cdba9 100644 --- a/crates/brush/Cargo.toml +++ b/crates/brush/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "drawcraft-brush" +name = "vectorcraft-brush" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft brushes: calligraphic, scatter, art, pattern and bristle brush definitions and stroke geometry" +description = "VectorCraft brushes: calligraphic, scatter, art, pattern and bristle brush definitions and stroke geometry" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } kurbo = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/brush/src/calli.rs b/crates/brush/src/calli.rs index 0a5c69799..a51638932 100644 --- a/crates/brush/src/calli.rs +++ b/crates/brush/src/calli.rs @@ -5,9 +5,9 @@ //! support point in direction `d` is `(a²(u·d)u + b²(v·d)v) / √(a²(u·d)² + b²(v·d)²)`, so the //! stroke's local width across direction `n` is `2·√(a²(u·n)² + b²(v·n)²)`. -use drawcraft_color::Paint; -use drawcraft_doc::Node; -use drawcraft_geom::{BezPath, PathData, Point, SubPath, Vec2}; +use vectorcraft_color::Paint; +use vectorcraft_doc::Node; +use vectorcraft_geom::{BezPath, PathData, Point, SubPath, Vec2}; use crate::track::{Rng, Track, normal, seed_of, tracks}; use crate::{Bristle, BristleShape, Calligraphic, filled, tolerance}; diff --git a/crates/brush/src/colorize.rs b/crates/brush/src/colorize.rs index a1ef4bf46..d8ba2af16 100644 --- a/crates/brush/src/colorize.rs +++ b/crates/brush/src/colorize.rs @@ -2,8 +2,8 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{AppearanceItem, Node}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Node}; use crate::Colorization; @@ -88,7 +88,7 @@ pub fn tint_node(n: &mut Node, color: &Color, amount: f32) { *swatch = None; } } - if let drawcraft_doc::NodeKind::Text(t) = &mut n.kind { + if let vectorcraft_doc::NodeKind::Text(t) = &mut n.kind { for r in &mut t.runs { if let Paint::Solid { color: c, .. } = &mut r.style.fill { *c = c.lerp(color, amount); diff --git a/crates/brush/src/defaults.rs b/crates/brush/src/defaults.rs index 1acd7afdf..19b09a1fd 100644 --- a/crates/brush/src/defaults.rs +++ b/crates/brush/src/defaults.rs @@ -4,9 +4,9 @@ use std::sync::{Arc, OnceLock}; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeId, NodeKind}; -use drawcraft_geom::{Anchor, FillRule, PathData, Point, Rect, SubPath, shapes}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeId, NodeKind}; +use vectorcraft_geom::{Anchor, FillRule, PathData, Point, Rect, SubPath, shapes}; use crate::{ArtBrush, ArtScale, Bristle, BristleShape, Brush, BrushKind, Calligraphic, Colorization, Direction, PatternBrush, PatternFit, Scatter}; diff --git a/crates/brush/src/lib.rs b/crates/brush/src/lib.rs index c78937334..55487bb32 100644 --- a/crates/brush/src/lib.rs +++ b/crates/brush/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft brushes: definitions (calligraphic, scatter, art, pattern, bristle), the default +//! VectorCraft brushes: definitions (calligraphic, scatter, art, pattern, bristle), the default //! library, and the geometry that turns a brushed stroke into filled art. //! //! Brush definitions live in the document under `Document::unknown["brushes"]` (a JSON array of @@ -18,10 +18,10 @@ mod warp; use std::sync::Arc; -use drawcraft_color::Paint; -use drawcraft_doc::{Appearance, AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, StrokeLayer}; -use drawcraft_geom::{BezPath, Rect}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::Paint; +use vectorcraft_doc::{Appearance, AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, StrokeLayer}; +use vectorcraft_geom::{BezPath, Rect}; pub use colorize::{colorize, tint_node}; pub use defaults::defaults; @@ -85,7 +85,7 @@ pub enum Colorization { /// Black and white stay; mid tones become the stroke colour, darker ones shades of it. TintsAndShades, /// The key colour becomes the stroke colour; other colours rotate by the same hue offset. - HueShift { key: drawcraft_color::Color }, + HueShift { key: vectorcraft_color::Color }, } /// Calligraphic brush: an elliptical nib swept along the path. Sizes are at 1 pt stroke weight @@ -376,7 +376,7 @@ pub fn node_pieces(doc: &Document, n: &Node) -> Option> { any = true; for mut p in stroke_pieces(b, &bp, st) { p.opacity *= st.opacity; - if st.blend != drawcraft_color::BlendMode::Normal { + if st.blend != vectorcraft_color::BlendMode::Normal { p.blend = st.blend; } out.push(p); @@ -431,7 +431,7 @@ pub fn expand(doc: &Document, n: &Node) -> Option { } /// A filled path node painted with `paint` (brush output). -pub(crate) fn filled(path: drawcraft_geom::PathData, paint: &Paint) -> Node { +pub(crate) fn filled(path: vectorcraft_geom::PathData, paint: &Paint) -> Node { Node::path(NodeId(0), path, Appearance { items: vec![AppearanceItem::Fill(FillLayer::new(paint.clone()))], effects: vec![] }) } @@ -446,7 +446,7 @@ pub fn preview_path(w: f64, h: f64) -> BezPath { /// Union of the geometric bounds of `nodes`. pub fn pieces_bounds(nodes: &[Node]) -> Option { - nodes.iter().fold(None, |acc, n| drawcraft_geom::union_opt(acc, n.visual_bounds())) + nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())) } #[cfg(test)] diff --git a/crates/brush/src/tests.rs b/crates/brush/src/tests.rs index c01d0a2f4..cc522ac8c 100644 --- a/crates/brush/src/tests.rs +++ b/crates/brush/src/tests.rs @@ -1,6 +1,6 @@ -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, StrokeLayer}; -use drawcraft_geom::{BezPath, Point, Rect, shapes}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, StrokeLayer}; +use vectorcraft_geom::{BezPath, Point, Rect, shapes}; use super::*; @@ -20,7 +20,7 @@ fn calli(angle: f64, roundness: f64, size: f64) -> Brush { } fn bounds(nodes: &[Node]) -> Rect { - nodes.iter().fold(None, |a, n| drawcraft_geom::union_opt(a, n.geometric_bounds())).unwrap() + nodes.iter().fold(None, |a, n| vectorcraft_geom::union_opt(a, n.geometric_bounds())).unwrap() } fn near(a: f64, b: f64, tol: f64) -> bool { @@ -81,7 +81,7 @@ fn closed_calligraphic_makes_a_ring() { let out = stroke_pieces(&calli(0.0, 100.0, 6.0), &bp, &stroke(1.0)); let pd = out[0].path_data().unwrap(); assert_eq!(pd.subpaths.len(), 2); - let poly_area = |sp: &drawcraft_geom::SubPath| { + let poly_area = |sp: &vectorcraft_geom::SubPath| { let a = &sp.anchors; (0..a.len()).map(|i| a[i].p.x * a[(i + 1) % a.len()].p.y - a[(i + 1) % a.len()].p.x * a[i].p.y).sum::() / 2.0 }; diff --git a/crates/brush/src/track.rs b/crates/brush/src/track.rs index 34002f411..ead927365 100644 --- a/crates/brush/src/track.rs +++ b/crates/brush/src/track.rs @@ -4,7 +4,7 @@ //! `s`; tangents are blended across the gentle vertices of flattened curves (so warped art bends //! smoothly) but stay discontinuous at real corners (so art folds there, like Illustrator). -use drawcraft_geom::{BezPath, PathEl, Point, Vec2}; +use vectorcraft_geom::{BezPath, PathEl, Point, Vec2}; /// Turn angle (degrees) above which a vertex is a corner. pub const CORNER_DEG: f64 = 30.0; diff --git a/crates/brush/src/warp.rs b/crates/brush/src/warp.rs index 9215ae852..bf4b07c88 100644 --- a/crates/brush/src/warp.rs +++ b/crates/brush/src/warp.rs @@ -6,8 +6,8 @@ use std::sync::Arc; -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::{Affine, BezPath, PathData, PathEl, Point, Rect, SubPath}; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::{Affine, BezPath, PathData, PathEl, Point, Rect, SubPath}; use crate::track::{Rng, Track, seed_of, tracks}; use crate::{ArtBrush, ArtScale, Direction, PatternBrush, PatternFit, Scatter, tolerance}; diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml index 8f41d544d..010c04753 100644 --- a/crates/color/Cargo.toml +++ b/crates/color/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "drawcraft-color" +name = "vectorcraft-color" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft color" +description = "VectorCraft color" [lints] workspace = true diff --git a/crates/color/src/cms/generic.rs b/crates/color/src/cms/generic.rs index e796591f6..fc1922e13 100644 --- a/crates/color/src/cms/generic.rs +++ b/crates/color/src/cms/generic.rs @@ -1,4 +1,4 @@ -//! "DrawCraft Generic CMYK (SWOP-like)": our own parametric press model. +//! "VectorCraft Generic CMYK (SWOP-like)": our own parametric press model. //! //! We can't redistribute Adobe's or ECI's characterisation profiles, so the built-in CMYK space is //! a small physical model instead of an ICC LUT. It is *ours* and only SWOP-*like*: the numbers diff --git a/crates/color/src/cms/mod.rs b/crates/color/src/cms/mod.rs index d134d8e5f..9fc84d1b6 100644 --- a/crates/color/src/cms/mod.rs +++ b/crates/color/src/cms/mod.rs @@ -29,7 +29,7 @@ pub const ADOBE_RGB: &str = "Adobe RGB (1998) compatible"; pub const DISPLAY_P3: &str = "Display P3"; pub const PROPHOTO_RGB: &str = "ProPhoto RGB"; /// Our parametric press model (see [`generic`]). Not an Adobe/ECI profile. -pub const GENERIC_CMYK: &str = "DrawCraft Generic CMYK (SWOP-like)"; +pub const GENERIC_CMYK: &str = "VectorCraft Generic CMYK (SWOP-like)"; /// Profile-free CMYK (`rgb = (1−c)(1−k)` …): uncalibrated, kept for legacy numbers. pub const DEVICE_CMYK: &str = "Device CMYK (uncalibrated)"; diff --git a/crates/color/src/lib.rs b/crates/color/src/lib.rs index bb82dac03..781160ba1 100644 --- a/crates/color/src/lib.rs +++ b/crates/color/src/lib.rs @@ -1,4 +1,4 @@ -//! Colour, paint and blend-mode types for DrawCraft. +//! Colour, paint and blend-mode types for VectorCraft. //! //! Colours keep the model the user picked them in (RGB, CMYK, Gray, HSB is a UI view of RGB), so //! documents don't drift when converting back and forth. Rendering asks for [`Color::to_rgba`]. diff --git a/crates/doc/Cargo.toml b/crates/doc/Cargo.toml index 7a3dcdcd5..fb90ad715 100644 --- a/crates/doc/Cargo.toml +++ b/crates/doc/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "drawcraft-doc" +name = "vectorcraft-doc" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft document model: artboards, layers, objects, appearance (pure data + serde)" +description = "VectorCraft document model: artboards, layers, objects, appearance (pure data + serde)" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } kurbo = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index a25079b0e..91ef1b84c 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -1,8 +1,8 @@ //! The appearance model: a stack of fills and strokes, each with its own opacity, blend mode and //! effects, plus object-level effects. -use drawcraft_color::{BlendMode, Color, Paint}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::{BlendMode, Color, Paint}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LineCap { @@ -105,7 +105,7 @@ impl WidthProfile { } } -/// A live effect in an appearance stack. Parameters are interpreted by `drawcraft-effects`. +/// A live effect in an appearance stack. Parameters are interpreted by `vectorcraft-effects`. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct Effect { /// Stable id, e.g. `stylize.dropShadow`, `distort.roughen`, `path.offsetPath`, `warp.arc`. diff --git a/crates/doc/src/hit.rs b/crates/doc/src/hit.rs index bf915940e..144fd8195 100644 --- a/crates/doc/src/hit.rs +++ b/crates/doc/src/hit.rs @@ -1,7 +1,7 @@ //! Hit testing over the document tree. -use drawcraft_geom::hit::{fill_contains, stroke_contains}; -use drawcraft_geom::{Point, Rect}; +use vectorcraft_geom::hit::{fill_contains, stroke_contains}; +use vectorcraft_geom::{Point, Rect}; use crate::{Document, Node, NodeId, NodeKind}; @@ -82,7 +82,7 @@ fn hit_children(parent: &Node, p: Point, opt: HitOptions, chain: &mut Vec Option { None } NodeKind::Compound { children, rule } => { - let mut bp = drawcraft_geom::BezPath::new(); + let mut bp = vectorcraft_geom::BezPath::new(); for c in children { if let Some(pd) = c.path_data() { bp.extend(pd.to_bezpath()); @@ -143,7 +143,7 @@ fn hit_leaf(n: &Node, p: Point, opt: HitOptions) -> Option { if stroke_contains(&bp, 0.0, opt.tol, p) { return Some(HitKind::Outline); } - (!opt.path_only && fill_contains(&bp, drawcraft_geom::FillRule::NonZero, p)).then_some(HitKind::Fill) + (!opt.path_only && fill_contains(&bp, vectorcraft_geom::FillRule::NonZero, p)).then_some(HitKind::Fill) } _ => None, } @@ -171,7 +171,7 @@ pub fn marquee(doc: &Document, r: Rect, scope: Option, leaves: bool) -> }; fn touches(n: &Node, r: Rect) -> bool { match &n.kind { - NodeKind::Path { path, .. } => drawcraft_geom::hit::intersects_rect(path, r), + NodeKind::Path { path, .. } => vectorcraft_geom::hit::intersects_rect(path, r), NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => { children.iter().any(|c| c.visible && touches(c, r)) } @@ -211,9 +211,9 @@ pub fn marquee(doc: &Document, r: Rect, scope: Option, leaves: bool) -> mod tests { use super::*; use crate::{Appearance, Node}; - use drawcraft_color::{Color, Paint}; - use drawcraft_geom::shapes; use std::sync::Arc; + use vectorcraft_color::{Color, Paint}; + use vectorcraft_geom::shapes; #[test] fn hits_topmost() { diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index b202b7770..5b1394237 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft document model: artboards, layers and objects, appearance, swatches (pure data + serde). +//! VectorCraft document model: artboards, layers and objects, appearance, swatches (pure data + serde). //! //! Documents are persistent trees: children are `Arc`, and edits go through //! [`Document::node_mut`], which clones only the nodes on the path from the root to the edited node. @@ -30,18 +30,18 @@ pub(crate) mod skip { } pub use appearance::{Appearance, AppearanceItem, Arrowhead, Dash, Effect, FillLayer, LineCap, LineJoin, StrokeAlign, StrokeLayer, WidthProfile}; -pub use drawcraft_color as color; -pub use drawcraft_geom as geom; pub use hit::{Hit, HitKind}; pub use live::{BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshPoint}; pub use node::{ImageObject, LAYER_COLORS, LayerColor, LiveShape, Node, NodeId, NodeKind, OpacityMask}; pub use pattern::{Overlap, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType}; pub use selection::{AnchorRef, Selection}; pub use text::{AreaOptions, CharStyle, FirstBaseline, Justify, ParaStyle, TextKind, TextObject, TextRun, TextStyleDef}; +pub use vectorcraft_color as color; +pub use vectorcraft_geom as geom; -use drawcraft_color::{Swatch, SwatchGroup}; -use drawcraft_geom::{Point, Rect}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::{Swatch, SwatchGroup}; +use vectorcraft_geom::{Point, Rect}; #[derive(Debug, thiserror::Error, PartialEq)] pub enum DocError { @@ -308,7 +308,7 @@ pub const FORMAT_VERSION: u32 = 1; impl Document { /// A new document with one artboard of `size` and one layer ("Layer 1"). pub fn new(width: f64, height: f64) -> Self { - let (swatches, swatch_groups) = drawcraft_color::default_swatches(); + let (swatches, swatch_groups) = vectorcraft_color::default_swatches(); let mut d = Self { version: FORMAT_VERSION, title: "Untitled-1".into(), @@ -541,11 +541,11 @@ impl Document { pub fn bounds_of(&self, ids: &[NodeId], visual: bool) -> Option { ids.iter() .filter_map(|id| self.node(*id)) - .fold(None, |acc, n| drawcraft_geom::union_opt(acc, if visual { n.visual_bounds() } else { n.geometric_bounds() })) + .fold(None, |acc, n| vectorcraft_geom::union_opt(acc, if visual { n.visual_bounds() } else { n.geometric_bounds() })) } /// Bounds of all art. pub fn art_bounds(&self) -> Option { - self.layers.iter().fold(None, |acc, l| drawcraft_geom::union_opt(acc, l.visual_bounds())) + self.layers.iter().fold(None, |acc, l| vectorcraft_geom::union_opt(acc, l.visual_bounds())) } /// Artboard index containing point `p` (topmost = last). pub fn artboard_at(&self, p: Point) -> Option { @@ -577,7 +577,7 @@ impl Document { } fn default_graphic_styles() -> Vec { - use drawcraft_color::{Color, Paint}; + use vectorcraft_color::{Color, Paint}; vec![ GraphicStyle { name: "Default Graphic Style".into(), appearance: Appearance::default_art() }, GraphicStyle { name: "Black Outline".into(), appearance: Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 1.0) }, @@ -595,7 +595,7 @@ fn default_graphic_styles() -> Vec { #[cfg(test)] mod tests { use super::*; - use drawcraft_geom::shapes; + use vectorcraft_geom::shapes; fn doc_with_rects() -> (Document, NodeId, NodeId) { let mut d = Document::new(612.0, 792.0); diff --git a/crates/doc/src/live.rs b/crates/doc/src/live.rs index c34e2e70f..ca32bdac6 100644 --- a/crates/doc/src/live.rs +++ b/crates/doc/src/live.rs @@ -17,9 +17,9 @@ use std::f64::consts::PI; use std::sync::Arc; -use drawcraft_color::{Color, Gradient, GradientPaint, Paint}; -use drawcraft_geom::{Affine, Anchor, BezPath, CubicBez, FillRule, ParamCurve, PathData, Point, Rect, Shape, SubPath, Vec2}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::{Color, Gradient, GradientPaint, Paint}; +use vectorcraft_geom::{Affine, Anchor, BezPath, CubicBez, FillRule, ParamCurve, PathData, Point, Rect, Shape, SubPath, Vec2}; use crate::appearance::{Appearance, AppearanceItem, FillLayer}; use crate::node::{Node, NodeId, NodeKind}; @@ -525,7 +525,7 @@ pub fn blend_expand(keys: &[Arc], spec: &BlendSpec) -> Vec { } // ===================================================================================== -// Warp maps (shared with drawcraft-effects' Warp effects) +// Warp maps (shared with vectorcraft-effects' Warp effects) // ===================================================================================== #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -688,8 +688,8 @@ fn flatten_subpath(sp: &SubPath, tol: f64) -> Vec { let mut bp = BezPath::new(); sp.to_bezpath_into(&mut bp); let mut pts: Vec = Vec::new(); - drawcraft_geom::kurbo::flatten(bp.iter(), tol, |el| match el { - drawcraft_geom::PathEl::MoveTo(p) | drawcraft_geom::PathEl::LineTo(p) if pts.last().is_none_or(|q| q.distance(p) > 1e-9) => pts.push(p), + vectorcraft_geom::kurbo::flatten(bp.iter(), tol, |el| match el { + vectorcraft_geom::PathEl::MoveTo(p) | vectorcraft_geom::PathEl::LineTo(p) if pts.last().is_none_or(|q| q.distance(p) > 1e-9) => pts.push(p), _ => {} }); pts @@ -1214,7 +1214,7 @@ impl GradientMesh { /// Union of the geometric bounds of `nodes`. pub fn nodes_bounds(nodes: &[Arc]) -> Option { - nodes.iter().fold(None, |acc, c| drawcraft_geom::union_opt(acc, c.geometric_bounds())) + nodes.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds())) } /// A `(rows+1)×(cols+1)` grid over `r` (row-major). @@ -1452,7 +1452,7 @@ pub(crate) fn max_outset(nodes: &[Arc]) -> f64 { #[cfg(test)] mod tests { use super::*; - use drawcraft_geom::shapes; + use vectorcraft_geom::shapes; fn rect_node(x: f64, y: f64, w: f64, h: f64, c: Color) -> Arc { Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(x, y, x + w, y + h)), Appearance::basic(Paint::solid(c), Paint::None, 0.0))) diff --git a/crates/doc/src/node.rs b/crates/doc/src/node.rs index 12135f434..1f0605296 100644 --- a/crates/doc/src/node.rs +++ b/crates/doc/src/node.rs @@ -2,9 +2,9 @@ use std::sync::Arc; -use drawcraft_color::BlendMode; -use drawcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::BlendMode; +use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; use crate::appearance::Appearance; use crate::live::{BlendSpec, EnvelopeKind, GradientMesh}; @@ -402,13 +402,13 @@ impl Node { NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } | NodeKind::Compound { children, .. } => children .iter() .filter(|c| c.visible || !matches!(self.kind, NodeKind::Layer { .. })) - .fold(None, |acc, c| drawcraft_geom::union_opt(acc, c.geometric_bounds())), + .fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.geometric_bounds())), NodeKind::Text(t) => t.bounds(), NodeKind::Image(im) => Some(im.xf.transform_rect_bbox(Rect::new(0.0, 0.0, im.width as f64, im.height as f64))), NodeKind::SymbolInstance { xf, .. } => Some(xf.transform_rect_bbox(Rect::new(-10.0, -10.0, 10.0, 10.0))), NodeKind::Blend { children, spec } => { let b = crate::live::nodes_bounds(children); - drawcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds())) + vectorcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds())) } NodeKind::Envelope { content, kind, .. } => crate::live::envelope_bounds(content, kind), NodeKind::Mesh(m) => m.bounds(), @@ -420,12 +420,12 @@ impl Node { match &self.kind { NodeKind::Group { children, clip: true } => children.first().and_then(|c| c.geometric_bounds()), NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => { - children.iter().fold(None, |acc, c| drawcraft_geom::union_opt(acc, c.visual_bounds())) + children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual_bounds())) } NodeKind::Blend { children, spec } => { - let b = children.iter().fold(None, |acc, c| drawcraft_geom::union_opt(acc, c.visual_bounds())); + let b = children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, c.visual_bounds())); let o = crate::live::max_outset(children); - drawcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds()).map(|r| r.inflate(o, o))) + vectorcraft_geom::union_opt(b, spec.spine.as_ref().and_then(|s| s.bounds()).map(|r| r.inflate(o, o))) } NodeKind::Envelope { content, .. } | NodeKind::Repeat(RepeatSpec { source: content, .. }) => { let o = crate::live::max_outset(content); @@ -512,7 +512,7 @@ impl Node { fn transform_paints(ap: &mut Appearance, a: Affine) { use crate::appearance::AppearanceItem; - use drawcraft_color::Paint; + use vectorcraft_color::Paint; for it in &mut ap.items { let p = match it { AppearanceItem::Fill(f) => &mut f.paint, diff --git a/crates/doc/src/pattern.rs b/crates/doc/src/pattern.rs index 7f960cf73..d151462a4 100644 --- a/crates/doc/src/pattern.rs +++ b/crates/doc/src/pattern.rs @@ -13,9 +13,9 @@ use std::sync::Arc; -use drawcraft_color::Paint; -use drawcraft_geom::{Affine, Point, Rect, Vec2}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::Paint; +use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use crate::appearance::AppearanceItem; use crate::node::{Node, NodeId, NodeKind}; @@ -210,7 +210,7 @@ impl PatternDef { } /// Visual bounds of the art (stroke included). pub fn art_bounds(&self) -> Option { - self.art.iter().fold(None, |acc, n| drawcraft_geom::union_opt(acc, n.visual_bounds())) + self.art.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())) } /// Re-derive the tile from the art (Size Tile to Art). pub fn fit_tile_to_art(&mut self) { @@ -490,7 +490,7 @@ impl RepeatSpec { /// Geometric bounds of all instances. pub fn bounds(&self) -> Option { let b = self.source_bounds()?; - self.transforms().into_iter().fold(None, |acc, m| drawcraft_geom::union_opt(acc, Some(m.transform_rect_bbox(b)))) + self.transforms().into_iter().fold(None, |acc, m| vectorcraft_geom::union_opt(acc, Some(m.transform_rect_bbox(b)))) } /// Apply a document transform (source + arrangement parameters). pub fn transform(&mut self, a: Affine, scale_strokes: bool) { @@ -545,7 +545,7 @@ pub fn is_repeat(n: &Node) -> bool { mod tests { use super::*; use crate::Appearance; - use drawcraft_geom::shapes; + use vectorcraft_geom::shapes; fn sq(x: f64, y: f64, s: f64) -> Arc { Arc::new(Node::path(NodeId(1), shapes::rectangle(Rect::new(x, y, x + s, y + s)), Appearance::default())) diff --git a/crates/doc/src/text.rs b/crates/doc/src/text.rs index 0ea4d832d..3a7367076 100644 --- a/crates/doc/src/text.rs +++ b/crates/doc/src/text.rs @@ -1,8 +1,8 @@ -//! Text objects (model only; layout and glyph outlines live in `drawcraft-text`). +//! Text objects (model only; layout and glyph outlines live in `vectorcraft-text`). -use drawcraft_color::{Color, Paint}; -use drawcraft_geom::{Affine, PathData, Point, Rect}; use serde::{Deserialize, Serialize}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_geom::{Affine, PathData, Point, Rect}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum Justify { diff --git a/crates/effects/Cargo.toml b/crates/effects/Cargo.toml index f46ea8dcc..244b7da50 100644 --- a/crates/effects/Cargo.toml +++ b/crates/effects/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "drawcraft-effects" +name = "vectorcraft-effects" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft live effects evaluated to geometry" +description = "VectorCraft live effects evaluated to geometry" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-pathops = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-pathops = { workspace = true } kurbo = { workspace = true } serde_json = { workspace = true } diff --git a/crates/effects/src/distort.rs b/crates/effects/src/distort.rs index 32fef093e..575b669f6 100644 --- a/crates/effects/src/distort.rs +++ b/crates/effects/src/distort.rs @@ -1,7 +1,7 @@ //! Distort & Transform effects. -use drawcraft_geom::{Affine, Anchor, ParamCurve, PathData, Point, Rect, SubPath, Vec2}; use serde_json::Value; +use vectorcraft_geom::{Affine, Anchor, ParamCurve, PathData, Point, Rect, SubPath, Vec2}; use crate::util::*; @@ -79,7 +79,7 @@ pub fn roughen(path: &PathData, b: Rect, p: &Value) -> PathData { let mut pts = vec![]; for i in 0..sp.segment_count() { let c = sp.segment(i); - let len = drawcraft_geom::kurbo::ParamCurveArclen::arclen(&c, 0.1); + let len = vectorcraft_geom::kurbo::ParamCurveArclen::arclen(&c, 0.1); let k = if spacing.is_finite() { ((len / spacing).ceil() as usize).clamp(1, 2000) } else { 1 }; for j in 0..k { pts.push(c.eval(j as f64 / k as f64)); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index cd84da592..ae6258914 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft live effects (Illustrator's Effect menu). +//! VectorCraft live effects (Illustrator's Effect menu). //! //! Effects live in appearance stacks as [`Effect`] `{id, params, visible}` records. This crate //! interprets them: @@ -24,9 +24,9 @@ mod warp; #[cfg(test)] mod tests; -use drawcraft_doc::Effect; -use drawcraft_geom::{BezPath, PathData, Rect}; use serde_json::{Map, Value, json}; +use vectorcraft_doc::Effect; +use vectorcraft_geom::{BezPath, PathData, Rect}; pub use raster::{RasterFx, outset, raster_effects}; pub use warp::{WarpStyle, warp_point}; @@ -306,14 +306,14 @@ fn apply_one(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomContext) - if off.abs() < 1e-9 { return path.clone(); } - drawcraft_pathops::offset_path(path, off, join(p, "joins"), num(p, "miterLimit", 4.0).clamp(1.0, 500.0)) + vectorcraft_pathops::offset_path(path, off, join(p, "joins"), num(p, "miterLimit", 4.0).clamp(1.0, 500.0)) } "path.outlineStroke" => { let w = p.get("width").and_then(Value::as_f64).unwrap_or(ctx.stroke_width); if w <= 0.0 { return path.clone(); } - drawcraft_pathops::outline_stroke(path, w, drawcraft_pathops::Cap::Butt, join(p, "joins"), num(p, "miterLimit", 10.0).max(1.0)) + vectorcraft_pathops::outline_stroke(path, w, vectorcraft_pathops::Cap::Butt, join(p, "joins"), num(p, "miterLimit", 10.0).max(1.0)) } "convertToShape.rectangle" | "convertToShape.roundedRectangle" | "convertToShape.ellipse" => stylize::convert_to_shape(id, b, p), "stylize.roundCorners" => stylize::round_corners(path, num(p, "radius", 10.0)), diff --git a/crates/effects/src/raster.rs b/crates/effects/src/raster.rs index 9bb16d5aa..f18ba7e65 100644 --- a/crates/effects/src/raster.rs +++ b/crates/effects/src/raster.rs @@ -1,8 +1,8 @@ //! Raster (painted) effects: descriptions for the renderer. -use drawcraft_doc::Effect; -use drawcraft_doc::color::{BlendMode, Color}; use serde_json::Value; +use vectorcraft_doc::Effect; +use vectorcraft_doc::color::{BlendMode, Color}; use crate::merged_params; use crate::util::*; diff --git a/crates/effects/src/stylize.rs b/crates/effects/src/stylize.rs index b6f2f6bdf..c7b3cff27 100644 --- a/crates/effects/src/stylize.rs +++ b/crates/effects/src/stylize.rs @@ -1,9 +1,9 @@ //! Convert to Shape, Round Corners and Scribble. -use drawcraft_geom::kurbo; -use drawcraft_geom::shapes::{self, KAPPA}; -use drawcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath}; use serde_json::Value; +use vectorcraft_geom::kurbo; +use vectorcraft_geom::shapes::{self, KAPPA}; +use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath}; use crate::util::*; diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index 1dbb23726..a18ffe13b 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -1,6 +1,6 @@ -use drawcraft_doc::Effect; -use drawcraft_geom::{PathData, Point, Rect, Shape, SubPath, shapes}; use serde_json::{Value, json}; +use vectorcraft_doc::Effect; +use vectorcraft_geom::{PathData, Point, Rect, Shape, SubPath, shapes}; use super::*; @@ -287,7 +287,7 @@ fn raster_effects_parse_and_outset() { assert_eq!(r.len(), 5); match &r[0] { RasterFx::DropShadow { mode, opacity, dx, dy, blur, color } => { - assert_eq!(*mode, drawcraft_doc::color::BlendMode::Multiply); + assert_eq!(*mode, vectorcraft_doc::color::BlendMode::Multiply); assert!(close(*opacity as f64, 0.5, 1e-6)); assert_eq!((*dx, *dy, *blur), (10.0, -4.0, 6.0)); assert_eq!(color.to_rgb(), [1.0, 0.0, 0.0]); @@ -301,7 +301,7 @@ fn raster_effects_parse_and_outset() { #[test] fn blend_mode_names() { - use drawcraft_doc::color::BlendMode; + use vectorcraft_doc::color::BlendMode; assert_eq!(raster::blend_mode("Color-Burn"), BlendMode::ColorBurn); assert_eq!(raster::blend_mode("screen"), BlendMode::Screen); assert_eq!(raster::blend_mode("???"), BlendMode::Normal); diff --git a/crates/effects/src/util.rs b/crates/effects/src/util.rs index 6defd176f..8ca784ae0 100644 --- a/crates/effects/src/util.rs +++ b/crates/effects/src/util.rs @@ -1,7 +1,7 @@ //! Parameter parsing, deterministic noise and path-mapping helpers. -use drawcraft_geom::{Anchor, CubicBez, ParamCurve, Point, Rect, SubPath, Vec2}; use serde_json::Value; +use vectorcraft_geom::{Anchor, CubicBez, ParamCurve, Point, Rect, SubPath, Vec2}; /// Number param (also accepts numeric strings such as `"10 pt"`); non-finite → default. pub fn num(p: &Value, key: &str, default: f64) -> f64 { @@ -30,11 +30,11 @@ pub fn text<'a>(p: &'a Value, key: &str, default: &'a str) -> &'a str { p.get(key).and_then(Value::as_str).unwrap_or(default) } -pub fn join(p: &Value, key: &str) -> drawcraft_pathops::Join { +pub fn join(p: &Value, key: &str) -> vectorcraft_pathops::Join { match text(p, key, "miter").to_ascii_lowercase().as_str() { - "round" => drawcraft_pathops::Join::Round, - "bevel" => drawcraft_pathops::Join::Bevel, - _ => drawcraft_pathops::Join::Miter, + "round" => vectorcraft_pathops::Join::Round, + "bevel" => vectorcraft_pathops::Join::Bevel, + _ => vectorcraft_pathops::Join::Miter, } } @@ -69,8 +69,8 @@ pub fn seg_cubic(sp: &SubPath, i: usize) -> CubicBez { if sp.segment_is_line(i) { CubicBez::new(c.p0, c.p0.lerp(c.p3, 1.0 / 3.0), c.p0.lerp(c.p3, 2.0 / 3.0), c.p3) } else { c } } -/// Non-linear path mapping (shared with live envelopes in `drawcraft-doc`). -pub use drawcraft_doc::live::map_nonlinear; +/// Non-linear path mapping (shared with live envelopes in `vectorcraft-doc`). +pub use vectorcraft_doc::live::map_nonlinear; /// Smooth subpath through `pts` (Catmull-Rom tangents). pub fn catmull_rom(pts: &[Point], closed: bool, tension: f64) -> SubPath { diff --git a/crates/effects/src/warp.rs b/crates/effects/src/warp.rs index b6ff44730..a4021d0fc 100644 --- a/crates/effects/src/warp.rs +++ b/crates/effects/src/warp.rs @@ -3,12 +3,12 @@ //! Each style is a map on normalised box coordinates (x, y) ∈ [-1, 1]² (y down). The path is //! split into short pieces whose control points are mapped (see `map_nonlinear`). -use drawcraft_geom::{PathData, Point, Rect}; use serde_json::Value; +use vectorcraft_geom::{PathData, Point, Rect}; use crate::util::*; -pub use drawcraft_doc::live::{WarpStyle, warp_point}; +pub use vectorcraft_doc::live::{WarpStyle, warp_point}; /// Apply a warp to `path` using box `b`. pub fn warp(path: &PathData, b: Rect, style: WarpStyle, p: &Value) -> PathData { diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index 45c5b89b0..287920b98 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -1,26 +1,26 @@ [package] -name = "drawcraft-engine" +name = "vectorcraft-engine" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft engine: sessions, history, selection, the command registry" +description = "VectorCraft engine: sessions, history, selection, the command registry" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-tools = { workspace = true } -drawcraft-text = { workspace = true } -drawcraft-render = { workspace = true } -drawcraft-pathops = { workspace = true } -drawcraft-brush = { workspace = true } -drawcraft-trace = { workspace = true } -drawcraft-svg = { workspace = true } -drawcraft-format = { workspace = true } -drawcraft-pdf = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-tools = { workspace = true } +vectorcraft-text = { workspace = true } +vectorcraft-render = { workspace = true } +vectorcraft-pathops = { workspace = true } +vectorcraft-brush = { workspace = true } +vectorcraft-trace = { workspace = true } +vectorcraft-svg = { workspace = true } +vectorcraft-format = { workspace = true } +vectorcraft-pdf = { workspace = true } kurbo = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -28,5 +28,5 @@ thiserror = { workspace = true } log = { workspace = true } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } diff --git a/crates/engine/src/cmd/brushsym.rs b/crates/engine/src/cmd/brushsym.rs index 137fd60af..b31afdf07 100644 --- a/crates/engine/src/cmd/brushsym.rs +++ b/crates/engine/src/cmd/brushsym.rs @@ -1,6 +1,6 @@ //! Brushes (Brushes panel, Paintbrush, Expand) and Symbols (Symbols panel, symbolism tools). //! -//! Brush definitions live in `drawcraft-brush` and are stored in `Document::unknown["brushes"]`. +//! Brush definitions live in `vectorcraft-brush` and are stored in `Document::unknown["brushes"]`. //! Symbol definitions are `Document::symbols`; symbols made here are *normalised*: the art is //! centred on the origin and scaled (non-uniformly, strokes untouched) into the ±10 pt box that //! `Node::geometric_bounds` assumes for instances, and each instance's `xf` scales it back. The @@ -8,11 +8,11 @@ use std::sync::Arc; -use drawcraft_brush::{self as brush, Brush}; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol}; -use drawcraft_geom::{Affine, Point, Vec2}; use serde_json::{Value, json}; +use vectorcraft_brush::{self as brush, Brush}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Document, FillLayer, Node, NodeId, NodeKind, Symbol}; +use vectorcraft_geom::{Affine, Point, Vec2}; use super::edit::selected_roots; use super::*; @@ -618,7 +618,7 @@ fn expanded_instance(d: &mut Document, inst: &Node) -> Option { } art.transform(*xf, false); let mut art = d.reid(&art); - if inst.opacity < 1.0 || inst.blend != drawcraft_color::BlendMode::Normal { + if inst.opacity < 1.0 || inst.blend != vectorcraft_color::BlendMode::Normal { if !matches!(art.kind, NodeKind::Group { .. }) { let id = d.alloc_id(); art = Node::group(id, vec![Arc::new(art)]); diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index 8ad5c7be4..9e24fe2b5 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -4,7 +4,7 @@ //! starts with "Live Paint". Its first child is a hidden group named "Live Paint Sources" holding //! the original paths (the planar map's inputs, used by Release, Merge and re-computation); then //! come the faces (paths named "Face", fill only) and the edges (open paths named "Edge", stroke -//! only). See `drawcraft_tools::builder` for the shared helpers. +//! only). See `vectorcraft_tools::builder` for the shared helpers. //! //! **Image Trace** makes a group named "Image Trace" holding the source image (hidden, bottom) //! and the traced filled paths above it, mapped with the image's transform. Expand drops the @@ -12,16 +12,16 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::appearance::{AppearanceItem, FillLayer, StrokeLayer}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; -use drawcraft_geom::{FillRule, PathData, Point, Rect, Shape as _}; -use drawcraft_pathops as po; -use drawcraft_tools::builder::{ +use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::appearance::{AppearanceItem, FillLayer, StrokeLayer}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; +use vectorcraft_geom::{FillRule, PathData, Point, Rect, Shape as _}; +use vectorcraft_pathops as po; +use vectorcraft_tools::builder::{ self as b, EDGE_NAME, FACE_NAME, LIVE_PAINT_NAME, SOURCES_NAME, edge_near, face_at, is_live_paint, sample_polyline, shapes_for, sorted_roots, }; -use drawcraft_trace as tr; -use serde_json::{Value, json}; +use vectorcraft_trace as tr; use super::edit::selected_roots; use super::paint::paint_from; @@ -673,7 +673,7 @@ fn trace_make(s: &mut Session, p: &Value, expand: bool) -> Result { // Pixel space of the decoded raster → the image object's pixel space → document. let sx = img.width.max(1) as f64 / raster.width.max(1) as f64; let sy = img.height.max(1) as f64 / raster.height.max(1) as f64; - let xf = img.xf * drawcraft_geom::Affine::scale_non_uniform(sx, sy); + let xf = img.xf * vectorcraft_geom::Affine::scale_non_uniform(sx, sy); let (npaths, anchors, colors) = (res.paths.len(), res.anchor_count(), res.palette.len()); let label = if expand { "Image Trace (Make and Expand)" } else { "Image Trace" }; let id = s.edit(label, |d, sel| { diff --git a/crates/engine/src/cmd/clipboard.rs b/crates/engine/src/cmd/clipboard.rs index 110ab49d1..e9b2903f8 100644 --- a/crates/engine/src/cmd/clipboard.rs +++ b/crates/engine/src/cmd/clipboard.rs @@ -1,14 +1,14 @@ //! System clipboard interchange: the copied objects as SVG markup (what other apps paste), and //! SVG markup from other apps turned into clipboard objects (pasted with the Paste commands). //! -//! The UI owns the platform clipboard; these commands only convert. Within DrawCraft the +//! The UI owns the platform clipboard; these commands only convert. Within VectorCraft the //! internal clipboard stays lossless (live effects, masks, symbols); SVG is the outside format. use std::sync::Arc; -use drawcraft_doc::{Document, Node, NodeId}; -use drawcraft_geom::{Affine, Point}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeId}; +use vectorcraft_geom::{Affine, Point}; use super::*; @@ -41,12 +41,12 @@ impl Session { } // The active document supplies swatches, symbols, patterns and image blobs. let mut d = self.active().map(|st| (*st.doc).clone()).unwrap_or_else(|| Document::new(100.0, 100.0)); - let mut layer = Node::layer(NodeId(u64::MAX), "Clipboard", drawcraft_doc::LayerColor::Preset(0)); + let mut layer = Node::layer(NodeId(u64::MAX), "Clipboard", vectorcraft_doc::LayerColor::Preset(0)); if let Some(ch) = layer.children_mut() { *ch = self.clipboard.iter().cloned().map(Arc::new).collect(); } d.layers = vec![Arc::new(layer)]; - Some(drawcraft_svg::export(&d, &drawcraft_svg::ExportOptions { artboard: None, object_ids: false, ..Default::default() })) + Some(vectorcraft_svg::export(&d, &vectorcraft_svg::ExportOptions { artboard: None, object_ids: false, ..Default::default() })) } } @@ -56,7 +56,7 @@ fn export_svg(s: &mut Session, _: &Value) -> Result { fn import_svg(s: &mut Session, p: &Value) -> Result { let svg = str_param(p, "svg").ok_or_else(|| bad("clipboard.importSvg", "missing svg"))?; - let src = drawcraft_svg::import(svg).map_err(|e| bad("clipboard.importSvg", e.to_string()))?; + let src = vectorcraft_svg::import(svg).map_err(|e| bad("clipboard.importSvg", e.to_string()))?; let mut nodes: Vec = src.layers.iter().flat_map(|l| l.children().cloned().unwrap_or_default()).map(|n| (*n).clone()).collect(); if nodes.is_empty() { return Err(bad("clipboard.importSvg", "the SVG has no drawable objects")); @@ -105,7 +105,7 @@ mod tests { s.execute("edit.copy", &json!({})).unwrap(); let svg = s.execute("clipboard.exportSvg", &json!({})).unwrap()["svg"].as_str().unwrap().to_string(); assert!(looks_like_svg(&svg) && svg.contains("#12ab34"), "{svg}"); - // As if pasted into another DrawCraft window. + // As if pasted into another VectorCraft window. let mut t = session(); assert_eq!(t.execute("clipboard.importSvg", &json!({"svg": svg, "center": [200, 150]})).unwrap()["count"], 1); t.execute("edit.pasteInPlace", &json!({})).unwrap(); @@ -131,7 +131,7 @@ mod tests { s.execute("edit.paste", &json!({})).unwrap(); let d = &s.doc().unwrap().doc; let compound = d.node(NodeId(c)).unwrap(); - assert!(compound.children().unwrap().iter().all(|ch| matches!(ch.kind, drawcraft_doc::NodeKind::Path { .. }))); + assert!(compound.children().unwrap().iter().all(|ch| matches!(ch.kind, vectorcraft_doc::NodeKind::Path { .. }))); } #[test] diff --git a/crates/engine/src/cmd/colorcmds.rs b/crates/engine/src/cmd/colorcmds.rs index 534e3ef8f..abc40f220 100644 --- a/crates/engine/src/cmd/colorcmds.rs +++ b/crates/engine/src/cmd/colorcmds.rs @@ -5,9 +5,9 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind}; use super::edit::selected_roots; use super::*; diff --git a/crates/engine/src/cmd/colormgmt.rs b/crates/engine/src/cmd/colormgmt.rs index 50fe43c3b..be83a99ec 100644 --- a/crates/engine/src/cmd/colormgmt.rs +++ b/crates/engine/src/cmd/colormgmt.rs @@ -2,17 +2,17 @@ //! through the CMS, gamut checks, View → Proof Setup / Proof Colors / Overprint Preview, the //! Separations Preview panel, Overprint Black and spot swatches. //! -//! Colour settings and the proof view are process-wide (`drawcraft_color::cms::active`, -//! `drawcraft_render::proof::view`). A document's assigned profiles are stored in +//! Colour settings and the proof view are process-wide (`vectorcraft_color::cms::active`, +//! `vectorcraft_render::proof::view`). A document's assigned profiles are stored in //! `Document.unknown["colorProfiles"]` (`{rgb?, cmyk?}`) and become active when assigned. use std::collections::BTreeMap; -use drawcraft_color::cms::{self, ColorSettings, Intent, Model, ProofTarget}; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::ColorMode; -use drawcraft_render::proof::{self, OVERPRINT_KEY}; use serde_json::{Value, json}; +use vectorcraft_color::cms::{self, ColorSettings, Intent, Model, ProofTarget}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::ColorMode; +use vectorcraft_render::proof::{self, OVERPRINT_KEY}; use super::*; @@ -145,14 +145,10 @@ fn settings_json(st: &ColorSettings) -> Value { } /// Colour settings/proof changes don't edit the document, but the canvas must redraw: bump the -/// revisions without marking documents dirty. +/// revisions (which never mark documents dirty). fn touch_all(s: &mut Session) { for d in &mut s.docs { - let clean = d.revision == d.saved_revision; d.revision += 1; - if clean { - d.saved_revision = d.revision; - } } } @@ -191,7 +187,7 @@ fn load_profile(_: &mut Session, _: &Value) -> Result { } /// The document's assigned profiles (`None` = working space). -pub fn doc_profiles(d: &drawcraft_doc::Document) -> (Option, Option) { +pub fn doc_profiles(d: &vectorcraft_doc::Document) -> (Option, Option) { let o = d.unknown.get(PROFILES_KEY); let get = |k: &str| o.and_then(|o| o.get(k)).and_then(Value::as_str).map(str::to_string); (get("rgb"), get("cmyk")) @@ -317,7 +313,7 @@ fn gamut_check(s: &mut Session, p: &Value) -> Result { } else { let st = s.doc()?; let ids = targets(s, p)?; - let mut collect = |n: &drawcraft_doc::Node| { + let mut collect = |n: &vectorcraft_doc::Node| { let mut n = n.clone(); proof::map_node_colors(&mut n, &mut |c, _| { cols.push(*c); @@ -451,10 +447,10 @@ fn overprint_black(s: &mut Session, p: &Value) -> Result { let mut hits: Vec = vec![]; for id in &ids { if let Some(n) = doc.node(*id) { - n.walk(&mut |m: &drawcraft_doc::Node| { + n.walk(&mut |m: &vectorcraft_doc::Node| { let bl = m.appearance.items.iter().any(|it| match it { - drawcraft_doc::AppearanceItem::Fill(l) => l.paint.color().is_some_and(|c| black_only(&c)), - drawcraft_doc::AppearanceItem::Stroke(l) => l.paint.color().is_some_and(|c| black_only(&c)), + vectorcraft_doc::AppearanceItem::Fill(l) => l.paint.color().is_some_and(|c| black_only(&c)), + vectorcraft_doc::AppearanceItem::Stroke(l) => l.paint.color().is_some_and(|c| black_only(&c)), }); if bl { hits.push(m.id.0); diff --git a/crates/engine/src/cmd/create.rs b/crates/engine/src/cmd/create.rs index f65962f29..d473be082 100644 --- a/crates/engine/src/cmd/create.rs +++ b/crates/engine/src/cmd/create.rs @@ -1,8 +1,8 @@ //! Creating art: shapes, paths, text. -use drawcraft_doc::{Appearance, CharStyle, LiveShape, Node, NodeKind, TextObject}; -use drawcraft_geom::{Affine, Anchor, AnchorKind, FillRule, PathData, Point, Rect, SubPath, shapes}; use serde_json::{Value, json}; +use vectorcraft_doc::{Appearance, CharStyle, LiveShape, Node, NodeKind, TextObject}; +use vectorcraft_geom::{Affine, Anchor, AnchorKind, FillRule, PathData, Point, Rect, SubPath, shapes}; use super::*; @@ -72,7 +72,7 @@ pub(crate) fn add_node(s: &mut Session, label: &str, kind: NodeKind, appearance: let (par, idx, _) = d.position(target).ok_or(crate::EngineError::NoNode(target))?; let mut mask = d.node(target).cloned().ok_or(crate::EngineError::NoNode(target))?; mask.id = d.alloc_id(); - mask.appearance = Appearance::basic(drawcraft_color::Paint::None, drawcraft_color::Paint::None, 0.0); + mask.appearance = Appearance::basic(vectorcraft_color::Paint::None, vectorcraft_color::Paint::None, 0.0); if let NodeKind::Path { clipping, .. } = &mut mask.kind { *clipping = true; } @@ -184,8 +184,8 @@ fn line(s: &mut Session, p: &Value) -> Result { let live = LiveShape::Line { a, b }; // Lines are stroked, never filled (Illustrator ignores the fill for new open lines). let appearance = Appearance::basic( - drawcraft_color::Paint::None, - if s.paint.stroke.is_none() { drawcraft_color::Paint::solid(drawcraft_color::Color::BLACK) } else { s.paint.stroke.clone() }, + vectorcraft_color::Paint::None, + if s.paint.stroke.is_none() { vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK) } else { s.paint.stroke.clone() }, s.paint.stroke_width.max(0.1), ); add_node(s, "Line", path_kind(live.to_path(), Some(live)), appearance, None) @@ -211,7 +211,7 @@ fn arc(s: &mut Session, p: &Value) -> Result { fn grid_group(s: &mut Session, label: &str, paths: Vec) -> Result { let parent = s.doc()?.insertion_parent(); - let ap = Appearance::basic(drawcraft_color::Paint::None, s.paint.stroke.clone(), s.paint.stroke_width); + let ap = Appearance::basic(vectorcraft_color::Paint::None, s.paint.stroke.clone(), s.paint.stroke_width); let id = s.edit(label, |d, sel| { let children = paths .into_iter() @@ -255,7 +255,7 @@ pub(crate) fn anchor_from_json(v: &Value) -> Option { fn path_create(s: &mut Session, p: &Value) -> Result { let path = if let Some(d) = str_param(p, "d") { - let bp = drawcraft_geom::BezPath::from_svg(d).map_err(|e| bad("path.create", format!("bad path data: {e}")))?; + let bp = vectorcraft_geom::BezPath::from_svg(d).map_err(|e| bad("path.create", format!("bad path data: {e}")))?; PathData::from_bezpath(&bp) } else { let anchors: Vec = p @@ -273,7 +273,7 @@ fn path_create(s: &mut Session, p: &Value) -> Result { let mut ap = Appearance::basic(s.paint.fill.clone(), s.paint.stroke.clone(), s.paint.stroke_width); // Open paths drawn with a stroke of None would be invisible: mimic Illustrator and keep what the user set. if !path.is_closed() && ap.stroke_paint().is_none() && ap.fill_paint().is_none() { - ap.set_stroke(drawcraft_color::Paint::solid(drawcraft_color::Color::BLACK)); + ap.set_stroke(vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK)); } add_node(s, "Pen", path_kind(path, None), ap, None) } @@ -293,17 +293,17 @@ fn text_create(s: &mut Session, p: &Value) -> Result { style.font_style = f.to_string(); } style.fill = match p.get("color").and_then(color_value) { - Some(c) => drawcraft_color::Paint::solid(c), - None if !s.paint.fill.is_none() && s.paint.fill != drawcraft_color::Paint::solid(drawcraft_color::Color::WHITE) => s.paint.fill.clone(), - None => drawcraft_color::Paint::solid(drawcraft_color::Color::BLACK), + Some(c) => vectorcraft_color::Paint::solid(c), + None if !s.paint.fill.is_none() && s.paint.fill != vectorcraft_color::Paint::solid(vectorcraft_color::Color::WHITE) => s.paint.fill.clone(), + None => vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), }; let mut t = TextObject::point(Point::new(x, y), text, style); if let Some(a) = p.get("area") { let w = f64_or(a, "width", 200.0); let h = f64_or(a, "height", 100.0); - t.kind = drawcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, w, h)) }; + t.kind = vectorcraft_doc::TextKind::Area { frame: shapes::rectangle(Rect::new(0.0, 0.0, w, h)) }; } - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); t.cached_bounds = Some(lay.bounds); add_node(s, "Type", NodeKind::Text(Box::new(t)), Appearance::default(), None) } diff --git a/crates/engine/src/cmd/distortcmds.rs b/crates/engine/src/cmd/distortcmds.rs index 531702327..976b7a828 100644 --- a/crates/engine/src/cmd/distortcmds.rs +++ b/crates/engine/src/cmd/distortcmds.rs @@ -1,17 +1,17 @@ //! Commands behind the distortion tools: width points, Liquify, Puppet Warp and the Perspective -//! Grid. The geometry kernels live in `drawcraft_tools::distort` (shared with the tools). +//! Grid. The geometry kernels live in `vectorcraft_tools::distort` (shared with the tools). //! //! The perspective grid definition is document data stored under //! `Document.unknown["perspectiveGrid"]` (read/written by [`grid_of`] / [`store_grid`]). use std::sync::Arc; -use drawcraft_doc::{Document, NodeId, NodeKind, WidthProfile}; -use drawcraft_geom::Point; -use drawcraft_tools::distort::liquify::{LiquifyParams, apply_stroke, dabs}; -use drawcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; -use drawcraft_tools::distort::{arap, collect_points, mesh_for, warp_node_with}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeId, NodeKind, WidthProfile}; +use vectorcraft_geom::Point; +use vectorcraft_tools::distort::liquify::{LiquifyParams, apply_stroke, dabs}; +use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; +use vectorcraft_tools::distort::{arap, collect_points, mesh_for, warp_node_with}; use super::edit::selected_roots; use super::*; diff --git a/crates/engine/src/cmd/docinfo.rs b/crates/engine/src/cmd/docinfo.rs index 3315ff40a..8d2cb49d4 100644 --- a/crates/engine/src/cmd/docinfo.rs +++ b/crates/engine/src/cmd/docinfo.rs @@ -2,10 +2,10 @@ use std::collections::{BTreeMap, BTreeSet}; -use drawcraft_color::Paint; -use drawcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind}; -use drawcraft_geom::{Affine, Rect}; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind}; +use vectorcraft_geom::{Affine, Rect}; use super::*; @@ -86,8 +86,8 @@ fn info(s: &mut Session, p: &Value) -> Result { } for item in &n.appearance.items { let paint = match item { - drawcraft_doc::AppearanceItem::Fill(f) => &f.paint, - drawcraft_doc::AppearanceItem::Stroke(s) => &s.paint, + vectorcraft_doc::AppearanceItem::Fill(f) => &f.paint, + vectorcraft_doc::AppearanceItem::Stroke(s) => &s.paint, }; if let Some(k) = paint_kind(paint) { *counts.entry(k).or_default() += 1; @@ -153,11 +153,11 @@ fn pixel_perfect(s: &mut Session, p: &Value) -> Result { // Whole-pixel stroke weights; odd ones centre on half pixels. let mut odd = false; for item in &mut node.appearance.items { - if let drawcraft_doc::AppearanceItem::Stroke(st) = item + if let vectorcraft_doc::AppearanceItem::Stroke(st) = item && st.width > 0.0 { st.width = st.width.round().max(1.0); - odd |= st.width as i64 % 2 == 1 && st.align == drawcraft_doc::StrokeAlign::Center; + odd |= st.width as i64 % 2 == 1 && st.align == vectorcraft_doc::StrokeAlign::Center; } } if let Some(b) = node.geometric_bounds() { diff --git a/crates/engine/src/cmd/docmenu.rs b/crates/engine/src/cmd/docmenu.rs index 4740cd048..fce823eae 100644 --- a/crates/engine/src/cmd/docmenu.rs +++ b/crates/engine/src/cmd/docmenu.rs @@ -5,8 +5,8 @@ use std::collections::BTreeSet; use std::sync::Arc; -use drawcraft_doc::{CharStyle, ColorMode, Guide, Node, NodeId, NodeKind, TextKind}; use serde_json::{Value, json}; +use vectorcraft_doc::{CharStyle, ColorMode, Guide, Node, NodeId, NodeKind, TextKind}; use super::colorcmds::{map_node_colors, to_cmyk}; use super::edit::selected_roots; @@ -344,7 +344,7 @@ fn make_guides(s: &mut Session, _: &Value) -> Result { Ok(json!({ "count": n })) } -fn guide_paths(d: &drawcraft_doc::Document) -> Vec { +fn guide_paths(d: &vectorcraft_doc::Document) -> Vec { let mut v = vec![]; d.walk(|n| { if matches!(n.kind, NodeKind::Path { guide: true, .. }) { @@ -456,11 +456,11 @@ fn color_mode(s: &mut Session, p: &Value) -> Result { if s.doc()?.doc.color_mode == mode { return ok(); } - let f = move |c: drawcraft_color::Color| match mode { + let f = move |c: vectorcraft_color::Color| match mode { ColorMode::Cmyk => to_cmyk(c), ColorMode::Rgb => { let [r, g, b] = c.to_rgb(); - drawcraft_color::Color::rgb(r, g, b) + vectorcraft_color::Color::rgb(r, g, b) } }; s.edit("Document Color Mode", |d, _| { @@ -470,7 +470,7 @@ fn color_mode(s: &mut Session, p: &Value) -> Result { map_node_colors(Arc::make_mut(l), &f, true, true); } for sw in &mut d.swatches { - if let drawcraft_color::Paint::Solid { color, .. } = &mut sw.paint { + if let vectorcraft_color::Paint::Solid { color, .. } = &mut sw.paint { *color = f(*color); } } diff --git a/crates/engine/src/cmd/draw2.rs b/crates/engine/src/cmd/draw2.rs index e257bf927..b9209bfb5 100644 --- a/crates/engine/src/cmd/draw2.rs +++ b/crates/engine/src/cmd/draw2.rs @@ -1,14 +1,14 @@ //! Commands backing the drawing tools (pencil, curvature, anchor tools, scissors, knife, eraser, //! blob brush, smooth, path eraser, join). -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; -use drawcraft_geom::hit::fill_contains; -use drawcraft_geom::{Anchor, AnchorKind, BezPath, FillRule, PathData, Point, Rect, SubPath, Vec2}; -use drawcraft_pathops as po; -use drawcraft_pathops::{BoolOp, Cap, Join, SimplifyOptions}; use kurbo::{ParamCurve, ParamCurveNearest}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; +use vectorcraft_geom::hit::fill_contains; +use vectorcraft_geom::{Anchor, AnchorKind, BezPath, FillRule, PathData, Point, Rect, SubPath, Vec2}; +use vectorcraft_pathops as po; +use vectorcraft_pathops::{BoolOp, Cap, Join, SimplifyOptions}; use super::create::add_node; use super::*; @@ -442,7 +442,7 @@ fn fit_freehand(pts: &[Point], tol: f64, closed: bool) -> SubPath { (kept[i], corner) }) .collect(); - let sp = drawcraft_tools::draw2::catmull_rom(&marked, closed); + let sp = vectorcraft_tools::draw2::catmull_rom(&marked, closed); if sp.anchors.len() < 3 { return sp; } @@ -527,7 +527,7 @@ fn curvature(s: &mut Session, p: &Value) -> Result { if pts.is_empty() { return Err(bad(C, "need at least one point")); } - let sp = drawcraft_tools::draw2::catmull_rom(&pts, bool_or(p, "closed", false)); + let sp = vectorcraft_tools::draw2::catmull_rom(&pts, bool_or(p, "closed", false)); if let Some(id) = id_param(p, "id") { s.edit("Curvature", |d, sel| { *path_mut(d, id)? = PathData::single(sp); diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 5b728a41a..885ce4987 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -2,9 +2,9 @@ use std::sync::Arc; -use drawcraft_doc::{Document, Node, NodeId, Unit}; -use drawcraft_geom::{Affine, Vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeId, Unit}; +use vectorcraft_geom::{Affine, Vec2}; use super::*; use crate::{EngineError, inspect}; @@ -64,12 +64,12 @@ fn file_new(s: &mut Session, p: &Value) -> Result { d.units = parse_unit(u).ok_or_else(|| bad("file.new", format!("unknown units `{u}`")))?; } if str_param(p, "colorMode").is_some_and(|m| m.eq_ignore_ascii_case("cmyk")) { - d.color_mode = drawcraft_doc::ColorMode::Cmyk; + d.color_mode = vectorcraft_doc::ColorMode::Cmyk; } let n = p.get("artboards").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000) as usize; for i in 1..n { - let r = drawcraft_geom::Rect::new(0.0, 0.0, w, h) + Vec2::new(i as f64 * (w + 20.0), 0.0); - d.artboards.push(drawcraft_doc::Artboard { + let r = vectorcraft_geom::Rect::new(0.0, 0.0, w, h) + Vec2::new(i as f64 * (w + 20.0), 0.0); + d.artboards.push(vectorcraft_doc::Artboard { id: (i + 1) as u32, name: format!("Artboard {}", i + 1), rect: r, @@ -152,7 +152,7 @@ pub(crate) fn selected_roots(s: &Session) -> Result> { .in_paint_order(&st.doc) .into_iter() .map(|id| match st.doc.parent_of(id).and_then(|p| st.doc.node(p).map(|n| (p, n))) { - Some((p, n)) if matches!(n.kind, drawcraft_doc::NodeKind::Compound { .. }) => p, + Some((p, n)) if matches!(n.kind, vectorcraft_doc::NodeKind::Compound { .. }) => p, _ => id, }) .collect(); @@ -226,7 +226,7 @@ fn paste(s: &mut Session, p: &Value, mode: PasteMode) -> Result { } _ => None, }; - let artboards: Vec = st.doc.artboards.iter().map(|a| a.rect).collect(); + let artboards: Vec = st.doc.artboards.iter().map(|a| a.rect).collect(); let dx = f64_or(p, "dx", off); let dy = f64_or(p, "dy", off); let label = match mode { @@ -274,7 +274,7 @@ fn duplicate(s: &mut Session, p: &Value) -> Result { } /// Duplicate `roots` directly above each original, transform the copies, select them. -pub(crate) fn duplicate_in(d: &mut Document, sel: &mut drawcraft_doc::Selection, roots: &[NodeId], xf: Affine) -> Result> { +pub(crate) fn duplicate_in(d: &mut Document, sel: &mut vectorcraft_doc::Selection, roots: &[NodeId], xf: Affine) -> Result> { let mut out = vec![]; for id in roots { let Some((par, idx, _)) = d.position(*id) else { continue }; diff --git a/crates/engine/src/cmd/effectcmd.rs b/crates/engine/src/cmd/effectcmd.rs index 4f8c52370..0584dc73e 100644 --- a/crates/engine/src/cmd/effectcmd.rs +++ b/crates/engine/src/cmd/effectcmd.rs @@ -1,14 +1,14 @@ //! Effect menu commands (live effects) and Object → Expand Appearance. //! //! Effects are stored on the object's appearance (`appearance.effects`) and evaluated at render -//! time by `drawcraft-effects` (re-exported by the renderer). +//! time by `vectorcraft-effects` (re-exported by the renderer). use std::sync::Arc; -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::{FillRule, PathData}; -use drawcraft_render::effects; use serde_json::{Value, json}; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::{FillRule, PathData}; +use vectorcraft_render::effects; use super::edit::selected_roots; use super::*; @@ -186,7 +186,7 @@ fn node_geometry(n: &Node) -> Option<(PathData, FillRule)> { } /// Bake the object-level geometry effects of `id` (and, recursively, of group members). -fn expand_node(d: &mut drawcraft_doc::Document, id: NodeId, out: &mut Vec) { +fn expand_node(d: &mut vectorcraft_doc::Document, id: NodeId, out: &mut Vec) { let Some(n) = d.node(id).cloned() else { return }; if let Some(children) = n.children() && matches!(n.kind, NodeKind::Group { .. }) @@ -240,8 +240,8 @@ fn expand_appearance(s: &mut Session, p: &Value) -> Result { #[cfg(test)] mod tests { - use drawcraft_doc::NodeKind; use serde_json::json; + use vectorcraft_doc::NodeKind; use crate::{NodeId, Session}; @@ -252,7 +252,7 @@ mod tests { (s, NodeId(r["id"].as_u64().unwrap())) } - fn node(s: &Session, id: NodeId) -> drawcraft_doc::Node { + fn node(s: &Session, id: NodeId) -> vectorcraft_doc::Node { s.doc().unwrap().doc.node(id).cloned().unwrap() } diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 06270361c..2ece33ad0 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -2,8 +2,8 @@ //! //! These live in the engine so every frontend (UI, CLI, MCP headless) shares them. -use drawcraft_doc::Document; use serde_json::{Value, json}; +use vectorcraft_doc::Document; use super::*; use crate::EngineError; @@ -15,13 +15,13 @@ pub fn specs() -> Vec { "Open Document", [], None, - "{path} or {name, dataBase64} → {index}; .drawcraft, .svg, .pdf, .ai (PDF-compatible)", + "{path} or {name, dataBase64} → {index}; .vectorcraft, .svg, .pdf, .ai (PDF-compatible)", always, open ), - cmd!("document.save", "Save Document", [], None, "{path?} native .drawcraft (default: the document's path) → {path}", has_doc, save), - cmd!(query "document.serialize", "Serialize Document", [], None, "{format: drawcraft|svg|pdf|png} → {dataBase64 | text}", has_doc, serialize), - cmd!("document.export", "Export Document", [], None, "{format: svg|png|pdf|drawcraft, path, scale?: 1, artboard?: 0}", has_doc, export), + cmd!("document.save", "Save Document", [], None, "{path?} native .vectorcraft (default: the document's path) → {path}", has_doc, save), + cmd!(query "document.serialize", "Serialize Document", [], None, "{format: vectorcraft|svg|pdf|png} → {dataBase64 | text}", has_doc, serialize), + cmd!("document.export", "Export Document", [], None, "{format: svg|png|pdf|vectorcraft, path, scale?: 1, artboard?: 0}", has_doc, export), cmd!( "document.exportForScreens", "Export for Screens", @@ -37,17 +37,18 @@ pub fn specs() -> Vec { fn load(name: &str, bytes: &[u8]) -> Result { let lower = name.to_ascii_lowercase(); - if lower.ends_with(".drawcraft") || drawcraft_format::sniff(bytes) { - return drawcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string())); + if vectorcraft_format::is_native_name(&lower) || vectorcraft_format::sniff(bytes) { + return vectorcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string())); } if lower.ends_with(".svg") || bytes.starts_with(b" Result { let (name, bytes, path) = match (str_param(p, "path"), str_param(p, "dataBase64")) { (Some(path), _) => (path.to_string(), std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?, Some(path.to_string())), (None, Some(b64)) => ( - str_param(p, "name").unwrap_or("untitled.drawcraft").to_string(), - drawcraft_format::base64_decode(b64).ok_or_else(|| bad("document.open", "bad base64"))?, + str_param(p, "name").unwrap_or("untitled.vectorcraft").to_string(), + vectorcraft_format::base64_decode(b64).ok_or_else(|| bad("document.open", "bad base64"))?, None, ), _ => return Err(bad("document.open", "give path or dataBase64")), }; let doc = load(&name, &bytes)?; - let native = name.to_ascii_lowercase().ends_with(".drawcraft"); + let native = vectorcraft_format::is_native_name(&name); let i = s.add_document(doc, if native { path } else { None }); Ok(json!({ "index": i })) } @@ -76,23 +77,23 @@ fn save(s: &mut Session, p: &Value) -> Result { .map(str::to_string) .or_else(|| s.active().and_then(|d| d.path.clone())) .ok_or_else(|| bad("document.save", "no path"))?; - let bytes = drawcraft_format::save_file(&s.doc()?.doc); + let bytes = vectorcraft_format::save_file(&s.doc()?.doc); std::fs::write(&path, bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?; let st = s.doc_mut()?; st.path = Some(path.clone()); - st.saved_revision = st.revision; + st.mark_saved(); Ok(json!({ "path": path })) } pub(crate) fn encode(s: &Session, format: &str, scale: f64, artboard: usize) -> Result> { let doc = &s.doc()?.doc; Ok(match format { - "drawcraft" => drawcraft_format::save_file(doc), - "svg" => drawcraft_svg::export(doc, &drawcraft_svg::ExportOptions { artboard: Some(artboard), ..Default::default() }).into_bytes(), - "pdf" => drawcraft_pdf::export(doc, &drawcraft_pdf::PdfOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?, + "vectorcraft" => vectorcraft_format::save_file(doc), + "svg" => vectorcraft_svg::export(doc, &vectorcraft_svg::ExportOptions { artboard: Some(artboard), ..Default::default() }).into_bytes(), + "pdf" => vectorcraft_pdf::export(doc, &vectorcraft_pdf::PdfOptions::default()).map_err(|e| EngineError::Other(e.to_string()))?, "png" | "jpg" | "jpeg" | "webp" => { let r = doc.artboards.get(artboard).map(|a| a.rect).ok_or_else(|| EngineError::Other("no such artboard".into()))?; - let img = drawcraft_render::Renderer::new().render_region(doc, r, scale.clamp(0.01, 64.0), format != "png" && format != "webp"); + let img = vectorcraft_render::Renderer::new().render_region(doc, r, scale.clamp(0.01, 64.0), format != "png" && format != "webp"); match format { "png" => img.to_png(), "webp" => img.to_webp(), @@ -104,12 +105,12 @@ pub(crate) fn encode(s: &Session, format: &str, scale: f64, artboard: usize) -> } fn serialize(s: &mut Session, p: &Value) -> Result { - let f = str_param(p, "format").unwrap_or("drawcraft"); + let f = str_param(p, "format").unwrap_or("vectorcraft"); let bytes = encode(s, f, f64_or(p, "scale", 1.0), p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize)?; if f == "svg" { return Ok(json!({ "text": String::from_utf8_lossy(&bytes) })); } - Ok(json!({ "dataBase64": drawcraft_format::base64_encode(&bytes) })) + Ok(json!({ "dataBase64": vectorcraft_format::base64_encode(&bytes) })) } fn export(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/fonts.rs b/crates/engine/src/cmd/fonts.rs index d12573124..3b87a7e32 100644 --- a/crates/engine/src/cmd/fonts.rs +++ b/crates/engine/src/cmd/fonts.rs @@ -3,8 +3,8 @@ use std::collections::BTreeMap; -use drawcraft_doc::{Document, NodeId, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeId, NodeKind}; use super::typecmd::refresh_bounds; use super::*; @@ -45,7 +45,7 @@ pub fn specs() -> Vec { fn scope(s: &Session, selection_only: bool) -> Result> { let st = s.doc()?; let mut v = vec![]; - let mut add = |n: &drawcraft_doc::Node| { + let mut add = |n: &vectorcraft_doc::Node| { n.walk(&mut |c| { if matches!(c.kind, NodeKind::Text(_)) && !v.contains(&c.id) { v.push(c.id); @@ -66,7 +66,7 @@ fn scope(s: &Session, selection_only: bool) -> Result> { Ok(v) } -fn text(d: &Document, id: NodeId) -> Option<&drawcraft_doc::TextObject> { +fn text(d: &Document, id: NodeId) -> Option<&vectorcraft_doc::TextObject> { match d.node(id).map(|n| &n.kind) { Some(NodeKind::Text(t)) => Some(t), _ => None, @@ -76,7 +76,7 @@ fn text(d: &Document, id: NodeId) -> Option<&drawcraft_doc::TextObject> { fn fonts(s: &mut Session, p: &Value) -> Result { let ids = scope(s, bool_or(p, "selectionOnly", false))?; let d = &s.doc()?.doc; - let db = drawcraft_text::FontDb::global(); + let db = vectorcraft_text::FontDb::global(); let families = db.families(); let mut found: BTreeMap<(String, String), (usize, Vec)> = BTreeMap::new(); for id in ids { @@ -110,13 +110,14 @@ fn replace(s: &mut Session, p: &Value) -> Result { const C: &str = "text.replaceFont"; let (ff, fs) = font_param(p, "from", C)?; let (tf, ts) = font_param(p, "to", C)?; - let db = drawcraft_text::FontDb::global(); + let db = vectorcraft_text::FontDb::global(); if !db.families().iter().any(|f| f.eq_ignore_ascii_case(&tf)) { return Err(bad(C, format!("font family `{tf}` is not available"))); } let ids = scope(s, bool_or(p, "selectionOnly", false))?; - let matches = - |st: &drawcraft_doc::CharStyle| st.font_family.eq_ignore_ascii_case(&ff) && fs.as_ref().is_none_or(|x| st.font_style.eq_ignore_ascii_case(x)); + let matches = |st: &vectorcraft_doc::CharStyle| { + st.font_family.eq_ignore_ascii_case(&ff) && fs.as_ref().is_none_or(|x| st.font_style.eq_ignore_ascii_case(x)) + }; let n = s.edit("Replace Font", |d, _| { let mut n = 0; for id in &ids { @@ -133,7 +134,7 @@ fn replace(s: &mut Session, p: &Value) -> Result { } } if changed { - drawcraft_text::edit::normalize(&mut t.runs); + vectorcraft_text::edit::normalize(&mut t.runs); refresh_bounds(t); } } @@ -186,3 +187,44 @@ mod tests { let _ = a; } } + +#[cfg(test)] +mod open_tests { + use super::*; + + #[test] + fn opened_documents_have_exact_text_bounds() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 200})).unwrap(); + let id = s.execute("text.create", &json!({"x": 10, "y": 60, "text": "WIDE TRACKED", "size": 40})).unwrap()["id"].as_u64().unwrap(); + s.execute("text.setStyle", &json!({"tracking": 400})).unwrap(); + let want = s.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap(); + // Save and reopen: the cache isn't in the file, yet the bounds must match. + let bytes = vectorcraft_format::save(&s.doc().unwrap().doc, false); + let reopened = vectorcraft_format::load(&bytes).unwrap(); + let mut t = Session::new(); + t.add_document(reopened, None); + let got = t.doc().unwrap().doc.node(NodeId(id)).unwrap().geometric_bounds().unwrap(); + assert!((got.width() - want.width()).abs() < 1e-6, "{got:?} vs {want:?}"); + } +} + +#[cfg(test)] +mod dirty_tests { + use super::*; + + #[test] + fn selection_never_marks_modified_and_undo_to_saved_is_clean() { + let mut s = Session::new(); + s.add_document(Document::new(100.0, 100.0), None); + let id = s.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); + s.doc_mut().unwrap().mark_saved(); + s.execute("select.none", &json!({})).unwrap(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + assert!(!s.doc().unwrap().is_dirty(), "selecting is not an edit"); + s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap(); + assert!(s.doc().unwrap().is_dirty()); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(!s.doc().unwrap().is_dirty(), "back to the saved state"); + } +} diff --git a/crates/engine/src/cmd/layer.rs b/crates/engine/src/cmd/layer.rs index 8ec74d365..c4b7f41ca 100644 --- a/crates/engine/src/cmd/layer.rs +++ b/crates/engine/src/cmd/layer.rs @@ -1,8 +1,8 @@ //! Layers panel, artboards. -use drawcraft_doc::{Artboard, LayerColor, NodeId, NodeKind}; -use drawcraft_geom::Rect; use serde_json::{Value, json}; +use vectorcraft_doc::{Artboard, LayerColor, NodeId, NodeKind}; +use vectorcraft_geom::Rect; use super::*; use crate::EngineError; @@ -73,7 +73,7 @@ fn new_sublayer(s: &mut Session, p: &Value) -> Result { let id = d.alloc_id(); let color = LayerColor::Preset(((d.layers.len() + n + 1) % 27) as u8); let nm = name.unwrap_or_else(|| format!("Layer {}", d.node_count())); - d.insert(Some(parent), usize::MAX, drawcraft_doc::Node::layer(id, &nm, color))?; + d.insert(Some(parent), usize::MAX, vectorcraft_doc::Node::layer(id, &nm, color))?; Ok(id) })?; Ok(json!({ "id": id.0 })) diff --git a/crates/engine/src/cmd/live.rs b/crates/engine/src/cmd/live.rs index 41a0de821..42ac95429 100644 --- a/crates/engine/src/cmd/live.rs +++ b/crates/engine/src/cmd/live.rs @@ -2,16 +2,16 @@ //! (Object → Create Gradient Mesh, Mesh tool edits). //! //! The objects are live document nodes (`NodeKind::Blend`, `NodeKind::Envelope`, -//! `NodeKind::Mesh`); evaluation lives in `drawcraft_doc::live` and the renderer. **Expand** -//! replaces a live object by its evaluated geometry (`drawcraft_render::expand_live`). +//! `NodeKind::Mesh`); evaluation lives in `vectorcraft_doc::live` and the renderer. **Expand** +//! replaces a live object by its evaluated geometry (`vectorcraft_render::expand_live`). use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::live::{self, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshAppearance, Spine}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; -use drawcraft_geom::{Affine, PathData, Point}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::live::{self, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshAppearance, Spine}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, Selection}; +use vectorcraft_geom::{Affine, PathData, Point}; use super::edit::selected_roots; use super::*; @@ -421,7 +421,7 @@ fn blend_expand(s: &mut Session, _: &Value) -> Result { let mut out = vec![]; for b in &blends { let Some(n) = d.node(*b).cloned() else { continue }; - let children = drawcraft_render::expand_live(&n).into_iter().map(Arc::new).collect(); + let children = vectorcraft_render::expand_live(&n).into_iter().map(Arc::new).collect(); let mut g = Node::new(n.id, NodeKind::Group { children, clip: false }); g.opacity = n.opacity; g.blend = n.blend; @@ -551,7 +551,7 @@ fn env_make_mesh(s: &mut Session, p: &Value) -> Result { let src = roots .iter() .filter_map(|id| st.doc.node(*id)) - .fold(None, |a, n| drawcraft_geom::union_opt(a, n.geometric_bounds())) + .fold(None, |a, n| vectorcraft_geom::union_opt(a, n.geometric_bounds())) .ok_or_else(|| bad(C, "selection has no bounds"))?; let kind = EnvelopeKind::Mesh { rows: rows as u32, cols: cols as u32, points: live::grid_points(src, rows as u32, cols as u32) }; let id = s.edit("Make Envelope", |d, sel| wrap(d, sel, &roots, |id, content| envelope(id, content, kind)))?; @@ -648,7 +648,7 @@ fn env_expand(s: &mut Session, _: &Value) -> Result { let mut out = vec![]; for e in &envs { let Some(n) = d.node(*e).cloned() else { continue }; - let children = drawcraft_render::expand_live(&n).into_iter().map(Arc::new).collect(); + let children = vectorcraft_render::expand_live(&n).into_iter().map(Arc::new).collect(); let mut g = Node::new(n.id, NodeKind::Group { children, clip: false }); g.opacity = n.opacity; g.blend = n.blend; diff --git a/crates/engine/src/cmd/menucmds.rs b/crates/engine/src/cmd/menucmds.rs index c9963e19d..8ab2e6901 100644 --- a/crates/engine/src/cmd/menucmds.rs +++ b/crates/engine/src/cmd/menucmds.rs @@ -4,10 +4,10 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, ImageObject, LiveShape, Node, NodeId, NodeKind}; -use drawcraft_geom::{Affine, Anchor, PathData, Point, Rect, Vec2, shapes}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, ImageObject, LiveShape, Node, NodeId, NodeKind}; +use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, Vec2, shapes}; use super::edit::{duplicate_in, selected_roots}; use super::*; @@ -79,7 +79,7 @@ pub fn specs() -> Vec { "Reset Bounding Box", ["Object", "Transform"], None, - "{} no-op: DrawCraft bounding boxes are always axis-aligned to the document → {changed: 0}", + "{} no-op: VectorCraft bounding boxes are always axis-aligned to the document → {changed: 0}", has_selection, |_, _| Ok(json!({ "changed": 0 })) ), @@ -277,7 +277,7 @@ fn transform_each(s: &mut Session, p: &Value) -> Result { k_mv = mv * (rng.next() * 2.0 - 1.0); k_rot = rot * (rng.next() * 2.0 - 1.0); } - let o = drawcraft_geom::reference_point(b, refi); + let o = vectorcraft_geom::reference_point(b, refi); let refl = Affine::scale_non_uniform(if ry { -1.0 } else { 1.0 }, if rx { -1.0 } else { 1.0 }); let m = Affine::translate((k_mh, k_mv)) * about(o, Affine::rotate(-k_rot.to_radians()) * refl * Affine::scale_non_uniform(k_sh / 100.0, k_sv / 100.0)); @@ -358,9 +358,9 @@ fn drop_live(n: &mut Node) { const MAX_PIXELS: f64 = 64.0e6; /// A copy of `doc` whose only content is `nodes` (paint order), for offscreen rendering. -fn isolated_doc(doc: &drawcraft_doc::Document, nodes: Vec) -> drawcraft_doc::Document { +fn isolated_doc(doc: &vectorcraft_doc::Document, nodes: Vec) -> vectorcraft_doc::Document { let mut tmp = doc.clone(); - let mut layer = Node::layer(NodeId(u64::MAX), "raster", drawcraft_doc::LayerColor::Preset(0)); + let mut layer = Node::layer(NodeId(u64::MAX), "raster", vectorcraft_doc::LayerColor::Preset(0)); if let Some(ch) = layer.children_mut() { *ch = nodes.into_iter().map(Arc::new).collect(); } @@ -368,7 +368,7 @@ fn isolated_doc(doc: &drawcraft_doc::Document, nodes: Vec) -> drawcraft_do tmp } -fn unique_key(d: &drawcraft_doc::Document, stem: &str) -> String { +fn unique_key(d: &vectorcraft_doc::Document, stem: &str) -> String { let mut i = d.images.len() + 1; loop { let k = format!("{stem}-{i}"); @@ -379,11 +379,11 @@ fn unique_key(d: &drawcraft_doc::Document, stem: &str) -> String { } } -fn render_png(doc: &drawcraft_doc::Document, region: Rect, scale: f64, white: bool) -> Result<(Vec, u32, u32)> { +fn render_png(doc: &vectorcraft_doc::Document, region: Rect, scale: f64, white: bool) -> Result<(Vec, u32, u32)> { if region.width() * region.height() * scale * scale > MAX_PIXELS || region.width() * scale > 65535.0 || region.height() * scale > 65535.0 { return Err(EngineError::Other("image would be too large; lower the resolution".into())); } - let mut r = drawcraft_render::Renderer::new(); + let mut r = vectorcraft_render::Renderer::new(); let img = r.render_region(doc, region, scale, white); Ok((img.to_png(), img.width, img.height)) } @@ -416,7 +416,7 @@ fn rasterize(s: &mut Session, p: &Value) -> Result { let id = s.edit("Rasterize", |d, sel| { let (par, idx, _) = d.position(top).ok_or(EngineError::NoNode(top))?; let key = unique_key(d, "raster"); - d.images.insert(key.clone(), drawcraft_doc::ImageBlob { mime: "image/png".into(), bytes: Arc::new(png) }); + d.images.insert(key.clone(), vectorcraft_doc::ImageBlob { mime: "image/png".into(), bytes: Arc::new(png) }); let id = d.alloc_id(); let xf = Affine::translate(region.origin().to_vec2()) * Affine::scale(1.0 / scale); let node = Node::new(id, NodeKind::Image(ImageObject { key, width: w, height: h, xf, link: None })); @@ -489,7 +489,7 @@ fn crop_image(s: &mut Session, p: &Value) -> Result { let (png, w, h) = render_png(&tmp, region, scale, false)?; s.edit("Crop Image", |d, _| { let key = unique_key(d, "crop"); - d.images.insert(key.clone(), drawcraft_doc::ImageBlob { mime: "image/png".into(), bytes: Arc::new(png) }); + d.images.insert(key.clone(), vectorcraft_doc::ImageBlob { mime: "image/png".into(), bytes: Arc::new(png) }); if let Some(n) = d.node_mut(id) { n.kind = NodeKind::Image(ImageObject { key, @@ -636,7 +636,7 @@ fn convert_to_artboards(s: &mut Session, _: &Value) -> Result { continue; } let aid = d.next_artboard_id(); - d.artboards.push(drawcraft_doc::Artboard { + d.artboards.push(vectorcraft_doc::Artboard { id: aid, name: format!("Artboard {aid}"), rect: b, diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index c0e1b2347..5e947f56d 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -30,16 +30,16 @@ mod select; mod textedit; pub mod textstyles; pub(crate) mod threads; -mod typecmd; +pub(crate) mod typecmd; mod typemenu; pub mod wand; mod xform; -use drawcraft_color::Color; -use drawcraft_doc::NodeId; -use drawcraft_geom::{Affine, Point}; use serde::Serialize; use serde_json::Value; +use vectorcraft_color::Color; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::{Affine, Point}; use crate::{EngineError, Result, Session}; diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 4067b9c2a..585a2f482 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -3,10 +3,10 @@ use std::sync::Arc; -use drawcraft_color::{BlendMode, Paint}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind}; -use drawcraft_geom::{Affine, FillRule, Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_color::{BlendMode, Paint}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind}; +use vectorcraft_geom::{Affine, FillRule, Point, Rect, Vec2}; use super::edit::{duplicate_in, selected_roots}; use super::*; @@ -270,7 +270,7 @@ fn arrange(s: &mut Session, how: Arrange) -> Result { }; s.edit(label, |d, _| { // Arrange within each parent independently (selections may span layers/groups). - let mut by_parent: Vec<(Option, Vec)> = vec![]; + let mut by_parent: Vec<(Option, Vec)> = vec![]; for id in &ids { let par = d.parent_of(*id); match by_parent.iter_mut().find(|(p, _)| *p == par) { @@ -746,7 +746,7 @@ fn set_bounds(s: &mut Session, p: &Value) -> Result { let ids = selected_roots(s)?; let b = s.doc()?.doc.bounds_of(&ids, false).ok_or_else(|| EngineError::Other("no bounds".into()))?; let refi = p.get("reference").and_then(Value::as_u64).unwrap_or(4) as usize; - let rp = drawcraft_geom::reference_point(b, refi); + let rp = vectorcraft_geom::reference_point(b, refi); let mut w = f64_or(p, "width", b.width()); let mut h = f64_or(p, "height", b.height()); if bool_or(p, "proportional", false) { @@ -784,12 +784,12 @@ fn set_live_shape(s: &mut Session, p: &Value) -> Result { for id in &ids { let Some(NodeKind::Path { path, live: Some(live), .. }) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; match live { - drawcraft_doc::LiveShape::Rectangle { radii, .. } => { + vectorcraft_doc::LiveShape::Rectangle { radii, .. } => { if let Some(r) = p.get("radius").and_then(Value::as_f64) { *radii = [r.max(0.0); 4]; } } - drawcraft_doc::LiveShape::Polygon { sides, .. } => { + vectorcraft_doc::LiveShape::Polygon { sides, .. } => { if let Some(n) = p.get("sides").and_then(Value::as_u64) { *sides = n.clamp(3, 1000) as u32; } diff --git a/crates/engine/src/cmd/opacitymask.rs b/crates/engine/src/cmd/opacitymask.rs index 06ceabf36..072b3bad6 100644 --- a/crates/engine/src/cmd/opacitymask.rs +++ b/crates/engine/src/cmd/opacitymask.rs @@ -1,10 +1,10 @@ //! Opacity masks (Transparency panel): make, release, enable/disable, link/unlink, clip, invert. //! -//! The mask art is stored on the masked object ([`drawcraft_doc::OpacityMask`]), outside the +//! The mask art is stored on the masked object ([`vectorcraft_doc::OpacityMask`]), outside the //! layer tree, so it is never hit-tested or selected. Its luminance sets the object's opacity. -use drawcraft_doc::{Node, NodeId, OpacityMask}; use serde_json::{Value, json}; +use vectorcraft_doc::{Node, NodeId, OpacityMask}; use super::edit::selected_roots; use super::*; @@ -306,8 +306,8 @@ mod tests { select(&mut s, &[a, b]); s.execute("transparency.makeOpacityMask", &json!({"invert": true})).unwrap(); let d = s.doc().unwrap().doc.clone(); - let bytes = drawcraft_format::save(&d, false); - let back = drawcraft_format::load(&bytes).unwrap(); + let bytes = vectorcraft_format::save(&d, false); + let back = vectorcraft_format::load(&bytes).unwrap(); assert_eq!(back.node(NodeId(a)).unwrap().mask, d.node(NodeId(a)).unwrap().mask); } } diff --git a/crates/engine/src/cmd/paint.rs b/crates/engine/src/cmd/paint.rs index b4f42b0f9..4aecee2da 100644 --- a/crates/engine/src/cmd/paint.rs +++ b/crates/engine/src/cmd/paint.rs @@ -1,8 +1,8 @@ //! Fill/stroke, Stroke panel, Appearance, Transparency, Swatches, Graphic Styles. -use drawcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, Paint}; -use drawcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, FillLayer, LineCap, LineJoin, NodeKind, StrokeAlign, StrokeLayer}; use serde_json::{Value, json}; +use vectorcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, Paint}; +use vectorcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, FillLayer, LineCap, LineJoin, NodeKind, StrokeAlign, StrokeLayer}; use super::*; use crate::EngineError; @@ -78,7 +78,7 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result> { let st = s.doc()?; // A pattern definition works as its swatch even without a swatch entry. if st.doc.swatch(name).is_none() && st.doc.pattern(name).is_some() { - return Ok(Some(drawcraft_doc::pattern::pattern_paint(name))); + return Ok(Some(vectorcraft_doc::pattern::pattern_paint(name))); } let sw = st.doc.swatch(name).ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?; let mut paint = sw.paint.clone(); @@ -99,7 +99,7 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result> { grad.stops = stops .iter() .filter_map(|st| { - Some(drawcraft_color::GradientStop { + Some(vectorcraft_color::GradientStop { offset: st.get("offset")?.as_f64()? as f32, color: color_value(st.get("color")?)?, opacity: st.get("opacity").and_then(Value::as_f64).unwrap_or(1.0) as f32, @@ -121,7 +121,7 @@ pub(crate) fn paint_from(s: &Session, p: &Value) -> Result> { } /// Leaf objects whose appearance should change for `ids` (groups apply to children). -fn paint_targets(s: &Session, p: &Value) -> Result> { +fn paint_targets(s: &Session, p: &Value) -> Result> { let ids = targets(s, p)?; let d = &s.doc()?.doc; let mut out = vec![]; @@ -290,9 +290,9 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result { } match str_param(p, "profile") { Some("uniform") => st.profile = None, - Some("lens") => st.profile = Some(drawcraft_doc::WidthProfile::lens()), - Some("taperStart") => st.profile = Some(drawcraft_doc::WidthProfile::taper_start()), - Some("taperEnd") => st.profile = Some(drawcraft_doc::WidthProfile::taper_end()), + Some("lens") => st.profile = Some(vectorcraft_doc::WidthProfile::lens()), + Some("taperStart") => st.profile = Some(vectorcraft_doc::WidthProfile::taper_start()), + Some("taperEnd") => st.profile = Some(vectorcraft_doc::WidthProfile::taper_end()), _ => {} } } @@ -412,7 +412,7 @@ fn add_effect(s: &mut Session, p: &Value) -> Result { s.edit("Add Effect", |d, _| { for nid in &ids { if let Some(n) = d.node_mut(*nid) { - n.appearance.effects.push(drawcraft_doc::Effect { id: id.clone(), params: params.clone(), visible: true }); + n.appearance.effects.push(vectorcraft_doc::Effect { id: id.clone(), params: params.clone(), visible: true }); } } Ok(()) @@ -456,7 +456,7 @@ fn style_new(s: &mut Session, p: &Value) -> Result { let ap = st.doc.node(id).map(|n| n.appearance.clone()).unwrap_or_default(); let name = str_param(p, "name").map(str::to_string).unwrap_or_else(|| format!("Graphic Style {}", st.doc.graphic_styles.len() + 1)); s.edit("New Graphic Style", |d, _| { - d.graphic_styles.push(drawcraft_doc::GraphicStyle { name: name.clone(), appearance: ap }); + d.graphic_styles.push(vectorcraft_doc::GraphicStyle { name: name.clone(), appearance: ap }); Ok(()) })?; Ok(json!({ "name": name })) @@ -479,7 +479,7 @@ fn swatch_new(s: &mut Session, p: &Value) -> Result { }; let global = bool_or(p, "global", false); s.edit("New Swatch", |d, _| { - d.swatches.push(drawcraft_color::Swatch { name: name.clone(), paint, global, spot: false }); + d.swatches.push(vectorcraft_color::Swatch { name: name.clone(), paint, global, spot: false }); Ok(()) })?; Ok(json!({ "name": name })) diff --git a/crates/engine/src/cmd/panelcmds.rs b/crates/engine/src/cmd/panelcmds.rs index dde0509e6..f2e1c90bc 100644 --- a/crates/engine/src/cmd/panelcmds.rs +++ b/crates/engine/src/cmd/panelcmds.rs @@ -2,9 +2,9 @@ //! appearance item duplicate/reorder, artboard reorder/duplicate, swatch groups, graphic style //! management and the advanced Character/Paragraph attributes. -use drawcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop, Paint, SwatchGroup}; -use drawcraft_doc::{Document, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_color::{Gradient, GradientKind, GradientPaint, GradientStop, Paint, SwatchGroup}; +use vectorcraft_doc::{Document, NodeKind}; use super::*; use crate::EngineError; @@ -90,7 +90,7 @@ fn leaf_targets(s: &Session, p: &Value) -> Result> { } /// Apply the gradient edits in `p` to `paint` (pure; unit-tested). -pub(crate) fn apply_gradient_edit(paint: &Paint, p: &Value, bounds: Option) -> std::result::Result { +pub(crate) fn apply_gradient_edit(paint: &Paint, p: &Value, bounds: Option) -> std::result::Result { let mut gp = match paint { Paint::Gradient(g) => (**g).clone(), _ => GradientPaint::new(Gradient::default()), @@ -143,7 +143,7 @@ pub(crate) fn apply_gradient_edit(paint: &Paint, p: &Value, bounds: Option Result { a.id = d.artboards.iter().map(|a| a.id).max().unwrap_or(0) + 1; a.name = format!("{} copy", src.name); let dx = right + 20.0 - src.rect.x0; - a.rect = drawcraft_geom::Rect::new(src.rect.x0 + dx, src.rect.y0, src.rect.x1 + dx, src.rect.y1); + a.rect = vectorcraft_geom::Rect::new(src.rect.x0 + dx, src.rect.y0, src.rect.x1 + dx, src.rect.y1); d.artboards.push(a); Ok(d.artboards.len() - 1) })?; @@ -311,7 +311,7 @@ fn unique_name(d: &Document, base: &str) -> String { fn swatch_new_group(s: &mut Session, p: &Value) -> Result { let names: Vec = p.get("swatches").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default(); - let colors: Vec = + let colors: Vec = p.get("colors").and_then(Value::as_array).map(|a| a.iter().filter_map(color_value).collect()).unwrap_or_default(); let requested = str_param(p, "name").map(str::to_string); let name = s.edit("New Color Group", |d, _| { @@ -325,7 +325,7 @@ fn swatch_new_group(s: &mut Session, p: &Value) -> Result { for c in &colors { let [r, g, b] = c.to_rgb(); let nm = format!("R={} G={} B={}", (r * 255.0).round(), (g * 255.0).round(), (b * 255.0).round()); - group.swatches.push(drawcraft_color::Swatch { name: nm, paint: Paint::solid(*c), global: false, spot: false }); + group.swatches.push(vectorcraft_color::Swatch { name: nm, paint: Paint::solid(*c), global: false, spot: false }); } d.swatch_groups.push(group); Ok(name) @@ -338,7 +338,7 @@ fn swatch_duplicate(s: &mut Session, p: &Value) -> Result { let new = s.edit("Duplicate Swatch", |d, _| { let src = d.swatch(&name).cloned().ok_or_else(|| EngineError::Other(format!("no swatch `{name}`")))?; let nm = unique_name(d, &format!("{name} copy")); - let copy = drawcraft_color::Swatch { name: nm.clone(), ..src }; + let copy = vectorcraft_color::Swatch { name: nm.clone(), ..src }; if let Some(pos) = d.swatches.iter().position(|sw| sw.name == name) { d.swatches.insert(pos + 1, copy); } else if let Some(g) = d.swatch_groups.iter_mut().find(|g| g.swatches.iter().any(|sw| sw.name == name)) { @@ -353,7 +353,7 @@ fn swatch_duplicate(s: &mut Session, p: &Value) -> Result { fn swatch_sort(s: &mut Session, _: &Value) -> Result { s.edit("Sort Swatches", |d, _| { // [None] and other bracketed specials stay first, like Illustrator. - let key = |sw: &drawcraft_color::Swatch| (!sw.name.starts_with('['), sw.name.to_lowercase()); + let key = |sw: &vectorcraft_color::Swatch| (!sw.name.starts_with('['), sw.name.to_lowercase()); d.swatches.sort_by_key(key); for g in &mut d.swatch_groups { g.swatches.sort_by_key(key); @@ -491,11 +491,11 @@ fn set_format(s: &mut Session, p: &Value) -> Result { // ---------- stroke extras ---------- /// Mirror a width profile along the path (t → 1 − t) or across it (swap left/right widths). -pub(crate) fn flip_profile(p: &drawcraft_doc::WidthProfile, along: bool) -> drawcraft_doc::WidthProfile { +pub(crate) fn flip_profile(p: &vectorcraft_doc::WidthProfile, along: bool) -> vectorcraft_doc::WidthProfile { let mut pts: Vec<(f64, f64, f64)> = if along { p.points.iter().map(|&(t, l, r)| (1.0 - t, l, r)).collect() } else { p.points.iter().map(|&(t, l, r)| (t, r, l)).collect() }; pts.sort_by(|a, b| a.0.total_cmp(&b.0)); - drawcraft_doc::WidthProfile { points: pts } + vectorcraft_doc::WidthProfile { points: pts } } fn stroke_advanced(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/path.rs b/crates/engine/src/cmd/path.rs index 91c6fa5be..1d8cedde9 100644 --- a/crates/engine/src/cmd/path.rs +++ b/crates/engine/src/cmd/path.rs @@ -2,9 +2,9 @@ use std::collections::BTreeSet; -use drawcraft_doc::{NodeId, NodeKind}; -use drawcraft_geom::{Anchor, AnchorKind, PathData, Point, SubPath, Vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::{Anchor, AnchorKind, PathData, Point, SubPath, Vec2}; use super::create::anchor_from_json; use super::*; @@ -91,7 +91,7 @@ pub fn specs() -> Vec { ] } -fn path_mut(d: &mut drawcraft_doc::Document, id: NodeId) -> Result<&mut PathData> { +fn path_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Result<&mut PathData> { let n = d.node_mut(id).ok_or(EngineError::NoNode(id))?; match &mut n.kind { NodeKind::Path { path, live, .. } => { @@ -160,7 +160,7 @@ pub(crate) fn move_anchors(s: &mut Session, p: &Value) -> Result { } None => { if let Some(n) = doc.node_mut(*id) { - n.transform(drawcraft_geom::Affine::translate(d), false); + n.transform(vectorcraft_geom::Affine::translate(d), false); } } } diff --git a/crates/engine/src/cmd/pathops.rs b/crates/engine/src/cmd/pathops.rs index 8eaeb9074..3657d6d56 100644 --- a/crates/engine/src/cmd/pathops.rs +++ b/crates/engine/src/cmd/pathops.rs @@ -1,16 +1,16 @@ //! Pathfinder panel (`object.pathfinder.*`) and Object → Path commands backed by -//! `drawcraft-pathops`: Offset Path, Outline Stroke, Simplify, Add Anchor Points, Split Into Grid, +//! `vectorcraft-pathops`: Offset Path, Outline Stroke, Simplify, Add Anchor Points, Split Into Grid, //! Clean Up and Divide Objects Below. use std::sync::Arc; -use drawcraft_color::{BlendMode, Paint}; -use drawcraft_doc::appearance::{AppearanceItem, LineCap, LineJoin, StrokeAlign, StrokeLayer}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, TextObject}; -use drawcraft_geom::{FillRule, PathData}; -use drawcraft_pathops as po; -use drawcraft_pathops::{BoolOp, PathfinderOp}; use serde_json::{Value, json}; +use vectorcraft_color::{BlendMode, Paint}; +use vectorcraft_doc::appearance::{AppearanceItem, LineCap, LineJoin, StrokeAlign, StrokeLayer}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind, TextObject}; +use vectorcraft_geom::{FillRule, PathData}; +use vectorcraft_pathops as po; +use vectorcraft_pathops::{BoolOp, PathfinderOp}; use super::edit::selected_roots; use super::*; @@ -223,13 +223,13 @@ pub(crate) fn node_path(n: &Node) -> Option<(PathData, FillRule)> { /// Glyph outlines of a text object in document coordinates. pub(crate) fn text_outline(t: &TextObject) -> PathData { - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); PathData::from_bezpath(&lay.to_bezpath()).transformed(t.xf) } /// Appearance a text object's outlines take: the first run's fill and stroke. pub(crate) fn text_style_appearance(fill: &Paint, stroke: &Paint, width: f64) -> Appearance { - let mut a = Appearance { items: vec![AppearanceItem::Fill(drawcraft_doc::appearance::FillLayer::new(fill.clone()))], effects: vec![] }; + let mut a = Appearance { items: vec![AppearanceItem::Fill(vectorcraft_doc::appearance::FillLayer::new(fill.clone()))], effects: vec![] }; if !stroke.is_none() && width > 0.0 { a.items.push(AppearanceItem::Stroke(StrokeLayer::new(stroke.clone(), width))); } @@ -504,7 +504,7 @@ fn outline_stroke(s: &mut Session, _: &Value) -> Result { } let mut stroke_node = shape_node(d, outline, Some(&l)); stroke_node.appearance = - Appearance { items: vec![AppearanceItem::Fill(drawcraft_doc::appearance::FillLayer::new(st.paint.clone()))], effects: vec![] }; + Appearance { items: vec![AppearanceItem::Fill(vectorcraft_doc::appearance::FillLayer::new(st.paint.clone()))], effects: vec![] }; let fill = l.appearance.fill().filter(|f| f.visible && !f.paint.is_none()).cloned(); let new = if let Some(f) = fill { let mut fill_node = l.clone(); diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index a0c2b316c..35b0e9b71 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -7,15 +7,15 @@ //! `object.pattern.cancel` restores the previous definition. Both remove the layer. //! //! **Repeat** objects are live nodes (`NodeKind::Repeat`) holding the source art and the -//! arrangement; `drawcraft_doc::pattern` evaluates the instances. +//! arrangement; `vectorcraft_doc::pattern` evaluates the instances. use std::sync::Arc; -use drawcraft_color::Paint; -use drawcraft_doc::pattern::{self, Overlap, PATTERN_EDIT_LAYER, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType, pattern_paint}; -use drawcraft_doc::{Document, Node, NodeId, NodeKind, Selection}; -use drawcraft_geom::Rect; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::pattern::{self, Overlap, PATTERN_EDIT_LAYER, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType, pattern_paint}; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind, Selection}; +use vectorcraft_geom::Rect; use super::edit::selected_roots; use super::*; @@ -300,7 +300,7 @@ fn pattern_make(s: &mut Session, p: &Value) -> Result { let c = def.tile.center(); def.tile = Rect::from_center_size(c, (w.unwrap_or(def.tile.width()).max(1e-3), h.unwrap_or(def.tile.height()).max(1e-3))); d.patterns.push(def); - d.swatches.push(drawcraft_color::Swatch { name: name.clone(), paint: pattern_paint(&name), global: false, spot: false }); + d.swatches.push(vectorcraft_color::Swatch { name: name.clone(), paint: pattern_paint(&name), global: false, spot: false }); if edit { enter_edit(d, sel, &name, None)?; } @@ -326,8 +326,8 @@ fn pattern_edit(s: &mut Session, p: &Value) -> Result { .filter_map(|id| st.doc.node(*id)) .find_map(|n| { n.appearance.items.iter().find_map(|it| match it { - drawcraft_doc::AppearanceItem::Fill(f) => pattern_name(&f.paint), - drawcraft_doc::AppearanceItem::Stroke(sk) => pattern_name(&sk.paint), + vectorcraft_doc::AppearanceItem::Fill(f) => pattern_name(&f.paint), + vectorcraft_doc::AppearanceItem::Stroke(sk) => pattern_name(&sk.paint), }) }) .ok_or_else(|| bad(C, "give a pattern name or select an object painted with a pattern"))? @@ -394,7 +394,7 @@ fn pattern_save_copy(s: &mut Session, p: &Value) -> Result { def.fit_tile_to_art(); } d.patterns.push(def); - d.swatches.push(drawcraft_color::Swatch { name: name.clone(), paint: pattern_paint(&name), global: false, spot: false }); + d.swatches.push(vectorcraft_color::Swatch { name: name.clone(), paint: pattern_paint(&name), global: false, spot: false }); Ok(name) })?; Ok(json!({ "name": name })) @@ -496,8 +496,8 @@ fn rename_pattern_uses(d: &mut Document, old: &str, new: &str) { if let Some(n) = d.node_mut(id) { for it in n.appearance.items.iter_mut() { match it { - drawcraft_doc::AppearanceItem::Fill(f) => fix(&mut f.paint), - drawcraft_doc::AppearanceItem::Stroke(s) => fix(&mut s.paint), + vectorcraft_doc::AppearanceItem::Fill(f) => fix(&mut f.paint), + vectorcraft_doc::AppearanceItem::Stroke(s) => fix(&mut s.paint), } } } @@ -525,7 +525,7 @@ fn pattern_delete(s: &mut Session, p: &Value) -> Result { } let n = s.edit("Delete Pattern", |d, _| { d.patterns.retain(|p| p.name != name); - let is_it = |sw: &drawcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == name); + let is_it = |sw: &vectorcraft_color::Swatch| matches!(&sw.paint, Paint::Pattern { pattern, .. } if *pattern == name); d.swatches.retain(|sw| !is_it(sw)); for g in &mut d.swatch_groups { g.swatches.retain(|sw| !is_it(sw)); @@ -535,8 +535,8 @@ fn pattern_delete(s: &mut Session, p: &Value) -> Result { if let Some(n) = d.node_mut(*id) { for it in n.appearance.items.iter_mut() { let p = match it { - drawcraft_doc::AppearanceItem::Fill(f) => &mut f.paint, - drawcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint, + vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint, + vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint, }; if matches!(p, Paint::Pattern { pattern, .. } if *pattern == name) { *p = Paint::None; @@ -657,7 +657,7 @@ fn repeat_grid(s: &mut Session, p: &Value) -> Result { let rows = p.get("rows").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32); let cols = p.get("cols").and_then(Value::as_u64).map(|v| v.clamp(1, 500) as u32); make_repeat(s, p, "Grid Repeat", |src| { - let b = drawcraft_doc::live::nodes_bounds(&src).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); + let b = vectorcraft_doc::live::nodes_bounds(&src).unwrap_or(Rect::new(0.0, 0.0, 10.0, 10.0)); let mut r = RepeatSpec::grid(src, hs.unwrap_or(b.width() / 4.0), vs.unwrap_or(b.height() / 4.0)); if let RepeatKind::Grid { rows: rr, cols: cc, .. } = &mut r.kind { *rr = rows.unwrap_or(*rr); @@ -709,7 +709,7 @@ fn repeat_expand(s: &mut Session, _: &Value) -> Result { let mut out = vec![]; for r in &reps { let Some(n) = d.node(*r).cloned() else { continue }; - let mut g = drawcraft_doc::live::expanded_group(&n, None); + let mut g = vectorcraft_doc::live::expanded_group(&n, None); // The group keeps the repeat's id; every generated instance node gets a fresh one. if let Some(ch) = g.children_mut() { diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 769b3a7f5..1cdbe340b 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -376,7 +376,7 @@ impl Session { let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions; let history_changed = p.history_states != self.prefs.history_states; self.prefs = p; - drawcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok()); + vectorcraft_render::set_default_threads(u16::try_from(self.prefs.render_threads).ok()); for st in &mut self.docs { if history_changed { st.history.limit = self.prefs.history_states as usize; diff --git a/crates/engine/src/cmd/recolor.rs b/crates/engine/src/cmd/recolor.rs index b280cdd43..2ab32e3a1 100644 --- a/crates/engine/src/cmd/recolor.rs +++ b/crates/engine/src/cmd/recolor.rs @@ -2,9 +2,9 @@ use std::collections::BTreeMap; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{AppearanceItem, Node, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Node, NodeKind}; use super::*; diff --git a/crates/engine/src/cmd/select.rs b/crates/engine/src/cmd/select.rs index dd5f59792..a84b6fa04 100644 --- a/crates/engine/src/cmd/select.rs +++ b/crates/engine/src/cmd/select.rs @@ -2,8 +2,8 @@ use std::collections::BTreeSet; -use drawcraft_doc::{Document, Node, NodeId, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; use super::*; diff --git a/crates/engine/src/cmd/textedit.rs b/crates/engine/src/cmd/textedit.rs index 6c7c729f6..478fd400e 100644 --- a/crates/engine/src/cmd/textedit.rs +++ b/crates/engine/src/cmd/textedit.rs @@ -1,11 +1,11 @@ //! Rich text editing commands: range edits and range styling (what the Type tool and the //! Character panel use while editing), creating area / on-path type from a path, and Fit Headline. -use drawcraft_color::Paint; -use drawcraft_doc::{Appearance, CharStyle, Node, NodeId, NodeKind, TextKind, TextObject, TextRun}; -use drawcraft_geom::Affine; -use drawcraft_text::edit; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::{Appearance, CharStyle, Node, NodeId, NodeKind, TextKind, TextObject, TextRun}; +use vectorcraft_geom::Affine; +use vectorcraft_text::edit; use super::typecmd::refresh_bounds; use super::*; @@ -60,7 +60,7 @@ pub fn specs() -> Vec { ] } -fn text_mut(d: &mut drawcraft_doc::Document, id: NodeId) -> Option<&mut TextObject> { +fn text_mut(d: &mut vectorcraft_doc::Document, id: NodeId) -> Option<&mut TextObject> { match d.node_mut(id).map(|n| &mut n.kind) { Some(NodeKind::Text(t)) => Some(t), _ => None, @@ -144,10 +144,10 @@ pub(crate) struct CharChange { pub(crate) fn features_param(p: &Value, cmd: &str) -> Result>> { let Some(v) = p.get("features").filter(|v| !v.is_null()) else { return Ok(None) }; let tags: Vec<&str> = v.as_array().ok_or_else(|| bad(cmd, "features must be a list of tags"))?.iter().filter_map(Value::as_str).collect(); - if let Some(t) = tags.iter().find(|t| !drawcraft_text::OtFeatures::known_tag(t)) { + if let Some(t) = tags.iter().find(|t| !vectorcraft_text::OtFeatures::known_tag(t)) { return Err(bad(cmd, format!("unknown OpenType feature `{t}` (liga, calt, dlig, smcp, frac, onum, tnum, ordn, swsh; prefix - to turn off)"))); } - Ok(Some(drawcraft_text::OtFeatures::default().with_tags(tags).to_tags())) + Ok(Some(vectorcraft_text::OtFeatures::default().with_tags(tags).to_tags())) } fn paint_param(p: &Value, k: &str, cmd: &str) -> Result> { @@ -220,9 +220,9 @@ impl CharChange { st.font_family = f.clone(); // Keep the style when the new family has it, else its closest match. if self.style.is_none() { - let styles = drawcraft_text::FontDb::global().styles(f); + let styles = vectorcraft_text::FontDb::global().styles(f); if !styles.is_empty() && !styles.iter().any(|s| s.eq_ignore_ascii_case(&st.font_style)) { - st.font_style = drawcraft_text::FontDb::global().face(f, &st.font_style).style.clone(); + st.font_style = vectorcraft_text::FontDb::global().face(f, &st.font_style).style.clone(); } } } @@ -322,12 +322,12 @@ fn create_in_path(s: &mut Session, p: &Value) -> Result { if let Some(f) = str_param(p, "font") { style.font_family = f.to_string(); } - if !s.paint.fill.is_none() && s.paint.fill != Paint::solid(drawcraft_color::Color::WHITE) { + if !s.paint.fill.is_none() && s.paint.fill != Paint::solid(vectorcraft_color::Color::WHITE) { style.fill = s.paint.fill.clone(); } let text = str_param(p, "text").unwrap_or("").to_string(); let start = match point_param(p, "at") { - Some(at) if on_path => drawcraft_text::path_fraction_at(&path.to_bezpath(), at).0, + Some(at) if on_path => vectorcraft_text::path_fraction_at(&path.to_bezpath(), at).0, _ => 0.0, }; let kind = if on_path { TextKind::OnPath { path: path.clone(), start } } else { TextKind::Area { frame: path.clone() } }; @@ -376,7 +376,7 @@ fn headline_tracking(t: &TextObject, target: f64) -> Option { for r in &mut h.runs { r.style.tracking = tr; } - let l = drawcraft_text::layout(drawcraft_text::FontDb::global(), &h); + let l = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &h); l.lines.first().map_or(0.0, |li| li.x1 - li.x0) }; // Width is linear in tracking: w(tr) = w0 + tr * slope. @@ -405,7 +405,7 @@ fn fit_headline(s: &mut Session, p: &Value) -> Result { s.edit("Fit Headline", |d, _| { for id in &ids { let Some(t) = text_mut(d, *id) else { continue }; - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let Some(first) = lay.lines.first() else { continue }; let target = first.avail.1 - first.avail.0; let Some(tr) = headline_tracking(t, target) else { continue }; diff --git a/crates/engine/src/cmd/textstyles.rs b/crates/engine/src/cmd/textstyles.rs index 87952bd8d..549a9185a 100644 --- a/crates/engine/src/cmd/textstyles.rs +++ b/crates/engine/src/cmd/textstyles.rs @@ -5,10 +5,10 @@ //! (Clear Overrides first resets to the Normal style). Redefining a style updates its users but keeps //! their local overrides: an attribute changes only where it still had the old style's value. -use drawcraft_doc::{CharStyle, Document, NodeId, NodeKind, ParaStyle, TextStyleDef}; use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::{Map, Value, json}; +use vectorcraft_doc::{CharStyle, Document, NodeId, NodeKind, ParaStyle, TextStyleDef}; use super::typecmd::{refresh_bounds, text_targets}; use super::*; @@ -167,7 +167,7 @@ fn all_text(d: &Document) -> Vec { v } -fn text_mut(d: &mut Document, id: NodeId) -> Option<&mut drawcraft_doc::TextObject> { +fn text_mut(d: &mut Document, id: NodeId) -> Option<&mut vectorcraft_doc::TextObject> { match d.node_mut(id).map(|n| &mut n.kind) { Some(NodeKind::Text(t)) => Some(t), _ => None, @@ -229,7 +229,7 @@ fn selection_attrs(s: &Session, p: &Value, kind: Kind) -> Result { let at = p.get("start").and_then(Value::as_u64).unwrap_or(0) as usize; - to_attrs(&drawcraft_text::edit::style_at(&t.runs, at)) + to_attrs(&vectorcraft_text::edit::style_at(&t.runs, at)) } Kind::Para => to_attrs(&t.para), })) @@ -322,7 +322,7 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result { let Some(t) = text_mut(d, *id) else { continue }; match kind { Kind::Char => { - let len = drawcraft_text::edit::runs_len(&t.runs); + let len = vectorcraft_text::edit::runs_len(&t.runs); let (a, b) = match range { Some(_) => { let g = |k: &str, def: usize| p.get(k).and_then(Value::as_u64).map_or(def, |v| (v as usize).min(len)); @@ -331,7 +331,7 @@ fn apply(s: &mut Session, p: &Value, kind: Kind) -> Result { None => (0, len), }; let mut err = None; - drawcraft_text::edit::style_range(&mut t.runs, a.min(b), a.max(b), |st| match char_fn(st) { + vectorcraft_text::edit::style_range(&mut t.runs, a.min(b), a.max(b), |st| match char_fn(st) { Ok(n) => *st = n, Err(e) => err = Some(e), }); @@ -460,7 +460,7 @@ mod tests { (s, id) } - fn runs(s: &Session, id: u64) -> Vec { + fn runs(s: &Session, id: u64) -> Vec { match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind { NodeKind::Text(t) => t.runs.clone(), _ => panic!(), @@ -529,12 +529,12 @@ mod tests { NodeKind::Text(t) => t.para.clone(), _ => panic!(), }; - assert_eq!((para(&s).justify, para(&s).space_after), (drawcraft_doc::Justify::Center, 6.0)); + assert_eq!((para(&s).justify, para(&s).space_after), (vectorcraft_doc::Justify::Center, 6.0)); // The Normal style can be redefined too (stored once redefined). s.execute("paraStyle.redefine", &json!({"name": NORMAL_PARA})).unwrap(); assert_eq!(s.execute("paraStyle.list", &json!({})).unwrap()["styles"][0]["attrs"]["justify"], "Center"); let d = s.doc().unwrap().doc.clone(); - let back = drawcraft_format::load(&drawcraft_format::save(&d, false)).unwrap(); + let back = vectorcraft_format::load(&vectorcraft_format::save(&d, false)).unwrap(); assert_eq!(back.para_styles, d.para_styles); } } diff --git a/crates/engine/src/cmd/threads.rs b/crates/engine/src/cmd/threads.rs index 29b3fb8d5..138f99e69 100644 --- a/crates/engine/src/cmd/threads.rs +++ b/crates/engine/src/cmd/threads.rs @@ -3,8 +3,8 @@ //! Frames are ordinary area text objects holding their slice of the story; the document lists the //! thread order. After every edit, threads whose frames changed are re-flowed ([`reflow`]). -use drawcraft_doc::{Document, Node, NodeId, NodeKind, TextKind, TextObject, TextRun}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind, TextKind, TextObject, TextRun}; use super::edit::selected_roots; use super::typecmd::refresh_bounds; @@ -92,10 +92,10 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) { } continue; } - drawcraft_text::edit::normalize(&mut story); + vectorcraft_text::edit::normalize(&mut story); let frames: Vec = live.iter().filter_map(|id| text_of(doc, *id).cloned()).collect(); let refs: Vec<&TextObject> = frames.iter().collect(); - let parts = drawcraft_text::thread::distribute(drawcraft_text::FontDb::global(), &refs, &story); + let parts = vectorcraft_text::thread::distribute(vectorcraft_text::FontDb::global(), &refs, &story); for (id, runs) in live.iter().zip(parts) { if let Some(NodeKind::Text(t)) = doc.node_mut(*id).map(|n| &mut n.kind) && t.runs != runs @@ -112,14 +112,14 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) { } /// A closed path converted to an empty area-type frame with `style`. -fn frame_from_path(n: &Node, style: drawcraft_doc::CharStyle) -> Option { +fn frame_from_path(n: &Node, style: vectorcraft_doc::CharStyle) -> Option { let NodeKind::Path { path, .. } = &n.kind else { return None }; if !path.subpaths.first().is_some_and(|s| s.closed) { return None; } - let mut t = TextObject::point(drawcraft_geom::Point::ZERO, "", style); + let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "", style); t.kind = TextKind::Area { frame: path.clone() }; - t.xf = drawcraft_geom::Affine::IDENTITY; + t.xf = vectorcraft_geom::Affine::IDENTITY; refresh_bounds(&mut t); Some(Node::new(n.id, NodeKind::Text(Box::new(t)))) } @@ -198,7 +198,7 @@ fn release(s: &mut Session, _: &Value) -> Result { if let Some(first) = keep.first() && let Some(NodeKind::Text(t)) = d.node_mut(*first).map(|n| &mut n.kind) { - drawcraft_text::edit::normalize(&mut story); + vectorcraft_text::edit::normalize(&mut story); t.runs = story; } let i = d.text_threads.iter().position(|t| *t == thread).unwrap_or(0); diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index a2e285a3b..549609bfe 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -3,9 +3,9 @@ use std::sync::Arc; -use drawcraft_doc::{Document, Justify, Node, NodeId, NodeKind, TextObject}; -use drawcraft_geom::PathData; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Justify, Node, NodeId, NodeKind, TextObject}; +use vectorcraft_geom::PathData; use super::edit::selected_roots; use super::pathops::{num_param, shape_node, text_style_appearance}; @@ -63,7 +63,7 @@ pub fn specs() -> Vec { /// Recompute the layout bounds cache after a text edit. pub(crate) fn refresh_bounds(t: &mut TextObject) { - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); t.cached_bounds = Some(lay.bounds); } @@ -94,7 +94,7 @@ fn create_outlines(s: &mut Session, _: &Value) -> Result { for tid in texts { let Some(n) = d.node(tid).cloned() else { continue }; let NodeKind::Text(t) = &n.kind else { continue }; - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let mut children = vec![]; for g in &lay.glyphs { let path = PathData::from_bezpath(&g.outline).transformed(t.xf); @@ -144,7 +144,7 @@ fn set_text(s: &mut Session, p: &Value) -> Result { for id in &ids { let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; let style = t.first_style(); - t.runs = vec![drawcraft_doc::TextRun { text: text.clone(), style }]; + t.runs = vec![vectorcraft_doc::TextRun { text: text.clone(), style }]; refresh_bounds(t); } Ok(()) @@ -185,8 +185,8 @@ fn set_style(s: &mut Session, p: &Value) -> Result { }; let fill = match p.get("fill") { None | Some(Value::Null) => None, - Some(Value::String(n)) if n.eq_ignore_ascii_case("none") => Some(drawcraft_color::Paint::None), - Some(v) => Some(drawcraft_color::Paint::solid(color_value(v).ok_or_else(|| bad(C, "bad fill colour"))?)), + Some(Value::String(n)) if n.eq_ignore_ascii_case("none") => Some(vectorcraft_color::Paint::None), + Some(v) => Some(vectorcraft_color::Paint::solid(color_value(v).ok_or_else(|| bad(C, "bad fill colour"))?)), }; let features = super::textedit::features_param(p, C)?; if font.is_none() @@ -244,7 +244,7 @@ fn area_options(s: &mut Session, p: &Value) -> Result { let d = &s.doc()?.doc; text_targets(s, p, C)? .into_iter() - .filter(|i| matches!(d.node(*i).map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, drawcraft_doc::TextKind::Area { .. }))) + .filter(|i| matches!(d.node(*i).map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }))) .collect() }; let first = ids.first().ok_or_else(|| bad(C, "select area type (text in a frame)"))?; @@ -265,7 +265,7 @@ fn area_options(s: &mut Session, p: &Value) -> Result { if !changed { return Ok(v); } - let mut opts: drawcraft_doc::AreaOptions = serde_json::from_value(v).map_err(|e| bad(C, e.to_string()))?; + let mut opts: vectorcraft_doc::AreaOptions = serde_json::from_value(v).map_err(|e| bad(C, e.to_string()))?; opts.rows = opts.rows.clamp(1, 100); opts.columns = opts.columns.clamp(1, 100); opts.gutter = opts.gutter.clamp(0.0, 10_000.0); @@ -300,7 +300,7 @@ mod area_tests { let q = s.execute("text.areaOptions", &json!({})).unwrap(); assert_eq!((q["rows"].as_u64(), q["columns"].as_u64()), (Some(1), Some(1))); let lay = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind { - NodeKind::Text(t) => drawcraft_text::layout(drawcraft_text::FontDb::global(), t), + NodeKind::Text(t) => vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t), _ => panic!(), }; assert_eq!(lay(&s).frames.len(), 1); @@ -321,8 +321,8 @@ mod area_tests { fn headline_fit(t: &TextObject, tracking: f64) -> Option<(f64, f64, usize)> { let mut probe = t.clone(); let para_end = probe.plain_text().find('\n').unwrap_or(usize::MAX); - drawcraft_text::edit::style_range(&mut probe.runs, 0, para_end.min(drawcraft_text::edit::runs_len(&t.runs)), |st| st.tracking = tracking); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &probe); + vectorcraft_text::edit::style_range(&mut probe.runs, 0, para_end.min(vectorcraft_text::edit::runs_len(&t.runs)), |st| st.tracking = tracking); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &probe); let cell = lay.frames.first()?; let line = lay.lines.first()?; let avail = cell.width() - 2.0 * t.area.inset - t.para.left_indent - t.para.right_indent; @@ -336,7 +336,7 @@ fn fit_headline(s: &mut Session, p: &Value) -> Result { let mut plans = vec![]; for id in &ids { let Some(NodeKind::Text(t)) = d.node(*id).map(|n| &n.kind) else { continue }; - if !matches!(t.kind, drawcraft_doc::TextKind::Area { .. }) { + if !matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }) { continue; } // Largest tracking that keeps the first paragraph on one line (bisection). @@ -355,8 +355,8 @@ fn fit_headline(s: &mut Session, p: &Value) -> Result { s.edit("Fit Headline", |d, _| { for (id, tr) in &plans { if let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) { - let end = t.plain_text().find('\n').unwrap_or(usize::MAX).min(drawcraft_text::edit::runs_len(&t.runs)); - drawcraft_text::edit::style_range(&mut t.runs, 0, end, |st| st.tracking = *tr); + let end = t.plain_text().find('\n').unwrap_or(usize::MAX).min(vectorcraft_text::edit::runs_len(&t.runs)); + vectorcraft_text::edit::style_range(&mut t.runs, 0, end, |st| st.tracking = *tr); refresh_bounds(t); } } diff --git a/crates/engine/src/cmd/typemenu.rs b/crates/engine/src/cmd/typemenu.rs index 8cc0e1848..c6b18fc73 100644 --- a/crates/engine/src/cmd/typemenu.rs +++ b/crates/engine/src/cmd/typemenu.rs @@ -2,9 +2,9 @@ //! Options, Fill with Placeholder Text, Insert Special/Whitespace/Break characters) and the text //! commands of the Edit menu (Find and Replace, Find Next, Paste without Formatting). -use drawcraft_doc::{CharStyle, Document, Justify, NodeId, NodeKind, TextKind, TextRun}; -use drawcraft_geom::{Affine, Rect, shapes}; use serde_json::{Value, json}; +use vectorcraft_doc::{CharStyle, Document, Justify, NodeId, NodeKind, TextKind, TextRun}; +use vectorcraft_geom::{Affine, Rect, shapes}; use super::typecmd::refresh_bounds; use super::*; @@ -286,7 +286,7 @@ fn to_area(s: &mut Session, p: &Value) -> Result { if !matches!(t.kind, TextKind::Point) { continue; } - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let b = if lay.bounds.area() > 0.0 { lay.bounds } else { t.estimate_bounds() }; let size = t.first_style().size; // A little slack on the right so the text doesn't rewrap. @@ -326,7 +326,7 @@ fn to_point(s: &mut Session, p: &Value) -> Result { let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue }; let TextKind::Area { frame } = &t.kind else { continue }; let fb = frame.bounds().unwrap_or_default(); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let plain = t.plain_text(); let baseline = lay.lines.first().map(|l| l.baseline).unwrap_or(fb.y0 + t.first_style().size * 0.8); // Soft wraps → explicit breaks (replace the space before the wrap when there is one). @@ -391,7 +391,7 @@ fn path_options(s: &mut Session, p: &Value) -> Result { // ---------- placeholder / insert ---------- -/// DrawCraft's own placeholder copy (not the classical Latin text). +/// VectorCraft's own placeholder copy (not the classical Latin text). pub(crate) const PLACEHOLDER: &str = "Sample copy flows here while the layout takes shape. Swap these words for real text once the \ design is settled. Every line is only a stand-in that shows size, rhythm and colour. Headlines, captions and body text all \ start as rough drafts like this one. Keep going until the frame is full and the page feels balanced."; @@ -589,7 +589,7 @@ fn find_next(s: &mut Session, p: &Value) -> Result { // ---------- Paste without Formatting ---------- -fn strip_formatting(n: &mut drawcraft_doc::Node) { +fn strip_formatting(n: &mut vectorcraft_doc::Node) { if let NodeKind::Text(t) = &mut n.kind { let text = t.plain_text(); t.runs = vec![TextRun { text, style: CharStyle::default() }]; diff --git a/crates/engine/src/cmd/wand.rs b/crates/engine/src/cmd/wand.rs index 111d8732a..8f6b118ee 100644 --- a/crates/engine/src/cmd/wand.rs +++ b/crates/engine/src/cmd/wand.rs @@ -2,10 +2,10 @@ //! Magic Wand panel's (session state, kept across tool switches); the tool runs //! `select.magicWand` on click. -use drawcraft_color::Paint; -use drawcraft_doc::{Document, Node, NodeId, NodeKind}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; use super::*; diff --git a/crates/engine/src/cmd/xform.rs b/crates/engine/src/cmd/xform.rs index fa093d9e2..99419723d 100644 --- a/crates/engine/src/cmd/xform.rs +++ b/crates/engine/src/cmd/xform.rs @@ -2,10 +2,10 @@ use std::sync::Arc; -use drawcraft_color::{Gradient, GradientGeom, GradientPaint, Paint}; -use drawcraft_doc::{Appearance, Node, NodeId, NodeKind}; -use drawcraft_geom::{Affine, Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_color::{Gradient, GradientGeom, GradientPaint, Paint}; +use vectorcraft_doc::{Appearance, Node, NodeId, NodeKind}; +use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use super::edit::selected_roots; use super::*; @@ -112,8 +112,8 @@ impl Projective { fn warp_paints(ap: &mut Appearance, pr: &Projective) { for it in &mut ap.items { let p = match it { - drawcraft_doc::AppearanceItem::Fill(f) => &mut f.paint, - drawcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint, + vectorcraft_doc::AppearanceItem::Fill(f) => &mut f.paint, + vectorcraft_doc::AppearanceItem::Stroke(s) => &mut s.paint, }; if let Paint::Gradient(g) = p && let Some(geom) = &mut g.geom diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index b7b689f3d..118aa7557 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -1,11 +1,11 @@ //! Agent-friendly document summaries and view-models for panels. -use drawcraft_doc::{Node, NodeKind}; use serde_json::{Value, json}; +use vectorcraft_doc::{Node, NodeKind}; use crate::Session; -fn rect_json(r: Option) -> Value { +fn rect_json(r: Option) -> Value { match r { Some(r) => json!({ "x": r.x0, "y": r.y0, "width": r.width(), "height": r.height() }), None => Value::Null, diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index eacbdf707..ebb54fb4a 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1,4 +1,4 @@ -//! The DrawCraft engine façade. +//! The VectorCraft engine façade. //! //! Every user-visible action is a command with a stable id (`object.group`, `select.same.fillColor`, //! `shape.rectangle`…) and JSON parameters. The egui UI, the CLI, the control channel and the MCP @@ -12,16 +12,16 @@ mod tooling; use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Document, NodeId, NodeKind, Selection}; -use drawcraft_geom::Affine; -use drawcraft_tools::{PaintDefaults, Tool}; use serde_json::Value; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Document, NodeId, NodeKind, Selection}; +use vectorcraft_geom::Affine; +use vectorcraft_tools::{PaintDefaults, Tool}; pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command}; -pub use drawcraft_doc as doc; -pub use drawcraft_tools as tools; pub use tooling::{UiRequest, ViewInfo}; +pub use vectorcraft_doc as doc; +pub use vectorcraft_tools as tools; #[derive(Debug, thiserror::Error)] pub enum EngineError { @@ -39,8 +39,8 @@ pub enum EngineError { Other(String), } -impl From for EngineError { - fn from(e: drawcraft_doc::DocError) -> Self { +impl From for EngineError { + fn from(e: vectorcraft_doc::DocError) -> Self { EngineError::Other(e.to_string()) } } @@ -82,9 +82,12 @@ pub struct DocState { pub selection: Selection, pub history: History, pub path: Option, - /// Increments on every change; UIs re-render when it moves. + /// Increments on every change (also selection and view-only changes); UIs re-render when it + /// moves. Not a dirty flag: see [`DocState::is_dirty`]. pub revision: u64, - pub saved_revision: u64, + /// The document as last saved (or opened). Structural sharing makes "unchanged" a pointer + /// comparison, and undoing back to the saved state counts as clean. + saved_doc: Arc, /// The layer new art goes into (the "current layer" in the Layers panel). pub active_layer: Option, /// Isolation mode container. @@ -103,13 +106,14 @@ static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64 impl DocState { pub fn new(doc: Document, path: Option) -> Self { let active_layer = doc.default_layer(); + let doc = Arc::new(doc); Self { - doc: Arc::new(doc), + saved_doc: doc.clone(), + doc, selection: Selection::default(), history: History { limit: 500, ..Default::default() }, path, revision: 1, - saved_revision: 1, active_layer, isolation: None, interaction: None, @@ -118,8 +122,13 @@ impl DocState { uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed), } } + /// Unsaved changes: the document differs from the saved one (selection changes don't count). pub fn is_dirty(&self) -> bool { - self.revision != self.saved_revision + !Arc::ptr_eq(&self.doc, &self.saved_doc) + } + /// Record the current document as saved. + pub fn mark_saved(&mut self) { + self.saved_doc = self.doc.clone(); } pub fn title(&self) -> String { self.path @@ -147,7 +156,7 @@ pub const MAX_COORD: f64 = 4.0e6; /// Cheap sanity check after an edit: artboards and the objects just touched (the selection) must /// have finite, in-range geometry, so saved files always reload and renderers never see NaN/∞. fn doc_sane(d: &Document, sel: &Selection) -> bool { - let ok = |r: drawcraft_geom::Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD); + let ok = |r: vectorcraft_geom::Rect| [r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite() && v.abs() <= MAX_COORD); d.artboards.iter().all(|a| ok(a.rect)) && sel.objects.iter().all(|id| d.node(*id).and_then(|n| n.geometric_bounds()).is_none_or(ok)) } @@ -416,7 +425,7 @@ pub struct Session { /// Which proxy is in front (true = Fill, false = Stroke) — the X key toggles. pub fill_active: bool, /// Internal clipboard (serialized nodes). - pub clipboard: Vec, + pub clipboard: Vec, /// Executed commands (for actions and debugging). pub journal: Vec<(String, Value)>, pub(crate) tool: Box, @@ -447,7 +456,7 @@ impl Session { fill_active: true, clipboard: vec![], journal: vec![], - tool: drawcraft_tools::create("selection"), + tool: vectorcraft_tools::create("selection"), last_view: ViewInfo::default(), depth: 0, draw_mode: DrawMode::Normal, @@ -485,7 +494,7 @@ impl Session { let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx)); let _ = self.apply_actions(acts); let _ = self.cancel_interaction(); - self.tool = drawcraft_tools::create(self.tool.id()); + self.tool = vectorcraft_tools::create(self.tool.id()); } pub fn set_active(&mut self, index: usize) -> bool { if index < self.docs.len() { @@ -503,7 +512,22 @@ impl Session { format!("Untitled-{}", self.untitled_counter) } /// Add a document and make it active. - pub fn add_document(&mut self, doc: Document, path: Option) -> usize { + pub fn add_document(&mut self, mut doc: Document, path: Option) -> usize { + // Text layout bounds are a cache (not saved): compute them now, or selection boxes and + // hit testing would use the rough estimate until each text object is edited. + let mut texts = vec![]; + doc.walk(|n| { + if matches!(n.kind, NodeKind::Text(_)) { + texts.push(n.id); + } + }); + for id in texts { + if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) + && t.cached_bounds.is_none() + { + cmd::typecmd::refresh_bounds(t); + } + } self.reset_tool_for_doc_switch(); let mut st = DocState::new(doc, path); st.history.limit = self.prefs.history_states as usize; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 555014972..923da0bd7 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -1,7 +1,7 @@ use serde_json::json; use super::*; -use drawcraft_tools::{PointerEvent, PointerKind}; +use vectorcraft_tools::{PointerEvent, PointerKind}; fn session() -> Session { let mut s = Session::new(); @@ -122,7 +122,7 @@ fn fill_and_stroke_commands() { assert_eq!(n.appearance.fill_paint().color().unwrap().to_hex(), "#ff0000"); let st = n.appearance.stroke().unwrap(); assert_eq!(st.width, 4.0); - assert_eq!(st.cap, drawcraft_doc::LineCap::Round); + assert_eq!(st.cap, vectorcraft_doc::LineCap::Round); assert_eq!(st.dash.as_ref().unwrap().pattern, vec![3.0, 2.0]); s.execute("paint.swap", &json!({})).unwrap(); assert_eq!(s.doc().unwrap().doc.node(a).unwrap().appearance.stroke_paint().color().unwrap().to_hex(), "#ff0000"); @@ -255,14 +255,14 @@ fn direct_selection_moves_one_anchor() { s.pointer(&PointerEvent::new(PointerKind::Drag, 90.0, 90.0), v).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Up, 90.0, 90.0), v).unwrap(); let p = s.doc().unwrap().doc.node(a).unwrap().path_data().unwrap().clone(); - assert_eq!(p.subpaths[0].anchors[0].p, drawcraft_geom::Point::new(90.0, 90.0)); - assert_eq!(p.subpaths[0].anchors[1].p, drawcraft_geom::Point::new(200.0, 100.0)); + assert_eq!(p.subpaths[0].anchors[0].p, vectorcraft_geom::Point::new(90.0, 90.0)); + assert_eq!(p.subpaths[0].anchors[1].p, vectorcraft_geom::Point::new(200.0, 100.0)); } #[test] fn text_create_has_bounds() { let mut s = session(); - let r = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hello DrawCraft", "size": 24})).unwrap(); + let r = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "Hello VectorCraft", "size": 24})).unwrap(); let n = s.doc().unwrap().doc.node(NodeId(r["id"].as_u64().unwrap())).unwrap().clone(); let b = n.geometric_bounds().unwrap(); assert!(b.width() > 100.0, "{b:?}"); diff --git a/crates/engine/src/tests_brushsym.rs b/crates/engine/src/tests_brushsym.rs index aa808b88c..f82226c13 100644 --- a/crates/engine/src/tests_brushsym.rs +++ b/crates/engine/src/tests_brushsym.rs @@ -1,9 +1,9 @@ //! Brushes and symbols: commands, tools, rendering and persistence. -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::Rect; -use drawcraft_tools::{Mods, PointerEvent, PointerKind}; use serde_json::{Value, json}; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::Rect; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; use super::*; @@ -361,7 +361,7 @@ fn brushed_strokes_and_instances_render() { let l = line(&mut s); s.execute("brush.apply", &json!({"name": "Chain", "ids": [l.0]})).unwrap(); let doc = s.doc().unwrap().doc.clone(); - let img = drawcraft_render::Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 400.0, 300.0), 1.0, true); + let img = vectorcraft_render::Renderer::new().render_region(&doc, Rect::new(0.0, 0.0, 400.0, 300.0), 1.0, true); let dark = |x: u32, y: u32| { let p = img.pixel(x, y); (p[0] as u32 + p[1] as u32 + p[2] as u32) < 300 @@ -374,17 +374,17 @@ fn brushed_strokes_and_instances_render() { } #[test] -fn brushes_and_symbols_survive_drawcraft_round_trip() { +fn brushes_and_symbols_survive_vectorcraft_round_trip() { let mut s = session(); make_symbol(&mut s); s.execute("brush.new", &json!({"type": "calligraphic", "name": "Mine", "params": {"size": 7}})).unwrap(); let l = line(&mut s); s.execute("brush.apply", &json!({"name": "Mine", "ids": [l.0]})).unwrap(); let doc = s.doc().unwrap().doc.clone(); - let bytes = drawcraft_format::save(&doc, false); - let back = drawcraft_format::load(&bytes).unwrap(); - assert_eq!(drawcraft_brush::library(&back), drawcraft_brush::library(&doc)); - assert_eq!(drawcraft_brush::find(&back, "Mine").map(|b| b.kind.type_id()), Some("calligraphic")); + let bytes = vectorcraft_format::save(&doc, false); + let back = vectorcraft_format::load(&bytes).unwrap(); + assert_eq!(vectorcraft_brush::library(&back), vectorcraft_brush::library(&doc)); + assert_eq!(vectorcraft_brush::find(&back, "Mine").map(|b| b.kind.type_id()), Some("calligraphic")); assert_eq!(back.symbols, doc.symbols); assert_eq!(back.unknown.get("symbolSizes"), doc.unknown.get("symbolSizes")); let bl = back.node(l).unwrap(); diff --git a/crates/engine/src/tests_build.rs b/crates/engine/src/tests_build.rs index ef28f23d4..a8b5e56ec 100644 --- a/crates/engine/src/tests_build.rs +++ b/crates/engine/src/tests_build.rs @@ -2,11 +2,11 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{ImageBlob, ImageObject, Node, NodeKind}; -use drawcraft_geom::{Affine, FillRule, PathData, Rect}; -use drawcraft_tools::{Mods, PointerEvent, PointerKind}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{ImageBlob, ImageObject, Node, NodeKind}; +use vectorcraft_geom::{Affine, FillRule, PathData, Rect}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; use super::*; @@ -38,12 +38,12 @@ fn node(s: &Session, id: NodeId) -> Node { } fn outline(n: &Node) -> (PathData, FillRule) { - drawcraft_tools::builder::node_outline(n).expect("path") + vectorcraft_tools::builder::node_outline(n).expect("path") } fn area(s: &Session, id: NodeId) -> f64 { let (p, r) = outline(&node(s, id)); - drawcraft_pathops::area(&p, r) + vectorcraft_pathops::area(&p, r) } /// Rect A (0..100) and rect B (50..150), both 100 tall, selected. A red, B blue. @@ -187,8 +187,8 @@ fn live_paint_make_builds_group_with_hidden_sources() { let mut s = session(); let g = live(&mut s); let n = node(&s, g); - assert!(drawcraft_tools::builder::is_live_paint(&n)); - let src = drawcraft_tools::builder::sources(&n).unwrap(); + assert!(vectorcraft_tools::builder::is_live_paint(&n)); + let src = vectorcraft_tools::builder::sources(&n).unwrap(); assert!(!src.visible); assert_eq!(src.children().unwrap().len(), 2); assert_eq!(layer_children(&s), 1); @@ -210,7 +210,7 @@ fn live_paint_fill_assigns_the_face_under_the_point() { assert_eq!(face.appearance.fill_paint(), Paint::solid(Color::from_hex("#00ff00").unwrap())); // The other faces are untouched. let gn = node(&s, g); - let green = drawcraft_tools::builder::faces(&gn).iter().filter(|f| f.appearance.fill_paint() == face.appearance.fill_paint()).count(); + let green = vectorcraft_tools::builder::faces(&gn).iter().filter(|f| f.appearance.fill_paint() == face.appearance.fill_paint()).count(); assert_eq!(green, 1); // Default paint = current fill; a miss is an error. s.paint.fill = Paint::solid(Color::rgb(1.0, 1.0, 0.0)); @@ -225,7 +225,7 @@ fn live_paint_fill_on_plain_paths_makes_the_group() { let (a, b) = two(&mut s); let r = s.execute("livePaint.fill", &json!({"ids": [a.0, b.0], "point": [125, 50], "color": "#123456"})).unwrap(); let g = NodeId(r["group"].as_u64().unwrap()); - assert!(drawcraft_tools::builder::is_live_paint(&node(&s, g))); + assert!(vectorcraft_tools::builder::is_live_paint(&node(&s, g))); assert_eq!(layer_children(&s), 1); // One undo step removes both the make and the fill. s.execute("edit.undo", &json!({})).unwrap(); @@ -253,7 +253,7 @@ fn live_paint_release_and_expand() { let r = s.execute("livePaint.expand", &json!({})).unwrap(); assert_eq!(ids(&r), vec![g]); let n = node(&s, g); - assert!(!drawcraft_tools::builder::is_live_paint(&n)); + assert!(!vectorcraft_tools::builder::is_live_paint(&n)); // Unpainted face and the hidden sources are gone: 2 faces + stroked edges. let ch = n.children().unwrap(); assert!(ch.iter().all(|c| c.visible)); @@ -283,9 +283,9 @@ fn live_paint_merge_keeps_paint_and_adds_faces() { assert_eq!(NodeId(r["id"].as_u64().unwrap()), g); assert!(r["faces"].as_u64().unwrap() > 3); let gn = node(&s, g); - let f = drawcraft_tools::builder::face_at(&gn, drawcraft_geom::Point::new(25.0, 50.0)).unwrap(); + let f = vectorcraft_tools::builder::face_at(&gn, vectorcraft_geom::Point::new(25.0, 50.0)).unwrap(); assert_eq!(f.appearance.fill_paint(), Paint::solid(Color::from_hex("#00ff00").unwrap())); - let src = drawcraft_tools::builder::sources(&gn).unwrap(); + let src = vectorcraft_tools::builder::sources(&gn).unwrap(); assert_eq!(src.children().unwrap().len(), 3, "the rectangle joined the sources"); assert_eq!(s.doc().unwrap().doc.parent_of(c), Some(src.id)); assert_eq!(layer_children(&s), 1); @@ -302,7 +302,7 @@ fn live_paint_bucket_tool_fills_face() { assert!(!s.overlays(v).is_empty(), "red face highlight"); s.pointer(&PointerEvent::new(PointerKind::Down, 125.0, 50.0), v).unwrap(); let gn = node(&s, g); - let f = drawcraft_tools::builder::face_at(&gn, drawcraft_geom::Point::new(125.0, 50.0)).unwrap(); + let f = vectorcraft_tools::builder::face_at(&gn, vectorcraft_geom::Point::new(125.0, 50.0)).unwrap(); assert_eq!(f.appearance.fill_paint(), s.paint.fill); // Live Paint Selection selects the face. s.select_tool("livePaintSelection", v).unwrap(); @@ -314,14 +314,14 @@ fn live_paint_bucket_tool_fills_face() { /// A 100×100 image with a black disc (r = 30 px), placed at (50, 50) scaled ×2. fn image_doc(s: &mut Session) -> NodeId { - let r = drawcraft_trace::Raster::from_fn(100, 100, |x, y| { + let r = vectorcraft_trace::Raster::from_fn(100, 100, |x, y| { let (dx, dy) = (x as f64 + 0.5 - 50.0, y as f64 + 0.5 - 50.0); if dx * dx + dy * dy <= 900.0 { [0, 0, 0, 255] } else { [255, 255, 255, 255] } }); add_image(s, &r, Affine::translate((50.0, 50.0)) * Affine::scale(2.0)) } -fn add_image(s: &mut Session, r: &drawcraft_trace::Raster, xf: Affine) -> NodeId { +fn add_image(s: &mut Session, r: &vectorcraft_trace::Raster, xf: Affine) -> NodeId { let png = r.encode_png(); let (w, h) = (r.width, r.height); s.edit("img", |d, sel| { @@ -354,7 +354,7 @@ fn image_trace_make_places_paths_over_the_image() { assert_eq!(paths.len(), 1); let (p, rule) = outline(&paths[0]); let want = std::f64::consts::PI * 60.0 * 60.0; // r = 30 px × 2 - let got = drawcraft_pathops::area(&p, rule); + let got = vectorcraft_pathops::area(&p, rule); assert!((got - want).abs() / want < 0.03, "{got} vs {want}"); let bb = p.bounds().unwrap(); assert!((bb.center().x - 150.0).abs() < 1.5 && (bb.center().y - 150.0).abs() < 1.5, "{bb:?}"); @@ -387,7 +387,7 @@ fn image_trace_release_and_expand() { #[test] fn image_trace_make_and_expand_and_color_presets() { let mut s = session(); - let r = drawcraft_trace::Raster::from_fn(60, 60, |x, y| match (x < 30, y < 30) { + let r = vectorcraft_trace::Raster::from_fn(60, 60, |x, y| match (x < 30, y < 30) { (true, true) => [230, 20, 20, 255], (false, true) => [20, 200, 30, 255], (true, false) => [20, 30, 220, 255], @@ -430,7 +430,7 @@ fn image_trace_object_remembers_its_settings() { let r = s.execute("imageTrace.make", &json!({"id": g.0, "preset": "6 Colors", "params": {"colors": 9}})).unwrap(); let g = NodeId(r["id"].as_u64().unwrap()); let d = s.doc().unwrap().doc.clone(); - let back = drawcraft_format::load(&drawcraft_format::save(&d, false)).unwrap(); + let back = vectorcraft_format::load(&vectorcraft_format::save(&d, false)).unwrap(); let t = back.node(g).unwrap().trace.clone().unwrap(); assert_eq!((t["preset"].as_str(), t["params"]["colors"].as_u64()), (Some("Custom"), Some(9))); // Expanded traces are plain groups. diff --git a/crates/engine/src/tests_colormgmt.rs b/crates/engine/src/tests_colormgmt.rs index 1b4c9c070..02f25c275 100644 --- a/crates/engine/src/tests_colormgmt.rs +++ b/crates/engine/src/tests_colormgmt.rs @@ -3,13 +3,13 @@ use std::sync::Mutex; -use drawcraft_color::cms::{self, Intent, Lab, delta_e2000}; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{ColorMode, Document}; -use drawcraft_geom::Affine; -use drawcraft_render::proof::{ProofSetup, ProofTarget}; -use drawcraft_render::{RenderOptions, Renderer}; use serde_json::{Value, json}; +use vectorcraft_color::cms::{self, Intent, Lab, delta_e2000}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{ColorMode, Document}; +use vectorcraft_geom::Affine; +use vectorcraft_render::proof::{ProofSetup, ProofTarget}; +use vectorcraft_render::{RenderOptions, Renderer}; use super::*; @@ -37,7 +37,7 @@ fn lab_of_rgb(rgb: [f32; 3]) -> Lab { cms::lab::srgb_to_lab(rgb) } -fn render(doc: &Document, opts: RenderOptions) -> drawcraft_render::Rendered { +fn render(doc: &Document, opts: RenderOptions) -> vectorcraft_render::Rendered { let mut r = Renderer::new(); r.threads = 0; r.render(doc, 100, 100, Affine::IDENTITY, &RenderOptions { background: Some([255, 255, 255, 255]), ..opts }) @@ -265,11 +265,11 @@ fn spot_colours_get_their_own_plate() { assert_eq!(vis, [false, false, false, false, true]); let r = s.execute("view.separationsPreview", &json!({"toggle": "Cyan"})).unwrap(); assert_eq!(r["plates"][0]["visible"], true); - assert_eq!(drawcraft_render::proof::active_proof().unwrap().separations.unwrap().len(), 2); - assert!(drawcraft_render::proof::overprint_preview_on(), "separations imply overprint preview"); + assert_eq!(vectorcraft_render::proof::active_proof().unwrap().separations.unwrap().len(), 2); + assert!(vectorcraft_render::proof::overprint_preview_on(), "separations imply overprint preview"); assert!(s.execute("view.separationsPreview", &json!({"only": "Mauve"})).is_err()); s.execute("view.separationsPreview", &json!({"on": false})).unwrap(); - assert!(drawcraft_render::proof::active_proof().is_none_or(|p| p.separations.is_none())); + assert!(vectorcraft_render::proof::active_proof().is_none_or(|p| p.separations.is_none())); } #[test] @@ -288,7 +288,7 @@ fn overprint_black_multiplies_in_preview() { assert!(over[2] as i32 > over[0] as i32 + 10, "overprint lets the cyan through: {over:?}"); assert!(over[2] < knock[2] + 60 && over[2] <= 128, "and stays dark: {over:?}"); s.execute("edit.colors.overprintBlack", &json!({"remove": true})).unwrap(); - assert!(drawcraft_render::proof::overprint_ids(&s.doc().unwrap().doc).is_empty()); + assert!(vectorcraft_render::proof::overprint_ids(&s.doc().unwrap().doc).is_empty()); } #[test] @@ -299,10 +299,10 @@ fn view_toggles_redraw_without_dirtying() { let r = s.execute("view.proofColors", &json!({"on": true})).unwrap(); assert_eq!(r["proofColors"], true); assert!(s.doc().unwrap().revision > rev, "canvas redraws"); - assert_eq!(s.doc().unwrap().revision, s.doc().unwrap().saved_revision, "not marked modified"); + assert!(!s.doc().unwrap().is_dirty(), "not marked modified"); let r = s.execute("view.proofSetup", &json!({"target": "deuteranopia", "intent": "perceptual"})).unwrap(); assert_eq!(r["target"], "deuteranopia"); - assert_eq!(drawcraft_render::proof::active_proof().unwrap().target, ProofTarget::Deuteranopia); + assert_eq!(vectorcraft_render::proof::active_proof().unwrap().target, ProofTarget::Deuteranopia); assert!(s.execute("view.proofSetup", &json!({"target": "cmyk:Nope"})).is_err()); s.execute("view.proofSetup", &json!({"target": "workingCmyk", "intent": "relative"})).unwrap(); let r = s.execute("view.proofColors", &json!({"on": false})).unwrap(); @@ -319,7 +319,7 @@ fn has_cmyk_op(pdf: &str) -> bool { } fn pdf_text(doc: &Document) -> String { - let bytes = drawcraft_pdf::export(doc, &drawcraft_pdf::PdfOptions { compress: false, created: Some(0), ..Default::default() }).unwrap(); + let bytes = vectorcraft_pdf::export(doc, &vectorcraft_pdf::PdfOptions { compress: false, created: Some(0), ..Default::default() }).unwrap(); String::from_utf8_lossy(&bytes).into_owned() } diff --git a/crates/engine/src/tests_distort.rs b/crates/engine/src/tests_distort.rs index cd1a65f80..832073a48 100644 --- a/crates/engine/src/tests_distort.rs +++ b/crates/engine/src/tests_distort.rs @@ -1,9 +1,9 @@ //! Width points, Liquify, Puppet Warp and Perspective Grid commands and tools. -use drawcraft_geom::{PathData, Point, Rect, Shape}; -use drawcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; -use drawcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use serde_json::json; +use vectorcraft_geom::{PathData, Point, Rect, Shape}; +use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use super::*; @@ -315,15 +315,15 @@ fn perspective_selection_moves_within_the_plane() { s.execute("perspective.attach", &json!({"ids": [a.0], "plane": "right"})).unwrap(); let g = grid(&s); let before: Vec = path(&s, a).anchors().map(|(_, _, an)| g.to_plane(Plane::Right, an.p).unwrap()).collect(); - let centre = before.iter().fold(drawcraft_geom::Vec2::ZERO, |a, p| a + p.to_vec2()) / before.len() as f64; + let centre = before.iter().fold(vectorcraft_geom::Vec2::ZERO, |a, p| a + p.to_vec2()) / before.len() as f64; let from = g.to_page(Plane::Right, centre.to_point()).unwrap(); - let to = g.to_page(Plane::Right, (centre + drawcraft_geom::Vec2::new(40.0, 20.0)).to_point()).unwrap(); + let to = g.to_page(Plane::Right, (centre + vectorcraft_geom::Vec2::new(40.0, 20.0)).to_point()).unwrap(); let n = undo_len(&s); gesture(&mut s, "perspectiveSelection", &[(from.x, from.y), (to.x, to.y), (to.x, to.y)], Mods::default()); assert_eq!(undo_len(&s), n + 1); let after: Vec = path(&s, a).anchors().map(|(_, _, an)| g.to_plane(Plane::Right, an.p).unwrap()).collect(); for (b, c) in before.iter().zip(&after) { - assert!((*c - *b - drawcraft_geom::Vec2::new(40.0, 20.0)).hypot() < 1e-6, "{b:?} → {c:?}"); + assert!((*c - *b - vectorcraft_geom::Vec2::new(40.0, 20.0)).hypot() < 1e-6, "{b:?} → {c:?}"); } } diff --git a/crates/engine/src/tests_draw2.rs b/crates/engine/src/tests_draw2.rs index effbde490..4e7116260 100644 --- a/crates/engine/src/tests_draw2.rs +++ b/crates/engine/src/tests_draw2.rs @@ -1,9 +1,9 @@ //! Drawing / path-editing tools driven through the session (tools → actions → `path.*` commands). -use drawcraft_doc::{NodeId, NodeKind}; -use drawcraft_geom::{FillRule, PathData, Point}; -use drawcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use serde_json::json; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::{FillRule, PathData, Point}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use super::*; use crate::tooling::{UiRequest, ViewInfo}; @@ -68,7 +68,7 @@ fn near(a: Point, b: Point) -> bool { } fn area(pd: &PathData) -> f64 { - drawcraft_pathops::area(pd, FillRule::NonZero) + vectorcraft_pathops::area(pd, FillRule::NonZero) } #[test] @@ -152,7 +152,7 @@ fn curvature_clicks_build_smooth_path() { assert_eq!(ps.len(), 1); let sp = &ps[0].1.subpaths[0]; assert_eq!(sp.anchors.len(), 3); - assert_eq!(sp.anchors[1].kind, drawcraft_geom::AnchorKind::Smooth); + assert_eq!(sp.anchors[1].kind, vectorcraft_geom::AnchorKind::Smooth); // Alt-click the middle point → corner. s.pointer(&PointerEvent::new(PointerKind::Down, 200.0, 200.0).with_mods(Mods { alt: true, ..Default::default() }), v).unwrap(); s.pointer(&PointerEvent::new(PointerKind::Up, 200.0, 200.0), v).unwrap(); @@ -219,7 +219,7 @@ fn anchor_point_tool_converts() { let id = rect(&mut s, 100.0, 100.0, 100.0, 100.0); drag(&mut s, "anchorPoint", &[(100.0, 100.0), (130.0, 80.0)], Mods::default()); let a = path(&s, id).subpaths[0].anchors[0]; - assert_eq!(a.kind, drawcraft_geom::AnchorKind::Smooth); + assert_eq!(a.kind, vectorcraft_geom::AnchorKind::Smooth); assert_eq!(a.h_out, Point::new(130.0, 80.0)); assert_eq!(a.h_in, Point::new(70.0, 120.0)); // Click the smooth anchor → corner again. diff --git a/crates/engine/src/tests_file.rs b/crates/engine/src/tests_file.rs index 839181591..694ed9c1b 100644 --- a/crates/engine/src/tests_file.rs +++ b/crates/engine/src/tests_file.rs @@ -51,10 +51,10 @@ fn batch_rolls_back_on_error() { fn serialize_and_reopen() { let mut s = session(); s.execute("shape.ellipse", &json!({"x": 10, "y": 10, "width": 80, "height": 40})).unwrap(); - let b64 = s.execute("document.serialize", &json!({"format": "drawcraft"})).unwrap()["dataBase64"].as_str().unwrap().to_string(); + let b64 = s.execute("document.serialize", &json!({"format": "vectorcraft"})).unwrap()["dataBase64"].as_str().unwrap().to_string(); let svg = s.execute("document.serialize", &json!({"format": "svg"})).unwrap()["text"].as_str().unwrap().to_string(); assert!(svg.contains(" assert_eq!(g.gradient.stops[0].color.to_hex(), "#00ff00"), + vectorcraft_color::Paint::Gradient(g) => assert_eq!(g.gradient.stops[0].color.to_hex(), "#00ff00"), other => panic!("{other:?}"), } } diff --git a/crates/engine/src/tests_live.rs b/crates/engine/src/tests_live.rs index 00b7cf6f3..a570bb60d 100644 --- a/crates/engine/src/tests_live.rs +++ b/crates/engine/src/tests_live.rs @@ -1,10 +1,10 @@ //! Live blends, envelopes and gradient meshes, driven through `Session::execute`. -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::live::{self, BlendOrientation, BlendSpacing, EnvelopeKind}; -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::{Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::live::{self, BlendOrientation, BlendSpacing, EnvelopeKind}; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::{Point, Rect}; use super::*; @@ -57,8 +57,8 @@ fn steps_of(s: &Session, id: NodeId) -> Vec { live::expand_live(&node(s, id)) } -fn render(s: &Session, region: Rect) -> drawcraft_render::Rendered { - drawcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, region, 1.0, true) +fn render(s: &Session, region: Rect) -> vectorcraft_render::Rendered { + vectorcraft_render::Renderer::new().render_region(&s.doc().unwrap().doc, region, 1.0, true) } fn all_anchors(nodes: &[Node]) -> Vec { @@ -446,7 +446,7 @@ fn envelope_transform_moves_mesh_points() { sel(&mut s, &[a]); let e = id_of(&s.execute("object.envelope.makeWithMesh", &json!({"rows": 1, "cols": 1})).unwrap()); s.edit("move", |d, _| { - d.node_mut(e).unwrap().transform(drawcraft_geom::Affine::translate((10.0, 20.0)), false); + d.node_mut(e).unwrap().transform(vectorcraft_geom::Affine::translate((10.0, 20.0)), false); Ok(()) }) .unwrap(); @@ -458,7 +458,7 @@ fn envelope_transform_moves_mesh_points() { // ---------- Gradient mesh ---------- -fn mesh_of(s: &Session, id: NodeId) -> drawcraft_doc::GradientMesh { +fn mesh_of(s: &Session, id: NodeId) -> vectorcraft_doc::GradientMesh { match &node(s, id).kind { NodeKind::Mesh(m) => m.clone(), k => panic!("{k:?}"), @@ -524,7 +524,7 @@ fn mesh_point_colour_move_and_undo() { s.execute("object.mesh.movePoint", &json!({"id": a.0, "index": 3, "x": 120, "y": 130})).unwrap(); assert_eq!(mesh_of(&s, a).points[3].p, Point::new(120.0, 130.0)); s.execute("object.mesh.movePoint", &json!({"id": a.0, "index": 3, "x": 125, "y": 130, "handle": 1})).unwrap(); - assert_eq!(mesh_of(&s, a).points[3].handles[1], drawcraft_geom::Vec2::new(5.0, 0.0)); + assert_eq!(mesh_of(&s, a).points[3].handles[1], vectorcraft_geom::Vec2::new(5.0, 0.0)); s.execute("edit.undo", &json!({})).unwrap(); s.execute("edit.undo", &json!({})).unwrap(); assert_eq!(mesh_of(&s, a).points[3].p, Point::new(100.0, 100.0)); @@ -587,7 +587,8 @@ fn mesh_hit_test_inside() { sel(&mut s, &[a]); s.execute("object.mesh.create", &json!({"rows": 2, "cols": 2})).unwrap(); let d = &s.doc().unwrap().doc; - let h = drawcraft_doc::hit::hit_test(d, Point::new(50.0, 30.0), drawcraft_doc::hit::HitOptions { tol: 1.0, outline: false, path_only: false }); + let h = + vectorcraft_doc::hit::hit_test(d, Point::new(50.0, 30.0), vectorcraft_doc::hit::HitOptions { tol: 1.0, outline: false, path_only: false }); assert_eq!(h.map(|h| h.leaf), Some(a)); } @@ -632,8 +633,8 @@ fn live_doc() -> Session { fn live_objects_round_trip_through_native_format() { let s = live_doc(); let d = &s.doc().unwrap().doc; - let bytes = drawcraft_format::save(d, false); - let back = drawcraft_format::load(&bytes).unwrap(); + let bytes = vectorcraft_format::save(d, false); + let back = vectorcraft_format::load(&bytes).unwrap(); assert_eq!(&back.layers, &d.layers); let mut kinds = vec![]; back.walk(|n| kinds.push(n.kind_label())); @@ -645,17 +646,17 @@ fn live_objects_round_trip_through_native_format() { #[test] fn svg_export_expands_live_objects() { let s = live_doc(); - let svg = drawcraft_svg::export(&s.doc().unwrap().doc, &Default::default()); + let svg = vectorcraft_svg::export(&s.doc().unwrap().doc, &Default::default()); // 4 blend objects + envelope content + 4 patches × 64 mesh pieces. let paths = svg.matches("= 4 + 1 + 256, "{paths}"); - assert!(drawcraft_svg::import(&svg).is_ok()); + assert!(vectorcraft_svg::import(&svg).is_ok()); } #[test] fn pdf_export_expands_live_objects() { let s = live_doc(); - let pdf = drawcraft_pdf::export(&s.doc().unwrap().doc, &Default::default()).unwrap(); + let pdf = vectorcraft_pdf::export(&s.doc().unwrap().doc, &Default::default()).unwrap(); assert!(pdf.starts_with(b"%PDF")); } @@ -663,7 +664,7 @@ fn pdf_export_expands_live_objects() { fn old_documents_still_load() { let s = session(); let d = &s.doc().unwrap().doc; - let back = drawcraft_format::load(&drawcraft_format::save(d, true)).unwrap(); + let back = vectorcraft_format::load(&vectorcraft_format::save(d, true)).unwrap(); assert_eq!(back.layers.len(), d.layers.len()); } diff --git a/crates/engine/src/tests_menucmds.rs b/crates/engine/src/tests_menucmds.rs index 00586fc72..2a39fa8b7 100644 --- a/crates/engine/src/tests_menucmds.rs +++ b/crates/engine/src/tests_menucmds.rs @@ -1,9 +1,9 @@ //! Menu long-tail commands (Object/Edit/Select/Type/View/File), driven through `Session::execute`. -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{ColorMode, LiveShape, Node, NodeKind, TextKind}; -use drawcraft_geom::Rect; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{ColorMode, LiveShape, Node, NodeKind, TextKind}; +use vectorcraft_geom::Rect; use super::*; @@ -308,7 +308,7 @@ fn blend_make_release_options() { assert_eq!(undo_len(&s), n + 1); let gn = node(&s, g); assert!(matches!(gn.kind, NodeKind::Blend { .. }), "live blend"); - let ch = drawcraft_doc::live::expand_live(&gn); + let ch = vectorcraft_doc::live::expand_live(&gn); assert_eq!(ch.len(), 5); // Middle step: halfway in position, size and colour. let mid = &ch[2]; @@ -318,7 +318,7 @@ fn blend_make_release_options() { assert!((c[0] - 0.5).abs() < 1e-3); // Options: 1 step. s.execute("object.blend.options", &json!({"steps": 1})).unwrap(); - assert_eq!(drawcraft_doc::live::expand_live(&node(&s, g)).len(), 3); + assert_eq!(vectorcraft_doc::live::expand_live(&node(&s, g)).len(), 3); // Release: keys only, back in the layer. let r = s.execute("object.blend.release", &json!({})).unwrap(); assert_eq!(r["ids"].as_array().unwrap().len(), 2); diff --git a/crates/engine/src/tests_panelcmds.rs b/crates/engine/src/tests_panelcmds.rs index 6fe2b4129..2f44d0f13 100644 --- a/crates/engine/src/tests_panelcmds.rs +++ b/crates/engine/src/tests_panelcmds.rs @@ -1,8 +1,8 @@ //! Panel-support commands (`cmd/panelcmds.rs`). -use drawcraft_color::{GradientKind, Paint}; -use drawcraft_doc::{AppearanceItem, NodeKind}; use serde_json::json; +use vectorcraft_color::{GradientKind, Paint}; +use vectorcraft_doc::{AppearanceItem, NodeKind}; use super::*; @@ -17,7 +17,7 @@ fn rect(s: &mut Session) -> NodeId { NodeId(r["id"].as_u64().unwrap()) } -fn node(s: &Session, id: NodeId) -> drawcraft_doc::Node { +fn node(s: &Session, id: NodeId) -> vectorcraft_doc::Node { s.doc().unwrap().doc.node(id).unwrap().clone() } @@ -158,9 +158,9 @@ fn stroke_advanced_scale_swap_flip() { let n = node(&s, id); let st = n.appearance.stroke().unwrap(); assert_eq!(st.start_arrow, None); - assert_eq!(st.end_arrow, Some(drawcraft_doc::Arrowhead::Triangle)); + assert_eq!(st.end_arrow, Some(vectorcraft_doc::Arrowhead::Triangle)); assert_eq!(st.arrow_scale, (200.0, 50.0)); - assert_eq!(st.profile.as_ref().unwrap().points, drawcraft_doc::WidthProfile::taper_start().points); + assert_eq!(st.profile.as_ref().unwrap().points, vectorcraft_doc::WidthProfile::taper_start().points); assert!(s.execute("stroke.setAdvanced", &json!({})).is_err()); assert!(s.execute("stroke.setAdvanced", &json!({"flipProfile": "sideways"})).is_err()); } diff --git a/crates/engine/src/tests_pathops.rs b/crates/engine/src/tests_pathops.rs index 8c29da502..8a2d9396b 100644 --- a/crates/engine/src/tests_pathops.rs +++ b/crates/engine/src/tests_pathops.rs @@ -1,9 +1,9 @@ //! Tests for Pathfinder, Object → Path and Type commands. -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, NodeKind}; -use drawcraft_geom::{FillRule, PathData, Rect}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, NodeKind}; +use vectorcraft_geom::{FillRule, PathData, Rect}; use super::*; @@ -22,7 +22,7 @@ fn ids(v: &Value) -> Vec { v["ids"].as_array().unwrap().iter().map(|x| NodeId(x.as_u64().unwrap())).collect() } -fn node(s: &Session, id: NodeId) -> drawcraft_doc::Node { +fn node(s: &Session, id: NodeId) -> vectorcraft_doc::Node { s.doc().unwrap().doc.node(id).cloned().unwrap() } @@ -39,7 +39,7 @@ fn path_of(s: &Session, id: NodeId) -> (PathData, FillRule) { fn area_of(s: &Session, id: NodeId) -> f64 { let (p, r) = path_of(s, id); - drawcraft_pathops::area(&p, r) + vectorcraft_pathops::area(&p, r) } fn bounds(s: &Session, id: NodeId) -> Rect { @@ -366,7 +366,7 @@ fn set_style_size_changes_bounds() { assert!(b1.width() > b0.width() * 1.8, "{b0:?} → {b1:?}"); let NodeKind::Text(tx) = node(&s, t).kind else { panic!() }; assert_eq!(tx.first_style().size, 24.0); - assert_eq!(tx.para.justify, drawcraft_doc::Justify::Center); + assert_eq!(tx.para.justify, vectorcraft_doc::Justify::Center); assert_eq!(tx.first_style().fill, Paint::solid(Color::rgb(1.0, 0.0, 0.0))); assert!(s.execute("text.setStyle", &json!({"justify": "diagonal"})).is_err()); assert!(s.execute("text.setStyle", &json!({})).is_err()); diff --git a/crates/engine/src/tests_pattern.rs b/crates/engine/src/tests_pattern.rs index 089e8e93e..0973bf067 100644 --- a/crates/engine/src/tests_pattern.rs +++ b/crates/engine/src/tests_pattern.rs @@ -1,10 +1,10 @@ //! Pattern swatches, pattern editing mode and Repeat, driven through `Session::execute`. -use drawcraft_color::Paint; -use drawcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType}; -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::{Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::pattern::{PATTERN_EDIT_LAYER, RepeatKind, TileType}; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::{Point, Rect}; use super::*; @@ -30,7 +30,7 @@ fn sel(s: &mut Session, ids: &[NodeId]) { s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::>()})).unwrap(); } -fn doc(s: &Session) -> &drawcraft_doc::Document { +fn doc(s: &Session) -> &vectorcraft_doc::Document { &s.doc().unwrap().doc } @@ -246,9 +246,9 @@ fn hit_testing_in_edit_mode_only_reaches_tile_layer() { sel(&mut s, &[r]); s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); let d = doc(&s); - let opt = drawcraft_doc::hit::HitOptions::default(); - assert!(drawcraft_doc::hit::hit_test(d, Point::new(220.0, 220.0), opt).is_none()); - let h = drawcraft_doc::hit::hit_test(d, Point::new(5.0, 5.0), opt).unwrap(); + let opt = vectorcraft_doc::hit::HitOptions::default(); + assert!(vectorcraft_doc::hit::hit_test(d, Point::new(220.0, 220.0), opt).is_none()); + let h = vectorcraft_doc::hit::hit_test(d, Point::new(5.0, 5.0), opt).unwrap(); assert_ne!(h.leaf, r); let _ = other; } @@ -262,8 +262,8 @@ fn save_load_roundtrip_keeps_patterns_and_repeats() { let r = rect(&mut s, 300.0, 50.0, 10.0, 10.0); sel(&mut s, &[r]); s.execute("object.repeat.radial", &json!({"instances": 6})).unwrap(); - let bytes = drawcraft_format::save(doc(&s), false); - let back = drawcraft_format::load(&bytes).unwrap(); + let bytes = vectorcraft_format::save(doc(&s), false); + let back = vectorcraft_format::load(&bytes).unwrap(); assert_eq!(back.patterns, doc(&s).patterns); assert_eq!(back.node(big).unwrap().appearance.fill_paint(), node(&s, big).appearance.fill_paint()); assert!(back.layers[0].children().unwrap().iter().any(|n| matches!(n.kind, NodeKind::Repeat(_)))); @@ -275,7 +275,7 @@ fn svg_export_writes_pattern_def() { make_dots(&mut s); let big = rect(&mut s, 100.0, 100.0, 50.0, 50.0); s.execute("paint.setFill", &json!({"ids": [big.0], "swatch": "Dots"})).unwrap(); - let svg = drawcraft_svg::export(doc(&s), &drawcraft_svg::ExportOptions::default()); + let svg = vectorcraft_svg::export(doc(&s), &vectorcraft_svg::ExportOptions::default()); assert!(svg.contains(" 500); } @@ -429,10 +429,10 @@ fn repeat_hit_test_uses_instances() { sel(&mut s, &[r]); s.execute("object.repeat.radial", &json!({"instances": 4, "radius": 50})).unwrap(); let d = doc(&s); - let opt = drawcraft_doc::hit::HitOptions::default(); - assert!(drawcraft_doc::hit::hit_test(d, Point::new(250.0, 150.0), opt).is_some()); + let opt = vectorcraft_doc::hit::HitOptions::default(); + assert!(vectorcraft_doc::hit::hit_test(d, Point::new(250.0, 150.0), opt).is_some()); // The empty centre of the ring is not the repeat. - assert!(drawcraft_doc::hit::hit_test(d, Point::new(200.0, 150.0), opt).is_none()); + assert!(vectorcraft_doc::hit::hit_test(d, Point::new(200.0, 150.0), opt).is_none()); } #[test] diff --git a/crates/engine/src/tests_textedit.rs b/crates/engine/src/tests_textedit.rs index 11ef02168..07b908c2e 100644 --- a/crates/engine/src/tests_textedit.rs +++ b/crates/engine/src/tests_textedit.rs @@ -1,10 +1,10 @@ //! Rich text editing: range edit/style commands, the Type tools through the session, area and //! on-path type from paths, Fit Headline, Convert To Area/Point Type. -use drawcraft_doc::{NodeId, NodeKind, TextKind, TextObject}; -use drawcraft_geom::Point; -use drawcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use serde_json::{Value, json}; +use vectorcraft_doc::{NodeId, NodeKind, TextKind, TextObject}; +use vectorcraft_geom::Point; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use super::*; @@ -127,8 +127,8 @@ fn type_tool_selection_replace_and_delete() { s.select_tool("type", v).unwrap(); // Click at the start of the text to edit it. let t = obj(&s, id); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); - let (a, b) = drawcraft_text::caret_position(&lay, 6); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); + let (a, b) = vectorcraft_text::caret_position(&lay, 6); let p = t.xf * a.midpoint(b); click(&mut s, p.x + 0.1, p.y); assert!(s.tool_wants_text()); @@ -203,12 +203,12 @@ fn area_type_from_a_circle_keeps_glyphs_inside() { let mut s = session(); let e = s.execute("shape.ellipse", &json!({"x": 100, "y": 100, "width": 240, "height": 240})).unwrap(); let pid = e["id"].as_u64().unwrap(); - let r = s.execute("text.createInPath", &json!({"path": pid, "mode": "area", "text": drawcraft_engine_placeholder()})).unwrap(); + let r = s.execute("text.createInPath", &json!({"path": pid, "mode": "area", "text": vectorcraft_engine_placeholder()})).unwrap(); let id = NodeId(r["id"].as_u64().unwrap()); assert!(s.doc().unwrap().doc.node(NodeId(pid)).is_none(), "the path became the frame"); let t = obj(&s, id); assert!(matches!(t.kind, TextKind::Area { .. })); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); assert!(lay.lines.len() > 5); let c = Point::new(220.0, 220.0); for g in lay.glyphs.iter().filter(|g| !g.outline.elements().is_empty()) { @@ -221,7 +221,7 @@ fn area_type_from_a_circle_keeps_glyphs_inside() { assert!(s.execute("text.createInPath", &json!({"path": 9999})).is_err()); } -fn drawcraft_engine_placeholder() -> String { +fn vectorcraft_engine_placeholder() -> String { "Type flows inside any closed shape, line by line, trimmed to the shape's edges at each band. ".repeat(4) } @@ -236,7 +236,7 @@ fn type_on_path_starts_where_clicked() { let t = obj(&s, NodeId(r["id"].as_u64().unwrap())); let TextKind::OnPath { start, .. } = t.kind else { panic!() }; assert!((start - 0.25).abs() < 0.01, "{start}"); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); let g0 = &lay.glyphs[0]; assert!((g0.origin.y - 300.0).abs() < 1e-6 && (g0.origin.x - 200.0).abs() < 1.0, "{:?}", g0.origin); } @@ -271,7 +271,7 @@ fn fit_headline_fills_the_frame_width() { let r = s.execute("type.fitHeadline", &json!({"ids": [id.0]})).unwrap(); assert!(r["tracking"][0].as_f64().unwrap() > 100.0, "{r}"); let t = obj(&s, id); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); let l0 = &lay.lines[0]; assert_eq!(&t.plain_text()[l0.start..l0.end], "Headline", "still one line"); assert!((l0.x1 - l0.x0 - 400.0).abs() < 1.0, "{}", l0.x1 - l0.x0); @@ -282,7 +282,7 @@ fn fit_headline_fills_the_frame_width() { } fn first_glyph_doc_pos(t: &TextObject) -> Point { - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); t.xf * lay.glyphs[0].origin } @@ -308,7 +308,7 @@ fn convert_area_point_keeps_text_in_place_for_every_alignment() { let t = obj(&s, id); let back = first_glyph_doc_pos(&t); assert!((back - before).hypot() < 0.01, "{j} (area again): {before:?} → {back:?}"); - let lay = drawcraft_text::layout(drawcraft_text::FontDb::global(), &t); + let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t); assert!(!lay.overflow); } } diff --git a/crates/engine/src/tests_xform.rs b/crates/engine/src/tests_xform.rs index ff73c67b2..4148e65ce 100644 --- a/crates/engine/src/tests_xform.rs +++ b/crates/engine/src/tests_xform.rs @@ -1,9 +1,9 @@ //! Transform & utility tools driven through `Session::pointer`, and their commands. -use drawcraft_color::{Color, Paint}; -use drawcraft_geom::{Point, Rect}; -use drawcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use serde_json::json; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_geom::{Point, Rect}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey}; use super::*; diff --git a/crates/engine/src/tooling.rs b/crates/engine/src/tooling.rs index 3bc31afe0..dbeb32201 100644 --- a/crates/engine/src/tooling.rs +++ b/crates/engine/src/tooling.rs @@ -1,8 +1,8 @@ //! Hosting the active tool: pointer/key events → actions → commands. -use drawcraft_geom::Point; -use drawcraft_tools::{Action, Cursor, Mods, Overlay, PointerEvent, ToolContext, ToolKey}; use serde_json::Value; +use vectorcraft_geom::Point; +use vectorcraft_tools::{Action, Cursor, Mods, Overlay, PointerEvent, ToolContext, ToolKey}; use crate::{Result, Session}; @@ -41,11 +41,11 @@ impl Session { } let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx)); self.apply_actions(acts)?; - self.tool = drawcraft_tools::create(id); + self.tool = vectorcraft_tools::create(id); Ok(()) } - pub(crate) fn with_tool_cx(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn drawcraft_tools::Tool, &ToolContext) -> R) -> R + pub(crate) fn with_tool_cx(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn vectorcraft_tools::Tool, &ToolContext) -> R) -> R where R: Default, { @@ -93,7 +93,7 @@ impl Session { pub fn overlays(&mut self, view: ViewInfo) -> Vec { let mut v = self.with_tool_cx(view, |t, cx| t.overlays(cx)); if let Some(d) = self.active() { - v.splice(0..0, drawcraft_tools::distort::perspective::grid_overlays(&d.doc, 1.0 / view.zoom.max(1e-9), self.tool.id())); + v.splice(0..0, vectorcraft_tools::distort::perspective::grid_overlays(&d.doc, 1.0 / view.zoom.max(1e-9), self.tool.id())); } v } diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 9d34d40e1..331bd2a62 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -2,17 +2,17 @@ //! junk params, must return Ok or Err (never panic), never leave an interaction open, and never //! corrupt the tree. -use drawcraft_engine::{Session, command_specs}; -use drawcraft_testkit::catch_quiet; -use drawcraft_testkit::fixtures::Fixture; -use drawcraft_testkit::invariants::{check_all, check_session, doc_json}; -use drawcraft_testkit::strategies::{junk_params, junk_values, param_keys}; use serde_json::{Value, json}; +use vectorcraft_engine::{Session, command_specs}; +use vectorcraft_testkit::catch_quiet; +use vectorcraft_testkit::fixtures::Fixture; +use vectorcraft_testkit::invariants::{check_all, check_session, doc_json}; +use vectorcraft_testkit::strategies::{junk_params, junk_values, param_keys}; /// Commands that replace/close the active document or touch the filesystem are fine to call, but a /// few need their paths redirected into a temp dir (handled by `junk_params`). fn safe_path() -> String { - drawcraft_testkit::temp_dir("sweep").join("out.bin").to_string_lossy().to_string() + vectorcraft_testkit::temp_dir("sweep").join("out.bin").to_string_lossy().to_string() } /// Run one call; returns a failure description or None. @@ -69,7 +69,7 @@ fn every_command_without_a_document() { match catch_quiet(|| s.execute(c.id, &json!({}))) { Err(p) => failures.push(format!("PANIC {} with no document: {p}", c.id)), // Anything that needs a document must say so via enablement, not fail later. - Ok(Err(drawcraft_engine::EngineError::NoDocument)) => failures.push(format!("{}: enabled without a document but needs one", c.id)), + Ok(Err(vectorcraft_engine::EngineError::NoDocument)) => failures.push(format!("{}: enabled without a document but needs one", c.id)), Ok(_) => {} } if s.in_interaction() { @@ -116,7 +116,7 @@ fn bug_huge_transform_makes_document_unloadable() { ] { let mut s = Fixture::Multi.session(); if s.execute(id, &p).is_ok() { - drawcraft_testkit::invariants::check_native_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{id} {p}: {e}")); + vectorcraft_testkit::invariants::check_native_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{id} {p}: {e}")); } } } @@ -127,7 +127,7 @@ fn bug_artboard_huge_props_break_svg() { let mut s = Fixture::Single.session(); let p = json!({"index": 0, key: 1e308}); if s.execute("artboard.setProps", &p).is_ok() { - drawcraft_testkit::invariants::check_svg_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{p}: {}", &e[..e.len().min(200)])); + vectorcraft_testkit::invariants::check_svg_roundtrip(&s.doc().unwrap().doc).unwrap_or_else(|e| panic!("{p}: {}", &e[..e.len().min(200)])); } } } @@ -254,7 +254,7 @@ fn structured_junk() { ("document.open", json!({"name": "x.svg", "dataBase64": "!!!"})), ("document.open", json!({"name": "x.svg", "dataBase64": "PHN2Zz4="})), ("document.open", json!({"name": "x.pdf", "dataBase64": "JVBERi0xLjQK"})), - ("document.open", json!({"name": "x.drawcraft", "dataBase64": "e30="})), + ("document.open", json!({"name": "x.vectorcraft", "dataBase64": "e30="})), ("document.activate", json!({"index": 99})), ("file.close", json!({"index": 99})), ("file.new", json!({"width": -1, "height": 0, "artboards": 1000000})), @@ -318,7 +318,7 @@ fn every_command_undoes_exactly() { let _ = s.execute("edit.undo", &json!({})); let after = doc_json(&s.doc().unwrap().doc); if after != before { - failures.push(format!("{}: {}", c.id, drawcraft_testkit::invariants::first_diff(&before, &after, "$"))); + failures.push(format!("{}: {}", c.id, vectorcraft_testkit::invariants::first_diff(&before, &after, "$"))); } } else if doc_json(&s.doc().unwrap().doc) != before && !c.params.contains("View state: not an undo step") { failures.push(format!("{}: changed the document without an undo step", c.id)); diff --git a/crates/engine/tests/known_bugs.rs b/crates/engine/tests/known_bugs.rs index 4d5a5bbeb..ea394663a 100644 --- a/crates/engine/tests/known_bugs.rs +++ b/crates/engine/tests/known_bugs.rs @@ -1,10 +1,10 @@ //! Minimal reproductions of engine bugs found by the property tests. Each is `#[ignore]`d with a //! `BUG:` reason until fixed; remove the attribute when the fix lands. -use drawcraft_engine::Session; -use drawcraft_testkit::fixtures::{exec, rect, select, session}; -use drawcraft_testkit::invariants::{check_native_roundtrip_exact, check_session, doc_json, first_diff}; use serde_json::json; +use vectorcraft_engine::Session; +use vectorcraft_testkit::fixtures::{exec, rect, select, session}; +use vectorcraft_testkit::invariants::{check_native_roundtrip_exact, check_session, doc_json, first_diff}; #[test] fn bug_group_inside_compound() { @@ -50,7 +50,7 @@ fn bug_bring_to_front_across_layers_underflows() { let b = rect(&mut s, 20.0, 0.0, 10.0, 10.0); for cmd in ["object.arrange.bringToFront", "object.arrange.bringForward"] { select(&mut s, &[a, b]); - let r = drawcraft_testkit::catch_quiet(|| s.execute(cmd, &json!({}))); + let r = vectorcraft_testkit::catch_quiet(|| s.execute(cmd, &json!({}))); assert!(r.is_ok(), "{cmd} panicked: {:?}", r.err()); check_session(&s).unwrap(); } diff --git a/crates/engine/tests/model_based.rs b/crates/engine/tests/model_based.rs index 00f433854..90578515d 100644 --- a/crates/engine/tests/model_based.rs +++ b/crates/engine/tests/model_based.rs @@ -1,11 +1,11 @@ //! Model-based / stateful property tests: random command sequences drawn from the registry, with //! structural invariants checked after every step and history/serialization invariants at the end. -use drawcraft_testkit::fixtures::{self, exec}; -use drawcraft_testkit::invariants::{self, check_all, check_session, doc_json, first_diff, redo_all, undo_all}; -use drawcraft_testkit::strategies::{Op, arb_ops}; use proptest::prelude::*; use serde_json::json; +use vectorcraft_testkit::fixtures::{self, exec}; +use vectorcraft_testkit::invariants::{self, check_all, check_session, doc_json, first_diff, redo_all, undo_all}; +use vectorcraft_testkit::strategies::{Op, arb_ops}; fn run_sequence(ops: &[Op], full_checks_every: usize) -> Result<(), TestCaseError> { let mut s = fixtures::session(); @@ -58,7 +58,7 @@ proptest! { } let want = doc_json(&s.doc().unwrap().doc); let journal = s.journal.clone(); - let mut r = drawcraft_engine::Session::new(); + let mut r = vectorcraft_engine::Session::new(); for (id, p) in &journal { let _ = r.execute(id, p); } @@ -132,8 +132,8 @@ fn empty_batch_is_harmless() { #[test] fn save_open_via_commands_roundtrips() { let mut s = fixtures::rich_session(); - let dir = drawcraft_testkit::temp_dir("engine-save"); - let path = dir.join("rich.drawcraft"); + let dir = vectorcraft_testkit::temp_dir("engine-save"); + let path = dir.join("rich.vectorcraft"); exec(&mut s, "document.save", json!({"path": path.to_str().unwrap()})); let want = doc_json(&s.doc().unwrap().doc); exec(&mut s, "document.open", json!({"path": path.to_str().unwrap()})); @@ -147,7 +147,7 @@ fn save_open_via_commands_roundtrips() { #[test] fn serialize_all_formats_on_rich_doc() { let mut s = fixtures::rich_session(); - for f in ["drawcraft", "svg", "pdf", "png"] { + for f in ["vectorcraft", "svg", "pdf", "png"] { let v = exec(&mut s, "document.serialize", json!({"format": f})); assert!(v.get("dataBase64").or(v.get("text")).is_some(), "{f}: {v}"); } @@ -156,8 +156,8 @@ fn serialize_all_formats_on_rich_doc() { let text = svg["text"] .as_str() .map(str::to_string) - .unwrap_or_else(|| String::from_utf8(drawcraft_format::base64_decode(svg["dataBase64"].as_str().unwrap()).unwrap()).unwrap()); - let back = drawcraft_svg::import(&text).unwrap(); + .unwrap_or_else(|| String::from_utf8(vectorcraft_format::base64_decode(svg["dataBase64"].as_str().unwrap()).unwrap()).unwrap()); + let back = vectorcraft_svg::import(&text).unwrap(); invariants::check_document(&back).unwrap(); assert!(back.node_count() >= 5); } diff --git a/crates/format/Cargo.toml b/crates/format/Cargo.toml index 755273646..d0fd4e90b 100644 --- a/crates/format/Cargo.toml +++ b/crates/format/Cargo.toml @@ -1,20 +1,20 @@ [package] -name = "drawcraft-format" +name = "vectorcraft-format" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft native .drawcraft format (versioned JSON, embedded images)" +description = "VectorCraft native .vectorcraft format (versioned JSON, embedded images)" [lints] workspace = true [dependencies] -drawcraft-doc = { workspace = true } +vectorcraft-doc = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } -drawcraft-geom = { workspace = true } +vectorcraft-geom = { workspace = true } diff --git a/crates/format/src/lib.rs b/crates/format/src/lib.rs index 6f3da4a2e..df2a66a94 100644 --- a/crates/format/src/lib.rs +++ b/crates/format/src/lib.rs @@ -1,9 +1,9 @@ -//! The native `.drawcraft` format. +//! The native `.vectorcraft` format. //! -//! A `.drawcraft` file is UTF-8 JSON: +//! A `.vectorcraft` file is UTF-8 JSON: //! ```json -//! { "format": "drawcraft", "version": 1, "generator": "DrawCraft 0.1.0", -//! "document": { …drawcraft_doc::Document… }, +//! { "format": "vectorcraft", "version": 1, "generator": "VectorCraft 0.1.0", +//! "document": { …vectorcraft_doc::Document… }, //! "images": { "": { "mime": "image/png", "data": "" } } } //! ``` //! It is lossless for everything in the document model and preserves unknown fields under @@ -13,18 +13,30 @@ use std::collections::BTreeMap; use std::sync::Arc; -use drawcraft_doc::{Document, ImageBlob}; use serde::{Deserialize, Serialize}; +use vectorcraft_doc::{Document, ImageBlob}; /// v2: anchors as `{p: [x, y], in?, out?, kind?}` and default-valued fields omitted (v1 files still load). pub const VERSION: u32 = 2; -pub const EXTENSION: &str = "drawcraft"; +pub const EXTENSION: &str = "vectorcraft"; +/// Extension and format name from before the project was renamed (DrawCraft): still opened. +pub const LEGACY_EXTENSION: &str = "drawcraft"; + +/// Is `ext` (without the dot, any case) a native document extension? +pub fn is_native_ext(ext: &str) -> bool { + ext.eq_ignore_ascii_case(EXTENSION) || ext.eq_ignore_ascii_case(LEGACY_EXTENSION) +} + +/// Does the file name or path end in a native document extension? +pub fn is_native_name(name: &str) -> bool { + std::path::Path::new(name).extension().and_then(|e| e.to_str()).is_some_and(is_native_ext) +} #[derive(Debug, thiserror::Error)] pub enum FormatError { - #[error("not a DrawCraft file: {0}")] - NotDrawcraft(String), - #[error("file version {0} is newer than this DrawCraft supports ({VERSION})")] + #[error("not a VectorCraft file: {0}")] + NotVectorcraft(String), + #[error("file version {0} is newer than this VectorCraft supports ({VERSION})")] TooNew(u32), #[error("invalid image data for `{0}`")] BadImage(String), @@ -51,9 +63,9 @@ struct File { pub fn save(doc: &Document, pretty: bool) -> Vec { let images = doc.images.iter().map(|(k, b)| (k.clone(), Image { mime: b.mime.clone(), data: base64_encode(&b.bytes) })).collect(); let f = File { - format: "drawcraft".into(), + format: "vectorcraft".into(), version: VERSION, - generator: format!("DrawCraft {}", env!("CARGO_PKG_VERSION")), + generator: format!("VectorCraft {}", env!("CARGO_PKG_VERSION")), document: doc.clone(), images, }; @@ -71,9 +83,9 @@ pub fn save_file(doc: &Document) -> Vec { } pub fn load(bytes: &[u8]) -> Result { - let f: File = serde_json::from_slice(bytes).map_err(|e| FormatError::NotDrawcraft(e.to_string()))?; - if f.format != "drawcraft" { - return Err(FormatError::NotDrawcraft(format!("format is `{}`", f.format))); + let f: File = serde_json::from_slice(bytes).map_err(|e| FormatError::NotVectorcraft(e.to_string()))?; + if f.format != EXTENSION && f.format != LEGACY_EXTENSION { + return Err(FormatError::NotVectorcraft(format!("format is `{}`", f.format))); } if f.version > VERSION { return Err(FormatError::TooNew(f.version)); @@ -87,10 +99,10 @@ pub fn load(bytes: &[u8]) -> Result { Ok(doc) } -/// Does this look like a `.drawcraft` file? +/// Does this look like a `.vectorcraft` file? pub fn sniff(bytes: &[u8]) -> bool { let head = &bytes[..bytes.len().min(256)]; - std::str::from_utf8(head).is_ok_and(|s| s.trim_start().starts_with('{') && s.contains("\"drawcraft\"")) + std::str::from_utf8(head).is_ok_and(|s| s.trim_start().starts_with('{') && (s.contains("\"vectorcraft\"") || s.contains("\"drawcraft\""))) } const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; @@ -139,8 +151,19 @@ pub fn base64_decode(s: &str) -> Option> { #[cfg(test)] mod tests { use super::*; - use drawcraft_doc::{Appearance, Node}; - use drawcraft_geom::{Rect, shapes}; + + #[test] + fn files_from_before_the_rename_still_open() { + let d = Document::new(100.0, 50.0); + let legacy = String::from_utf8(save(&d, false)).unwrap().replacen("\"format\":\"vectorcraft\"", "\"format\":\"drawcraft\"", 1); + assert!(legacy.contains("\"drawcraft\"")); + assert!(sniff(legacy.as_bytes())); + assert_eq!(load(legacy.as_bytes()).unwrap().artboards[0].rect, d.artboards[0].rect); + assert!(is_native_name("old/Poster.DrawCraft") && is_native_name("new.vectorcraft") && !is_native_name("x.svg")); + assert!(load(br#"{"format":"other","version":1,"document":{}}"#).is_err()); + } + use vectorcraft_doc::{Appearance, Node}; + use vectorcraft_geom::{Rect, shapes}; #[test] fn roundtrip() { diff --git a/crates/format/tests/prop_format.rs b/crates/format/tests/prop_format.rs index 7820f6c75..2d2ba623f 100644 --- a/crates/format/tests/prop_format.rs +++ b/crates/format/tests/prop_format.rs @@ -1,14 +1,14 @@ -//! `.drawcraft` format: round trips, robustness against garbage, versioning, unknown fields. +//! `.vectorcraft` format: round trips, robustness against garbage, versioning, unknown fields. use std::sync::Arc; -use drawcraft_doc::{Document, ImageBlob, NodeId}; -use drawcraft_format::{base64_decode, base64_encode, load, save, sniff}; -use drawcraft_testkit::fixtures; -use drawcraft_testkit::invariants::{check_document, check_native_roundtrip, check_native_roundtrip_exact, doc_json, json_approx_eq}; -use drawcraft_testkit::strategies::arb_ops; use proptest::prelude::*; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, ImageBlob, NodeId}; +use vectorcraft_format::{base64_decode, base64_encode, load, save, sniff}; +use vectorcraft_testkit::fixtures; +use vectorcraft_testkit::invariants::{check_document, check_native_roundtrip, check_native_roundtrip_exact, doc_json, json_approx_eq}; +use vectorcraft_testkit::strategies::arb_ops; fn rich_doc() -> Document { (*fixtures::rich_session().doc().unwrap().doc).clone() @@ -84,8 +84,8 @@ fn rich_document_roundtrips() { let bytes = save(&d, true); assert!(sniff(&bytes)); let v: Value = serde_json::from_slice(&bytes).unwrap(); - assert_eq!(v["format"], "drawcraft"); - assert_eq!(v["version"], drawcraft_format::VERSION); + assert_eq!(v["format"], "vectorcraft"); + assert_eq!(v["version"], vectorcraft_format::VERSION); } #[test] @@ -100,14 +100,14 @@ fn sniff_distinguishes_formats() { #[test] fn rejects_newer_versions_and_other_formats() { let mut v: Value = serde_json::from_slice(&save(&Document::new(1.0, 1.0), false)).unwrap(); - v["version"] = json!(drawcraft_format::VERSION + 1); - assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(drawcraft_format::FormatError::TooNew(_)))); - v["version"] = json!(drawcraft_format::VERSION); + v["version"] = json!(vectorcraft_format::VERSION + 1); + assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(vectorcraft_format::FormatError::TooNew(_)))); + v["version"] = json!(vectorcraft_format::VERSION); v["format"] = json!("other"); - assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(drawcraft_format::FormatError::NotDrawcraft(_)))); - v["format"] = json!("drawcraft"); + assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(vectorcraft_format::FormatError::NotVectorcraft(_)))); + v["format"] = json!("vectorcraft"); v["images"] = json!({"x": {"mime": "image/png", "data": "not base64!"}}); - assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(drawcraft_format::FormatError::BadImage(_)))); + assert!(matches!(load(&serde_json::to_vec(&v).unwrap()), Err(vectorcraft_format::FormatError::BadImage(_)))); } #[test] diff --git a/crates/geom/Cargo.toml b/crates/geom/Cargo.toml index 23776ecc1..ab404c6be 100644 --- a/crates/geom/Cargo.toml +++ b/crates/geom/Cargo.toml @@ -1,9 +1,9 @@ [package] -name = "drawcraft-geom" +name = "vectorcraft-geom" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft geom" +description = "VectorCraft geom" [lints] workspace = true @@ -14,6 +14,6 @@ serde = { workspace = true } thiserror = { workspace = true } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } serde_json = { workspace = true } diff --git a/crates/geom/src/lib.rs b/crates/geom/src/lib.rs index f6cb6db5e..e3db5bd03 100644 --- a/crates/geom/src/lib.rs +++ b/crates/geom/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft geometry kernel. +//! VectorCraft geometry kernel. //! //! The editable path model is anchor based (like every Illustrator tool thinks about paths): //! a [`PathData`] is a list of [`SubPath`]s, each a list of [`Anchor`]s with absolute in/out diff --git a/crates/geom/tests/prop_geom.rs b/crates/geom/tests/prop_geom.rs index d771a70f5..b3bf993b2 100644 --- a/crates/geom/tests/prop_geom.rs +++ b/crates/geom/tests/prop_geom.rs @@ -1,10 +1,10 @@ //! Geometry property tests: PathData ↔ BezPath, bounds, hit testing, anchors and shapes. -use drawcraft_geom::hit::{distance_to_outline, fill_contains, stroke_contains}; -use drawcraft_geom::{Affine, BezPath, FillRule, PathData, PathEl, Point, Rect, Shape, Vec2, shapes}; -use drawcraft_testkit::geom::{grid, hausdorff, polygon_area, rect_contains, sample, sample_path}; -use drawcraft_testkit::strategies::{arb_closed_shape, arb_convex_polygon, arb_path_data, arb_point, arb_rect, arb_star_polygon, polygon_path}; use proptest::prelude::*; +use vectorcraft_geom::hit::{distance_to_outline, fill_contains, stroke_contains}; +use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, PathEl, Point, Rect, Shape, Vec2, shapes}; +use vectorcraft_testkit::geom::{grid, hausdorff, polygon_area, rect_contains, sample, sample_path}; +use vectorcraft_testkit::strategies::{arb_closed_shape, arb_convex_polygon, arb_path_data, arb_point, arb_rect, arb_star_polygon, polygon_path}; fn els_close(a: &BezPath, b: &BezPath, eps: f64) -> bool { let (a, b) = (a.elements(), b.elements()); diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 571ed6da1..7234d53eb 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -1,24 +1,24 @@ [package] -name = "drawcraft-mcp" +name = "vectorcraft-mcp" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft MCP server: JSON-RPC 2.0 over stdio, driving a running app or a headless session" +description = "VectorCraft MCP server: JSON-RPC 2.0 over stdio, driving a running app or a headless session" [lints] workspace = true [dependencies] -drawcraft-engine = { workspace = true } -drawcraft-render = { workspace = true } -drawcraft-svg = { workspace = true } -drawcraft-format = { workspace = true } -drawcraft-tools = { workspace = true } -drawcraft-geom = { workspace = true } -drawcraft-doc = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-render = { workspace = true } +vectorcraft-svg = { workspace = true } +vectorcraft-format = { workspace = true } +vectorcraft-tools = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-doc = { workspace = true } serde_json = { workspace = true } log = { workspace = true } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } diff --git a/crates/mcp/src/backend.rs b/crates/mcp/src/backend.rs index 8af63771a..57f9bbe62 100644 --- a/crates/mcp/src/backend.rs +++ b/crates/mcp/src/backend.rs @@ -7,7 +7,7 @@ use std::time::Duration; use serde_json::{Value, json}; /// Something that answers control-channel methods (`engine.execute`, `document.inspect`, -/// `ui.pointer`, `ui.render`, `app.export`, …). See `drawcraft_ui_egui::control` for the list. +/// `ui.pointer`, `ui.render`, `app.export`, …). See `vectorcraft_ui_egui::control` for the list. pub trait Backend { /// Call one method. `Ok` carries the `result`, `Err` the error message. fn call(&mut self, method: &str, params: Value) -> Result; @@ -17,7 +17,7 @@ pub trait Backend { fn describe(&self) -> String; } -/// A running DrawCraft app, reached through its loopback control port. +/// A running VectorCraft app, reached through its loopback control port. pub struct Remote { addr: String, conn: Option<(BufReader, TcpStream)>, @@ -83,7 +83,7 @@ impl Backend for Remote { self.conn = None; self.roundtrip(&line).map_err(|e| { self.conn = None; - format!("DrawCraft app at {} is not reachable: {e}", self.addr) + format!("VectorCraft app at {} is not reachable: {e}", self.addr) })? } }; diff --git a/crates/mcp/src/headless.rs b/crates/mcp/src/headless.rs index e0ea0b4f5..eacb1d769 100644 --- a/crates/mcp/src/headless.rs +++ b/crates/mcp/src/headless.rs @@ -1,10 +1,10 @@ //! An in-process engine session that answers the control-channel methods itself. -use drawcraft_engine::{Session, UiRequest, ViewInfo}; -use drawcraft_geom::Point; -use drawcraft_render::Renderer; -use drawcraft_tools::{Mods, PointerEvent, PointerKind, TOOL_GROUPS, ToolKey}; use serde_json::{Value, json}; +use vectorcraft_engine::{Session, UiRequest, ViewInfo}; +use vectorcraft_geom::Point; +use vectorcraft_render::Renderer; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind, TOOL_GROUPS, ToolKey}; use crate::backend::Backend; @@ -25,10 +25,10 @@ impl Default for Headless { /// Commands the desktop app adds on top of the engine; the headless backend implements them too so /// `run_command` behaves the same in both modes. const HOST_COMMANDS: &[(&str, &str, &str)] = &[ - ("file.open", "Open…", "{path} open a .drawcraft or .svg file as a new document"), - ("file.save", "Save", "{path?} save as .drawcraft (default: the document's path)"), + ("file.open", "Open…", "{path} open a .vectorcraft or .svg file as a new document"), + ("file.save", "Save", "{path?} save as .vectorcraft (default: the document's path)"), ("file.saveAs", "Save As…", "{path}"), - ("file.export", "Export…", "{format?: svg|png|drawcraft, path, scale?, artboard?}"), + ("file.export", "Export…", "{format?: svg|png|vectorcraft, path, scale?, artboard?}"), ("tool.select", "Select Tool", "{tool} e.g. selection, directSelection, pen, rectangle, ellipse, polygon, star, lineSegment"), ]; @@ -126,7 +126,7 @@ impl Headless { fn select_tool(&mut self, p: &Value) -> Result { let t = s(p, "tool").ok_or("missing `tool`")?; - if drawcraft_tools::tool_info(t).is_none() { + if vectorcraft_tools::tool_info(t).is_none() { return Err(format!("unknown tool `{t}` (see ui.tool.list)")); } self.session.select_tool(t, self.view).map_err(|e| e.to_string())?; @@ -181,7 +181,7 @@ impl Headless { self.apply_ui_requests(reqs, &mut out)?; return Ok(json!({"handledBy": "tool", "requests": out})); } - if let Some(c) = drawcraft_engine::command_specs().iter().find(|c| c.shortcut.is_some_and(|sc| shortcut_matches(sc, key, mods))) { + if let Some(c) = vectorcraft_engine::command_specs().iter().find(|c| c.shortcut.is_some_and(|sc| shortcut_matches(sc, key, mods))) { let r = self.exec(c.id, &json!({}))?; return Ok(json!({"handledBy": "command", "command": c.id, "result": r})); } @@ -211,31 +211,31 @@ impl Headless { std::fs::write(path, &png).map_err(|e| format!("write {path}: {e}"))?; Ok(json!({"path": path, "width": img.width, "height": img.height})) } - None => Ok(json!({"width": img.width, "height": img.height, "pngBase64": drawcraft_format::base64_encode(&png)})), + None => Ok(json!({"width": img.width, "height": img.height, "pngBase64": vectorcraft_format::base64_encode(&png)})), } } - /// `app.open {path}`: `.drawcraft` or `.svg` as a new active document. + /// `app.open {path}`: `.vectorcraft` or `.svg` as a new active document. pub fn open(&mut self, p: &Value) -> Result { let path = s(p, "path").ok_or("missing `path`")?; let bytes = std::fs::read(path).map_err(|e| format!("read {path}: {e}"))?; let name = file_name(path); let e = ext_of(path); let mut warnings = vec![]; - let (doc, keep_path) = if e == "drawcraft" || drawcraft_format::sniff(&bytes) { - let mut d = drawcraft_format::load(&bytes).map_err(|e| e.to_string())?; + let (doc, keep_path) = if vectorcraft_format::is_native_ext(&e) || vectorcraft_format::sniff(&bytes) { + let mut d = vectorcraft_format::load(&bytes).map_err(|e| e.to_string())?; if d.title.is_empty() { d.title = name.clone(); } (d, true) } else if e == "svg" || bytes.starts_with(b" Result { let st = self.session.active().ok_or("no document")?; let path = s(p, "path").map(str::to_string).or_else(|| st.path.clone()).ok_or("missing `path` (document was never saved)")?; - let bytes = drawcraft_format::save_file(&st.doc); + let bytes = vectorcraft_format::save_file(&st.doc); std::fs::write(&path, bytes).map_err(|e| format!("write {path}: {e}"))?; if let Some(st) = self.session.active_mut() { st.path = Some(path.clone()); - st.saved_revision = st.revision; + st.mark_saved(); } Ok(json!({"path": path})) } - /// `app.export {format?, path, scale?, artboard?}`: svg | png | pdf | jpg | webp | drawcraft (format defaults to the extension). + /// `app.export {format?, path, scale?, artboard?}`: svg | png | pdf | jpg | webp | vectorcraft (format defaults to the extension). pub fn export(&mut self, p: &Value) -> Result { let path = s(p, "path").ok_or("missing `path`")?; let fmt = s(p, "format").map(str::to_ascii_lowercase).unwrap_or_else(|| ext_of(path)); @@ -263,7 +263,7 @@ impl Headless { let bytes = match fmt.as_str() { "svg" => { let ab = p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize; - drawcraft_svg::export(&doc, &drawcraft_svg::ExportOptions { artboard: Some(ab), ..Default::default() }).into_bytes() + vectorcraft_svg::export(&doc, &vectorcraft_svg::ExportOptions { artboard: Some(ab), ..Default::default() }).into_bytes() } "png" => { let idx = p.get("artboard").and_then(Value::as_u64).unwrap_or(0) as usize; @@ -271,7 +271,7 @@ impl Headless { let scale = p.get("scale").and_then(Value::as_f64).unwrap_or(1.0).clamp(0.01, 16.0); self.renderer.render_region(&doc, r, scale, false).to_png() } - "drawcraft" => drawcraft_format::save_file(&doc), + "vectorcraft" => vectorcraft_format::save_file(&doc), // pdf, jpg, webp…: the engine's exporter (same bytes as the app). other => { let params = json!({"path": path, "format": other, "scale": p.get("scale"), "artboard": p.get("artboard")}); @@ -310,7 +310,7 @@ impl Backend for Headless { "app.export" => self.export(p), "app.quit" => Ok(Value::Null), m if m.starts_with("ui.") => { - Err(format!("`{m}` needs the desktop app (start `drawcraft --control 7979` and use `drawcraft-cli mcp --connect`)")) + Err(format!("`{m}` needs the desktop app (start `vectorcraft --control 7979` and use `vectorcraft-cli mcp --connect`)")) } other => Err(format!("unknown method `{other}`")), } diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index e16035d52..74e751ab5 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -1,14 +1,14 @@ -//! DrawCraft's MCP server. +//! VectorCraft's MCP server. //! //! [Model Context Protocol](https://modelcontextprotocol.io) over stdio: newline-delimited -//! JSON-RPC 2.0, hand-written (no async runtime). The server exposes DrawCraft as a set of MCP +//! JSON-RPC 2.0, hand-written (no async runtime). The server exposes VectorCraft as a set of MCP //! tools and resources and forwards everything to a [`Backend`]: //! //! - [`Remote`] talks to a running desktop app through its loopback JSON-lines control channel -//! (`drawcraft --control 7979`): one `{"id","method","params"}` line in, one +//! (`vectorcraft --control 7979`): one `{"id","method","params"}` line in, one //! `{"id","ok","result"|"error"}` line out. -//! - [`Headless`] hosts an in-process [`drawcraft_engine::Session`] and implements the same -//! control-channel method names itself (rendering screenshots with `drawcraft-render`), so agents +//! - [`Headless`] hosts an in-process [`vectorcraft_engine::Session`] and implements the same +//! control-channel method names itself (rendering screenshots with `vectorcraft-render`), so agents //! can draw and look at the result without a window. //! //! Entry points: [`Server::serve`] (stdio loop) and [`Server::handle_line`] (one message). diff --git a/crates/mcp/src/server.rs b/crates/mcp/src/server.rs index 3cae76cc1..57ebb88c4 100644 --- a/crates/mcp/src/server.rs +++ b/crates/mcp/src/server.rs @@ -18,15 +18,15 @@ const INVALID_PARAMS: i64 = -32602; const INTERNAL_ERROR: i64 = -32603; const RESOURCE_NOT_FOUND: i64 = -32002; -const INSTRUCTIONS: &str = "DrawCraft is a vector illustration app (an Illustrator clone). Coordinates are points in \ +const INSTRUCTIONS: &str = "VectorCraft is a vector illustration app (an Illustrator clone). Coordinates are points in \ document space (y down, origin at the first artboard's top-left; a new document is 612×792). Draw with draw_shape / \ draw_path, change colours with set_paint, look with screenshot and inspect_document. Every menu action is a command: \ find it with list_commands and run it with run_command. New objects become the selection, and most commands act on the \ selection (or on explicit `ids`)."; /// Resource URIs. -pub const DOC_URI: &str = "drawcraft://document"; -pub const DOC_JSON_URI: &str = "drawcraft://document/json"; +pub const DOC_URI: &str = "vectorcraft://document"; +pub const DOC_JSON_URI: &str = "vectorcraft://document/json"; /// An MCP server bound to one backend. pub struct Server { @@ -133,7 +133,7 @@ impl Server { Ok(json!({ "protocolVersion": version, "capabilities": {"tools": {}, "resources": {}}, - "serverInfo": {"name": "drawcraft", "title": "DrawCraft", "version": env!("CARGO_PKG_VERSION")}, + "serverInfo": {"name": "vectorcraft", "title": "VectorCraft", "version": env!("CARGO_PKG_VERSION")}, "instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()), })) } diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index f34921ea6..d3302a6eb 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -28,7 +28,7 @@ fn text_of(result: &Value) -> String { } fn tmp(name: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("drawcraft-mcp-test-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("vectorcraft-mcp-test-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); dir.join(name) } @@ -79,7 +79,7 @@ fn serve_loop_writes_one_line_per_request() { let lines: Vec = String::from_utf8(out).unwrap().lines().map(|l| serde_json::from_str(l).unwrap()).collect(); assert_eq!(lines.len(), 2); assert_eq!(lines[0]["result"]["protocolVersion"], PROTOCOL_VERSION); - assert_eq!(lines[0]["result"]["serverInfo"]["name"], "drawcraft"); + assert_eq!(lines[0]["result"]["serverInfo"]["name"], "vectorcraft"); assert!(lines[0]["result"]["capabilities"]["tools"].is_object()); assert!(lines[0]["result"]["capabilities"]["resources"].is_object()); assert!(lines[1]["result"]["tools"].as_array().unwrap().len() >= 19); @@ -181,7 +181,7 @@ fn headless_end_to_end() { assert_eq!(r["isError"], false, "{r}"); let img = r["content"].as_array().unwrap().iter().find(|c| c["type"] == "image").expect("image content"); assert_eq!(img["mimeType"], "image/png"); - let png = drawcraft_format::base64_decode(img["data"].as_str().unwrap()).unwrap(); + let png = vectorcraft_format::base64_decode(img["data"].as_str().unwrap()).unwrap(); assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n"); assert_eq!(std::fs::read(&shot_path).unwrap(), png); @@ -193,7 +193,7 @@ fn headless_end_to_end() { assert!(svg.contains("(text).unwrap()["layers"].is_array()); - let v = rpc(&mut s, 12, "resources/read", json!({"uri": "drawcraft://document/json"})); + let v = rpc(&mut s, 12, "resources/read", json!({"uri": "vectorcraft://document/json"})); assert!(v["result"]["contents"][0]["text"].as_str().unwrap().len() > 10); - let v = rpc(&mut s, 13, "resources/read", json!({"uri": "drawcraft://nope"})); + let v = rpc(&mut s, 13, "resources/read", json!({"uri": "vectorcraft://nope"})); assert_eq!(v["error"]["code"], -32002); } diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 755cfb213..beba0dfa2 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -73,7 +73,7 @@ pub fn tool_definitions() -> Vec { tool( "list_commands", "List commands", - "List DrawCraft commands (id, label, menu path, shortcut, parameter description, enabled state). Every editing action is a command; run any of them with run_command. Coordinates are points in document space, y down, origin at the first artboard's top-left.", + "List VectorCraft commands (id, label, menu path, shortcut, parameter description, enabled state). Every editing action is a command; run any of them with run_command. Coordinates are points in document space, y down, origin at the first artboard's top-left.", obj( json!({"filter": string("Case-insensitive substring matched against id, label and menu path (e.g. \"align\", \"Object\")"), "enabledOnly": {"type": "boolean", "description": "Only commands that can run right now"}}), &[], @@ -83,7 +83,7 @@ pub fn tool_definitions() -> Vec { tool( "run_command", "Run command", - "Execute any DrawCraft command by id with JSON params (see list_commands for ids and params). Examples: {\"command\":\"object.group\"}, {\"command\":\"object.align\",\"params\":{\"align\":\"left\"}}, {\"command\":\"file.new\",\"params\":{\"width\":800,\"height\":600}}. Returns the command's result (e.g. {id} for creation commands).", + "Execute any VectorCraft command by id with JSON params (see list_commands for ids and params). Examples: {\"command\":\"object.group\"}, {\"command\":\"object.align\",\"params\":{\"align\":\"left\"}}, {\"command\":\"file.new\",\"params\":{\"width\":800,\"height\":600}}. Returns the command's result (e.g. {id} for creation commands).", obj( json!({"command": string("Command id, e.g. shape.rectangle, object.group, paint.setFill"), "params": {"type": "object", "description": "Command parameters"}}), &["command"], @@ -246,24 +246,24 @@ pub fn tool_definitions() -> Vec { tool( "open_file", "Open file", - "Open a .drawcraft or .svg file as a new, active document.", + "Open a .vectorcraft or .svg file as a new, active document.", obj(json!({"path": string("File path")}), &["path"]), false, ), tool( "save_file", "Save file", - "Save the active document in the native .drawcraft format.", + "Save the active document in the native .vectorcraft format.", obj(json!({"path": string("Destination (default: the document's current path)")}), &[]), false, ), tool( "export", "Export", - "Export the active document: svg (artboard viewBox), png (rendered artboard) or drawcraft (native).", + "Export the active document: svg (artboard viewBox), png (rendered artboard) or vectorcraft (native).", obj( json!({ - "format": {"type": "string", "enum": ["svg", "png", "pdf", "jpg", "webp", "drawcraft"], "description": "Default: from the path's extension"}, + "format": {"type": "string", "enum": ["svg", "png", "pdf", "jpg", "webp", "vectorcraft"], "description": "Default: from the path's extension"}, "path": string("Destination file"), "scale": num("PNG pixels per point (default 1)"), }), @@ -293,7 +293,7 @@ fn need_ui(b: &dyn Backend, tool: &str) -> Result<(), String> { Ok(()) } else { Err(format!( - "`{tool}` needs the desktop app: run `drawcraft --control 7979` and start the server with `drawcraft-cli mcp --connect 127.0.0.1:7979`" + "`{tool}` needs the desktop app: run `vectorcraft --control 7979` and start the server with `vectorcraft-cli mcp --connect 127.0.0.1:7979`" )) } } @@ -392,7 +392,7 @@ fn screenshot(b: &mut dyn Backend, a: &Args) -> Result { let target = match path { Some(p) => p, None => { - tmp = std::env::temp_dir().join(format!("drawcraft-window-{}.png", std::process::id())).to_string_lossy().to_string(); + tmp = std::env::temp_dir().join(format!("vectorcraft-window-{}.png", std::process::id())).to_string_lossy().to_string(); &tmp } }; @@ -412,7 +412,7 @@ fn screenshot(b: &mut dyn Backend, a: &Args) -> Result { let r = b.call("ui.render", p)?; let b64 = r.get("pngBase64").and_then(Value::as_str).ok_or("renderer returned no image")?; if let Some(path) = path { - let png = drawcraft_format::base64_decode(b64).ok_or("renderer returned bad base64")?; + let png = vectorcraft_format::base64_decode(b64).ok_or("renderer returned bad base64")?; std::fs::write(path, png).map_err(|e| format!("write {path}: {e}"))?; } Ok(ToolResult { @@ -427,7 +427,7 @@ fn screenshot(b: &mut dyn Backend, a: &Args) -> Result { fn image_result(png: &[u8], info: Value) -> ToolResult { ToolResult { content: vec![ - json!({"type": "image", "data": drawcraft_format::base64_encode(png), "mimeType": "image/png"}), + json!({"type": "image", "data": vectorcraft_format::base64_encode(png), "mimeType": "image/png"}), json!({"type": "text", "text": info.to_string()}), ], is_error: false, @@ -543,12 +543,12 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result Server { Server::new(Box::new(Headless::with_document())) @@ -29,7 +29,7 @@ fn call(s: &mut Server, name: &str, args: Value) -> Value { } fn document(s: &mut Server) -> Document { - let v = rpc(s, 2, "resources/read", json!({"uri": "drawcraft://document/json"})); + let v = rpc(s, 2, "resources/read", json!({"uri": "vectorcraft://document/json"})); let text = v["result"]["contents"][0]["text"].as_str().unwrap_or_else(|| panic!("{v}")); serde_json::from_str(text).expect("document JSON") } @@ -96,8 +96,8 @@ fn tool_definitions_are_valid() { #[test] fn every_tool_survives_empty_and_junk_arguments() { - let dir = drawcraft_testkit::temp_dir("mcp-junk"); - let safe = dir.join("junk.drawcraft").to_string_lossy().to_string(); + let dir = vectorcraft_testkit::temp_dir("mcp-junk"); + let safe = dir.join("junk.vectorcraft").to_string_lossy().to_string(); let mut failures = vec![]; for t in tool_definitions() { let name = t["name"].as_str().unwrap().to_string(); @@ -210,7 +210,7 @@ proptest! { #[test] fn run_command_sequences_match_local_session(ops in arb_ops(5..40)) { let mut srv = server(); - let mut local = drawcraft_testkit::fixtures::session_with(612.0, 792.0); + let mut local = vectorcraft_testkit::fixtures::session_with(612.0, 792.0); // Match the headless default document. let d_remote = doc_json(&document(&mut srv)); let d_local = doc_json(&local.doc().unwrap().doc); @@ -226,7 +226,7 @@ proptest! { check_document(&got).map_err(TestCaseError::fail)?; { let (g, l) = (doc_json(&got), doc_json(&local.doc().unwrap().doc)); // 1e-12: JSON text round trips aren't bit-exact (serde_json without float_roundtrip; see // crates/format/tests/prop_format.rs::bug_f64_bit_exact_roundtrip). - prop_assert!(drawcraft_testkit::invariants::json_approx_eq(&g, &l, 1e-12), "MCP and local sessions diverged: {}", drawcraft_testkit::invariants::first_diff(&l, &g, "$")); } + prop_assert!(vectorcraft_testkit::invariants::json_approx_eq(&g, &l, 1e-12), "MCP and local sessions diverged: {}", vectorcraft_testkit::invariants::first_diff(&l, &g, "$")); } } } @@ -235,8 +235,8 @@ proptest! { /// so an agent can continue it in the next call; that case is not listed.) #[test] fn tools_never_leave_interactions_open() { - use drawcraft_mcp::call_tool; - use drawcraft_testkit::invariants::check_session; + use vectorcraft_mcp::call_tool; + use vectorcraft_testkit::invariants::check_session; let cases = [ ("pointer_gesture", json!({"tool": "rectangle", "events": [{"kind": "down", "x": 10, "y": 10}, {"kind": "bogus", "x": 50, "y": 50}]})), ("pointer_gesture", json!({"tool": "pen", "events": [{"kind": "down", "x": 10, "y": 10}, {"kind": "drag", "x": "a", "y": 50}]})), diff --git a/crates/pathops/Cargo.toml b/crates/pathops/Cargo.toml index 3eaca6f64..87e80da85 100644 --- a/crates/pathops/Cargo.toml +++ b/crates/pathops/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "drawcraft-pathops" +name = "vectorcraft-pathops" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft path operations: booleans (Pathfinder), shape builder regions, offset, outline stroke, simplify" +description = "VectorCraft path operations: booleans (Pathfinder), shape builder regions, offset, outline stroke, simplify" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } +vectorcraft-geom = { workspace = true } kurbo = { workspace = true } thiserror = { workspace = true } linesweeper = { workspace = true } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } diff --git a/crates/pathops/README.md b/crates/pathops/README.md index 267da6642..5454f1c3a 100644 --- a/crates/pathops/README.md +++ b/crates/pathops/README.md @@ -1,6 +1,6 @@ -# drawcraft-pathops +# vectorcraft-pathops -Path operations for DrawCraft: booleans, the Pathfinder panel, Shape Builder regions, offset path, outline stroke, simplify and the other Object → Path commands. Everything takes and returns `drawcraft_geom::PathData`. +Path operations for VectorCraft: booleans, the Pathfinder panel, Shape Builder regions, offset path, outline stroke, simplify and the other Object → Path commands. Everything takes and returns `vectorcraft_geom::PathData`. ## Booleans (curve-preserving) diff --git a/crates/pathops/src/boolean.rs b/crates/pathops/src/boolean.rs index 5abf31e0b..41ed5d031 100644 --- a/crates/pathops/src/boolean.rs +++ b/crates/pathops/src/boolean.rs @@ -7,9 +7,9 @@ use std::collections::BTreeSet; -use drawcraft_geom::{Anchor, FillRule, PathData, SubPath}; use kurbo::{BezPath, CubicBez, PathSeg, Point, Shape as _}; use linesweeper::topology::{ContourIdx, Contours, Topology, WindingNumber}; +use vectorcraft_geom::{Anchor, FillRule, PathData, SubPath}; use crate::PathOpsError; use crate::fit::{end_tangent, fit_single, is_straight, sample, start_tangent}; diff --git a/crates/pathops/src/edit.rs b/crates/pathops/src/edit.rs index 7a03cffcb..e3e4aba74 100644 --- a/crates/pathops/src/edit.rs +++ b/crates/pathops/src/edit.rs @@ -1,8 +1,8 @@ //! Object → Path commands: Simplify, Smooth, Remove redundant points, Add Anchor Points, Average, //! Join, Split Into Grid. -use drawcraft_geom::{Anchor, PathData, SubPath}; use kurbo::{ParamCurveArclen, Point, Rect, Vec2}; +use vectorcraft_geom::{Anchor, PathData, SubPath}; use crate::boolean::{Seg, segs_to_subpath}; use crate::fit::{end_tangent, fit_cubics, fit_single, is_straight, sample, start_tangent}; diff --git a/crates/pathops/src/lib.rs b/crates/pathops/src/lib.rs index ad1c4c900..6908e26af 100644 --- a/crates/pathops/src/lib.rs +++ b/crates/pathops/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft path operations: booleans (Pathfinder), shape builder regions, offset, outline stroke, +//! VectorCraft path operations: booleans (Pathfinder), shape builder regions, offset, outline stroke, //! simplify and the other Object → Path commands. //! //! Booleans are curve-preserving: `linesweeper`'s robust sweep-line works directly on cubic diff --git a/crates/pathops/src/offset.rs b/crates/pathops/src/offset.rs index 1c821d57c..838180a9d 100644 --- a/crates/pathops/src/offset.rs +++ b/crates/pathops/src/offset.rs @@ -1,7 +1,7 @@ //! Offset Path and Outline Stroke. -use drawcraft_geom::{FillRule, PathData, SubPath}; use kurbo::{BezPath, PathEl, Stroke, StrokeOpts}; +use vectorcraft_geom::{FillRule, PathData, SubPath}; pub use kurbo::{Cap, Join}; diff --git a/crates/pathops/src/pathfinder.rs b/crates/pathops/src/pathfinder.rs index b6d808e4e..91f049fa7 100644 --- a/crates/pathops/src/pathfinder.rs +++ b/crates/pathops/src/pathfinder.rs @@ -1,8 +1,8 @@ //! Illustrator's Pathfinder panel and Shape Builder regions, over an ordered stack of shapes //! (index 0 = back-most, last = front-most). -use drawcraft_geom::{FillRule, PathData}; use kurbo::{BezPath, ParamCurve, ParamCurveNearest, Point, Shape as _}; +use vectorcraft_geom::{FillRule, PathData}; use crate::boolean::{Arrangement, Seg, all_contours_to_path, contours_to_path, fill_bezpath, segs_to_subpath, unite_all}; diff --git a/crates/pathops/tests/ops.rs b/crates/pathops/tests/ops.rs index 2ac9eecd2..1bf29736e 100644 --- a/crates/pathops/tests/ops.rs +++ b/crates/pathops/tests/ops.rs @@ -1,8 +1,8 @@ use std::f64::consts::PI; -use drawcraft_geom::{FillRule, PathData, SubPath}; -use drawcraft_pathops::*; use kurbo::{ParamCurve, Point, Rect, Shape as _}; +use vectorcraft_geom::{FillRule, PathData, SubPath}; +use vectorcraft_pathops::*; const NZ: FillRule = FillRule::NonZero; diff --git a/crates/pathops/tests/prop_pathops.rs b/crates/pathops/tests/prop_pathops.rs index c6ba9cc88..83d6e2b5e 100644 --- a/crates/pathops/tests/prop_pathops.rs +++ b/crates/pathops/tests/prop_pathops.rs @@ -1,19 +1,21 @@ //! Path-operation properties on concave polygons, ellipses and random smooth curves (complements //! `props.rs`, which uses rectangles and circles). -use drawcraft_geom::{FillRule, PathData, Point, Rect}; -use drawcraft_pathops::*; -use drawcraft_testkit::geom::{directed_hausdorff, grid, hausdorff, polygon_area, rel_close}; -use drawcraft_testkit::strategies::{arb_closed_shape, arb_convex_polygon, arb_ellipse, arb_rect, arb_smooth_curve, arb_star_polygon, polygon_path}; use proptest::prelude::*; +use vectorcraft_geom::{FillRule, PathData, Point, Rect}; +use vectorcraft_pathops::*; +use vectorcraft_testkit::geom::{directed_hausdorff, grid, hausdorff, polygon_area, rel_close}; +use vectorcraft_testkit::strategies::{ + arb_closed_shape, arb_convex_polygon, arb_ellipse, arb_rect, arb_smooth_curve, arb_star_polygon, polygon_path, +}; const NZ: FillRule = FillRule::NonZero; fn inside(p: &PathData, q: Point) -> bool { - drawcraft_geom::hit::fill_contains(&p.to_bezpath(), NZ, q) + vectorcraft_geom::hit::fill_contains(&p.to_bezpath(), NZ, q) } fn near_edge(p: &PathData, q: Point, eps: f64) -> bool { - drawcraft_geom::hit::distance_to_outline(&p.to_bezpath(), q) < eps + vectorcraft_geom::hit::distance_to_outline(&p.to_bezpath(), q) < eps } proptest! { @@ -203,9 +205,9 @@ proptest! { #[test] fn non_finite_input_is_an_error() { - let mut p = drawcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)); + let mut p = vectorcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)); p.subpaths[0].anchors[1].p.x = f64::NAN; - let q = drawcraft_geom::shapes::rectangle(Rect::new(5.0, 5.0, 15.0, 15.0)); + let q = vectorcraft_geom::shapes::rectangle(Rect::new(5.0, 5.0, 15.0, 15.0)); assert_eq!(try_boolean(&p, NZ, &q, NZ, BoolOp::Union, DEFAULT_PRECISION), Err(PathOpsError::NonFinite)); assert!(boolean(&p, NZ, &q, NZ, BoolOp::Union).is_empty()); assert!(offset_path(&q, f64::NAN, Join::Round, 4.0).is_empty()); @@ -215,7 +217,7 @@ fn non_finite_input_is_an_error() { #[test] fn empty_inputs() { let e = PathData::default(); - let q = drawcraft_geom::shapes::rectangle(Rect::new(5.0, 5.0, 15.0, 15.0)); + let q = vectorcraft_geom::shapes::rectangle(Rect::new(5.0, 5.0, 15.0, 15.0)); assert!(rel_close(area(&boolean(&e, NZ, &q, NZ, BoolOp::Union), NZ), 100.0, 1e-9)); assert!(boolean(&e, NZ, &q, NZ, BoolOp::Intersect).is_empty()); assert!(pathfinder(PathfinderOp::Divide, &[]).is_empty()); @@ -225,7 +227,7 @@ fn empty_inputs() { #[test] fn bug_simplify_adds_anchors() { - use drawcraft_geom::{Anchor, SubPath}; + use vectorcraft_geom::{Anchor, SubPath}; let a = |p: (f64, f64), i: (f64, f64), o: (f64, f64)| Anchor::with_handles(p.into(), i.into(), o.into()); let c = PathData::single(SubPath::new( vec![ @@ -245,7 +247,7 @@ fn bug_simplify_adds_anchors() { #[test] fn offset_large_delta_keeps_area() { for (r, d) in [(1.0, 5.0), (1.0, 3.0), (10.0, 30.0)] { - let p = drawcraft_geom::shapes::ellipse(Rect::new(0.0, 0.0, 2.0 * r, 2.0 * r)); + let p = vectorcraft_geom::shapes::ellipse(Rect::new(0.0, 0.0, 2.0 * r, 2.0 * r)); let got = area(&offset_path(&p, d, Join::Round, 4.0), NZ); let want = std::f64::consts::PI * (r + d) * (r + d); assert!(rel_close(got, want, 1e-2), "r={r} d={d}: {got} vs {want}"); diff --git a/crates/pathops/tests/props.rs b/crates/pathops/tests/props.rs index b124e593b..9ffadb0b2 100644 --- a/crates/pathops/tests/props.rs +++ b/crates/pathops/tests/props.rs @@ -1,9 +1,9 @@ use std::f64::consts::PI; -use drawcraft_geom::{FillRule, PathData, SubPath}; -use drawcraft_pathops::*; use kurbo::{ParamCurve, Point, Rect, Shape as _}; use proptest::prelude::*; +use vectorcraft_geom::{FillRule, PathData, SubPath}; +use vectorcraft_pathops::*; const NZ: FillRule = FillRule::NonZero; diff --git a/crates/pdf/Cargo.toml b/crates/pdf/Cargo.toml index 69f6fa6d9..b960417f0 100644 --- a/crates/pdf/Cargo.toml +++ b/crates/pdf/Cargo.toml @@ -1,18 +1,18 @@ [package] -name = "drawcraft-pdf" +name = "vectorcraft-pdf" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft PDF export and import" +description = "VectorCraft PDF export and import" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-text = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-text = { workspace = true } kurbo = { workspace = true } thiserror = { workspace = true } image = { workspace = true } diff --git a/crates/pdf/src/export.rs b/crates/pdf/src/export.rs index ffee4341f..dd5399842 100644 --- a/crates/pdf/src/export.rs +++ b/crates/pdf/src/export.rs @@ -1,10 +1,7 @@ -//! Document → PDF (krilla). Mirrors the tree walk of `drawcraft-render`. +//! Document → PDF (krilla). Mirrors the tree walk of `vectorcraft-render`. use std::collections::HashMap; -use drawcraft_color::{BlendMode, Color, GradientKind, Paint}; -use drawcraft_doc::{AppearanceItem, Arrowhead, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, TextObject}; -use drawcraft_geom::{Affine, BezPath, FillRule, Rect}; use krilla::color::{cmyk, luma, rgb}; use krilla::configure::{Archival, ConfigurationBuilder, PdfVersion}; use krilla::geom::{Path, PathBuilder, Size, Transform}; @@ -15,6 +12,9 @@ use krilla::page::PageSettings; use krilla::paint::{Fill, LinearGradient, RadialGradient, SpreadMethod, Stop, Stroke, StrokeDash}; use krilla::surface::Surface; use kurbo::{ParamCurve, ParamCurveDeriv, PathEl, Shape, Vec2}; +use vectorcraft_color::{BlendMode, Color, GradientKind, Paint}; +use vectorcraft_doc::{AppearanceItem, Arrowhead, Document, LineCap, LineJoin, Node, NodeKind, StrokeAlign, StrokeLayer, TextObject}; +use vectorcraft_geom::{Affine, BezPath, FillRule, Rect}; use crate::{Compatibility, ExportReport, PdfError, PdfOptions}; @@ -57,7 +57,7 @@ pub fn export_with_report(doc: &Document, opts: &PdfOptions) -> Result krilla::color::Color { match *c { Color::Rgb { r, g, b } => rgb::Color::new(q(r), q(g), q(b)).into(), Color::Cmyk { c, m, y, k } => cmyk::Color::new(q(c), q(m), q(y), q(k)).into(), - // DrawCraft grey is ink coverage (0 = white); PDF DeviceGray is lightness. + // VectorCraft grey is ink coverage (0 = white); PDF DeviceGray is lightness. Color::Gray { k } => luma::Color::new(q(1.0 - k)).into(), } } @@ -200,8 +200,8 @@ impl Exporter<'_> { /// A colour for the page: in CMYK documents RGB colours are separated into DeviceCMYK through /// the active colour settings, so the file carries press values. fn col(&mut self, c: &Color) -> krilla::color::Color { - if self.doc.color_mode == drawcraft_doc::ColorMode::Cmyk && matches!(c, Color::Rgb { .. }) { - let cms = drawcraft_color::cms::active(); + if self.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk && matches!(c, Color::Rgb { .. }) { + let cms = vectorcraft_color::cms::active(); let [cc, m, y, k] = cms.to_cmyk(c, cms.settings().intent); return cmyk::Color::new(q(cc), q(m), q(y), q(k)).into(); } @@ -216,7 +216,7 @@ impl Exporter<'_> { .and_then(|n| self.doc.swatches.iter().find(|s| s.spot && s.name == n)) .and_then(|s| s.paint.color().map(|sc| (s.name.clone(), sc))); let Some((name, sc)) = spot else { return self.col(c) }; - let cms = drawcraft_color::cms::active(); + let cms = vectorcraft_color::cms::active(); let intent = cms.settings().intent; let full = cms.to_cmyk(&sc, intent); let total: f32 = full.iter().sum(); @@ -317,7 +317,7 @@ impl Exporter<'_> { /// Opacity mask → luminosity soft mask. Outside the art the backdrop is black (clip) or white; /// invert is a white Difference rect on top (luminance is linear, so luma(1 − c) = 1 − luma(c)). - fn soft_mask(&mut self, s: &mut Surface, m: &drawcraft_doc::OpacityMask, page: Rect) -> krilla::mask::Mask { + fn soft_mask(&mut self, s: &mut Surface, m: &vectorcraft_doc::OpacityMask, page: Rect) -> krilla::mask::Mask { let backdrop = to_path(&page.inflate(1.0, 1.0).to_path(0.1)); let white = || Fill { paint: rgb::Color::new(255, 255, 255).into(), opacity: NormalizedF32::ONE, rule: krilla::paint::FillRule::NonZero }; let mut sb = s.stream_builder(); @@ -444,7 +444,7 @@ impl Exporter<'_> { } // Live blends/envelopes/meshes export their evaluated (expanded) form. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = drawcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_doc::live::expand_deep(n, None); for c in g.children().into_iter().flatten() { self.node(s, c, page, false); } @@ -589,7 +589,7 @@ impl Exporter<'_> { } fn text(&mut self, s: &mut Surface, n: &Node, t: &TextObject) { - let layout = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let tb = t.xf.transform_rect_bbox(layout.bounds); s.push_transform(&xf(t.xf)); for (i, run) in t.runs.iter().enumerate() { @@ -668,7 +668,7 @@ impl Exporter<'_> { img } - fn image(&mut self, s: &mut Surface, im: &drawcraft_doc::ImageObject) { + fn image(&mut self, s: &mut Surface, im: &vectorcraft_doc::ImageObject) { let Some(img) = self.load_image(&im.key) else { return }; let Some(size) = Size::from_wh(im.width.max(1) as f32, im.height.max(1) as f32) else { return }; s.push_transform(&xf(im.xf)); diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index 8bad725b2..ddfefa2a2 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -1,14 +1,8 @@ -//! PDF → Document (hayro-interpret device that builds a DrawCraft node tree). +//! PDF → Document (hayro-interpret device that builds a VectorCraft node tree). use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use drawcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; -use drawcraft_doc::{ - Appearance, AppearanceItem, Artboard, Dash, Document, FillLayer, ImageBlob, ImageObject, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, - StrokeLayer, -}; -use drawcraft_geom::{FillRule, PathData}; use hayro_interpret::font::Glyph; use hayro_interpret::pattern::Pattern; use hayro_interpret::shading::ShadingType; @@ -19,6 +13,12 @@ use hayro_interpret::{ use hayro_syntax::Pdf; use hayro_syntax::object::Name; use kurbo::{Affine, BezPath, Point, Rect, Shape, Vec2}; +use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::{ + Appearance, AppearanceItem, Artboard, Dash, Document, FillLayer, ImageBlob, ImageObject, LayerColor, LineCap, LineJoin, Node, NodeId, NodeKind, + StrokeLayer, +}; +use vectorcraft_geom::{FillRule, PathData}; use crate::{ImportOptions, ImportReport, PdfError}; @@ -325,7 +325,7 @@ impl Builder { self.push_node(n); } - /// Convert a hayro paint to a DrawCraft paint and opacity. `bbox` is the painted area in + /// Convert a hayro paint to a VectorCraft paint and opacity. `bbox` is the painted area in /// document coordinates (for patterns). fn paint(&mut self, p: &hayro_interpret::Paint<'_>) -> (Paint, f32) { match p { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index 25ca6f4ee..889f22245 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft PDF export and import. +//! VectorCraft PDF export and import. //! //! - [`export`] writes one PDF page per artboard with `krilla`: vector paths (fills, strokes with //! caps/joins/miter/dashes, non-zero/even-odd), opacity and blend modes (transparency groups), @@ -16,7 +16,7 @@ mod import; pub use export::{export, export_with_report}; pub use import::{import, import_with_report}; -use drawcraft_doc::Document; +use vectorcraft_doc::Document; /// PDF standard / version to target. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index 61c1c8807..82e212184 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -1,10 +1,10 @@ use std::sync::Arc; -use drawcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, Paint}; -use drawcraft_doc::{ +use vectorcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, Paint}; +use vectorcraft_doc::{ Appearance, AppearanceItem, Artboard, CharStyle, Dash, Document, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, TextObject, }; -use drawcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; +use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; use crate::*; @@ -68,7 +68,7 @@ fn metadata_creator_and_title() { let mut d = doc(100.0, 100.0); d.title = "Poster".into(); let s = uncompressed(&d); - assert!(s.contains("DrawCraft")); + assert!(s.contains("VectorCraft")); assert!(s.contains("Poster")); } @@ -178,7 +178,7 @@ fn roundtrip_stroke_params_merge_fill_and_stroke() { #[test] fn roundtrip_open_path_and_even_odd() { let mut d = doc(200.0, 200.0); - let mut bp = drawcraft_geom::BezPath::new(); + let mut bp = vectorcraft_geom::BezPath::new(); bp.move_to((10.0, 10.0)); bp.curve_to((50.0, 0.0), (80.0, 100.0), (150.0, 60.0)); add(&mut d, Node::path(NodeId(0), PathData::from_bezpath(&bp), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0))); @@ -202,7 +202,7 @@ fn roundtrip_open_path_and_even_odd() { trait Exact { fn bounding_box_exact(&self) -> Rect; } -impl Exact for drawcraft_geom::BezPath { +impl Exact for vectorcraft_geom::BezPath { fn bounding_box_exact(&self) -> Rect { kurbo::Shape::bounding_box(self) } @@ -348,7 +348,7 @@ fn text_exported_as_outlines() { let l = leaves(&r.document); assert!(!l.is_empty()); assert!(l.iter().all(|n| matches!(n.kind, NodeKind::Path { .. }))); - let b = l.iter().fold(None, |acc, n| drawcraft_geom::union_opt(acc, n.geometric_bounds())).unwrap(); + let b = l.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.geometric_bounds())).unwrap(); assert!(b.x0 >= 9.0 && b.x1 < 120.0 && b.y1 <= 56.0 && b.y0 > 25.0, "{b:?}"); } @@ -503,14 +503,14 @@ fn stroke_alignment_and_arrowheads_export() { shapes::rectangle(Rect::new(20.0, 20.0, 120.0, 120.0)), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 6.0), ); - p.appearance.stroke_mut().unwrap().align = drawcraft_doc::StrokeAlign::Inside; + p.appearance.stroke_mut().unwrap().align = vectorcraft_doc::StrokeAlign::Inside; add(&mut d, p); let mut line = Node::path( NodeId(0), shapes::line(Point::new(10.0, 150.0), Point::new(150.0, 150.0)), Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 2.0), ); - line.appearance.stroke_mut().unwrap().end_arrow = Some(drawcraft_doc::Arrowhead::Triangle); + line.appearance.stroke_mut().unwrap().end_arrow = Some(vectorcraft_doc::Arrowhead::Triangle); add(&mut d, line); let r = export_with_report(&d, &PdfOptions::default()).unwrap(); let out = import(&r.bytes).unwrap(); @@ -538,7 +538,7 @@ fn opacity_mask_exports_as_luminosity_soft_mask() { let mut d = doc(100.0, 100.0); let mut n = rect_node(Rect::new(10.0, 10.0, 90.0, 90.0), Color::rgb(1.0, 0.0, 0.0)); let art = rect_node(Rect::new(10.0, 10.0, 50.0, 90.0), Color::WHITE); - n.mask = Some(Box::new(drawcraft_doc::OpacityMask::new(art, true))); + n.mask = Some(Box::new(vectorcraft_doc::OpacityMask::new(art, true))); add(&mut d, n); let bytes = export(&d, &PdfOptions { compress: false, ..Default::default() }).unwrap(); let text = String::from_utf8_lossy(&bytes); diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 70b727a8a..e4bc84570 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -1,20 +1,20 @@ [package] -name = "drawcraft-render" +name = "vectorcraft-render" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft renderer: document to pixels (vello_cpu backend)" +description = "VectorCraft renderer: document to pixels (vello_cpu backend)" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-text = { workspace = true } -drawcraft-effects = { workspace = true } -drawcraft-brush = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-text = { workspace = true } +vectorcraft-effects = { workspace = true } +vectorcraft-brush = { workspace = true } vello_cpu = { workspace = true } # Filter types (Gaussian blur, drop shadow) are not re-exported by vello_cpu 0.2. vello_common = { version = "0.2", default-features = false } @@ -25,6 +25,6 @@ log = { workspace = true } vello_cpu = { workspace = true, features = ["multithreading"] } [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } serde_json = { workspace = true } diff --git a/crates/render/examples/bench.rs b/crates/render/examples/bench.rs index ce6c3cf86..470f418b6 100644 --- a/crates/render/examples/bench.rs +++ b/crates/render/examples/bench.rs @@ -1,9 +1,9 @@ -//! `cargo run --release -p drawcraft-render --example bench` — render timing on synthetic documents. +//! `cargo run --release -p vectorcraft-render --example bench` — render timing on synthetic documents. -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, Document, Node}; -use drawcraft_geom::{Affine, Point, Rect, shapes}; -use drawcraft_render::{RenderOptions, Renderer}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, Node}; +use vectorcraft_geom::{Affine, Point, Rect, shapes}; +use vectorcraft_render::{RenderOptions, Renderer}; fn rng(seed: &mut u64) -> f64 { *seed ^= *seed << 13; @@ -42,12 +42,12 @@ fn main() { let x = rng(&mut seed) * 1500.0; let y = rng(&mut seed) * 1150.0 + 20.0; let id = d.alloc_id(); - let t = drawcraft_doc::TextObject::point( + let t = vectorcraft_doc::TextObject::point( Point::new(x, y), - &format!("Label {i} — DrawCraft"), - drawcraft_doc::CharStyle { size: 10.0 + (i % 5) as f64 * 4.0, ..Default::default() }, + &format!("Label {i} — VectorCraft"), + vectorcraft_doc::CharStyle { size: 10.0 + (i % 5) as f64 * 4.0, ..Default::default() }, ); - d.insert(Some(l), usize::MAX, Node::new(id, drawcraft_doc::NodeKind::Text(Box::new(t)))).unwrap(); + d.insert(Some(l), usize::MAX, Node::new(id, vectorcraft_doc::NodeKind::Text(Box::new(t)))).unwrap(); } let mut r = Renderer::new(); if let Ok(t) = std::env::var("THREADS") { diff --git a/crates/render/src/brush_fx.rs b/crates/render/src/brush_fx.rs index e3d0bf3d4..2fa32741d 100644 --- a/crates/render/src/brush_fx.rs +++ b/crates/render/src/brush_fx.rs @@ -1,6 +1,6 @@ //! Brushed strokes and symbol-instance staining in the renderer. //! -//! A stroke with `brush: Some(name)` is painted as the filled art `drawcraft_brush::stroke_pieces` +//! A stroke with `brush: Some(name)` is painted as the filled art `vectorcraft_brush::stroke_pieces` //! generates. Pieces are cached per (node, stroke item) and validated by the node's path, the //! stroke layer and the brush definition (an `Arc` from the per-frame parsed library), so edits to //! any of them regenerate the art and nothing else does. @@ -8,9 +8,9 @@ use std::collections::HashMap; use std::sync::Arc; -use drawcraft_brush::Brush; -use drawcraft_doc::{AppearanceItem, Document, Node, StrokeLayer}; -use drawcraft_geom::{BezPath, Rect}; +use vectorcraft_brush::Brush; +use vectorcraft_doc::{AppearanceItem, Document, Node, StrokeLayer}; +use vectorcraft_geom::{BezPath, Rect}; use vello_cpu::RenderContext; use crate::{Frame, Renderer, blend_mode}; @@ -27,7 +27,7 @@ struct Entry { #[derive(Default)] pub(crate) struct BrushCache { /// The document value the library was parsed from (`None` = defaults). - src: Option>, + src: Option>, /// (frame stamp, document address) the library was last validated for. checked: (u64, usize), lib: HashMap>, @@ -41,19 +41,19 @@ impl BrushCache { return; } self.checked = key; - let cur = doc.unknown.get(drawcraft_brush::DOC_KEY); + let cur = doc.unknown.get(vectorcraft_brush::DOC_KEY); if self.src.as_ref().is_some_and(|s| s.as_ref() == cur) { return; } self.src = Some(cur.cloned()); - self.lib = drawcraft_brush::library(doc).into_iter().map(|b| (b.name.clone(), Arc::new(b))).collect(); + self.lib = vectorcraft_brush::library(doc).into_iter().map(|b| (b.name.clone(), Arc::new(b))).collect(); } } /// Cull bounds that allow for brush art reaching past the stroke (scatter, wide nibs). pub(crate) fn cull_bounds(n: &Node) -> Option { let b = crate::fx::cull_bounds(n)?; - if !drawcraft_brush::has_brush(n) { + if !vectorcraft_brush::has_brush(n) { return Some(b); } let w = n.appearance.items.iter().filter_map(|i| if let AppearanceItem::Stroke(s) = i { Some(s.width) } else { None }).fold(1.0, f64::max); @@ -69,7 +69,7 @@ pub(crate) fn instance_art(art: &Node, inst: &Node) -> Node { && f.visible && let Some(c) = f.paint.color() { - drawcraft_brush::tint_node(&mut a, &c, f.opacity); + vectorcraft_brush::tint_node(&mut a, &c, f.opacity); } a } @@ -90,7 +90,7 @@ impl Renderer { e.pieces.clone() } _ => { - let pieces = Arc::new(drawcraft_brush::stroke_pieces(&brush, bp, st)); + let pieces = Arc::new(vectorcraft_brush::stroke_pieces(&brush, bp, st)); if self.brushes.entries.len() > 2048 { self.brushes.entries.retain(|_, e| stamp.saturating_sub(e.stamp) <= 3); } @@ -98,9 +98,9 @@ impl Renderer { pieces } }; - let layered = st.opacity < 1.0 || st.blend != drawcraft_color::BlendMode::Normal; + let layered = st.opacity < 1.0 || st.blend != vectorcraft_color::BlendMode::Normal; if layered { - ctx.set_transform(drawcraft_geom::Affine::IDENTITY); + ctx.set_transform(vectorcraft_geom::Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(st.blend)), Some(st.opacity), None, None); } for p in pieces.iter() { @@ -115,9 +115,9 @@ impl Renderer { #[cfg(test)] mod tests { - use drawcraft_color::{Color, Paint}; - use drawcraft_doc::{Appearance, Document, Node}; - use drawcraft_geom::{Affine, Rect, shapes}; + use vectorcraft_color::{Color, Paint}; + use vectorcraft_doc::{Appearance, Document, Node}; + use vectorcraft_geom::{Affine, Rect, shapes}; use crate::{RenderOptions, Rendered, Renderer}; @@ -169,11 +169,11 @@ mod tests { let mut r = Renderer::new(); let mut d = doc_with_line(Some("3 pt. Round"), 1.0); let a = render(&mut r, &d); - let mut lib = drawcraft_brush::library(&d); - if let drawcraft_brush::BrushKind::Calligraphic(c) = &mut lib[0].kind { + let mut lib = vectorcraft_brush::library(&d); + if let vectorcraft_brush::BrushKind::Calligraphic(c) = &mut lib[0].kind { c.size = 20.0; } - drawcraft_brush::store(&mut d, &lib); + vectorcraft_brush::store(&mut d, &lib); let b = render(&mut r, &d); assert!(!dark(a.pixel(100, 58)) && dark(b.pixel(100, 58))); } @@ -182,13 +182,13 @@ mod tests { fn stained_symbol_instance() { let mut d = Document::new(100.0, 100.0); let art = Node::path( - drawcraft_doc::NodeId(0), + vectorcraft_doc::NodeId(0), shapes::rectangle(Rect::new(-10.0, -10.0, 10.0, 10.0)), Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0), ); - d.symbols.push(drawcraft_doc::Symbol { name: "S".into(), art: std::sync::Arc::new(art) }); + d.symbols.push(vectorcraft_doc::Symbol { name: "S".into(), art: std::sync::Arc::new(art) }); let id = d.alloc_id(); - let mut inst = Node::new(id, drawcraft_doc::NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((50.0, 50.0)) }); + let mut inst = Node::new(id, vectorcraft_doc::NodeKind::SymbolInstance { symbol: "S".into(), xf: Affine::translate((50.0, 50.0)) }); inst.appearance.set_fill(Paint::solid(Color::rgb(1.0, 0.0, 0.0))); let l = d.layers[0].id; d.insert(Some(l), 0, inst).unwrap(); diff --git a/crates/render/src/fx.rs b/crates/render/src/fx.rs index 040be86e2..3696652fa 100644 --- a/crates/render/src/fx.rs +++ b/crates/render/src/fx.rs @@ -1,7 +1,7 @@ //! Live effects in the renderer. //! //! Geometry effects (object-level, then per fill/stroke) rewrite the path before painting via -//! `drawcraft-effects`. Raster effects use vello_cpu filter layers. Those only work in +//! `vectorcraft-effects`. Raster effects use vello_cpu filter layers. Those only work in //! single-threaded contexts, so on the multithreaded pipeline each filter layer is rendered //! offscreen on the calling thread (cropped to its reach) and composited back with the same blend //! mode and opacity ([`Renderer::with_filters`]): @@ -13,9 +13,9 @@ //! - Inner Glow: a blurred inverse silhouette (Edge) or the blurred silhouette (Center), clipped //! to the shape and painted above it. -use drawcraft_doc::{AppearanceItem, Effect, Node, NodeKind}; -use drawcraft_effects::{self as effects, GeomContext, RasterFx}; -use drawcraft_geom::{Affine, BezPath, FillRule, Rect, Shape}; +use vectorcraft_doc::{AppearanceItem, Effect, Node, NodeKind}; +use vectorcraft_effects::{self as effects, GeomContext, RasterFx}; +use vectorcraft_geom::{Affine, BezPath, FillRule, Rect, Shape}; use vello_common::filter_effects::{EdgeMode, Filter, FilterPrimitive}; use vello_cpu::RenderContext; use vello_cpu::peniko; @@ -103,7 +103,7 @@ pub(crate) fn visual_bounds(n: &Node) -> Option { pub(crate) fn cull_bounds(n: &Node) -> Option { match &n.kind { NodeKind::Layer { children, .. } | NodeKind::Group { children, clip: false } => { - children.iter().fold(None, |acc, c| drawcraft_geom::union_opt(acc, cull_bounds(c))) + children.iter().fold(None, |acc, c| vectorcraft_geom::union_opt(acc, cull_bounds(c))) } _ if has_fx(n) => visual_bounds(n), _ => n.visual_bounds(), @@ -124,7 +124,7 @@ fn blur_filter(sigma: f64) -> Filter { Filter::from_primitive(FilterPrimitive::GaussianBlur { std_deviation: sigma.max(0.0) as f32, edge_mode: EdgeMode::None }) } -fn pcolor(c: &drawcraft_doc::color::Color) -> peniko::Color { +fn pcolor(c: &vectorcraft_doc::color::Color) -> peniko::Color { let [r, g, b] = c.to_rgb(); peniko::Color::new([r, g, b, 1.0]) } @@ -160,7 +160,7 @@ impl Renderer { }; let mut gs = g.clone(); gs.apply_affine(Affine::translate((dx, dy))); - let reach = reach + drawcraft_geom::Vec2::new(dx, dy); + let reach = reach + vectorcraft_geom::Vec2::new(dx, dy); self.with_filters(ctx, f, reach, blur, Some((mode, opacity)), |r, c, fr, comp| { c.set_transform(fr.view); c.push_layer(None, comp.map(|m| blend_mode(m.0)), comp.map(|m| m.1), None, Some(filter)); @@ -251,8 +251,8 @@ impl Renderer { f: &Frame, reach: Rect, blur: f64, - composite: Option<(drawcraft_doc::color::BlendMode, f32)>, - draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame, Option<(drawcraft_doc::color::BlendMode, f32)>), + composite: Option<(vectorcraft_doc::color::BlendMode, f32)>, + draw: impl FnOnce(&mut Self, &mut RenderContext, &Frame, Option<(vectorcraft_doc::color::BlendMode, f32)>), ) { if !f.mt { return draw(self, ctx, f, composite); @@ -273,7 +273,7 @@ impl Renderer { off.flush(); let mut pm = vello_cpu::Pixmap::new(w, h); off.render(&mut pm, &mut self.resources); - let layered = composite.is_some_and(|(m, o)| m != drawcraft_doc::color::BlendMode::Normal || o < 1.0); + let layered = composite.is_some_and(|(m, o)| m != vectorcraft_doc::color::BlendMode::Normal || o < 1.0); if let Some((m, o)) = composite.filter(|_| layered) { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(m)), Some(o), None, None); @@ -298,7 +298,7 @@ impl Renderer { if !fl.visible || fl.paint.is_none() { continue; } - let layered = fl.opacity < 1.0 || fl.blend != drawcraft_doc::color::BlendMode::Normal; + let layered = fl.opacity < 1.0 || fl.blend != vectorcraft_doc::color::BlendMode::Normal; if layered { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(fl.blend)), Some(fl.opacity), None, None); @@ -329,10 +329,10 @@ impl Renderer { #[cfg(test)] mod tests { - use drawcraft_doc::color::{Color, Paint}; - use drawcraft_doc::{Appearance, Document, Effect, Node}; - use drawcraft_geom::{Affine, Rect, shapes}; use serde_json::json; + use vectorcraft_doc::color::{Color, Paint}; + use vectorcraft_doc::{Appearance, Document, Effect, Node}; + use vectorcraft_geom::{Affine, Rect, shapes}; use crate::{RenderOptions, Rendered, Renderer}; @@ -376,8 +376,8 @@ mod tests { let id = d.layers[0].children().unwrap()[0].id; let mut n = (*d.node(id).unwrap()).clone(); n.appearance = Appearance::basic(Paint::None, Paint::solid(Color::BLACK), 4.0); - if let Some(drawcraft_doc::AppearanceItem::Stroke(s)) = - n.appearance.items.iter_mut().find(|i| matches!(i, drawcraft_doc::AppearanceItem::Stroke(_))) + if let Some(vectorcraft_doc::AppearanceItem::Stroke(s)) = + n.appearance.items.iter_mut().find(|i| matches!(i, vectorcraft_doc::AppearanceItem::Stroke(_))) { s.effects = vec![Effect { id: "stylize.outerGlow".into(), params: json!({"color": "#ff0000", "opacity": 100, "blur": 6}), visible: true }]; diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 2ba4a40e2..44d167732 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1,9 +1,9 @@ -//! DrawCraft renderer: document → premultiplied RGBA pixels. +//! VectorCraft renderer: document → premultiplied RGBA pixels. //! //! The backend is `vello_cpu` (SIMD, sparse strips). Callers give a *view transform* mapping //! document points to output pixels; the renderer culls by bounds, evaluates appearance stacks //! (multiple fills/strokes, opacity, blend modes, stroke alignment, dashes), clip groups, -//! gradients, images and text (via `drawcraft-text` glyph outlines). +//! gradients, images and text (via `vectorcraft-text` glyph outlines). #![forbid(unsafe_code)] mod brush_fx; @@ -17,15 +17,15 @@ mod width; use std::collections::HashMap; use std::sync::Arc; -use drawcraft_doc::{AppearanceItem, Document, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextObject}; -use drawcraft_geom::{Affine, BezPath, FillRule, Rect, Shape}; +use vectorcraft_doc::{AppearanceItem, Document, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextObject}; +use vectorcraft_geom::{Affine, BezPath, FillRule, Rect, Shape}; use vello_cpu::kurbo; use vello_cpu::peniko::{self, BlendMode, Compose, Mix}; use vello_cpu::{Pixmap, RenderContext, Resources}; -pub use drawcraft_effects as effects; pub use live::expand_live; pub use pattern::render_pattern_swatch; +pub use vectorcraft_effects as effects; pub use vello_cpu; pub use width::width_outline; @@ -344,7 +344,7 @@ impl Renderer { let mut acc: Option = None; for c in children { if c.visible { - acc = drawcraft_geom::union_opt(acc, self.bounds_of(c)); + acc = vectorcraft_geom::union_opt(acc, self.bounds_of(c)); } } acc @@ -405,7 +405,7 @@ impl Renderer { } /// Render an opacity mask's art offscreen and turn its luminance into a coverage mask. - fn opacity_mask(&mut self, f: &Frame, m: &drawcraft_doc::OpacityMask, w: u16, h: u16) -> vello_cpu::Mask { + fn opacity_mask(&mut self, f: &Frame, m: &vectorcraft_doc::OpacityMask, w: u16, h: u16) -> vello_cpu::Mask { let mut mctx = single_threaded_context(w, h); self.draw_node(&mut mctx, &Frame { mt: false, ..*f }, &m.art, true); mctx.flush(); @@ -420,7 +420,7 @@ impl Renderer { // Fast path for plain paths: cached geometry, opacity folded into the paint. if let NodeKind::Path { rule, guide: false, .. } = &a.kind && !f.opts.outline - && a.blend == drawcraft_color::BlendMode::Normal + && a.blend == vectorcraft_color::BlendMode::Normal && !a.isolate && !fx::has_fx(a) && (a.opacity >= 1.0 || painted_items(a) == 1) @@ -436,7 +436,7 @@ impl Renderer { } if let NodeKind::Text(t) = &a.kind && a.opacity >= 1.0 - && a.blend == drawcraft_color::BlendMode::Normal + && a.blend == vectorcraft_color::BlendMode::Normal && !fx::has_fx(a) { let g = self.text_geom_of(a, t); @@ -444,7 +444,7 @@ impl Renderer { self.stats.drawn += 1; return; } - if drawcraft_doc::live::is_live(a) { + if vectorcraft_doc::live::is_live(a) { return self.draw_live(ctx, f, a); } self.draw_node(ctx, f, a, true); @@ -469,7 +469,7 @@ impl Renderer { let mut layers = 0; let template_dim = matches!(n.kind, NodeKind::Layer { template: true, .. }) && f.opts.dim_templates; let opacity = if template_dim { n.opacity * 0.5 } else { n.opacity }; - if !outline && (opacity < 1.0 || n.blend != drawcraft_color::BlendMode::Normal || n.isolate) { + if !outline && (opacity < 1.0 || n.blend != vectorcraft_color::BlendMode::Normal || n.isolate) { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(n.blend)), Some(opacity), None, None); layers += 1; @@ -551,7 +551,7 @@ impl Renderer { return; } // Bounds only matter to gradients and patterns (their geometry is relative to the object). - let solid = |p: &drawcraft_color::Paint| matches!(p, drawcraft_color::Paint::Solid { .. } | drawcraft_color::Paint::None); + let solid = |p: &vectorcraft_color::Paint| matches!(p, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None); let needs_bounds = n.appearance.items.iter().any(|i| match i { AppearanceItem::Fill(fl) => !solid(&fl.paint), AppearanceItem::Stroke(st) => !solid(&st.paint), @@ -563,7 +563,7 @@ impl Renderer { if !fl.visible || fl.paint.is_none() { continue; } - let layered = fl.opacity < 1.0 || fl.blend != drawcraft_color::BlendMode::Normal; + let layered = fl.opacity < 1.0 || fl.blend != vectorcraft_color::BlendMode::Normal; if layered { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(fl.blend)), Some(fl.opacity), None, None); @@ -592,9 +592,9 @@ impl Renderer { } /// Multiply the folded object opacity into a solid paint. - fn fold_alpha(&self, ctx: &mut RenderContext, p: &drawcraft_color::Paint) { + fn fold_alpha(&self, ctx: &mut RenderContext, p: &vectorcraft_color::Paint) { if self.alpha < 1.0 - && let drawcraft_color::Paint::Solid { color, .. } = p + && let vectorcraft_color::Paint::Solid { color, .. } = p { let [r, g, b, a] = color.to_rgba8(self.alpha); ctx.set_paint(peniko::Color::from_rgba8(r, g, b, a)); @@ -602,7 +602,7 @@ impl Renderer { } fn draw_stroke(&mut self, ctx: &mut RenderContext, f: &Frame, bp: &BezPath, rule: FillRule, st: &StrokeLayer, bounds: Rect) { - let layered = st.opacity < 1.0 || st.blend != drawcraft_color::BlendMode::Normal || st.align == StrokeAlign::Outside; + let layered = st.opacity < 1.0 || st.blend != vectorcraft_color::BlendMode::Normal || st.align == StrokeAlign::Outside; if layered { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(st.blend)), Some(st.opacity), None, None); @@ -770,7 +770,7 @@ impl Renderer { } } - fn draw_image(&mut self, ctx: &mut RenderContext, f: &Frame, im: &drawcraft_doc::ImageObject) { + fn draw_image(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject) { let rect = Rect::new(0.0, 0.0, im.width as f64, im.height as f64); if f.opts.outline { let mut p = rect.to_path(0.1); @@ -823,11 +823,11 @@ fn painted_items(n: &Node) -> usize { .items .iter() .filter(|i| match i { - AppearanceItem::Fill(f) => f.visible && !f.paint.is_none() && matches!(f.paint, drawcraft_color::Paint::Solid { .. }), - AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && matches!(s.paint, drawcraft_color::Paint::Solid { .. }) && s.dash.is_none(), + AppearanceItem::Fill(f) => f.visible && !f.paint.is_none() && matches!(f.paint, vectorcraft_color::Paint::Solid { .. }), + AppearanceItem::Stroke(s) => s.visible && !s.paint.is_none() && s.width > 0.0 && matches!(s.paint, vectorcraft_color::Paint::Solid { .. }) && s.dash.is_none(), }) .count() - .max(if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !matches!(f.paint, drawcraft_color::Paint::Solid { .. } | drawcraft_color::Paint::None)) || matches!(i, AppearanceItem::Stroke(s) if s.visible && !matches!(s.paint, drawcraft_color::Paint::Solid { .. } | drawcraft_color::Paint::None))) { 2 } else { 0 }) + .max(if n.appearance.items.iter().any(|i| matches!(i, AppearanceItem::Fill(f) if f.visible && !matches!(f.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None)) || matches!(i, AppearanceItem::Stroke(s) if s.visible && !matches!(s.paint, vectorcraft_color::Paint::Solid { .. } | vectorcraft_color::Paint::None))) { 2 } else { 0 }) } /// Preferred rasterizer thread count set by the app (Preferences → Performance); negative = automatic. @@ -850,8 +850,8 @@ pub fn default_threads() -> u16 { if o >= 0 { return o.min(64) as u16; } - if std::env::var_os("DRAWCRAFT_RENDER_THREADS").is_some() { - return std::env::var("DRAWCRAFT_RENDER_THREADS").ok().and_then(|v| v.parse().ok()).unwrap_or(0); + if std::env::var_os("VECTORCRAFT_RENDER_THREADS").is_some() { + return std::env::var("VECTORCRAFT_RENDER_THREADS").ok().and_then(|v| v.parse().ok()).unwrap_or(0); } std::thread::available_parallelism().map(|n| (n.get().saturating_sub(1)).min(4) as u16).unwrap_or(0) } @@ -865,7 +865,7 @@ struct TextGeom { } fn text_geom(t: &TextObject) -> TextGeom { - let layout = drawcraft_text::layout(drawcraft_text::FontDb::global(), t); + let layout = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t); let mut runs = vec![BezPath::new(); t.runs.len()]; let mut all = BezPath::new(); for g in &layout.glyphs { @@ -888,8 +888,8 @@ fn fill_rule(r: FillRule) -> peniko::Fill { } } -pub(crate) fn blend_mode(b: drawcraft_color::BlendMode) -> BlendMode { - use drawcraft_color::BlendMode as B; +pub(crate) fn blend_mode(b: vectorcraft_color::BlendMode) -> BlendMode { + use vectorcraft_color::BlendMode as B; let mix = match b { B::Normal => Mix::Normal, B::Darken => Mix::Darken, diff --git a/crates/render/src/live.rs b/crates/render/src/live.rs index 41010a07f..f510fd068 100644 --- a/crates/render/src/live.rs +++ b/crates/render/src/live.rs @@ -1,6 +1,6 @@ //! Live objects in the renderer. //! -//! Blends and envelopes are evaluated by `drawcraft_doc::live` and the resulting objects cached +//! Blends and envelopes are evaluated by `vectorcraft_doc::live` and the resulting objects cached //! by `Arc` identity of the live node (so the steps keep stable `Arc`s and hit the geometry cache //! frame after frame). Gradient meshes are tessellated into many small solid-colour quads (vello //! has no mesh shading), with the density chosen from the on-screen patch size; each quad is @@ -9,9 +9,9 @@ use std::collections::HashMap; use std::sync::Arc; -use drawcraft_doc::live::{self, GradientMesh, MeshQuad}; -use drawcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeKind, StrokeLayer}; -use drawcraft_geom::{Affine, BezPath, PathData, Point}; +use vectorcraft_doc::live::{self, GradientMesh, MeshQuad}; +use vectorcraft_doc::{Appearance, AppearanceItem, FillLayer, Node, NodeKind, StrokeLayer}; +use vectorcraft_geom::{Affine, BezPath, PathData, Point}; use vello_cpu::RenderContext; use vello_cpu::peniko; @@ -113,7 +113,7 @@ impl Renderer { pub(crate) fn draw_live(&mut self, ctx: &mut RenderContext, f: &Frame, a: &Arc) { let stamp = self.stamp; self.live.tick(stamp); - let layered = !f.opts.outline && (a.opacity < 1.0 || a.blend != drawcraft_color::BlendMode::Normal || a.isolate); + let layered = !f.opts.outline && (a.opacity < 1.0 || a.blend != vectorcraft_color::BlendMode::Normal || a.isolate); if layered { ctx.set_transform(Affine::IDENTITY); ctx.push_layer(None, Some(blend_mode(a.blend)), Some(a.opacity), None, None); diff --git a/crates/render/src/paint.rs b/crates/render/src/paint.rs index 6d8454fa1..8c3d030ab 100644 --- a/crates/render/src/paint.rs +++ b/crates/render/src/paint.rs @@ -1,13 +1,13 @@ //! Paint conversion (solid, gradients, patterns), arrowheads and image decoding. -use drawcraft_color::{GradientKind, Paint}; -use drawcraft_doc::{Arrowhead, Document, StrokeLayer}; -use drawcraft_geom::{Affine, BezPath, Point, Rect, Shape, Vec2}; +use vectorcraft_color::{GradientKind, Paint}; +use vectorcraft_doc::{Arrowhead, Document, StrokeLayer}; +use vectorcraft_geom::{Affine, BezPath, Point, Rect, Shape, Vec2}; use vello_cpu::RenderContext; use vello_cpu::kurbo::{self, ParamCurve, ParamCurveDeriv}; use vello_cpu::peniko::{self, ColorStop}; -fn color(c: &drawcraft_color::Color, alpha: f32) -> peniko::Color { +fn color(c: &vectorcraft_color::Color, alpha: f32) -> peniko::Color { let [r, g, b, a] = c.to_rgba8(alpha); peniko::Color::from_rgba8(r, g, b, a) } diff --git a/crates/render/src/pattern.rs b/crates/render/src/pattern.rs index 427a0099c..714696a52 100644 --- a/crates/render/src/pattern.rs +++ b/crates/render/src/pattern.rs @@ -14,9 +14,9 @@ use std::cell::RefCell; use std::collections::HashMap; use std::sync::Arc; -use drawcraft_doc::pattern::{Overlap, PatternDef, TileType}; -use drawcraft_doc::{Document, Node}; -use drawcraft_geom::{Affine, Rect, Shape}; +use vectorcraft_doc::pattern::{Overlap, PatternDef, TileType}; +use vectorcraft_doc::{Document, Node}; +use vectorcraft_geom::{Affine, Rect, Shape}; use vello_cpu::peniko::{self, Extend, ImageQuality, ImageSampler}; use vello_cpu::{Pixmap, RenderContext}; @@ -207,10 +207,10 @@ impl Renderer { #[cfg(test)] mod tests { use super::*; - use drawcraft_color::{Color, Paint}; - use drawcraft_doc::pattern::{PatternEdit, RepeatSpec, pattern_paint}; - use drawcraft_doc::{Appearance, NodeId, NodeKind}; - use drawcraft_geom::shapes; + use vectorcraft_color::{Color, Paint}; + use vectorcraft_doc::pattern::{PatternEdit, RepeatSpec, pattern_paint}; + use vectorcraft_doc::{Appearance, NodeId, NodeKind}; + use vectorcraft_geom::shapes; /// A 20×20 tile with a red 10×10 square in its top-left corner. fn doc_with_pattern(tt: TileType) -> Document { @@ -291,7 +291,7 @@ mod tests { assert!(is_red(r.pixel(10, 10)) && is_white(r.pixel(30, 10)) && is_red(r.pixel(50, 10))); let mut d2 = d.clone(); let id = d2.layers[0].children().unwrap()[0].id; - drawcraft_doc::pattern::transform_pattern_paints(d2.node_mut(id).unwrap(), Affine::translate((10.0, 0.0))); + vectorcraft_doc::pattern::transform_pattern_paints(d2.node_mut(id).unwrap(), Affine::translate((10.0, 0.0))); let r = render(&d2, 1.0); assert!(is_white(r.pixel(5, 5)) && is_red(r.pixel(15, 5))); } diff --git a/crates/render/src/proof.rs b/crates/render/src/proof.rs index 2d811b651..7d51b31c8 100644 --- a/crates/render/src/proof.rs +++ b/crates/render/src/proof.rs @@ -18,10 +18,10 @@ use std::borrow::Cow; use std::sync::{Arc, RwLock}; -use drawcraft_color::cms::{self, Cms, PROCESS_PLATES}; -pub use drawcraft_color::cms::{Intent, ProofSetup, ProofTarget}; -use drawcraft_color::{BlendMode, Color, Paint}; -use drawcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind}; +use vectorcraft_color::cms::{self, Cms, PROCESS_PLATES}; +pub use vectorcraft_color::cms::{Intent, ProofSetup, ProofTarget}; +use vectorcraft_color::{BlendMode, Color, Paint}; +use vectorcraft_doc::{AppearanceItem, Document, Node, NodeId, NodeKind}; use crate::RenderOptions; diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 2056625ba..aa1201a00 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -1,7 +1,7 @@ use super::*; -use drawcraft_color::{Color, Gradient, GradientPaint, Paint}; -use drawcraft_doc::{Appearance, Node}; -use drawcraft_geom::shapes; +use vectorcraft_color::{Color, Gradient, GradientPaint, Paint}; +use vectorcraft_doc::{Appearance, Node}; +use vectorcraft_geom::shapes; fn rect_node(d: &mut Document, r: Rect, fill: Paint, stroke: Paint, w: f64) -> Node { let id = d.alloc_id(); @@ -77,7 +77,7 @@ fn multiply_blend() { let mut d = Document::new(100.0, 100.0); let a = rect_node(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), Paint::solid(Color::rgb(1.0, 1.0, 0.0)), Paint::None, 0.0); let mut b = rect_node(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), Paint::solid(Color::rgb(0.0, 1.0, 1.0)), Paint::None, 0.0); - b.blend = drawcraft_color::BlendMode::Multiply; + b.blend = vectorcraft_color::BlendMode::Multiply; let l = d.layers[0].id; d.insert(Some(l), 0, a).unwrap(); d.insert(Some(l), 1, b).unwrap(); @@ -193,8 +193,8 @@ fn arrowheads_render() { assert!(r.pixel(73, 47)[0] < 128); } -use drawcraft_doc::Arrowhead; -use drawcraft_geom::Point; +use vectorcraft_doc::Arrowhead; +use vectorcraft_geom::Point; /// A red 10..90 square masked by a white square covering only its left half (10..50), /// plus a mid-grey strip (50..70) — rendered both multi- and single-threaded. @@ -205,7 +205,7 @@ fn masked_doc(clip: bool, invert: bool) -> Document { let grey = rect_node(&mut d, Rect::new(50.0, 10.0, 70.0, 90.0), Paint::solid(Color::rgb(0.5, 0.5, 0.5)), Paint::None, 0.0); let gid = d.alloc_id(); let art = Node::group(gid, vec![Arc::new(white), Arc::new(grey)]); - let mut m = drawcraft_doc::OpacityMask::new(art, clip); + let mut m = vectorcraft_doc::OpacityMask::new(art, clip); m.invert = invert; n.mask = Some(Box::new(m)); let l = d.layers[0].id; diff --git a/crates/render/src/width.rs b/crates/render/src/width.rs index e96bb8c91..b7901343c 100644 --- a/crates/render/src/width.rs +++ b/crates/render/src/width.rs @@ -6,8 +6,8 @@ //! projecting caps (the round cap blends the two side widths); closed subpaths become two loops //! of opposite orientation filled with the non-zero rule. -use drawcraft_doc::{LineCap, StrokeAlign, StrokeLayer, WidthProfile}; -use drawcraft_geom::kurbo::{self, BezPath, PathEl, Point, Vec2}; +use vectorcraft_doc::{LineCap, StrokeAlign, StrokeLayer, WidthProfile}; +use vectorcraft_geom::kurbo::{self, BezPath, PathEl, Point, Vec2}; /// The outline to fill instead of stroking, when `st` has a width profile (and no dashes). pub(crate) fn outline_for(bp: &BezPath, st: &StrokeLayer, tol: f64) -> Option { @@ -255,7 +255,7 @@ mod tests { let a = area(&width_outline(&c, 10.0, &p, LineCap::Butt, 0.001)); let want = 2.0 * std::f64::consts::PI * 50.0 * 10.0; assert!((a - want).abs() / want < 0.01, "{a} vs {want}"); - let st = StrokeLayer { profile: None, ..StrokeLayer::new(drawcraft_color::Paint::solid(drawcraft_color::Color::BLACK), 4.0) }; + let st = StrokeLayer { profile: None, ..StrokeLayer::new(vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK), 4.0) }; assert!(outline_for(&c, &st, 0.1).is_none()); } } diff --git a/crates/render/tests/golden.rs b/crates/render/tests/golden.rs index 8a39d64e7..d56c87139 100644 --- a/crates/render/tests/golden.rs +++ b/crates/render/tests/golden.rs @@ -1,12 +1,12 @@ //! Pixel-sample "golden" tests on small fixtures: gradients, blend modes, clip groups, stroke //! alignment/dashes/arrowheads, effects, text, and opacity folding vs layer compositing. -use drawcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; -use drawcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, Document, Effect, FillLayer, LineCap, Node, StrokeAlign, StrokeLayer}; -use drawcraft_geom::{Point, Rect, shapes}; -use drawcraft_testkit::fixtures::{DocBuilder, exec, session_with}; -use drawcraft_testkit::raster::{Image, assert_rgb, assert_similar, diff, render_view}; use serde_json::json; +use vectorcraft_color::{BlendMode, Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::{Appearance, AppearanceItem, Arrowhead, Dash, Document, Effect, FillLayer, LineCap, Node, StrokeAlign, StrokeLayer}; +use vectorcraft_geom::{Point, Rect, shapes}; +use vectorcraft_testkit::fixtures::{DocBuilder, exec, session_with}; +use vectorcraft_testkit::raster::{Image, assert_rgb, assert_similar, diff, render_view}; const W: u32 = 100; @@ -473,8 +473,8 @@ fn invisible_and_hidden_nodes_are_not_drawn() { let mut b = DocBuilder::new(100.0, 100.0); let id = b.rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::BLACK, |_| {}); let d = b.build(); - let opts = drawcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), hidden: vec![id], ..Default::default() }; - let r = drawcraft_render::Renderer::new().render(&d, 100, 100, drawcraft_geom::Affine::IDENTITY, &opts); + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), hidden: vec![id], ..Default::default() }; + let r = vectorcraft_render::Renderer::new().render(&d, 100, 100, vectorcraft_geom::Affine::IDENTITY, &opts); assert_eq!(Image::from_rendered(&r).ink(), 0); } @@ -483,8 +483,8 @@ fn outline_mode_draws_thin_black_edges_only() { let mut b = DocBuilder::new(100.0, 100.0); b.rect(Rect::new(20.0, 20.0, 80.0, 80.0), Color::rgb(1.0, 0.0, 0.0), |_| {}); let d = b.build(); - let opts = drawcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), outline: true, ..Default::default() }; - let img = Image::from_rendered(&drawcraft_render::Renderer::new().render(&d, 100, 100, drawcraft_geom::Affine::IDENTITY, &opts)); + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), outline: true, ..Default::default() }; + let img = Image::from_rendered(&vectorcraft_render::Renderer::new().render(&d, 100, 100, vectorcraft_geom::Affine::IDENTITY, &opts)); assert_rgb(&img, 50, 50, [255, 255, 255], 2); assert_eq!(img.count(|p| p[0] > 200 && p[1] < 50), 0, "red paint visible in outline mode"); assert!(img.ink() > 150 && img.ink() < 600, "outline ink {}", img.ink()); @@ -495,7 +495,7 @@ fn png_encoding_roundtrips_pixels() { let mut b = DocBuilder::new(100.0, 100.0); b.rect(Rect::new(10.0, 10.0, 60.0, 60.0), Color::rgb(0.2, 0.4, 0.6), |n| n.opacity = 0.5); let d = b.build(); - let r = drawcraft_render::Renderer::new().render_region(&d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, false); + let r = vectorcraft_render::Renderer::new().render_region(&d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, false); let decoded = Image::from_png(&r.to_png()).unwrap(); assert_eq!(decoded, Image::from_rendered(&r)); } @@ -508,9 +508,9 @@ fn renderer_reuse_matches_fresh_renderer() { let mut b = DocBuilder::new(100.0, 100.0); b.path(shapes::ellipse(Rect::new(30.0, 30.0, 90.0, 90.0)), fill_only(Paint::solid(Color::rgb(0.9, 0.2, 0.1))), |_| {}); let d2 = b.build(); - let opts = drawcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; - let mut r = drawcraft_render::Renderer::new(); - let id = drawcraft_geom::Affine::IDENTITY; + let opts = vectorcraft_render::RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() }; + let mut r = vectorcraft_render::Renderer::new(); + let id = vectorcraft_geom::Affine::IDENTITY; let _ = r.render(&d1, 100, 100, id, &opts); let reused = Image::from_rendered(&r.render(&d2, 100, 100, id, &opts)); assert_eq!(reused, render(&d2)); @@ -522,10 +522,10 @@ fn scaled_render_matches_upscaled_geometry() { let mut b = DocBuilder::new(50.0, 50.0); b.path(shapes::ellipse(Rect::new(5.0, 5.0, 45.0, 40.0)), fill_only(Paint::solid(Color::rgb(0.1, 0.6, 0.3))), |_| {}); let d = b.build(); - let a = Image::from_rendered(&drawcraft_render::Renderer::new().render_region(&d, Rect::new(0.0, 0.0, 50.0, 50.0), 2.0, true)); + let a = Image::from_rendered(&vectorcraft_render::Renderer::new().render_region(&d, Rect::new(0.0, 0.0, 50.0, 50.0), 2.0, true)); let mut b = DocBuilder::new(100.0, 100.0); b.path(shapes::ellipse(Rect::new(10.0, 10.0, 90.0, 80.0)), fill_only(Paint::solid(Color::rgb(0.1, 0.6, 0.3))), |_| {}); - let c = Image::from_rendered(&drawcraft_render::Renderer::new().render_region(&b.build(), Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, true)); + let c = Image::from_rendered(&vectorcraft_render::Renderer::new().render_region(&b.build(), Rect::new(0.0, 0.0, 100.0, 100.0), 1.0, true)); assert_similar(&a, &c, 2.0, 0.0); let _: Option = None; } diff --git a/crates/svg/Cargo.toml b/crates/svg/Cargo.toml index d23f50dad..093bf3fcf 100644 --- a/crates/svg/Cargo.toml +++ b/crates/svg/Cargo.toml @@ -1,17 +1,17 @@ [package] -name = "drawcraft-svg" +name = "vectorcraft-svg" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft SVG import and export" +description = "VectorCraft SVG import and export" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } kurbo = { workspace = true } usvg = { workspace = true } thiserror = { workspace = true } @@ -20,6 +20,6 @@ image = { workspace = true } svgtypes = "0.16" [dev-dependencies] -drawcraft-testkit = { workspace = true } +vectorcraft-testkit = { workspace = true } proptest = { workspace = true } image = { workspace = true } diff --git a/crates/svg/src/export.rs b/crates/svg/src/export.rs index 626621395..7e99db97a 100644 --- a/crates/svg/src/export.rs +++ b/crates/svg/src/export.rs @@ -2,10 +2,12 @@ use std::collections::{HashMap, HashSet}; -use drawcraft_color::{BlendMode, GradientKind, GradientPaint, Paint}; -use drawcraft_doc::{AppearanceItem, Document, FillLayer, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextKind, TextObject}; -use drawcraft_doc::{CharStyle, Justify}; -use drawcraft_geom::{Affine, FillRule, PathData, Point, Rect, Vec2}; +use vectorcraft_color::{BlendMode, GradientKind, GradientPaint, Paint}; +use vectorcraft_doc::{ + AppearanceItem, Document, FillLayer, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextKind, TextObject, +}; +use vectorcraft_doc::{CharStyle, Justify}; +use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, Vec2}; use crate::{ExportOptions, Styling, base64_encode, fmt_num, xml_escape}; @@ -533,12 +535,12 @@ impl Writer<'_> { } NodeKind::Image(im) => { let r = Rect::new(0.0, 0.0, im.width as f64, im.height as f64); - let p = drawcraft_geom::shapes::rectangle(r).transformed(im.xf); + let p = vectorcraft_geom::shapes::rectangle(r).transformed(im.xf); out.push((self.path_d(&p, self.xf), FillRule::NonZero)); } NodeKind::Text(_) | NodeKind::SymbolInstance { .. } => {} NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = drawcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_doc::live::expand_deep(n, None); self.clip_shapes(&g, out); } } @@ -546,7 +548,7 @@ impl Writer<'_> { /// An object with an opacity mask: `` around the unmasked object. The mask /// art goes in ``; no-clip adds a white backdrop and invert a colour-inverting filter. - fn masked(&mut self, n: &Node, m: &drawcraft_doc::OpacityMask) { + fn masked(&mut self, n: &Node, m: &vectorcraft_doc::OpacityMask) { const BIG: &str = "x=\"-100000\" y=\"-100000\" width=\"200000\" height=\"200000\""; let mid = self.fresh_id("mask"); let (body, depth) = (std::mem::take(&mut self.body), self.depth); @@ -681,7 +683,7 @@ impl Writer<'_> { } // Live blends/envelopes/meshes export their evaluated (expanded) form. NodeKind::Blend { .. } | NodeKind::Envelope { .. } | NodeKind::Mesh(_) | NodeKind::Repeat(_) => { - let g = drawcraft_doc::live::expand_deep(n, None); + let g = vectorcraft_doc::live::expand_deep(n, None); self.node(&g); } } diff --git a/crates/svg/src/import.rs b/crates/svg/src/import.rs index 2dce9689e..a64f2a0c0 100644 --- a/crates/svg/src/import.rs +++ b/crates/svg/src/import.rs @@ -4,13 +4,13 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; -use drawcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; -use drawcraft_doc::{ +use usvg::roxmltree; +use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::{ Appearance, AppearanceItem, CharStyle, Dash, Document, FillLayer, ImageBlob, ImageObject, Justify, LayerColor, LineCap, LineJoin, Node, NodeKind, StrokeLayer, TextKind, TextObject, TextRun, }; -use drawcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Vec2}; -use usvg::roxmltree; +use vectorcraft_geom::{Affine, BezPath, FillRule, PathData, Point, Vec2}; use crate::SvgError; @@ -42,7 +42,7 @@ pub(crate) fn import(svg: &str) -> Result<(Document, Vec), SvgError> { let usvg::Node::Group(g) = c else { continue }; let children = im.children(g, base * aff(g.transform())); let id = im.doc.alloc_id(); - let mut l = Node::layer(id, g.id(), LayerColor::Preset((i % drawcraft_doc::LAYER_COLORS.len()) as u8)); + let mut l = Node::layer(id, g.id(), LayerColor::Preset((i % vectorcraft_doc::LAYER_COLORS.len()) as u8)); if let Some(ch) = l.children_mut() { *ch = children; } @@ -208,7 +208,7 @@ impl Importer { } /// `` → opacity mask (luminance; alpha masks are approximated by their luminance). - fn opacity_mask(&mut self, m: &usvg::Mask, ts: Affine, label: &str) -> Option> { + fn opacity_mask(&mut self, m: &usvg::Mask, ts: Affine, label: &str) -> Option> { if m.kind() == usvg::MaskType::Alpha { self.warn(format!("alpha mask on {label} imported as a luminance opacity mask")); } @@ -220,7 +220,7 @@ impl Importer { return None; } let art = self.named("", NodeKind::Group { children, clip: false }); - Some(Box::new(drawcraft_doc::OpacityMask::new(art, true))) + Some(Box::new(vectorcraft_doc::OpacityMask::new(art, true))) } fn clip_node(&mut self, cp: &usvg::ClipPath, ts: Affine) -> Option { diff --git a/crates/svg/src/lib.rs b/crates/svg/src/lib.rs index e9b99d3ce..b6f135b6a 100644 --- a/crates/svg/src/lib.rs +++ b/crates/svg/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft SVG import and export. +//! VectorCraft SVG import and export. //! //! * [`export`] writes a [`Document`] as SVG 1.1 with our own writer (presentation attributes, //! inline styles or internal CSS classes; gradients in `` with `userSpaceOnUse`; clip groups as @@ -28,8 +28,8 @@ mod export; mod import; -pub use drawcraft_doc::Document; -pub use drawcraft_doc::TextObject; +pub use vectorcraft_doc::Document; +pub use vectorcraft_doc::TextObject; /// How style properties are written (Illustrator's "Styling" option). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/svg/tests/prop_svg.rs b/crates/svg/tests/prop_svg.rs index ef6f576f4..0f917e362 100644 --- a/crates/svg/tests/prop_svg.rs +++ b/crates/svg/tests/prop_svg.rs @@ -1,15 +1,15 @@ //! SVG export/import properties: never panics, always re-imports, and plain art survives the round //! trip visually (render comparison with a perceptual tolerance). -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::Appearance; -use drawcraft_geom::{PathData, Rect, shapes}; -use drawcraft_svg::{ExportOptions, Styling, export, import}; -use drawcraft_testkit::fixtures::{self, DocBuilder, art_nodes}; -use drawcraft_testkit::invariants::check_document; -use drawcraft_testkit::raster::{assert_similar, render_artboard}; -use drawcraft_testkit::strategies::{arb_closed_shape, arb_ops, arb_path_data}; use proptest::prelude::*; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::Appearance; +use vectorcraft_geom::{PathData, Rect, shapes}; +use vectorcraft_svg::{ExportOptions, Styling, export, import}; +use vectorcraft_testkit::fixtures::{self, DocBuilder, art_nodes}; +use vectorcraft_testkit::invariants::check_document; +use vectorcraft_testkit::raster::{assert_similar, render_artboard}; +use vectorcraft_testkit::strategies::{arb_closed_shape, arb_ops, arb_path_data}; fn all_options() -> Vec { let mut v = vec![]; diff --git a/crates/svg/tests/roundtrip.rs b/crates/svg/tests/roundtrip.rs index 76bdcbd3e..f4136a8ea 100644 --- a/crates/svg/tests/roundtrip.rs +++ b/crates/svg/tests/roundtrip.rs @@ -1,12 +1,12 @@ use std::sync::Arc; -use drawcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, Paint}; -use drawcraft_doc::{ +use vectorcraft_color::{BlendMode, Color, Gradient, GradientGeom, GradientKind, GradientPaint, Paint}; +use vectorcraft_doc::{ Appearance, AppearanceItem, CharStyle, Dash, Document, FillLayer, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeId, NodeKind, StrokeAlign, StrokeLayer, TextObject, }; -use drawcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; -use drawcraft_svg::{ExportOptions, Styling, export, import, import_with_report}; +use vectorcraft_geom::{Affine, FillRule, PathData, Point, Rect, shapes}; +use vectorcraft_svg::{ExportOptions, Styling, export, import, import_with_report}; fn doc_with(nodes: Vec) -> Document { let mut d = Document::new(200.0, 200.0); @@ -647,7 +647,7 @@ fn masked_doc(clip: bool, invert: bool) -> Document { let white = Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0); let mut n = rect_node(&mut d, Rect::new(10.0, 10.0, 90.0, 90.0), red); let art = rect_node(&mut d, Rect::new(10.0, 10.0, 50.0, 90.0), white); - let mut m = drawcraft_doc::OpacityMask::new(art, clip); + let mut m = vectorcraft_doc::OpacityMask::new(art, clip); m.invert = invert; n.mask = Some(Box::new(m)); let l = d.layers[0].id; diff --git a/crates/testkit/Cargo.toml b/crates/testkit/Cargo.toml index 4182eb835..153f3176a 100644 --- a/crates/testkit/Cargo.toml +++ b/crates/testkit/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "drawcraft-testkit" +name = "vectorcraft-testkit" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft test helpers: fixtures, generators, render comparison, geometry assertions" +description = "VectorCraft test helpers: fixtures, generators, render comparison, geometry assertions" publish = false [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-engine = { workspace = true } -drawcraft-render = { workspace = true } -drawcraft-format = { workspace = true } -drawcraft-svg = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-render = { workspace = true } +vectorcraft-format = { workspace = true } +vectorcraft-svg = { workspace = true } kurbo = { workspace = true } serde_json = { workspace = true } proptest = { workspace = true } diff --git a/crates/testkit/src/fixtures.rs b/crates/testkit/src/fixtures.rs index 68a9a2f44..c79c39ed5 100644 --- a/crates/testkit/src/fixtures.rs +++ b/crates/testkit/src/fixtures.rs @@ -2,11 +2,11 @@ use std::sync::Arc; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Appearance, Document, Node, NodeId, NodeKind}; -use drawcraft_engine::Session; -use drawcraft_geom::{PathData, Rect, shapes}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Appearance, Document, Node, NodeId, NodeKind}; +use vectorcraft_engine::Session; +use vectorcraft_geom::{PathData, Rect, shapes}; /// Execute a command and panic with context on error. #[track_caller] @@ -108,7 +108,7 @@ pub fn rich_session() -> Session { exec(&mut s, "effect.apply", json!({"effect": "distort.roughen", "params": {"size": 3, "detail": 5, "seed": 7}})); let l = id_of(&exec(&mut s, "shape.line", json!({"x1": 20, "y1": 300, "x2": 200, "y2": 350}))); exec(&mut s, "stroke.set", json!({"weight": 3, "endArrow": "Arrow"})); - let _ = id_of(&exec(&mut s, "text.create", json!({"x": 250, "y": 300, "text": "DrawCraft", "size": 30}))); + let _ = id_of(&exec(&mut s, "text.create", json!({"x": 250, "y": 300, "text": "VectorCraft", "size": 30}))); exec(&mut s, "layer.new", json!({"name": "Top"})); let d = rect(&mut s, 350.0, 200.0, 80.0, 80.0); let e = ellipse(&mut s, 380.0, 230.0, 80.0, 80.0); diff --git a/crates/testkit/src/geom.rs b/crates/testkit/src/geom.rs index a25bb8cc3..77827a034 100644 --- a/crates/testkit/src/geom.rs +++ b/crates/testkit/src/geom.rs @@ -1,7 +1,7 @@ //! Geometry assertions. -use drawcraft_geom::{BezPath, ParamCurve, PathData, Point, Rect}; use kurbo::{ParamCurveNearest, Shape}; +use vectorcraft_geom::{BezPath, ParamCurve, PathData, Point, Rect}; pub fn approx(a: f64, b: f64, eps: f64) -> bool { (a - b).abs() <= eps diff --git a/crates/testkit/src/invariants.rs b/crates/testkit/src/invariants.rs index add9efc57..452c96109 100644 --- a/crates/testkit/src/invariants.rs +++ b/crates/testkit/src/invariants.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; -use drawcraft_doc::{Document, NodeKind}; -use drawcraft_engine::Session; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeKind}; +use vectorcraft_engine::Session; /// Serialize a document to JSON (the canonical form for equality checks). pub fn doc_json(doc: &Document) -> Value { @@ -88,11 +88,11 @@ pub fn check_session(s: &Session) -> Result<(), String> { Ok(()) } -/// `.drawcraft` save → load must reproduce the document (numbers compared to 1e-12 relative — +/// `.vectorcraft` save → load must reproduce the document (numbers compared to 1e-12 relative — /// see [`check_native_roundtrip_exact`] for the bit-exact version). pub fn check_native_roundtrip(doc: &Document) -> Result<(), String> { - let bytes = drawcraft_format::save(doc, false); - let back = drawcraft_format::load(&bytes).map_err(|e| format!("load: {e}"))?; + let bytes = vectorcraft_format::save(doc, false); + let back = vectorcraft_format::load(&bytes).map_err(|e| format!("load: {e}"))?; let (a, b) = (doc_json(doc), doc_json(&back)); if !json_approx_eq(&a, &b, 1e-12) { return Err(format!("native round trip differs:\n{}", first_diff(&a, &b, "$"))); @@ -100,10 +100,10 @@ pub fn check_native_roundtrip(doc: &Document) -> Result<(), String> { Ok(()) } -/// Bit-exact `.drawcraft` round trip (every f64 must survive). +/// Bit-exact `.vectorcraft` round trip (every f64 must survive). pub fn check_native_roundtrip_exact(doc: &Document) -> Result<(), String> { - let bytes = drawcraft_format::save(doc, false); - let back = drawcraft_format::load(&bytes).map_err(|e| format!("load: {e}"))?; + let bytes = vectorcraft_format::save(doc, false); + let back = vectorcraft_format::load(&bytes).map_err(|e| format!("load: {e}"))?; let (a, b) = (doc_json(doc), doc_json(&back)); if a != b { return Err(format!("native round trip differs:\n{}", first_diff(&a, &b, "$"))); @@ -126,8 +126,8 @@ pub fn json_approx_eq(a: &Value, b: &Value, rel: f64) -> bool { /// SVG export must not panic and must re-import. pub fn check_svg_roundtrip(doc: &Document) -> Result { - let svg = drawcraft_svg::export(doc, &drawcraft_svg::ExportOptions::default()); - drawcraft_svg::import(&svg).map_err(|e| format!("svg re-import: {e}\n{svg}")) + let svg = vectorcraft_svg::export(doc, &vectorcraft_svg::ExportOptions::default()); + vectorcraft_svg::import(&svg).map_err(|e| format!("svg re-import: {e}\n{svg}")) } /// `document.inspect` must succeed and serialize. diff --git a/crates/testkit/src/lib.rs b/crates/testkit/src/lib.rs index 9e0dadf5f..5d339202f 100644 --- a/crates/testkit/src/lib.rs +++ b/crates/testkit/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft test helpers. +//! VectorCraft test helpers. //! //! - [`fixtures`]: sessions and documents in known states (empty, single/multi selection, a "rich" //! document touching most node kinds), plus a [`fixtures::DocBuilder`] for building documents @@ -6,7 +6,7 @@ //! - [`strategies`]: proptest strategies for geometry and for engine command sequences ([`strategies::Op`]), and //! junk-parameter generators for fuzzing the command registry. //! - [`invariants`]: structural checks for documents and sessions, and round-trip checks -//! (`.drawcraft`, SVG, `document.inspect`). +//! (`.vectorcraft`, SVG, `document.inspect`). //! - [`raster`]: rendering helpers and image comparison with a perceptual tolerance. //! - [`geom`]: geometry assertions (approximate equality, curve sampling, Hausdorff distance). //! @@ -19,14 +19,14 @@ pub mod invariants; pub mod raster; pub mod strategies; -pub use drawcraft_engine::{Session, doc::NodeId}; pub use serde_json::{Value, json}; +pub use vectorcraft_engine::{Session, doc::NodeId}; /// Re-exports so dependents without direct dependencies (e.g. app test crates) can use them. -pub use {drawcraft_doc as doc, drawcraft_format as format, drawcraft_render as render, drawcraft_svg as svg}; +pub use {vectorcraft_doc as doc, vectorcraft_format as format, vectorcraft_render as render, vectorcraft_svg as svg}; /// A per-process temporary directory for test output (created on first use). pub fn temp_dir(tag: &str) -> std::path::PathBuf { - let dir = std::env::temp_dir().join(format!("drawcraft-testkit-{tag}-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!("vectorcraft-testkit-{tag}-{}", std::process::id())); std::fs::create_dir_all(&dir).expect("create temp dir"); dir } diff --git a/crates/testkit/src/raster.rs b/crates/testkit/src/raster.rs index 57751587f..a639291fe 100644 --- a/crates/testkit/src/raster.rs +++ b/crates/testkit/src/raster.rs @@ -1,8 +1,8 @@ //! Rendering helpers and image comparison with a perceptual tolerance. -use drawcraft_doc::Document; -use drawcraft_geom::{Affine, Rect}; -use drawcraft_render::{RenderOptions, Rendered, Renderer}; +use vectorcraft_doc::Document; +use vectorcraft_geom::{Affine, Rect}; +use vectorcraft_render::{RenderOptions, Rendered, Renderer}; /// A straight-alpha RGBA8 image. #[derive(Clone, Debug, PartialEq)] diff --git a/crates/testkit/src/strategies.rs b/crates/testkit/src/strategies.rs index 335398734..54641fc63 100644 --- a/crates/testkit/src/strategies.rs +++ b/crates/testkit/src/strategies.rs @@ -1,9 +1,9 @@ //! Proptest strategies: geometry, engine command sequences, junk parameters. -use drawcraft_engine::{Session, command_specs}; -use drawcraft_geom::{Anchor, PathData, Point, Rect, SubPath}; use proptest::prelude::*; use serde_json::{Value, json}; +use vectorcraft_engine::{Session, command_specs}; +use vectorcraft_geom::{Anchor, PathData, Point, Rect, SubPath}; use crate::fixtures::{art_nodes, top_level_ids}; @@ -54,12 +54,12 @@ pub fn polygon_path(pts: &[Point]) -> PathData { /// An axis-aligned ellipse as a path. pub fn arb_ellipse() -> impl Strategy { - arb_rect().prop_map(drawcraft_geom::shapes::ellipse) + arb_rect().prop_map(vectorcraft_geom::shapes::ellipse) } /// Rectangles, ellipses and star-shaped polygons. pub fn arb_closed_shape() -> impl Strategy { - prop_oneof![arb_rect().prop_map(drawcraft_geom::shapes::rectangle), arb_ellipse(), arb_star_polygon().prop_map(|p| polygon_path(&p)),] + prop_oneof![arb_rect().prop_map(vectorcraft_geom::shapes::rectangle), arb_ellipse(), arb_star_polygon().prop_map(|p| polygon_path(&p)),] } /// A random cubic Bézier path (open or closed, possibly several subpaths, arbitrary handles). @@ -259,7 +259,7 @@ pub fn arb_ops(len: std::ops::Range) -> impl Strategy> { prop::collection::vec(arb_op(), len) } -fn pick(ids: &[drawcraft_doc::NodeId], i: usize) -> Option { +fn pick(ids: &[vectorcraft_doc::NodeId], i: usize) -> Option { if ids.is_empty() { None } else { Some(ids[i % ids.len()].0) } } @@ -275,7 +275,7 @@ impl Op { art_nodes(d) .iter() .filter(|n| { - !d.parent_of(n.id).and_then(|p| d.node(p)).is_some_and(|p| matches!(p.kind, drawcraft_doc::NodeKind::Compound { .. })) + !d.parent_of(n.id).and_then(|p| d.node(p)).is_some_and(|p| matches!(p.kind, vectorcraft_doc::NodeKind::Compound { .. })) }) .map(|n| n.id) .collect() @@ -401,7 +401,7 @@ impl Op { } /// Apply this op to the session. - pub fn apply(&self, s: &mut Session) -> drawcraft_engine::Result { + pub fn apply(&self, s: &mut Session) -> vectorcraft_engine::Result { let (id, p) = self.command(s); s.execute(&id, &p) } @@ -409,7 +409,7 @@ impl Op { /// Keep the unused-import lint quiet when only some helpers are used. #[doc(hidden)] -pub fn _top_level(s: &Session) -> Vec { +pub fn _top_level(s: &Session) -> Vec { s.active().map(|d| top_level_ids(&d.doc)).unwrap_or_default() } diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml index 25544d227..9fa90d839 100644 --- a/crates/text/Cargo.toml +++ b/crates/text/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "drawcraft-text" +name = "vectorcraft-text" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft text: font database, shaping, layout, glyph outlines" +description = "VectorCraft text: font database, shaping, layout, glyph outlines" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-doc = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-doc = { workspace = true } kurbo = { workspace = true } skrifa = { workspace = true } harfrust = { workspace = true } diff --git a/crates/text/src/edit.rs b/crates/text/src/edit.rs index f3b53d68e..b58cc04d7 100644 --- a/crates/text/src/edit.rs +++ b/crates/text/src/edit.rs @@ -4,7 +4,7 @@ use std::ops::Range; -use drawcraft_doc::{CharStyle, TextRun}; +use vectorcraft_doc::{CharStyle, TextRun}; /// Total byte length of `runs`. pub fn runs_len(runs: &[TextRun]) -> usize { diff --git a/crates/text/src/features.rs b/crates/text/src/features.rs index 620714ed1..64af1706e 100644 --- a/crates/text/src/features.rs +++ b/crates/text/src/features.rs @@ -1,7 +1,7 @@ //! OpenType feature selection (the OpenType panel), resolved per character style. -use drawcraft_doc::CharStyle; use harfrust::{Feature, Tag}; +use vectorcraft_doc::CharStyle; /// OpenType features applied during shaping. Kerning, `case` and ligature suppression are driven /// by the character style (`kerning`, `all_caps`, `tracking`); the rest are layout-wide options. @@ -146,8 +146,8 @@ impl OtFeatures { mod tests { use super::*; use crate::{FontDb, layout}; - use drawcraft_doc::TextObject; - use drawcraft_geom::Point; + use vectorcraft_doc::TextObject; + use vectorcraft_geom::Point; fn glyphs(features: &[&str]) -> usize { let st = CharStyle { font_family: "Source Serif 4".into(), features: features.iter().map(|s| s.to_string()).collect(), ..Default::default() }; diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index af1ceec78..903fabb47 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -2,8 +2,8 @@ use std::ops::Range; -use drawcraft_doc::{CharStyle, Justify, ParaStyle, TextKind, TextObject}; use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathEl, PathSeg, Point, Rect, Shape, Vec2}; +use vectorcraft_doc::{CharStyle, Justify, ParaStyle, TextKind, TextObject}; use crate::composer::{Breakpoint, compose}; use crate::fontdb::FontDb; diff --git a/crates/text/src/lib.rs b/crates/text/src/lib.rs index dc841125f..64a76205c 100644 --- a/crates/text/src/lib.rs +++ b/crates/text/src/lib.rs @@ -1,6 +1,6 @@ -//! DrawCraft text: font database, shaping, layout, glyph outlines. +//! VectorCraft text: font database, shaping, layout, glyph outlines. //! -//! API contract used by `drawcraft-render`, `drawcraft-tools` and the UI: +//! API contract used by `vectorcraft-render`, `vectorcraft-tools` and the UI: //! - [`FontDb::global`]: process-wide database preloaded with the bundled OFL fonts. //! - [`layout`]: lays out a [`TextObject`] into glyph outlines in *text space* (apply `t.xf` to //! get document coordinates), plus line and caret information. @@ -18,13 +18,13 @@ mod layout; mod shape; pub mod thread; -pub use drawcraft_doc::TextObject; pub use features::OtFeatures; pub use fontdb::{FALLBACK_FAMILY, FontDb, FontFace}; use kurbo::{BezPath, Point, Rect, Vec2}; pub use layout::{layout, layout_with}; +pub use vectorcraft_doc::TextObject; -pub use drawcraft_doc::FirstBaseline; +pub use vectorcraft_doc::FirstBaseline; /// Paragraph composer (Paragraph panel menu). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] diff --git a/crates/text/src/shape.rs b/crates/text/src/shape.rs index 2722968f8..aab095aa5 100644 --- a/crates/text/src/shape.rs +++ b/crates/text/src/shape.rs @@ -3,10 +3,10 @@ use std::ops::Range; use std::sync::Arc; -use drawcraft_doc::CharStyle; use harfrust::{Direction, Feature, ShapeOptions, UnicodeBuffer}; use skrifa::MetadataProvider; use skrifa::instance::{LocationRef, Size}; +use vectorcraft_doc::CharStyle; use crate::features::OtFeatures; use crate::fontdb::{FontDb, FontFace}; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index a733fd57f..0283c3877 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -1,7 +1,7 @@ use super::*; -use drawcraft_doc::{CharStyle, Justify, TextKind, TextRun}; -use drawcraft_geom::PathData; use kurbo::{Affine, Shape}; +use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun}; +use vectorcraft_geom::PathData; fn db() -> &'static FontDb { FontDb::global() diff --git a/crates/text/src/tests_typo.rs b/crates/text/src/tests_typo.rs index ed2c1571c..99335006b 100644 --- a/crates/text/src/tests_typo.rs +++ b/crates/text/src/tests_typo.rs @@ -2,9 +2,9 @@ //! hyphenation, OpenType features, area type options. use super::*; -use drawcraft_doc::{CharStyle, Justify, TextKind, TextRun}; -use drawcraft_geom::PathData; use kurbo::{Affine, Circle, Shape}; +use vectorcraft_doc::{CharStyle, Justify, TextKind, TextRun}; +use vectorcraft_geom::PathData; fn db() -> &'static FontDb { FontDb::global() diff --git a/crates/text/src/thread.rs b/crates/text/src/thread.rs index 0dafa1125..0652d4d4a 100644 --- a/crates/text/src/thread.rs +++ b/crates/text/src/thread.rs @@ -4,7 +4,7 @@ //! testing need nothing special; after an edit the engine joins the slices and re-distributes them //! with [`distribute`]. -use drawcraft_doc::{TextObject, TextRun}; +use vectorcraft_doc::{TextObject, TextRun}; use crate::edit::{normalize, runs_len, slice_runs, style_at}; use crate::{FontDb, layout}; @@ -59,11 +59,11 @@ pub fn distribute(db: &FontDb, frames: &[&TextObject], runs: &[TextRun]) -> Vec< #[cfg(test)] mod tests { use super::*; - use drawcraft_doc::{CharStyle, TextKind}; - use drawcraft_geom::{Affine, Rect, shapes}; + use vectorcraft_doc::{CharStyle, TextKind}; + use vectorcraft_geom::{Affine, Rect, shapes}; fn frame(x: f64) -> TextObject { - let mut t = TextObject::point(drawcraft_geom::Point::ZERO, "", CharStyle::default()); + let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "", CharStyle::default()); t.kind = TextKind::Area { frame: shapes::rectangle(Rect::new(x, 0.0, x + 120.0, 60.0)) }; t.xf = Affine::IDENTITY; t diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index fdec31d7d..5c6c12978 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -1,19 +1,19 @@ [package] -name = "drawcraft-tools" +name = "vectorcraft-tools" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft tool state machines: pointer events in, commands and overlays out" +description = "VectorCraft tool state machines: pointer events in, commands and overlays out" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-pathops = { workspace = true } -drawcraft-text = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-pathops = { workspace = true } +vectorcraft-text = { workspace = true } kurbo = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/tools/src/bbox.rs b/crates/tools/src/bbox.rs index ae75f5ed8..2227bef27 100644 --- a/crates/tools/src/bbox.rs +++ b/crates/tools/src/bbox.rs @@ -1,6 +1,6 @@ //! Bounding-box handle geometry shared by the Selection tool and the UI. -use drawcraft_geom::{Affine, Point, Rect, Vec2}; +use vectorcraft_geom::{Affine, Point, Rect, Vec2}; /// The 8 handles, clockwise from top-left. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -106,7 +106,7 @@ pub fn rotate_for_drag(center: Point, from: Point, to: Point, snap: bool) -> (Af /// Move constrained to 45° multiples when `shift`. pub fn move_delta(from: Point, to: Point, shift: bool) -> Vec2 { let d = to - from; - if shift { drawcraft_geom::constrain_angle(d, 45.0) } else { d } + if shift { vectorcraft_geom::constrain_angle(d, 45.0) } else { d } } #[cfg(test)] diff --git a/crates/tools/src/builder.rs b/crates/tools/src/builder.rs index 3f6a5a97e..e78341b60 100644 --- a/crates/tools/src/builder.rs +++ b/crates/tools/src/builder.rs @@ -1,7 +1,7 @@ //! Shape Builder (Shift+M), Live Paint Bucket (K) and Live Paint Selection (Shift+L). //! //! The tools only emit commands (`shapeBuilder.merge`, `livePaint.fill`, `livePaint.strokeEdge`, -//! `select.*`); the planar arrangement comes from `drawcraft_pathops::regions`. +//! `select.*`); the planar arrangement comes from `vectorcraft_pathops::regions`. //! //! **Live Paint groups** are ordinary groups (no special node kind): the group's name starts with //! [`LIVE_PAINT_PREFIX`]; its first child is a hidden group named [`SOURCES_NAME`] holding the @@ -12,10 +12,10 @@ use std::sync::Arc; -use drawcraft_doc::{Document, Node, NodeId, NodeKind}; -use drawcraft_geom::{BezPath, FillRule, PathData, Point, Rect, Shape as _}; -use drawcraft_pathops as po; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeId, NodeKind}; +use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect, Shape as _}; +use vectorcraft_pathops as po; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, json_ids}; @@ -561,8 +561,8 @@ pub fn create(id: &str) -> Option> { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::{Appearance, Selection}; - use drawcraft_geom::shapes; + use vectorcraft_doc::{Appearance, Selection}; + use vectorcraft_geom::shapes; fn two_rects() -> (Document, NodeId, NodeId) { let mut d = Document::new(500.0, 500.0); diff --git a/crates/tools/src/direct.rs b/crates/tools/src/direct.rs index 7c977d8db..f6e2b6edd 100644 --- a/crates/tools/src/direct.rs +++ b/crates/tools/src/direct.rs @@ -5,10 +5,10 @@ //! reshape, marquee to select anchors. Group Selection: click selects the leaf; each further click //! on it adds the next enclosing group. -use drawcraft_doc::hit::hit_test; -use drawcraft_doc::{AnchorRef, NodeId, NodeKind}; -use drawcraft_geom::{Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_doc::hit::hit_test; +use vectorcraft_doc::{AnchorRef, NodeId, NodeKind}; +use vectorcraft_geom::{Point, Rect}; use crate::bbox::move_delta; use crate::select::matrix_json; @@ -171,7 +171,7 @@ impl Tool for DirectSelectionTool { let d = move_delta(start, p, ev.mods.shift); out.push(Action::Preview( "object.transform".into(), - json!({"matrix": matrix_json(drawcraft_geom::Affine::translate(d)), "copy": ev.mods.alt}), + json!({"matrix": matrix_json(vectorcraft_geom::Affine::translate(d)), "copy": ev.mods.alt}), )); out } @@ -231,7 +231,7 @@ impl Tool for DirectSelectionTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn click_anchor_selects_it() { diff --git a/crates/tools/src/distort.rs b/crates/tools/src/distort.rs index ccef76f76..ab22cdba0 100644 --- a/crates/tools/src/distort.rs +++ b/crates/tools/src/distort.rs @@ -19,9 +19,9 @@ mod width; use std::sync::Arc; -use drawcraft_color::Paint; -use drawcraft_doc::{Appearance, AppearanceItem, Node, NodeKind}; -use drawcraft_geom::{Affine, Point, Vec2}; +use vectorcraft_color::Paint; +use vectorcraft_doc::{Appearance, AppearanceItem, Node, NodeKind}; +use vectorcraft_geom::{Affine, Point, Vec2}; use crate::Tool; @@ -122,21 +122,21 @@ pub fn collect_points(n: &Node, out: &mut Vec) { pub(crate) const BLUE: [u8; 3] = [0x4a, 0x7c, 0xff]; /// A small diamond polygon at `p` (half-diagonal `r`). -pub(crate) fn diamond(p: Point, r: f64) -> drawcraft_geom::BezPath { +pub(crate) fn diamond(p: Point, r: f64) -> vectorcraft_geom::BezPath { crate::xform::polygon(&[Point::new(p.x, p.y - r), Point::new(p.x + r, p.y), Point::new(p.x, p.y + r), Point::new(p.x - r, p.y)], true) } /// A circle / ellipse outline for brush feedback. -pub(crate) fn ellipse_path(c: Point, rx: f64, ry: f64, angle_deg: f64) -> drawcraft_geom::BezPath { - use drawcraft_geom::Shape; - let e = drawcraft_geom::kurbo::Ellipse::new(c, (rx.max(0.01), ry.max(0.01)), angle_deg.to_radians()); +pub(crate) fn ellipse_path(c: Point, rx: f64, ry: f64, angle_deg: f64) -> vectorcraft_geom::BezPath { + use vectorcraft_geom::Shape; + let e = vectorcraft_geom::kurbo::Ellipse::new(c, (rx.max(0.01), ry.max(0.01)), angle_deg.to_radians()); e.to_path(0.05) } #[cfg(test)] mod tests { use super::*; - use drawcraft_geom::{PathData, Rect, shapes}; + use vectorcraft_geom::{PathData, Rect, shapes}; #[test] fn create_covers_all_distort_tools() { @@ -160,7 +160,7 @@ mod tests { #[test] fn warp_node_maps_all_points_and_drops_live_shape() { - let mut n = Node::path(drawcraft_doc::NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()); + let mut n = Node::path(vectorcraft_doc::NodeId(1), shapes::rectangle(Rect::new(0.0, 0.0, 10.0, 10.0)), Appearance::default_art()); warp_node_with(&mut n, &|p| Point::new(p.x * 2.0, p.y + 1.0)); let b = n.geometric_bounds().unwrap(); assert_eq!((b.x0, b.y0, b.x1, b.y1), (0.0, 1.0, 20.0, 11.0)); diff --git a/crates/tools/src/distort/arap.rs b/crates/tools/src/distort/arap.rs index 61cbc7cc7..fbe8bbf4d 100644 --- a/crates/tools/src/distort/arap.rs +++ b/crates/tools/src/distort/arap.rs @@ -15,8 +15,8 @@ //! - Artwork points (anchors and handles) follow the mesh through barycentric coordinates in //! their rest triangle. -use drawcraft_geom::kurbo::ParamCurveNearest; -use drawcraft_geom::{BezPath, Point, Rect, Shape, Vec2}; +use vectorcraft_geom::kurbo::ParamCurveNearest; +use vectorcraft_geom::{BezPath, Point, Rect, Shape, Vec2}; /// Banded symmetric positive definite matrix (lower band stored row by row). #[derive(Clone, Debug)] @@ -282,12 +282,12 @@ fn closed_winding(o: &BezPath, p: Point) -> i32 { }; for el in o.elements() { match el { - drawcraft_geom::PathEl::MoveTo(_) => { + vectorcraft_geom::PathEl::MoveTo(_) => { flush(&mut cur, closed, &mut total); closed = false; cur.push(*el); } - drawcraft_geom::PathEl::ClosePath => { + vectorcraft_geom::PathEl::ClosePath => { closed = true; cur.push(*el); } @@ -468,7 +468,7 @@ pub fn auto_pins(mesh: &Mesh, count: usize) -> Vec { #[cfg(test)] mod tests { use super::*; - use drawcraft_geom::shapes; + use vectorcraft_geom::shapes; #[test] fn band_cholesky_solves_spd_systems() { diff --git a/crates/tools/src/distort/liquify.rs b/crates/tools/src/distort/liquify.rs index c708b708d..a56d5f268 100644 --- a/crates/tools/src/distort/liquify.rs +++ b/crates/tools/src/distort/liquify.rs @@ -11,9 +11,9 @@ //! displacement field weighted by a smooth falloff `(1 − r²)²` inside the (elliptical, rotated) //! brush. Noise for Scallop/Crystallize/Wrinkle is a hash of the dab and anchor indices. -use drawcraft_geom::kurbo::ParamCurveArclen; -use drawcraft_geom::{Anchor, AnchorKind, PathData, Point, Rect, SubPath, Vec2}; use serde_json::{Value, json}; +use vectorcraft_geom::kurbo::ParamCurveArclen; +use vectorcraft_geom::{Anchor, AnchorKind, PathData, Point, Rect, SubPath, Vec2}; use super::ellipse_path; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext}; @@ -251,9 +251,9 @@ fn subdivide(sp: &mut SubPath, prm: &LiquifyParams, c: Point, spacing: f64) { return; } let cub = sp.segment(seg); - let sb = drawcraft_geom::kurbo::ParamCurveExtrema::bounding_box(&cub); + let sb = vectorcraft_geom::kurbo::ParamCurveExtrema::bounding_box(&cub); let touched = sb.intersect(bb).area() > 0.0 || (sb.width() == 0.0 || sb.height() == 0.0) && sb.inflate(1e-6, 1e-6).intersect(bb).area() > 0.0; - if !touched || !(0..=8).any(|i| prm.falloff(c, drawcraft_geom::kurbo::ParamCurve::eval(&cub, i as f64 / 8.0)) > 0.0) { + if !touched || !(0..=8).any(|i| prm.falloff(c, vectorcraft_geom::kurbo::ParamCurve::eval(&cub, i as f64 / 8.0)) > 0.0) { seg += 1; continue; } @@ -553,7 +553,7 @@ impl Tool for LiquifyTool { #[cfg(test)] mod tests { use super::*; - use drawcraft_geom::shapes; + use vectorcraft_geom::shapes; fn square() -> PathData { shapes::rectangle(Rect::new(0.0, 0.0, 200.0, 200.0)) diff --git a/crates/tools/src/distort/pathutil.rs b/crates/tools/src/distort/pathutil.rs index 6c723673b..d162d0f4c 100644 --- a/crates/tools/src/distort/pathutil.rs +++ b/crates/tools/src/distort/pathutil.rs @@ -1,7 +1,7 @@ //! Arc-length helpers: width points live at a fraction (0..1) of a subpath's length. -use drawcraft_geom::kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; -use drawcraft_geom::{PathData, Point, SubPath, Vec2}; +use vectorcraft_geom::kurbo::{ParamCurve, ParamCurveArclen, ParamCurveDeriv}; +use vectorcraft_geom::{PathData, Point, SubPath, Vec2}; const ACC: f64 = 1e-4; @@ -42,7 +42,7 @@ pub fn eval_fraction(sp: &SubPath, f: f64) -> Option<(Point, Vec2)> { } /// Unit tangent of a cubic at `t` (falls back to the chord for degenerate derivatives). -pub fn tangent(c: &drawcraft_geom::CubicBez, t: f64) -> Vec2 { +pub fn tangent(c: &vectorcraft_geom::CubicBez, t: f64) -> Vec2 { let d = c.deriv().eval(t).to_vec2(); let d = if d.hypot() > 1e-9 { d diff --git a/crates/tools/src/distort/perspective.rs b/crates/tools/src/distort/perspective.rs index f2bd903f3..0ab111f35 100644 --- a/crates/tools/src/distort/perspective.rs +++ b/crates/tools/src/distort/perspective.rs @@ -17,10 +17,10 @@ use std::collections::BTreeMap; -use drawcraft_doc::{Document, NodeId}; -use drawcraft_geom::{Point, Rect}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeId}; +use vectorcraft_geom::{Point, Rect}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext}; @@ -606,7 +606,7 @@ impl Tool for PerspectiveSelectionTool { { return vec![Action::Exec("perspective.plane.set".into(), json!({"plane": pl.id()}))]; } - let Some(h) = drawcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options()) else { + let Some(h) = vectorcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options()) else { return vec![Action::Exec("select.set".into(), json!({"ids": []}))]; }; let top = h.top_object(cx.isolation); @@ -671,7 +671,7 @@ impl Tool for PerspectiveSelectionTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn grid(kind: u8) -> PerspectiveGrid { PerspectiveGrid::preset(kind, Rect::new(0.0, 0.0, 800.0, 600.0)) diff --git a/crates/tools/src/distort/puppet.rs b/crates/tools/src/distort/puppet.rs index 233aa4cb7..575e29693 100644 --- a/crates/tools/src/distort/puppet.rs +++ b/crates/tools/src/distort/puppet.rs @@ -7,9 +7,9 @@ //! with the dragged pin at the pointer), committed on release as one undo step. The mesh follows //! the current shape, so successive drags compose. -use drawcraft_doc::{Document, NodeId}; -use drawcraft_geom::{BezPath, Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, NodeId}; +use vectorcraft_geom::{BezPath, Point, Rect}; use super::arap::{Mesh, MeshOptions, auto_pins}; use super::{BLUE, collect_points}; @@ -92,7 +92,7 @@ impl Tool for PuppetWarpTool { self.sync(cx); if self.ids.is_empty() { // Click an object to select it; pins appear on the next event. - if let Some(h) = drawcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options()) { + if let Some(h) = vectorcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options()) { return vec![Action::Exec("select.set".into(), json!({"ids": [h.top_object(cx.isolation).0]}))]; } return vec![]; @@ -202,7 +202,7 @@ impl Tool for PuppetWarpTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn puppet_tool_autopins_adds_and_drags() { diff --git a/crates/tools/src/distort/width.rs b/crates/tools/src/distort/width.rs index b10890679..01a98ae8d 100644 --- a/crates/tools/src/distort/width.rs +++ b/crates/tools/src/distort/width.rs @@ -7,9 +7,9 @@ //! point. Gestures preview `stroke.widthPoint.set {id, t, left, right, index?}` (side widths in //! points) and commit on release; Delete runs `stroke.widthPoint.remove {id, index}`. -use drawcraft_doc::{Document, NodeId, NodeKind, StrokeLayer}; -use drawcraft_geom::{Point, Vec2}; use serde_json::json; +use vectorcraft_doc::{Document, NodeId, NodeKind, StrokeLayer}; +use vectorcraft_geom::{Point, Vec2}; use super::pathutil::{eval_fraction, left_normal, nearest_fraction}; use super::{BLUE, diamond}; @@ -190,7 +190,7 @@ impl Tool for WidthTool { if let Some(NodeKind::Path { path, .. }) = cx.doc.node(d.spot.id).map(|n| &n.kind) && let Some(sp) = path.subpaths.get(d.spot.sub) { - let one = drawcraft_geom::PathData::single(sp.clone()); + let one = vectorcraft_geom::PathData::single(sp.clone()); if let Some((_, t, q, tan, _)) = nearest_fraction(&one, p) { d.t = t; d.spot.p = q; @@ -270,8 +270,8 @@ impl Tool for WidthTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::{Appearance, Node, Selection, WidthProfile}; - use drawcraft_geom::{PathData, SubPath}; + use vectorcraft_doc::{Appearance, Node, Selection, WidthProfile}; + use vectorcraft_geom::{PathData, SubPath}; fn doc_line() -> (Document, NodeId) { let mut d = Document::new(500.0, 500.0); diff --git a/crates/tools/src/draw2.rs b/crates/tools/src/draw2.rs index 5f1bc912d..c1e410e66 100644 --- a/crates/tools/src/draw2.rs +++ b/crates/tools/src/draw2.rs @@ -8,9 +8,9 @@ mod curvature; mod family; mod gesture; -use drawcraft_doc::{NodeId, NodeKind}; -use drawcraft_geom::{Anchor, AnchorKind, BezPath, Point, SubPath}; use serde_json::{Value, json}; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::{Anchor, AnchorKind, BezPath, Point, SubPath}; use crate::{Tool, ToolContext}; diff --git a/crates/tools/src/draw2/anchor.rs b/crates/tools/src/draw2/anchor.rs index 9495544e1..5533b1df4 100644 --- a/crates/tools/src/draw2/anchor.rs +++ b/crates/tools/src/draw2/anchor.rs @@ -6,9 +6,9 @@ //! handle → move it independently; drag a segment → reshape the curve. //! Scissors: click a path to split it there. -use drawcraft_doc::NodeId; -use drawcraft_geom::Point; use serde_json::json; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::Point; use super::{hit_anchor, hit_segment}; use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext}; @@ -198,7 +198,7 @@ impl Tool for AnchorTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn add_and_delete_anchor_clicks() { diff --git a/crates/tools/src/draw2/curvature.rs b/crates/tools/src/draw2/curvature.rs index bd1025e18..a71519710 100644 --- a/crates/tools/src/draw2/curvature.rs +++ b/crates/tools/src/draw2/curvature.rs @@ -5,9 +5,9 @@ //! first point to close; Backspace/Delete removes the last touched point; Esc/Enter ends the path. //! A rubber band shows the curve to the cursor. -use drawcraft_doc::NodeId; -use drawcraft_geom::Point; use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::Point; use super::{FEEDBACK, catmull_rom}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -174,7 +174,7 @@ impl Tool for CurvatureTool { let mut pts = self.pts.clone(); pts.push((h, false)); let sp = catmull_rom(&pts, false); - let mut bp = drawcraft_geom::BezPath::new(); + let mut bp = vectorcraft_geom::BezPath::new(); sp.to_bezpath_into(&mut bp); o.push(Overlay::Path { path: bp, color: FEEDBACK, width: 1.0, dashed: false }); } @@ -192,7 +192,7 @@ impl Tool for CurvatureTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn first_click_creates_then_notify_tracks_path() { diff --git a/crates/tools/src/draw2/family.rs b/crates/tools/src/draw2/family.rs index 90c02a22b..9510da344 100644 --- a/crates/tools/src/draw2/family.rs +++ b/crates/tools/src/draw2/family.rs @@ -4,8 +4,8 @@ //! without dragging asks the UI for the options dialog. While dragging, ↑/↓ change the spiral's //! segments, the grid rows or the concentric dividers; ←/→ change grid columns / radial dividers. -use drawcraft_geom::{Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_geom::{Point, Rect, Vec2}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -219,7 +219,7 @@ impl Tool for FamilyTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn grid_drag_and_arrows() { diff --git a/crates/tools/src/draw2/gesture.rs b/crates/tools/src/draw2/gesture.rs index e2ef430bf..7df2dd9ba 100644 --- a/crates/tools/src/draw2/gesture.rs +++ b/crates/tools/src/draw2/gesture.rs @@ -6,9 +6,9 @@ //! near an end of a selected open path continues it. The other tools collect the drag polyline //! (shown as an overlay) and run one command on release. -use drawcraft_doc::NodeId; -use drawcraft_geom::Point; use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::Point; use super::{FEEDBACK, points_json, polyline}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -257,7 +257,7 @@ impl Tool for GestureTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn drag(t: &mut GestureTool, cx: &ToolContext, pts: &[(f64, f64)], m: Mods) -> Vec> { let mut v = vec![t.pointer(cx, &PointerEvent::new(PointerKind::Down, pts[0].0, pts[0].1))]; diff --git a/crates/tools/src/guides.rs b/crates/tools/src/guides.rs index 70582aecc..709933f21 100644 --- a/crates/tools/src/guides.rs +++ b/crates/tools/src/guides.rs @@ -1,8 +1,8 @@ //! Smart Guides: snapping to anchors, object bounds (edges/centres), artboards, with the magenta //! construction lines and labels Illustrator users expect. -use drawcraft_doc::{Document, NodeId, NodeKind}; -use drawcraft_geom::{Point, Rect, Vec2}; +use vectorcraft_doc::{Document, NodeId, NodeKind}; +use vectorcraft_geom::{Point, Rect, Vec2}; use crate::{Overlay, ToolContext}; @@ -137,7 +137,7 @@ impl Targets { pub fn snap_draw(cx: &ToolContext, p: Point, exclude: &[NodeId]) -> (Point, Vec) { if cx.snap_to_grid { let s = cx.doc.grid.spacing / cx.doc.grid.subdivisions.max(1) as f64; - return (drawcraft_geom::snap::snap_point_to_grid(p, s), vec![]); + return (vectorcraft_geom::snap::snap_point_to_grid(p, s), vec![]); } if !cx.smart_guides { return (p, vec![]); diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index d469d2e0b..800d480f6 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft tools: pointer events in → commands and overlays out. +//! VectorCraft tools: pointer events in → commands and overlays out. //! //! Tools never mutate the document directly. They emit [`Action`]s that the engine executes: //! `Begin` snapshots the document, each `Preview` re-applies one command on top of that snapshot @@ -22,11 +22,11 @@ pub mod symbolism; pub mod text; pub mod xform; -use drawcraft_color::Paint; -use drawcraft_doc::{Document, NodeId, Selection}; -use drawcraft_geom::{BezPath, Point, Rect}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use vectorcraft_color::Paint; +use vectorcraft_doc::{Document, NodeId, Selection}; +use vectorcraft_geom::{BezPath, Point, Rect}; pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info}; @@ -154,8 +154,8 @@ impl ToolContext<'_> { pub fn tol(&self, px: f64) -> f64 { px / self.zoom.max(1e-9) } - pub fn hit_options(&self) -> drawcraft_doc::hit::HitOptions { - drawcraft_doc::hit::HitOptions { tol: self.tol(3.0), outline: self.outline, path_only: false } + pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions { + vectorcraft_doc::hit::HitOptions { tol: self.tol(3.0), outline: self.outline, path_only: false } } } @@ -280,8 +280,8 @@ pub(crate) fn json_ids(ids: &[NodeId]) -> Value { #[cfg(test)] pub(crate) mod testutil { use super::*; - use drawcraft_doc::{Appearance, Node}; - use drawcraft_geom::shapes; + use vectorcraft_doc::{Appearance, Node}; + use vectorcraft_geom::shapes; pub fn doc_with_rect() -> (Document, NodeId) { let mut d = Document::new(500.0, 500.0); @@ -292,7 +292,11 @@ pub(crate) mod testutil { } pub fn paint() -> PaintDefaults { - PaintDefaults { fill: Paint::solid(drawcraft_color::Color::WHITE), stroke: Paint::solid(drawcraft_color::Color::BLACK), stroke_width: 1.0 } + PaintDefaults { + fill: Paint::solid(vectorcraft_color::Color::WHITE), + stroke: Paint::solid(vectorcraft_color::Color::BLACK), + stroke_width: 1.0, + } } pub fn cx<'a>(d: &'a Document, s: &'a Selection, p: &'a PaintDefaults) -> ToolContext<'a> { diff --git a/crates/tools/src/meshblend.rs b/crates/tools/src/meshblend.rs index 0bea1f73c..7bfd167c7 100644 --- a/crates/tools/src/meshblend.rs +++ b/crates/tools/src/meshblend.rs @@ -7,10 +7,10 @@ //! colour unless Shift); drag a mesh point → `object.mesh.movePoint` previews; Alt-click a mesh //! point → `object.mesh.deletePoint`. -use drawcraft_color::Paint; -use drawcraft_doc::{NodeId, NodeKind}; -use drawcraft_geom::Point; use serde_json::json; +use vectorcraft_color::Paint; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::Point; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext}; @@ -25,7 +25,7 @@ pub fn create(id: &str) -> Option> { } fn hit_top(cx: &ToolContext, p: Point) -> Option<(NodeId, NodeId)> { - let h = drawcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options())?; + let h = vectorcraft_doc::hit::hit_test(cx.doc, p, cx.hit_options())?; Some((h.top_object(cx.isolation), h.leaf)) } @@ -191,15 +191,15 @@ impl Tool for MeshTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn blend_tool_two_clicks_make_blend() { let (mut d, a) = doc_with_rect(); let l = d.layers[0].id; let b = d.alloc_id(); - let r = drawcraft_geom::shapes::rectangle(drawcraft_geom::Rect::new(300.0, 100.0, 350.0, 150.0)); - d.insert(Some(l), 1, drawcraft_doc::Node::path(b, r, drawcraft_doc::Appearance::default_art())).unwrap(); + let r = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(300.0, 100.0, 350.0, 150.0)); + d.insert(Some(l), 1, vectorcraft_doc::Node::path(b, r, vectorcraft_doc::Appearance::default_art())).unwrap(); let (s, p) = (Selection::default(), paint()); let c = cx(&d, &s, &p); let mut t = create("blend").unwrap(); diff --git a/crates/tools/src/pen.rs b/crates/tools/src/pen.rs index 7ecbf8467..70411f9c2 100644 --- a/crates/tools/src/pen.rs +++ b/crates/tools/src/pen.rs @@ -5,9 +5,9 @@ //! Enter/Esc (or switching tools) ends the path. Clicking the end of a selected open path continues //! it. The rubber-band preview shows the next segment. -use drawcraft_doc::{NodeId, NodeKind}; -use drawcraft_geom::{BezPath, Point}; use serde_json::json; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::{BezPath, Point}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -44,7 +44,7 @@ impl Tool for PenTool { self.drag.is_some() } fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec { - let exclude: Vec = if self.drawing { cx.selection.objects.clone() } else { vec![] }; + let exclude: Vec = if self.drawing { cx.selection.objects.clone() } else { vec![] }; let (mut p, _) = if matches!(ev.kind, PointerKind::Down) { crate::guides::snap_draw(cx, ev.pos, &exclude) } else { (ev.pos, vec![]) }; let tol = cx.tol(5.0); let active = if self.drawing { active_path(cx) } else { None }; @@ -59,7 +59,7 @@ impl Tool for PenTool { PointerKind::Down => { if let Some((id, first, last, _)) = active { if ev.mods.shift { - p = last + drawcraft_geom::constrain_angle(p - last, 45.0); + p = last + vectorcraft_geom::constrain_angle(p - last, 45.0); } if p.distance(first) <= tol { self.drag = Some((first, true)); @@ -86,7 +86,7 @@ impl Tool for PenTool { let Some((a, closing)) = self.drag else { return vec![] }; let mut out_h = ev.pos; if ev.mods.shift { - out_h = a + drawcraft_geom::constrain_angle(ev.pos - a, 45.0); + out_h = a + vectorcraft_geom::constrain_angle(ev.pos - a, 45.0); } let alt = ev.mods.alt; let Some((id, ..)) = active_path(cx).or(active) else { @@ -165,7 +165,7 @@ impl Tool for PenTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn first_click_creates_path() { diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 76000ca1c..55f50203c 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -2,10 +2,10 @@ //! bounding-box scale (Shift proportional, Alt from centre) and rotate (outside corners, Shift 45°), //! double-click to enter isolation mode. -use drawcraft_doc::NodeId; -use drawcraft_doc::hit::{hit_test, marquee}; -use drawcraft_geom::{Affine, Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; +use vectorcraft_doc::hit::{hit_test, marquee}; +use vectorcraft_geom::{Affine, Point, Rect}; use crate::bbox::{Handle, hit_handle, in_rotate_zone, move_delta, rotate_for_drag, scale_for_drag}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, json_ids}; @@ -77,10 +77,10 @@ impl Tool for SelectionTool { self.state = State::Idle; if let Some(h) = hit_test(cx.doc, p, cx.hit_options()) { let top = h.top_object(cx.isolation); - if cx.doc.node(top).is_some_and(|n| matches!(n.kind, drawcraft_doc::NodeKind::Group { .. })) { + if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Group { .. })) { return vec![Action::Exec("object.isolate".into(), json!({ "id": top.0 }))]; } - if cx.doc.node(top).is_some_and(|n| matches!(n.kind, drawcraft_doc::NodeKind::Text(_))) { + if cx.doc.node(top).is_some_and(|n| matches!(n.kind, vectorcraft_doc::NodeKind::Text(_))) { return vec![Action::SwitchTool("type".into())]; } } else if cx.isolation.is_some() { @@ -136,7 +136,7 @@ impl Tool for SelectionTool { } let mut d = move_delta(start, p, m.shift); if !began { - let sel_ids: Vec = cx.selection.objects.clone(); + let sel_ids: Vec = cx.selection.objects.clone(); self.start_bounds = cx.doc.bounds_of(&sel_ids, false); self.targets = cx.smart_guides.then(|| crate::guides::Targets::collect(cx.doc, &sel_ids, None)); } @@ -248,7 +248,7 @@ fn handle_cursor(h: Handle) -> Cursor { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) diff --git a/crates/tools/src/shape.rs b/crates/tools/src/shape.rs index 7c1d66a44..3338ff8f2 100644 --- a/crates/tools/src/shape.rs +++ b/crates/tools/src/shape.rs @@ -4,8 +4,8 @@ //! drag asks the UI for the size dialog (like Illustrator). ↑/↓ during a polygon/star drag change the //! side/point count. -use drawcraft_geom::{Point, Rect}; use serde_json::{Value, json}; +use vectorcraft_geom::{Point, Rect}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -76,14 +76,14 @@ impl ShapeTool { } "lineSegment" => { let (a, b) = if m.alt { (start - (p - start), p) } else { (start, p) }; - let b = if m.shift { a + drawcraft_geom::constrain_angle(b - a, 45.0) } else { b }; + let b = if m.shift { a + vectorcraft_geom::constrain_angle(b - a, 45.0) } else { b }; ("shape.line".into(), json!({ "x1": a.x, "y1": a.y, "x2": b.x, "y2": b.y })) } _ => { let mut d = p - start; if m.shift { let s = d.x.abs().max(d.y.abs()); - d = drawcraft_geom::Vec2::new(s * d.x.signum(), s * d.y.signum()); + d = vectorcraft_geom::Vec2::new(s * d.x.signum(), s * d.y.signum()); } let r = if m.alt { Rect::from_points(start - d, start + d) } else { Rect::from_points(start, start + d) }; let cmd = if self.id == "ellipse" { "shape.ellipse" } else { "shape.rectangle" }; @@ -217,7 +217,7 @@ impl Tool for ShapeTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn rect_drag() { diff --git a/crates/tools/src/symbolism.rs b/crates/tools/src/symbolism.rs index b3eab0470..2108ec317 100644 --- a/crates/tools/src/symbolism.rs +++ b/crates/tools/src/symbolism.rs @@ -7,8 +7,8 @@ //! `path.freehand` previews into `brush.freehand` when a brush is current (tool option `brush`, //! else the document's current brush). -use drawcraft_geom::{Point, Shape}; use serde_json::{Value, json}; +use vectorcraft_geom::{Point, Shape}; use crate::draw2::{FEEDBACK, GestureTool, points_json}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -258,7 +258,7 @@ impl Tool for BrushPaintTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn sprayer_previews_all_points_and_commits() { diff --git a/crates/tools/src/text.rs b/crates/tools/src/text.rs index 85a38c772..8ef68732a 100644 --- a/crates/tools/src/text.rs +++ b/crates/tools/src/text.rs @@ -14,11 +14,11 @@ use std::sync::{Arc, Mutex}; -use drawcraft_doc::hit::hit_test; -use drawcraft_doc::{NodeId, NodeKind, TextKind, TextObject, TextRun}; -use drawcraft_geom::{BezPath, Point, Rect, Shape}; -use drawcraft_text::{FontDb, TextLayout, edit}; use serde_json::{Value, json}; +use vectorcraft_doc::hit::hit_test; +use vectorcraft_doc::{NodeId, NodeKind, TextKind, TextObject, TextRun}; +use vectorcraft_geom::{BezPath, Point, Rect, Shape}; +use vectorcraft_text::{FontDb, TextLayout, edit}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -84,7 +84,7 @@ impl TypeTool { { return l.clone(); } - let l = Arc::new(drawcraft_text::layout(FontDb::global(), t)); + let l = Arc::new(vectorcraft_text::layout(FontDb::global(), t)); *c = Some((t.clone(), l.clone())); l } @@ -187,7 +187,7 @@ impl TypeTool { TextKind::Area { frame } => frame.bounds().is_some_and(|b| b.inflate(tol, tol).contains(local)), _ => lay.bounds.inflate(tol, tol).contains(local), }; - inside.then(|| drawcraft_text::hit_byte(&lay, local)) + inside.then(|| vectorcraft_text::hit_byte(&lay, local)) } fn select_word_or_para(&mut self, cx: &ToolContext, para: bool) { @@ -199,7 +199,7 @@ impl TypeTool { /// Path under `p` for the Area Type / Type on a Path tools. fn path_at(cx: &ToolContext, p: Point, closed: bool) -> Option { - let h = hit_test(cx.doc, p, drawcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, path_only: false })?; + let h = hit_test(cx.doc, p, vectorcraft_doc::hit::HitOptions { tol: cx.tol(4.0), outline: true, path_only: false })?; match &cx.doc.node(h.leaf)?.kind { NodeKind::Path { path, .. } if !closed || path.is_closed() => Some(h.leaf), _ => None, @@ -213,7 +213,7 @@ impl TypeTool { && let Some(NodeKind::Text(t)) = cx.doc.node(h.leaf).map(|n| &n.kind) { let lay = self.layout(t); - let byte = drawcraft_text::hit_byte(&lay, t.xf.inverse() * start); + let byte = vectorcraft_text::hit_byte(&lay, t.xf.inverse() * start); self.start_editing(h.leaf, byte); self.clicks = (Some(start), 1); out.push(Action::Exec("select.set".into(), json!({"ids": [h.leaf.0]}))); @@ -247,7 +247,7 @@ impl TypeTool { // The out port: bottom-right of the frame (end of the path for type on a path). let port = match &t.kind { TextKind::Area { frame } => frame.bounds().map(|b| t.xf * Point::new(b.x1, b.y1)), - TextKind::OnPath { path, .. } => path.to_bezpath().segments().last().map(|s| t.xf * drawcraft_geom::ParamCurve::end(&s)), + TextKind::OnPath { path, .. } => path.to_bezpath().segments().last().map(|s| t.xf * vectorcraft_geom::ParamCurve::end(&s)), TextKind::Point => None, }; let Some(c) = port else { return }; @@ -321,7 +321,7 @@ impl Tool for TypeTool { if self.selecting { if let Some(t) = self.current(cx) { let lay = self.layout(&t); - self.caret = drawcraft_text::hit_byte(&lay, t.xf.inverse() * ev.pos); + self.caret = vectorcraft_text::hit_byte(&lay, t.xf.inverse() * ev.pos); self.clicks = (None, 0); } } else if self.press.is_some() { @@ -416,7 +416,7 @@ impl Tool for TypeTool { ToolKey::Up | ToolKey::Down => { let d = if key == ToolKey::Up { -1 } else { 1 }; let from = if a != b && !mods.shift { if d < 0 { a } else { b } } else { self.caret }; - let x = self.goal_x.unwrap_or_else(|| drawcraft_text::caret_position(&lay, from).0.x); + let x = self.goal_x.unwrap_or_else(|| vectorcraft_text::caret_position(&lay, from).0.x); let to = if mods.cmd { // Cmd+Up/Down: paragraph start / end. let p = edit::paragraph_at(&text, from); @@ -428,18 +428,18 @@ impl Tool for TypeTool { p.end } } else { - drawcraft_text::caret_vertical(&lay, from, d, x) + vectorcraft_text::caret_vertical(&lay, from, d, x) }; let out = self.move_to(to, mods.shift); self.goal_x = Some(x); out } ToolKey::Home => { - let to = if mods.cmd { 0 } else { drawcraft_text::line_home(&lay, self.caret) }; + let to = if mods.cmd { 0 } else { vectorcraft_text::line_home(&lay, self.caret) }; self.move_to(to, mods.shift) } ToolKey::End => { - let to = if mods.cmd { len } else { drawcraft_text::line_end_of(&lay, self.caret) }; + let to = if mods.cmd { len } else { vectorcraft_text::line_end_of(&lay, self.caret) }; self.move_to(to, mods.shift) } _ => vec![], @@ -525,10 +525,10 @@ impl Tool for TypeTool { } let (a, b) = (self.caret.min(self.anchor).min(len), self.caret.max(self.anchor).min(len)); if a == b { - let (p, q) = drawcraft_text::caret_position(&lay, self.caret.min(len)); + let (p, q) = vectorcraft_text::caret_position(&lay, self.caret.min(len)); o.push(Overlay::Line { a: t.xf * p, b: t.xf * q, color: [0, 0, 0], dashed: false }); } else { - for q in drawcraft_text::selection_quads(&lay, a, b) { + for q in vectorcraft_text::selection_quads(&lay, a, b) { o.push(Overlay::Highlight { quad: q.map(|p| t.xf * p), color: [60, 120, 255, 90] }); } } diff --git a/crates/tools/src/text_tests.rs b/crates/tools/src/text_tests.rs index f4253a3c4..16fabcd44 100644 --- a/crates/tools/src/text_tests.rs +++ b/crates/tools/src/text_tests.rs @@ -1,9 +1,9 @@ -//! Type tool state-machine tests (the engine side is covered in drawcraft-engine's +//! Type tool state-machine tests (the engine side is covered in vectorcraft-engine's //! `tests_textedit`). use super::*; use crate::testutil::{cx, paint}; -use drawcraft_doc::{CharStyle, Document, Node, Selection}; +use vectorcraft_doc::{CharStyle, Document, Node, Selection}; fn doc_with_text(text: &str) -> (Document, NodeId) { let mut d = Document::new(500.0, 500.0); @@ -140,8 +140,8 @@ fn double_and_triple_click_select_word_and_paragraph() { let (d, id, mut tool) = editing("Alpha beta\ngamma"); let (sel, p) = (Selection::default(), paint()); let t = TypeTool::text(&cx(&d, &sel, &p), id).unwrap().clone(); - let lay = drawcraft_text::layout(FontDb::global(), &t); - let (top, bot) = drawcraft_text::caret_position(&lay, 7); + let lay = vectorcraft_text::layout(FontDb::global(), &t); + let (top, bot) = vectorcraft_text::caret_position(&lay, 7); let at = t.xf * top.midpoint(bot); let c = cx(&d, &sel, &p); for k in [PointerKind::Down, PointerKind::Up, PointerKind::Down, PointerKind::Up, PointerKind::DoubleClick] { @@ -159,9 +159,9 @@ fn drag_selects_and_overlay_highlights() { let (sel, p) = (Selection::default(), paint()); let c = cx(&d, &sel, &p); let t = TypeTool::text(&c, id).unwrap().clone(); - let lay = drawcraft_text::layout(FontDb::global(), &t); + let lay = vectorcraft_text::layout(FontDb::global(), &t); let pt = |b: usize| { - let (a, bb) = drawcraft_text::caret_position(&lay, b); + let (a, bb) = vectorcraft_text::caret_position(&lay, b); t.xf * a.midpoint(bb) }; let (a, b) = (pt(7), pt(9)); diff --git a/crates/tools/src/xform.rs b/crates/tools/src/xform.rs index 16bff4654..706ccb0c1 100644 --- a/crates/tools/src/xform.rs +++ b/crates/tools/src/xform.rs @@ -12,8 +12,8 @@ mod measure; mod transform; mod wand; -use drawcraft_doc::{Document, NodeId, NodeKind}; -use drawcraft_geom::{BezPath, Point, Rect, Shape}; +use vectorcraft_doc::{Document, NodeId, NodeKind}; +use vectorcraft_geom::{BezPath, Point, Rect, Shape}; use crate::{Overlay, Tool}; @@ -50,7 +50,7 @@ pub const BLUE: [u8; 3] = [0x4a, 0x7c, 0xff]; /// The reference-point target (circle + crosshair) drawn at `p`, `r` in document units. pub(crate) fn target_overlays(p: Point, r: f64) -> Vec { - let circle = drawcraft_geom::kurbo::Circle::new(p, r * 0.6).to_path(0.05); + let circle = vectorcraft_geom::kurbo::Circle::new(p, r * 0.6).to_path(0.05); vec![ Overlay::Path { path: circle, color: CYAN, width: 1.0, dashed: false }, Overlay::Line { a: Point::new(p.x - r, p.y), b: Point::new(p.x + r, p.y), color: CYAN, dashed: false }, diff --git a/crates/tools/src/xform/artboard.rs b/crates/tools/src/xform/artboard.rs index ba36d1760..83b68fed8 100644 --- a/crates/tools/src/xform/artboard.rs +++ b/crates/tools/src/xform/artboard.rs @@ -5,8 +5,8 @@ //! (Shift proportional, Alt from centre), drag on the pasteboard draws a new artboard, Delete removes //! the active one and Escape returns to the Selection tool. -use drawcraft_geom::{Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_geom::{Point, Rect, Vec2}; use super::{BLUE, polygon, rect_corners}; use crate::bbox::{Handle, hit_handle, move_delta, scale_for_drag}; @@ -232,7 +232,7 @@ impl Tool for ArtboardTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) diff --git a/crates/tools/src/xform/eyedropper.rs b/crates/tools/src/xform/eyedropper.rs index f73e6273d..8c4bca488 100644 --- a/crates/tools/src/xform/eyedropper.rs +++ b/crates/tools/src/xform/eyedropper.rs @@ -4,10 +4,10 @@ //! paint defaults (`appearance.copyFrom`). Shift-click: sample only the colour under the cursor into //! the active fill/stroke (`paint.sampleColor`). -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::hit::{HitKind, hit_test}; -use drawcraft_geom::Point; use serde_json::json; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::hit::{HitKind, hit_test}; +use vectorcraft_geom::Point; use super::paint_owner; use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext}; @@ -15,14 +15,14 @@ use crate::{Action, Cursor, Mods, PointerEvent, PointerKind, Tool, ToolContext}; pub struct EyedropperTool; /// The colour of `paint` at `p` (gradients are sampled along their vector). -pub fn paint_color_at(paint: &Paint, bounds: Option, p: Point) -> Option { +pub fn paint_color_at(paint: &Paint, bounds: Option, p: Point) -> Option { match paint { Paint::Solid { color, .. } => Some(*color), Paint::Gradient(g) => { let geom = g.resolve(bounds?); let v = geom.end - geom.start; let t = match g.gradient.kind { - drawcraft_color::GradientKind::Radial => (p - geom.start).hypot() / v.hypot().max(1e-9), + vectorcraft_color::GradientKind::Radial => (p - geom.start).hypot() / v.hypot().max(1e-9), _ => (p - geom.start).dot(v) / v.hypot2().max(1e-9), }; Some(g.gradient.sample(t.clamp(0.0, 1.0) as f32).0) @@ -40,13 +40,13 @@ impl Tool for EyedropperTool { if ev.kind != PointerKind::Down { return vec![]; } - let opts = drawcraft_doc::hit::HitOptions { path_only: false, ..cx.hit_options() }; + let opts = vectorcraft_doc::hit::HitOptions { path_only: false, ..cx.hit_options() }; let Some(h) = hit_test(cx.doc, ev.pos, opts) else { return vec![] }; let src = paint_owner(cx.doc, h.leaf); let Some(n) = cx.doc.node(src) else { return vec![] }; if ev.mods.shift { let (fill, stroke) = match &n.kind { - drawcraft_doc::NodeKind::Text(t) => { + vectorcraft_doc::NodeKind::Text(t) => { let st = t.runs.first().map(|r| r.style.clone()); (st.as_ref().map(|s| s.fill.clone()).unwrap_or_default(), st.map(|s| s.stroke).unwrap_or_default()) } @@ -70,7 +70,7 @@ impl Tool for EyedropperTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn click_copies_appearance_shift_samples_color() { @@ -92,9 +92,9 @@ mod tests { #[test] fn gradient_sampled_along_vector() { - let g = drawcraft_color::GradientPaint::new(drawcraft_color::Gradient::default()); + let g = vectorcraft_color::GradientPaint::new(vectorcraft_color::Gradient::default()); let paint = Paint::Gradient(Box::new(g)); - let r = drawcraft_geom::Rect::new(0.0, 0.0, 100.0, 10.0); + let r = vectorcraft_geom::Rect::new(0.0, 0.0, 100.0, 10.0); assert_eq!(paint_color_at(&paint, Some(r), Point::new(0.0, 5.0)).unwrap().to_hex(), "#ffffff"); assert_eq!(paint_color_at(&paint, Some(r), Point::new(100.0, 5.0)).unwrap().to_hex(), "#000000"); } diff --git a/crates/tools/src/xform/free.rs b/crates/tools/src/xform/free.rs index 2b794cffd..6235df008 100644 --- a/crates/tools/src/xform/free.rs +++ b/crates/tools/src/xform/free.rs @@ -6,8 +6,8 @@ //! Cmd / Cmd+Alt+Shift while dragging a corner like classic Illustrator) move the corners of the //! box and preview `object.distort`. -use drawcraft_geom::{Affine, Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use super::{BLUE, polygon, rect_corners}; use crate::bbox::{Handle, hit_handle, in_rotate_zone, move_delta, rotate_for_drag, scale_for_drag}; @@ -308,7 +308,7 @@ impl Tool for FreeTransformTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) diff --git a/crates/tools/src/xform/gradient.rs b/crates/tools/src/xform/gradient.rs index 87f07d139..01df26a6d 100644 --- a/crates/tools/src/xform/gradient.rs +++ b/crates/tools/src/xform/gradient.rs @@ -5,10 +5,10 @@ //! the object under the pointer. The overlay is the gradient annotator: a bar from a round start //! handle to a square end handle, with a tick for each colour stop. -use drawcraft_color::Paint; -use drawcraft_doc::hit::hit_test; -use drawcraft_geom::{Point, Vec2}; use serde_json::{Value, json}; +use vectorcraft_color::Paint; +use vectorcraft_doc::hit::hit_test; +use vectorcraft_geom::{Point, Vec2}; use super::paint_owner; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -81,7 +81,7 @@ impl Tool for GradientTool { } out.push(Action::Begin("Gradient".into())); } - let end = if ev.mods.shift { start + drawcraft_geom::constrain_angle(p - start, 45.0) } else { p }; + let end = if ev.mods.shift { start + vectorcraft_geom::constrain_angle(p - start, 45.0) } else { p }; self.drag = Some((start, end, true)); out.push(Action::Preview("paint.setGradientGeom".into(), json!({ "start": [start.x, start.y], "end": [end.x, end.y] }))); out @@ -123,7 +123,7 @@ impl Tool for GradientTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn drag_sets_gradient_vector_and_constrains() { diff --git a/crates/tools/src/xform/measure.rs b/crates/tools/src/xform/measure.rs index 97d90baa8..b55aa5b34 100644 --- a/crates/tools/src/xform/measure.rs +++ b/crates/tools/src/xform/measure.rs @@ -2,8 +2,8 @@ //! 45°). The result stays on screen (and in the tool options, for agents) until the next drag; the //! document is never changed. -use drawcraft_geom::{Point, Vec2}; use serde_json::{Value, json}; +use vectorcraft_geom::{Point, Vec2}; use super::{CYAN, fmt_pt}; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext}; @@ -39,7 +39,7 @@ impl Tool for MeasureTool { PointerKind::Drag | PointerKind::Up if self.dragging => { if let Some((a, _)) = self.line { let b = if ev.mods.shift { - a + drawcraft_geom::constrain_angle(ev.pos - a, 45.0) + a + vectorcraft_geom::constrain_angle(ev.pos - a, 45.0) } else { crate::guides::snap_draw(cx, ev.pos, &[]).0 }; @@ -86,7 +86,7 @@ impl Tool for MeasureTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn drag_reports_distance_and_angle() { diff --git a/crates/tools/src/xform/transform.rs b/crates/tools/src/xform/transform.rs index 7403bc11b..b0c7f6554 100644 --- a/crates/tools/src/xform/transform.rs +++ b/crates/tools/src/xform/transform.rs @@ -5,9 +5,9 @@ //! at release makes a copy). Alt-click sets the point and opens the tool's dialog; double-click //! or Return opens the dialog too. -use drawcraft_doc::NodeId; -use drawcraft_geom::{Affine, Point, Rect, Vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::NodeId; +use vectorcraft_geom::{Affine, Point, Rect, Vec2}; use super::target_overlays; use crate::bbox::rotate_for_drag; @@ -104,10 +104,10 @@ impl TransformTool { TransformKind::Reflect => { let mut v = p - o; if shift { - v = drawcraft_geom::constrain_angle(v, 45.0); + v = vectorcraft_geom::constrain_angle(v, 45.0); } let theta = if v.hypot() < 1e-9 { std::f64::consts::FRAC_PI_2 } else { v.y.atan2(v.x) }; - (reflect_matrix(o, theta), format!("{:.1}°", drawcraft_geom::normalize_deg(-theta.to_degrees()))) + (reflect_matrix(o, theta), format!("{:.1}°", vectorcraft_geom::normalize_deg(-theta.to_degrees()))) } TransformKind::Scale => { let d0 = start - o; @@ -274,7 +274,7 @@ impl Tool for TransformTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; fn ev(kind: PointerKind, x: f64, y: f64) -> PointerEvent { PointerEvent::new(kind, x, y) diff --git a/crates/tools/src/xform/wand.rs b/crates/tools/src/xform/wand.rs index b1906ab94..5375f52d4 100644 --- a/crates/tools/src/xform/wand.rs +++ b/crates/tools/src/xform/wand.rs @@ -7,10 +7,10 @@ use std::collections::BTreeSet; -use drawcraft_doc::hit::hit_test; -use drawcraft_doc::{AnchorRef, NodeId, NodeKind}; -use drawcraft_geom::Point; use serde_json::{Value, json}; +use vectorcraft_doc::hit::hit_test; +use vectorcraft_doc::{AnchorRef, NodeId, NodeKind}; +use vectorcraft_geom::Point; use super::paint_owner; use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey}; @@ -163,7 +163,7 @@ impl Tool for LassoTool { mod tests { use super::*; use crate::testutil::*; - use drawcraft_doc::Selection; + use vectorcraft_doc::Selection; #[test] fn magic_wand_emits_the_engine_command() { diff --git a/crates/trace/Cargo.toml b/crates/trace/Cargo.toml index fe12a80b7..51a7e5d2e 100644 --- a/crates/trace/Cargo.toml +++ b/crates/trace/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "drawcraft-trace" +name = "vectorcraft-trace" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft Image Trace: raster to vector tracing (quantize, boundary follow, polygon, Bezier fit)" +description = "VectorCraft Image Trace: raster to vector tracing (quantize, boundary follow, polygon, Bezier fit)" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-pathops = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-pathops = { workspace = true } kurbo = { workspace = true } image = { workspace = true } serde = { workspace = true } diff --git a/crates/trace/src/fit.rs b/crates/trace/src/fit.rs index 625a8353f..22c54793f 100644 --- a/crates/trace/src/fit.rs +++ b/crates/trace/src/fit.rs @@ -1,7 +1,7 @@ //! Loop → polygon → Bézier curves. -use drawcraft_geom::{AnchorKind, CubicBez, ParamCurve, PathData, Point, SubPath}; -use drawcraft_pathops::{SimplifyOptions, simplify_with}; +use vectorcraft_geom::{AnchorKind, CubicBez, ParamCurve, PathData, Point, SubPath}; +use vectorcraft_pathops::{SimplifyOptions, simplify_with}; use crate::contour::Loop; diff --git a/crates/trace/src/lib.rs b/crates/trace/src/lib.rs index 4fc30d87f..c28c1db90 100644 --- a/crates/trace/src/lib.rs +++ b/crates/trace/src/lib.rs @@ -1,4 +1,4 @@ -//! DrawCraft Image Trace: raster → vector. +//! VectorCraft Image Trace: raster → vector. //! //! A clean-room tracer built from the published ideas behind bitmap tracers (the potrace paper //! and classic colour quantisation), not from any existing implementation: @@ -15,7 +15,7 @@ //! 4. **Polygon**: each loop is reduced to a polygon whose vertices stay within a fidelity //! dependent distance of the pixel boundary (Douglas–Peucker on the crack vertices, which //! removes the staircase). -//! 5. **Curves**: the polygon is fitted with cubic Béziers (`drawcraft_pathops::simplify_with`, +//! 5. **Curves**: the polygon is fitted with cubic Béziers (`vectorcraft_pathops::simplify_with`, //! least squares with corner detection); optionally nearly-straight curves snap to lines. //! //! Colour layers are traced either *abutting* (each colour's own area; shapes share edges) or @@ -26,8 +26,8 @@ mod contour; mod fit; mod quantize; -use drawcraft_geom::PathData; use serde::{Deserialize, Serialize}; +use vectorcraft_geom::PathData; pub use contour::{Component, Loop, trace_mask}; pub use quantize::{Quantized, TRANSPARENT, denoise, quantize}; diff --git a/crates/trace/src/tests.rs b/crates/trace/src/tests.rs index 61be59ff5..5ce4a91af 100644 --- a/crates/trace/src/tests.rs +++ b/crates/trace/src/tests.rs @@ -15,7 +15,7 @@ fn disc(size: u32, cx: f64, cy: f64, r: f64) -> Raster { } /// Net filled area (outer contours positive, holes negative). -fn net_area(p: &drawcraft_geom::PathData) -> f64 { +fn net_area(p: &vectorcraft_geom::PathData) -> f64 { p.subpaths.iter().map(|s| s.area()).sum() } diff --git a/crates/ui-egui/Cargo.toml b/crates/ui-egui/Cargo.toml index f764c135d..7de7893d8 100644 --- a/crates/ui-egui/Cargo.toml +++ b/crates/ui-egui/Cargo.toml @@ -1,25 +1,25 @@ [package] -name = "drawcraft-ui-egui" +name = "vectorcraft-ui-egui" version.workspace = true edition.workspace = true license.workspace = true -description = "DrawCraft egui frontend (Illustrator-style UI); swappable — nothing below depends on it" +description = "VectorCraft egui frontend (Illustrator-style UI); swappable — nothing below depends on it" [lints] workspace = true [dependencies] -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-render = { workspace = true } -drawcraft-text = { workspace = true } -drawcraft-tools = { workspace = true } -drawcraft-engine = { workspace = true } -drawcraft-svg = { workspace = true } -drawcraft-effects = { workspace = true } -drawcraft-pdf = { workspace = true } -drawcraft-format = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-render = { workspace = true } +vectorcraft-text = { workspace = true } +vectorcraft-tools = { workspace = true } +vectorcraft-engine = { workspace = true } +vectorcraft-svg = { workspace = true } +vectorcraft-effects = { workspace = true } +vectorcraft-pdf = { workspace = true } +vectorcraft-format = { workspace = true } egui = { workspace = true } egui_extras = { workspace = true } serde = { workspace = true } diff --git a/crates/ui-egui/src/brand.rs b/crates/ui-egui/src/brand.rs index a5fd79474..800e2dffb 100644 --- a/crates/ui-egui/src/brand.rs +++ b/crates/ui-egui/src/brand.rs @@ -1,4 +1,4 @@ -//! The DrawCraft brand mark (original artwork, drawn in code): a warm diagonal-gradient rounded +//! The VectorCraft brand mark (original artwork, drawn in code): a warm diagonal-gradient rounded //! square with a white pen nib — the same design as the app icon rendered by `cargo xtask bundle`. use egui::epaint::{Mesh, Vertex, WHITE_UV}; diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 6ad33b805..80956d45e 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1,15 +1,15 @@ //! The document canvas: rendering, rulers, navigation, pointer routing to tools and on-canvas //! selection visuals (bounding box, anchors, handles, smart-guide style labels). -use drawcraft_doc::{Node, NodeKind}; -use drawcraft_geom::{Affine, BezPath, PathEl, Point, Rect}; -use drawcraft_tools::{Cursor, Mods, Overlay, PointerEvent, PointerKind}; use egui::{Color32, CornerRadius, Pos2, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::json; +use vectorcraft_doc::{Node, NodeKind}; +use vectorcraft_geom::{Affine, BezPath, PathEl, Point, Rect}; +use vectorcraft_tools::{Cursor, Mods, Overlay, PointerEvent, PointerKind}; use crate::state::View; use crate::theme::{self, Tokens}; -use crate::{CacheKey, DrawcraftApp, now_ms, widgets}; +use crate::{CacheKey, VectorcraftApp, now_ms, widgets}; const RULER: f32 = 16.0; @@ -45,10 +45,10 @@ impl Xf { [Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1)].iter().map(|p| self.to_screen(*p)).collect() } /// Convert a screen-space delta to a document delta. - pub fn delta_to_doc(&self, d: egui::Vec2) -> drawcraft_geom::Vec2 { + pub fn delta_to_doc(&self, d: egui::Vec2) -> vectorcraft_geom::Vec2 { let (sn, cs) = self.rot.sin_cos(); let (dx, dy) = (d.x as f64, d.y as f64); - drawcraft_geom::Vec2::new((dx * cs + dy * sn) / self.zoom, (-dx * sn + dy * cs) / self.zoom) + vectorcraft_geom::Vec2::new((dx * cs + dy * sn) / self.zoom, (-dx * sn + dy * cs) / self.zoom) } pub fn rect_to_screen(&self, r: Rect) -> egui::Rect { egui::Rect::from_two_pos(self.to_screen(Point::new(r.x0, r.y0)), self.to_screen(Point::new(r.x1, r.y1))) @@ -90,7 +90,7 @@ pub fn mods(m: egui::Modifiers, space: bool) -> Mods { } /// Fit the view (View → Fit Artboard / Fit All / Actual Size). -pub fn fit(app: &mut DrawcraftApp, how: &str) { +pub fn fit(app: &mut VectorcraftApp, how: &str) { let Some(rect) = app.canvas_rect else { if let Some(v) = app.view_mut() { v.fitted = false; @@ -117,7 +117,7 @@ pub fn fit(app: &mut DrawcraftApp, how: &str) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let full = ui.available_rect_before_wrap(); if app.session.active().is_none() { @@ -181,7 +181,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }; if !app.canvas.worker_started { app.canvas.worker_started = true; - if std::env::var_os("DRAWCRAFT_SYNC_RENDER").is_none() { + if std::env::var_os("VECTORCRAFT_SYNC_RENDER").is_none() { app.canvas.worker = crate::render_worker::Worker::spawn(ui.ctx().clone()); } } @@ -199,10 +199,10 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { * Affine::rotate(v.rotation.to_radians()) * Affine::scale(v.zoom * ppp as f64) * Affine::translate(-v.center.to_vec2()); - let opts = drawcraft_render::RenderOptions { outline: app.ui.view.outline, background: None, artboards: false, ..Default::default() }; - let opts = drawcraft_render::RenderOptions { - proof: drawcraft_render::proof::active_proof(), - overprint_preview: drawcraft_render::proof::overprint_preview_on(), + let opts = vectorcraft_render::RenderOptions { outline: app.ui.view.outline, background: None, artboards: false, ..Default::default() }; + let opts = vectorcraft_render::RenderOptions { + proof: vectorcraft_render::proof::active_proof(), + overprint_preview: vectorcraft_render::proof::overprint_preview_on(), ..opts }; // Light documents render synchronously (no lag vs overlays); heavy ones go to the worker. @@ -288,7 +288,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { let c = app.session.cursor(p, mods(m, space), view_info); let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::new("tool-cursor"))); // Caps Lock gives precise (crosshair) cursors, like Illustrator; env opt-out for system cursors. - let custom = std::env::var_os("DRAWCRAFT_SYSTEM_CURSORS").is_none(); + let custom = std::env::var_os("VECTORCRAFT_SYSTEM_CURSORS").is_none(); match ui.input(|i| i.pointer.hover_pos()) { Some(hp) if custom && crate::cursors::paint(&painter, c, hp) => egui::CursorIcon::None, _ => cursor_icon(c), @@ -323,7 +323,7 @@ fn cursor_icon(c: Cursor) -> egui::CursorIcon { } } -fn handle_input(app: &mut DrawcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect) { +fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect) { let (pointer, m, space, scroll, zoom_delta) = ui.input(|i| (i.pointer.clone(), i.modifiers, i.key_down(egui::Key::Space), i.smooth_scroll_delta, i.zoom_delta())); let v = *app.view().unwrap_or(&View::default()); @@ -400,7 +400,7 @@ fn handle_input(app: &mut DrawcraftApp, ui: &Ui, resp: &egui::Response, rect: eg deg = (deg / 15.0).round() * 15.0; } if let Some(vm) = app.view_mut() { - vm.rotation = drawcraft_geom::normalize_deg(deg); + vm.rotation = vectorcraft_geom::normalize_deg(deg); } } Drag::ZoomBox { .. } => {} @@ -460,13 +460,13 @@ fn handle_input(app: &mut DrawcraftApp, ui: &Ui, resp: &egui::Response, rect: eg } /// Send a pointer event to the active tool and act on UI requests (dialogs, tool switches). -pub fn dispatch(app: &mut DrawcraftApp, ev: &PointerEvent, view: drawcraft_engine::ViewInfo) { +pub fn dispatch(app: &mut VectorcraftApp, ev: &PointerEvent, view: vectorcraft_engine::ViewInfo) { match app.session.pointer(ev, view) { Ok(reqs) => { for r in reqs { match r { - drawcraft_engine::UiRequest::Dialog(kind, p) => crate::dialogs::open_tool_dialog(app, &kind, p), - drawcraft_engine::UiRequest::SwitchTool(t) => app.select_tool(&t), + vectorcraft_engine::UiRequest::Dialog(kind, p) => crate::dialogs::open_tool_dialog(app, &kind, p), + vectorcraft_engine::UiRequest::SwitchTool(t) => app.select_tool(&t), } } } @@ -636,7 +636,7 @@ fn node_outline(n: &Node) -> BezPath { NodeKind::Path { path, .. } => bp.extend(path.to_bezpath()), NodeKind::Text(_) | NodeKind::Image(_) | NodeKind::SymbolInstance { .. } => { if let Some(b) = c.geometric_bounds() { - bp.extend(drawcraft_geom::shapes::rectangle(b).to_bezpath()); + bp.extend(vectorcraft_geom::shapes::rectangle(b).to_bezpath()); } } _ => {} @@ -644,14 +644,14 @@ fn node_outline(n: &Node) -> BezPath { bp } -fn hover_highlight(app: &DrawcraftApp, p: &egui::Painter, xf: &Xf) { +fn hover_highlight(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) { let Some(h) = app.hover_doc else { return }; if app.session.tool_busy() || !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection") { return; } let Some(st) = app.session.active() else { return }; - let opt = drawcraft_doc::hit::HitOptions { tol: 3.0 / xf.zoom, outline: app.ui.view.outline, path_only: false }; - let Some(hit) = drawcraft_doc::hit::hit_test(&st.doc, h, opt) else { return }; + let opt = vectorcraft_doc::hit::HitOptions { tol: 3.0 / xf.zoom, outline: app.ui.view.outline, path_only: false }; + let Some(hit) = vectorcraft_doc::hit::hit_test(&st.doc, h, opt) else { return }; let id = if app.session.tool_id() == "selection" { hit.top_object(st.isolation) } else { hit.leaf }; if st.selection.contains(id) { return; @@ -677,7 +677,7 @@ fn anchor_square(p: &egui::Painter, c: Pos2, color: Color32, filled: bool, size: } } -fn selection_overlay(app: &DrawcraftApp, p: &egui::Painter, xf: &Xf) { +fn selection_overlay(app: &VectorcraftApp, p: &egui::Painter, xf: &Xf) { let Some(st) = app.session.active() else { return }; let tool = app.session.tool_id(); let direct = matches!(tool, "directSelection" | "pen" | "addAnchor" | "deleteAnchor" | "anchorPoint" | "curvature"); @@ -727,7 +727,7 @@ fn selection_overlay(app: &DrawcraftApp, p: &egui::Painter, xf: &Xf) { // Live corner widgets (Direct Selection on a single live rectangle). if tool == "directSelection" && st.selection.len() == 1 - && let Some(NodeKind::Path { live: Some(drawcraft_doc::LiveShape::Rectangle { w, h, radii, xf: lxf }), .. }) = + && let Some(NodeKind::Path { live: Some(vectorcraft_doc::LiveShape::Rectangle { w, h, radii, xf: lxf }), .. }) = st.doc.node(st.selection.objects[0]).map(|n| &n.kind) { let color = c32(st.doc.layer_color(st.selection.objects[0])); @@ -743,7 +743,7 @@ fn selection_overlay(app: &DrawcraftApp, p: &egui::Painter, xf: &Xf) { let Some(b) = st.doc.bounds_of(&st.selection.objects, false) else { return }; let color = c32(st.doc.layer_color(st.selection.objects[0])); p.add(Shape::closed_line(xf.quad(b), Stroke::new(1.0, color))); - for h in drawcraft_tools::bbox::Handle::ALL { + for h in vectorcraft_tools::bbox::Handle::ALL { let c = xf.to_screen(h.pos(b)); let hr = egui::Rect::from_center_size(c, vec2(6.0, 6.0)); p.rect_filled(hr, 0.0, Color32::WHITE); @@ -805,13 +805,13 @@ fn draw_overlays(p: &egui::Painter, xf: &Xf, overlays: &[Overlay], t: &Tokens) { } /// The Home screen shown when no document is open. -fn home(app: &mut DrawcraftApp, ui: &mut Ui, rect: egui::Rect) { +fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) { let t = Tokens::get(ui.ctx()); ui.painter().rect_filled(rect, 0.0, t.panel_darker); let inner = rect.shrink2(vec2((rect.width() - 820.0).max(40.0) / 2.0, 60.0)); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(inner).layout(egui::Layout::top_down(egui::Align::Min))); let ui = &mut child; - ui.label(egui::RichText::new("Welcome to DrawCraft").font(theme::semibold(26.0)).color(t.text)); + ui.label(egui::RichText::new("Welcome to VectorCraft").font(theme::semibold(26.0)).color(t.text)); ui.add_space(4.0); ui.label(egui::RichText::new("Vector illustration — fast, open, scriptable.").size(14.0).color(t.text_dim)); ui.add_space(22.0); @@ -854,10 +854,10 @@ fn home(app: &mut DrawcraftApp, ui: &mut Ui, rect: egui::Rect) { } fn kurbo_flatten(p: &BezPath, tol: f64, f: &mut impl FnMut(PathEl)) { - drawcraft_geom::kurbo::flatten(p.elements().iter().copied(), tol, f); + vectorcraft_geom::kurbo::flatten(p.elements().iter().copied(), tol, f); } -fn upload(app: &mut DrawcraftApp, ctx: &egui::Context, img: &drawcraft_render::Rendered) { +fn upload(app: &mut VectorcraftApp, ctx: &egui::Context, img: &vectorcraft_render::Rendered) { let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); match &mut app.canvas.texture { Some(tex) => tex.set(color, egui::TextureOptions::LINEAR), @@ -866,7 +866,7 @@ fn upload(app: &mut DrawcraftApp, ctx: &egui::Context, img: &drawcraft_render::R } /// The Contextual Task Bar: a floating pill under the selection with the most likely next actions. -fn task_bar(app: &mut DrawcraftApp, ui: &mut Ui, xf: &Xf) { +fn task_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; if st.selection.is_empty() || !matches!(app.session.tool_id(), "selection" | "directSelection" | "groupSelection") { diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index dfcf3352f..7423730b1 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -1,16 +1,16 @@ //! Window chrome: application bar (menus), Control bar, document tabs, status bar. -use drawcraft_color::Paint; -use drawcraft_doc::NodeKind; use egui::{CornerRadius, Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::json; +use vectorcraft_color::Paint; +use vectorcraft_doc::NodeKind; use crate::state::{ZOOM_STOPS, zoom_label}; use crate::theme::{self, Tokens}; use crate::widgets::{self, paint_chip}; -use crate::{DrawcraftApp, icons, menus}; +use crate::{VectorcraftApp, icons, menus}; -pub fn app_bar(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let left = if app.integrated_titlebar { 78 } else { 8 }; egui::Panel::top("app_bar").exact_size(44.0).frame(egui::Frame::NONE.fill(t.app_bar).inner_margin(egui::Margin { left, right: 14, top: 0, bottom: 0 }).stroke(Stroke::new(1.0, t.border))).show(ui, |ui| { @@ -25,7 +25,7 @@ pub fn app_bar(app: &mut DrawcraftApp, ui: &mut Ui) { ui.add_space(2.0); if app.native_menu { let full = ui.max_rect(); - ui.painter().text(full.center(), egui::Align2::CENTER_CENTER, "DrawCraft", egui::FontId::proportional(13.5), t.text); + ui.painter().text(full.center(), egui::Align2::CENTER_CENTER, "VectorCraft", egui::FontId::proportional(13.5), t.text); } else { menus::menu_bar(app, ui); } @@ -70,7 +70,7 @@ fn chip_button(ui: &mut Ui, paint: &Paint, stroke_style: bool, tip: &str) -> egu } /// The Control bar (Window → Control), context-sensitive like Illustrator's. -pub fn control_bar(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::top("control_bar") .exact_size(34.0) @@ -121,7 +121,7 @@ pub fn control_bar(app: &mut DrawcraftApp, ui: &mut Ui) { if ui.link(egui::RichText::new("Stroke:").size(12.0).color(t.text).underline()).clicked() { app.ui.open_panel = Some("stroke".into()); } - if let Some(w) = widgets::num_field(ui, "cb-stroke", Some(weight), drawcraft_doc::Unit::Points, 64.0) { + if let Some(w) = widgets::num_field(ui, "cb-stroke", Some(weight), vectorcraft_doc::Unit::Points, 64.0) { app.run("stroke.set", json!({"weight": w})).ok(); } ui.add_space(4.0); @@ -179,7 +179,7 @@ pub fn control_bar(app: &mut DrawcraftApp, ui: &mut Ui) { } /// Document tab strip: "Name* @ 66.67% (RGB/Preview)". -pub fn doc_tabs(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), 35.0), Sense::hover()); ui.painter().rect_filled(strip, 0.0, t.tab_strip); @@ -190,7 +190,7 @@ pub fn doc_tabs(app: &mut DrawcraftApp, ui: &mut Ui) { let active = app.session.active_index(); for (i, d) in app.session.documents().iter().enumerate() { let zoom = app.views.get(i).map(|v| v.zoom).unwrap_or(1.0); - let mode = if d.doc.color_mode == drawcraft_doc::ColorMode::Cmyk { "CMYK" } else { "RGB" }; + let mode = if d.doc.color_mode == vectorcraft_doc::ColorMode::Cmyk { "CMYK" } else { "RGB" }; let vm = if app.ui.view.outline { "Outline" } else { "Preview" }; let title = format!("{}{} @ {} ({mode}/{vm})", d.title(), if d.is_dirty() { "*" } else { "" }, zoom_label(zoom).replace('%', " %")); let is_active = Some(i) == active; @@ -276,7 +276,7 @@ pub fn doc_tabs(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn status_bar(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::bottom("status_bar") .exact_size(24.0) @@ -324,7 +324,7 @@ pub fn status_bar(app: &mut DrawcraftApp, ui: &mut Ui) { widgets::icon_button(ui, icon, "", false, 18.0); } ui.separator(); - let tool = drawcraft_tools::tool_info(app.session.tool_id()).map(|t| t.label.trim_end_matches(" Tool")).unwrap_or(""); + let tool = vectorcraft_tools::tool_info(app.session.tool_id()).map(|t| t.label.trim_end_matches(" Tool")).unwrap_or(""); ui.label(egui::RichText::new(tool).size(11.5).color(t.text_dim)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { ui.label( @@ -425,7 +425,7 @@ fn hint_for(tool: &str) -> &'static [(&'static str, bool)] { } } -pub fn hint_bar(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn hint_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); egui::Panel::bottom("hint_bar") .exact_size(28.0) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index b92789209..ee7bdedb7 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -19,10 +19,10 @@ use std::sync::mpsc::Sender; -use drawcraft_tools::{Mods, PointerEvent, PointerKind}; use serde_json::{Value, json}; +use vectorcraft_tools::{Mods, PointerEvent, PointerKind}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::canvas::Xf; pub type ControlResponse = Value; @@ -58,7 +58,7 @@ fn wrap(r: Result) -> Outcome { } } -pub fn all_commands(app: &DrawcraftApp) -> Value { +pub fn all_commands(app: &VectorcraftApp) -> Value { let mut v: Vec = app.session.commands().into_iter().map(|c| serde_json::to_value(c).unwrap_or_default()).collect(); for (id, label, sc, params) in crate::menus::UI_COMMANDS { v.push(json!({"id": id, "label": label, "shortcut": sc, "params": params, "enabled": crate::menus::enabled(app, id), "ui": true})); @@ -66,7 +66,7 @@ pub fn all_commands(app: &DrawcraftApp) -> Value { Value::Array(v) } -pub fn inspect(app: &DrawcraftApp, ctx: &egui::Context) -> Value { +pub fn inspect(app: &VectorcraftApp, ctx: &egui::Context) -> Value { let r = ctx.content_rect(); json!({ "tool": app.session.tool_id(), @@ -97,7 +97,7 @@ fn key_from(name: &str) -> Option { }) } -pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { +pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRequest) -> Outcome { let p = &req.params; let s = |k: &str| p.get(k).and_then(Value::as_str); match req.method.as_str() { @@ -112,7 +112,7 @@ pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context, req: &ControlRequest) "ui.inspect" => ok(inspect(app, ctx)), "ui.menu.list" => ok(serde_json::to_value(crate::menus::menu_entries(app)).unwrap_or_default()), "ui.tool.select" => wrap(app.run("tool.select", json!({"tool": s("tool").unwrap_or("")}))), - "ui.tool.list" => ok(serde_json::to_value(drawcraft_tools::TOOL_GROUPS).unwrap_or_default()), + "ui.tool.list" => ok(serde_json::to_value(vectorcraft_tools::TOOL_GROUPS).unwrap_or_default()), "ui.pointer" => { let Some(events) = p.get("events").and_then(Value::as_array) else { return err("missing `events`") }; let base_mods: Mods = p.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or_default(); @@ -135,7 +135,7 @@ pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context, req: &ControlRequest) None => return err("no canvas yet"), } } else { - drawcraft_geom::Point::new(x, y) + vectorcraft_geom::Point::new(x, y) }; let mods = e.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or(base_mods); crate::canvas::dispatch(app, &PointerEvent { kind, pos, mods, pressure: 1.0 }, view); @@ -287,7 +287,7 @@ pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context, req: &ControlRequest) Some(w) => wrap(w(path, &png).map(|_| json!({"path": path, "width": img.width, "height": img.height}))), None => err("no writer"), }, - None => ok(json!({"width": img.width, "height": img.height, "pngBase64": drawcraft_format::base64_encode(&png)})), + None => ok(json!({"width": img.width, "height": img.height, "pngBase64": vectorcraft_format::base64_encode(&png)})), } } "app.open" => wrap(app.run("file.open", json!({"path": s("path")}))), @@ -307,13 +307,13 @@ pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context, req: &ControlRequest) } } -pub fn save_screenshot(app: &mut DrawcraftApp, image: &egui::ColorImage, path: Option<&str>) -> Value { +pub fn save_screenshot(app: &mut VectorcraftApp, image: &egui::ColorImage, path: Option<&str>) -> Value { let [w, h] = image.size; let Some(path) = path else { return json!({"ok": true, "result": {"width": w, "height": h}}); }; let rgba: Vec = image.pixels.iter().flat_map(|c| c.to_array()).collect(); - let img = drawcraft_render::Rendered { width: w as u32, height: h as u32, pixels: rgba }; + let img = vectorcraft_render::Rendered { width: w as u32, height: h as u32, pixels: rgba }; let png = img.to_png(); match app.services.write.as_mut() { Some(wr) => match wr(path, &png) { diff --git a/crates/ui-egui/src/cursors.rs b/crates/ui-egui/src/cursors.rs index 2f9db8d64..e8f20a81f 100644 --- a/crates/ui-egui/src/cursors.rs +++ b/crates/ui-egui/src/cursors.rs @@ -2,8 +2,8 @@ //! halo, hotspot at `p`, Illustrator's visual grammar: solid arrow (Selection), hollow arrow (Direct //! Selection), pen nib with state badges, crosshair for drawing tools, curved arrows for rotate. -use drawcraft_tools::Cursor; use egui::{Color32, Painter, Pos2, Shape, Stroke, pos2, vec2}; +use vectorcraft_tools::Cursor; const INK: Color32 = Color32::BLACK; const HALO: Color32 = Color32::WHITE; diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 0fac13bde..09089d0a5 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -5,10 +5,10 @@ use serde_json::{Value, json}; use crate::state::Dialog; use crate::theme::{self, Tokens}; -use crate::{DrawcraftApp, widgets}; +use crate::{VectorcraftApp, widgets}; /// A click with a shape tool opens its size dialog. -pub fn open_tool_dialog(app: &mut DrawcraftApp, kind: &str, p: Value) { +pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) { let x = p.get("x").and_then(Value::as_f64).unwrap_or(0.0); let y = p.get("y").and_then(Value::as_f64).unwrap_or(0.0); let d = match kind { @@ -120,7 +120,7 @@ fn title(kind: &str) -> &'static str { } /// Apply the open dialog (OK). -pub fn confirm(app: &mut DrawcraftApp) -> Result { +pub fn confirm(app: &mut VectorcraftApp) -> Result { let Some(d) = app.ui.dialog.clone() else { return Err("no dialog open".into()) }; match d.kind.as_str() { "preferences" => return crate::prefs_dialog::confirm(app), @@ -243,18 +243,18 @@ fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) { } } -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if app.ui.about { let mut open = true; - egui::Window::new("About DrawCraft") + egui::Window::new("About VectorCraft") .collapsible(false) .resizable(false) .open(&mut open) .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0]) .show(ctx, |ui| { - ui.label(egui::RichText::new("DrawCraft").font(theme::semibold(22.0))); + ui.label(egui::RichText::new("VectorCraft").font(theme::semibold(22.0))); ui.label(format!("Version {} — open-source vector illustration in pure Rust.", env!("CARGO_PKG_VERSION"))); - ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + DrawCraft."); + ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3, Inter, JetBrains Mono (OFL). Icons: Lucide (ISC) + VectorCraft."); }); app.ui.about = open; } @@ -313,7 +313,7 @@ pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { } "newDocument" => new_document(ui, &mut d), "allTools" => { - for g in drawcraft_tools::TOOL_GROUPS { + for g in vectorcraft_tools::TOOL_GROUPS { ui.horizontal_wrapped(|ui| { for tool in g.iter() { if ui.button(tool.label.trim_end_matches(" Tool")).clicked() { @@ -440,7 +440,7 @@ fn new_document(ui: &mut egui::Ui, d: &mut Dialog) { } /// Export for Screens: artboard picker (thumbnails + checkboxes), format/scale rows, destination. -fn export_for_screens_ui(app: &mut DrawcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { +fn export_for_screens_ui(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) { let t = Tokens::get(ui.ctx()); let names: Vec = app.session.active().map(|s| s.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); let mut boards: Vec = @@ -547,7 +547,7 @@ fn export_for_screens_ui(app: &mut DrawcraftApp, ui: &mut egui::Ui, d: &mut Dial } /// A small cached rendering of artboard `i` (keyed by document revision). -fn artboard_thumb(app: &mut DrawcraftApp, ctx: &egui::Context, i: usize) -> Option { +fn artboard_thumb(app: &mut VectorcraftApp, ctx: &egui::Context, i: usize) -> Option { let st = app.session.active()?; let key = egui::Id::new(("ab-thumb", i, st.revision)); if let Some(t) = ctx.data(|d| d.get_temp::(key)) { @@ -575,7 +575,7 @@ fn recolor_map(d: &Dialog) -> Value { /// Recolor Artwork: current → new colour rows, harmony rules, randomize. Returns true when changed. fn recolor_ui(ui: &mut egui::Ui, d: &mut Dialog) -> bool { - use drawcraft_color::Color; + use vectorcraft_color::Color; let t = Tokens::get(ui.ctx()); let mut pairs: Vec = d.fields.get("pairs").and_then(Value::as_array).cloned().unwrap_or_default(); let mut changed = false; diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 67b081b78..985e5c9d2 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -5,11 +5,11 @@ use egui::{CornerRadius, Sense, Stroke, Ui, vec2}; use crate::state::{DockTab, ICON_PANEL_GROUPS, ICON_PANELS}; use crate::theme::{self, Tokens}; -use crate::{DrawcraftApp, icons, panels, widgets}; +use crate::{VectorcraftApp, icons, panels, widgets}; const ICON_COL: f32 = 38.0; -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); // Main tabbed group. egui::Panel::right("dock") @@ -74,7 +74,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } /// An icon panel popped out next to the icon column. -pub fn floating_panel(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn floating_panel(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(id) = app.ui.open_panel.clone() else { return }; let Some((_, label, _)) = ICON_PANELS.iter().find(|p| p.0 == id) else { return }; let t = Tokens::get(ctx); diff --git a/crates/ui-egui/src/find_font.rs b/crates/ui-egui/src/find_font.rs index 2bbe018a6..a1dc4fc57 100644 --- a/crates/ui-egui/src/find_font.rs +++ b/crates/ui-egui/src/find_font.rs @@ -4,33 +4,33 @@ use egui::Ui; use serde_json::{Value, json}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::state::Dialog; use crate::theme::{self, Tokens}; use crate::widgets; /// Open the dialog. -pub fn open(app: &mut DrawcraftApp) { +pub fn open(app: &mut VectorcraftApp) { app.ui.dialog = Some(Dialog::new("findFont", json!({ "selected": 0, "family": "Source Sans 3", "style": "", "selectionOnly": false }))); } -fn fonts(app: &mut DrawcraftApp) -> Vec { +fn fonts(app: &mut VectorcraftApp) -> Vec { app.session.execute("text.fonts", &json!({})).ok().and_then(|v| v.as_array().cloned()).unwrap_or_default() } -pub fn confirm(app: &mut DrawcraftApp) -> Result { +pub fn confirm(app: &mut VectorcraftApp) -> Result { app.ui.dialog = None; Ok(Value::Null) } /// Replace the selected document font with the chosen one (`all`: everywhere, else the selection). -fn change(app: &mut DrawcraftApp, d: &Dialog, from: &Value, all: bool) -> Result { +fn change(app: &mut VectorcraftApp, d: &Dialog, from: &Value, all: bool) -> Result { let style = d.str("style"); let to = if style.is_empty() { json!({ "family": d.str("family") }) } else { json!({ "family": d.str("family"), "style": style }) }; app.run("text.replaceFont", json!({ "from": { "family": from["family"], "style": from["style"] }, "to": to, "selectionOnly": !all })) } -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(mut d) = app.ui.dialog.clone() else { return }; let t = Tokens::get(ctx); let list = fonts(app); @@ -75,7 +75,7 @@ pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { }); ui.add_space(10.0); widgets::subheader(ui, "Replace With Font"); - let families = drawcraft_text::FontDb::global().families(); + let families = vectorcraft_text::FontDb::global().families(); let fam = d.str("family"); ui.horizontal(|ui| { let names: Vec<&str> = families.iter().map(String::as_str).collect(); @@ -83,7 +83,7 @@ pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { d.fields.insert("family".into(), json!(families[i])); d.fields.insert("style".into(), json!("")); } - let styles = drawcraft_text::FontDb::global().styles(&d.str("family")); + let styles = vectorcraft_text::FontDb::global().styles(&d.str("family")); let mut opts: Vec<&str> = vec!["(closest)"]; opts.extend(styles.iter().map(String::as_str)); let cur = if d.str("style").is_empty() { "(closest)".to_string() } else { d.str("style") }; @@ -134,7 +134,7 @@ mod tests { #[test] fn dialog_draws_and_changes_all() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi", "font": "Missing Family"})).unwrap(); open(&mut app); diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs index 1c11f82b0..c68952449 100644 --- a/crates/ui-egui/src/icon_data.rs +++ b/crates/ui-egui/src/icon_data.rs @@ -1,4 +1,4 @@ -//! Generated icon table (`assets/icons`). Lucide icons are ISC-licensed; `dc-*` icons are DrawCraft's own. +//! Generated icon table (`assets/icons`). Lucide icons are ISC-licensed; `dc-*` icons are VectorCraft's own. //! Regenerate when icons are added: see crates/ui-egui/README.md. pub static ICONS: &[(&str, &[u8])] = &[ diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index a659a2a85..8166b547a 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -2,10 +2,10 @@ use std::sync::Arc; -use drawcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind}; -use drawcraft_geom::Affine; +use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind}; +use vectorcraft_geom::Affine; -use crate::DrawcraftApp; +use crate::VectorcraftApp; fn ext(name: &str) -> String { std::path::Path::new(name).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default() @@ -78,25 +78,25 @@ fn hash(b: &[u8]) -> u64 { } /// Open bytes as a new document. -pub fn open_bytes(app: &mut DrawcraftApp, name: &str, bytes: &[u8], path: Option) -> Result<(), String> { +pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option) -> Result<(), String> { let e = ext(name); - let (doc, keep_path) = if e == "drawcraft" || drawcraft_format::sniff(bytes) { - let mut d = drawcraft_format::load(bytes).map_err(|e| e.to_string())?; + let (doc, keep_path) = if vectorcraft_format::is_native_ext(&e) || vectorcraft_format::sniff(bytes) { + let mut d = vectorcraft_format::load(bytes).map_err(|e| e.to_string())?; if d.title.is_empty() { d.title = name.to_string(); } (d, true) } else if e == "svg" || bytes.starts_with(b" Result<(), String> { +pub fn open_dialog(app: &mut VectorcraftApp) -> Result<(), String> { if let Some(f) = app.services.open_async.as_mut() { f(); return Ok(()); @@ -122,7 +122,7 @@ pub fn open_dialog(app: &mut DrawcraftApp) -> Result<(), String> { open_path(app, &path) } -pub fn open_path(app: &mut DrawcraftApp, path: &str) -> Result<(), String> { +pub fn open_path(app: &mut VectorcraftApp, path: &str) -> Result<(), String> { let read = app.services.read.as_ref().ok_or("no file reader")?; let bytes = read(path)?; let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or(path.to_string()); @@ -131,7 +131,7 @@ pub fn open_path(app: &mut DrawcraftApp, path: &str) -> Result<(), String> { Ok(()) } -fn write_out(app: &mut DrawcraftApp, path: &str, bytes: &[u8]) -> Result<(), String> { +fn write_out(app: &mut VectorcraftApp, path: &str, bytes: &[u8]) -> Result<(), String> { if let Some(dl) = app.services.download.as_mut() { let name = std::path::Path::new(path).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or(path.to_string()); dl(&name, bytes); @@ -141,29 +141,29 @@ fn write_out(app: &mut DrawcraftApp, path: &str, bytes: &[u8]) -> Result<(), Str w(path, bytes) } -fn suggested(app: &DrawcraftApp, ext: &str) -> String { +fn suggested(app: &VectorcraftApp, ext: &str) -> String { let t = app.session.active().map(|d| d.title()).unwrap_or_else(|| "Untitled".into()); let stem = std::path::Path::new(&t).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or(t); format!("{stem}.{ext}") } /// File → Save / Save As (native format). -pub fn save(app: &mut DrawcraftApp, path: Option, save_as: bool) -> Result { +pub fn save(app: &mut VectorcraftApp, path: Option, save_as: bool) -> Result { let st = app.session.active().ok_or("no document")?; let existing = if save_as { None } else { st.path.clone() }; let path = match path.or(existing) { Some(p) => p, - None if app.services.download.is_some() => suggested(app, "drawcraft"), + None if app.services.download.is_some() => suggested(app, "vectorcraft"), None => { - let s = suggested(app, "drawcraft"); + let s = suggested(app, "vectorcraft"); app.services.pick_save.as_mut().and_then(|f| f(&s)).ok_or("cancelled")? } }; - let bytes = drawcraft_format::save_file(&app.session.active().unwrap().doc); + let bytes = vectorcraft_format::save_file(&app.session.active().unwrap().doc); write_out(app, &path, &bytes)?; if let Some(st) = app.session.active_mut() { st.path = Some(path.clone()); - st.saved_revision = st.revision; + st.mark_saved(); } app.status(format!("Saved {path}")); note_recent(app, &path); @@ -171,7 +171,7 @@ pub fn save(app: &mut DrawcraftApp, path: Option, save_as: bool) -> Resu } /// Put `path` at the top of File → Open Recent Files (capped by Preferences → Recent Files). -pub fn note_recent(app: &mut DrawcraftApp, path: &str) { +pub fn note_recent(app: &mut VectorcraftApp, path: &str) { let r = &mut app.ui.recent_files; r.retain(|p| p != path); r.insert(0, path.to_string()); @@ -179,7 +179,7 @@ pub fn note_recent(app: &mut DrawcraftApp, path: &str) { } /// Export the document as SVG / PDF / PNG / JPEG / WebP. -pub fn export(app: &mut DrawcraftApp, format: &str, path: Option, scale: f64) -> Result { +pub fn export(app: &mut VectorcraftApp, format: &str, path: Option, scale: f64) -> Result { let st = app.session.active().ok_or("no document")?; let doc = st.doc.clone(); let path = match path { @@ -191,8 +191,8 @@ pub fn export(app: &mut DrawcraftApp, format: &str, path: Option, scale: } }; let bytes = match format { - "svg" => drawcraft_svg::export(&doc, &drawcraft_svg::ExportOptions { artboard: Some(0), ..Default::default() }).into_bytes(), - "pdf" => drawcraft_pdf::export(&doc, &drawcraft_pdf::PdfOptions::default()).map_err(|e| e.to_string())?, + "svg" => vectorcraft_svg::export(&doc, &vectorcraft_svg::ExportOptions { artboard: Some(0), ..Default::default() }).into_bytes(), + "pdf" => vectorcraft_pdf::export(&doc, &vectorcraft_pdf::PdfOptions::default()).map_err(|e| e.to_string())?, "png" | "jpg" | "jpeg" | "webp" => { let r = doc.artboards.first().map(|a| a.rect).ok_or("no artboard")?; let img = app.canvas.renderer.render_region(&doc, r, scale, format == "jpg" || format == "jpeg"); @@ -210,11 +210,11 @@ pub fn export(app: &mut DrawcraftApp, format: &str, path: Option, scale: } /// File → Place… (embed an image or SVG into the active document). -pub fn place_bytes(app: &mut DrawcraftApp, name: &str, bytes: &[u8]) -> Result<(), String> { +pub fn place_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8]) -> Result<(), String> { let e = ext(name); if e == "svg" { let s = std::str::from_utf8(bytes).map_err(|_| "SVG is not UTF-8".to_string())?; - let src = drawcraft_svg::import(s).map_err(|e| e.to_string())?; + let src = vectorcraft_svg::import(s).map_err(|e| e.to_string())?; let nodes: Vec = src.layers.iter().flat_map(|l| l.children().cloned().unwrap_or_default()).map(|n| (*n).clone()).collect(); app.session.clipboard = nodes; // Straight to the engine: `app.run` would let the system clipboard replace these nodes. diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index b944c6d2c..6625797ee 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1,6 +1,6 @@ -//! DrawCraft's egui frontend: an Illustrator-style UI over `drawcraft-engine`. +//! VectorCraft's egui frontend: an Illustrator-style UI over `vectorcraft-engine`. //! -//! The UI is thin: every action goes through [`DrawcraftApp::run`], which dispatches UI commands +//! The UI is thin: every action goes through [`VectorcraftApp::run`], which dispatches UI commands //! (view/window) here and everything else to the engine. The same entry point serves menus, //! shortcuts, the ⌘K palette and the control channel ([`control`]). #![forbid(unsafe_code)] @@ -32,8 +32,8 @@ pub mod workspaces; use std::sync::mpsc::{Receiver, Sender}; use std::sync::{Arc, Mutex}; -use drawcraft_engine::{Session, ViewInfo}; use serde_json::{Value, json}; +use vectorcraft_engine::{Session, ViewInfo}; pub use control::{ControlRequest, ControlResponse}; pub use state::{UiState, View}; @@ -66,7 +66,7 @@ pub struct Services { /// Cached canvas raster. pub struct CanvasCache { - pub renderer: drawcraft_render::Renderer, + pub renderer: vectorcraft_render::Renderer, pub texture: Option, pub key: Option, pub last_ms: f64, @@ -96,7 +96,7 @@ pub struct Perf { pub fps: f64, } -pub struct DrawcraftApp { +pub struct VectorcraftApp { pub session: Session, pub ui: UiState, pub views: Vec, @@ -125,7 +125,7 @@ pub struct DrawcraftApp { /// Canvas rect of the last frame (screen points), for control-channel coordinate mapping. pub canvas_rect: Option, /// Hover position in document coordinates. - pub hover_doc: Option, + pub hover_doc: Option, /// System clipboard: SVG to publish next frame, the last SVG we published (so pasting it back /// uses the lossless internal clipboard) and text that arrived with a Paste event. clipboard_out: Option, @@ -133,7 +133,7 @@ pub struct DrawcraftApp { pub(crate) clipboard_in: Option, } -impl DrawcraftApp { +impl VectorcraftApp { pub fn new(session: Session, services: Services) -> Self { let views = session.documents().iter().map(|_| View::default()).collect(); Self { @@ -142,7 +142,7 @@ impl DrawcraftApp { views, services, canvas: CanvasCache { - renderer: drawcraft_render::Renderer::new(), + renderer: vectorcraft_render::Renderer::new(), texture: None, key: None, last_ms: 0.0, @@ -233,7 +233,7 @@ impl DrawcraftApp { /// clipboard (centred in the view). Our own published SVG keeps the lossless internal copy. fn adopt_system_clipboard(&mut self) { let text = self.clipboard_in.take().or_else(|| self.services.clipboard_read.as_mut().and_then(|f| f())); - let Some(text) = text.filter(|t| drawcraft_engine::cmd::clipboard::looks_like_svg(t)) else { return }; + let Some(text) = text.filter(|t| vectorcraft_engine::cmd::clipboard::looks_like_svg(t)) else { return }; if self.clipboard_published.as_deref() == Some(text.as_str()) { return; } @@ -250,7 +250,7 @@ impl DrawcraftApp { if let Err(e) = self.session.select_tool(id, v) { self.ui.status = e.to_string(); } - if let Some(g) = drawcraft_tools::catalog::group_of(id) + if let Some(g) = vectorcraft_tools::catalog::group_of(id) && let Some(slot) = self.ui.group_tool.get_mut(g) { *slot = id.to_string(); @@ -361,7 +361,7 @@ pub fn now_ms() -> f64 { /// eframe isn't a dependency of this crate (the host owns the event loop); these entry points are /// called from the host's `eframe::App` impl. -impl DrawcraftApp { +impl VectorcraftApp { /// Per-frame logic before layout (control channel, shortcuts, inbox). pub fn logic(&mut self, ctx: &egui::Context) { if !self.styled { diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d60574a05..a792e1cd0 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1,13 +1,13 @@ //! The menu bar (Illustrator's menu tree) and UI-level commands. //! -//! Items bound to a command id run through [`DrawcraftApp::run`]. Items not implemented yet are +//! Items bound to a command id run through [`VectorcraftApp::run`]. Items not implemented yet are //! listed (disabled, with their shortcut) so the full surface is visible and discoverable; the //! parity tracker drives them to "done". use serde::Serialize; use serde_json::{Value, json}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::io; use crate::state::{DockTab, ICON_PANELS, next_zoom}; use crate::theme::{self, Brightness, Tokens}; @@ -73,7 +73,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("shortcuts.list", "List Keyboard Shortcuts", "", "{query?} → [{id, label, group, shortcut, default, overridden}]"), ("shortcuts.conflicts", "Keyboard Shortcut Conflicts", "", "{} → [{shortcut, ids}]"), ("shortcuts.reset", "Reset Keyboard Shortcuts", "", "{}"), - ("shortcuts.preset", "Keyboard Shortcut Set", "", "{name: \"DrawCraft Defaults\" | \"Illustrator Defaults\"}"), + ("shortcuts.preset", "Keyboard Shortcut Set", "", "{name: \"VectorCraft Defaults\" | \"Illustrator Defaults\"}"), ("shortcuts.export", "Export Keyboard Shortcuts…", "", "{path?}"), ("shortcuts.import", "Import Keyboard Shortcuts…", "", "{path? | data?}"), ("view.outline", "Outline", "Cmd+Y", "{} toggle Outline/Preview"), @@ -120,13 +120,13 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ ("ui.recolorDialog", "Recolor Artwork…", "", "{} open Recolor Artwork (engine: recolor.colors / recolor.apply)"), ("effect.applyLast", "Apply Last Effect", "Cmd+Shift+E", "{}"), ("file.export.pdf", "Save as PDF…", "", "{path?}"), - ("help.about", "About DrawCraft", "", "{}"), + ("help.about", "About VectorCraft", "", "{}"), ("help.commandPalette", "Search Commands…", "Cmd+Shift+/", "{}"), - ("app.quit", "Quit DrawCraft", "Cmd+Q", "{}"), + ("app.quit", "Quit VectorCraft", "Cmd+Q", "{}"), ]; /// Handle a UI command. `None` = not a UI command (the engine handles it). -pub fn run_ui_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option> { +pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { if let Some(r) = crate::panels::character::intercept_text_command(app, id) { return Some(r); } @@ -199,7 +199,7 @@ pub fn run_ui_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option Option { let z = p.get("zoom").and_then(Value::as_f64).unwrap_or(100.0) / 100.0; let center = - p.get("center").and_then(Value::as_array).and_then(|a| Some(drawcraft_geom::Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))); + p.get("center").and_then(Value::as_array).and_then(|a| Some(vectorcraft_geom::Point::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))); match app.view_mut() { Some(v) => { v.zoom = z.clamp(0.0313, 640.0); @@ -326,7 +326,7 @@ pub fn run_ui_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option Err("multiple windows land with M11.5".into()), "tool.select" => match s("tool") { - Some(t) if drawcraft_tools::tool_info(&t).is_some() => { + Some(t) if vectorcraft_tools::tool_info(&t).is_some() => { app.select_tool(&t); Ok(json!({"tool": app.session.tool_id()})) } @@ -340,7 +340,7 @@ pub fn run_ui_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option { let id = s("effect").unwrap_or_default(); - match drawcraft_effects::effect_catalog().into_iter().find(|e| e.id == id) { + match vectorcraft_effects::effect_catalog().into_iter().find(|e| e.id == id) { Some(e) => { let mut fields = e.defaults.as_object().cloned().unwrap_or_default(); fields.insert("__effect".into(), json!(e.id)); @@ -392,7 +392,7 @@ pub fn run_ui_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option Option { +pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option { let v = &app.ui.view; Some(match id { "view.outline" => v.outline, @@ -417,11 +417,11 @@ pub fn checked(app: &DrawcraftApp, id: &str, p: &Value) -> Option { } "window.workspace" => p.get("name").and_then(Value::as_str) == Some(app.ui.workspace.as_str()), "window.brightness" => p.get("brightness").and_then(Value::as_str).and_then(Brightness::parse) == Some(app.ui.brightness), - "view.proofColors" => drawcraft_render::proof::view().proof_colors, - "view.overprintPreview" => drawcraft_render::proof::view().overprint, - "view.proofSetup" => p.get("target").and_then(Value::as_str) == Some(drawcraft_render::proof::view().setup.target.id().as_str()), + "view.proofColors" => vectorcraft_render::proof::view().proof_colors, + "view.overprintPreview" => vectorcraft_render::proof::view().overprint, + "view.proofSetup" => p.get("target").and_then(Value::as_str) == Some(vectorcraft_render::proof::view().setup.target.id().as_str()), "file.documentColorMode" | "object.convertDocumentColorMode" => { - let cmyk = app.session.active().is_some_and(|d| d.doc.color_mode == drawcraft_engine::doc::ColorMode::Cmyk); + let cmyk = app.session.active().is_some_and(|d| d.doc.color_mode == vectorcraft_engine::doc::ColorMode::Cmyk); p.get("mode").and_then(Value::as_str) == Some(if cmyk { "cmyk" } else { "rgb" }) } _ => return None, @@ -429,7 +429,7 @@ pub fn checked(app: &DrawcraftApp, id: &str, p: &Value) -> Option { } /// Label for toggles whose text flips (Outline/Preview, Hide/Show …). -pub fn dynamic_label(app: &DrawcraftApp, id: &str, label: &str) -> String { +pub fn dynamic_label(app: &VectorcraftApp, id: &str, label: &str) -> String { let v = &app.ui.view; match id { "view.outline" => if v.outline { "Preview" } else { "Outline" }.into(), @@ -475,8 +475,8 @@ pub fn shortcut_of(id: &str) -> Option<&'static str> { } /// Is a command currently enabled? -pub fn enabled(app: &DrawcraftApp, id: &str) -> bool { - if let Some(c) = drawcraft_engine::find_command(id) { +pub fn enabled(app: &VectorcraftApp, id: &str) -> bool { + if let Some(c) = vectorcraft_engine::find_command(id) { return (c.enabled)(&app.session).is_ok(); } match id { @@ -509,15 +509,15 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { let panel = |label: &'static str, id: &'static str| cp(label, "window.panel", json!({ "panel": id })); vec![ ( - "DrawCraft", + "VectorCraft", vec![ - c("About DrawCraft", "help.about"), + c("About VectorCraft", "help.about"), Sep, c("Settings…", "edit.preferences"), Sep, sub("UI Brightness", Brightness::ALL.iter().map(|b| cp(b.label(), "window.brightness", json!({"brightness": b.id()}))).collect()), Sep, - c("Quit DrawCraft", "app.quit"), + c("Quit VectorCraft", "app.quit"), ], ), ( @@ -1058,12 +1058,12 @@ pub fn menu_tree() -> Vec<(&'static str, Vec)> { sub("Symbol Libraries", vec![todo("Arrows"), todo("Charts"), todo("Web Buttons and Bars")]), ], ), - ("Help", vec![c("Search Commands…", "help.commandPalette"), todos("DrawCraft Help…", "F1"), Sep, c("About DrawCraft", "help.about")]), + ("Help", vec![c("Search Commands…", "help.commandPalette"), todos("VectorCraft Help…", "F1"), Sep, c("About VectorCraft", "help.about")]), ] } /// Render the menu bar. -pub fn menu_bar(app: &mut DrawcraftApp, ui: &mut egui::Ui) { +pub fn menu_bar(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let mut clicked: Option<(String, Value)> = None; let tree = menu_tree(); @@ -1085,7 +1085,7 @@ pub fn menu_bar(app: &mut DrawcraftApp, ui: &mut egui::Ui) { } } -fn render_items(app: &DrawcraftApp, ui: &mut egui::Ui, items: &[Item], clicked: &mut Option<(String, Value)>) { +fn render_items(app: &VectorcraftApp, ui: &mut egui::Ui, items: &[Item], clicked: &mut Option<(String, Value)>) { let t = Tokens::get(ui.ctx()); for it in items { match it { @@ -1148,7 +1148,7 @@ pub fn click_target(label: &str, id: &str, p: &Value) -> (String, Value) { } /// Invoke a menu/command id with UI side effects (dialogs for "…" commands that need input). -pub fn invoke(app: &mut DrawcraftApp, id: &str, p: Value) { +pub fn invoke(app: &mut VectorcraftApp, id: &str, p: Value) { // Commands whose menu item opens a dialog in Illustrator. let dialog = match id { "object.move" => Some(("move", json!({"dx": "0 pt", "dy": "0 pt"}))), @@ -1229,8 +1229,8 @@ pub struct MenuEntry { } /// Flattened menu for `ui.menu.list`. -pub fn menu_entries(app: &DrawcraftApp) -> Vec { - fn walk(app: &DrawcraftApp, path: Vec, items: &[Item], out: &mut Vec) { +pub fn menu_entries(app: &VectorcraftApp) -> Vec { + fn walk(app: &VectorcraftApp, path: Vec, items: &[Item], out: &mut Vec) { for it in items { match it { Item::Cmd(l, id, p) => out.push(MenuEntry { @@ -1322,7 +1322,7 @@ fn insert_items(list: &[(&'static str, &'static str)]) -> Vec { /// doesn't allocate forever). fn font_items() -> Vec { static NAMES: std::sync::Mutex> = std::sync::Mutex::new(Vec::new()); - let fams = drawcraft_text::FontDb::global().families(); + let fams = vectorcraft_text::FontDb::global().families(); let Ok(mut names) = NAMES.lock() else { return vec![] }; fams.iter() .map(|f| { @@ -1341,7 +1341,7 @@ fn font_items() -> Vec { /// The Effect menu, built from the effects catalogue (Illustrator Effects), plus raster effects. fn effect_menu() -> Vec { - let cat = drawcraft_effects::effect_catalog(); + let cat = vectorcraft_effects::effect_catalog(); let mut out = vec![ c("Apply Last Effect", "effect.applyLast"), todos("Last Effect", "Cmd+Alt+Shift+E"), @@ -1395,8 +1395,8 @@ mod tests { fn recent_files_track_opens_and_saves() { let dir = std::env::temp_dir().join(format!("dc-recent-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); - let mut app = DrawcraftApp::new( - drawcraft_engine::Session::new(), + let mut app = VectorcraftApp::new( + vectorcraft_engine::Session::new(), crate::Services { read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))), write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))), @@ -1404,12 +1404,12 @@ mod tests { }, ); app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); - let (a, b) = (dir.join("a.drawcraft"), dir.join("b.drawcraft")); + let (a, b) = (dir.join("a.vectorcraft"), dir.join("b.vectorcraft")); for p in [&a, &b, &a] { app.run("file.saveAs", json!({"path": p.to_string_lossy()})).unwrap(); } assert_eq!(app.ui.recent_files, [a.to_string_lossy(), b.to_string_lossy()]); - assert_eq!(dynamic_label(&app, "file.openRecent2", ""), "b.drawcraft"); + assert_eq!(dynamic_label(&app, "file.openRecent2", ""), "b.vectorcraft"); assert!(enabled(&app, "file.openRecent2") && !enabled(&app, "file.openRecent3")); app.run("file.openRecent2", json!({})).unwrap(); assert_eq!(app.session.documents().len(), 2); @@ -1425,7 +1425,7 @@ mod tests { for it in items { match it { Item::Cmd(_, id, _) => { - if drawcraft_engine::find_command(id).is_none() + if vectorcraft_engine::find_command(id).is_none() && !UI_COMMANDS.iter().any(|c| c.0 == *id) && !id.starts_with("object.path.") && *id != "type.createOutlines" diff --git a/crates/ui-egui/src/palette.rs b/crates/ui-egui/src/palette.rs index 378ee61e7..cd4ce8451 100644 --- a/crates/ui-egui/src/palette.rs +++ b/crates/ui-egui/src/palette.rs @@ -3,16 +3,16 @@ use serde_json::json; use crate::theme::Tokens; -use crate::{DrawcraftApp, menus}; +use crate::{VectorcraftApp, menus}; -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { if !app.ui.palette_open { return; } let t = Tokens::get(ctx); let q = app.ui.palette_query.to_lowercase(); let mut items: Vec<(String, String, String)> = vec![]; // (label, kind/id, shortcut) - for c in drawcraft_engine::command_specs() { + for c in vectorcraft_engine::command_specs() { let path = c.menu.join(" › "); let label = if path.is_empty() { c.label.to_string() } else { format!("{path} › {}", c.label) }; items.push((label, c.id.to_string(), menus::shortcut_of(c.id).unwrap_or("").to_string())); @@ -20,7 +20,7 @@ pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { for c in menus::UI_COMMANDS { items.push((c.1.to_string(), c.0.to_string(), menus::shortcut_of(c.0).unwrap_or("").to_string())); } - for tool in drawcraft_tools::catalog::all_tools() { + for tool in vectorcraft_tools::catalog::all_tools() { items.push((tool.label.to_string(), format!("tool:{}", tool.id), crate::shortcut_editor::tool_shortcut(tool.id).unwrap_or("").to_string())); } let matches: Vec<&(String, String, String)> = items diff --git a/crates/ui-egui/src/panels/actions.rs b/crates/ui-egui/src/panels/actions.rs index f7b9d3c4c..e22fa744e 100644 --- a/crates/ui-egui/src/panels/actions.rs +++ b/crates/ui-egui/src/panels/actions.rs @@ -7,7 +7,7 @@ use serde_json::{Value, json}; use crate::theme::Tokens; use crate::widgets::{self, dim_label}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Action { @@ -44,13 +44,13 @@ pub fn default_sets() -> Vec { } /// Run an action as a single undoable batch. -pub fn play(app: &mut DrawcraftApp, set: usize, idx: usize) -> Result { +pub fn play(app: &mut VectorcraftApp, set: usize, idx: usize) -> Result { let Some(a) = app.ui.action_sets.get(set).and_then(|s| s.actions.get(idx)).cloned() else { return Err("no such action".into()) }; let commands: Vec = a.steps.iter().map(|(c, p)| json!({"command": c, "params": p})).collect(); app.run("command.batch", json!({"label": a.name, "commands": commands})) } -pub fn show(app: &mut DrawcraftApp, ui: &mut egui::Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let selected_id = egui::Id::new("actions-selected"); let mut selected: Option<(usize, usize)> = ui.data(|d| d.get_temp(selected_id)); @@ -149,7 +149,7 @@ mod tests { for set in default_sets() { for a in set.actions { for (c, _) in a.steps { - assert!(drawcraft_engine::find_command(&c).is_some(), "{c}"); + assert!(vectorcraft_engine::find_command(&c).is_some(), "{c}"); } } } diff --git a/crates/ui-egui/src/panels/align.rs b/crates/ui-egui/src/panels/align.rs index a6ed0089e..f0567a9a4 100644 --- a/crates/ui-egui/src/panels/align.rs +++ b/crates/ui-egui/src/panels/align.rs @@ -1,12 +1,12 @@ //! Align panel: Align Objects, Distribute Objects, Distribute Spacing (with a spacing value when //! aligning to a key object) and Align To (selection / key object / artboard). -use drawcraft_doc::Unit; use egui::Ui; use serde_json::{Value, json}; +use vectorcraft_doc::Unit; use super::{pstate, selection_len, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -53,7 +53,7 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [ ("dc-dist-right", "Horizontal Distribute Right", "horizontal", "right"), ]; -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let n = selection_len(app); let to: AlignTo = pstate(ui.ctx(), "align-to"); widgets::subheader(ui, "Align Objects:"); @@ -124,7 +124,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "align-hide-options"); if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { set_pstate(ui.ctx(), "align-hide-options", !hidden); diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index 818c19a7d..3be3192e1 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -4,15 +4,15 @@ use std::sync::OnceLock; -use drawcraft_color::{BlendMode, Paint}; -use drawcraft_doc::{AppearanceItem, Effect, Node}; use egui::{Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::{Value, json}; +use vectorcraft_color::{BlendMode, Paint}; +use vectorcraft_doc::{AppearanceItem, Effect, Node}; use super::{first_selected, pstate, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; const ROW: f32 = 30.0; const EYE_W: f32 = 26.0; @@ -29,7 +29,7 @@ pub enum Sel { fn catalog() -> &'static [(String, String, Vec)] { static C: OnceLock)>> = OnceLock::new(); C.get_or_init(|| { - drawcraft_effects::effect_catalog() + vectorcraft_effects::effect_catalog() .into_iter() .map(|e| (e.id.to_string(), e.label.trim_end_matches('…').to_string(), e.menu.iter().map(|s| s.to_string()).collect())) .collect() @@ -114,7 +114,7 @@ fn link(ui: &mut Ui, pos: egui::Pos2, id: impl std::hash::Hash + std::fmt::Debug resp.clicked() } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let node = first_selected(app); let sel: Sel = pstate(ui.ctx(), "ap-sel"); @@ -146,7 +146,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } /// The defaults for new art when nothing is selected (read-only rows). -fn default_stack(app: &mut DrawcraftApp, ui: &mut Ui) { +fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) { let p = app.session.paint.clone(); for (lbl, paint, w) in [("Stroke:", p.stroke, Some(p.stroke_width)), ("Fill:", p.fill, None)] { let (r, _) = row(ui, false); @@ -162,7 +162,7 @@ fn default_stack(app: &mut DrawcraftApp, ui: &mut Ui) { text(ui, r.left_center() + vec2(EYE_W + 24.0, 0.0), "Opacity: Default", false); } -fn stack(app: &mut DrawcraftApp, ui: &mut Ui, n: &Node, sel: Sel) { +fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel) { let t = Tokens::get(ui.ctx()); let items: Vec<(usize, AppearanceItem)> = n.appearance.items.iter().cloned().enumerate().rev().collect(); let mut drop: Option<(usize, usize)> = None; @@ -202,7 +202,7 @@ fn stack(app: &mut DrawcraftApp, ui: &mut Ui, n: &Node, sel: Sel) { if is_stroke { let fr = Rect::from_min_size(pos2(r.left() + EYE_W + 104.0, r.center().y - 12.0), vec2(56.0, 24.0)); let mut child = ui.new_child(egui::UiBuilder::new().max_rect(fr).layout(egui::Layout::left_to_right(egui::Align::Center))); - if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(width), drawcraft_doc::Unit::Points, 56.0) { + if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(width), vectorcraft_doc::Unit::Points, 56.0) { app.run("appearance.setItem", json!({"index": i, "weight": w})).ok(); } } @@ -270,13 +270,13 @@ fn target_index(rows: &[(usize, Rect)], y: f32) -> usize { }) } -fn sub_row(_app: &mut DrawcraftApp, ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug + Copy, label: &str, visible: bool, depth: usize) { +fn sub_row(_app: &mut VectorcraftApp, ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug + Copy, label: &str, visible: bool, depth: usize) { let (r, _) = row(ui, false); eye(ui, r, (id, "eye"), visible, false); text(ui, r.left_center() + vec2(EYE_W + 24.0 + depth as f32 * 12.0, 0.0), label, false); } -fn effect_row(app: &mut DrawcraftApp, ui: &mut Ui, k: usize, e: &Effect, selected: bool) { +fn effect_row(app: &mut VectorcraftApp, ui: &mut Ui, k: usize, e: &Effect, selected: bool) { let t = Tokens::get(ui.ctx()); let open: bool = pstate(ui.ctx(), &format!("ap-fx-open-{k}")); let (r, resp) = row(ui, selected); @@ -305,9 +305,9 @@ fn effect_row(app: &mut DrawcraftApp, ui: &mut Ui, k: usize, e: &Effect, selecte /// Inline editor for an applied effect's parameters (numbers, booleans, strings, colours); /// edits go through `effect.setParams`. -fn effect_editor(app: &mut DrawcraftApp, ui: &mut Ui, k: usize, e: &Effect) { +fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, k: usize, e: &Effect) { let t = Tokens::get(ui.ctx()); - let defaults = drawcraft_effects::effect_catalog().into_iter().find(|c| c.id == e.id).map(|c| c.defaults).unwrap_or(Value::Null); + let defaults = vectorcraft_effects::effect_catalog().into_iter().find(|c| c.id == e.id).map(|c| c.defaults).unwrap_or(Value::Null); let mut params = defaults.as_object().cloned().unwrap_or_default(); if let Some(cur) = e.params.as_object() { for (key, v) in cur { @@ -372,7 +372,7 @@ pub fn humanize(key: &str) -> String { } /// Small swatch grid inside the appearance chip popup. -fn swatch_picker(app: &mut DrawcraftApp, ui: &mut Ui, index: usize) { +fn swatch_picker(app: &mut VectorcraftApp, ui: &mut Ui, index: usize) { let Some(st) = app.session.active() else { return }; let mut all: Vec<(String, Paint)> = st.doc.swatches.iter().map(|s| (s.name.clone(), s.paint.clone())).collect(); for g in &st.doc.swatch_groups { @@ -397,7 +397,7 @@ fn swatch_picker(app: &mut DrawcraftApp, ui: &mut Ui, index: usize) { } } -fn bottom(app: &mut DrawcraftApp, ui: &mut Ui, node: Option<&Node>, sel: Sel) { +fn bottom(app: &mut VectorcraftApp, ui: &mut Ui, node: Option<&Node>, sel: Sel) { let has = node.is_some(); widgets::bottom_bar(ui, |ui| { if widgets::icon_button_enabled(ui, "dc-new-stroke", "Add New Stroke", false, has, 24.0).clicked() { @@ -429,7 +429,7 @@ fn bottom(app: &mut DrawcraftApp, ui: &mut Ui, node: Option<&Node>, sel: Sel) { }); } -fn delete_selected(app: &mut DrawcraftApp, ui: &Ui, sel: Sel) { +fn delete_selected(app: &mut VectorcraftApp, ui: &Ui, sel: Sel) { let ok = match sel { Sel::Item(i) => app.run("appearance.removeItem", json!({"index": i})).is_ok(), Sel::Effect(k) => app.run("effect.remove", json!({"index": k})).is_ok(), @@ -441,7 +441,7 @@ fn delete_selected(app: &mut DrawcraftApp, ui: &Ui, sel: Sel) { } /// The fx menu: effects grouped by their Effect-menu submenu; opens the effect dialog. -fn fx_menu(app: &mut DrawcraftApp, ui: &mut Ui) { +fn fx_menu(app: &mut VectorcraftApp, ui: &mut Ui) { let mut groups: Vec<(String, Vec<(String, String)>)> = vec![]; for (id, label, menu) in catalog() { let g = menu.get(1).cloned().unwrap_or_else(|| "Other".into()); @@ -462,7 +462,7 @@ fn fx_menu(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let node = first_selected(app); let has = node.is_some(); let sel: Sel = pstate(ui.ctx(), "ap-sel"); diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index 0089a7aed..1c5265c75 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -6,13 +6,13 @@ use serde_json::json; use super::{pstate, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; fn selected(ui: &Ui, n: usize) -> usize { pstate::(ui.ctx(), "ab-sel").min(n.saturating_sub(1)) } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let abs: Vec = app.session.active().map(|d| d.doc.artboards.iter().map(|a| a.name.clone()).collect()).unwrap_or_default(); if abs.is_empty() { @@ -97,7 +97,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let n = app.session.active().map(|d| d.doc.artboards.len()).unwrap_or(0); let sel = selected(ui, n); if menu_item(ui, "New Artboard", n > 0, false) { diff --git a/crates/ui-egui/src/panels/brushes.rs b/crates/ui-egui/src/panels/brushes.rs index 54950c59f..1b159cb50 100644 --- a/crates/ui-egui/src/panels/brushes.rs +++ b/crates/ui-egui/src/panels/brushes.rs @@ -4,12 +4,12 @@ use std::cell::RefCell; use std::collections::HashMap; -use drawcraft_doc::{Appearance, Document, Node, color::Color, color::Paint}; use egui::{Sense, Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{Appearance, Document, Node, color::Color, color::Paint}; use super::{first_selected, pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -17,7 +17,7 @@ const KINDS: [(&str, &str); 5] = [("calligraphic", "Calligraphic"), ("scatter", "Scatter"), ("art", "Art"), ("bristle", "Bristle"), ("pattern", "Pattern")]; /// (name, type) of every brush in the active document's library. -pub fn brushes(app: &mut DrawcraftApp) -> (Vec<(String, String)>, Option) { +pub fn brushes(app: &mut VectorcraftApp) -> (Vec<(String, String)>, Option) { let Ok(v) = app.run("brush.list", json!({})) else { return (vec![], None) }; let list = v["brushes"] .as_array() @@ -30,7 +30,7 @@ pub fn brushes(app: &mut DrawcraftApp) -> (Vec<(String, String)>, Option /// definition's JSON. fn preview(ui: &Ui, def: &Value, size: egui::Vec2) -> Option { thread_local! { - static RENDERER: RefCell = RefCell::new(drawcraft_render::Renderer::new()); + static RENDERER: RefCell = RefCell::new(vectorcraft_render::Renderer::new()); static CACHE: RefCell> = RefCell::new(HashMap::new()); } let ppp = ui.ctx().pixels_per_point() as f64; @@ -45,14 +45,14 @@ fn preview(ui: &Ui, def: &Value, size: egui::Vec2) -> Option Option Option { +fn selected_brush(app: &VectorcraftApp) -> Option { first_selected(app).and_then(|n| n.appearance.stroke().and_then(|s| s.brush.clone())) } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let (list, current) = brushes(app); let sel_brush = selected_brush(app); @@ -169,7 +169,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let sel = selected_brush(app); let (_, current) = brushes(app); let target = sel.clone().or(current); diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index 8fc5afced..05e89a670 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -3,13 +3,13 @@ use std::sync::{Mutex, OnceLock}; -use drawcraft_doc::{CharStyle, NodeId, NodeKind, Unit}; -use drawcraft_tools::{Mods, ToolKey}; use egui::{Key, Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{CharStyle, NodeId, NodeKind, Unit}; +use vectorcraft_tools::{Mods, ToolKey}; use super::{first_selected, pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -19,11 +19,11 @@ pub const SCALE_PRESETS: [f64; 8] = [25.0, 50.0, 75.0, 90.0, 100.0, 110.0, 125.0 /// The character style shown by the panel: the selected range's (or the caret's) while the Type /// tool edits text, else the first selected text object's first run. -pub(crate) fn text_style(app: &DrawcraftApp) -> Option<(CharStyle, drawcraft_doc::ParaStyle)> { +pub(crate) fn text_style(app: &VectorcraftApp) -> Option<(CharStyle, vectorcraft_doc::ParaStyle)> { if let Some((id, a, b)) = text_editing(app) && let Some(NodeKind::Text(t)) = app.session.active().and_then(|d| d.doc.node(id)).map(|n| &n.kind) { - return Some((drawcraft_text::edit::insertion_style(&t.runs, a, b), t.para.clone())); + return Some((vectorcraft_text::edit::insertion_style(&t.runs, a, b), t.para.clone())); } let n = first_selected(app)?; match &n.kind { @@ -32,16 +32,16 @@ pub(crate) fn text_style(app: &DrawcraftApp) -> Option<(CharStyle, drawcraft_doc } } -fn style(app: &mut DrawcraftApp, p: Value) { +fn style(app: &mut VectorcraftApp, p: Value) { char_cmd(app, "text.setStyle", p); } -fn format(app: &mut DrawcraftApp, p: Value) { +fn format(app: &mut VectorcraftApp, p: Value) { char_cmd(app, "text.setFormat", p); } /// Apply character attributes: to the selected range while the Type tool edits text (the whole /// object when nothing is selected), else to the selected text objects. -fn char_cmd(app: &mut DrawcraftApp, cmd: &str, p: Value) { +fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) { let Some((id, a, b)) = text_editing(app) else { app.run(cmd, p).ok(); return; @@ -59,7 +59,7 @@ fn char_cmd(app: &mut DrawcraftApp, cmd: &str, p: Value) { // ---------- Type tool editing glue (selection, clipboard, keys) ---------- /// The text object the Type tool is editing and its selection (clamped to the text). -pub(crate) fn text_editing(app: &DrawcraftApp) -> Option<(NodeId, usize, usize)> { +pub(crate) fn text_editing(app: &VectorcraftApp) -> Option<(NodeId, usize, usize)> { let o = app.session.tool_options(); let id = NodeId(o.get("editing")?.as_u64()?); let len = match &app.session.active()?.doc.node(id)?.kind { @@ -71,7 +71,7 @@ pub(crate) fn text_editing(app: &DrawcraftApp) -> Option<(NodeId, usize, usize)> } /// Close the Type tool's typing session (one undo step) before another command edits the text. -pub(crate) fn end_typing(app: &mut DrawcraftApp) { +pub(crate) fn end_typing(app: &mut VectorcraftApp) { if app.session.tool_options().get("typing").and_then(Value::as_bool) == Some(true) { app.session.set_tool_option("commitTyping", &json!(true)); app.session.commit_interaction().ok(); @@ -84,7 +84,7 @@ static CTX: OnceLock = OnceLock::new(); /// can't read the system clipboard). static CLIP: Mutex = Mutex::new(String::new()); -fn text_copy(app: &mut DrawcraftApp) -> bool { +fn text_copy(app: &mut VectorcraftApp) -> bool { let Some((id, a, b)) = text_editing(app) else { return false }; if a == b { return false; @@ -99,14 +99,14 @@ fn text_copy(app: &mut DrawcraftApp) -> bool { true } -fn text_key(app: &mut DrawcraftApp, k: ToolKey, m: Mods) { +fn text_key(app: &mut VectorcraftApp, k: ToolKey, m: Mods) { let v = app.view_info(); if let Err(e) = app.session.tool_key(k, m, v) { app.status(e.to_string()); } } -fn text_paste(app: &mut DrawcraftApp, s: Option) { +fn text_paste(app: &mut VectorcraftApp, s: Option) { let s = s.unwrap_or_else(|| CLIP.lock().unwrap_or_else(|e| e.into_inner()).clone()); if !s.is_empty() { let v = app.view_info(); @@ -118,7 +118,7 @@ fn text_paste(app: &mut DrawcraftApp, s: Option) { /// Edit-menu commands while the Type tool edits text act on the text (Cut/Copy/Paste/Select /// All/Clear). `None` = not intercepted. -pub(crate) fn intercept_text_command(app: &mut DrawcraftApp, id: &str) -> Option> { +pub(crate) fn intercept_text_command(app: &mut VectorcraftApp, id: &str) -> Option> { if !app.session.tool_wants_text() { return None; } @@ -149,7 +149,7 @@ enum TextInput { } /// Route editing keys (with modifiers), clipboard events and Cmd+A/C/X/V to the Type tool. -pub(crate) fn route_type_input(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub(crate) fn route_type_input(app: &mut VectorcraftApp, ctx: &egui::Context) { CTX.get_or_init(|| ctx.clone()); let mut todo = vec![]; ctx.input_mut(|i| { @@ -228,18 +228,18 @@ fn cell(ui: &mut Ui, label: &str, tip: &str, add: impl FnOnce(&mut Ui)) { }); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some((s, _)) = text_style(app) else { super::empty_state(ui, "type", "No text selected", "Select a text object to edit its character attributes."); return; }; - let fams = drawcraft_text::FontDb::global().families(); + let fams = vectorcraft_text::FontDb::global().families(); let names: Vec<&str> = fams.iter().map(String::as_str).collect(); let w = ui.available_width(); if let Some(i) = widgets::dropdown(ui, "ch-font", &s.font_family, &names, w - 4.0) { style(app, json!({"font": names[i]})); } - let styles = drawcraft_text::FontDb::global().styles(&s.font_family); + let styles = vectorcraft_text::FontDb::global().styles(&s.font_family); let snames: Vec<&str> = styles.iter().map(String::as_str).collect(); if let Some(i) = widgets::dropdown(ui, "ch-style", &s.font_style, &snames, w - 4.0) { style(app, json!({"style": snames[i]})); @@ -369,7 +369,7 @@ fn style_toggle(ui: &mut Ui, g: Glyph, tip: &str, on: bool, enabled: bool) -> bo enabled && resp.clicked() } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let st = text_style(app); let has = st.is_some(); let hidden: bool = pstate(ui.ctx(), "ch-hide-options"); @@ -396,8 +396,8 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { } ui.separator(); #[cfg(not(target_arch = "wasm32"))] - if menu_item(ui, "Show System Fonts", true, drawcraft_text::FontDb::global().families().len() > 4) { - let n = drawcraft_text::FontDb::global().load_system_fonts(); + if menu_item(ui, "Show System Fonts", true, vectorcraft_text::FontDb::global().families().len() > 4) { + let n = vectorcraft_text::FontDb::global().load_system_fonts(); app.ui.status = format!("{n} system font faces available"); } if menu_item(ui, "Reset Panel", has, false) { diff --git a/crates/ui-egui/src/panels/color.rs b/crates/ui-egui/src/panels/color.rs index 0f37bf0ca..eb2f44dbe 100644 --- a/crates/ui-egui/src/panels/color.rs +++ b/crates/ui-egui/src/panels/color.rs @@ -2,14 +2,14 @@ //! value fields, hex field, None/Black/White chips, spectrum ramp and the Fill/Stroke proxy. //! When the active paint is a gradient, the sliders edit the Gradient panel's selected stop. -use drawcraft_color::{Color, Paint}; use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::json; +use vectorcraft_color::{Color, Paint}; use super::{active_paint, color_json, live_run, paint_target, pstate, push_recent, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, Live, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Mode { @@ -159,7 +159,7 @@ enum Target { Stop { paint: Paint, index: usize, color: Color }, } -fn target(app: &DrawcraftApp, ui: &Ui) -> Target { +fn target(app: &VectorcraftApp, ui: &Ui) -> Target { let p = active_paint(app); match &p { Paint::Gradient(g) => { @@ -172,7 +172,7 @@ fn target(app: &DrawcraftApp, ui: &Ui) -> Target { } } -fn apply(app: &mut DrawcraftApp, ui: &Ui, tgt: &Target, c: Color, phase: Live) { +fn apply(app: &mut VectorcraftApp, ui: &Ui, tgt: &Target, c: Color, phase: Live) { match tgt { Target::Paint(_) => { let cmd = paint_target(app); @@ -194,7 +194,7 @@ fn apply(app: &mut DrawcraftApp, ui: &Ui, tgt: &Target, c: Color, phase: Live) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let tgt = target(app, ui); let color = match &tgt { @@ -370,7 +370,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "color-hide-options"); if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { set_pstate(ui.ctx(), "color-hide-options", !hidden); diff --git a/crates/ui-egui/src/panels/color_guide.rs b/crates/ui-egui/src/panels/color_guide.rs index 38a7a2712..f3c2ef78d 100644 --- a/crates/ui-egui/src/panels/color_guide.rs +++ b/crates/ui-egui/src/panels/color_guide.rs @@ -1,13 +1,13 @@ //! Color Guide panel: base colour, harmony-rule dropdown with the harmony strip, and a variation //! grid (tints/shades, warm/cool or vivid/muted) whose cells apply to the active proxy. -use drawcraft_color::harmony::Harmony; -use drawcraft_color::{Color, Paint}; use egui::{Rect, Sense, Ui, vec2}; use serde_json::json; +use vectorcraft_color::harmony::Harmony; +use vectorcraft_color::{Color, Paint}; use super::{active_paint, color_json, paint_target, pstate, push_recent, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -52,7 +52,7 @@ fn harmony(ui: &Ui) -> Harmony { Harmony::ALL[i.min(Harmony::ALL.len() - 1)] } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = crate::theme::Tokens::get(ui.ctx()); let base = match active_paint(app) { Paint::Solid { color, .. } => color, @@ -129,7 +129,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let mode: Variation = pstate(ui.ctx(), "cg-mode"); for (m, l) in [(Variation::TintsShades, "Show Tints/Shades"), (Variation::WarmCool, "Show Warm/Cool"), (Variation::VividMuted, "Show Vivid/Muted")] diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index a30aef092..fc21c2923 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -5,7 +5,7 @@ use egui::Ui; use serde_json::{Value, json}; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -38,7 +38,7 @@ fn row(ui: &mut Ui, label: &str, value: String) { }); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let sel_only: bool = pstate(ui.ctx(), "docinfo-sel"); let Ok(i) = app.session.execute("document.info", &json!({ "selectionOnly": sel_only })) else { widgets::dim_label(ui, "No document"); @@ -97,7 +97,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let sel: bool = pstate(ui.ctx(), "docinfo-sel"); if menu_item(ui, "Selection Only", true, sel) { set_pstate(ui.ctx(), "docinfo-sel", !sel); @@ -110,7 +110,7 @@ mod tests { #[test] fn draws_headless() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap(); let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index e8306976e..cd2cd80fa 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -5,13 +5,13 @@ use std::cell::RefCell; use std::collections::HashMap; -use drawcraft_geom::{Affine, BezPath, PathEl}; -use drawcraft_text::FontDb; use egui::{Color32, Sense, Ui, vec2}; use serde_json::json; +use vectorcraft_geom::{Affine, BezPath, PathEl}; +use vectorcraft_text::FontDb; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -36,7 +36,7 @@ thread_local! { pub(crate) fn rasterize(path: &BezPath, w: usize, h: usize) -> Vec { let mut edges: Vec<(f64, f64, f64, f64)> = vec![]; let (mut start, mut last) = (None, (0.0, 0.0)); - drawcraft_geom::kurbo::flatten(path, 0.1, |el| match el { + vectorcraft_geom::kurbo::flatten(path, 0.1, |el| match el { PathEl::MoveTo(p) => { if let Some(s) = start.replace((p.x, p.y)) { edges.push((last.0, last.1, s.0, s.1)); @@ -91,7 +91,7 @@ pub(crate) fn rasterize(path: &BezPath, w: usize, h: usize) -> Vec { acc.into_iter().map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8).collect() } -fn glyph_texture(ctx: &egui::Context, face: &drawcraft_text::FontFace, gid: u32, px: u32) -> egui::TextureHandle { +fn glyph_texture(ctx: &egui::Context, face: &vectorcraft_text::FontFace, gid: u32, px: u32) -> egui::TextureHandle { let key = (face.id(), gid, px); if let Some(t) = TEX.with(|c| c.borrow().get(&key).cloned()) { return t; @@ -119,7 +119,7 @@ fn glyph_texture(ctx: &egui::Context, face: &drawcraft_text::FontFace, gid: u32, } /// Insert `ch` at the Type tool's caret, else append it to the selected text. -fn insert(app: &mut DrawcraftApp, ch: char) { +fn insert(app: &mut VectorcraftApp, ch: char) { let s = ch.to_string(); if app.session.tool_wants_text() { let v = app.view_info(); @@ -134,13 +134,13 @@ fn insert(app: &mut DrawcraftApp, ch: char) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let db = FontDb::global(); let current = super::character::text_style(app).map(|(s, _)| (s.font_family, s.font_style)); // The panel browses the current text's font unless another font was picked here. let picked: Option<(String, String)> = pstate(ui.ctx(), "gl-font"); - let (family, style) = picked.or(current).unwrap_or_else(|| (drawcraft_text::FALLBACK_FAMILY.to_string(), "Regular".to_string())); + let (family, style) = picked.or(current).unwrap_or_else(|| (vectorcraft_text::FALLBACK_FAMILY.to_string(), "Regular".to_string())); let face = db.face(&family, &style); let subset: usize = pstate(ui.ctx(), "gl-subset"); let (_, lo, hi) = SUBSETS[subset.min(SUBSETS.len() - 1)]; @@ -205,7 +205,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, "Use the Selected Text's Font", true, false) { set_pstate::>(ui.ctx(), "gl-font", None); } diff --git a/crates/ui-egui/src/panels/gradient.rs b/crates/ui-egui/src/panels/gradient.rs index a4801bc23..adc7973a2 100644 --- a/crates/ui-egui/src/panels/gradient.rs +++ b/crates/ui-egui/src/panels/gradient.rs @@ -2,14 +2,14 @@ //! stops and midpoint diamonds (click below the ramp adds a stop, drag a stop off to remove it), //! and stop Opacity / Location fields. Stop colours are edited with the Color panel. -use drawcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; use super::{active_paint, color_json, live_run, pstate, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, Live, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; /// Minimum number of stops (Illustrator won't delete below two). pub const MIN_STOPS: usize = 2; @@ -96,14 +96,14 @@ pub fn stops_json(stops: &[GradientStop]) -> Value { // ---------- UI ---------- -fn current(app: &DrawcraftApp) -> Option { +fn current(app: &VectorcraftApp) -> Option { match active_paint(app) { Paint::Gradient(g) => Some(*g), _ => None, } } -fn edit(app: &mut DrawcraftApp, params: Value, phase: Live) { +fn edit(app: &mut VectorcraftApp, params: Value, phase: Live) { let mut p = params; p["stroke"] = json!(!app.session.fill_active); live_run(app, "Gradient", "paint.editGradient", p, phase); @@ -118,7 +118,7 @@ enum Drag { Mid(usize), } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let gp = current(app); let fallback = GradientPaint::new(Gradient::default()); @@ -241,7 +241,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } /// The gradient slider: ramp, stops below, midpoint diamonds above. -fn ramp(app: &mut DrawcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { +fn ramp(app: &mut VectorcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { let t = Tokens::get(ui.ctx()); let w = ui.available_width(); let (area, _) = ui.allocate_exact_size(vec2(w, 50.0), Sense::hover()); @@ -389,7 +389,7 @@ fn ramp(app: &mut DrawcraftApp, ui: &mut Ui, g: &Gradient, is_grad: bool) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let g = current(app); menu_item(ui, "Hide Options", false, false); if menu_item(ui, "Add to Swatches", g.is_some(), false) diff --git a/crates/ui-egui/src/panels/graphic_styles.rs b/crates/ui-egui/src/panels/graphic_styles.rs index ea530bd59..41c35d299 100644 --- a/crates/ui-egui/src/panels/graphic_styles.rs +++ b/crates/ui-egui/src/panels/graphic_styles.rs @@ -1,11 +1,11 @@ //! Graphic Styles panel: thumbnail grid or list of the document's styles. -use drawcraft_doc::Appearance; use egui::{Rect, Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::json; +use vectorcraft_doc::Appearance; use super::{pstate, selection_len, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -29,7 +29,7 @@ fn preview(ui: &Ui, r: Rect, ap: &Appearance) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let styles: Vec<(String, Appearance)> = app.session.active().map(|d| d.doc.graphic_styles.iter().map(|g| (g.name.clone(), g.appearance.clone())).collect()).unwrap_or_default(); @@ -110,7 +110,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let sel: Option = pstate(ui.ctx(), "gs-sel"); let list: bool = pstate(ui.ctx(), "gs-list"); if menu_item(ui, "New Graphic Style…", selection_len(app) > 0, false) { diff --git a/crates/ui-egui/src/panels/history.rs b/crates/ui-egui/src/panels/history.rs index 4829a1efd..007e944f7 100644 --- a/crates/ui-egui/src/panels/history.rs +++ b/crates/ui-egui/src/panels/history.rs @@ -5,7 +5,7 @@ use serde_json::json; use crate::theme::Tokens; use crate::widgets::menu_item; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; /// Undo (negative) / redo (positive) steps to reach row `target` when `current` is the current /// row (row 0 = the document as opened). @@ -13,7 +13,7 @@ pub fn steps_to(current: usize, target: usize) -> i64 { target as i64 - current as i64 } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { super::empty_state(ui, "history", "No document", "Open a document to see its history."); @@ -59,7 +59,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { menu_item(ui, "History Options…", false, false); } diff --git a/crates/ui-egui/src/panels/image_trace.rs b/crates/ui-egui/src/panels/image_trace.rs index f4f52928d..948c49754 100644 --- a/crates/ui-egui/src/panels/image_trace.rs +++ b/crates/ui-egui/src/panels/image_trace.rs @@ -6,7 +6,7 @@ use egui::Ui; use serde_json::{Value, json}; use super::{first_selected, pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; const MODES: [(&str, &str); 3] = [("blackAndWhite", "Black and White"), ("grayscale", "Grayscale"), ("color", "Color")]; @@ -22,7 +22,7 @@ struct TraceUi { synced: Value, } -fn presets(app: &mut DrawcraftApp) -> Vec<(String, Value)> { +fn presets(app: &mut VectorcraftApp) -> Vec<(String, Value)> { let v = app.session.execute("imageTrace.presets", &json!({})).unwrap_or_default(); v["presets"] .as_array() @@ -31,14 +31,14 @@ fn presets(app: &mut DrawcraftApp) -> Vec<(String, Value)> { } /// What the selection is: an Image Trace object (with its stored settings), a plain image, or neither. -fn target(app: &DrawcraftApp) -> (bool, bool, Option) { +fn target(app: &VectorcraftApp) -> (bool, bool, Option) { let Some(n) = first_selected(app) else { return (false, false, None) }; let trace = n.name.as_deref() == Some("Image Trace") - && n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, drawcraft_doc::NodeKind::Image(_)))); - (trace, matches!(n.kind, drawcraft_doc::NodeKind::Image(_)), n.trace.map(|t| *t)) + && n.children().is_some_and(|c| c.first().is_some_and(|i| matches!(i.kind, vectorcraft_doc::NodeKind::Image(_)))); + (trace, matches!(n.kind, vectorcraft_doc::NodeKind::Image(_)), n.trace.map(|t| *t)) } -fn trace(app: &mut DrawcraftApp, st: &mut TraceUi) { +fn trace(app: &mut VectorcraftApp, st: &mut TraceUi) { let preset = if st.preset == "Custom" { "Default" } else { st.preset.as_str() }; match app.run("imageTrace.make", json!({ "preset": preset, "params": st.params })) { Ok(r) => st.info = Some((r["paths"].as_u64().unwrap_or(0), r["anchors"].as_u64().unwrap_or(0), r["colors"].as_u64().unwrap_or(0))), @@ -60,7 +60,7 @@ fn slider(ui: &mut Ui, label: &str, v: &mut f64, range: std::ops::RangeInclusive out } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let all = presets(app); let mut st: TraceUi = pstate(ui.ctx(), "image-trace"); if st.params.is_null() { @@ -163,7 +163,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { set_pstate(ui.ctx(), "image-trace", st); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let (is_trace, _, _) = target(app); if menu_item(ui, "Release", is_trace, false) { app.run("imageTrace.release", json!({})).ok(); @@ -183,7 +183,7 @@ mod tests { #[test] fn panel_and_menu_draw_headless() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); for _ in 0..2 { diff --git a/crates/ui-egui/src/panels/info.rs b/crates/ui-egui/src/panels/info.rs index 7884f7b28..9af9879c0 100644 --- a/crates/ui-egui/src/panels/info.rs +++ b/crates/ui-egui/src/panels/info.rs @@ -1,11 +1,11 @@ //! Info panel: cursor X/Y, selection W/H, the pointer's distance/angle from the selection, and the //! selection's fill and stroke colour values. -use drawcraft_color::{Color, Paint}; use egui::{Sense, Ui, vec2}; +use vectorcraft_color::{Color, Paint}; use super::{current_paints, first_selected, pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -43,7 +43,7 @@ fn paint_block(ui: &mut Ui, p: &Paint) { }); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let units = app.session.active().map(|d| d.doc.units).unwrap_or_default(); let p = app.hover_doc.unwrap_or_default(); @@ -74,7 +74,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { paint_block(ui, &f); paint_block(ui, &s); if let Some(n) = first_selected(app) - && let drawcraft_doc::NodeKind::Text(tx) = &n.kind + && let vectorcraft_doc::NodeKind::Text(tx) = &n.kind { widgets::divider(ui); let st = tx.first_style(); @@ -83,7 +83,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "info-hide-options"); if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { set_pstate(ui.ctx(), "info-hide-options", !hidden); diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 8eb8f578f..547bc7f6a 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -3,12 +3,12 @@ use std::collections::HashSet; -use drawcraft_doc::{Node, NodeId, NodeKind}; use egui::{Color32, Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::json; +use vectorcraft_doc::{Node, NodeId, NodeKind}; use crate::theme::Tokens; -use crate::{DrawcraftApp, icons, widgets}; +use crate::{VectorcraftApp, icons, widgets}; const ROW: f32 = 26.0; @@ -16,7 +16,7 @@ fn expanded_id() -> egui::Id { egui::Id::new("layers-expanded") } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { return }; let doc = st.doc.clone(); @@ -101,7 +101,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { #[allow(clippy::too_many_arguments)] fn row( ui: &mut Ui, - doc: &drawcraft_doc::Document, + doc: &vectorcraft_doc::Document, n: &Node, depth: usize, sel: &HashSet, @@ -275,7 +275,7 @@ fn row( None => (None, 0), }; // A layer dropped on a non-layer goes into that row's container; top level only for layers. - let src_is_layer = doc.node(drawcraft_doc::NodeId(src)).is_some_and(|x| x.is_layer()); + let src_is_layer = doc.node(vectorcraft_doc::NodeId(src)).is_some_and(|x| x.is_layer()); if parent.is_some() || src_is_layer { actions.push(("node.move".into(), json!({"id": src, "parent": parent.map(|p| p.0), "index": index}))); } @@ -292,11 +292,11 @@ fn row( /// A real rendered thumbnail, cached by node identity (unchanged nodes keep their `Arc` /// allocation, so the address is a free change detector). Only rendered for visible rows. -fn real_thumb(ui: &Ui, doc: &drawcraft_doc::Document, n: &Node, r: egui::Rect) -> bool { +fn real_thumb(ui: &Ui, doc: &vectorcraft_doc::Document, n: &Node, r: egui::Rect) -> bool { use std::cell::RefCell; use std::collections::HashMap; thread_local! { - static RENDERER: RefCell = RefCell::new(drawcraft_render::Renderer::new()); + static RENDERER: RefCell = RefCell::new(vectorcraft_render::Renderer::new()); static CACHE: RefCell> = RefCell::new(HashMap::new()); } if !ui.is_rect_visible(r) { diff --git a/crates/ui-egui/src/panels/magic_wand.rs b/crates/ui-egui/src/panels/magic_wand.rs index 45ec6e77d..a6e3b1502 100644 --- a/crates/ui-egui/src/panels/magic_wand.rs +++ b/crates/ui-egui/src/panels/magic_wand.rs @@ -6,15 +6,15 @@ use egui::Ui; use serde_json::{Value, json}; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; -fn options(app: &mut DrawcraftApp) -> Value { +fn options(app: &mut VectorcraftApp) -> Value { app.session.execute("magicWand.options", &json!({})).unwrap_or_default() } /// One row: checkbox + tolerance field. -fn row(app: &mut DrawcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str, tol: Option<(&str, &str, f64)>) { +fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str, tol: Option<(&str, &str, f64)>) { let on = o[flag].as_bool().unwrap_or(false); ui.horizontal(|ui| { ui.set_min_height(26.0); @@ -35,7 +35,7 @@ fn row(app: &mut DrawcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str, }); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let o = options(app); row(app, ui, &o, "Fill Color", "fillColor", Some(("fillTolerance", "", 255.0))); if !pstate::(ui.ctx(), "wand-hide-stroke") { @@ -50,7 +50,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { for (label, key) in [("Stroke Options", "wand-hide-stroke"), ("Transparency Options", "wand-hide-transparency")] { let hidden: bool = pstate(ui.ctx(), key); if menu_item(ui, &format!("{} {label}", if hidden { "Show" } else { "Hide" }), true, false) { @@ -69,7 +69,7 @@ mod tests { #[test] fn panel_and_menu_draw_headless() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap(); for _ in 0..2 { diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 25f761ba0..34e15d602 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -34,27 +34,27 @@ pub mod text_styles; pub mod transform; pub mod transparency; -use drawcraft_color::{Color, Paint}; -use drawcraft_doc::{Node, NodeKind}; use egui::{Rect, Sense, Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_color::{Color, Paint}; +use vectorcraft_doc::{Node, NodeKind}; use crate::theme::Tokens; use crate::widgets::{Live, dim_label}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; /// The first selected node (cloned), if any. -pub fn first_selected(app: &DrawcraftApp) -> Option { +pub fn first_selected(app: &VectorcraftApp) -> Option { let st = app.session.active()?; st.selection.objects.first().and_then(|id| st.doc.node(*id)).cloned() } /// Number of selected objects. -pub(crate) fn selection_len(app: &DrawcraftApp) -> usize { +pub(crate) fn selection_len(app: &VectorcraftApp) -> usize { app.session.active().map(|d| d.selection.len()).unwrap_or(0) } -pub fn show_icon_panel(app: &mut DrawcraftApp, ui: &mut Ui, id: &str) { +pub fn show_icon_panel(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { match id { "swatches" => swatches::show(app, ui), "color" => color::show(app, ui), @@ -92,7 +92,7 @@ pub fn show_icon_panel(app: &mut DrawcraftApp, ui: &mut Ui, id: &str) { } /// Items of a panel's (≡) menu. Unimplemented Illustrator items are listed disabled. -pub fn panel_menu_items(app: &mut DrawcraftApp, ui: &mut Ui, id: &str) { +pub fn panel_menu_items(app: &mut VectorcraftApp, ui: &mut Ui, id: &str) { match id { "swatches" => swatches::menu(app, ui), "color" => color::menu(app, ui), @@ -129,7 +129,7 @@ pub fn panel_menu_items(app: &mut DrawcraftApp, ui: &mut Ui, id: &str) { } /// The ≡ panel-menu button drawn into `rect` (the right end of a panel's title/tab strip). -pub fn panel_menu(app: &mut DrawcraftApp, ui: &mut Ui, id: &str, rect: Rect) { +pub fn panel_menu(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, rect: Rect) { let t = Tokens::get(ui.ctx()); let resp = ui.interact(rect, ui.id().with(("panel-menu", id)), Sense::click()); icons::paint(ui, "menu", rect.shrink(1.0), if resp.hovered() { t.text_strong } else { t.text_dim }); @@ -140,7 +140,7 @@ pub fn panel_menu(app: &mut DrawcraftApp, ui: &mut Ui, id: &str, rect: Rect) { }); } -pub fn libraries(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn libraries(_app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); ui.add_space(20.0); ui.vertical_centered(|ui| { @@ -157,13 +157,13 @@ pub fn libraries(_app: &mut DrawcraftApp, ui: &mut Ui) { // ---------- shared helpers ---------- /// The paint command for the active proxy (Fill or Stroke). -pub(crate) fn paint_target(app: &DrawcraftApp) -> &'static str { +pub(crate) fn paint_target(app: &VectorcraftApp) -> &'static str { if app.session.fill_active { "paint.setFill" } else { "paint.setStroke" } } /// Fill and stroke as the proxies show them: the first selected object's (text uses its first /// run's style), else the defaults for new art. -pub(crate) fn current_paints(app: &DrawcraftApp) -> (Paint, Paint) { +pub(crate) fn current_paints(app: &VectorcraftApp) -> (Paint, Paint) { match first_selected(app) { Some(n) => match &n.kind { NodeKind::Text(t) => { @@ -177,13 +177,13 @@ pub(crate) fn current_paints(app: &DrawcraftApp) -> (Paint, Paint) { } /// The paint behind the active proxy. -pub(crate) fn active_paint(app: &DrawcraftApp) -> Paint { +pub(crate) fn active_paint(app: &VectorcraftApp) -> Paint { let (f, s) = current_paints(app); if app.session.fill_active { f } else { s } } /// Draw the Fill/Stroke proxy and handle its clicks through commands. -pub(crate) fn proxy(app: &mut DrawcraftApp, ui: &mut Ui, size: f32) { +pub(crate) fn proxy(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) { let (f, s) = current_paints(app); let (a, b, swap, def) = crate::widgets::fill_stroke_proxy(ui, &f, &s, app.session.fill_active, size); if (a && !app.session.fill_active) || (b && app.session.fill_active) { @@ -199,7 +199,7 @@ pub(crate) fn proxy(app: &mut DrawcraftApp, ui: &mut Ui, size: f32) { /// Run `cmd` live: while dragging, preview on top of an interaction snapshot; on release, commit /// it as one undo step. Falls back to a plain run when no document is open. -pub(crate) fn live_run(app: &mut DrawcraftApp, label: &str, cmd: &str, params: Value, phase: Live) { +pub(crate) fn live_run(app: &mut VectorcraftApp, label: &str, cmd: &str, params: Value, phase: Live) { match phase { Live::Idle => {} Live::Dragging | Live::Released => { @@ -239,7 +239,7 @@ pub(crate) fn push_recent(ctx: &egui::Context, c: Color) { } /// "Recent Colors" header + a row of chips; clicking one applies it to the active proxy. -pub(crate) fn recent_colors_row(app: &mut DrawcraftApp, ui: &mut Ui) { +pub(crate) fn recent_colors_row(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); crate::widgets::subheader(ui, "Recent Colors"); let recent = recent_colors(ui.ctx()); @@ -314,7 +314,7 @@ mod tests { assert_eq!(paint_params(&Paint::None), json!({"none": true})); let p = paint_params(&Paint::solid(Color::cmyk(0.1, 0.2, 0.3, 0.4))); assert!((p["color"]["k"].as_f64().unwrap() - 0.4).abs() < 1e-6); - let g = Paint::Gradient(Box::new(drawcraft_color::GradientPaint::new(Default::default()))); + let g = Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default()))); let p = paint_params(&g); assert_eq!(p["gradient"]["kind"], "linear"); assert_eq!(p["gradient"]["stops"].as_array().unwrap().len(), 2); diff --git a/crates/ui-egui/src/panels/navigator.rs b/crates/ui-egui/src/panels/navigator.rs index dacf1ebe1..111a16944 100644 --- a/crates/ui-egui/src/panels/navigator.rs +++ b/crates/ui-egui/src/panels/navigator.rs @@ -4,17 +4,17 @@ use std::cell::RefCell; -use drawcraft_geom::{Point, Rect as DRect}; use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::json; +use vectorcraft_geom::{Point, Rect as DRect}; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; struct Cache { - renderer: Option, + renderer: Option, key: Option<(usize, u64, u32, u32, bool)>, tex: Option, } @@ -47,7 +47,7 @@ pub fn slider_to_zoom(t: f64) -> f64 { (3.13f64.ln() + t.clamp(0.0, 1.0) * (64000f64.ln() - 3.13f64.ln())).exp() } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { super::empty_state(ui, "map", "No document", "Open a document to navigate it."); @@ -73,7 +73,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { let mut c = c.borrow_mut(); if c.key != Some(key) || c.tex.is_none() { let s = (pw as f64 / region.width()).min(ph as f64 / region.height()); - let r = c.renderer.get_or_insert_with(drawcraft_render::Renderer::new).render_region(&doc, region, s, false); + let r = c.renderer.get_or_insert_with(vectorcraft_render::Renderer::new).render_region(&doc, region, s, false); let img = egui::ColorImage::from_rgba_premultiplied([r.width as usize, r.height as usize], &r.pixels); match &mut c.tex { Some(tx) => tx.set(img, egui::TextureOptions::LINEAR), @@ -137,7 +137,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(_app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(_app: &mut VectorcraftApp, ui: &mut Ui) { let ab: bool = pstate(ui.ctx(), "nav-ab-only"); if menu_item(ui, "View Artboard Only", true, ab) { set_pstate(ui.ctx(), "nav-ab-only", !ab); diff --git a/crates/ui-egui/src/panels/opentype.rs b/crates/ui-egui/src/panels/opentype.rs index c3874efc0..a73c2e9e8 100644 --- a/crates/ui-egui/src/panels/opentype.rs +++ b/crates/ui-egui/src/panels/opentype.rs @@ -1,12 +1,12 @@ //! OpenType panel: per-character OpenType features (ligatures, alternates, swashes, small caps, //! ordinals, fractions, figure style) for the selected text or the Type tool's selected characters. -use drawcraft_text::OtFeatures; use egui::Ui; use serde_json::json; +use vectorcraft_text::OtFeatures; use super::character::{text_editing, text_style}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; /// A checkbox row: label and the feature flag it toggles. @@ -15,7 +15,7 @@ type Toggle = (&'static str, fn(&mut OtFeatures) -> &mut bool); const FIGURES: [&str; 4] = ["Default Figure", "Tabular Lining", "Proportional Oldstyle", "Tabular Oldstyle"]; /// Apply `f` to the selection's features (the Type tool range, or whole objects). -fn set(app: &mut DrawcraftApp, f: OtFeatures) { +fn set(app: &mut VectorcraftApp, f: OtFeatures) { let tags = f.to_tags(); let r = match text_editing(app) { Some((id, a, b)) if b > a => app.run("text.setRangeStyle", json!({ "id": id.0, "start": a, "end": b, "features": tags })), @@ -27,7 +27,7 @@ fn set(app: &mut DrawcraftApp, f: OtFeatures) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let Some((st, _)) = text_style(app) else { widgets::dim_label(ui, "Select text to set its OpenType features."); return; @@ -71,7 +71,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { if menu_item(ui, "Reset OpenType Features", text_style(app).is_some(), false) { set(app, OtFeatures::default()); } @@ -83,7 +83,7 @@ mod tests { #[test] fn toggles_reach_the_text() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); let id = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "fi 1/2"})).unwrap()["id"].clone(); app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); diff --git a/crates/ui-egui/src/panels/paragraph.rs b/crates/ui-egui/src/panels/paragraph.rs index 4f90c513c..59f7b5ad9 100644 --- a/crates/ui-egui/src/panels/paragraph.rs +++ b/crates/ui-egui/src/panels/paragraph.rs @@ -1,12 +1,12 @@ //! Paragraph panel: seven alignment buttons, indents, space before/after and Hyphenate. -use drawcraft_doc::{Justify, Unit}; use egui::{Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{Justify, Unit}; use super::character::text_style; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -21,11 +21,11 @@ pub const ALIGNMENTS: [(Justify, &str, &str, &str); 7] = [ ]; /// Paragraph attributes apply to the whole text object (ending a Type tool typing session first). -fn format(app: &mut DrawcraftApp, p: Value) { +fn format(app: &mut VectorcraftApp, p: Value) { para_cmd(app, "text.setFormat", p); } -fn para_cmd(app: &mut DrawcraftApp, cmd: &str, mut p: Value) { +fn para_cmd(app: &mut VectorcraftApp, cmd: &str, mut p: Value) { if let Some((id, _, _)) = super::character::text_editing(app) { super::character::end_typing(app); p["ids"] = json!([id.0]); @@ -33,7 +33,7 @@ fn para_cmd(app: &mut DrawcraftApp, cmd: &str, mut p: Value) { app.run(cmd, p).ok(); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some((_, para)) = text_style(app) else { super::empty_state(ui, "pilcrow", "No text selected", "Select a text object to edit its paragraph attributes."); @@ -85,7 +85,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let has = text_style(app).is_some(); let hidden: bool = pstate(ui.ctx(), "pa-hide-options"); if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { diff --git a/crates/ui-egui/src/panels/pathfinder.rs b/crates/ui-egui/src/panels/pathfinder.rs index af5e85945..eab9b1d72 100644 --- a/crates/ui-egui/src/panels/pathfinder.rs +++ b/crates/ui-egui/src/panels/pathfinder.rs @@ -1,10 +1,10 @@ -//! Pathfinder panel: Shape Modes (+ Expand) and Pathfinders, with icons drawn for DrawCraft. +//! Pathfinder panel: Shape Modes (+ Expand) and Pathfinders, with icons drawn for VectorCraft. use egui::Ui; use serde_json::json; use super::{pstate, selection_len, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; pub const SHAPE_MODES: [(&str, &str, &str); 4] = [ @@ -23,13 +23,13 @@ pub const PATHFINDERS: [(&str, &str, &str); 6] = [ ("dc-pf-minus-back", "Minus Back", "minusBack"), ]; -fn run(app: &mut DrawcraftApp, ui: &Ui, op: &str, label: &str) { +fn run(app: &mut VectorcraftApp, ui: &Ui, op: &str, label: &str) { if app.run(&format!("object.pathfinder.{op}"), json!({})).is_ok() { set_pstate(ui.ctx(), "pf-last", Some((op.to_string(), label.to_string()))); } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let n = selection_len(app); let t = crate::theme::Tokens::get(ui.ctx()); widgets::subheader(ui, "Shape Modes:"); @@ -60,7 +60,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { menu_item(ui, "Trap…", false, false); let last: Option<(String, String)> = pstate(ui.ctx(), "pf-last"); let label = last.as_ref().map(|(_, l)| format!("Repeat {l}")).unwrap_or_else(|| "Repeat Pathfinder".into()); diff --git a/crates/ui-egui/src/panels/pattern_options.rs b/crates/ui-egui/src/panels/pattern_options.rs index 912c7b33e..2abb93daa 100644 --- a/crates/ui-egui/src/panels/pattern_options.rs +++ b/crates/ui-egui/src/panels/pattern_options.rs @@ -3,20 +3,20 @@ //! Overlap, Copies, Dim Copies, Show Tile Edge; Save a Copy / Done / Cancel. Outside editing //! mode it lists the document's patterns with an Edit button. -use drawcraft_doc::pattern::{PatternDef, RepeatKind, TileType}; -use drawcraft_doc::{NodeKind, Unit}; use egui::Ui; use serde_json::{Value, json}; +use vectorcraft_doc::pattern::{PatternDef, RepeatKind, TileType}; +use vectorcraft_doc::{NodeKind, Unit}; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::widgets::{self, menu_item}; const TILE_LABELS: [&str; 5] = ["Grid", "Brick by Row", "Brick by Column", "Hex by Column", "Hex by Row"]; const BRICK: [(&str, f64); 4] = [("1/2", 0.5), ("1/3", 1.0 / 3.0), ("1/4", 0.25), ("1/5", 0.2)]; const COPIES: [(&str, u32); 4] = [("3 x 3", 3), ("5 x 5", 5), ("7 x 7", 7), ("9 x 9", 9)]; -fn editing(app: &DrawcraftApp) -> Option<(PatternDef, Unit)> { +fn editing(app: &VectorcraftApp) -> Option<(PatternDef, Unit)> { let st = app.session.active()?; let pe = st.doc.pattern_edit.as_ref()?; Some((st.doc.pattern(&pe.pattern)?.clone(), st.doc.units)) @@ -31,14 +31,14 @@ fn row(ui: &mut Ui, label: &str, add: impl FnOnce(&mut Ui)) { }); } -fn set(app: &mut DrawcraftApp, name: &str, mut p: Value) { +fn set(app: &mut VectorcraftApp, name: &str, mut p: Value) { p["name"] = json!(name); if let Err(e) = app.run("pattern.options", p) { app.status(e); } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if app.session.active().is_none() { super::empty_state(ui, "swatch-book", "No document", "Open a document to edit its patterns."); return; @@ -157,7 +157,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } /// Not editing: the document's patterns, one to pick and edit. -fn idle(app: &mut DrawcraftApp, ui: &mut Ui) { +fn idle(app: &mut VectorcraftApp, ui: &mut Ui) { let names: Vec = app.session.active().map(|st| st.doc.patterns.iter().map(|p| p.name.clone()).collect()).unwrap_or_default(); widgets::dim_label(ui, "Pattern options are available in pattern editing mode."); ui.add_space(4.0); @@ -182,7 +182,7 @@ fn idle(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let editing = editing(app).is_some(); if menu_item(ui, "Save a Copy", editing, false) { app.run("object.pattern.saveCopy", json!({})).ok(); @@ -196,7 +196,7 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { } /// Current values of the selected repeat as Repeat Options dialog fields. -pub fn repeat_fields(app: &DrawcraftApp) -> Option { +pub fn repeat_fields(app: &VectorcraftApp) -> Option { let st = app.session.active()?; let spec = st.selection.objects.iter().find_map(|id| { st.doc.ancestry(*id)?.into_iter().rev().find_map(|a| match &st.doc.node(a)?.kind { diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 2595e9da1..abf796230 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -1,15 +1,15 @@ //! The Properties panel: context-sensitive sections like Illustrator's. -use drawcraft_doc::{NodeKind, Unit}; use egui::{Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::json; +use vectorcraft_doc::{NodeKind, Unit}; use super::first_selected; use crate::theme::Tokens; use crate::widgets::{self, dim_label, divider, section_header}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { dim_label(ui, "No document open"); @@ -106,7 +106,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -fn document_sections(app: &mut DrawcraftApp, ui: &mut Ui) { +fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let units = app.session.active().map(|d| d.doc.units).unwrap_or_default(); section_header(ui, "Document"); ui.horizontal(|ui| { @@ -185,7 +185,7 @@ fn document_sections(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn transform_section(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(st) = app.session.active() else { return }; let units = st.doc.units; let Some(b) = st.doc.bounds_of(&st.selection.objects, false) else { @@ -193,7 +193,7 @@ pub fn transform_section(app: &mut DrawcraftApp, ui: &mut Ui) { return; }; let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4); - let rp = drawcraft_geom::reference_point(b, refi); + let rp = vectorcraft_geom::reference_point(b, refi); section_header(ui, "Transform"); ui.horizontal(|ui| { if let Some(i) = widgets::reference_point(ui, refi) { @@ -240,7 +240,7 @@ pub fn transform_section(app: &mut DrawcraftApp, ui: &mut Ui) { && let NodeKind::Path { live: Some(live), .. } = &n.kind { match live { - drawcraft_doc::LiveShape::Rectangle { radii, .. } => { + vectorcraft_doc::LiveShape::Rectangle { radii, .. } => { ui.horizontal(|ui| { dim_label(ui, "Corner Radius:"); if let Some(r) = widgets::num_field(ui, "radius", Some(radii[0]), units, 80.0) { @@ -248,7 +248,7 @@ pub fn transform_section(app: &mut DrawcraftApp, ui: &mut Ui) { } }); } - drawcraft_doc::LiveShape::Polygon { sides, .. } => { + vectorcraft_doc::LiveShape::Polygon { sides, .. } => { ui.horizontal(|ui| { dim_label(ui, "Sides:"); if let Some(s) = widgets::plain_field(ui, "sides", *sides as f64, "", 0, 60.0) { @@ -261,7 +261,7 @@ pub fn transform_section(app: &mut DrawcraftApp, ui: &mut Ui) { } } -fn appearance_section(app: &mut DrawcraftApp, ui: &mut Ui) { +fn appearance_section(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(n) = first_selected(app) else { return }; section_header(ui, "Appearance"); @@ -316,7 +316,7 @@ fn appearance_section(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn type_sections(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) { let Some(n) = first_selected(app) else { dim_label(ui, "Select a text object"); return; @@ -327,7 +327,7 @@ pub fn type_sections(app: &mut DrawcraftApp, ui: &mut Ui) { }; let s = tx.first_style(); section_header(ui, "Character"); - let fams = drawcraft_text::FontDb::global().families(); + let fams = vectorcraft_text::FontDb::global().families(); let names: Vec<&str> = fams.iter().map(String::as_str).collect(); if let Some(i) = widgets::dropdown(ui, "font", &s.font_family, &names, ui.available_width() - 4.0) { app.run("text.setStyle", json!({"font": names[i]})).ok(); diff --git a/crates/ui-egui/src/panels/separations.rs b/crates/ui-egui/src/panels/separations.rs index 1244906e0..96f4eb1c7 100644 --- a/crates/ui-egui/src/panels/separations.rs +++ b/crates/ui-egui/src/panels/separations.rs @@ -3,12 +3,12 @@ //! colour-management controls behind Edit → Color Settings… / Assign Profile… and View → Proof //! Setup (working spaces, intent, black-point compensation, proof target, gamut check). -use drawcraft_color::cms::{self, Intent, ProfileKind}; -use drawcraft_render::proof; use egui::{Sense, Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_color::cms::{self, Intent, ProfileKind}; +use vectorcraft_render::proof; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, dim_label, menu_item}; @@ -21,13 +21,13 @@ const PROOF_TARGETS: [(&str, &str); 6] = [ ("deuteranopia", "Color blindness – Deuteranopia-type"), ]; -fn run(app: &mut DrawcraftApp, id: &str, p: Value) { +fn run(app: &mut VectorcraftApp, id: &str, p: Value) { if let Err(e) = app.run(id, p) { app.ui.status = e; } } -fn plates_section(app: &mut DrawcraftApp, ui: &mut Ui) { +fn plates_section(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let v = proof::view(); let on = v.separations.is_some(); @@ -80,7 +80,7 @@ fn profile_dropdown(ui: &mut Ui, id: &str, current: &str, kind: ProfileKind, ext widgets::dropdown(ui, id, current, &refs, 210.0).map(|i| names[i].clone()) } -fn settings_section(app: &mut DrawcraftApp, ui: &mut Ui) { +fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) { let st = cms::active_settings(); widgets::section_header(ui, "Color Settings"); ui.horizontal(|ui| { @@ -123,7 +123,7 @@ fn settings_section(app: &mut DrawcraftApp, ui: &mut Ui) { } }); if let Some(d) = app.session.active() { - let (_, cmyk) = drawcraft_engine::cmd::colormgmt::doc_profiles(&d.doc); + let (_, cmyk) = vectorcraft_engine::cmd::colormgmt::doc_profiles(&d.doc); const WORKING: &str = "Working CMYK (don't tag)"; ui.horizontal(|ui| { ui.label("Assign:"); @@ -135,7 +135,7 @@ fn settings_section(app: &mut DrawcraftApp, ui: &mut Ui) { } } -fn proof_section(app: &mut DrawcraftApp, ui: &mut Ui) { +fn proof_section(app: &mut VectorcraftApp, ui: &mut Ui) { let v = proof::view(); widgets::section_header(ui, "Proof Setup"); let cur = v.setup.target.id(); @@ -157,7 +157,7 @@ fn proof_section(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { plates_section(app, ui); widgets::divider(ui); proof_section(app, ui); @@ -165,7 +165,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { settings_section(app, ui); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let v = proof::view(); if menu_item(ui, "Overprint Preview", true, v.overprint) { run(app, "view.overprintPreview", json!({})); diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index b5926bbc1..a0c932a5f 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -1,14 +1,14 @@ //! Stroke panel: weight spinner + presets, cap / corner / align toggles, miter limit, dashed line //! with three dash/gap pairs, arrowheads with drawn previews, arrow scale, width profiles. -use drawcraft_doc::{Arrowhead, LineCap, LineJoin, StrokeAlign, StrokeLayer, Unit, WidthProfile}; use egui::{Color32, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{Arrowhead, LineCap, LineJoin, StrokeAlign, StrokeLayer, Unit, WidthProfile}; use super::{first_selected, pstate, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; pub const WEIGHT_PRESETS: [f64; 22] = [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]; @@ -72,15 +72,15 @@ fn arrow_label(a: Option) -> String { } } -fn stroke_now(app: &DrawcraftApp) -> Option { +fn stroke_now(app: &VectorcraftApp) -> Option { first_selected(app).and_then(|n| n.appearance.stroke().cloned()) } -fn set(app: &mut DrawcraftApp, p: Value) { +fn set(app: &mut VectorcraftApp, p: Value) { app.run("stroke.set", p).ok(); } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let st = stroke_now(app); let weight = st.as_ref().map(|s| s.width).unwrap_or(app.session.paint.stroke_width); @@ -383,7 +383,7 @@ fn profile_dropdown(ui: &mut Ui, cur: &str) -> Option<&'static str> { out } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "stroke-hide-options"); if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { set_pstate(ui.ctx(), "stroke-hide-options", !hidden); diff --git a/crates/ui-egui/src/panels/swatches.rs b/crates/ui-egui/src/panels/swatches.rs index 3959f4016..ab9165de3 100644 --- a/crates/ui-egui/src/panels/swatches.rs +++ b/crates/ui-egui/src/panels/swatches.rs @@ -1,14 +1,14 @@ //! Swatches panel: proxy, Recent Colors, thumbnail grid (15.5 pt tiles on a 17 pt pitch) or list, //! colour groups as folders, None/Registration first, bottom bar and panel menu. -use drawcraft_color::Paint; use egui::{Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::json; +use vectorcraft_color::Paint; use super::{active_paint, paint_target, pstate, push_recent, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, menu_item, swatch_tile}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; /// View modes of the swatch list. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] @@ -85,11 +85,11 @@ enum Entry { const REGISTRATION: &str = "[Registration]"; -fn entries(app: &DrawcraftApp, kind: Kind) -> Vec { +fn entries(app: &VectorcraftApp, kind: Kind) -> Vec { let Some(st) = app.session.active() else { return vec![] }; let d = &st.doc; let mut out = vec![]; - let sw = |s: &drawcraft_color::Swatch| Entry::Swatch { name: s.name.clone(), paint: s.paint.clone(), global: s.global, spot: s.spot }; + let sw = |s: &vectorcraft_color::Swatch| Entry::Swatch { name: s.name.clone(), paint: s.paint.clone(), global: s.global, spot: s.spot }; // None first, then Registration, then the rest (Illustrator's order). let (specials, rest): (Vec<_>, Vec<_>) = d.swatches.iter().partition(|s| s.paint.is_none()); for s in specials.iter().filter(|s| kind.accepts(&s.paint, false)) { @@ -112,7 +112,7 @@ fn entries(app: &DrawcraftApp, kind: Kind) -> Vec { out } -fn apply(app: &mut DrawcraftApp, ui: &Ui, e: &Entry) { +fn apply(app: &mut VectorcraftApp, ui: &Ui, e: &Entry) { let target = paint_target(app); match e { Entry::Registration => { @@ -131,7 +131,7 @@ fn apply(app: &mut DrawcraftApp, ui: &Ui, e: &Entry) { } /// A pattern swatch drawn as a rendered tile (cached by the definition's identity and size). -fn pattern_thumb(app: &DrawcraftApp, ui: &Ui, r: Rect, paint: &Paint) { +fn pattern_thumb(app: &VectorcraftApp, ui: &Ui, r: Rect, paint: &Paint) { use std::cell::RefCell; use std::collections::HashMap; type Key = (String, Vec, String, u32); @@ -149,7 +149,7 @@ fn pattern_thumb(app: &DrawcraftApp, ui: &Ui, r: Rect, paint: &Paint) { px, ); let tex = CACHE.with(|c| c.borrow().get(&key).cloned()).unwrap_or_else(|| { - let tex = drawcraft_render::render_pattern_swatch(&st.doc, pattern, px).map(|img| { + let tex = vectorcraft_render::render_pattern_swatch(&st.doc, pattern, px).map(|img| { let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); ui.ctx().load_texture(format!("pattern-swatch-{pattern}-{px}"), color, egui::TextureOptions::LINEAR) }); @@ -189,7 +189,7 @@ fn selected_name(ui: &Ui) -> Option { pstate::>(ui.ctx(), "swatch-selected") } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); if app.session.active().is_none() { super::empty_state(ui, "swatch-book", "No document", "Open a document to see its swatches."); @@ -352,7 +352,7 @@ fn deletable(ui: &Ui) -> Option { selected_name(ui).filter(|n| !n.starts_with('[')) } -fn bottom(app: &mut DrawcraftApp, ui: &mut Ui) { +fn bottom(app: &mut VectorcraftApp, ui: &mut Ui) { let kind: Kind = pstate(ui.ctx(), "swatch-kind"); widgets::bottom_bar(ui, |ui| { widgets::icon_button_enabled(ui, "library", "Swatch Libraries (on the roadmap)", false, false, 24.0); @@ -383,7 +383,7 @@ fn bottom(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -fn new_swatch(app: &mut DrawcraftApp) { +fn new_swatch(app: &mut VectorcraftApp) { let p = active_paint(app); let params = match &p { Paint::None => json!({}), @@ -392,7 +392,7 @@ fn new_swatch(app: &mut DrawcraftApp) { app.run("swatch.new", params).ok(); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let view: View = pstate(ui.ctx(), "swatch-view"); let sel = deletable(ui); if menu_item(ui, "New Swatch…", true, false) { @@ -437,12 +437,12 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { #[cfg(test)] mod tests { use super::*; - use drawcraft_color::Color; + use vectorcraft_color::Color; #[test] fn kind_filter() { let solid = Paint::solid(Color::BLACK); - let grad = Paint::Gradient(Box::new(drawcraft_color::GradientPaint::new(Default::default()))); + let grad = Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default()))); assert!(Kind::All.accepts(&grad, false)); assert!(Kind::Color.accepts(&solid, false)); assert!(!Kind::Color.accepts(&grad, false)); diff --git a/crates/ui-egui/src/panels/symbols.rs b/crates/ui-egui/src/panels/symbols.rs index 566b970ca..486fb20e6 100644 --- a/crates/ui-egui/src/panels/symbols.rs +++ b/crates/ui-egui/src/panels/symbols.rs @@ -4,19 +4,19 @@ use std::cell::RefCell; use std::collections::HashMap; -use drawcraft_doc::{Document, Node, NodeKind}; use egui::{Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::{Value, json}; +use vectorcraft_doc::{Document, Node, NodeKind}; use super::{pstate, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; /// A thumbnail of symbol `name` (an instance at its natural size), cached by the art's identity. fn thumb(ui: &Ui, doc: &Document, name: &str, size: f32) -> Option { thread_local! { - static RENDERER: RefCell = RefCell::new(drawcraft_render::Renderer::new()); + static RENDERER: RefCell = RefCell::new(vectorcraft_render::Renderer::new()); static CACHE: RefCell> = RefCell::new(HashMap::new()); } let sym = doc.symbols.iter().find(|s| s.name == name)?; @@ -34,7 +34,7 @@ fn thumb(ui: &Ui, doc: &Document, name: &str, size: f32) -> Option Option Option { +fn current(app: &mut VectorcraftApp) -> Option { app.run("symbol.list", json!({})).ok().and_then(|v| v["current"].as_str().map(str::to_string)) } -fn has_instances_selected(app: &DrawcraftApp) -> bool { +fn has_instances_selected(app: &VectorcraftApp) -> bool { app.session.active().is_some_and(|st| { st.selection.objects.iter().any(|id| { let mut any = false; @@ -66,7 +66,7 @@ fn has_instances_selected(app: &DrawcraftApp) -> bool { }) } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(doc) = app.session.active().map(|d| d.doc.clone()) else { return }; let names: Vec = doc.symbols.iter().map(|s| s.name.clone()).collect(); @@ -152,7 +152,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let sel = current(app); let has_sel = app.session.active().is_some_and(|d| !d.selection.is_empty()); let inst = has_instances_selected(app); diff --git a/crates/ui-egui/src/panels/text_styles.rs b/crates/ui-egui/src/panels/text_styles.rs index 0f8766885..972987a2b 100644 --- a/crates/ui-egui/src/panels/text_styles.rs +++ b/crates/ui-egui/src/panels/text_styles.rs @@ -9,7 +9,7 @@ use serde_json::{Map, Value, json}; use super::character::{text_editing, text_style}; use super::{pstate, selection_len, set_pstate}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; @@ -32,7 +32,7 @@ impl Kind { } /// (name, attrs, uses) for every style, Normal first. -fn styles(app: &mut DrawcraftApp, kind: Kind) -> Vec<(String, Map, u64)> { +fn styles(app: &mut VectorcraftApp, kind: Kind) -> Vec<(String, Map, u64)> { let v = app.session.execute(&format!("{}.list", kind.prefix()), &json!({})).unwrap_or_default(); v["styles"] .as_array() @@ -51,7 +51,7 @@ fn styles(app: &mut DrawcraftApp, kind: Kind) -> Vec<(String, Map } /// The selection's style name (Normal when none) and its attributes, if text is selected. -fn current(app: &DrawcraftApp, kind: Kind) -> Option<(Option, Map)> { +fn current(app: &VectorcraftApp, kind: Kind) -> Option<(Option, Map)> { let (c, p) = text_style(app)?; let v = match kind { Kind::Char => serde_json::to_value(&c), @@ -64,7 +64,7 @@ fn current(app: &DrawcraftApp, kind: Kind) -> Option<(Option, Map Value { +fn target(app: &VectorcraftApp, kind: Kind, mut p: Value) -> Value { if let Some((id, a, b)) = text_editing(app) { p["id"] = json!(id.0); if kind == Kind::Char && b > a { @@ -75,7 +75,7 @@ fn target(app: &DrawcraftApp, kind: Kind, mut p: Value) -> Value { p } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui, kind: Kind) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let t = Tokens::get(ui.ctx()); let all = styles(app, kind); let cur = current(app, kind); @@ -127,7 +127,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui, kind: Kind) { }); } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui, kind: Kind) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui, kind: Kind) { let sel: Option = pstate(ui.ctx(), kind.key()); let normal = if kind == Kind::Char { "[Normal Character Style]" } else { "[Normal Paragraph Style]" }; let user = sel.as_ref().is_some_and(|s| s != normal); @@ -171,7 +171,7 @@ mod tests { #[test] fn panels_draw_and_apply_headless() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); let id = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap()["id"].clone(); app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); diff --git a/crates/ui-egui/src/panels/transform.rs b/crates/ui-egui/src/panels/transform.rs index 982c98ecf..6a91c42b7 100644 --- a/crates/ui-egui/src/panels/transform.rs +++ b/crates/ui-egui/src/panels/transform.rs @@ -1,14 +1,14 @@ //! Transform panel: reference point, X/Y/W/H with the constrain link, rotate and shear, flips, //! live-shape properties and the Scale Corners / Scale Strokes & Effects options. -use drawcraft_doc::NodeKind; use egui::Ui; use serde_json::json; +use vectorcraft_doc::NodeKind; use super::{first_selected, pstate, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; pub const ANGLE_PRESETS: [f64; 9] = [-180.0, -135.0, -90.0, -45.0, 0.0, 45.0, 90.0, 135.0, 180.0]; @@ -21,7 +21,7 @@ pub fn constrained(w: f64, h: f64, new_w: Option, new_h: Option) -> (f } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let Some(st) = app.session.active() else { widgets::dim_label(ui, "No document"); @@ -32,7 +32,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4); let link: bool = pstate(ui.ctx(), "xf-link"); let has = bounds.is_some(); - let rp = bounds.map(|b| drawcraft_geom::reference_point(b, refi)); + let rp = bounds.map(|b| vectorcraft_geom::reference_point(b, refi)); ui.horizontal(|ui| { if let Some(i) = widgets::reference_point(ui, refi) { ui.data_mut(|d| d.insert_temp(egui::Id::new("refpt"), i)); @@ -98,7 +98,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { { widgets::divider(ui); match live { - drawcraft_doc::LiveShape::Rectangle { radii, .. } => { + vectorcraft_doc::LiveShape::Rectangle { radii, .. } => { widgets::subheader(ui, "Rectangle Properties:"); ui.horizontal(|ui| { widgets::dim_label(ui, "Corner Radius:"); @@ -107,7 +107,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } }); } - drawcraft_doc::LiveShape::Polygon { sides, .. } => { + vectorcraft_doc::LiveShape::Polygon { sides, .. } => { widgets::subheader(ui, "Polygon Properties:"); ui.horizontal(|ui| { widgets::dim_label(ui, "Sides:"); @@ -133,7 +133,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hidden: bool = pstate(ui.ctx(), "xf-hide-options"); let has = super::selection_len(app) > 0; if menu_item(ui, if hidden { "Show Options" } else { "Hide Options" }, true, false) { diff --git a/crates/ui-egui/src/panels/transparency.rs b/crates/ui-egui/src/panels/transparency.rs index 61f1c8146..3095fd8c1 100644 --- a/crates/ui-egui/src/panels/transparency.rs +++ b/crates/ui-egui/src/panels/transparency.rs @@ -2,16 +2,16 @@ //! opacity-mask controls (make/release, link, clip, invert; Shift-click the mask to disable it), //! Isolate Blending and Knockout Group. -use drawcraft_color::{BlendMode, Paint}; use egui::{Sense, Stroke, StrokeKind, Ui, vec2}; use serde_json::json; +use vectorcraft_color::{BlendMode, Paint}; use super::{current_paints, first_selected, live_run, pstate, selection_len, set_pstate}; use crate::theme::Tokens; use crate::widgets::{self, Live, menu_item}; -use crate::{DrawcraftApp, icons}; +use crate::{VectorcraftApp, icons}; -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let n = first_selected(app); let has = n.is_some(); @@ -139,7 +139,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { } } -pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { let hide_thumbs: bool = pstate(ui.ctx(), "tr-hide-thumbs"); let hide_opts: bool = pstate(ui.ctx(), "tr-hide-options"); if menu_item(ui, if hide_thumbs { "Show Thumbnails" } else { "Hide Thumbnails" }, true, false) { @@ -193,10 +193,10 @@ pub fn menu(app: &mut DrawcraftApp, ui: &mut Ui) { /// A rendered thumbnail of `n` (which may live outside the tree, like mask art). One texture per /// slot, re-rendered only when the document revision, object or size changes. -fn node_thumb(app: &DrawcraftApp, ui: &Ui, slot: &str, n: &drawcraft_doc::Node, r: egui::Rect, bg: Option<[u8; 4]>) -> bool { +fn node_thumb(app: &VectorcraftApp, ui: &Ui, slot: &str, n: &vectorcraft_doc::Node, r: egui::Rect, bg: Option<[u8; 4]>) -> bool { use std::cell::RefCell; thread_local! { - static RENDERER: RefCell = RefCell::new(drawcraft_render::Renderer::new()); + static RENDERER: RefCell = RefCell::new(vectorcraft_render::Renderer::new()); } let Some(st) = app.session.active() else { return false }; let px = (r.width().min(r.height()) * ui.ctx().pixels_per_point()).round().max(8.0) as u32; @@ -221,10 +221,10 @@ fn node_thumb(app: &DrawcraftApp, ui: &Ui, slot: &str, n: &drawcraft_doc::Node, #[cfg(test)] mod tests { use super::*; - use drawcraft_engine::Session; + use vectorcraft_engine::Session; /// Run the panel and its ≡ menu for one headless frame. - fn frame(app: &mut DrawcraftApp) { + fn frame(app: &mut VectorcraftApp) { let ctx = egui::Context::default(); let mut out = ctx.run_ui(egui::RawInput::default(), |ui| { show(app, ui); @@ -235,8 +235,8 @@ mod tests { #[test] fn panel_draws_with_and_without_a_mask() { - let mut app = DrawcraftApp::new(Session::new(), Default::default()); - let r = |app: &mut DrawcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let r = |app: &mut VectorcraftApp, id: &str, p: serde_json::Value| app.session.execute(id, &p).unwrap(); r(&mut app, "file.new", json!({"width": 100, "height": 100})); frame(&mut app); let a = r(&mut app, "shape.rectangle", json!({"x": 0, "y": 0, "width": 50, "height": 50}))["id"].clone(); diff --git a/crates/ui-egui/src/prefs_dialog.rs b/crates/ui-egui/src/prefs_dialog.rs index 365302ce7..f571972a3 100644 --- a/crates/ui-egui/src/prefs_dialog.rs +++ b/crates/ui-egui/src/prefs_dialog.rs @@ -4,13 +4,13 @@ //! through `prefs.set`. Also: persistence of engine prefs inside `UiState` and the per-frame //! application of the UI-side preferences (brightness, canvas colour, UI scaling, render threads). -use drawcraft_engine::Prefs; -use drawcraft_engine::cmd::prefscmds::{PREF_CATEGORIES, PREF_SPECS, PrefKind}; use serde_json::{Map, Value, json}; +use vectorcraft_engine::Prefs; +use vectorcraft_engine::cmd::prefscmds::{PREF_CATEGORIES, PREF_SPECS, PrefKind}; use crate::state::Dialog; use crate::theme::{self, Brightness, Tokens}; -use crate::{DrawcraftApp, widgets}; +use crate::{VectorcraftApp, widgets}; /// UI-only fields shown in the dialog (backed by `UiState`, not engine prefs). const UI_FIELDS: &[(&str, &str, &str)] = &[ @@ -19,7 +19,7 @@ const UI_FIELDS: &[(&str, &str, &str)] = &[ ("__taskBar", "General", "Enable Contextual Task Bar"), ]; -pub fn open(app: &mut DrawcraftApp, category: Option<&str>) { +pub fn open(app: &mut VectorcraftApp, category: Option<&str>) { let mut fields: Map = app.session.prefs.to_json().as_object().cloned().unwrap_or_default(); let cat = category.and_then(|c| PREF_CATEGORIES.iter().find(|x| x.eq_ignore_ascii_case(c))).copied().unwrap_or(PREF_CATEGORIES[0]); fields.insert("__category".into(), json!(cat)); @@ -30,7 +30,7 @@ pub fn open(app: &mut DrawcraftApp, category: Option<&str>) { } /// OK: validate and apply every preference in the dialog (nothing changes on error). -pub fn confirm(app: &mut DrawcraftApp) -> Result { +pub fn confirm(app: &mut VectorcraftApp) -> Result { let Some(d) = app.ui.dialog.clone() else { return Err("no dialog open".into()) }; let values: Map = d.fields.iter().filter(|(k, _)| !k.starts_with("__")).map(|(k, v)| (k.clone(), v.clone())).collect(); app.run("prefs.set", json!({ "values": values }))?; @@ -43,12 +43,12 @@ pub fn confirm(app: &mut DrawcraftApp) -> Result { } /// Copy the engine prefs into `UiState` so the host persists them with the UI state. -pub fn snapshot(app: &mut DrawcraftApp) { +pub fn snapshot(app: &mut VectorcraftApp) { app.ui.engine_prefs = app.session.prefs.to_json(); } /// Restore engine prefs from `UiState` after the host loaded saved UI state. -pub fn restore(app: &mut DrawcraftApp) { +pub fn restore(app: &mut VectorcraftApp) { let mut p: Prefs = serde_json::from_value(app.ui.engine_prefs.clone()).unwrap_or_default(); if app.ui.engine_prefs.is_null() { // Older preference files: keep the saved brightness. @@ -65,7 +65,7 @@ struct Applied { } /// Per frame: push UI-side preferences into egui / the renderer when they change. -pub fn apply_runtime(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn apply_runtime(app: &mut VectorcraftApp, ctx: &egui::Context) { let p = &app.session.prefs; if let Some(b) = Brightness::parse(&p.ui_brightness) && b != app.ui.brightness @@ -83,8 +83,8 @@ pub fn apply_runtime(app: &mut DrawcraftApp, ctx: &egui::Context) { ctx.data_mut(|d| d.insert_temp(egui::Id::NULL, t)); } if prev.is_some_and(|pr| pr.threads != want.threads) { - drawcraft_render::set_default_threads(u16::try_from(want.threads).ok()); - app.canvas.renderer.threads = drawcraft_render::default_threads(); + vectorcraft_render::set_default_threads(u16::try_from(want.threads).ok()); + app.canvas.renderer.threads = vectorcraft_render::default_threads(); app.canvas.worker = None; app.canvas.worker_started = false; } @@ -97,7 +97,7 @@ pub fn apply_runtime(app: &mut DrawcraftApp, ctx: &egui::Context) { } } -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(mut d) = app.ui.dialog.clone() else { return }; let t = Tokens::get(ctx); let (mut ok, mut cancel, mut reset) = (false, false, false); @@ -258,7 +258,7 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) { PrefKind::Color => { labeled(ui, sp.label, |ui| { let hex = v.as_str().unwrap_or("#000000"); - let c = drawcraft_color::Color::from_hex(hex).map(|c| c.to_rgba8(1.0)).unwrap_or([0, 0, 0, 255]); + let c = vectorcraft_color::Color::from_hex(hex).map(|c| c.to_rgba8(1.0)).unwrap_or([0, 0, 0, 255]); let mut rgb = [c[0], c[1], c[2]]; if ui.color_edit_button_srgb(&mut rgb).changed() { d.fields.insert(sp.key.into(), json!(format!("#{:02x}{:02x}{:02x}", rgb[0], rgb[1], rgb[2]))); @@ -300,10 +300,10 @@ fn labeled(ui: &mut egui::Ui, label: &str, add: impl FnOnce(&mut egui::Ui)) { mod tests { use super::*; use crate::Services; - use drawcraft_engine::Session; + use vectorcraft_engine::Session; - fn app() -> DrawcraftApp { - DrawcraftApp::new(Session::new(), Services::default()) + fn app() -> VectorcraftApp { + VectorcraftApp::new(Session::new(), Services::default()) } #[test] diff --git a/crates/ui-egui/src/render_worker.rs b/crates/ui-egui/src/render_worker.rs index 97d40b993..1a3778336 100644 --- a/crates/ui-egui/src/render_worker.rs +++ b/crates/ui-egui/src/render_worker.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use std::sync::mpsc::{Receiver, Sender, channel}; -use drawcraft_doc::Document; -use drawcraft_geom::Affine; -use drawcraft_render::{RenderOptions, Rendered, Renderer}; +use vectorcraft_doc::Document; +use vectorcraft_geom::Affine; +use vectorcraft_render::{RenderOptions, Rendered, Renderer}; use crate::CacheKey; @@ -41,7 +41,7 @@ impl Worker { let (tx, jobs) = channel::(); let (done_tx, rx) = channel::(); std::thread::Builder::new() - .name("drawcraft-render".into()) + .name("vectorcraft-render".into()) .spawn(move || { let mut r = Renderer::new(); while let Ok(mut job) = jobs.recv() { diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index efc009334..6f9381cae 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -15,9 +15,9 @@ use serde_json::{Value, json}; use crate::state::{Dialog, UiState}; use crate::theme::{self, Tokens}; -use crate::{DrawcraftApp, menus, widgets}; +use crate::{VectorcraftApp, menus, widgets}; -pub const PRESETS: &[&str] = &["DrawCraft Defaults", "Illustrator Defaults"]; +pub const PRESETS: &[&str] = &["VectorCraft Defaults", "Illustrator Defaults"]; pub const CUSTOM: &str = "Custom"; /// Bumped whenever overrides or the workspace list change (the native menu rebuilds on it). @@ -61,7 +61,7 @@ fn global_override(key: &str) -> Option<&'static str> { /// Registry default shortcut of a command (engine or UI command). pub fn default_command_shortcut(id: &str) -> Option<&'static str> { - if let Some(c) = drawcraft_engine::find_command(id) { + if let Some(c) = vectorcraft_engine::find_command(id) { return c.shortcut; } menus::UI_COMMANDS.iter().find(|c| c.0 == id).map(|c| c.2).filter(|s| !s.is_empty()) @@ -70,7 +70,7 @@ pub fn default_command_shortcut(id: &str) -> Option<&'static str> { /// Default shortcut of an entry key (`tool:` or a command id). pub fn default_of(key: &str) -> Option<&'static str> { match key.strip_prefix("tool:") { - Some(t) => drawcraft_tools::tool_info(t).and_then(|t| t.shortcut), + Some(t) => vectorcraft_tools::tool_info(t).and_then(|t| t.shortcut), None => default_command_shortcut(key), } } @@ -98,21 +98,21 @@ pub fn tool_shortcut(tool: &str) -> Option<&'static str> { match global_override(&format!("tool:{tool}")) { Some("") => None, Some(s) => Some(s), - None => drawcraft_tools::tool_info(tool).and_then(|t| t.shortcut), + None => vectorcraft_tools::tool_info(tool).and_then(|t| t.shortcut), } } /// Tool whose (effective) shortcut is `key` ("V", "Shift+M"). pub fn tool_for_key(key: &str) -> Option<&'static str> { let n = normalize(key)?; - drawcraft_tools::catalog::all_tools().find(|t| tool_shortcut(t.id).and_then(normalize).as_deref() == Some(n.as_str())).map(|t| t.id) + vectorcraft_tools::catalog::all_tools().find(|t| tool_shortcut(t.id).and_then(normalize).as_deref() == Some(n.as_str())).map(|t| t.id) } /// Command whose effective shortcut is exactly `key` (for single-key command shortcuts: X, D, /). pub fn command_for_key(key: &str) -> Option<&'static str> { let n = normalize(key)?; let hit = |id: &'static str| command_shortcut(id).and_then(normalize).as_deref() == Some(n.as_str()); - drawcraft_engine::command_specs().iter().map(|c| c.id).chain(menus::UI_COMMANDS.iter().map(|c| c.0)).find(|id| hit(id)) + vectorcraft_engine::command_specs().iter().map(|c| c.id).chain(menus::UI_COMMANDS.iter().map(|c| c.0)).find(|id| hit(id)) } // ---------- chords ---------- @@ -183,11 +183,11 @@ pub struct Entry { pub fn entries() -> &'static [Entry] { static E: OnceLock> = OnceLock::new(); E.get_or_init(|| { - let mut v: Vec = drawcraft_tools::catalog::all_tools() + let mut v: Vec = vectorcraft_tools::catalog::all_tools() .map(|t| Entry { key: format!("tool:{}", t.id), label: t.label.to_string(), group: "Tools".into(), is_tool: true }) .collect(); let mut seen = HashSet::new(); - for c in drawcraft_engine::command_specs() { + for c in vectorcraft_engine::command_specs() { if (c.menu.is_empty() && c.shortcut.is_none()) || !seen.insert(c.id) { continue; } @@ -284,13 +284,13 @@ pub fn preset(name: &str) -> Option> { PRESETS.contains(&name).then(BTreeMap::new) } -/// Export format: `{"format": "drawcraft-shortcuts", "set": name, "overrides": {...}}`. +/// Export format: `{"format": "vectorcraft-shortcuts", "set": name, "overrides": {...}}`. pub fn export_json(set: &str, overrides: &BTreeMap) -> Value { - json!({"format": "drawcraft-shortcuts", "version": 1, "set": set, "overrides": overrides}) + json!({"format": "vectorcraft-shortcuts", "version": 1, "set": set, "overrides": overrides}) } pub fn import_json(v: &Value) -> Result<(String, BTreeMap), String> { - let o = v.get("overrides").and_then(Value::as_object).ok_or("not a DrawCraft shortcut set (missing `overrides`)")?; + let o = v.get("overrides").and_then(Value::as_object).ok_or("not a VectorCraft shortcut set (missing `overrides`)")?; let mut out = BTreeMap::new(); for (k, v) in o { let s = v.as_str().ok_or_else(|| format!("shortcut for `{k}` must be a string"))?; @@ -304,7 +304,7 @@ pub fn import_json(v: &Value) -> Result<(String, BTreeMap), Stri // ---------- UI commands ---------- -pub fn open(app: &mut DrawcraftApp) { +pub fn open(app: &mut VectorcraftApp) { let ov = serde_json::to_value(&app.ui.shortcut_overrides).unwrap_or(json!({})); app.ui.dialog = Some(Dialog::new( "shortcuts", @@ -317,7 +317,7 @@ fn dialog_overrides(d: &Dialog) -> BTreeMap { } /// OK: commit the working set. -pub fn confirm(app: &mut DrawcraftApp) -> Result { +pub fn confirm(app: &mut VectorcraftApp) -> Result { let Some(d) = app.ui.dialog.clone() else { return Err("no dialog open".into()) }; app.ui.shortcut_overrides = dialog_overrides(&d); app.ui.shortcut_set = d.str("set"); @@ -327,7 +327,7 @@ pub fn confirm(app: &mut DrawcraftApp) -> Result { } /// `shortcuts.*` UI commands. `None` = not ours. -pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option> { +pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { let s = |k: &str| p.get(k).and_then(Value::as_str).map(str::to_string); let r = match id { "edit.keyboardShortcuts" => { @@ -382,7 +382,7 @@ pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option { let v = export_json(&app.ui.shortcut_set, &app.ui.shortcut_overrides); let bytes = serde_json::to_vec_pretty(&v).unwrap_or_default(); - let path = s("path").or_else(|| app.services.pick_save.as_mut().and_then(|f| f("DrawCraft Shortcuts.json"))); + let path = s("path").or_else(|| app.services.pick_save.as_mut().and_then(|f| f("VectorCraft Shortcuts.json"))); match path { Some(path) => match app.services.write.as_mut() { Some(w) => w(&path, &bytes).map(|_| json!({"path": path})), @@ -390,7 +390,7 @@ pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option match app.services.download.as_mut() { Some(dl) => { - dl("DrawCraft Shortcuts.json", &bytes); + dl("VectorCraft Shortcuts.json", &bytes); Ok(Value::Null) } None => Ok(v), @@ -426,13 +426,13 @@ pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option bool { +pub fn is_recording(app: &VectorcraftApp) -> bool { app.ui.dialog.as_ref().is_some_and(|d| d.kind == "shortcuts" && !d.str("__recording").is_empty()) } // ---------- dialog ---------- -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(mut d) = app.ui.dialog.clone() else { return }; let t = Tokens::get(ctx); let mut ov = dialog_overrides(&d); diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 974c9c973..4398bb068 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -1,11 +1,11 @@ //! Keyboard shortcuts: command shortcuts from the registry, single-key tool shortcuts, arrows, //! and tool keys (Enter/Esc/↑/↓ while drawing). -use drawcraft_tools::{Mods, ToolKey}; use egui::{Key, KeyboardShortcut, Modifiers}; use serde_json::json; +use vectorcraft_tools::{Mods, ToolKey}; -use crate::DrawcraftApp; +use crate::VectorcraftApp; /// Parse "Cmd+Shift+]" into an egui shortcut. `Cmd` is Command on macOS and Ctrl elsewhere. pub fn parse(s: &str) -> Option { @@ -46,7 +46,7 @@ pub fn parse(s: &str) -> Option { /// Every command shortcut in effect (user overrides from Edit → Keyboard Shortcuts win). fn all_shortcuts() -> Vec<(KeyboardShortcut, &'static str)> { let mut v: Vec<(KeyboardShortcut, &'static str)> = vec![]; - for c in drawcraft_engine::command_specs() { + for c in vectorcraft_engine::command_specs() { if let Some(sc) = crate::menus::shortcut_of(c.id).and_then(parse) { v.push((sc, c.id)); } @@ -63,7 +63,7 @@ fn all_shortcuts() -> Vec<(KeyboardShortcut, &'static str)> { v } -pub fn handle(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { if app.ui.dialog.is_some() || app.ui.palette_open { if crate::shortcut_editor::is_recording(app) { return; @@ -208,7 +208,7 @@ mod tests { use super::*; /// One headless frame delivering `events` to the shortcut handler. - fn frame(app: &mut DrawcraftApp, events: Vec) -> egui::FullOutput { + fn frame(app: &mut VectorcraftApp, events: Vec) -> egui::FullOutput { let ctx = egui::Context::default(); let mut out = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { handle(app, ui.ctx()); @@ -220,7 +220,7 @@ mod tests { #[test] fn copy_and_paste_events_use_the_system_clipboard() { - let mut app = DrawcraftApp::new(drawcraft_engine::Session::new(), Default::default()); + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); app.session.execute("file.new", &json!({"width": 200, "height": 200})).unwrap(); let id = app.session.execute("shape.rectangle", &json!({"x": 0, "y": 0, "width": 10, "height": 10})).unwrap()["id"].clone(); app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); @@ -256,7 +256,7 @@ mod tests { #[test] fn all_registered_shortcuts_parse() { - for c in drawcraft_engine::command_specs() { + for c in vectorcraft_engine::command_specs() { if let Some(s) = c.shortcut && s != "D" && s != "X" diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index 8d16fc715..67484023d 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -1,8 +1,8 @@ //! Serializable UI state (read and driven by the control channel). -use drawcraft_geom::Point; use serde::{Deserialize, Serialize}; use serde_json::Value; +use vectorcraft_geom::Point; use crate::theme::Brightness; @@ -150,7 +150,7 @@ impl Dialog { pub fn f64(&self, k: &str, d: f64) -> f64 { match self.fields.get(k) { Some(Value::Number(n)) => n.as_f64().unwrap_or(d), - Some(Value::String(s)) => drawcraft_doc::Unit::Points.parse(s).unwrap_or(d), + Some(Value::String(s)) => vectorcraft_doc::Unit::Points.parse(s).unwrap_or(d), _ => d, } } @@ -235,7 +235,7 @@ impl UiState { self.status.clear(); self.about = false; self.open_panel = None; - if self.group_tool.len() != drawcraft_tools::TOOL_GROUPS.len() { + if self.group_tool.len() != vectorcraft_tools::TOOL_GROUPS.len() { self.group_tool = UiState::default().group_tool; } self @@ -259,7 +259,7 @@ impl Default for UiState { view: ViewFlags::default(), dialog: None, flyout: None, - group_tool: drawcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(), + group_tool: vectorcraft_tools::TOOL_GROUPS.iter().map(|g| g[0].id.to_string()).collect(), status: String::new(), palette_open: false, palette_query: String::new(), diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index b645c6da8..d71475c00 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -2,12 +2,12 @@ //! Draw, Modify, Type, Navigate, Color). Window → Toolbars → Advanced shows every tool group. //! Bottom: fill/stroke proxy, colour/gradient/none, drawing modes, screen mode, Edit Toolbar. -use drawcraft_tools::{TOOL_GROUPS, ToolInfo, tool_info}; use egui::{Color32, CornerRadius, Sense, Stroke, Ui, pos2, vec2}; use serde_json::json; +use vectorcraft_tools::{TOOL_GROUPS, ToolInfo, tool_info}; use crate::theme::{self, Tokens}; -use crate::{DrawcraftApp, icons, widgets}; +use crate::{VectorcraftApp, icons, widgets}; const PITCH: f32 = 30.0; const WIDTH: f32 = 48.0; @@ -55,7 +55,7 @@ fn tip(t: &ToolInfo) -> String { } /// Slots of the current layout: (category label for the first slot of a category, tool ids). -fn slots(app: &DrawcraftApp) -> Vec<(Option<&'static str>, Vec<&'static str>)> { +fn slots(app: &VectorcraftApp) -> Vec<(Option<&'static str>, Vec<&'static str>)> { if app.ui.toolbar_advanced { TOOL_GROUPS.iter().map(|g| (None, g.iter().map(|t| t.id).collect())).collect() } else { @@ -64,7 +64,7 @@ fn slots(app: &DrawcraftApp) -> Vec<(Option<&'static str>, Vec<&'static str>)> { } /// Remember the tool shown in the slot that contains `id`. -pub fn remember(app: &mut DrawcraftApp, id: &str) { +pub fn remember(app: &mut VectorcraftApp, id: &str) { for (_, s) in slots(app) { if s.contains(&id) { app.ui.slot_tool.insert(s[0].to_string(), id.to_string()); @@ -72,7 +72,7 @@ pub fn remember(app: &mut DrawcraftApp, id: &str) { } } -pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { +pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let all = slots(app); let avail = ui.available_height(); @@ -177,7 +177,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) { flyout(app, ui.ctx()); } -fn bottom_controls(app: &mut DrawcraftApp, ui: &mut Ui, t: &Tokens) { +fn bottom_controls(app: &mut VectorcraftApp, ui: &mut Ui, t: &Tokens) { let (fill, stroke) = match app.session.active().and_then(|d| d.selection.objects.first().and_then(|id| d.doc.node(*id))) { Some(n) if !n.is_container() => (n.appearance.fill_paint(), n.appearance.stroke_paint()), _ => (app.session.paint.fill.clone(), app.session.paint.stroke.clone()), @@ -203,19 +203,19 @@ fn bottom_controls(app: &mut DrawcraftApp, ui: &mut Ui, t: &Tokens) { ui.add_space((ui.available_width() - 27.0) / 2.0); ui.spacing_mut().item_spacing.x = 2.0; let s = 7.5; - let last = app.session.paint.fill.color().unwrap_or(drawcraft_color::Color::BLACK); + let last = app.session.paint.fill.color().unwrap_or(vectorcraft_color::Color::BLACK); let (r, resp) = ui.allocate_exact_size(vec2(s, s), Sense::click()); - widgets::paint_chip(ui, r, &drawcraft_color::Paint::solid(last)); + widgets::paint_chip(ui, r, &vectorcraft_color::Paint::solid(last)); if resp.on_hover_text("Color (,)").clicked() { app.run(target, json!({"color": last.to_hex()})).ok(); } let (r, resp) = ui.allocate_exact_size(vec2(s, s), Sense::click()); - widgets::paint_chip(ui, r, &drawcraft_color::Paint::Gradient(Box::new(drawcraft_color::GradientPaint::new(Default::default())))); + widgets::paint_chip(ui, r, &vectorcraft_color::Paint::Gradient(Box::new(vectorcraft_color::GradientPaint::new(Default::default())))); if resp.on_hover_text("Gradient (.)").clicked() { app.run(target, json!({"gradient": {"kind": "linear"}})).ok(); } let (r, resp) = ui.allocate_exact_size(vec2(s, s), Sense::click()); - widgets::paint_chip(ui, r, &drawcraft_color::Paint::None); + widgets::paint_chip(ui, r, &vectorcraft_color::Paint::None); if resp.on_hover_text("None (/)").clicked() { app.run(target, json!({"none": true})).ok(); } @@ -225,9 +225,9 @@ fn bottom_controls(app: &mut DrawcraftApp, ui: &mut Ui, t: &Tokens) { let modes = ["dc-draw-normal", "dc-draw-behind", "dc-draw-inside"]; let names = ["Draw Normal", "Draw Behind", "Draw Inside"]; let m = match app.session.draw_mode { - drawcraft_engine::DrawMode::Normal => 0, - drawcraft_engine::DrawMode::Behind => 1, - drawcraft_engine::DrawMode::Inside => 2, + vectorcraft_engine::DrawMode::Normal => 0, + vectorcraft_engine::DrawMode::Behind => 1, + vectorcraft_engine::DrawMode::Inside => 2, }; if widgets::icon_button(ui, modes[m], &format!("{} (Shift+D)", names[m]), m != 0, 26.0).clicked() { app.run("view.drawMode", json!({})).ok(); @@ -242,7 +242,7 @@ fn bottom_controls(app: &mut DrawcraftApp, ui: &mut Ui, t: &Tokens) { let _ = t; } -fn flyout(app: &mut DrawcraftApp, ctx: &egui::Context) { +fn flyout(app: &mut VectorcraftApp, ctx: &egui::Context) { if app.ui.flyout.is_none() { return; } diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index f0d48e487..968b6e9bc 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -1,8 +1,8 @@ //! Shared widgets in Illustrator's panel style. Colours come from theme tokens. -use drawcraft_color::Paint; -use drawcraft_doc::Unit; use egui::{Color32, CornerRadius, Pos2, Rect, Response, Sense, Stroke, StrokeKind, Ui, Vec2, pos2, vec2}; +use vectorcraft_color::Paint; +use vectorcraft_doc::Unit; use crate::icons; use crate::theme::{self, Tokens}; @@ -184,7 +184,7 @@ pub fn paint_chip(ui: &Ui, rect: Rect, paint: &Paint) { p.rect_filled(rect, 0.0, Color32::from_rgb(r, g, b)); } Paint::Gradient(gp) => { - if gp.gradient.kind == drawcraft_color::GradientKind::Radial { + if gp.gradient.kind == vectorcraft_color::GradientKind::Radial { // Outer colour fills the corners beyond the largest circle. let (c, _) = gp.gradient.sample(1.0); let [r, g, b, _] = c.to_rgba8(1.0); @@ -197,7 +197,7 @@ pub fn paint_chip(ui: &Ui, rect: Rect, paint: &Paint) { let (c, _) = gp.gradient.sample(tt); let [r, g, b, _] = c.to_rgba8(1.0); let rr = Rect::from_min_size(pos2(rect.left() + i as f32 * w, rect.top()), vec2(w + 0.5, rect.height())); - if gp.gradient.kind == drawcraft_color::GradientKind::Radial { + if gp.gradient.kind == vectorcraft_color::GradientKind::Radial { let (c, _) = gp.gradient.sample(1.0 - tt); let [r, g, b, _] = c.to_rgba8(1.0); let s = rect.width().min(rect.height()) * tt / 2.0; diff --git a/crates/ui-egui/src/workspaces.rs b/crates/ui-egui/src/workspaces.rs index 8346b1dbe..7441fb8f7 100644 --- a/crates/ui-egui/src/workspaces.rs +++ b/crates/ui-egui/src/workspaces.rs @@ -10,7 +10,7 @@ use serde_json::{Value, json}; use crate::state::{Dialog, DockTab, UiState}; use crate::theme::{self, Tokens}; -use crate::{DrawcraftApp, widgets}; +use crate::{VectorcraftApp, widgets}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] @@ -233,7 +233,7 @@ pub fn menu_items() -> Vec { } /// The app-bar workspace switcher's menu. -pub fn popup(app: &mut DrawcraftApp, ui: &mut egui::Ui) { +pub fn popup(app: &mut VectorcraftApp, ui: &mut egui::Ui) { ui.set_min_width(210.0); ui.set_max_width(240.0); let mut clicked: Option<(&'static str, Value)> = None; @@ -264,7 +264,7 @@ pub fn popup(app: &mut DrawcraftApp, ui: &mut egui::Ui) { // ---------- commands ---------- -pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option> { +pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { let s = |k: &str| p.get(k).and_then(Value::as_str).map(str::to_string); let r = match id { "window.workspace" => match s("name").and_then(|n| find(&app.ui, &n)) { @@ -300,7 +300,7 @@ pub fn run_command(app: &mut DrawcraftApp, id: &str, p: &Value) -> Option Result { +pub fn confirm(app: &mut VectorcraftApp) -> Result { let Some(d) = app.ui.dialog.clone() else { return Err("no dialog open".into()) }; let r = match d.kind.as_str() { "newWorkspace" => save_as(&mut app.ui, &d.str("name")).map(|_| Value::Null), @@ -312,7 +312,7 @@ pub fn confirm(app: &mut DrawcraftApp) -> Result { r } -pub fn show(app: &mut DrawcraftApp, ctx: &egui::Context) { +pub fn show(app: &mut VectorcraftApp, ctx: &egui::Context) { let Some(mut d) = app.ui.dialog.clone() else { return }; let t = Tokens::get(ctx); let (mut ok, mut cancel) = (false, false); diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 070e03a80..c7d83ca9e 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -1,6 +1,6 @@ # Control protocol -`drawcraft --control ` listens on `127.0.0.1:` (loopback only). One JSON request per line: +`vectorcraft --control ` listens on `127.0.0.1:` (loopback only). One JSON request per line: `{"id": 1, "method": "ui.inspect", "params": {}}` → `{"id": 1, "ok": true, "result": {...}}` or `{"id":1,"ok":false,"error":"..."}`. | Method | Params | | diff --git a/docs/development.md b/docs/development.md index c2912c1bb..d77f990fa 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,12 +2,12 @@ ## Web build -`apps/drawcraft-web` runs the same `DrawcraftApp` 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. +`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. ```sh brew install trunk # or: cargo install trunk --locked rustup target add wasm32-unknown-unknown -cd apps/drawcraft-web +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 ``` @@ -16,7 +16,7 @@ Any static file server works for `dist/web`, for example `python3 -m http.server URL flag: `?webgl` forces the WebGL2 backend. -How the web shell (`apps/drawcraft-web/src/web.rs`) differs from desktop: +How the web shell (`apps/vectorcraft-web/src/web.rs`) differs from desktop: - **Open** sets `Services::open_async`, which shows `rfd::AsyncFileDialog`. The bytes arrive in `Services::inbox`, which the app drains every frame. - **Save / Save As / Export** go through `Services::download`: a Blob, an object URL and a temporary ``, all created from Rust. There is no save dialog, so the suggested name becomes the download name. diff --git a/docs/images/art-neon-drive.png b/docs/images/art-neon-drive.png index d8c2afa5e..7d154eac7 100644 Binary files a/docs/images/art-neon-drive.png and b/docs/images/art-neon-drive.png differ diff --git a/docs/images/dusk-poster.png b/docs/images/dusk-poster.png index 707c6306c..55bcbafcd 100644 Binary files a/docs/images/dusk-poster.png and b/docs/images/dusk-poster.png differ diff --git a/docs/images/shot-1-neon.png b/docs/images/shot-1-neon.png index a9ec75982..a950e8a65 100644 Binary files a/docs/images/shot-1-neon.png and b/docs/images/shot-1-neon.png differ diff --git a/docs/images/shot-2-ribbons.png b/docs/images/shot-2-ribbons.png index 2761e440e..be00f9511 100644 Binary files a/docs/images/shot-2-ribbons.png and b/docs/images/shot-2-ribbons.png differ diff --git a/docs/images/shot-3-sheet.png b/docs/images/shot-3-sheet.png index fe861d71c..f7e8ce813 100644 Binary files a/docs/images/shot-3-sheet.png and b/docs/images/shot-3-sheet.png differ diff --git a/docs/images/shot-4-bezier.png b/docs/images/shot-4-bezier.png index 7fc33c235..1fdb852cd 100644 Binary files a/docs/images/shot-4-bezier.png and b/docs/images/shot-4-bezier.png differ diff --git a/docs/mcp.md b/docs/mcp.md index ab4df8e9e..0d0f122fb 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1,15 +1,15 @@ -# DrawCraft MCP server +# VectorCraft MCP server -`drawcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio +`vectorcraft-cli mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io) server on stdio (newline-delimited JSON-RPC 2.0, protocol `2025-06-18`; `2025-03-26` and `2024-11-05` also accepted). Agents use it to -draw, inspect and look at DrawCraft documents. +draw, inspect and look at VectorCraft documents. It has two backends: | Mode | How | What works | |---|---|---| -| **Remote** | `drawcraft-cli mcp --connect 127.0.0.1:7979` (the app must run with `drawcraft --control 7979`) | Everything. Tool calls are forwarded over the [control protocol](control-protocol.md), so you watch the app change live. | -| **Headless** | `drawcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui`, `type_text`, `open_panel`, `screenshot {window:true}`). | +| **Remote** | `vectorcraft-cli mcp --connect 127.0.0.1:7979` (the app must run with `vectorcraft --control 7979`) | Everything. Tool calls are forwarded over the [control protocol](control-protocol.md), so you watch the app change live. | +| **Headless** | `vectorcraft-cli mcp --headless` | An in-process engine session with a CPU renderer. Everything except the UI-only tools (`inspect_ui`, `type_text`, `open_panel`, `screenshot {window:true}`). | With no flag, the server tries `127.0.0.1:7979` and falls back to headless. Logs go to stderr; stdout carries only protocol messages. @@ -17,20 +17,20 @@ only protocol messages. ## Build and register ```sh -cargo build --release -p drawcraft-cli -claude mcp add drawcraft -- "$PWD/target/release/drawcraft-cli" mcp +cargo build --release -p vectorcraft-cli +claude mcp add vectorcraft -- "$PWD/target/release/vectorcraft-cli" mcp # or pin a mode: -claude mcp add drawcraft-headless -- "$PWD/target/release/drawcraft-cli" mcp --headless -claude mcp add drawcraft-app -- "$PWD/target/release/drawcraft-cli" mcp --connect 127.0.0.1:7979 +claude mcp add vectorcraft-headless -- "$PWD/target/release/vectorcraft-cli" mcp --headless +claude mcp add vectorcraft-app -- "$PWD/target/release/vectorcraft-cli" mcp --connect 127.0.0.1:7979 ``` Other clients use the same command in their JSON config: ```json -{"mcpServers": {"drawcraft": {"command": "/abs/path/target/release/drawcraft-cli", "args": ["mcp"]}}} +{"mcpServers": {"vectorcraft": {"command": "/abs/path/target/release/vectorcraft-cli", "args": ["mcp"]}}} ``` -For a live session, start the app first: `cargo run --release -p drawcraft -- --control 7979`. +For a live session, start the app first: `cargo run --release -p vectorcraft -- --control 7979`. ## Tools @@ -57,9 +57,9 @@ or a full `paint.setFill` params object (`{"gradient": …}`, `{"swatch": "name" | `invoke_menu` | `{command, params?}` | Invokes a menu item by command id. Includes UI commands such as `view.*` and `window.*` in remote mode. | | `open_panel` | `{panel}` | Remote only. | | `screenshot` | `{path?, scale?, artboard?, window?}` | Returns MCP image content (`image/png`, base64) plus a text block. Renders the artboard; `window:true` captures the app window (remote only). | -| `open_file` | `{path}` | Opens `.drawcraft` or `.svg` as a new active document. | -| `save_file` | `{path?}` | Saves in the native `.drawcraft` format. | -| `export` | `{path, format?, scale?}` | `svg`, `png` or `drawcraft`. When `format` is omitted, it comes from the path's extension. | +| `open_file` | `{path}` | Opens `.vectorcraft` or `.svg` as a new active document. | +| `save_file` | `{path?}` | Saves in the native `.vectorcraft` format. | +| `export` | `{path, format?, scale?}` | `svg`, `png` or `vectorcraft`. When `format` is omitted, it comes from the path's extension. | | `undo` / `redo` | `{}` | | Errors (unknown tool, bad arguments, a disabled or failing command) come back as a normal result with @@ -69,8 +69,8 @@ Errors (unknown tool, bad arguments, a disabled or failing command) come back as | URI | Content | |---|---| -| `drawcraft://document` | `document.inspect` summary (JSON) | -| `drawcraft://document/json` | The complete document model (JSON) | +| `vectorcraft://document` | `document.inspect` summary (JSON) | +| `vectorcraft://document/json` | The complete document model (JSON) | ## Examples @@ -108,19 +108,19 @@ printf '%s\n' \ '{"jsonrpc":"2.0","method":"notifications/initialized"}' \ '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"ellipse","x":10,"y":10,"width":100,"height":80,"fill":"#ff0000"}}}' \ '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inspect_document","arguments":{}}}' \ - | target/release/drawcraft-cli mcp --headless + | target/release/vectorcraft-cli mcp --headless ``` ## Headless batch CLI ```sh -drawcraft-cli commands # command catalogue (JSON) -drawcraft-cli run --in in.svg \ +vectorcraft-cli commands # command catalogue (JSON) +vectorcraft-cli run --in in.svg \ --cmd select.all --cmd paint.setFill --params '{"color":"#ff0000"}' \ --export out.svg --export out.png --scale 2 -drawcraft-cli run --cmd file.new --params '{"width":800,"height":600}' \ +vectorcraft-cli run --cmd file.new --params '{"width":800,"height":600}' \ --cmd shape.star --params '{"cx":400,"cy":300,"radius1":200,"radius2":90}' \ - --export star.drawcraft + --export star.vectorcraft ``` `run` prints one JSON line per step (`open`, `cmd`, `export`) and exits non-zero on the first failure. `--params` diff --git a/examples/dusk-poster.svg b/examples/dusk-poster.svg index a1c93b763..a8eca1c77 100644 --- a/examples/dusk-poster.svg +++ b/examples/dusk-poster.svg @@ -60,6 +60,6 @@ DUSK - Made in DrawCraft — vectors, pure Rust + Made in VectorCraft — vectors, pure Rust diff --git a/examples/dusk-poster.drawcraft b/examples/dusk-poster.vectorcraft similarity index 99% rename from examples/dusk-poster.drawcraft rename to examples/dusk-poster.vectorcraft index 5d06309ad..b9d32524e 100644 --- a/examples/dusk-poster.drawcraft +++ b/examples/dusk-poster.vectorcraft @@ -1,7 +1,7 @@ { - "format": "drawcraft", + "format": "vectorcraft", "version": 1, - "generator": "DrawCraft 0.1.0", + "generator": "VectorCraft 0.1.0", "document": { "version": 1, "title": "Dusk Poster", @@ -6581,7 +6581,7 @@ ], "runs": [ { - "text": "Made in DrawCraft — vectors, pure Rust", + "text": "Made in VectorCraft — vectors, pure Rust", "style": { "font_family": "Source Sans 3", "font_style": "Regular", diff --git a/examples/feature-sheet.drawcraft b/examples/feature-sheet.vectorcraft similarity index 99% rename from examples/feature-sheet.drawcraft rename to examples/feature-sheet.vectorcraft index a424a8788..b78b7b271 100644 --- a/examples/feature-sheet.drawcraft +++ b/examples/feature-sheet.vectorcraft @@ -1,7 +1,7 @@ { - "format": "drawcraft", + "format": "vectorcraft", "version": 1, - "generator": "DrawCraft 0.1.0", + "generator": "VectorCraft 0.1.0", "document": { "version": 1, "title": "Feature Sheet", diff --git a/examples/neon-drive.drawcraft b/examples/neon-drive.vectorcraft similarity index 99% rename from examples/neon-drive.drawcraft rename to examples/neon-drive.vectorcraft index 5616b7c6b..4ab37d631 100644 --- a/examples/neon-drive.drawcraft +++ b/examples/neon-drive.vectorcraft @@ -1,7 +1,7 @@ { - "format": "drawcraft", + "format": "vectorcraft", "version": 1, - "generator": "DrawCraft 0.1.0", + "generator": "VectorCraft 0.1.0", "document": { "version": 1, "title": "Neon Drive", @@ -13388,7 +13388,7 @@ ], "runs": [ { - "text": "made with DrawCraft", + "text": "made with VectorCraft", "style": { "font_family": "Source Sans 3", "font_style": "Regular", diff --git a/examples/ribbons.drawcraft b/examples/ribbons.vectorcraft similarity index 99% rename from examples/ribbons.drawcraft rename to examples/ribbons.vectorcraft index 79b7b7226..bb7d74503 100644 --- a/examples/ribbons.drawcraft +++ b/examples/ribbons.vectorcraft @@ -1,7 +1,7 @@ { - "format": "drawcraft", + "format": "vectorcraft", "version": 1, - "generator": "DrawCraft 0.1.0", + "generator": "VectorCraft 0.1.0", "document": { "version": 1, "title": "Ribbons", diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index 2a3c9d869..39141eeea 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -7,7 +7,7 @@ publish = false [dependencies] serde_json = { workspace = true } -drawcraft-doc = { workspace = true } -drawcraft-geom = { workspace = true } -drawcraft-color = { workspace = true } -drawcraft-render = { workspace = true } +vectorcraft-doc = { workspace = true } +vectorcraft-geom = { workspace = true } +vectorcraft-color = { workspace = true } +vectorcraft-render = { workspace = true } diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs index 122c7928f..2f66217b8 100644 --- a/xtask/src/assets.rs +++ b/xtask/src/assets.rs @@ -32,6 +32,7 @@ const ASSET_EXT: &[&str] = &[ "ase", "aco", "abr", + "vectorcraft", "drawcraft", "mp4", "wav", diff --git a/xtask/src/bundle.rs b/xtask/src/bundle.rs index 3b48117bf..ac494a0a1 100644 --- a/xtask/src/bundle.rs +++ b/xtask/src/bundle.rs @@ -1,13 +1,13 @@ -//! `cargo xtask bundle`: build the release app and wrap it as `dist/DrawCraft.app` (macOS), with an -//! icon rendered by DrawCraft's own renderer. +//! `cargo xtask bundle`: build the release app and wrap it as `dist/VectorCraft.app` (macOS), with an +//! icon rendered by VectorCraft's own renderer. use std::path::Path; use std::process::Command; -use drawcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; -use drawcraft_doc::{Appearance, Document, Node}; -use drawcraft_geom::{Anchor, PathData, Point, Rect, SubPath, shapes}; -use drawcraft_render::Renderer; +use vectorcraft_color::{Color, Gradient, GradientKind, GradientPaint, GradientStop, Paint}; +use vectorcraft_doc::{Appearance, Document, Node}; +use vectorcraft_geom::{Anchor, PathData, Point, Rect, SubPath, shapes}; +use vectorcraft_render::Renderer; /// The app icon: a warm-gradient rounded square with a white pen nib (our own artwork). pub fn icon_png(size: u32) -> Vec { @@ -77,21 +77,21 @@ pub fn icon_png(size: u32) -> Vec { pub fn run(root: &Path) -> Result<(), String> { let status = Command::new(std::env::var("CARGO").unwrap_or_else(|_| "cargo".into())) .current_dir(root) - .args(["build", "--release", "-p", "drawcraft", "-p", "drawcraft-cli"]) + .args(["build", "--release", "-p", "vectorcraft", "-p", "vectorcraft-cli"]) .status() .map_err(|e| e.to_string())?; if !status.success() { return Err("release build failed".into()); } let target = std::env::var("CARGO_TARGET_DIR").map(std::path::PathBuf::from).unwrap_or_else(|_| root.join("target")); - let app = root.join("dist/DrawCraft.app/Contents"); - let _ = std::fs::remove_dir_all(root.join("dist/DrawCraft.app")); + let app = root.join("dist/VectorCraft.app/Contents"); + let _ = std::fs::remove_dir_all(root.join("dist/VectorCraft.app")); std::fs::create_dir_all(app.join("MacOS")).map_err(|e| e.to_string())?; std::fs::create_dir_all(app.join("Resources")).map_err(|e| e.to_string())?; - std::fs::copy(target.join("release/drawcraft"), app.join("MacOS/DrawCraft")).map_err(|e| format!("copy app: {e}"))?; - std::fs::copy(target.join("release/drawcraft-cli"), app.join("MacOS/drawcraft-cli")).map_err(|e| format!("copy cli: {e}"))?; + std::fs::copy(target.join("release/vectorcraft"), app.join("MacOS/VectorCraft")).map_err(|e| format!("copy app: {e}"))?; + std::fs::copy(target.join("release/vectorcraft-cli"), app.join("MacOS/vectorcraft-cli")).map_err(|e| format!("copy cli: {e}"))?; // Icon set → .icns via iconutil (macOS build tool). - let iconset = root.join("dist/DrawCraft.iconset"); + let iconset = root.join("dist/VectorCraft.iconset"); let _ = std::fs::remove_dir_all(&iconset); std::fs::create_dir_all(&iconset).map_err(|e| e.to_string())?; for (sz, name) in [ @@ -108,12 +108,12 @@ pub fn run(root: &Path) -> Result<(), String> { ] { std::fs::write(iconset.join(format!("icon_{name}.png")), icon_png(sz)).map_err(|e| e.to_string())?; } - std::fs::write(root.join("dist/DrawCraft-icon.png"), icon_png(1024)).map_err(|e| e.to_string())?; + std::fs::write(root.join("dist/VectorCraft-icon.png"), icon_png(1024)).map_err(|e| e.to_string())?; let ok = Command::new("iconutil") .args(["-c", "icns"]) .arg(&iconset) .arg("-o") - .arg(app.join("Resources/DrawCraft.icns")) + .arg(app.join("Resources/VectorCraft.icns")) .status() .map(|s| s.success()) .unwrap_or(false); @@ -125,18 +125,18 @@ pub fn run(root: &Path) -> Result<(), String> { r#" -CFBundleNameDrawCraft -CFBundleDisplayNameDrawCraft -CFBundleIdentifierai.storyteller.drawcraft -CFBundleExecutableDrawCraft -CFBundleIconFileDrawCraft +CFBundleNameVectorCraft +CFBundleDisplayNameVectorCraft +CFBundleIdentifierai.storyteller.vectorcraft +CFBundleExecutableVectorCraft +CFBundleIconFileVectorCraft CFBundlePackageTypeAPPL CFBundleShortVersionString{version} CFBundleVersion{version} NSHighResolutionCapable LSMinimumSystemVersion11.0 CFBundleDocumentTypes - CFBundleTypeNameDrawCraft DocumentCFBundleTypeExtensionsdrawcraftCFBundleTypeRoleEditor + CFBundleTypeNameVectorCraft DocumentCFBundleTypeExtensionsvectorcraftCFBundleTypeRoleEditor CFBundleTypeNameSVGCFBundleTypeExtensionssvgCFBundleTypeRoleEditor CFBundleTypeNamePDF / IllustratorCFBundleTypeExtensionspdfaiCFBundleTypeRoleViewer @@ -144,6 +144,6 @@ pub fn run(root: &Path) -> Result<(), String> { "# ); std::fs::write(app.join("Info.plist"), plist).map_err(|e| e.to_string())?; - println!("built {}", root.join("dist/DrawCraft.app").display()); + println!("built {}", root.join("dist/VectorCraft.app").display()); Ok(()) } diff --git a/xtask/src/layers.rs b/xtask/src/layers.rs index 77e44ec75..6ca42d1c2 100644 --- a/xtask/src/layers.rs +++ b/xtask/src/layers.rs @@ -30,7 +30,7 @@ impl Class { } } -/// The layering table. Names are package names without the `drawcraft-` +/// The layering table. Names are package names without the `vectorcraft-` /// prefix. pub const TABLE: &[(&str, Class)] = &[ ("geom", Class::Layer(0)), @@ -51,7 +51,7 @@ pub const TABLE: &[(&str, Class)] = &[ ("mcp", Class::Layer(6)), ("testkit", Class::Testkit), // L7 apps and tooling - ("drawcraft", Class::Exempt), + ("vectorcraft", Class::Exempt), ("cli", Class::Exempt), ("web", Class::Exempt), ("xtask", Class::Exempt), @@ -79,7 +79,7 @@ pub const UI_CRATES: &[&str] = &["egui", "eframe", "winit", "egui_kittest", "rfd pub const UI_MIN_LAYER: u8 = 6; pub fn short_name(pkg: &str) -> &str { - pkg.strip_prefix("drawcraft-").unwrap_or(pkg) + pkg.strip_prefix("vectorcraft-").unwrap_or(pkg) } pub fn classify(pkg: &str) -> Option { @@ -137,7 +137,7 @@ impl std::fmt::Display for Violation { write!(f, "{krate}: standalone crate must not depend on workspace crate {dep}") } Violation::TestkitAsNormalDep { krate } => { - write!(f, "{krate}: drawcraft-testkit may only be a dev-dependency") + write!(f, "{krate}: vectorcraft-testkit may only be a dev-dependency") } Violation::UiBelowL6 { krate, dep, layer } => { write!(f, "{krate} (L{layer}) depends on UI crate `{dep}`; UI toolkits are only allowed in L6+") @@ -245,76 +245,76 @@ mod tests { #[test] fn clean_downward_graph_passes() { let g = [ - c("drawcraft-geom", &[("kurbo", Normal, false)]), - c("drawcraft-doc", &[("drawcraft-geom", Normal, true)]), - c("drawcraft-engine", &[("drawcraft-doc", Normal, true), ("drawcraft-testkit", Dev, true)]), - c("drawcraft-ui-egui", &[("drawcraft-engine", Normal, true), ("egui", Normal, false)]), - c("drawcraft-cli", &[("drawcraft-ui-egui", Normal, true)]), + c("vectorcraft-geom", &[("kurbo", Normal, false)]), + c("vectorcraft-doc", &[("vectorcraft-geom", Normal, true)]), + c("vectorcraft-engine", &[("vectorcraft-doc", Normal, true), ("vectorcraft-testkit", Dev, true)]), + c("vectorcraft-ui-egui", &[("vectorcraft-engine", Normal, true), ("egui", Normal, false)]), + c("vectorcraft-cli", &[("vectorcraft-ui-egui", Normal, true)]), ]; assert!(check(&g).is_empty(), "{:?}", check(&g)); } #[test] fn upward_dependency_flagged() { - let v = check(&[c("drawcraft-doc", &[("drawcraft-engine", Normal, true)])]); + let v = check(&[c("vectorcraft-doc", &[("vectorcraft-engine", Normal, true)])]); assert!(matches!(v[..], [Violation::Upward { from: 1, to: 5, .. }])); } #[test] fn sideways_dependency_flagged() { - let v = check(&[c("drawcraft-text", &[("drawcraft-pathops", Normal, true)])]); + let v = check(&[c("vectorcraft-text", &[("vectorcraft-pathops", Normal, true)])]); assert!(matches!(v[..], [Violation::Upward { from: 2, to: 2, .. }])); } #[test] fn l0_foundation_chain_allowed_one_way() { - assert!(check(&[c("drawcraft-color", &[("drawcraft-geom", Normal, true)])]).is_empty()); - let v = check(&[c("drawcraft-geom", &[("drawcraft-color", Normal, true)])]); + assert!(check(&[c("vectorcraft-color", &[("vectorcraft-geom", Normal, true)])]).is_empty()); + let v = check(&[c("vectorcraft-geom", &[("vectorcraft-color", Normal, true)])]); assert!(matches!(v[..], [Violation::Upward { from: 0, to: 0, .. }])); } #[test] fn self_dev_dependency_ignored() { - assert!(check(&[c("drawcraft-doc", &[("drawcraft-doc", Dev, true)])]).is_empty()); + assert!(check(&[c("vectorcraft-doc", &[("vectorcraft-doc", Dev, true)])]).is_empty()); } #[test] fn upward_dev_dependency_flagged() { - let v = check(&[c("drawcraft-geom", &[("drawcraft-doc", Dev, true)])]); + let v = check(&[c("vectorcraft-geom", &[("vectorcraft-doc", Dev, true)])]); assert!(matches!(v[..], [Violation::Upward { kind: Dev, .. }])); } #[test] fn ui_crates_below_l6_flagged() { for dep in ["egui", "eframe", "winit", "egui_kittest", "rfd", "bevy_ecs", "bevy"] { - let v = check(&[c("drawcraft-engine", &[(dep, Normal, false)])]); + let v = check(&[c("vectorcraft-engine", &[(dep, Normal, false)])]); assert!(matches!(v[..], [Violation::UiBelowL6 { layer: 5, .. }]), "{dep}"); } - assert!(check(&[c("drawcraft-engine", &[("egui_extras_not", Normal, false)])]).is_empty()); - assert!(check(&[c("drawcraft-mcp", &[("winit", Normal, false)])]).is_empty()); + assert!(check(&[c("vectorcraft-engine", &[("egui_extras_not", Normal, false)])]).is_empty()); + assert!(check(&[c("vectorcraft-mcp", &[("winit", Normal, false)])]).is_empty()); } #[test] fn unregistered_crate_is_error() { - let v = check(&[c("drawcraft-mystery", &[])]); - assert!(matches!(&v[..], [Violation::Unregistered { krate }] if krate == "drawcraft-mystery")); + let v = check(&[c("vectorcraft-mystery", &[])]); + assert!(matches!(&v[..], [Violation::Unregistered { krate }] if krate == "vectorcraft-mystery")); assert!(v[0].to_string().contains("register")); } #[test] fn testkit_only_as_dev_dependency() { - let v = check(&[c("drawcraft-render", &[("drawcraft-testkit", Normal, true)])]); + let v = check(&[c("vectorcraft-render", &[("vectorcraft-testkit", Normal, true)])]); assert!(matches!(v[..], [Violation::TestkitAsNormalDep { .. }])); - assert!(check(&[c("drawcraft-render", &[("drawcraft-testkit", Dev, true)])]).is_empty()); + assert!(check(&[c("vectorcraft-render", &[("vectorcraft-testkit", Dev, true)])]).is_empty()); // testkit itself may use anything up to L5 but not L6 crates. - assert!(check(&[c("drawcraft-testkit", &[("drawcraft-engine", Normal, true)])]).is_empty()); - assert!(!check(&[c("drawcraft-testkit", &[("drawcraft-ui-egui", Normal, true)])]).is_empty()); + assert!(check(&[c("vectorcraft-testkit", &[("vectorcraft-engine", Normal, true)])]).is_empty()); + assert!(!check(&[c("vectorcraft-testkit", &[("vectorcraft-ui-egui", Normal, true)])]).is_empty()); } #[test] fn apps_and_xtask_exempt() { - for app in ["drawcraft", "drawcraft-cli", "drawcraft-web", "xtask"] { - assert!(check(&[c(app, &[("egui", Normal, false), ("drawcraft-ui-egui", Normal, true)])]).is_empty()); + for app in ["vectorcraft", "vectorcraft-cli", "vectorcraft-web", "xtask"] { + assert!(check(&[c(app, &[("egui", Normal, false), ("vectorcraft-ui-egui", Normal, true)])]).is_empty()); } } @@ -322,17 +322,17 @@ mod tests { fn metadata_parsing() { let meta: Value = serde_json::from_str( r#"{"packages":[ - {"name":"drawcraft-doc","dependencies":[ - {"name":"drawcraft-geom","kind":null,"path":"/x/crates/geom"}, + {"name":"vectorcraft-doc","dependencies":[ + {"name":"vectorcraft-geom","kind":null,"path":"/x/crates/geom"}, {"name":"serde","kind":null}, {"name":"proptest","kind":"dev"}]}, - {"name":"drawcraft-geom","dependencies":[]} + {"name":"vectorcraft-geom","dependencies":[]} ]}"#, ) .unwrap(); let g = from_metadata(&meta).unwrap(); assert_eq!(g.len(), 2); - let doc = g.iter().find(|c| c.name == "drawcraft-doc").unwrap(); + let doc = g.iter().find(|c| c.name == "vectorcraft-doc").unwrap(); assert!(doc.deps[0].workspace && !doc.deps[1].workspace); assert_eq!(doc.deps[2].kind, Dev); assert!(check(&g).is_empty()); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index b8a7932a1..65f56e957 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -21,7 +21,7 @@ commands: ci fmt --check, clippy -D warnings, test, assets, layers, wasm (stops at first failure) corpus [--download] show where test corpora live; --download fetches PngSuite into corpus/pngsuite - bundle build dist/DrawCraft.app (macOS) with an icon rendered by DrawCraft + bundle build dist/VectorCraft.app (macOS) with an icon rendered by VectorCraft stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`) "; @@ -201,7 +201,7 @@ fn cmd_corpus(download: bool) -> Result<(), String> { "Test corpora live under {} (git-ignored, never committed). Tests that use a corpus skip cleanly when it is absent. - corpus/pngsuite/ PngSuite (public domain) — drawcraft-codecs compares every file + corpus/pngsuite/ PngSuite (public domain) — vectorcraft-codecs compares every file against the `image` crate. Fetch: cargo xtask corpus --download corpus/psd/ PSD samples from MIT/BSD projects (ag-psd, psd-tools test data). Copy files in manually; licences must be MIT/BSD/CC0.