mirror of
https://github.com/penpot/penpot.git
synced 2026-08-29 08:08:46 +00:00
🐛 Fix shapes cut when overflowing a non-clipping board
This commit is contained in:
parent
2ce202c7d8
commit
6584de6987
@ -911,7 +911,7 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
// the same tiles for the active modifier set, so the eviction
|
||||
// here is redundant and doubles the per-emission cost.
|
||||
if !prev_modifier_ids.is_empty() && !render_state.options.is_interactive_transform() {
|
||||
render_state.update_tiles_shapes(&prev_modifier_ids, &mut state.shapes)?;
|
||||
render_state.update_tiles_shapes(&prev_modifier_ids, &state.shapes)?;
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
|
||||
@ -4202,22 +4202,8 @@ impl RenderState {
|
||||
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.
|
||||
*/
|
||||
/// Diffs the shape's tile set, leaving cached tiles alone. For callers where the
|
||||
/// index moves but painted content does not: pan/zoom, and re-indexed ancestors.
|
||||
pub fn update_shape_tiles_incremental(
|
||||
&mut self,
|
||||
shape: &Shape,
|
||||
@ -4378,22 +4364,14 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
self.index_dependent_ancestors(&ids, tree);
|
||||
|
||||
performance::end_measure!("rebuild_touched_tiles");
|
||||
}
|
||||
|
||||
/// Invalidates extended rectangles and updates tiles for a set of shapes
|
||||
///
|
||||
/// This function takes a set of shape IDs and for each one:
|
||||
/// 1. Invalidates the extrect cache
|
||||
/// 2. Updates the tiles to ensure proper rendering
|
||||
///
|
||||
/// This is useful when you have a pre-computed set of shape IDs that need to be refreshed,
|
||||
/// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection).
|
||||
pub fn update_tiles_shapes(
|
||||
&mut self,
|
||||
shape_ids: &[Uuid],
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
) -> Result<()> {
|
||||
/// Re-indexes a set of shapes and evicts the cached tiles they dirty. Extrect caches
|
||||
/// are not dropped here; `State::touch_shape` invalidates them at the source.
|
||||
pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolRef) -> Result<()> {
|
||||
performance::begin_measure!("invalidate_and_update_tiles");
|
||||
for shape_id in shape_ids {
|
||||
if let Some(shape) = tree.get(shape_id) {
|
||||
@ -4439,24 +4417,51 @@ impl RenderState {
|
||||
self.surfaces.invalidate_cached_tiles_intersecting(dirty);
|
||||
}
|
||||
|
||||
/// Rebuilds tiles for shapes with modifiers and processes their ancestors
|
||||
///
|
||||
/// This function applies transformation modifiers to shapes and updates their tiles.
|
||||
/// Additionally, it processes all ancestors of modified shapes to ensure their
|
||||
/// extended rectangles are properly recalculated and their tiles are updated.
|
||||
/// This is crucial for frames and groups that contain transformed children.
|
||||
fn index_dependent_ancestors(&mut self, ids: &HashSet<Uuid>, tree: ShapesPoolRef) {
|
||||
if ids.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut ancestors = Vec::<Uuid>::new();
|
||||
let mut seen = HashSet::<Uuid>::new();
|
||||
|
||||
for id in ids.iter() {
|
||||
ancestors.clear();
|
||||
tree.collect_dependent_ancestors(id, &mut ancestors);
|
||||
|
||||
for ancestor_id in ancestors.iter() {
|
||||
if ids.contains(ancestor_id) || !seen.insert(*ancestor_id) {
|
||||
continue;
|
||||
}
|
||||
let Some(shape) = tree.get(ancestor_id) else {
|
||||
continue;
|
||||
};
|
||||
// A hidden ancestor paints nothing, but keep climbing: its parent may.
|
||||
if shape.hidden() {
|
||||
continue;
|
||||
}
|
||||
// Diffs the tile set instead of removing and re-adding every tile, which
|
||||
// matters because this runs once per gesture frame.
|
||||
let _ = self.update_shape_tiles_incremental(shape, tree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(
|
||||
&mut self,
|
||||
tree: ShapesPoolMutRef<'_>,
|
||||
ids: &[Uuid],
|
||||
) -> Result<()> {
|
||||
// During interactive transform, skip ancestor invalidation: walking up to the
|
||||
// parent frame evicts every tile the frame covers, including dense tiles with
|
||||
// many siblings. Ancestor extrect caches are already invalidated by
|
||||
// `ShapesPool::set_modifiers`; the tile index is reconciled post-gesture by
|
||||
// the committing code path (rebuild_touched_tiles).
|
||||
// `set_modifiers` runs per pointer move, this runs once per rAF, so the ancestor
|
||||
// caches are dropped here. Must precede any read of their tile coverage below.
|
||||
for id in ids {
|
||||
tree.invalidate_ancestors_extrect(id);
|
||||
}
|
||||
|
||||
if self.options.is_interactive_transform() {
|
||||
self.update_tiles_shapes(ids, tree)?;
|
||||
let modified: HashSet<Uuid> = ids.iter().copied().collect();
|
||||
self.index_dependent_ancestors(&modified, tree);
|
||||
} else {
|
||||
let ancestors = all_with_ancestors(ids, tree, false);
|
||||
self.update_tiles_shapes(&ancestors, tree)?;
|
||||
|
||||
@ -960,6 +960,14 @@ impl Shape {
|
||||
Bounds::from_rect(&rect)
|
||||
}
|
||||
|
||||
pub fn extrect_depends_on_children(&self) -> bool {
|
||||
match self.shape_type {
|
||||
Type::Group(Group { masked: true }) => true,
|
||||
Type::Group(_) | Type::Frame(_) => !self.clip_content,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_children_bounds(
|
||||
&self,
|
||||
bounds: Bounds,
|
||||
@ -1125,20 +1133,18 @@ impl Shape {
|
||||
|
||||
fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
|
||||
let mut bounds = self.own_extrect_bounds();
|
||||
bounds = self.apply_children_bounds(bounds, shapes_pool, scale);
|
||||
bounds = self.apply_children_blur(bounds, shapes_pool);
|
||||
|
||||
if !self.transform.is_identity() {
|
||||
// Expand everything in the shape's local axis-aligned space first (strokes,
|
||||
// shadows, blur, children). Only after that do we map the resulting bounds
|
||||
// through the shape transform so rotation/skew is reflected in the final
|
||||
// extrect.
|
||||
let mut matrix = self.transform;
|
||||
let center = self.center();
|
||||
matrix.post_translate(center);
|
||||
matrix.pre_translate(-center);
|
||||
bounds.transform_mut(&matrix);
|
||||
}
|
||||
|
||||
bounds = self.apply_children_bounds(bounds, shapes_pool, scale);
|
||||
bounds = self.apply_children_blur(bounds, shapes_pool);
|
||||
|
||||
bounds.to_rect()
|
||||
}
|
||||
|
||||
|
||||
@ -341,21 +341,13 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn touch_current(&mut self) {
|
||||
// `mark_touched` only drives incremental on-screen tile invalidation;
|
||||
// the headless export path has no render state, so skip it there.
|
||||
if self.loading || !has_render_state() {
|
||||
return;
|
||||
}
|
||||
if let Some(current_id) = self.current_id {
|
||||
let prev = self
|
||||
.shapes
|
||||
.get(¤t_id)
|
||||
.map(|shape| shape.extrect(&self.shapes, 1.0));
|
||||
get_render_state().mark_touched_with_prev(current_id, prev);
|
||||
self.touch_shape(current_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touch_shape(&mut self, id: Uuid) {
|
||||
self.shapes.invalidate_ancestors_extrect(&id);
|
||||
if self.loading || !has_render_state() {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -59,6 +59,8 @@ pub struct ShapesPoolImpl {
|
||||
structure: HashMap<usize, Vec<StructureEntry>>,
|
||||
/// Scale content values, keyed by index
|
||||
scale_content: HashMap<usize, f32>,
|
||||
/// Scratch for ancestor walks: `invalidate_ancestors_extrect` runs on every mutation.
|
||||
ancestor_scratch: Vec<Uuid>,
|
||||
}
|
||||
|
||||
// Type aliases - no longer need lifetimes!
|
||||
@ -78,6 +80,7 @@ impl ShapesPoolImpl {
|
||||
modifier_uuids: Vec::new(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
ancestor_scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -239,6 +242,54 @@ impl ShapesPoolImpl {
|
||||
self.modified_shape_cache.clear()
|
||||
}
|
||||
|
||||
pub fn collect_dependent_ancestors(&self, id: &Uuid, out: &mut Vec<Uuid>) {
|
||||
let Some(idx) = self.uuid_to_idx.get(id).copied() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut current = self.shapes[idx].parent_id;
|
||||
let mut depth = 0;
|
||||
|
||||
while let Some(parent_id) = current {
|
||||
if parent_id.is_nil() {
|
||||
break;
|
||||
}
|
||||
let Some(parent_idx) = self.uuid_to_idx.get(&parent_id).copied() else {
|
||||
break;
|
||||
};
|
||||
if !self.shapes[parent_idx].extrect_depends_on_children() {
|
||||
break;
|
||||
}
|
||||
|
||||
out.push(parent_id);
|
||||
current = self.shapes[parent_idx].parent_id;
|
||||
|
||||
// A chain longer than the pool means the parent links form a cycle.
|
||||
depth += 1;
|
||||
if depth >= self.shapes.len() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_ancestors_extrect(&mut self, id: &Uuid) {
|
||||
let mut scratch = std::mem::take(&mut self.ancestor_scratch);
|
||||
scratch.clear();
|
||||
self.collect_dependent_ancestors(id, &mut scratch);
|
||||
|
||||
for parent_id in scratch.iter() {
|
||||
let Some(parent_idx) = self.uuid_to_idx.get(parent_id).copied() else {
|
||||
continue;
|
||||
};
|
||||
self.shapes[parent_idx].invalidate_extrect();
|
||||
if let Some(cell) = self.modified_shape_cache.get_mut(&parent_idx) {
|
||||
*cell = OnceCell::new();
|
||||
}
|
||||
}
|
||||
|
||||
self.ancestor_scratch = scratch;
|
||||
}
|
||||
|
||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
||||
@ -254,10 +305,8 @@ impl ShapesPoolImpl {
|
||||
// When CLJS sends only root shapes (translation on drag), descendants
|
||||
// need the same matrix.
|
||||
// For resize/rotate, propagate-modifiers already includes all descendants.
|
||||
// Descendants are NOT pushed into `ids` / `modifier_uuids`: tile invalidation
|
||||
// via rebuild_modifier_tiles only runs for roots, which is sufficient because
|
||||
// descendants always lie inside the parent's bounding box and are therefore
|
||||
// covered by the parent's old/new tile ranges.
|
||||
// Descendants are NOT pushed into `ids` / `modifier_uuids`: rebuild_modifier_tiles
|
||||
// runs for roots, and drops the non-clipping ancestors' extrects separately.
|
||||
let root_pairs: Vec<(usize, skia::Matrix)> = ids
|
||||
.iter()
|
||||
.filter_map(|uuid| {
|
||||
@ -289,13 +338,15 @@ impl ShapesPoolImpl {
|
||||
// Compute ancestors before consuming `ids` so we can move it into
|
||||
// `modifier_uuids` without a clone.
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
// rebuild_modifier_tiles doesn't process every descendant individually.
|
||||
self.modifier_uuids = ids;
|
||||
|
||||
for uuid in all_ids {
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
|
||||
// rebuild_modifier_tiles doesn't process every descendant individually.
|
||||
self.modifier_uuids = ids;
|
||||
}
|
||||
|
||||
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
|
||||
@ -402,6 +453,7 @@ impl ShapesPoolImpl {
|
||||
modifier_uuids: Vec::new(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
ancestor_scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -484,6 +536,7 @@ impl Clone for ShapesPoolImpl {
|
||||
modifier_uuids: self.modifier_uuids.clone(),
|
||||
structure: self.structure.clone(),
|
||||
scale_content: self.scale_content.clone(),
|
||||
ancestor_scratch: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user