M5.1: background render worker with reprojection; Layers rename + drag reorder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
db38f51046
commit
d0007a604d
|
|
@ -133,21 +133,42 @@ 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), revision: st.revision, zoom: v.zoom, cx: v.center.x, cy: v.center.y, w, h, outline: app.ui.view.outline, ppp, hidden: vec![] };
|
||||
if !app.canvas.worker_started {
|
||||
app.canvas.worker_started = true;
|
||||
if std::env::var_os("DRAWCRAFT_SYNC_RENDER").is_none() {
|
||||
app.canvas.worker = crate::render_worker::Worker::spawn(ui.ctx().clone());
|
||||
}
|
||||
}
|
||||
// Upload finished background renders.
|
||||
if let Some(done) = app.canvas.worker.as_mut().and_then(|w| w.poll()) {
|
||||
upload(app, ui.ctx(), &done.img);
|
||||
app.canvas.key = Some(done.key);
|
||||
app.perf.render_ms = done.ms;
|
||||
app.canvas.last_ms = done.ms;
|
||||
}
|
||||
if app.canvas.key.as_ref() != Some(&key) || app.canvas.texture.is_none() {
|
||||
let t0 = now_ms();
|
||||
let view = Affine::translate((w as f64 / 2.0, h as f64 / 2.0)) * Affine::scale(v.zoom * ppp as f64) * Affine::translate(-v.center.to_vec2());
|
||||
let opts = drawcraft_render::RenderOptions { outline: app.ui.view.outline, background: None, artboards: false, ..Default::default() };
|
||||
let img = app.canvas.renderer.render(&doc, w, h, view, &opts);
|
||||
let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
match &mut app.canvas.texture {
|
||||
Some(tex) => tex.set(color, egui::TextureOptions::LINEAR),
|
||||
None => app.canvas.texture = Some(ui.ctx().load_texture("canvas", color, egui::TextureOptions::LINEAR)),
|
||||
// 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();
|
||||
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 }),
|
||||
_ => {
|
||||
let t0 = now_ms();
|
||||
let img = app.canvas.renderer.render(&doc, w, h, view, &opts);
|
||||
upload(app, ui.ctx(), &img);
|
||||
app.canvas.key = Some(key.clone());
|
||||
app.perf.render_ms = now_ms() - t0;
|
||||
app.canvas.last_ms = app.perf.render_ms;
|
||||
}
|
||||
}
|
||||
app.canvas.key = Some(key);
|
||||
app.perf.render_ms = now_ms() - t0;
|
||||
}
|
||||
if let Some(tex) = &app.canvas.texture {
|
||||
painter.image(tex.id(), rect, egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
if let (Some(tex), Some(k)) = (&app.canvas.texture, &app.canvas.key) {
|
||||
// Reproject the last frame if it was rendered for a different view.
|
||||
let old = Xf { rect, zoom: k.zoom, center: Point::new(k.cx, k.cy) };
|
||||
let a = xf.to_screen(old.to_doc(rect.min));
|
||||
let b = xf.to_screen(old.to_doc(rect.max));
|
||||
painter.image(tex.id(), egui::Rect::from_min_max(a, b), egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
// Artboard edges and names.
|
||||
let active_ab = 0;
|
||||
|
|
@ -746,3 +767,11 @@ fn home(app: &mut DrawcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
|||
fn kurbo_flatten(p: &BezPath, tol: f64, f: &mut impl FnMut(PathEl)) {
|
||||
drawcraft_geom::kurbo::flatten(p.elements().iter().copied(), tol, f);
|
||||
}
|
||||
|
||||
fn upload(app: &mut DrawcraftApp, ctx: &egui::Context, img: &drawcraft_render::Rendered) {
|
||||
let color = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels);
|
||||
match &mut app.canvas.texture {
|
||||
Some(tex) => tex.set(color, egui::TextureOptions::LINEAR),
|
||||
None => app.canvas.texture = Some(ctx.load_texture("canvas", color, egui::TextureOptions::LINEAR)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub mod icons;
|
|||
pub mod io;
|
||||
pub mod menus;
|
||||
pub mod palette;
|
||||
pub mod render_worker;
|
||||
pub mod panels;
|
||||
pub mod shortcuts;
|
||||
pub mod state;
|
||||
|
|
@ -60,6 +61,8 @@ pub struct CanvasCache {
|
|||
pub texture: Option<egui::TextureHandle>,
|
||||
pub key: Option<CacheKey>,
|
||||
pub last_ms: f64,
|
||||
pub worker: Option<render_worker::Worker>,
|
||||
pub worker_started: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
|
@ -116,7 +119,7 @@ impl DrawcraftApp {
|
|||
ui: UiState::default(),
|
||||
views,
|
||||
services,
|
||||
canvas: CanvasCache { renderer: drawcraft_render::Renderer::new(), texture: None, key: None, last_ms: 0.0 },
|
||||
canvas: CanvasCache { renderer: drawcraft_render::Renderer::new(), texture: None, key: None, last_ms: 0.0, worker: None, worker_started: false },
|
||||
perf: Perf::default(),
|
||||
integrated_titlebar: false,
|
||||
control_rx: None,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
});
|
||||
ui.data_mut(|d| d.insert_temp(expanded_id(), expanded));
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
ui.data_mut(|d| d.remove::<u64>(egui::Id::new("layers-drag")));
|
||||
}
|
||||
// Bottom bar.
|
||||
let (bar, _) = ui.allocate_exact_size(vec2(ui.available_width(), 30.0), Sense::hover());
|
||||
ui.painter().line_segment([bar.left_top(), bar.right_top()], Stroke::new(1.0, t.divider));
|
||||
|
|
@ -73,7 +76,7 @@ pub fn show(app: &mut DrawcraftApp, ui: &mut Ui) {
|
|||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn row(ui: &mut Ui, doc: &drawcraft_doc::Document, n: &Node, depth: usize, sel: &HashSet<NodeId>, current: Option<NodeId>, expanded: &mut HashSet<u64>, actions: &mut Vec<(String, serde_json::Value)>, t: &Tokens) {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), Sense::click());
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW), Sense::click_and_drag());
|
||||
let is_sel = sel.contains(&n.id);
|
||||
let child_sel = !is_sel && n.children().is_some_and(|_| {
|
||||
let mut any = false;
|
||||
|
|
@ -148,7 +151,30 @@ fn row(ui: &mut Ui, doc: &drawcraft_doc::Document, n: &Node, depth: usize, sel:
|
|||
// Name.
|
||||
let name = n.display_name();
|
||||
let font = if n.is_layer() { egui::FontId::proportional(12.5) } else { egui::FontId::proportional(12.0) };
|
||||
ui.painter().with_clip_rect(egui::Rect::from_min_max(egui::pos2(x, r.top()), egui::pos2(r.right() - 44.0, r.bottom()))).text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name, font, t.text);
|
||||
let rename_id = egui::Id::new("layers-rename");
|
||||
let renaming: Option<(u64, String)> = ui.data(|d| d.get_temp(rename_id));
|
||||
let name_rect = egui::Rect::from_min_max(egui::pos2(x - 2.0, r.top() + 3.0), egui::pos2(r.right() - 44.0, r.bottom() - 3.0));
|
||||
match renaming {
|
||||
Some((rid, mut buf)) if rid == n.id.0 => {
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(name_rect));
|
||||
let te = child.add(egui::TextEdit::singleline(&mut buf).desired_width(name_rect.width()).font(font.clone()));
|
||||
te.request_focus();
|
||||
if te.lost_focus() {
|
||||
ui.data_mut(|d| d.remove::<(u64, String)>(rename_id));
|
||||
if child.input(|i| !i.key_pressed(egui::Key::Escape)) && buf != name {
|
||||
let cmd = if n.is_layer() { "layer.setProps" } else { "object.setProps" };
|
||||
let key = if n.is_layer() { "id" } else { "ids" };
|
||||
let idv = if n.is_layer() { json!(n.id.0) } else { json!([n.id.0]) };
|
||||
actions.push((cmd.into(), json!({key: idv, "name": buf})));
|
||||
}
|
||||
} else {
|
||||
ui.data_mut(|d| d.insert_temp(rename_id, (n.id.0, buf)));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
ui.painter().with_clip_rect(name_rect).text(egui::pos2(x, r.center().y), egui::Align2::LEFT_CENTER, name.clone(), font, t.text);
|
||||
}
|
||||
}
|
||||
// Target circle and selection square.
|
||||
let tc = egui::pos2(r.right() - 30.0, r.center().y);
|
||||
let styled = !n.appearance.is_basic() || n.opacity < 1.0;
|
||||
|
|
@ -181,9 +207,36 @@ fn row(ui: &mut Ui, doc: &drawcraft_doc::Document, n: &Node, depth: usize, sel:
|
|||
actions.push(("select.set".into(), json!({"ids": [n.id.0]})));
|
||||
}
|
||||
}
|
||||
if resp.double_clicked() && n.is_layer() {
|
||||
// Rename via Layer Options (later: inline edit).
|
||||
if resp.double_clicked() {
|
||||
ui.data_mut(|d| d.insert_temp(egui::Id::new("layers-rename"), (n.id.0, n.display_name())));
|
||||
}
|
||||
// Drag to reorder: drop onto a row moves the dragged node above it (into its parent).
|
||||
let drag_id = egui::Id::new("layers-drag");
|
||||
if resp.drag_started() {
|
||||
ui.data_mut(|d| d.insert_temp(drag_id, n.id.0));
|
||||
}
|
||||
let dragging: Option<u64> = ui.data(|d| d.get_temp(drag_id));
|
||||
if let Some(src) = dragging
|
||||
&& src != n.id.0
|
||||
&& ui.rect_contains_pointer(r)
|
||||
{
|
||||
let above = ui.input(|i| i.pointer.hover_pos()).is_some_and(|p| p.y < r.center().y);
|
||||
let y = if above { r.top() } else { r.bottom() };
|
||||
ui.painter().line_segment([egui::pos2(r.left() + 46.0, y), egui::pos2(r.right(), y)], Stroke::new(2.0, t.accent));
|
||||
if ui.input(|i| i.pointer.any_released()) {
|
||||
let (parent, index) = match doc.position(n.id) {
|
||||
Some((par, idx, _)) => (par, if above { idx + 1 } else { idx }),
|
||||
None => (None, 0),
|
||||
};
|
||||
// A layer dropped on a non-layer goes into that row's container; top level only for layers.
|
||||
let src_is_layer = doc.node(drawcraft_doc::NodeId(src)).is_some_and(|x| x.is_layer());
|
||||
if parent.is_some() || src_is_layer {
|
||||
actions.push(("node.move".into(), json!({"id": src, "parent": parent.map(|p| p.0), "index": index})));
|
||||
}
|
||||
ui.data_mut(|d| d.remove::<u64>(drag_id));
|
||||
}
|
||||
}
|
||||
|
||||
if has_children && expanded.contains(&n.id.0) {
|
||||
for c in n.children().unwrap().iter().rev() {
|
||||
row(ui, doc, c, depth + 1, sel, current, expanded, actions, t);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,84 @@
|
|||
//! Background rendering (native): heavy documents render off the UI thread while the canvas shows
|
||||
//! the previous frame reprojected to the current view. Light documents render synchronously so
|
||||
//! edits never lag behind overlays.
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
use drawcraft_doc::Document;
|
||||
use drawcraft_geom::Affine;
|
||||
use drawcraft_render::{RenderOptions, Rendered, Renderer};
|
||||
|
||||
use crate::CacheKey;
|
||||
|
||||
pub struct Job {
|
||||
pub key: CacheKey,
|
||||
pub doc: Arc<Document>,
|
||||
pub w: u32,
|
||||
pub h: u32,
|
||||
pub view: Affine,
|
||||
pub opts: RenderOptions,
|
||||
}
|
||||
|
||||
pub struct Done {
|
||||
pub key: CacheKey,
|
||||
pub img: Rendered,
|
||||
pub ms: f64,
|
||||
}
|
||||
|
||||
pub struct Worker {
|
||||
tx: Sender<Job>,
|
||||
rx: Receiver<Done>,
|
||||
pub pending: Option<CacheKey>,
|
||||
}
|
||||
|
||||
impl Worker {
|
||||
/// Spawn the worker thread (None on wasm, where rendering stays synchronous).
|
||||
pub fn spawn(ctx: egui::Context) -> Option<Self> {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return None;
|
||||
}
|
||||
let (tx, jobs) = channel::<Job>();
|
||||
let (done_tx, rx) = channel::<Done>();
|
||||
std::thread::Builder::new()
|
||||
.name("drawcraft-render".into())
|
||||
.spawn(move || {
|
||||
let mut r = Renderer::new();
|
||||
while let Ok(mut job) = jobs.recv() {
|
||||
// Skip stale jobs: only the newest matters.
|
||||
while let Ok(newer) = jobs.try_recv() {
|
||||
job = newer;
|
||||
}
|
||||
let t0 = crate::now_ms();
|
||||
let img = r.render(&job.doc, job.w, job.h, job.view, &job.opts);
|
||||
if done_tx.send(Done { key: job.key, img, ms: crate::now_ms() - t0 }).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
}
|
||||
})
|
||||
.ok()?;
|
||||
Some(Self { tx, rx, pending: None })
|
||||
}
|
||||
|
||||
pub fn submit(&mut self, job: Job) {
|
||||
if self.pending.as_ref() == Some(&job.key) {
|
||||
return;
|
||||
}
|
||||
self.pending = Some(job.key.clone());
|
||||
let _ = self.tx.send(job);
|
||||
}
|
||||
|
||||
pub fn poll(&mut self) -> Option<Done> {
|
||||
let mut last = None;
|
||||
while let Ok(d) = self.rx.try_recv() {
|
||||
last = Some(d);
|
||||
}
|
||||
if let Some(d) = &last
|
||||
&& self.pending.as_ref() == Some(&d.key)
|
||||
{
|
||||
self.pending = None;
|
||||
}
|
||||
last
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue