Merge main into fix/shape-builder-merge-off-ui-thread

This commit is contained in:
dotCooCoo
2026-10-28 16:48:55 -07:00
467 changed files with 6951 additions and 5642 deletions
+5 -1
View File
@@ -32,4 +32,8 @@ jobs:
- run: cargo clippy --locked -p vectorcraft --all-targets -- -D warnings
- run: cargo test --locked -p vectorcraft
- name: Check pending submission and queue refusal feedback
run: cargo test --locked -p vectorcraft-ui-egui tests_printps
shell: bash
# A filter that matches nothing still passes, so fail unless some test ran.
run: |
cargo test --locked -p vectorcraft-ui-egui --lib -- tests::printps | tee printps.log
grep -Eq 'test result: ok\. [1-9][0-9]* passed' printps.log || { echo "::error::tests::printps matched no tests"; exit 1; }
-54
View File
@@ -1,54 +0,0 @@
# Experimental, unsupported Windows 7 SP1 x64 portable build (docs/windows7.md). It runs by hand and
# weekly and for changes to the compatibility path; the weekly run shows when a dependency update starts
# importing a post-Windows-7 API. packaging/windows/windows7.ps1 does the work: it is the only build
# that uses the patched vendor/windows-link. A modern Windows runner proves compilation and host
# smoke tests only: run the checklist in docs/windows7.md on actual Windows 7 before claiming support.
name: Windows 7 x64 compatibility
on:
pull_request:
paths:
- "Cargo.toml"
- "Cargo.lock"
- "apps/vectorcraft/Cargo.toml"
- "apps/vectorcraft/src/printing.rs"
- "apps/vectorcraft/src/printing/**"
- "crates/ui-egui/src/print.rs"
- "crates/ui-egui/src/tests/printps.rs"
- "packaging/windows/windows7.ps1"
- ".github/workflows/windows7.yml"
schedule:
- cron: "23 5 * * 1" # weekly
workflow_dispatch:
permissions:
contents: read
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
build:
runs-on: windows-2022
timeout-minutes: 90
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
with:
arch: x64
- uses: dtolnay/rust-toolchain@e2a55d2ffb04f378e9626c28d38b36d230d1e12f # master
with:
toolchain: nightly-2026-10-01
components: rust-src
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
key: windows7-x64
- name: Build, check and test the portable package
shell: pwsh
run: ./packaging/windows/windows7.ps1 -Test
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: vectorcraft-windows7-x64-portable
path: dist/release/vectorcraft-windows7-x64-portable.zip
if-no-files-found: error
+1
View File
@@ -35,6 +35,7 @@ Formerly **DrawCraft** (renamed 2026-10-01): old `.drawcraft` files and `"format
- **Never crash: no panics in shipped code.** A crash loses the user's unsaved work. Files, pasted data, MCP and control messages, and command params are all untrusted input.
- Fallible work returns `Result` (or `Option`); callers handle it or propagate it with `?`. Failures reach the user as an error message (`EngineError` for commands, the crate's own error type or `String` below the engine), never as an abort.
- Workspace clippy lints ban these outside tests, so `cargo xtask ci` fails on them: `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`, `unimplemented!`. Use instead: `?`, `.ok_or(…)?`, `let … else { return … }`, `if let`, `unwrap_or` / `unwrap_or_default` / `unwrap_or_else`, `.first()` / `.last()` / `.split_last()`, and a real error for a match arm you believe is impossible.
- New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit `[workspace.lints]`. Production crate roots must declare `#![forbid(unsafe_code)]` before modules or other crate items; crate-level `//!` documentation may precede it.
- Don't panic implicitly either: use `.get(i)` instead of `v[i]` or `&s[a..b]` when the index comes from data. Don't divide integers by something that can be zero, cap sizes and counts read from input (allocations, images, loops), and check that floats are finite before casting them or looping on them.
- Don't silence errors just to satisfy the lint: use `let _ =` or `.ok()` only where ignoring the failure is the right behaviour, with a comment saying why.
- Tests may unwrap and panic (`#[test]`, `#[cfg(test)]`, `tests/`, `testkit`). Integration-test files start with an `#![allow(…)]` for that.
Generated
+28 -29
View File
@@ -1310,7 +1310,6 @@ dependencies = [
"egui-wgpu",
"egui-winit",
"egui_glow",
"glow",
"glutin",
"glutin-winit",
"image",
@@ -6036,7 +6035,7 @@ dependencies = [
[[package]]
name = "vectorcraft"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"arboard",
"block2 0.6.2",
@@ -6077,7 +6076,7 @@ dependencies = [
[[package]]
name = "vectorcraft-affinity"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"crc32fast",
"flate2",
@@ -6088,7 +6087,7 @@ dependencies = [
[[package]]
name = "vectorcraft-brush"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"kurbo 0.13.1",
"serde",
@@ -6100,7 +6099,7 @@ dependencies = [
[[package]]
name = "vectorcraft-cad"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"kurbo 0.13.1",
@@ -6116,7 +6115,7 @@ dependencies = [
[[package]]
name = "vectorcraft-cli"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"log",
"objc2-core-foundation",
@@ -6138,7 +6137,7 @@ dependencies = [
[[package]]
name = "vectorcraft-color"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"kurbo 0.13.1",
"moxcms",
@@ -6150,7 +6149,7 @@ dependencies = [
[[package]]
name = "vectorcraft-doc"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"chrono",
"hayro-ccitt",
@@ -6169,7 +6168,7 @@ dependencies = [
[[package]]
name = "vectorcraft-effects"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"kurbo 0.13.1",
@@ -6185,7 +6184,7 @@ dependencies = [
[[package]]
name = "vectorcraft-engine"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"craft-docpath",
"crc32fast",
@@ -6224,7 +6223,7 @@ dependencies = [
[[package]]
name = "vectorcraft-eps"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"fax",
"flate2",
@@ -6248,7 +6247,7 @@ dependencies = [
[[package]]
name = "vectorcraft-format"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"flate2",
"proptest",
@@ -6263,7 +6262,7 @@ dependencies = [
[[package]]
name = "vectorcraft-geom"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"kurbo 0.13.1",
"proptest",
@@ -6275,7 +6274,7 @@ dependencies = [
[[package]]
name = "vectorcraft-mcp"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"log",
@@ -6294,7 +6293,7 @@ dependencies = [
[[package]]
name = "vectorcraft-metafile"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"kurbo 0.13.1",
@@ -6308,7 +6307,7 @@ dependencies = [
[[package]]
name = "vectorcraft-pathops"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"kurbo 0.13.1",
"linesweeper",
@@ -6320,7 +6319,7 @@ dependencies = [
[[package]]
name = "vectorcraft-pdf"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"aes",
"chrono",
@@ -6351,7 +6350,7 @@ dependencies = [
[[package]]
name = "vectorcraft-plugins"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"serde",
"serde_json",
@@ -6365,7 +6364,7 @@ dependencies = [
[[package]]
name = "vectorcraft-render"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"flate2",
"gif",
@@ -6394,14 +6393,14 @@ dependencies = [
[[package]]
name = "vectorcraft-scriptcheck"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"proptest",
]
[[package]]
name = "vectorcraft-svg"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"craft-docpath",
"flate2",
@@ -6426,7 +6425,7 @@ dependencies = [
[[package]]
name = "vectorcraft-testkit"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"fax",
"image",
@@ -6444,7 +6443,7 @@ dependencies = [
[[package]]
name = "vectorcraft-text"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"harfrust 0.13.3",
"icu_segmenter",
@@ -6465,7 +6464,7 @@ dependencies = [
[[package]]
name = "vectorcraft-three-d"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"vectorcraft-geom",
"vectorcraft-pathops",
@@ -6473,7 +6472,7 @@ dependencies = [
[[package]]
name = "vectorcraft-tools"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"kurbo 0.13.1",
@@ -6489,7 +6488,7 @@ dependencies = [
[[package]]
name = "vectorcraft-trace"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"image",
"kurbo 0.13.1",
@@ -6501,7 +6500,7 @@ dependencies = [
[[package]]
name = "vectorcraft-ui-egui"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"bytemuck",
"craft-keymap",
@@ -6539,7 +6538,7 @@ dependencies = [
[[package]]
name = "vectorcraft-web"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"eframe",
"egui",
@@ -7670,7 +7669,7 @@ checksum = "9440ea3e5aeabb0ac63af70daf835274065238cdd0cec83418f417eae38bacee"
[[package]]
name = "xtask"
version = "0.17.0"
version = "0.18.0"
dependencies = [
"serde_json",
]
+4 -1
View File
@@ -4,7 +4,7 @@ members = ["crates/*", "apps/*", "xtask"]
default-members = ["crates/*", "apps/vectorcraft", "apps/vectorcraft-cli", "xtask"]
[workspace.package]
version = "0.17.0"
version = "0.18.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -116,6 +116,9 @@ webbrowser = "1"
unsafe_code = "deny"
[workspace.lints.clippy]
cognitive_complexity = "deny"
too_many_arguments = "deny"
wildcard_imports = "deny"
dbg_macro = "warn"
# VectorCraft never crashes: shipped code returns `Result`/`Option` instead of panicking (see
# AGENTS.md › Robustness). Tests may unwrap (clippy.toml).
-5
View File
@@ -30,11 +30,6 @@ based in part on the work of the Independent JPEG Group.
WebAssembly plug-ins run in the wasmi interpreter (MIT OR Apache-2.0); its wasmparser dependency is
under the Apache License 2.0 with the LLVM exception.
The experimental Windows 7 build (packaging/windows/windows7.ps1) compiles a modified copy of the
windows-link crate 0.2.1 (vendor/windows-link; Microsoft, MIT OR Apache-2.0) that binds CoTaskMemFree
through ole32.dll. Its license texts are in vendor/windows-link and ship in that build's ZIP; every
other build uses the unmodified crate from crates.io.
Builds made with craft-fonts (the CRAFT_FONTS_DIR build option; all official releases) embed the
fonts listed in https://github.com/storytold/craft-fonts/blob/main/ATTRIBUTION.md (BIZ UDPGothic,
Shippori Mincho, BIZ UDMincho, Noto Sans CJK SC and Noto Sans Arabic in the current manifest;
-2
View File
@@ -227,8 +227,6 @@ claude mcp add vectorcraft -- /path/to/vectorcraft-cli mcp --automation-read-roo
The details are in [`docs/mcp.md`](docs/mcp.md) and [`docs/control-protocol.md`](docs/control-protocol.md).
For the experimental, unsupported 64-bit Windows 7 build, see [Windows 7 instructions](docs/windows7.md).
## Status
VectorCraft is under active development. [**ROADMAP.md**](ROADMAP.md) has the stage, the numbers by dimension
-1
View File
@@ -35,7 +35,6 @@ winsafe = { version = "0.0.29", default-features = false, features = ["kernel",
# DirectWrite's system font collection and GDI's font list (the desktop app's src/system_fonts.rs,
# shared): fonts that font services such as Adobe Fonts load in place (#579, #1258). MIT OR
# Apache-2.0.
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies]
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi"] }
# CoreText's font manager (the desktop app's src/mac_fonts.rs, shared): fonts that apps and font
+2 -2
View File
@@ -46,7 +46,7 @@ mod perf;
#[path = "../../vectorcraft/src/prefs_dir.rs"]
mod prefs_dir;
/// The desktop app's DirectWrite font lister, shared: exports and MCP find the same fonts.
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
#[path = "../../vectorcraft/src/system_fonts.rs"]
mod system_fonts;
@@ -162,7 +162,7 @@ fn help() -> Result<(), String> {
fn main() -> ExitCode {
// Before any font lookup: the fonts font services load (#579).
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
system_fonts::install();
#[cfg(target_os = "macos")]
mac_fonts::install();
+110 -74
View File
@@ -447,113 +447,158 @@ fn mcp_protocol_over_stdio() {
let mut stdin = child.stdin.take().unwrap();
let mut lines = BufReader::new(child.stdout.take().unwrap()).lines();
let mut notes = vec![];
fn send(stdin: &mut std::process::ChildStdin, v: Value) {
writeln!(stdin, "{v}").unwrap();
exercise_mcp_core(&mut stdin, &mut lines, &mut notes);
exercise_mcp_resource_errors(&mut stdin, &mut lines, &mut notes);
exercise_mcp_document_resources(&mut stdin, &mut lines, &mut notes);
exercise_mcp_logging(&mut stdin, &mut lines, &mut notes);
exercise_mcp_parse_error(&mut stdin, &mut lines, &mut notes);
drop(stdin);
for line in lines {
let v: Value = serde_json::from_str(&line.unwrap()).unwrap();
assert_eq!(v["jsonrpc"], "2.0", "{v}");
notes.push(v);
}
// Next reply with this id; notifications (no id) are stashed for later.
fn recv(lines: &mut std::io::Lines<BufReader<std::process::ChildStdout>>, notes: &mut Vec<Value>, id: u64) -> Value {
loop {
let line = lines.next().unwrap().unwrap();
let v: Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["jsonrpc"], "2.0", "{v}");
if v.get("id").and_then(|i| i.as_u64()) == Some(id) {
return v;
}
notes.push(v);
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert!(!String::from_utf8_lossy(&out.stderr).contains("panicked"));
let messages = notes.iter().filter(|n| n["method"] == "notifications/message").count();
assert!(messages > 0, "no log records after logging/setLevel: {notes:?}");
}
fn mcp_send(stdin: &mut std::process::ChildStdin, v: Value) {
writeln!(stdin, "{v}").unwrap();
}
fn mcp_recv(lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>, notes: &mut Vec<Value>, id: u64) -> Value {
loop {
let line = lines.next().unwrap().unwrap();
let v: Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["jsonrpc"], "2.0", "{v}");
if v.get("id").and_then(|i| i.as_u64()) == Some(id) {
return v;
}
notes.push(v);
}
}
send(
&mut stdin,
fn exercise_mcp_core(
stdin: &mut std::process::ChildStdin,
lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
notes: &mut Vec<Value>,
) {
mcp_send(
stdin,
json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"0"}}}),
);
let r = recv(&mut lines, &mut notes, 1);
let r = mcp_recv(lines, notes, 1);
assert_eq!(r["result"]["serverInfo"]["name"], "vectorcraft");
assert!(r["result"]["capabilities"]["prompts"].is_object(), "{r}");
send(&mut stdin, json!({"jsonrpc":"2.0","method":"notifications/initialized"}));
mcp_send(stdin, json!({"jsonrpc":"2.0","method":"notifications/initialized"}));
send(&mut stdin, json!({"jsonrpc":"2.0","id":2,"method":"prompts/list","params":{}}));
let r = recv(&mut lines, &mut notes, 2);
mcp_send(stdin, json!({"jsonrpc":"2.0","id":2,"method":"prompts/list","params":{}}));
let r = mcp_recv(lines, notes, 2);
assert!(r["result"]["prompts"].as_array().is_some_and(|p| !p.is_empty()), "{r}");
send(&mut stdin, json!({"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"poster","arguments":{"brief":"a jazz festival"}}}));
let r = recv(&mut lines, &mut notes, 3);
mcp_send(stdin, json!({"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"poster","arguments":{"brief":"a jazz festival"}}}));
let r = mcp_recv(lines, notes, 3);
assert!(r["result"]["messages"][0]["content"]["text"].as_str().is_some_and(|t| t.contains("a jazz festival")), "{r}");
send(
&mut stdin,
mcp_send(
stdin,
json!({"jsonrpc":"2.0","id":4,"method":"completion/complete","params":{"ref":{"type":"ref/prompt","name":"export-set"},"argument":{"name":"formats","value":"sv"}}}),
);
let r = recv(&mut lines, &mut notes, 4);
let r = mcp_recv(lines, notes, 4);
assert!(r["result"]["completion"]["values"].as_array().is_some_and(|v| v.contains(&json!("svg"))), "{r}");
send(&mut stdin, json!({"jsonrpc":"2.0","id":5,"method":"resources/list","params":{}}));
assert!(recv(&mut lines, &mut notes, 5)["result"]["resources"].as_array().is_some_and(|v| v.len() == 3));
send(&mut stdin, json!({"jsonrpc":"2.0","id":6,"method":"resources/templates/list","params":{}}));
assert!(recv(&mut lines, &mut notes, 6)["result"]["resourceTemplates"].as_array().is_some_and(|v| v.len() >= 4));
mcp_send(stdin, json!({"jsonrpc":"2.0","id":5,"method":"resources/list","params":{}}));
assert!(mcp_recv(lines, notes, 5)["result"]["resources"].as_array().is_some_and(|v| v.len() == 3));
mcp_send(stdin, json!({"jsonrpc":"2.0","id":6,"method":"resources/templates/list","params":{}}));
assert!(mcp_recv(lines, notes, 6)["result"]["resourceTemplates"].as_array().is_some_and(|v| v.len() >= 4));
send(
&mut stdin,
mcp_send(
stdin,
json!({"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"rectangle","x":10,"y":10,"width":60,"height":40}}}),
);
let r = recv(&mut lines, &mut notes, 7);
let r = mcp_recv(lines, notes, 7);
assert_eq!(r["result"]["isError"], false, "{r}");
let id: i64 = serde_json::from_str::<Value>(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap()["id"].as_i64().unwrap();
send(&mut stdin, json!({"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri": format!("vectorcraft://object/{id}")}}));
let r = recv(&mut lines, &mut notes, 8);
mcp_send(stdin, json!({"jsonrpc":"2.0","id":8,"method":"resources/read","params":{"uri": format!("vectorcraft://object/{id}")}}));
let r = mcp_recv(lines, notes, 8);
let body: Value = serde_json::from_str(r["result"]["contents"][0]["text"].as_str().unwrap()).unwrap();
assert_eq!(body["id"].as_i64(), Some(id), "{body}");
}
fn exercise_mcp_resource_errors(
stdin: &mut std::process::ChildStdin,
lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
notes: &mut Vec<Value>,
) {
// Unknown resources (-32002) and bad template values (-32602) stay apart.
send(&mut stdin, json!({"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"vectorcraft://nope" }}));
assert_eq!(recv(&mut lines, &mut notes, 9)["error"]["code"], -32002);
send(&mut stdin, json!({"jsonrpc":"2.0","id":10,"method":"resources/read","params":{"uri":"vectorcraft://object/"}}));
assert_eq!(recv(&mut lines, &mut notes, 10)["error"]["code"], -32602);
mcp_send(stdin, json!({"jsonrpc":"2.0","id":9,"method":"resources/read","params":{"uri":"vectorcraft://nope" }}));
assert_eq!(mcp_recv(lines, notes, 9)["error"]["code"], -32002);
mcp_send(stdin, json!({"jsonrpc":"2.0","id":10,"method":"resources/read","params":{"uri":"vectorcraft://object/"}}));
assert_eq!(mcp_recv(lines, notes, 10)["error"]["code"], -32602);
}
fn exercise_mcp_document_resources(
stdin: &mut std::process::ChildStdin,
lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
notes: &mut Vec<Value>,
) {
// Large containers slice through run_command document.node: a truncated level reports
// childCount, and a bad limit is a tool error, not a crash or a full read.
send(
&mut stdin,
mcp_send(
stdin,
json!({"jsonrpc":"2.0","id":20,"method":"tools/call","params":{"name":"draw_shape","arguments":{"shape":"ellipse","x":90,"y":10,"width":30,"height":30}}}),
);
assert_eq!(recv(&mut lines, &mut notes, 20)["result"]["isError"], false);
let run = |id: u64, command: &str, params: Value| json!({"jsonrpc":"2.0","id":id,"method":"tools/call","params":{"name":"run_command","arguments":{"command":command,"params":params}}});
send(&mut stdin, run(21, "select.all", json!({})));
recv(&mut lines, &mut notes, 21);
send(&mut stdin, run(22, "object.group", json!({})));
let r = recv(&mut lines, &mut notes, 22);
assert_eq!(mcp_recv(lines, notes, 20)["result"]["isError"], false);
mcp_send(stdin, run_command(21, "select.all", json!({})));
mcp_recv(lines, notes, 21);
mcp_send(stdin, run_command(22, "object.group", json!({})));
let r = mcp_recv(lines, notes, 22);
let group = serde_json::from_str::<Value>(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap()["id"].clone();
send(&mut stdin, run(23, "document.node", json!({"id": group, "summary": true, "childLimit": 1})));
let r = recv(&mut lines, &mut notes, 23);
mcp_send(stdin, run_command(23, "document.node", json!({"id": group, "summary": true, "childLimit": 1})));
let r = mcp_recv(lines, notes, 23);
let sliced: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
assert_eq!(sliced["children"].as_array().map(Vec::len), Some(1), "{sliced}");
assert_eq!(sliced["childCount"], 2, "{sliced}");
send(&mut stdin, run(24, "document.node", json!({"id": group, "summary": true, "depth": -1})));
assert_eq!(recv(&mut lines, &mut notes, 24)["result"]["isError"], true);
mcp_send(stdin, run_command(24, "document.node", json!({"id": group, "summary": true, "depth": -1})));
assert_eq!(mcp_recv(lines, notes, 24)["result"]["isError"], true);
// Skeleton then drill: a depth-0 inspect is the layer skeleton with counts, and
// document.find locates nodes without dumping the tree.
send(&mut stdin, run(25, "document.inspect", json!({"depth": 0})));
let r = recv(&mut lines, &mut notes, 25);
mcp_send(stdin, run_command(25, "document.inspect", json!({"depth": 0})));
let r = mcp_recv(lines, notes, 25);
let skel: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
assert!(skel["layers"][0].get("children").is_none(), "{skel}");
assert!(skel["artboards"].as_array().is_some_and(|a| !a.is_empty()), "{skel}");
send(&mut stdin, run(26, "document.find", json!({"kind": "ellipse"})));
let r = recv(&mut lines, &mut notes, 26);
mcp_send(stdin, run_command(26, "document.find", json!({"kind": "ellipse"})));
let r = mcp_recv(lines, notes, 26);
let found: Value = serde_json::from_str(r["result"]["content"][0]["text"].as_str().unwrap()).unwrap();
assert_eq!(found["total"], 1, "{found}");
assert!(found["matches"][0]["path"].as_array().is_some_and(|p| !p.is_empty()), "{found}");
send(&mut stdin, run(27, "document.find", json!({})));
assert_eq!(recv(&mut lines, &mut notes, 27)["result"]["isError"], true);
mcp_send(stdin, run_command(27, "document.find", json!({})));
assert_eq!(mcp_recv(lines, notes, 27)["result"]["isError"], true);
}
fn exercise_mcp_logging(
stdin: &mut std::process::ChildStdin,
lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
notes: &mut Vec<Value>,
) {
// The binary installed its logger: records arrive as notifications/message, and only
// after the client asked for them.
send(&mut stdin, json!({"jsonrpc":"2.0","id":11,"method":"logging/setLevel","params":{"level":"debug"}}));
assert!(recv(&mut lines, &mut notes, 11).get("error").is_none());
send(&mut stdin, json!({"jsonrpc":"2.0","id":12,"method":"tools/list","params":{}}));
assert!(recv(&mut lines, &mut notes, 12)["result"]["tools"].as_array().is_some_and(|v| !v.is_empty()));
mcp_send(stdin, json!({"jsonrpc":"2.0","id":11,"method":"logging/setLevel","params":{"level":"debug"}}));
assert!(mcp_recv(lines, notes, 11).get("error").is_none());
mcp_send(stdin, json!({"jsonrpc":"2.0","id":12,"method":"tools/list","params":{}}));
assert!(mcp_recv(lines, notes, 12)["result"]["tools"].as_array().is_some_and(|v| !v.is_empty()));
}
fn exercise_mcp_parse_error(
stdin: &mut std::process::ChildStdin,
lines: &mut std::io::Lines<std::io::BufReader<std::process::ChildStdout>>,
notes: &mut Vec<Value>,
) {
// Malformed input is a parse error, and the server keeps serving afterwards.
stdin.write_all(b"{not json\n").unwrap();
let parse_err: Value = loop {
@@ -566,21 +611,12 @@ fn mcp_protocol_over_stdio() {
};
assert_eq!(parse_err["error"]["code"], -32700);
assert!(parse_err["id"].is_null(), "{parse_err}");
send(&mut stdin, json!({"jsonrpc":"2.0","id":13,"method":"ping","params":{}}));
assert!(recv(&mut lines, &mut notes, 13).get("error").is_none());
drop(stdin);
// Drain whatever the debug window queued; every line off the protocol stream parses.
for line in lines.by_ref() {
let line = line.unwrap();
let v: Value = serde_json::from_str(&line).unwrap();
assert_eq!(v["jsonrpc"], "2.0", "{v}");
notes.push(v);
}
let out = child.wait_with_output().unwrap();
assert!(out.status.success());
assert!(!String::from_utf8_lossy(&out.stderr).contains("panicked"));
let messages = notes.iter().filter(|n| n["method"] == "notifications/message").count();
assert!(messages > 0, "no log records after logging/setLevel: {notes:?}");
mcp_send(stdin, json!({"jsonrpc":"2.0","id":13,"method":"ping","params":{}}));
assert!(mcp_recv(lines, notes, 13).get("error").is_none());
}
fn run_command(id: u64, command: &str, params: Value) -> Value {
json!({"jsonrpc":"2.0","id":id,"method":"tools/call","params":{"name":"run_command","arguments":{"command":command,"params":params}}})
}
/// `vectorcraft-cli commands | head -1` panicked with "failed printing to
-6
View File
@@ -13,8 +13,6 @@ workspace = true
default = ["wgpu", "accessibility"]
wgpu = ["eframe/wgpu", "dep:winsafe"]
accessibility = ["eframe/accesskit"]
# Build with --no-default-features --features windows7 and the win7 Rust target.
windows7 = ["eframe/glow"]
[dependencies]
vectorcraft-engine = { workspace = true, features = ["bundled-fonts"] }
@@ -37,9 +35,6 @@ image = { workspace = true }
webbrowser = { workspace = true }
# The window's monitors, size and position (the same winit eframe runs on).
winit = { version = "0.30", default-features = false }
# The win7 target retains the viewer fallback; craft-print's WinRT PDF renderer needs newer Windows.
[target.'cfg(all(not(target_arch = "wasm32"), not(target_vendor = "win7")))'.dependencies]
craft-print = { workspace = true }
[target.'cfg(windows)'.dependencies]
@@ -56,7 +51,6 @@ winsafe = { version = "0.0.29", default-features = false, features = ["user"], o
# Win32_UI_WindowsAndMessaging has `MSG` and `WM_POINTERDOWN`, which src/windows_stylus.rs uses to
# read a pen's barrel button from its pointer messages. Win32_UI_Input_Pointer has
# `GetPointerPenInfo`, which it uses to read the pen's pressure and which end of the pen is down.
[target.'cfg(all(windows, not(target_vendor = "win7")))'.dependencies]
windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] }
[target.'cfg(target_os = "macos")'.dependencies]
+6 -9
View File
@@ -1,5 +1,7 @@
use super::prefs::save_prefs;
use super::*;
#[cfg(all(windows, not(target_vendor = "win7")))]
use super::windows_stylus;
use super::{App, VectorcraftApp, file_access, window};
/// The parts of [`App::raw_input_hook`] that every window needs, for the OS windows of floating
/// panels: a click or Escape that closes the macOS menu bar's open menu, and the pen barrel
@@ -10,12 +12,12 @@ pub(crate) fn native_input_hook(app: &VectorcraftApp) -> Option<vectorcraft_ui_e
app.services.native_menu.is_some().then(|| std::sync::Arc::new(mac_menu::raw_input_hook) as vectorcraft_ui_egui::NativeInputHook)
}
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
pub(crate) fn native_input_hook(_: &VectorcraftApp) -> Option<vectorcraft_ui_egui::NativeInputHook> {
Some(std::sync::Arc::new(windows_stylus::raw_input_hook))
}
#[cfg(not(any(target_os = "macos", all(windows, not(target_vendor = "win7")))))]
#[cfg(not(any(target_os = "macos", windows)))]
pub(crate) fn native_input_hook(_: &VectorcraftApp) -> Option<vectorcraft_ui_egui::NativeInputHook> {
None
}
@@ -41,7 +43,7 @@ impl eframe::App for App {
mac_menu::raw_input_hook(raw);
}
// A pen contact with the barrel button held presses the secondary button (Windows Ink).
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
windows_stylus::raw_input_hook(raw);
file_access::unconfined(|| self.app.raw_input_hook(ctx, raw));
}
@@ -65,15 +67,10 @@ impl eframe::App for App {
discard_marked_text();
}
}
#[cfg(not(feature = "windows7"))]
fn on_exit(&mut self) {
file_access::unconfined(|| save_prefs(&self.app));
end_before_teardown(self.frames);
}
#[cfg(feature = "windows7")]
fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) {
file_access::unconfined(|| save_prefs(&self.app));
}
}
impl App {
+4 -2
View File
@@ -164,7 +164,9 @@ mod win {
use clipboard_win::{formats, options::NoClear, raw};
use super::*;
use super::{
BITMAP, Cursor, Flavour, PNG, SVG, SystemClipboard, TEXT, copied_file, decode_png, dib_image, png_flavour, text_of, windows_formats,
};
/// The device-independent bitmaps, the one that carries alpha first. Each is there whenever the
/// other or a `CF_BITMAP` is: the system converts between them.
@@ -309,7 +311,7 @@ mod win {
mod portable {
use std::borrow::Cow;
use super::*;
use super::{BITMAP, Flavour, PNG, SystemClipboard, TEXT, copied_file, decode_png, png_flavour, text_of};
/// What our last write put on the clipboard.
enum Ours {
+8 -2
View File
@@ -1,4 +1,10 @@
use super::*;
#[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"))]
use super::portal_save;
#[cfg(target_os = "linux")]
use super::tablet;
#[cfg(all(windows, not(target_vendor = "win7")))]
use super::windows_stylus;
use super::{ClipboardProbeFactory, FilePick, PickRequest, Services, clipboard, cursor_theme, fileio, printing};
/// The window file dialogs belong to.
pub(crate) type Parent = Option<std::sync::Arc<winit::window::Window>>;
@@ -266,7 +272,7 @@ pub(crate) fn services(parent: Parent, ui: egui::Context) -> Services {
#[cfg(target_os = "macos")]
stylus: mac_tablet::start(),
// Windows reads it from the pen's pointer messages (`windows_stylus`).
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
stylus: Some(windows_stylus::start()),
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
write: Some(Box::new(write_file)),
+3 -14
View File
@@ -15,13 +15,6 @@
//! keyboard isn't confined.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#[cfg(all(feature = "windows7", any(feature = "wgpu", feature = "accessibility")))]
compile_error!("windows7 requires --no-default-features (wgpu and accessibility must be disabled)");
#[cfg(all(windows, feature = "windows7", not(target_vendor = "win7")))]
compile_error!("windows7 requires --target x86_64-win7-windows-msvc; the ordinary Windows target still imports newer APIs");
#[cfg(all(target_vendor = "win7", not(feature = "windows7")))]
compile_error!("the win7 target requires --no-default-features --features windows7");
mod automation_args;
mod clipboard;
#[cfg(feature = "wgpu")]
@@ -50,7 +43,7 @@ mod open_documents;
mod portal_save;
mod prefs_dir;
mod printing;
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
mod system_fonts;
mod tablet;
mod window;
@@ -190,7 +183,7 @@ fn main() -> std::process::ExitCode {
let logger = logging::install();
vectorcraft_ui_egui::i18n::detect_system_lang_in_background();
// Before the first font scan (the app's start): the fonts font services load (#579).
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
system_fonts::install();
#[cfg(target_os = "macos")]
mac_fonts::install();
@@ -295,7 +288,7 @@ fn main() -> std::process::ExitCode {
// With Windows Ink, winit reports a pen as a touch and drops its barrel button and which end
// is down; the message hook notes each pointer contact and whether it starts with the barrel
// button held, and writes the pen's end into the pen feed (`windows_stylus`).
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
let options = {
use winit::platform::windows::EventLoopBuilderExtWindows as _;
let mut options = options;
@@ -436,10 +429,6 @@ fn main() -> std::process::ExitCode {
let (loss, ctx) = (graphics_loss.clone(), cc.egui_ctx.clone());
rs.device.set_device_lost_callback(move |reason, msg| loss.report(&ctx, format!("{reason:?}: {msg}")));
}
#[cfg(feature = "windows7")]
{
app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into());
}
app.custom_titlebar = custom_titlebar;
if let Some(port) = control_port {
let rx = control_server::start(port, cc.egui_ctx.clone());
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{VectorcraftApp, fileio, prefs_dir};
/// Where UI preferences live: ~/Library/Application Support/VectorCraft (macOS),
/// %APPDATA%\VectorCraft (Windows), $XDG_CONFIG_HOME or ~/.config/vectorcraft (Linux).
+3 -11
View File
@@ -1,15 +1,8 @@
//! Desktop printer transport. VectorCraft composes every page and copy before submission;
//! craft-print owns discovery and spooling. Windows 7 keeps its viewer-based transport because
//! Windows.Data.Pdf is unavailable there.
#[cfg(not(all(windows, any(feature = "windows7", target_vendor = "win7"))))]
//! craft-print owns discovery and spooling.
use std::process::Command;
use vectorcraft_ui_egui::print::{PrintJob, PrintService, Printer};
#[cfg(all(windows, any(feature = "windows7", target_vendor = "win7")))]
mod windows7;
#[cfg(all(windows, any(feature = "windows7", target_vendor = "win7")))]
use windows7 as system;
pub struct SystemPrint;
impl PrintService for SystemPrint {
fn printers(&mut self) -> Vec<Printer> {
@@ -36,9 +29,8 @@ impl PrintService for SystemPrint {
}
}
#[cfg(not(all(windows, any(feature = "windows7", target_vendor = "win7"))))]
mod system {
use super::*;
use super::{Command, PrintJob, Printer};
pub fn printers() -> Vec<Printer> {
craft_print::printers().into_iter().map(|p| Printer { name: p.name, default: p.default }).collect()
}
@@ -95,7 +87,7 @@ mod system {
}
}
#[cfg(all(test, windows, not(any(feature = "windows7", target_vendor = "win7"))))]
#[cfg(all(test, windows))]
mod tests {
use super::system::setup_command;
use std::ffi::OsStr;
-129
View File
@@ -1,129 +0,0 @@
//! The legacy viewer print verb is needed on Windows 7, where Windows.Data.Pdf is absent.
use std::path::PathBuf;
use std::process::Command;
use vectorcraft_ui_egui::print::{PrintJob, Printer};
/// Where a job's PDF waits for the spooler: a fresh file in the temp folder. Files of jobs more
/// than an hour old (the print verb reads them after Print returns) are removed first.
fn job_file(pdf: &[u8]) -> Result<PathBuf, String> {
let dir = std::env::temp_dir().join("vectorcraft-print");
std::fs::create_dir_all(&dir).map_err(|e| format!("can't make {}: {e}", dir.display()))?;
for e in std::fs::read_dir(&dir).into_iter().flatten().flatten() {
let old = e.metadata().and_then(|m| m.modified()).ok().and_then(|t| t.elapsed().ok()).is_some_and(|age| age.as_secs() > 3600);
if old {
// Another job may still be reading it: leave it for next time.
let _ = std::fs::remove_file(e.path());
}
}
let stamp = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or_default();
let path = dir.join(format!("job-{}-{stamp}.pdf", std::process::id()));
// The viewer reads asynchronously, so this cannot use immediate drop cleanup. Never
// truncate an existing path, and remove incomplete files or refused launches immediately.
use std::io::Write as _;
let mut file =
std::fs::OpenOptions::new().write(true).create_new(true).open(&path).map_err(|e| format!("can't create {}: {e}", path.display()))?;
if let Err(e) = file.write_all(pdf).and_then(|_| file.flush()) {
drop(file);
let _ = std::fs::remove_file(&path);
return Err(format!("can't write {}: {e}", path.display()));
}
Ok(path)
}
/// Run `c` → its output, or its error message.
fn output(c: &mut Command) -> Result<String, String> {
let program = c.get_program().to_string_lossy().to_string();
let out = c.output().map_err(|e| format!("can't run {program}: {e}"))?;
if out.status.success() {
return Ok(String::from_utf8_lossy(&out.stdout).into_owned());
}
let err = String::from_utf8_lossy(&out.stderr).trim().to_string();
Err(if err.is_empty() { format!("{program} failed ({})", out.status) } else { err })
}
/// PowerShell's `<Default>\t<Name>` lines → the printers.
fn windows_printers(text: &str) -> Vec<Printer> {
text.lines()
.filter_map(|l| l.trim_end_matches('\r').split_once('\t'))
.filter(|(_, name)| !name.trim().is_empty())
.map(|(default, name)| Printer { name: name.trim().to_string(), default: default.trim().eq_ignore_ascii_case("true") })
.collect()
}
/// `s` as a PowerShell single-quoted string.
fn ps_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "''"))
}
fn powershell(script: &str) -> Command {
use std::os::windows::process::CommandExt as _;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
let mut c = Command::new(
std::path::PathBuf::from(std::env::var_os("SystemRoot").unwrap_or_else(|| "C:\\Windows".into()))
.join("System32")
.join("WindowsPowerShell")
.join("v1.0")
.join("powershell.exe"),
);
c.args([
"-NoProfile",
"-NonInteractive",
"-Command",
&format!("$ErrorActionPreference = 'Stop'; [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false); {script}"),
])
.creation_flags(CREATE_NO_WINDOW);
c
}
pub fn printers() -> Vec<Printer> {
let script = "Get-WmiObject Win32_Printer | ForEach-Object { \"$($_.Default)`t$($_.Name)\" }";
output(&mut powershell(script)).map(|t| windows_printers(&t)).unwrap_or_default()
}
fn submit(job: &PrintJob, file: &std::path::Path) -> Result<String, String> {
// The PDF viewer's print verb reads the file after this returns: it stays for a while.
let path = ps_quote(&file.to_string_lossy());
let script = match job.printer {
Some(p) => format!(
"Start-Process -FilePath {path} -Verb PrintTo -ArgumentList {} -WindowStyle Hidden",
ps_quote(&format!("\"{}\"", p.replace('"', "")))
),
None => format!("Start-Process -FilePath {path} -Verb Print -WindowStyle Hidden"),
};
output(&mut powershell(&script)).map(|_| String::new()).map_err(|e| format!("no app prints PDF files here ({e})"))
}
pub fn setup(_printer: Option<&str>) -> Result<(), String> {
let mut command = {
let mut command = Command::new("control.exe");
command.arg("printers");
command
};
command.spawn().map(|_| ()).map_err(|e| e.to_string())
}
pub fn print(job: &PrintJob) -> Result<String, String> {
let file = job_file(job.pdf)?;
let result = submit(job, &file);
if result.is_err() {
// No viewer accepted the launch, so no asynchronous reader needs this document.
let _ = std::fs::remove_file(file);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn powershell_lines_and_quotes_are_preserved() {
assert_eq!(windows_printers("True\tOffice Laser\r\nFalse\t "), [Printer { name: "Office Laser".into(), default: true }]);
assert_eq!(ps_quote("it's.pdf"), "'it''s.pdf'");
}
#[test]
fn job_files_hold_the_pdf_for_the_async_viewer() {
let path = job_file(b"%PDF-1.7 test").unwrap();
assert_eq!(std::fs::read(&path).unwrap(), b"%PDF-1.7 test");
std::fs::remove_file(path).unwrap();
}
}
+3 -3
View File
@@ -1,4 +1,6 @@
use super::*;
use super::window;
#[cfg(target_os = "linux")]
use super::xwayland;
/// The window, Dock, taskbar and app-switcher icon (`assets/app-icon/`, see its README). macOS gets
/// the version with Apple's transparent margin; elsewhere the full-bleed tile. The app ID matches
@@ -113,8 +115,6 @@ pub(crate) fn native_options(custom_titlebar: bool) -> eframe::NativeOptions {
.with_title_shown(false)
.with_icon(app_icon())
.with_app_id("ai.storyteller.vectorcraft"),
#[cfg(feature = "windows7")]
renderer: eframe::Renderer::Glow,
..Default::default()
}
}
+2 -2
View File
@@ -16,7 +16,7 @@
//! mouse message that Windows did not make from a pen or a touch
//! (<https://learn.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages>)
//! clears the feed.
#![cfg_attr(not(all(windows, not(target_vendor = "win7"))), allow(dead_code))]
#![cfg_attr(not(windows), allow(dead_code))]
use std::cell::RefCell;
use std::sync::OnceLock;
@@ -68,7 +68,7 @@ pub fn start() -> StylusFeed {
/// message it takes from the thread's queue, before dispatching it: notes each pointer contact's id
/// and whether the barrel button was held, and writes the pen's pressure and end into the feed.
/// It never consumes the message.
#[cfg(all(windows, not(target_vendor = "win7")))]
#[cfg(windows)]
#[allow(unsafe_code)]
pub fn msg_hook(msg: *const std::ffi::c_void) -> bool {
use windows::Win32::UI::Input::Pointer::{GetPointerPenInfo, POINTER_FLAG_INCONTACT, POINTER_PEN_INFO};
+1
View File
@@ -6,3 +6,4 @@ allow-panic-in-tests = true
disallowed-types = [
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
]
cognitive-complexity-threshold = 30
+1 -1
View File
@@ -1,5 +1,5 @@
use super::read::MAX_TREE_DEPTH;
use super::*;
use super::{Affine, Kind, Node, ObjId, Point, Rect, Stream, Tag};
/// The bounds of what visible `nodes` draw (control points of outlines, corners of images, frames
/// and tables), in document pixels.
+4 -1
View File
@@ -1,5 +1,8 @@
use super::bounds::{describe, is_container, mask_extent, nodes_bounds, pages, rect, tree_bounds, union};
use super::*;
use super::{
Affine, Archive, BTreeMap, Blend, Document, Effect, Error, HashSet, Image, Kind, Limits, Node, ObjId, Paint, Path, Point, Rect, Spread, Stream,
Tag, paint, stream,
};
/// Largest embedded colour profile kept (real CMYK profiles are a few megabytes).
const MAX_PROFILE_BYTES: usize = 32 << 20;
+1 -1
View File
@@ -1,6 +1,6 @@
use super::profile::{PressureProfile, WidthProfile};
use super::stroke_style::{ArrowAlign, Arrowhead, Dash, LineCap, LineJoin, StrokeAlign};
use super::*;
use super::{BlendMode, Deserialize, Paint, Serialize};
/// A live effect in an appearance stack. Parameters are interpreted by `vectorcraft-effects`.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, Serialize};
/// Variable-width profile: (position 0..1 along the path, left width factor, right width factor),
/// in order along the path. Two points at the same position make a discontinuous point: the
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum LineCap {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, Deserialize, Node, NodeId, NodeKind, Rect, Serialize, SymbolBoxes, skip};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Artboard {
+4 -1
View File
@@ -3,7 +3,10 @@ use super::graphic_style::default_graphic_styles;
use super::guides::{GridPrefs, RulerMode};
use super::limits::{DocError, FORMAT_VERSION, MAX_COORD, MAX_NEXT_ID};
use super::units::{ColorMode, Unit};
use super::*;
use super::{
Arc, BTreeMap, ColorProfiles, DocMetadata, DocSetup, Document, LAYER_COLORS, LayerColor, Node, NodeId, NodeKind, PatternDef, Places, Point,
RasterEffectsSettings, Rect, Vec2,
};
impl Document {
/// Is everything but the layers the same in `other`, as the next version of the document after
+4 -1
View File
@@ -1,7 +1,10 @@
use super::graphic_style::GraphicStyle;
use super::limits::MAX_NEXT_ID;
use super::records::{ImageBlob, SavedSelection, SavedView};
use super::*;
use super::{
Arc, Document, ImageObject, Node, NodeKind, SYMBOL_EXPORT_TYPE, SYMBOL_NINE_SLICE, SYMBOL_REGISTRATION, SymbolExportType, SymbolNineSlice,
SymbolRegistration,
};
impl Document {
/// The pixels image `im` shows: its own ([`ImageObject::pixels`]) or this document's.
+1 -1
View File
@@ -1,6 +1,6 @@
use super::document_ext::free_id;
use super::guides::Guide;
use super::*;
use super::{Affine, Arc, Document, Node, NodeId, NodeKind, PatternDef, Point, Rect, Selection, SymbolBoxes, Vec2, artboard_holds};
impl Document {
/// Union of the geometric bounds of `ids` (their visual bounds when `visual`), with symbol
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Appearance, Deserialize, Knockout, Node, Rect, Serialize, skip};
/// A named graphic style (Graphic Styles panel): an appearance plus the object's transparency.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, NodeId, Serialize, Vec2};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Guide {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::NodeId;
/// Coordinates beyond this many points exceed the supported document range.
pub const MAX_COORD: f64 = 4.0e6;
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, PathData, Serialize};
// =====================================================================================
// Data
+1 -1
View File
@@ -1,6 +1,6 @@
use super::expand::{Hooks, Outliner, Warper, map_node};
use super::mesh::{GradientMesh, MeshPoint, grid_len, grid_points, nodes_bounds, surface_of};
use super::*;
use super::{Affine, Arc, Color, Deserialize, Node, NodeId, NodeKind, PathData, Point, Rect, Serialize, Vec2, yes};
/// How an envelope distorts its content.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+4 -1
View File
@@ -1,7 +1,10 @@
use super::envelope::{EnvelopeOptions, expand_envelope};
use super::mesh::GradientMesh;
use super::warp::map_nonlinear;
use super::*;
use super::{
Affine, Appearance, AppearanceItem, Arc, FillLayer, ImageObject, Node, NodeId, NodeKind, Paint, PathData, Point, Rect, SubPath, Vec2,
blend_expand,
};
/// Optional hook that converts nodes the pure evaluation can't map (text) to outlines.
pub type Outliner<'a> = Option<&'a dyn Fn(&Node) -> Option<Node>>;
+4 -1
View File
@@ -1,6 +1,9 @@
use super::envelope::{EnvelopeKind, H_DOWN, H_LEFT, H_RIGHT, H_UP, envelope_grid, mesh_at};
use super::warp::{WarpStyle, flatten_subpath, resample, warp_point};
use super::*;
use super::{
Affine, Anchor, Arc, Color, CubicBez, Deserialize, Node, ParamCurve, PathData, Point, Rect, Serialize, Shape, SubPath, Vec2, fifty, lerp_color,
one,
};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MeshPoint {
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Anchor, BezPath, CubicBez, PI, ParamCurve, PathData, Point, SubPath};
// =====================================================================================
// Warp maps (shared with vectorcraft-effects' Warp effects)
+1 -1
View File
@@ -1,6 +1,6 @@
use super::kind::NodeKind;
use super::live_shape::LiveShape;
use super::*;
use super::{Deserialize, Node, Serialize};
/// Window → Attributes: an object's settings that don't change how it prints.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,5 +1,5 @@
use super::kind::NodeKind;
use super::*;
use super::{BezPath, FillRule, Node, Outliner, Rect, shapes};
/// Unites filled regions (each under its own fill rule) into one path filled non-zero. Booleans
/// live above this crate (`vectorcraft-pathops`), so callers supply it.
+1 -1
View File
@@ -2,7 +2,7 @@ use super::attributes::ObjectAttributes;
use super::kind::NodeKind;
use super::live_shape::LiveShape;
use super::opacity::OpacityMask;
use super::*;
use super::{Node, PathData};
/// The parts of a [`Node`] that few objects have: kept out of line, so that the nodes without
/// them stay small ([`Node::extras`]).
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, Serialize};
/// Stable per-document object id. Never reused.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
+1 -1
View File
@@ -1,6 +1,6 @@
use super::ids::LayerColor;
use super::live_shape::LiveShape;
use super::*;
use super::{Affine, Arc, BlendSpec, Deserialize, EnvelopeKind, FillRule, GradientMesh, Node, PathData, RepeatSpec, Serialize, TextObject, yes};
/// Embedded or linked raster image.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Affine, CornerKind, Deserialize, PathData, Point, Rect, Serialize, cut_corners, shapes};
/// Live shape parameters (Illustrator's "Live Shapes"). The node's path is regenerated from these
/// while the shape stays live; editing anchors directly converts it to a plain path.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::kind::NodeKind;
use super::*;
use super::{Arc, Color, Deserialize, Node, Paint, Serialize, yes};
/// Opacity mask: the luminance of the mask art sets the object's opacity (white = opaque).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -3,7 +3,7 @@ use super::ids::{LayerColor, NodeId};
use super::kind::NodeKind;
use super::live_shape::{Scaling, moves_only};
use super::opacity::Knockout;
use super::*;
use super::{Affine, Appearance, Arc, BlendMode, EnvelopeKind, FillRule, Node, PathData, Point, Rect, RepeatSpec, SYMBOL_SQUARE, Shape, SymbolBoxes};
impl Node {
pub fn new(id: NodeId, kind: NodeKind) -> Self {
+1 -1
View File
@@ -3,7 +3,7 @@ use super::extras::{Extras, NO_EXTRAS, NodeExtras};
use super::ids::NodeId;
use super::kind::NodeKind;
use super::opacity::{Knockout, OpacityMask};
use super::*;
use super::{Appearance, BlendMode, Deserialize, Node, PathData, Serialize, one, yes};
/// How a [`Node`] is written: the flat field list of the file format, in its order, borrowed from
/// the node and its extras (nothing is copied). The node reads back through `NodeWire`.
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Arc, Deserialize, Node, NodeId, Point, Serialize};
/// New art on a locked artboard ([`Document::new_art`]). Setting it changes the document in
/// place, so a selection change that sets it doesn't replace the document.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::para_style::FirstBaseline;
use super::*;
use super::{Deserialize, PathData, Serialize};
/// Area Type Options "Align" (vertical): where the lines of each row/column sit in it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,5 +1,5 @@
use super::para_style::ParaDirection;
use super::*;
use super::{Appearance, AppearanceItem, Color, Dash, Deserialize, FillLayer, LineCap, LineJoin, Paint, Serialize, StrokeAlign, StrokeLayer};
/// Character attributes (the Character panel).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+1 -1
View File
@@ -3,7 +3,7 @@ use super::char_style::CharStyle;
use super::para_style::ParaStyle;
use super::runs::{RubySpan, TextKind, TextRun, move_anchors, reverse_from_end};
use super::tabs_lists::ParaList;
use super::*;
use super::{Affine, PathData, Point, Rect, TextObject, TextStyleScale, Vec2};
impl TextObject {
/// None for collapsed or unrepresentable transforms, whose type dimensions cannot
+1 -1
View File
@@ -1,6 +1,6 @@
use super::char_style::CharStyle;
use super::tabs_lists::{ParaList, TabStop};
use super::*;
use super::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub enum Justify {
+1 -1
View File
@@ -1,5 +1,5 @@
use super::char_style::CharStyle;
use super::*;
use super::{Affine, Deserialize, PathData, Point, Rect, Serialize, Vec2, yes};
/// A named character or paragraph style: the attributes it sets (a subset of [`CharStyle`] or
/// [`ParaStyle`] fields, by their serialized names). Text using it records the name.
+1 -1
View File
@@ -1,5 +1,5 @@
use super::char_style::{CharPosition, CharStyle};
use super::*;
use super::{Deserialize, Serialize};
/// Tab stop alignment (Tabs panel).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{Deserialize, Serialize};
/// Measurement units (display only; the model is always in points).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
+1 -1
View File
@@ -1,4 +1,4 @@
use super::*;
use super::{CROP_MARKS, EXTRUDE, INFLATE, PATHFINDER_EFFECTS, RASTERIZE, REVOLVE, ROTATE, TRAP, Value, json, trap};
/// Catalogue entry for one effect.
#[derive(Clone, Debug, PartialEq)]
+1 -1
View File
@@ -4,7 +4,7 @@ use serde_json::Value;
use vectorcraft_geom::kurbo::ParamCurveArclen;
use vectorcraft_geom::{Affine, Anchor, ParamCurve, PathData, Point, Rect, SubPath, Vec2};
use crate::util::*;
use crate::util::{catmull_rom, fit_piece, flag, map_nonlinear_within, mean_size, noise, normal_at, num, pieces, seed, seg_cubic, smooth_points};
fn diag(b: Rect) -> f64 {
b.width().hypot(b.height()).max(1e-6)
+5 -2
View File
@@ -1,5 +1,8 @@
use super::params::{is_geometry, merged_params};
use super::*;
use super::{
Appearance, AppearanceItem, BezPath, Effect, FillRule, Node, NodeKind, PathData, Rect, StrokeLayer, Value, WarpStyle, distort, stroke, stylize,
util, warp,
};
/// Context the geometry effects may need from the object's appearance.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
@@ -167,7 +170,7 @@ fn builds_within(id: &str) -> bool {
/// [`apply_one`] whose result may pass `max` when the effect doesn't build within it.
fn apply_unchecked(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &GeomContext, max: usize) -> PathData {
use util::*;
use util::{join, num};
if let Some(plugin) = vectorcraft_plugins::effect::plugin_id(id) {
let paints = vectorcraft_plugins::effect::Paints { appearance: ctx.appearance, item: ctx.item };
return vectorcraft_plugins::effect::apply(plugin, p, path, b, ctx.rule, paints).unwrap_or_else(|| path.clone());
+1 -1
View File
@@ -1,5 +1,5 @@
use super::catalog::{catalog_index, plugin_info};
use super::*;
use super::{CROP_MARKS, Effect, Map, Node, PIXEL_EFFECTS, RASTERIZE, Value, is_adjustment, is_pathfinder, is_three_d, json, util};
/// Dialog defaults for `id` (`None` for unknown effects).
pub fn default_params(id: &str) -> Option<Value> {
+8
View File
@@ -1755,6 +1755,10 @@ fn brush_strokes_params_take_defaults_and_stay_in_range() {
fx("brushStrokes.sumiE", json!({"strokeWidth": 2, "strokePressure": 16, "contrast": -1})),
PixelFx::SumiE { width: 3.0, pressure: 15.0, contrast: 0.0 }
);
brush_strokes_outsets();
}
fn brush_strokes_outsets() {
// Outsets: the strokes and the shifts reach out of the object as far as into it; Accented
// Edges and Sumi-e keep the object's shape.
let b = Rect::new(0.0, 0.0, 100.0, 50.0);
@@ -2561,6 +2565,10 @@ fn artistic_params_take_defaults_and_stay_in_range() {
fx("artistic.plasticWrap", json!({"highlightStrength": -1, "detail": 99, "smoothness": 0})),
PixelFx::PlasticWrap { highlight: 0.0, detail: 15.0, smoothness: 1.0 }
);
artistic_outsets_and_docs();
}
fn artistic_outsets_and_docs() {
for id in [
"artistic.coloredPencil",
"artistic.cutout",
+1 -1
View File
@@ -9,7 +9,7 @@ use vectorcraft_geom::{Point, Rect};
use crate::merged_params;
use crate::pixel::{self, PixelFx, PixelSpace};
use crate::util::*;
use crate::util::{flag, num, text};
/// Effect › Rasterize's id.
pub const RASTERIZE: &str = "rasterize";
+1 -1
View File
@@ -5,7 +5,7 @@ use vectorcraft_geom::kurbo;
use vectorcraft_geom::shapes::{self, KAPPA};
use vectorcraft_geom::{Affine, Anchor, PathData, Point, Rect, SubPath, corners};
use crate::util::*;
use crate::util::{catmull_rom, flag, noise, num, seed};
/// Convert to Shape (rectangle / rounded rectangle / ellipse) around the bounds.
pub fn convert_to_shape(id: &str, b: Rect, p: &Value) -> PathData {
+1 -1
View File
@@ -6,7 +6,7 @@
use serde_json::Value;
use vectorcraft_geom::{PathData, Point, Rect};
use crate::util::*;
use crate::util::{map_nonlinear_within, num, text};
pub use vectorcraft_doc::live::{WarpStyle, warp_point};
+4 -1
View File
@@ -28,8 +28,11 @@ use vectorcraft_geom::Rect;
use super::edit::{outermost, selected_roots};
use super::opacitymask::percent;
use super::paint::paint_from;
use super::*;
use super::{AppearanceAttrs, EyedropperAttrs, FillAttrs, StrokeAttrs};
use super::{
CommandSpec, NodeId, Result, Session, bad, check_numbers, check_switches, cmd, has_doc, has_selection, id_param, ids_param, index, leaf_targets,
ok, paint_targets, str_param, switch_or, targets,
};
use crate::EngineError;
pub fn specs() -> Vec<CommandSpec> {
+1 -1
View File
@@ -8,7 +8,7 @@ use vectorcraft_doc::{ExportAsset, NodeId};
use super::edit::{roots_of, selected_roots};
use super::fileio::{SCREEN_SETTINGS_KEYS, check_screen_settings, export_crop, export_screens, store_screen_settings};
use super::*;
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, checked_ids_param, cmd, has_doc, ok, str_param};
/// The longest asset name kept (characters).
const MAX_NAME: usize = 255;
+1 -1
View File
@@ -10,7 +10,7 @@ use vectorcraft_geom::FillRule;
use super::overprint::{AttributesInfo, apply_overprint, edit_counted, overprint_jobs};
use super::path::paths_in;
use super::*;
use super::{CommandSpec, NodeId, Result, Session, bad, cmd, has_doc, str_param, targets};
/// How many URLs the Attributes panel's recent list keeps.
const RECENT_URLS: usize = 10;
+1 -1
View File
@@ -22,7 +22,7 @@ use super::clipboard::resources::SwatchChoices;
use super::patterncmds::add_pattern;
use super::swatch::str_list;
use super::swatchlib::{FileData, LibraryFile, LibraryInfo, stem};
use super::*;
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, has_doc, str_param};
/// The extension of saved brush libraries: native documents.
pub const BRUSHES_EXT: &str = vectorcraft_format::EXTENSION;
+4 -1
View File
@@ -18,7 +18,10 @@ use vectorcraft_geom::{Affine, Point, Vec2};
use super::edit::{outermost, selected_roots};
use super::pathops::{remove_each, separate};
use super::swatch::{anchor, names_param};
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, cmd, color_value, find_command, has_doc, has_selection, id_param, ids_param, number_or, ok,
str_param, switch_or,
};
use crate::EngineError;
mod brushes;
+5 -1
View File
@@ -1,4 +1,8 @@
use super::*;
use super::{
AppearanceItem, Arc, BTreeSet, Bristle, Brush, BrushKind, Color, Document, EngineError, Node, NodeId, NodeKind, Paint, Result, Session, Value,
anchor, bad, brush, check_numbers, find_command, id_param, ids_json, json, name_param, names_param, ok, roots, selection_art, set_unknown,
str_param,
};
// ---------- brushes ----------
+5 -1
View File
@@ -1,4 +1,8 @@
use super::*;
use super::{
Affine, AppearanceItem, Arc, CURRENT_SYMBOL, Color, Document, EngineError, FillLayer, HALF, Node, NodeId, NodeKind, Paint, Point, Result,
SIZES_KEY, Session, Symbol, SymbolExportType, SymbolNineSlice, SymbolRegistration, Value, Vec2, bad, brush, color_value, ids_json, json,
name_param, number_or, ok, remove_each, roots, selection_art, separate, set_unknown, str_param, switch_or,
};
// ---------- symbols ----------
+4 -1
View File
@@ -27,7 +27,10 @@ use super::draw2::replace_with_pieces;
use super::edit::{outermost, selected_roots};
use super::paint::paint_from;
use super::pathops::{Beside, shape_node};
use super::*;
use super::{
CommandSpec, EngineError, Result, Session, always, bad, check_numbers, cmd, color_value, f64_or, finite_numbers, has_doc, id_param, ids_param,
number_or, point_param, str_param, switch_or,
};
mod live_paint;
mod shape_builder;
@@ -1,4 +1,8 @@
use super::*;
use super::{
Appearance, Arc, Beside, Document, EDGE_NAME, EngineError, FACE_NAME, GapOptions, GapSize, LIVE_PAINT_NAME, Node, NodeId, Paint, PathData, Point,
Rect, Result, SOURCES_NAME, Selection, Session, SubPath, Value, b, bad, build_children, edge_near, face_at, finite, id_param, ids_json,
is_live_paint, json, number_or, paint_from, po, point_param, repaint_from, replace_roots, root_ids, source_nodes, source_shapes, switch_or,
};
/// `options` as stored on a Live Paint group: none for the defaults.
fn stored_gap_options(options: GapOptions) -> Option<Box<GapOptions>> {
@@ -1,4 +1,8 @@
use super::*;
use super::{
Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, SubPath, Touched, Value, b, bad, check_numbers,
f64_or, fill_param, finite, gap_param, ids_json, json, marquee_param, po, points_param, remnant, replace_roots, replace_with_pieces, root_ids,
shape_node, switch_or,
};
pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "shapeBuilder.merge";
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Appearance, AppearanceItem, Arc, Beside, Color, Document, EngineError, LineCap, LineJoin, LiveShape, Node, NodeId, NodeKind, Paint, Point,
Result, Session, StrokeLayer, TRACE_NAME, TraceView, Value, bad, fill_only, ids_json, json, root_ids, shape_node, str_param, switch_or, tr,
};
// ---------- Image Trace ----------
+1 -1
View File
@@ -25,7 +25,7 @@ pub use flavours::{AI, BITMAP, EMF, EPS, FILE_HEAD, Flavour, PASTE_ORDER, PDF, P
pub(crate) use resources::SwatchChoices;
pub use resources::{ArtResources, art_resources};
use super::*;
use super::{CommandSpec, Result, Session, always, bad, cmd, f64_or, has_doc, point_param, str_param};
use crate::DocState;
pub fn specs() -> Vec<CommandSpec> {
+1 -1
View File
@@ -12,7 +12,7 @@ use vectorcraft_geom::{Affine, Point};
use super::super::fileio;
use super::super::place::pt_per_px;
use super::*;
use super::{Clipboard, CommandSpec, Result, Session, always, bad, cmd, f64_or, has_doc, looks_like_svg, point_param, str_param};
/// Plain text: a type-only copy's text, else the SVG markup (Include SVG Code).
pub const TEXT: &str = "text/plain";
+3 -1
View File
@@ -18,7 +18,9 @@ use vectorcraft_doc::recolor::{ColorVisitor, Reach, map_links, recolor_node};
use vectorcraft_doc::{Document, ImageBlob, Node, NodeId, NodeKind};
use super::edit::selected_roots;
use super::*;
use super::{
CommandSpec, EngineError, Result, Session, always, bad, bool_or, cmd, color_value, f64_or, has_selection, ids_param, str_param, unique_name,
};
pub fn specs() -> Vec<CommandSpec> {
vec![
+3 -1
View File
@@ -17,7 +17,9 @@ use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::ColorMode;
use vectorcraft_render::proof;
use super::*;
#[cfg(not(target_arch = "wasm32"))]
use super::EngineError;
use super::{CommandSpec, Result, Session, always, bad, bool_or, cmd, color_value, has_doc, str_param, targets};
/// `Document.unknown` key under which files before format v3 kept the assigned profiles (migrated
/// to `Document::color_profiles` on load).
+1 -1
View File
@@ -9,7 +9,7 @@ use vectorcraft_color::harmony::{Harmony, THEME_SIZE};
use vectorcraft_color::recolor::ColorKey;
use vectorcraft_tools::params::color_json;
use super::*;
use super::{CommandSpec, Result, Session, always, bad, cmd, color_value, has_doc, str_param};
/// A saved colour theme ([`crate::Prefs::color_themes`]).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+4 -1
View File
@@ -5,7 +5,10 @@ use vectorcraft_doc::{Appearance, CharStyle, LiveShape, Node, NodeKind, TextObje
use vectorcraft_geom::{Affine, Anchor, AnchorKind, FillRule, PathData, Point, Rect, SubPath, shapes};
use super::newart::NewArt;
use super::*;
use super::{
CommandSpec, NodeId, Result, Session, bad, bool_or, check_numbers, check_switches, cmd, color_value, f64_or, f64_req, finite_numbers, has_doc,
number_or, str_param, switch_or,
};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -8,7 +8,7 @@ use vectorcraft_doc::NodeId;
use vectorcraft_svg::{CssOptions, CssSheet, css_rules};
use super::fileio::{default_name, encode_with_warnings, isolated, write_file};
use super::*;
use super::{CommandSpec, Result, Session, bad, cmd, has_doc, str_param, targets};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -15,7 +15,7 @@ use vectorcraft_pathops::BoolOp;
use super::draw2::{components, erase_outline, ids_json, replace_with_pieces};
use super::edit::selected_roots;
use super::pathops::{Beside, node_path};
use super::*;
use super::{CommandSpec, Result, Session, bad, cmd, finite_numbers, has_selection, point_param, str_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -1,5 +1,5 @@
//! Linked dimensions: generated groups share the existing text/path render and export pipeline.
use super::*;
use super::{CommandSpec, EngineError, Result, Session, cmd, has_doc};
use serde_json::{Value, json};
use std::sync::Arc;
use vectorcraft_color::{Color, Paint};
+4 -1
View File
@@ -14,7 +14,10 @@ use vectorcraft_tools::distort::perspective::{self as persp, PerspectiveGrid, Pl
use vectorcraft_tools::distort::{PinSet, arap, collect_points, mesh_for, project_node, stroke_owner, warp_from_rest, warp_node_with};
use super::edit::{duplicate_in, outermost, selected_roots};
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, check_switches, cmd, f64_or, f64_req, finite_numbers, has_doc, has_selection, id_param,
ids_param, matrix_param, number_or, ok, point_indices, point_param, str_param, switch_or, targets,
};
use crate::EngineError;
mod perspective;
@@ -1,4 +1,8 @@
use super::*;
use super::{
Affine, Arc, Document, EngineError, Homography, NodeId, NodeKind, PerspectiveGrid, Plane, Point, Result, Session, Value, bad, check_numbers,
check_switches, collect_points, duplicate_in, finite_numbers, id_param, ids_param, json, matrix_param, number_or, ok, outermost, persp,
point_param, project_node, selected_roots, str_param, switch_or,
};
// ---------- perspective grid ----------
+1 -1
View File
@@ -9,7 +9,7 @@ use vectorcraft_doc::{ColorMode, Document, Node, NodeId, NodeKind, RepeatKind, R
use vectorcraft_geom::{Affine, Rect};
use vectorcraft_text::embed::Embedding;
use super::*;
use super::{CommandSpec, Result, Session, bad, bool_or, cmd, has_doc, has_selection, str_param, targets};
pub fn specs() -> Vec<CommandSpec> {
vec![
+4 -1
View File
@@ -12,7 +12,10 @@ use vectorcraft_geom::Vec2;
use super::edit::selected_roots;
use super::pathops::Beside;
use super::select::{selectable_ids, walk_selectable};
use super::*;
use super::{
CommandSpec, EngineError, Point, Result, Session, bad, bool_or, checked_ids_param, cmd, f64_or, f64_req, has_doc, has_selection, id_param, ok,
str_param,
};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -7,7 +7,7 @@ use vectorcraft_doc::{Background, CharPosition, CharStyle, DocSetup, Document, E
use super::flatten::{FlattenOptions, FlattenerPreset};
use super::*;
use super::{CommandSpec, Result, Session, bad, cmd, color_value, has_doc};
const C: &str = "document.setup";
+4 -1
View File
@@ -17,7 +17,10 @@ use vectorcraft_tools::draw2::{eraser_area, eraser_tip};
use super::create::{add_look, insert_look};
use super::newart::NewArt;
use super::pathops::{Beside, remove_each};
use super::*;
use super::{
CommandSpec, Result, Session, bad, check_numbers, cmd, f64_or, has_anchors, has_doc, has_selection, id_param, index, number_or, ok, str_param,
switch_or,
};
use crate::EngineError;
mod anchors;
+4 -1
View File
@@ -1,5 +1,8 @@
use super::common::{MAX_INPUT, path_kind, path_mut, point_entry, usize_opt, usize_req, within_input_bound};
use super::*;
use super::{
Anchor, BTreeSet, Beside, Color, Document, EngineError, Node, NodeId, NodeKind, Paint, PathData, Point, Result, Selection, Session, Value, Vec2,
add_look, bad, check_numbers, f64_or, id_param, json, number_or, ok, po, str_param, switch_or,
};
// ---------- Curvature ----------
+1 -1
View File
@@ -2,7 +2,7 @@ use super::anchors::{convert_anchor, curvature, curvature_edit, remove_anchor, r
use super::cut::{erase_region, erase_segments, knife, split};
use super::freehand::{blob, freehand};
use super::smooth::{join_scrub, smooth_region};
use super::*;
use super::{CommandSpec, cmd, has_anchors, has_doc, has_selection};
pub fn specs() -> Vec<CommandSpec> {
vec![
+4 -1
View File
@@ -1,4 +1,7 @@
use super::*;
use super::{
Appearance, Arc, Beside, Cap, Color, Document, EngineError, FillRule, Join, Node, NodeId, NodeKind, Paint, PathData, Point, PressureProfile,
Rect, Result, Selection, Session, SubPath, Value, Vec2, bad, index, json, po,
};
// ---------- helpers ----------
+4 -1
View File
@@ -1,7 +1,10 @@
use super::common::{
dist_to_polyline, ids_json, path_mut, path_of, points_param, poly_bounds, replace_with_pieces, sibling_with, target_paths, usize_opt,
};
use super::*;
use super::{
Anchor, AnchorKind, Beside, BezPath, BoolOp, Cap, EngineError, FillRule, Join, NodeId, ParamCurve, PathData, Point, Result, Session, SubPath,
Value, bad, eraser_area, eraser_tip, f64_or, fill_contains, id_param, json, number_or, po,
};
/// Split a filled path into connected pieces (each outer contour with its holes).
pub(in super::super) fn components(pd: &PathData) -> Vec<PathData> {
+4 -1
View File
@@ -2,7 +2,10 @@ use super::common::{
brush_region, path_kind, path_mut, path_of, points_param, poly_bounds, samples_param, stroke_paint, stroke_pressure, subpath_len, target_paths,
within_input_bound,
};
use super::*;
use super::{
Anchor, Appearance, BTreeSet, BoolOp, Color, EngineError, FillRule, NewArt, NodeId, NodeKind, Paint, PathData, Point, PressureProfile, Result,
Selection, Session, SimplifyOptions, SubPath, Value, bad, id_param, insert_look, json, number_or, po, remove_each, str_param, switch_or,
};
// ---------- Pencil / Paintbrush ----------
+4 -1
View File
@@ -1,5 +1,8 @@
use super::common::{dist_to_polyline, ids_json, path_mut, path_of, points_param, poly_bounds, target_paths};
use super::*;
use super::{
Anchor, BezPath, CubicBez, NodeId, ParamCurve, ParamCurveNearest, PathData, Point, Result, Session, SimplifyOptions, SubPath, Value, json,
number_or, po,
};
// ---------- Smooth ----------
+5 -1
View File
@@ -8,7 +8,11 @@ use vectorcraft_doc::{Document, Node, NodeId, RulerMode, Unit};
use vectorcraft_geom::{Affine, Point, Vec2};
use super::clipboard::SwatchChoices;
use super::*;
use super::{
CommandSpec, Result, Session, always, bad, bool_or, can_clear_history, can_redo, can_undo, checked_ids_param, cmd, f64_or, f64_req,
finite_numbers, has_clipboard, has_doc, has_selection, has_selection_or_artboard_tool, has_selection_or_guides, id_param, index, ok, point_param,
str_param,
};
use crate::{Clipboard, EngineError, inspect};
/// Register editing commands, including document-to-document artwork drops and their parameter contracts.
+1 -1
View File
@@ -15,7 +15,7 @@ use vectorcraft_render::effects;
use super::appearance::{ItemTarget, aligned, appearance_targets, index_param, item_target, item_target_at, shared_stack};
use super::expand::has_seeded;
use super::rasterfx;
use super::*;
use super::{CommandSpec, EngineError, NodeId, Result, Session, always, bad, cmd, fileio, has_doc, str_param};
pub fn specs() -> Vec<CommandSpec> {
vec![
+1 -1
View File
@@ -22,7 +22,7 @@ use super::edit::selected_roots;
use super::menucmds::{run_raw, squash};
use super::object::as_clipping_path;
use super::pathops::{assemble, node_path, outline_strokes, replace_leaves, styled_copy, with_transparency};
use super::*;
use super::{CommandSpec, EngineError, Result, Session, bad, bool_or, cmd, f64_or, has_selection, str_param};
const C: &str = "object.expand";
/// Expand Gradient To → Specify … Objects: the default and the most.
+1 -1
View File
@@ -7,7 +7,7 @@ use serde_json::{Map, Value, json};
use vectorcraft_doc::metadata::{MAX_RATING, iso8601};
use vectorcraft_doc::{ColorMode, CopyrightStatus, DocMetadata, Document};
use super::*;
use super::{Result, Session, bad};
const C: &str = "file.info";
/// Longest text field, in characters.
+1 -1
View File
@@ -92,7 +92,7 @@ pub use svg::options_map as svg_options;
#[cfg(not(target_arch = "wasm32"))]
pub use vectorcraft_format::write_atomic;
use super::*;
use super::{CommandSpec, Result, Session, always, bad, cmd, has_doc, has_selection};
use crate::EngineError;
mod access;

Some files were not shown because too many files have changed in this diff Show More