mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 drag-overlay fast path for interactive transforms
This commit is contained in:
parent
3296ca0303
commit
49a699b61b
@ -490,7 +490,22 @@ pub extern "C" fn set_modifiers_end() -> Result<()> {
|
||||
let opts = &mut state.render_state.options;
|
||||
opts.set_fast_mode(false);
|
||||
opts.set_interactive_transform(false);
|
||||
|
||||
// Invalidate every tile covered by the overlay before dropping
|
||||
// it: the atlas still carries holes where the snapshots sat and
|
||||
// the next non-overlay render must repaint those regions from
|
||||
// scratch (otherwise the shapes would reappear at their old
|
||||
// position or gaps would remain when modifiers are committed).
|
||||
let overlay_ids: Option<Vec<Uuid>> = state
|
||||
.render_state
|
||||
.drag_overlay
|
||||
.as_ref()
|
||||
.map(|o| o.shape_ids.iter().copied().collect());
|
||||
state.render_state.drag_overlay = None;
|
||||
if let Some(ids) = overlay_ids {
|
||||
let _ = state.rebuild_modifier_tiles(ids);
|
||||
}
|
||||
|
||||
state.render_state.cancel_animation_frame();
|
||||
performance::end_measure!("set_modifiers_end");
|
||||
});
|
||||
@ -990,8 +1005,19 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
}
|
||||
|
||||
with_state_mut!(state, {
|
||||
// Try to set up the drag-overlay fast path before mutating the
|
||||
// pool. The setup captures untransformed snapshots and requires
|
||||
// access to the pre-modifier geometry; after this returns it is
|
||||
// safe to apply modifiers. `used_overlay == true` also means
|
||||
// `ensure_drag_overlay_setup` already invalidated the tiles it
|
||||
// cares about, so we skip the broader `rebuild_modifier_tiles`
|
||||
// invalidation (the walker won't be running during the gesture
|
||||
// from now on — the fast render path will bypass it).
|
||||
let used_overlay = state.ensure_drag_overlay_setup(&ids)?;
|
||||
state.set_modifiers(modifiers);
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
if !used_overlay {
|
||||
state.rebuild_modifier_tiles(ids)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -1665,6 +1665,72 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Fast-path render used while the drag-overlay is active.
|
||||
///
|
||||
/// Once every selected shape has a cached snapshot and the atlas
|
||||
/// has been hole-punched (see `drag_overlay::DragOverlay::is_ready`),
|
||||
/// a drag frame is reduced to: one atlas blit (the stable backdrop)
|
||||
/// plus one `draw_image_rect` per selected shape with the current
|
||||
/// modifier matrix concatenated in document space. The tile walker
|
||||
/// is not touched at all.
|
||||
fn render_drag_overlay(&mut self, tree: ShapesPoolRef) -> Result<()> {
|
||||
performance::begin_measure!("render_drag_overlay");
|
||||
self.reset_canvas();
|
||||
|
||||
let dpr = self.options.dpr();
|
||||
let viewbox = self.viewbox;
|
||||
let bg_color = self.background_color;
|
||||
|
||||
// Stable backdrop (atlas carries the hole-punched scene).
|
||||
self.surfaces.draw_atlas_to_target(viewbox, dpr, bg_color);
|
||||
|
||||
if let Some(overlay) = self.drag_overlay.as_ref() {
|
||||
let sampling = self.sampling_options;
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
let s = viewbox.zoom * dpr;
|
||||
|
||||
canvas.save();
|
||||
canvas.reset_matrix();
|
||||
let size = canvas.base_layer_size();
|
||||
canvas.clip_rect(
|
||||
skia::Rect::from_xywh(0.0, 0.0, size.width as f32, size.height as f32),
|
||||
None,
|
||||
true,
|
||||
);
|
||||
canvas.translate((viewbox.pan_x * s, viewbox.pan_y * s));
|
||||
canvas.scale((s, s));
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
for (id, snap) in overlay.snapshots.iter() {
|
||||
let modifier = tree.modifier_of(id).copied();
|
||||
canvas.save();
|
||||
if let Some(m) = modifier {
|
||||
canvas.concat(&m);
|
||||
}
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
&snap.image,
|
||||
None,
|
||||
snap.source_doc_rect,
|
||||
sampling,
|
||||
&paint,
|
||||
);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
// UI + debug overlays (selection rects, rulers, etc.) still
|
||||
// need to land on top of the composited scene.
|
||||
ui::render(self, tree);
|
||||
debug::render_wasm_label(self);
|
||||
|
||||
self.flush_and_submit();
|
||||
wapi::notify_tiles_render_complete!();
|
||||
performance::end_measure!("render_drag_overlay");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
@ -1672,6 +1738,20 @@ impl RenderState {
|
||||
timestamp: i32,
|
||||
sync_render: bool,
|
||||
) -> Result<()> {
|
||||
// Drag-overlay fast path: skip the tile walker entirely and
|
||||
// composite cached snapshots over the hole-punched atlas. Only
|
||||
// kicks in from the second `set_modifiers` of the gesture
|
||||
// onwards — the first one runs the walker with `exclude_ids`
|
||||
// active to produce the clean backdrop (see below).
|
||||
if self.options.is_interactive_transform()
|
||||
&& self
|
||||
.drag_overlay
|
||||
.as_ref()
|
||||
.map_or(false, |o| o.is_ready())
|
||||
{
|
||||
return self.render_drag_overlay(tree);
|
||||
}
|
||||
|
||||
let _start = performance::begin_timed_log!("start_render_loop");
|
||||
let scale = self.get_scale();
|
||||
|
||||
@ -1756,6 +1836,19 @@ impl RenderState {
|
||||
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
|
||||
// When the drag overlay has captured snapshots but the atlas
|
||||
// backdrop has not yet been produced, we must complete the
|
||||
// tile walker (with the overlay filter active) in this single
|
||||
// rAF so the atlas ends up hole-punched before the next frame
|
||||
// takes the fast `render_drag_overlay` path. A non-sync render
|
||||
// could leave the atlas with leftover ghosts of the dragged
|
||||
// shapes for however many frames the walker needs to finish.
|
||||
let overlay_building = self
|
||||
.drag_overlay
|
||||
.as_ref()
|
||||
.map_or(false, |o| !o.backdrop_ready);
|
||||
let sync_render = sync_render || overlay_building;
|
||||
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
} else {
|
||||
@ -1772,6 +1865,16 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// The first frame of a drag with the overlay enabled runs the
|
||||
// walker with the selected shapes excluded, which rewrites the
|
||||
// tiles and atlas without them. Mark the backdrop ready so
|
||||
// subsequent frames bypass the walker entirely.
|
||||
if overlay_building {
|
||||
if let Some(overlay) = self.drag_overlay.as_mut() {
|
||||
overlay.backdrop_ready = true;
|
||||
}
|
||||
}
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
performance::end_timed_log!("start_render_loop", _start);
|
||||
Ok(())
|
||||
@ -1827,6 +1930,126 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Render a single shape (and its subtree) into an `skia::Image`
|
||||
/// without touching any workspace state. Used by the drag-overlay
|
||||
/// fast path to snapshot the selected shapes at gesture start.
|
||||
///
|
||||
/// Returns `(image, source_doc_rect, captured_scale)`. The caller
|
||||
/// draws the image into `source_doc_rect` in document space and
|
||||
/// (optionally) concatenates the shape's current modifier matrix on
|
||||
/// top to place it at its transformed position.
|
||||
pub fn capture_shape_image(
|
||||
&mut self,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
timestamp: i32,
|
||||
) -> Result<Option<(skia::Image, Rect, f32)>> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
|
||||
// Mirror the save/restore logic from `render_shape_pixels` so the
|
||||
// workspace render context is not perturbed by this off-screen pass.
|
||||
let saved_focus_mode = self.focus_mode.clone();
|
||||
let saved_export_context = self.export_context;
|
||||
let saved_render_area = self.render_area;
|
||||
let saved_render_area_with_margins = self.render_area_with_margins;
|
||||
let saved_current_tile = self.current_tile;
|
||||
let saved_pending_nodes = std::mem::take(&mut self.pending_nodes);
|
||||
let saved_nested_fills = std::mem::take(&mut self.nested_fills);
|
||||
let saved_nested_blurs = std::mem::take(&mut self.nested_blurs);
|
||||
let saved_nested_shadows = std::mem::take(&mut self.nested_shadows);
|
||||
let saved_ignore_nested_blurs = self.ignore_nested_blurs;
|
||||
let saved_preview_mode = self.preview_mode;
|
||||
|
||||
// Disable fast_mode / interactive_transform while capturing so
|
||||
// the snapshot contains full-quality effects (drop shadows,
|
||||
// blurs, etc.). Otherwise the cached image would be rasterized
|
||||
// with the same shortcuts used during pan/zoom and the dragged
|
||||
// shape would visually lose its shadows the moment the overlay
|
||||
// takes over.
|
||||
let saved_fast_mode = self.options.is_fast_mode();
|
||||
let saved_interactive_transform = self.options.is_interactive_transform();
|
||||
self.options.set_fast_mode(false);
|
||||
self.options.set_interactive_transform(false);
|
||||
|
||||
self.focus_mode.clear();
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
let Some(shape) = tree.get(id) else {
|
||||
// Restore and bail if the shape vanished mid-gesture.
|
||||
self.options.set_fast_mode(saved_fast_mode);
|
||||
self.options
|
||||
.set_interactive_transform(saved_interactive_transform);
|
||||
self.focus_mode = saved_focus_mode;
|
||||
self.export_context = saved_export_context;
|
||||
self.render_area = saved_render_area;
|
||||
self.render_area_with_margins = saved_render_area_with_margins;
|
||||
self.current_tile = saved_current_tile;
|
||||
self.pending_nodes = saved_pending_nodes;
|
||||
self.nested_fills = saved_nested_fills;
|
||||
self.nested_blurs = saved_nested_blurs;
|
||||
self.nested_shadows = saved_nested_shadows;
|
||||
self.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
self.preview_mode = saved_preview_mode;
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let source_doc_rect = shape.extrect(tree, scale);
|
||||
let mut extrect = source_doc_rect;
|
||||
self.export_context = Some((extrect, scale));
|
||||
let margins = self.surfaces.margins;
|
||||
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||
|
||||
self.surfaces.resize_export_surface(scale, extrect);
|
||||
self.render_area = extrect;
|
||||
self.render_area_with_margins = extrect;
|
||||
self.surfaces.update_render_context(extrect, scale);
|
||||
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: *id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
flattened: false,
|
||||
});
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
|
||||
self.export_context = None;
|
||||
self.surfaces
|
||||
.flush_and_submit(&mut self.gpu_state, target_surface);
|
||||
|
||||
let image = self.surfaces.snapshot(target_surface);
|
||||
|
||||
// Restore workspace state.
|
||||
self.options.set_fast_mode(saved_fast_mode);
|
||||
self.options
|
||||
.set_interactive_transform(saved_interactive_transform);
|
||||
self.focus_mode = saved_focus_mode;
|
||||
self.export_context = saved_export_context;
|
||||
self.render_area = saved_render_area;
|
||||
self.render_area_with_margins = saved_render_area_with_margins;
|
||||
self.current_tile = saved_current_tile;
|
||||
self.pending_nodes = saved_pending_nodes;
|
||||
self.nested_fills = saved_nested_fills;
|
||||
self.nested_blurs = saved_nested_blurs;
|
||||
self.nested_shadows = saved_nested_shadows;
|
||||
self.ignore_nested_blurs = saved_ignore_nested_blurs;
|
||||
self.preview_mode = saved_preview_mode;
|
||||
|
||||
let workspace_scale = self.get_scale();
|
||||
if let Some(tile) = self.current_tile {
|
||||
self.update_render_context(tile);
|
||||
} else if !self.render_area.is_empty() {
|
||||
self.surfaces
|
||||
.update_render_context(self.render_area, workspace_scale);
|
||||
}
|
||||
|
||||
Ok(Some((image, source_doc_rect, scale)))
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
&mut self,
|
||||
id: &Uuid,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
mod shapes_pool;
|
||||
mod text_editor;
|
||||
@ -7,6 +7,8 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
||||
pub use text_editor::*;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::performance;
|
||||
use crate::render::drag_overlay::{DragOverlay, DragOverlaySnapshot};
|
||||
use crate::render::RenderState;
|
||||
use crate::shapes::Shape;
|
||||
use crate::tiles;
|
||||
@ -296,6 +298,92 @@ impl State {
|
||||
self.shapes.set_modifiers(modifiers);
|
||||
}
|
||||
|
||||
/// Set up the drag-overlay fast path for the current gesture, if
|
||||
/// enabled and not already set up. Returns `true` when the overlay
|
||||
/// is active after this call (either just created or pre-existing).
|
||||
///
|
||||
/// Must be called **before** `set_modifiers` applies the new
|
||||
/// transforms to the pool so snapshots are captured at the
|
||||
/// untransformed position. If setup fails for any reason (shape
|
||||
/// missing, capture error, empty selection) the overlay is left
|
||||
/// absent and the caller should fall back to the regular tile
|
||||
/// walker path (`rebuild_modifier_tiles`).
|
||||
pub fn ensure_drag_overlay_setup(&mut self, ids: &[Uuid]) -> Result<bool> {
|
||||
{
|
||||
let opts = &self.render_state.options;
|
||||
if !opts.is_drag_overlay() || !opts.is_interactive_transform() {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
if self.render_state.drag_overlay.is_some() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if ids.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut shape_ids: HashSet<Uuid> = HashSet::with_capacity(ids.len());
|
||||
for id in ids {
|
||||
if let Some(shape) = self.shapes.get(id) {
|
||||
if !shape.hidden {
|
||||
shape_ids.insert(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
if shape_ids.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let timestamp = performance::get_time();
|
||||
let scale = self.render_state.get_scale();
|
||||
let mut snapshots: HashMap<Uuid, DragOverlaySnapshot> =
|
||||
HashMap::with_capacity(shape_ids.len());
|
||||
|
||||
// Collect into a Vec first so we are not holding a borrow of
|
||||
// `shape_ids` while we call into render_state which reborrows
|
||||
// self via `&self.shapes`.
|
||||
let capture_ids: Vec<Uuid> = shape_ids.iter().copied().collect();
|
||||
for id in capture_ids {
|
||||
match self
|
||||
.render_state
|
||||
.capture_shape_image(&id, &self.shapes, scale, timestamp)?
|
||||
{
|
||||
Some((image, source_doc_rect, captured_scale)) => {
|
||||
snapshots.insert(
|
||||
id,
|
||||
DragOverlaySnapshot {
|
||||
image,
|
||||
source_doc_rect,
|
||||
captured_scale,
|
||||
},
|
||||
);
|
||||
}
|
||||
None => return Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
if snapshots.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Invalidate tiles intersecting the selected shapes so the next
|
||||
// walker pass rebuilds them with the overlay filter active,
|
||||
// leaving a hole-punched atlas.
|
||||
let ids_vec: Vec<Uuid> = shape_ids.iter().copied().collect();
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut self.shapes, ids_vec)?;
|
||||
|
||||
self.render_state.drag_overlay = Some(DragOverlay {
|
||||
shape_ids,
|
||||
snapshots,
|
||||
backdrop_ready: false,
|
||||
});
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn touch_current(&mut self) {
|
||||
if !self.loading {
|
||||
if let Some(current_id) = self.current_id {
|
||||
|
||||
@ -211,6 +211,15 @@ impl ShapesPoolImpl {
|
||||
self.modified_shape_cache.clear()
|
||||
}
|
||||
|
||||
/// Returns the modifier matrix currently applied to `id`, if any.
|
||||
/// Used by the drag-overlay fast path to composite cached snapshots
|
||||
/// at their transformed position without reading the modified shape
|
||||
/// through `get()` (which also rebuilds geometry).
|
||||
pub fn modifier_of(&self, id: &Uuid) -> Option<&Matrix> {
|
||||
let idx = self.uuid_to_idx.get(id)?;
|
||||
self.modifiers.get(idx)
|
||||
}
|
||||
|
||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user