mirror of
https://github.com/penpot/penpot.git
synced 2026-09-08 13:09:22 +00:00
🐛 Fix shapes cut when overflowing a non-clipping board
This commit is contained in:
parent
2ce202c7d8
commit
17dfbef211
@ -911,7 +911,7 @@ pub extern "C" fn clean_modifiers() -> Result<()> {
|
|||||||
// the same tiles for the active modifier set, so the eviction
|
// the same tiles for the active modifier set, so the eviction
|
||||||
// here is redundant and doubles the per-emission cost.
|
// here is redundant and doubles the per-emission cost.
|
||||||
if !prev_modifier_ids.is_empty() && !render_state.options.is_interactive_transform() {
|
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(())
|
Ok(())
|
||||||
|
|||||||
@ -4202,22 +4202,8 @@ impl RenderState {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/// Diffs the shape's tile set, leaving cached tiles alone. For callers where the
|
||||||
* Incremental version of update_shape_tiles for pan/zoom operations.
|
/// index moves but painted content does not: pan/zoom.
|
||||||
* 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(
|
pub fn update_shape_tiles_incremental(
|
||||||
&mut self,
|
&mut self,
|
||||||
shape: &Shape,
|
shape: &Shape,
|
||||||
@ -4381,19 +4367,10 @@ impl RenderState {
|
|||||||
performance::end_measure!("rebuild_touched_tiles");
|
performance::end_measure!("rebuild_touched_tiles");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Invalidates extended rectangles and updates tiles for a set of shapes
|
/// Re-indexes a set of shapes and evicts the cached tiles they dirty. Extrect caches
|
||||||
///
|
/// are not dropped here: `State::touch_shape` and `rebuild_modifier_tiles` invalidate
|
||||||
/// This function takes a set of shape IDs and for each one:
|
/// them at the source.
|
||||||
/// 1. Invalidates the extrect cache
|
pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolRef) -> Result<()> {
|
||||||
/// 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<()> {
|
|
||||||
performance::begin_measure!("invalidate_and_update_tiles");
|
performance::begin_measure!("invalidate_and_update_tiles");
|
||||||
for shape_id in shape_ids {
|
for shape_id in shape_ids {
|
||||||
if let Some(shape) = tree.get(shape_id) {
|
if let Some(shape) = tree.get(shape_id) {
|
||||||
@ -4439,22 +4416,17 @@ impl RenderState {
|
|||||||
self.surfaces.invalidate_cached_tiles_intersecting(dirty);
|
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.
|
|
||||||
pub fn rebuild_modifier_tiles(
|
pub fn rebuild_modifier_tiles(
|
||||||
&mut self,
|
&mut self,
|
||||||
tree: ShapesPoolMutRef<'_>,
|
tree: ShapesPoolMutRef<'_>,
|
||||||
ids: &[Uuid],
|
ids: &[Uuid],
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// During interactive transform, skip ancestor invalidation: walking up to the
|
// `set_modifiers` runs per pointer move, this runs once per rAF, so the ancestor
|
||||||
// parent frame evicts every tile the frame covers, including dense tiles with
|
// caches are dropped here. Must precede any read of their tile coverage below.
|
||||||
// many siblings. Ancestor extrect caches are already invalidated by
|
for id in ids {
|
||||||
// `ShapesPool::set_modifiers`; the tile index is reconciled post-gesture by
|
tree.invalidate_ancestors_extrect(id);
|
||||||
// the committing code path (rebuild_touched_tiles).
|
}
|
||||||
|
|
||||||
if self.options.is_interactive_transform() {
|
if self.options.is_interactive_transform() {
|
||||||
self.update_tiles_shapes(ids, tree)?;
|
self.update_tiles_shapes(ids, tree)?;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -960,6 +960,14 @@ impl Shape {
|
|||||||
Bounds::from_rect(&rect)
|
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(
|
fn apply_children_bounds(
|
||||||
&self,
|
&self,
|
||||||
bounds: Bounds,
|
bounds: Bounds,
|
||||||
@ -1124,21 +1132,21 @@ impl Shape {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
|
fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
|
||||||
|
// Own outsets (strokes, shadows, blur) are local-space, so they expand before the
|
||||||
|
// shape transform. Children extrects are already world-space: join them after it.
|
||||||
let mut bounds = self.own_extrect_bounds();
|
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() {
|
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 mut matrix = self.transform;
|
||||||
let center = self.center();
|
let center = self.center();
|
||||||
matrix.post_translate(center);
|
matrix.post_translate(center);
|
||||||
matrix.pre_translate(-center);
|
matrix.pre_translate(-center);
|
||||||
bounds.transform_mut(&matrix);
|
bounds.transform_mut(&matrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bounds = self.apply_children_bounds(bounds, shapes_pool, scale);
|
||||||
|
bounds = self.apply_children_blur(bounds, shapes_pool);
|
||||||
|
|
||||||
bounds.to_rect()
|
bounds.to_rect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -341,21 +341,13 @@ impl State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn touch_current(&mut self) {
|
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 {
|
if let Some(current_id) = self.current_id {
|
||||||
let prev = self
|
self.touch_shape(current_id);
|
||||||
.shapes
|
|
||||||
.get(¤t_id)
|
|
||||||
.map(|shape| shape.extrect(&self.shapes, 1.0));
|
|
||||||
get_render_state().mark_touched_with_prev(current_id, prev);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn touch_shape(&mut self, id: Uuid) {
|
pub fn touch_shape(&mut self, id: Uuid) {
|
||||||
|
self.shapes.invalidate_ancestors_extrect(&id);
|
||||||
if self.loading || !has_render_state() {
|
if self.loading || !has_render_state() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -239,6 +239,33 @@ impl ShapesPoolImpl {
|
|||||||
self.modified_shape_cache.clear()
|
self.modified_shape_cache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drops the extrect cache of every ancestor whose extrect grows with this shape.
|
||||||
|
/// Stops at the first ancestor that clips: its extrect no longer follows the child,
|
||||||
|
/// and neither does anything above it.
|
||||||
|
pub fn invalidate_ancestors_extrect(&mut self, id: &Uuid) {
|
||||||
|
let mut current = self
|
||||||
|
.uuid_to_idx
|
||||||
|
.get(id)
|
||||||
|
.and_then(|idx| self.shapes[*idx].parent_id);
|
||||||
|
|
||||||
|
while let Some(parent_id) = current.filter(|parent_id| !parent_id.is_nil()) {
|
||||||
|
let Some(parent_idx) = self.uuid_to_idx.get(&parent_id).copied() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
if !self.shapes[parent_idx].extrect_depends_on_children() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shapes[parent_idx].invalidate_extrect();
|
||||||
|
// The cached modified shape is a clone, with its own copy of the extrect cache.
|
||||||
|
if let Some(cell) = self.modified_shape_cache.get_mut(&parent_idx) {
|
||||||
|
*cell = OnceCell::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
current = self.shapes[parent_idx].parent_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||||
let mut ids = Vec::<Uuid>::new();
|
let mut ids = Vec::<Uuid>::new();
|
||||||
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
||||||
@ -254,10 +281,8 @@ impl ShapesPoolImpl {
|
|||||||
// When CLJS sends only root shapes (translation on drag), descendants
|
// When CLJS sends only root shapes (translation on drag), descendants
|
||||||
// need the same matrix.
|
// need the same matrix.
|
||||||
// For resize/rotate, propagate-modifiers already includes all descendants.
|
// For resize/rotate, propagate-modifiers already includes all descendants.
|
||||||
// Descendants are NOT pushed into `ids` / `modifier_uuids`: tile invalidation
|
// Descendants are NOT pushed into `ids` / `modifier_uuids`: rebuild_modifier_tiles
|
||||||
// via rebuild_modifier_tiles only runs for roots, which is sufficient because
|
// runs for roots, and drops the non-clipping ancestors' extrects separately.
|
||||||
// descendants always lie inside the parent's bounding box and are therefore
|
|
||||||
// covered by the parent's old/new tile ranges.
|
|
||||||
let root_pairs: Vec<(usize, skia::Matrix)> = ids
|
let root_pairs: Vec<(usize, skia::Matrix)> = ids
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|uuid| {
|
.filter_map(|uuid| {
|
||||||
@ -289,13 +314,15 @@ impl ShapesPoolImpl {
|
|||||||
// Compute ancestors before consuming `ids` so we can move it into
|
// Compute ancestors before consuming `ids` so we can move it into
|
||||||
// `modifier_uuids` without a clone.
|
// `modifier_uuids` without a clone.
|
||||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
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 {
|
for uuid in all_ids {
|
||||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
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>>) {
|
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user