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:
parent
acb2566d89
commit
c06fad7b2b
|
|
@ -853,7 +853,9 @@ dependencies = [
|
|||
name = "drawcraft-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"drawcraft-geom",
|
||||
"drawcraft-mcp",
|
||||
"drawcraft-render",
|
||||
"drawcraft-testkit",
|
||||
"serde_json",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ path = "src/main.rs"
|
|||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
drawcraft-geom = { workspace = true }
|
||||
drawcraft-mcp = { workspace = true }
|
||||
drawcraft-render = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,8 +94,12 @@ pub struct DocState {
|
|||
pub last_transform: Option<(Affine, bool)>,
|
||||
/// Selection saved by Select → Reselect.
|
||||
pub last_selection_cmd: Option<(String, Value)>,
|
||||
/// Process-unique id of this open document (tab indices shift when tabs close).
|
||||
pub uid: u64,
|
||||
}
|
||||
|
||||
static NEXT_DOC_UID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
|
||||
|
||||
impl DocState {
|
||||
pub fn new(doc: Document, path: Option<String>) -> Self {
|
||||
let active_layer = doc.default_layer();
|
||||
|
|
@ -111,6 +115,7 @@ impl DocState {
|
|||
interaction: None,
|
||||
last_transform: None,
|
||||
last_selection_cmd: None,
|
||||
uid: NEXT_DOC_UID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
pub fn is_dirty(&self) -> bool {
|
||||
|
|
|
|||
|
|
@ -235,6 +235,10 @@ impl Renderer {
|
|||
if !st.visible || st.paint.is_none() || st.width <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
// Brushed strokes keep their brush art under effects (e.g. a glowing scatter brush).
|
||||
if st.brush.is_some() && self.draw_brush(ctx, f, n, &ig, st) {
|
||||
continue;
|
||||
}
|
||||
self.draw_stroke(ctx, f, &ig, rule, st, ib);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
let ppp = ui.ctx().pixels_per_point();
|
||||
let (w, h) = ((rect.width() * ppp).round().max(1.0) as u32, (rect.height() * ppp).round().max(1.0) as u32);
|
||||
let key = CacheKey {
|
||||
doc: app.session.active_index().unwrap_or(0),
|
||||
doc: st.uid as usize,
|
||||
revision: st.revision,
|
||||
zoom: v.zoom,
|
||||
cx: v.center.x,
|
||||
|
|
@ -186,7 +186,9 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
// Upload finished background renders.
|
||||
if let Some(done) = app.canvas.worker.as_mut().and_then(|w| w.poll()) {
|
||||
if let Some(done) = app.canvas.worker.as_mut().and_then(|w| w.poll())
|
||||
&& done.key.doc == key.doc
|
||||
{
|
||||
upload(app, ui.ctx(), &done.img);
|
||||
app.canvas.key = Some(done.key);
|
||||
app.perf.render_ms = done.ms;
|
||||
|
|
@ -204,7 +206,9 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
..opts
|
||||
};
|
||||
// Light documents render synchronously (no lag vs overlays); heavy ones go to the worker.
|
||||
let heavy = app.canvas.last_ms > 8.0 && app.canvas.texture.is_some();
|
||||
// A document switch renders synchronously so another document's frame is never shown.
|
||||
let same_doc = app.canvas.key.as_ref().is_some_and(|k| k.doc == key.doc);
|
||||
let heavy = app.canvas.last_ms > 8.0 && app.canvas.texture.is_some() && same_doc;
|
||||
match (&mut app.canvas.worker, heavy) {
|
||||
(Some(worker), true) => worker.submit(crate::render_worker::Job { key: key.clone(), doc: doc.clone(), w, h, view, opts }),
|
||||
_ => {
|
||||
|
|
@ -218,6 +222,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
}
|
||||
if let (Some(tex), Some(k)) = (&app.canvas.texture, &app.canvas.key)
|
||||
&& k.doc == key.doc
|
||||
&& (k.rot - v.rotation).abs() < 1e-9
|
||||
{
|
||||
// Reproject the last frame if it was rendered for a different view.
|
||||
|
|
|
|||
Loading…
Reference in New Issue