mirror of
https://github.com/penpot/penpot.git
synced 2026-02-04 19:51:57 -05:00
Compare commits
9 Commits
develop
...
elenatorro
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8480dd8b8b | ||
|
|
d8143963fd | ||
|
|
5243a34e14 | ||
|
|
743b9d22c2 | ||
|
|
452269d0fc | ||
|
|
0d726df9a8 | ||
|
|
111c9a480b | ||
|
|
271515904e | ||
|
|
b0b09b7acc |
@@ -302,8 +302,7 @@ pub extern "C" fn set_view_end() {
|
||||
{
|
||||
let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START };
|
||||
performance::console_log!(
|
||||
"[PERF] view_interaction (zoom_changed={}): {}ms",
|
||||
zoom_changed,
|
||||
"[PERF] view_interaction: {}ms",
|
||||
total_time
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ use crate::wapi;
|
||||
pub use fonts::*;
|
||||
pub use images::*;
|
||||
|
||||
// This is the extra are used for tile rendering.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 2;
|
||||
// This is the extra area used for tile rendering (tiles beyond viewport).
|
||||
// Higher values pre-render more tiles, reducing empty squares during pan but using more memory.
|
||||
const VIEWPORT_INTEREST_AREA_THRESHOLD: i32 = 3;
|
||||
const MAX_BLOCKING_TIME_MS: i32 = 32;
|
||||
const NODE_BATCH_THRESHOLD: i32 = 3;
|
||||
|
||||
@@ -2063,8 +2064,13 @@ impl RenderState {
|
||||
}
|
||||
} else {
|
||||
performance::begin_measure!("render_shape_tree::uncached");
|
||||
// Only allow stopping (yielding) if the current tile is NOT visible.
|
||||
// This ensures all visible tiles render synchronously before showing,
|
||||
// eliminating empty squares during zoom. Interest-area tiles can still yield.
|
||||
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
||||
let can_stop = allow_stop && !tile_is_visible;
|
||||
let (is_empty, early_return) =
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?;
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
@@ -2189,17 +2195,16 @@ impl RenderState {
|
||||
* Given a shape, check the indexes and update it's location in the tile set
|
||||
* returns the tiles that have changed in the process.
|
||||
*/
|
||||
pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Vec<tiles::Tile> {
|
||||
pub fn update_shape_tiles(&mut self, shape: &Shape, tree: ShapesPoolRef) -> HashSet<tiles::Tile> {
|
||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||
|
||||
let old_tiles = self
|
||||
// Collect old tiles to avoid borrow conflict with remove_shape_at
|
||||
let old_tiles: Vec<_> = self
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map_or(Vec::new(), |tiles| tiles.iter().copied().collect());
|
||||
.map_or(Vec::new(), |t| t.iter().copied().collect());
|
||||
|
||||
let new_tiles = (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)));
|
||||
|
||||
let mut result = HashSet::<tiles::Tile>::new();
|
||||
let mut result = HashSet::<tiles::Tile>::with_capacity(old_tiles.len());
|
||||
|
||||
// First, remove the shape from all tiles where it was previously located
|
||||
for tile in old_tiles {
|
||||
@@ -2208,12 +2213,66 @@ impl RenderState {
|
||||
}
|
||||
|
||||
// Then, add the shape to the new tiles
|
||||
for tile in new_tiles {
|
||||
for tile in (rsx..=rex).flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y))) {
|
||||
self.tiles.add_shape_at(tile, shape.id);
|
||||
result.insert(tile);
|
||||
}
|
||||
|
||||
result.iter().copied().collect()
|
||||
result
|
||||
}
|
||||
|
||||
/*
|
||||
* Incremental version of update_shape_tiles for pan/zoom operations.
|
||||
* Updates the tile index and returns ONLY tiles that need cache invalidation.
|
||||
*
|
||||
* During pan operations, shapes don't move in world coordinates. The interest
|
||||
* area (viewport) moves, which changes which tiles we track in the index, but
|
||||
* tiles that were already cached don't need re-rendering just because the
|
||||
* viewport moved.
|
||||
*
|
||||
* This function:
|
||||
* 1. Updates the tile index (adds/removes shapes from tiles based on interest area)
|
||||
* 2. Returns empty vec for cache invalidation (pan doesn't change tile content)
|
||||
*
|
||||
* Tile cache invalidation only happens when shapes actually move or change,
|
||||
* which is handled by rebuild_touched_tiles, not during pan/zoom.
|
||||
*/
|
||||
pub fn update_shape_tiles_incremental(
|
||||
&mut self,
|
||||
shape: &Shape,
|
||||
tree: ShapesPoolRef,
|
||||
) -> Vec<tiles::Tile> {
|
||||
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree);
|
||||
|
||||
let old_tiles: HashSet<tiles::Tile> = self
|
||||
.tiles
|
||||
.get_tiles_of(shape.id)
|
||||
.map_or(HashSet::new(), |tiles| tiles.iter().copied().collect());
|
||||
|
||||
let new_tiles: HashSet<tiles::Tile> = (rsx..=rex)
|
||||
.flat_map(|x| (rsy..=rey).map(move |y| tiles::Tile::from(x, y)))
|
||||
.collect();
|
||||
|
||||
// Tiles where shape is being removed from index (left interest area)
|
||||
let removed: Vec<_> = old_tiles.difference(&new_tiles).copied().collect();
|
||||
// Tiles where shape is being added to index (entered interest area)
|
||||
let added: Vec<_> = new_tiles.difference(&old_tiles).copied().collect();
|
||||
|
||||
// Update the index: remove from old tiles
|
||||
for tile in &removed {
|
||||
self.tiles.remove_shape_at(*tile, shape.id);
|
||||
}
|
||||
|
||||
// Update the index: add to new tiles
|
||||
for tile in &added {
|
||||
self.tiles.add_shape_at(*tile, shape.id);
|
||||
}
|
||||
|
||||
// Don't invalidate cache for pan/zoom - the tile content hasn't changed,
|
||||
// only the interest area moved. Tiles that were cached are still valid.
|
||||
// New tiles that entered the interest area will be rendered fresh since
|
||||
// they weren't in the cache anyway.
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -2239,12 +2298,22 @@ impl RenderState {
|
||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||
performance::begin_measure!("rebuild_tiles_shallow");
|
||||
|
||||
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||
// Check if zoom changed - if so, we need full cache invalidation
|
||||
// because tiles are rendered at specific zoom levels
|
||||
let zoom_changed = self.zoom_changed();
|
||||
|
||||
let mut tiles_to_invalidate = HashSet::<tiles::Tile>::new();
|
||||
let mut nodes = vec![Uuid::nil()];
|
||||
while let Some(shape_id) = nodes.pop() {
|
||||
if let Some(shape) = tree.get(&shape_id) {
|
||||
if shape_id != Uuid::nil() {
|
||||
all_tiles.extend(self.update_shape_tiles(shape, tree));
|
||||
if zoom_changed {
|
||||
// Zoom changed: use full update that tracks all affected tiles
|
||||
tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree));
|
||||
} else {
|
||||
// Pan only: use incremental update that preserves valid cached tiles
|
||||
self.update_shape_tiles_incremental(shape, tree);
|
||||
}
|
||||
} else {
|
||||
// We only need to rebuild tiles from the first level.
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
@@ -2254,11 +2323,11 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
if zoom_changed {
|
||||
// Zoom changed: clear all cached tiles since they're at wrong zoom level
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
}
|
||||
// Pan only: no cache invalidation needed - tiles content unchanged
|
||||
|
||||
performance::end_measure!("rebuild_tiles_shallow");
|
||||
}
|
||||
@@ -2307,7 +2376,7 @@ impl RenderState {
|
||||
|
||||
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
||||
|
||||
let ids = self.touched_ids.clone();
|
||||
let ids = std::mem::take(&mut self.touched_ids);
|
||||
|
||||
for shape_id in ids.iter() {
|
||||
if let Some(shape) = tree.get(shape_id) {
|
||||
@@ -2322,8 +2391,6 @@ impl RenderState {
|
||||
self.remove_cached_tile(tile);
|
||||
}
|
||||
|
||||
self.clean_touched();
|
||||
|
||||
performance::end_measure!("rebuild_touched_tiles");
|
||||
}
|
||||
|
||||
|
||||
@@ -1115,6 +1115,25 @@ impl Shape {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns children in forward (non-reversed) order - useful for layout calculations
|
||||
pub fn children_ids_iter_forward(&self, include_hidden: bool) -> Box<dyn Iterator<Item = &Uuid> + '_> {
|
||||
if include_hidden {
|
||||
return Box::new(self.children.iter());
|
||||
}
|
||||
|
||||
if let Type::Bool(_) = self.shape_type {
|
||||
Box::new([].iter())
|
||||
} else if let Type::Group(group) = self.shape_type {
|
||||
if group.masked {
|
||||
Box::new(self.children.iter().skip(1))
|
||||
} else {
|
||||
Box::new(self.children.iter())
|
||||
}
|
||||
} else {
|
||||
Box::new(self.children.iter())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn all_children(
|
||||
&self,
|
||||
shapes: ShapesPoolRef,
|
||||
|
||||
@@ -282,7 +282,7 @@ fn propagate_reflow(
|
||||
state: &State,
|
||||
entries: &mut VecDeque<Modifier>,
|
||||
bounds: &mut HashMap<Uuid, Bounds>,
|
||||
layout_reflows: &mut Vec<Uuid>,
|
||||
layout_reflows: &mut HashSet<Uuid>,
|
||||
reflown: &mut HashSet<Uuid>,
|
||||
modifiers: &HashMap<Uuid, Matrix>,
|
||||
) {
|
||||
@@ -312,7 +312,7 @@ fn propagate_reflow(
|
||||
}
|
||||
|
||||
if !skip_reflow {
|
||||
layout_reflows.push(*id);
|
||||
layout_reflows.insert(*id);
|
||||
}
|
||||
}
|
||||
Type::Group(Group { masked: true }) => {
|
||||
@@ -394,7 +394,7 @@ pub fn propagate_modifiers(
|
||||
let mut modifiers = HashMap::<Uuid, Matrix>::new();
|
||||
let mut bounds = HashMap::<Uuid, Bounds>::new();
|
||||
let mut reflown = HashSet::<Uuid>::new();
|
||||
let mut layout_reflows = Vec::<Uuid>::new();
|
||||
let mut layout_reflows = HashSet::<Uuid>::new();
|
||||
|
||||
// We first propagate the transforms to the children and then after
|
||||
// recalculate the layouts. The layout can create further transforms that
|
||||
@@ -424,13 +424,12 @@ pub fn propagate_modifiers(
|
||||
}
|
||||
}
|
||||
|
||||
for id in layout_reflows.iter() {
|
||||
if reflown.contains(id) {
|
||||
for id in std::mem::take(&mut layout_reflows) {
|
||||
if reflown.contains(&id) {
|
||||
continue;
|
||||
}
|
||||
reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds);
|
||||
reflow_shape(&id, state, &mut reflown, &mut entries, &mut bounds);
|
||||
}
|
||||
layout_reflows = Vec::new();
|
||||
}
|
||||
|
||||
modifiers
|
||||
|
||||
@@ -183,15 +183,18 @@ fn initialize_tracks(
|
||||
) -> Vec<TrackData> {
|
||||
let mut tracks = Vec::<TrackData>::new();
|
||||
let mut current_track = TrackData::default();
|
||||
let mut children = shape.children_ids(true);
|
||||
let mut first = true;
|
||||
|
||||
if flex_data.is_reverse() {
|
||||
children.reverse();
|
||||
}
|
||||
// When is_reverse() is true, we need forward order (children_ids_iter_forward).
|
||||
// When is_reverse() is false, we need reversed order (children_ids_iter).
|
||||
let children_iter: Box<dyn Iterator<Item = Uuid>> = if flex_data.is_reverse() {
|
||||
Box::new(shape.children_ids_iter_forward(true).copied())
|
||||
} else {
|
||||
Box::new(shape.children_ids_iter(true).copied())
|
||||
};
|
||||
|
||||
for child_id in children.iter() {
|
||||
let Some(child) = shapes.get(child_id) else {
|
||||
for child_id in children_iter {
|
||||
let Some(child) = shapes.get(&child_id) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -292,7 +295,7 @@ fn distribute_fill_main_space(layout_axis: &LayoutAxis, tracks: &mut [TrackData]
|
||||
track.main_size += delta;
|
||||
|
||||
if (child.main_size - child.max_main_size).abs() < MIN_SIZE {
|
||||
to_resize_children.remove(i);
|
||||
to_resize_children.swap_remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,7 +332,7 @@ fn distribute_fill_across_space(layout_axis: &LayoutAxis, tracks: &mut [TrackDat
|
||||
left_space -= delta;
|
||||
|
||||
if (track.across_size - track.max_across_size).abs() < MIN_SIZE {
|
||||
to_resize_tracks.remove(i);
|
||||
to_resize_tracks.swap_remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::shapes::{
|
||||
};
|
||||
use crate::state::ShapesPoolRef;
|
||||
use crate::uuid::Uuid;
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::collections::{HashMap, HashSet, VecDeque};
|
||||
|
||||
use super::common::GetBounds;
|
||||
|
||||
@@ -537,7 +537,7 @@ fn cell_bounds(
|
||||
|
||||
pub fn create_cell_data<'a>(
|
||||
layout_bounds: &Bounds,
|
||||
children: &[Uuid],
|
||||
children: &HashSet<Uuid>,
|
||||
shapes: ShapesPoolRef<'a>,
|
||||
cells: &Vec<GridCell>,
|
||||
column_tracks: &[TrackData],
|
||||
@@ -614,7 +614,7 @@ pub fn grid_cell_data<'a>(
|
||||
|
||||
let bounds = &mut HashMap::<Uuid, Bounds>::new();
|
||||
let layout_bounds = shape.bounds();
|
||||
let children = shape.children_ids(false);
|
||||
let children: HashSet<Uuid> = shape.children_ids_iter(false).copied().collect();
|
||||
|
||||
let column_tracks = calculate_tracks(
|
||||
true,
|
||||
@@ -707,7 +707,7 @@ pub fn reflow_grid_layout(
|
||||
) -> VecDeque<Modifier> {
|
||||
let mut result = VecDeque::new();
|
||||
let layout_bounds = bounds.find(shape);
|
||||
let children = shape.children_ids(true);
|
||||
let children: HashSet<Uuid> = shape.children_ids_iter(true).copied().collect();
|
||||
|
||||
let column_tracks = calculate_tracks(
|
||||
true,
|
||||
|
||||
@@ -209,16 +209,19 @@ impl PendingTiles {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) {
|
||||
self.list.clear();
|
||||
|
||||
let columns = tile_viewbox.interest_rect.width();
|
||||
let rows = tile_viewbox.interest_rect.height();
|
||||
|
||||
// Generate tiles in spiral order from center
|
||||
fn generate_spiral(rect: &TileRect) -> Vec<Tile> {
|
||||
let columns = rect.width();
|
||||
let rows = rect.height();
|
||||
let total = columns * rows;
|
||||
|
||||
let mut cx = tile_viewbox.interest_rect.center_x();
|
||||
let mut cy = tile_viewbox.interest_rect.center_y();
|
||||
if total <= 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(total as usize);
|
||||
let mut cx = rect.center_x();
|
||||
let mut cy = rect.center_y();
|
||||
|
||||
let ratio = (columns as f32 / rows as f32).ceil() as i32;
|
||||
|
||||
@@ -228,7 +231,7 @@ impl PendingTiles {
|
||||
let mut direction = 0;
|
||||
let mut current = 0;
|
||||
|
||||
self.list.push(Tile(cx, cy));
|
||||
result.push(Tile(cx, cy));
|
||||
while current < total {
|
||||
match direction {
|
||||
0 => cx += 1,
|
||||
@@ -238,7 +241,7 @@ impl PendingTiles {
|
||||
_ => unreachable!("Invalid direction"),
|
||||
}
|
||||
|
||||
self.list.push(Tile(cx, cy));
|
||||
result.push(Tile(cx, cy));
|
||||
|
||||
direction_current += 1;
|
||||
let direction_total = if direction % 2 == 0 {
|
||||
@@ -258,18 +261,44 @@ impl PendingTiles {
|
||||
}
|
||||
current += 1;
|
||||
}
|
||||
self.list.reverse();
|
||||
result.reverse();
|
||||
result
|
||||
}
|
||||
|
||||
// Create a new list where the cached tiles go first
|
||||
let iter1 = self
|
||||
.list
|
||||
.iter()
|
||||
.filter(|t| surfaces.has_cached_tile_surface(**t));
|
||||
let iter2 = self
|
||||
.list
|
||||
.iter()
|
||||
.filter(|t| !surfaces.has_cached_tile_surface(**t));
|
||||
self.list = iter1.chain(iter2).copied().collect();
|
||||
pub fn update(&mut self, tile_viewbox: &TileViewbox, surfaces: &Surfaces) {
|
||||
self.list.clear();
|
||||
|
||||
// Generate spiral for the interest area (viewport + margin)
|
||||
let spiral = Self::generate_spiral(&tile_viewbox.interest_rect);
|
||||
|
||||
// Partition tiles into 4 priority groups (highest priority = processed last due to pop()):
|
||||
// 1. visible + cached (fastest - just blit from cache)
|
||||
// 2. visible + uncached (user sees these, render next)
|
||||
// 3. interest + cached (pre-rendered area, blit from cache)
|
||||
// 4. interest + uncached (lowest priority - background pre-render)
|
||||
let mut visible_cached = Vec::new();
|
||||
let mut visible_uncached = Vec::new();
|
||||
let mut interest_cached = Vec::new();
|
||||
let mut interest_uncached = Vec::new();
|
||||
|
||||
for tile in spiral {
|
||||
let is_visible = tile_viewbox.visible_rect.contains(&tile);
|
||||
let is_cached = surfaces.has_cached_tile_surface(tile);
|
||||
|
||||
match (is_visible, is_cached) {
|
||||
(true, true) => visible_cached.push(tile),
|
||||
(true, false) => visible_uncached.push(tile),
|
||||
(false, true) => interest_cached.push(tile),
|
||||
(false, false) => interest_uncached.push(tile),
|
||||
}
|
||||
}
|
||||
|
||||
// Build final list with lowest priority first (they get popped last)
|
||||
// Order: interest_uncached, interest_cached, visible_uncached, visible_cached
|
||||
self.list.extend(interest_uncached);
|
||||
self.list.extend(interest_cached);
|
||||
self.list.extend(visible_uncached);
|
||||
self.list.extend(visible_cached);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Option<Tile> {
|
||||
|
||||
Reference in New Issue
Block a user