mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 19:28:12 +00:00
🎉 Ignore frames and groups when they have no visual extra information
This commit is contained in:
parent
3e99ad036c
commit
1810df232b
@ -10,7 +10,6 @@ mod shadows;
|
||||
mod strokes;
|
||||
mod surfaces;
|
||||
pub mod text;
|
||||
|
||||
mod ui;
|
||||
|
||||
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
||||
@ -53,6 +52,25 @@ pub struct NodeRenderState {
|
||||
mask: bool,
|
||||
}
|
||||
|
||||
/// Get simplified children of a container, flattening nested flattened containers
|
||||
fn get_simplified_children<'a>(tree: ShapesPoolRef<'a>, shape: &'a Shape) -> Vec<Uuid> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for child_id in shape.children_ids_iter(false) {
|
||||
if let Some(child) = tree.get(child_id) {
|
||||
if child.can_flatten() {
|
||||
// Child is flattened: recursively get its simplified children
|
||||
result.extend(get_simplified_children(tree, child));
|
||||
} else {
|
||||
// Child is not flattened: add it directly
|
||||
result.push(*child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
impl NodeRenderState {
|
||||
pub fn is_root(&self) -> bool {
|
||||
self.id.is_nil()
|
||||
@ -1038,6 +1056,7 @@ impl RenderState {
|
||||
// reorder by distance to the center.
|
||||
self.current_tile = None;
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None);
|
||||
|
||||
if sync_render {
|
||||
@ -1478,31 +1497,42 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if visited_children {
|
||||
self.render_shape_exit(element, visited_mask);
|
||||
// Skip render_shape_exit for flattened containers
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_exit(element, visited_mask);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() {
|
||||
let transformed_element: Cow<Shape> = Cow::Borrowed(element);
|
||||
|
||||
// Aggressive early exit: check hidden and selrect first (fastest checks)
|
||||
// Aggressive early exit: check hidden first (fastest check)
|
||||
if transformed_element.hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
let selrect = transformed_element.selrect();
|
||||
if !selrect.intersects(self.render_area) {
|
||||
continue;
|
||||
}
|
||||
// For frames and groups, we must use extrect because they can have nested content
|
||||
// that extends beyond their selrect. Using selrect for early exit would incorrectly
|
||||
// skip frames/groups that have nested content in the current tile.
|
||||
let is_container = matches!(
|
||||
transformed_element.shape_type,
|
||||
crate::shapes::Type::Frame(_) | crate::shapes::Type::Group(_)
|
||||
);
|
||||
|
||||
// For simple shapes without effects, selrect check is sufficient
|
||||
// Only calculate expensive extrect for shapes with effects that might extend bounds
|
||||
let scale = self.get_scale();
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = if !has_effects {
|
||||
let is_visible = if is_container {
|
||||
// Containers (frames/groups) must always use extrect to include nested content
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
!transformed_element.visually_insignificant(scale, tree)
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else {
|
||||
// Shape with effects: need extrect for accurate bounds
|
||||
let extrect = transformed_element.extrect(tree, scale);
|
||||
@ -1521,7 +1551,12 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
// Skip render_shape_enter/exit for flattened containers
|
||||
// If a container was flattened, it doesn't affect children visually, so we skip
|
||||
// the expensive enter/exit operations and process children directly
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_enter(element, mask);
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
let scale: f32 = self.get_scale();
|
||||
@ -1725,14 +1760,18 @@ impl RenderState {
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
}
|
||||
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
// Skip nested state updates for flattened containers
|
||||
// Flattened containers don't affect children, so we don't need to track their state
|
||||
if !element.can_flatten() {
|
||||
match element.shape_type {
|
||||
Type::Frame(_) if Self::frame_clip_layer_blur(element).is_some() => {
|
||||
self.nested_blurs.push(None);
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Type::Frame(_) | Type::Group(_) => {
|
||||
self.nested_blurs.push(element.blur);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Set the node as visited_children before processing children
|
||||
@ -1747,24 +1786,35 @@ impl RenderState {
|
||||
if element.is_recursive() {
|
||||
let children_clip_bounds =
|
||||
node_render_state.get_children_clip_bounds(element, None);
|
||||
let mut children_ids: Vec<_> = element.children_ids_iter(false).collect();
|
||||
|
||||
let children_ids: Vec<_> = if element.can_flatten() {
|
||||
// Container was flattened: get simplified children (which skip this level)
|
||||
get_simplified_children(tree, element)
|
||||
} else {
|
||||
// Container not flattened: use original children
|
||||
element.children_ids_iter(false).copied().collect()
|
||||
};
|
||||
|
||||
// Z-index ordering on Layouts
|
||||
if element.has_layout() {
|
||||
let children_ids = if element.has_layout() {
|
||||
let mut ids = children_ids;
|
||||
if element.is_flex() && !element.is_flex_reverse() {
|
||||
children_ids.reverse();
|
||||
ids.reverse();
|
||||
}
|
||||
|
||||
children_ids.sort_by(|id1, id2| {
|
||||
ids.sort_by(|id1, id2| {
|
||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||
z2.cmp(&z1)
|
||||
});
|
||||
}
|
||||
ids
|
||||
} else {
|
||||
children_ids
|
||||
};
|
||||
|
||||
for child_id in children_ids.iter() {
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: **child_id,
|
||||
id: *child_id,
|
||||
visited_children: false,
|
||||
clip_bounds: children_clip_bounds.clone(),
|
||||
visited_mask: false,
|
||||
|
||||
@ -920,27 +920,15 @@ impl Shape {
|
||||
}
|
||||
|
||||
Type::Group(_) | Type::Frame(_) if !self.clip_content => {
|
||||
// Use selrect as a fast approximation first, then calculate
|
||||
// extrect only if needed. This avoids expensive recursive extrect calculations
|
||||
// for children that don't significantly expand the bounds.
|
||||
// For frames and groups, we must always calculate extrect for all children
|
||||
// to ensure accurate bounds that include nested content across all tiles.
|
||||
// Using selrect for children can cause frames to be incorrectly omitted from
|
||||
// tiles where they have nested content.
|
||||
for child_id in self.children_ids_iter(false) {
|
||||
if let Some(child_shape) = shapes_pool.get(child_id) {
|
||||
// Fast path: check if child has effects that might expand bounds
|
||||
// If no effects, selrect is likely sufficient
|
||||
let has_effects = !child_shape.shadows.is_empty()
|
||||
|| child_shape.blur.is_some()
|
||||
|| !child_shape.strokes.is_empty()
|
||||
|| matches!(child_shape.shape_type, Type::Group(_) | Type::Frame(_));
|
||||
|
||||
if has_effects {
|
||||
// Calculate full extrect for shapes with effects
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
} else {
|
||||
// No effects, selrect is sufficient (much faster)
|
||||
let child_selrect = child_shape.selrect();
|
||||
rect.join(child_selrect);
|
||||
}
|
||||
// Always calculate full extrect for children to ensure accurate bounds
|
||||
let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
|
||||
rect.join(child_extrect);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1436,6 +1424,61 @@ impl Shape {
|
||||
!self.fills.is_empty()
|
||||
}
|
||||
|
||||
/// Determines if this frame or group can be flattened (doesn't affect children visually)
|
||||
/// A container can be flattened if it has no visual effects that affect its children
|
||||
/// and doesn't render its own content (no fills/strokes)
|
||||
pub fn can_flatten(&self) -> bool {
|
||||
// Only frames and groups can be flattened
|
||||
if !matches!(self.shape_type, Type::Frame(_) | Type::Group(_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cannot flatten if it has visual effects that affect children:
|
||||
|
||||
if self.clip_content {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.transform.is_identity() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.opacity != 1.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blend_mode() != BlendMode::default() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.blur.is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.shadows.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Type::Group(group) = &self.shape_type {
|
||||
if group.masked {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if self.hidden {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the container itself has fills/strokes, it renders something visible
|
||||
// We cannot flatten containers that render their own background/border
|
||||
// because they need to be rendered even if they don't affect children
|
||||
if self.has_fills() || self.has_visible_strokes() {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Checks if this shape needs a layer for rendering due to visual effects
|
||||
/// (opacity < 1.0, non-default blend mode, or frame clip layer blur)
|
||||
pub fn needs_layer(&self) -> bool {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user