M5: drawcraft-cli bench; canvas keyed by per-document uid (no stale frame from another tab); brushed strokes keep brush art under effects

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-09-30 23:38:38 -04:00
co-authored by Claude Opus 5.5
parent acb2566d89
commit c06fad7b2b
6 changed files with 73 additions and 3 deletions
+52
View File
@@ -4,6 +4,7 @@
//! 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 commands
//! drawcraft-cli bench FILE [--size 2880x1800] [--iters 5]
//! ```
#![forbid(unsafe_code)]
@@ -27,6 +28,10 @@ USAGE:
drawcraft-cli commands
Print the command catalogue as JSON.
drawcraft-cli bench FILE [--size WxH] [--iters N]
Render FILE (.drawcraft/.svg) fitted to WxH (default 2880x1800) and print ms per frame
(warm), multithreaded and single-threaded.
";
fn main() -> ExitCode {
@@ -35,6 +40,7 @@ fn main() -> ExitCode {
Some("mcp") => mcp(&args[1..]),
Some("run") => run(&args[1..]),
Some("commands") => commands(),
Some("bench") => bench(&args[1..]),
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");
Ok(())
@@ -150,3 +156,49 @@ fn run(args: &[String]) -> Result<(), String> {
}
Ok(())
}
fn bench(args: &[String]) -> Result<(), String> {
let mut file = None;
let (mut w, mut h, mut iters) = (2880u32, 1800u32, 5u32);
let mut it = args.iter();
while let Some(a) = it.next() {
match a.as_str() {
"--size" => {
let v = it.next().ok_or("--size needs WxH")?;
let (a, b) = v.split_once('x').ok_or("--size needs WxH")?;
w = a.parse().map_err(|_| "bad width")?;
h = b.parse().map_err(|_| "bad height")?;
}
"--iters" => iters = it.next().and_then(|v| v.parse().ok()).ok_or("--iters needs a number")?,
f if file.is_none() => file = Some(f.to_string()),
other => return Err(format!("unknown bench option `{other}`")),
}
}
let file = file.ok_or("bench needs a FILE")?;
let mut hl = Headless::new();
hl.call("app.open", json!({ "path": file })).map_err(|e| format!("open {file}: {e}"))?;
let doc = hl.session.doc().map_err(|e| e.to_string())?.doc.clone();
let b = doc.artboards.first().map(|a| a.rect).ok_or("document has no artboard")?;
let z = (w as f64 / b.width()).min(h as f64 / b.height()) * 0.95;
let view = drawcraft_geom::Affine::translate((w as f64 / 2.0, h as f64 / 2.0))
* drawcraft_geom::Affine::scale(z)
* drawcraft_geom::Affine::translate(-b.center().to_vec2());
let opts = drawcraft_render::RenderOptions::default();
println!("{file}: {} nodes, {w}x{h}", doc.layers.iter().map(|l| l.count()).sum::<usize>());
for threads in [drawcraft_render::default_threads(), 0] {
let mut r = drawcraft_render::Renderer::new();
r.threads = threads;
r.render(&doc, w, h, view, &opts);
let t = std::time::Instant::now();
for _ in 0..iters {
r.render(&doc, w, h, view, &opts);
}
println!(
" threads {threads}: {:.1} ms/frame (drawn {}, culled {})",
t.elapsed().as_secs_f64() * 1000.0 / iters as f64,
r.stats.drawn,
r.stats.culled
);
}
Ok(())
}