mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M1.9: headless CLI/MCP export PDF/JPG/WebP; app JPEG/WebP export; ui.screenshot errors instead of hanging without a presented frame; docs (roadmap re-estimate, agent guide: bench, locked screens, disk)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5e15994012
commit
abe2ed7080
10 files changed
+56
-35
No files matched your search
@@ -30,10 +30,11 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p drawcraft -- --control 7979 [file.svg|file.drawcraft]`
|
||||
- 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`.
|
||||
- **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 bench FILE [--size WxH] [--iters N]` prints ms/frame (multi- and single-threaded). Check it before and after renderer changes.
|
||||
- MCP: `drawcraft-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.
|
||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; write manifests atomically. Each target dir grows to ~30 GB: delete yours when you finish (a full disk fails links with `errno=28`).
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and time-to-parity estimates. Update it whenever a milestone task lands.
|
||||
@@ -30,10 +30,11 @@ DrawCraft is a clean-room, open-source, Rust-native vector illustration app targ
|
||||
## Running and looking at the app
|
||||
- `cargo run --release -p drawcraft -- --control 7979 [file.svg|file.drawcraft]`
|
||||
- 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`.
|
||||
- **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 bench FILE [--size WxH] [--iters N]` prints ms/frame (multi- and single-threaded). Check it before and after renderer changes.
|
||||
- MCP: `drawcraft-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.
|
||||
- Parallel agents: separate `CARGO_TARGET_DIR` per agent; edit only the crates you own; write manifests atomically. Each target dir grows to ~30 GB: delete yours when you finish (a full disk fails links with `errno=28`).
|
||||
|
||||
## Roadmap
|
||||
`ROADMAP.md` (committed) tracks status, milestones and time-to-parity estimates. Update it whenever a milestone task lands.
|
||||
@@ -62,6 +62,8 @@ cargo run --release -p drawcraft # desktop app
|
||||
cargo run --release -p drawcraft -- examples/dusk-poster.drawcraft
|
||||
cargo run --release -p drawcraft -- --control 7979 # + JSON control channel
|
||||
cargo run --release -p drawcraft-cli -- mcp # MCP server (stdio)
|
||||
cargo run --release -p drawcraft-cli -- run --in examples/ribbons.drawcraft --export out.pdf # headless batch
|
||||
cargo run --release -p drawcraft-cli -- bench examples/neon-drive.drawcraft # render timing
|
||||
cargo xtask bundle # dist/DrawCraft.app (macOS)
|
||||
cd apps/drawcraft-web && trunk build --release # web build → dist/web
|
||||
cargo xtask ci # fmt, clippy, tests, layering, wasm
|
||||
@@ -73,7 +75,7 @@ Register with Claude Code: `claude mcp add drawcraft -- /path/to/drawcraft-cli m
|
||||
|
||||
DrawCraft is under active development. See **[ROADMAP.md](ROADMAP.md)** for what ships today, milestones, and honest time-to-parity estimates.
|
||||
|
||||
Workspace: `crates/{geom, color, doc, pathops, text, effects, trace, brush, render, svg, pdf, format, tools, engine, ui-egui, mcp, testkit}`, `apps/{drawcraft, drawcraft-cli, drawcraft-web}`. The egui frontend is a separate crate, so the UI can be swapped without touching the engine. Agent guide: [`CLAUDE.md`](CLAUDE.md).
|
||||
Workspace: `crates/{geom, color, doc, pathops, text, effects, trace, brush, render, svg, pdf, format, tools, engine, ui-egui, mcp, testkit}`, `apps/{drawcraft, drawcraft-cli, drawcraft-web}`. The egui frontend is a separate crate, so the UI can be swapped without touching the engine. Agent and contributor rules (clean-room, the asset policy, quality gates): [`AGENTS.md`](AGENTS.md). Every bundled asset is listed with its licence in [`ASSETS.md`](ASSETS.md).
|
||||
|
||||
## Crafting Apps
|
||||
|
||||
|
||||
+10
-9
@@ -4,7 +4,7 @@ DrawCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe
|
||||
|
||||
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.
|
||||
|
||||
_Last updated: 2026-09-30._
|
||||
_Last updated: 2026-10-01._
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -13,7 +13,7 @@ _Last updated: 2026-09-30._
|
||||
| Infrastructure (engine, command registry, history, render, formats, MCP, web, packaging, tests) | **~90%** |
|
||||
| Look & feel vs Illustrator 2026 default workspace (measured) | **~75–80%** |
|
||||
| Feature surface vs full Illustrator | **~45–50%** |
|
||||
| Estimated time to **checklist parity** (every menu item, tool, panel and dialog functional) | **~170–260 h** |
|
||||
| Estimated time to **checklist parity** (every menu item, tool, panel and dialog functional) | **~210–340 h** (sum of the milestone rows below) |
|
||||
| Estimated time to **1:1 feel** (edge cases, modifier nuances, typographic/colour fidelity) | **~500+ h** |
|
||||
|
||||
### Shipped so far
|
||||
@@ -40,6 +40,7 @@ _Last updated: 2026-09-30._
|
||||
- **Geometry and effects:**
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below.
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, and raster drop shadow, glows and feather.
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
||||
- **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:**
|
||||
@@ -55,21 +56,21 @@ _Last updated: 2026-09-30._
|
||||
| M0 | Skeleton + vertical slice | ✅ done | — |
|
||||
| M1 | Selection, transform, layers, MCP | ✅ mostly done (rotated persistent bbox pending) | 3–5 |
|
||||
| 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, freeform gradients pending | 10–15 |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | 🟡 Export for Screens (PNG/JPG/WebP/SVG/PDF × scales) done; system clipboard SVG/PNG interop, EPS/DXF pending | 8–12 |
|
||||
| M5 | Performance | 🟡 background render + caches + MT done; dirty-region rendering, GPU backend spike | 10–20 |
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | 🟡 panels done; opacity masks done (make/release, clip, invert, disable, link; render + SVG `<mask>` 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 SVG/PNG interop, EPS/DXF pending | 8–12 |
|
||||
| M5 | Performance | 🟡 background render + caches + MT done; `drawcraft-cli bench`; raster-effect docs fall back to single-threaded (neon-drive demo: 214 ms/frame) — MT filters, 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) | 🟡 in progress | 20–30 |
|
||||
| M8 | Transform & distort (Puppet Warp, Liquify tools, Envelopes, Blends, Perspective Grid) | 🟡 live Blends + Envelopes (warp/mesh/top object) done; Puppet Warp, Liquify, Perspective Grid pending | 20–30 |
|
||||
| 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 |
|
||||
| M9 | Live effects (+ 3D & Materials) | 🟡 2D effects done; 3D pending | 25–40 |
|
||||
| M10 | Brushes, symbols, patterns, Repeat | 🟡 pattern swatches (5 tile types, Pattern Options, editing mode, SVG `<pattern>`/PDF export) and live Repeat (radial/grid/mirror) done; brushes/symbols in progress | 8–15 |
|
||||
| M11 | Artboards & views (artboard panel/tool done; print tiling, multiple windows, presentation polish) | 🟡 | 10–15 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork done; CMYK/ICC, separations, Graphs pending | 25–40 |
|
||||
| M12 | Advanced color & art (CMYK/ICC, separations, Gradient Mesh, Live Paint, Image Trace, Graphs) | 🟡 Gradient Mesh, Live Paint, Image Trace (12 presets, 18 ms/1k² image), Recolor Artwork, colour management (ICC, soft proofing, separations preview) done; Graphs pending | 12–20 |
|
||||
| M13 | Automation (Actions ✅ record/playback, persisted; variables, scripting, batch) | 🟡 | 8–12 |
|
||||
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | ⬜ | 20–30 |
|
||||
| M14 | 1.0 polish (preferences, shortcut editor, workspaces, accessibility, packaging for all OSes) | 🟡 Preferences, shortcut editor, workspaces done; accessibility, Windows/Linux packaging pending | 12–20 |
|
||||
| — | Interaction fidelity pass (every tool's modifiers, Properties panel per context, isolation, nuance) | ⬜ | 40–60 |
|
||||
| — | Hardening at scale (big-file corpus, fuzzing, cross-platform + browser QA) | 🟡 | 30–50 |
|
||||
| | **Total to checklist parity** | | **~170–260** |
|
||||
| | **Total to checklist parity** (sum of the rows above) | | **~210–340** |
|
||||
|
||||
## Out of scope (by design or by law)
|
||||
- **Native `.ai` private data:** it's undocumented. We read the PDF-compatible part, so Illustrator-only live objects arrive as appearance.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! ```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|.drawcraft]... [--scale 2]
|
||||
//! 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]
|
||||
//! ```
|
||||
@@ -24,7 +24,7 @@ USAGE:
|
||||
|
||||
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, .drawcraft by extension). Prints one JSON result per step.
|
||||
order, export (.svg, .png, .pdf, .jpg, .webp, .drawcraft by extension). Prints one JSON result per step.
|
||||
|
||||
drawcraft-cli commands
|
||||
Print the command catalogue as JSON.
|
||||
|
||||
@@ -254,7 +254,7 @@ impl Headless {
|
||||
Ok(json!({"path": path}))
|
||||
}
|
||||
|
||||
/// `app.export {format?, path, scale?, artboard?}`: svg | png | drawcraft (format defaults to the extension).
|
||||
/// `app.export {format?, path, scale?, artboard?}`: svg | png | pdf | jpg | webp | drawcraft (format defaults to the extension).
|
||||
pub fn export(&mut self, p: &Value) -> Result<Value, String> {
|
||||
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));
|
||||
@@ -272,7 +272,12 @@ impl Headless {
|
||||
self.renderer.render_region(&doc, r, scale, false).to_png()
|
||||
}
|
||||
"drawcraft" => drawcraft_format::save(&doc, true),
|
||||
other => return Err(format!("unknown export format `{other}` (svg, png, drawcraft)")),
|
||||
// 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")});
|
||||
let r = self.session.execute("document.export", ¶ms).map_err(|e| e.to_string())?;
|
||||
return Ok(json!({"path": path, "format": fmt, "bytes": r["bytes"]}));
|
||||
}
|
||||
};
|
||||
std::fs::write(path, &bytes).map_err(|e| format!("write {path}: {e}"))?;
|
||||
Ok(json!({"path": path, "format": fmt, "bytes": bytes.len()}))
|
||||
|
||||
@@ -263,7 +263,7 @@ pub fn tool_definitions() -> Vec<Value> {
|
||||
"Export the active document: svg (artboard viewBox), png (rendered artboard) or drawcraft (native).",
|
||||
obj(
|
||||
json!({
|
||||
"format": {"type": "string", "enum": ["svg", "png", "drawcraft"], "description": "Default: from the path's extension"},
|
||||
"format": {"type": "string", "enum": ["svg", "png", "pdf", "jpg", "webp", "drawcraft"], "description": "Default: from the path's extension"},
|
||||
"path": string("Destination file"),
|
||||
"scale": num("PNG pixels per point (default 1)"),
|
||||
}),
|
||||
@@ -543,8 +543,8 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
|
||||
.and_then(Value::as_str)
|
||||
.map(str::to_ascii_lowercase)
|
||||
.unwrap_or_else(|| std::path::Path::new(path).extension().map(|e| e.to_string_lossy().to_ascii_lowercase()).unwrap_or_default());
|
||||
if !["svg", "png", "drawcraft"].contains(&fmt.as_str()) {
|
||||
return Err(format!("unknown export format `{fmt}` (svg, png, drawcraft)"));
|
||||
if !["svg", "png", "pdf", "jpg", "jpeg", "webp", "drawcraft"].contains(&fmt.as_str()) {
|
||||
return Err(format!("unknown export format `{fmt}` (svg, png, pdf, jpg, webp, drawcraft)"));
|
||||
}
|
||||
let scale = a.get("scale").and_then(Value::as_f64).unwrap_or(1.0);
|
||||
// The desktop app exports native files through Save (which also sets the document path).
|
||||
|
||||
@@ -167,7 +167,7 @@ pub fn save(app: &mut DrawcraftApp, path: Option<String>, save_as: bool) -> Resu
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Export the document as SVG / PNG / JPEG.
|
||||
/// Export the document as SVG / PDF / PNG / JPEG / WebP.
|
||||
pub fn export(app: &mut DrawcraftApp, format: &str, path: Option<String>, scale: f64) -> Result<String, String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
let doc = st.doc.clone();
|
||||
@@ -182,10 +182,14 @@ pub fn export(app: &mut DrawcraftApp, format: &str, path: Option<String>, 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())?,
|
||||
"png" | "jpg" | "jpeg" => {
|
||||
"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 != "png");
|
||||
if format == "png" { img.to_png() } else { encode_jpeg(&img)? }
|
||||
let img = app.canvas.renderer.render_region(&doc, r, scale, format == "jpg" || format == "jpeg");
|
||||
match format {
|
||||
"png" => img.to_png(),
|
||||
"webp" => img.to_webp(),
|
||||
_ => img.to_jpeg(90),
|
||||
}
|
||||
}
|
||||
other => return Err(format!("unknown export format `{other}`")),
|
||||
};
|
||||
@@ -194,11 +198,6 @@ pub fn export(app: &mut DrawcraftApp, format: &str, path: Option<String>, scale:
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn encode_jpeg(img: &drawcraft_render::Rendered) -> Result<Vec<u8>, String> {
|
||||
let _ = img;
|
||||
Err("JPEG export lands with M4.6".into())
|
||||
}
|
||||
|
||||
/// File → Place… (embed an image or SVG into the active document).
|
||||
pub fn place_bytes(app: &mut DrawcraftApp, name: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
let e = ext(name);
|
||||
|
||||
@@ -108,7 +108,8 @@ pub struct DrawcraftApp {
|
||||
/// Commands whose shortcuts the native menu handles (skip them in egui to avoid double firing).
|
||||
pub native_shortcuts: std::collections::HashSet<String>,
|
||||
control_rx: Option<Receiver<ControlRequest>>,
|
||||
pending_screenshots: Vec<(u64, Option<String>, Sender<ControlResponse>)>,
|
||||
/// (token, path, reply, deadline ms): a window that isn't presented never delivers its frame.
|
||||
pending_screenshots: Vec<(u64, Option<String>, Sender<ControlResponse>, f64)>,
|
||||
queued_screenshots: Vec<(u64, f64, u32)>,
|
||||
screenshot_token: u64,
|
||||
/// Synthetic input events (from the control channel) injected one step per frame.
|
||||
@@ -237,7 +238,7 @@ impl DrawcraftApp {
|
||||
let token = self.screenshot_token;
|
||||
let settle = ctx.global_style().animation_time as f64 * 2000.0 + 80.0;
|
||||
self.queued_screenshots.push((token, now_ms() + settle, 0));
|
||||
self.pending_screenshots.push((token, path, reply));
|
||||
self.pending_screenshots.push((token, path, reply, now_ms() + settle + 8000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,11 +279,22 @@ impl DrawcraftApp {
|
||||
.collect()
|
||||
});
|
||||
for (token, image) in events {
|
||||
if let Some(i) = self.pending_screenshots.iter().position(|(t, _, _)| *t == token) {
|
||||
let (_, path, reply) = self.pending_screenshots.remove(i);
|
||||
if let Some(i) = self.pending_screenshots.iter().position(|(t, ..)| *t == token) {
|
||||
let (_, path, reply, _) = self.pending_screenshots.remove(i);
|
||||
let _ = reply.send(control::save_screenshot(self, &image, path.as_deref()));
|
||||
}
|
||||
}
|
||||
let now = now_ms();
|
||||
self.pending_screenshots.retain(|(_, _, reply, deadline)| {
|
||||
if now < *deadline {
|
||||
return true;
|
||||
}
|
||||
let _ = reply.send(serde_json::json!({
|
||||
"ok": false,
|
||||
"error": "no frame was presented (screen locked, window minimized or fully covered); ui.render still renders the artboard"
|
||||
}));
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
/// Show a transient status message.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
|
||||
| `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?}` | |
|
||||
| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value}` | fill and submit the open dialog |
|
||||
| `ui.screenshot` | `{path?}` | capture the window (PNG) |
|
||||
| `ui.screenshot` | `{path?}` | capture the window (PNG). Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error |
|
||||
| `ui.render` | `{path?, scale?}` | render the artboard headlessly (PNG) |
|
||||
| `ui.resize` / `ui.focus` | | |
|
||||
| `app.open` / `app.save` / `app.export` / `app.quit` | `{path}` / `{path?}` / `{format, path, scale?}` | |
|
||||
Reference in new issue
Block a user