From abe2ed7080513cc7fda3125e36f74ccbce58ecbb Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 00:30:38 -0400 Subject: [PATCH] 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 --- AGENTS.md | 5 +++-- CLAUDE.md | 5 +++-- README.md | 4 +++- ROADMAP.md | 19 ++++++++++--------- apps/drawcraft-cli/src/main.rs | 4 ++-- crates/mcp/src/headless.rs | 9 +++++++-- crates/mcp/src/tools.rs | 6 +++--- crates/ui-egui/src/io.rs | 17 ++++++++--------- crates/ui-egui/src/lib.rs | 20 ++++++++++++++++---- docs/control-protocol.md | 2 +- 10 files changed, 56 insertions(+), 35 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 63df3522..665ad060 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/CLAUDE.md b/CLAUDE.md index 63df3522..665ad060 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/README.md b/README.md index 3c247c2f..808427a1 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 7ecca09e..61635b00 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 `` 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 `` 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 ``/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. diff --git a/apps/drawcraft-cli/src/main.rs b/apps/drawcraft-cli/src/main.rs index 4b5637a7..ddea5fc5 100644 --- a/apps/drawcraft-cli/src/main.rs +++ b/apps/drawcraft-cli/src/main.rs @@ -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. diff --git a/crates/mcp/src/headless.rs b/crates/mcp/src/headless.rs index 8720650a..55ffd476 100644 --- a/crates/mcp/src/headless.rs +++ b/crates/mcp/src/headless.rs @@ -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 { 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()})) diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 7fa51dda..755cfb21 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -263,7 +263,7 @@ pub fn tool_definitions() -> Vec { "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, 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, scale: f64) -> Result { 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, 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, scale: Ok(path) } -fn encode_jpeg(img: &drawcraft_render::Rendered) -> Result, 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); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 07bdf2a4..1b314391 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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, control_rx: Option>, - pending_screenshots: Vec<(u64, Option, Sender)>, + /// (token, path, reply, deadline ms): a window that isn't presented never delivers its frame. + pending_screenshots: Vec<(u64, Option, Sender, 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. diff --git a/docs/control-protocol.md b/docs/control-protocol.md index a54f0669..070e03a8 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -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?}` | |