mirror of
https://github.com/penpot/penpot.git
synced 2026-05-22 00:13:41 +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;
|
let opts = &mut state.render_state.options;
|
||||||
opts.set_fast_mode(false);
|
opts.set_fast_mode(false);
|
||||||
opts.set_interactive_transform(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;
|
state.render_state.drag_overlay = None;
|
||||||
|
if let Some(ids) = overlay_ids {
|
||||||
|
let _ = state.rebuild_modifier_tiles(ids);
|
||||||
|
}
|
||||||
|
|
||||||
state.render_state.cancel_animation_frame();
|
state.render_state.cancel_animation_frame();
|
||||||
performance::end_measure!("set_modifiers_end");
|
performance::end_measure!("set_modifiers_end");
|
||||||
});
|
});
|
||||||
@ -990,8 +1005,19 @@ pub extern "C" fn set_modifiers() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
with_state_mut!(state, {
|
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.set_modifiers(modifiers);
|
||||||
state.rebuild_modifier_tiles(ids)?;
|
if !used_overlay {
|
||||||
|
state.rebuild_modifier_tiles(ids)?;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1665,6 +1665,72 @@ impl RenderState {
|
|||||||
Ok(())
|
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(
|
pub fn start_render_loop(
|
||||||
&mut self,
|
&mut self,
|
||||||
base_object: Option<&Uuid>,
|
base_object: Option<&Uuid>,
|
||||||
@ -1672,6 +1738,20 @@ impl RenderState {
|
|||||||
timestamp: i32,
|
timestamp: i32,
|
||||||
sync_render: bool,
|
sync_render: bool,
|
||||||
) -> Result<()> {
|
) -> 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 _start = performance::begin_timed_log!("start_render_loop");
|
||||||
let scale = self.get_scale();
|
let scale = self.get_scale();
|
||||||
|
|
||||||
@ -1756,6 +1836,19 @@ impl RenderState {
|
|||||||
|
|
||||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
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 {
|
if sync_render {
|
||||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||||
} else {
|
} 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_measure!("start_render_loop");
|
||||||
performance::end_timed_log!("start_render_loop", _start);
|
performance::end_timed_log!("start_render_loop", _start);
|
||||||
Ok(())
|
Ok(())
|
||||||
@ -1827,6 +1930,126 @@ impl RenderState {
|
|||||||
Ok(())
|
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(
|
pub fn render_shape_pixels(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: &Uuid,
|
id: &Uuid,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
|
use skia_safe::{self as skia, textlayout::FontCollection, Path, Point};
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
mod shapes_pool;
|
mod shapes_pool;
|
||||||
mod text_editor;
|
mod text_editor;
|
||||||
@ -7,6 +7,8 @@ pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef};
|
|||||||
pub use text_editor::*;
|
pub use text_editor::*;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
use crate::performance;
|
||||||
|
use crate::render::drag_overlay::{DragOverlay, DragOverlaySnapshot};
|
||||||
use crate::render::RenderState;
|
use crate::render::RenderState;
|
||||||
use crate::shapes::Shape;
|
use crate::shapes::Shape;
|
||||||
use crate::tiles;
|
use crate::tiles;
|
||||||
@ -296,6 +298,92 @@ impl State {
|
|||||||
self.shapes.set_modifiers(modifiers);
|
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) {
|
pub fn touch_current(&mut self) {
|
||||||
if !self.loading {
|
if !self.loading {
|
||||||
if let Some(current_id) = self.current_id {
|
if let Some(current_id) = self.current_id {
|
||||||
|
|||||||
@ -211,6 +211,15 @@ impl ShapesPoolImpl {
|
|||||||
self.modified_shape_cache.clear()
|
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>) {
|
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||||
// Initialize the cache cells for affected shapes
|
// Initialize the cache cells for affected shapes
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user