|
|
|
@@ -10,9 +10,12 @@ mod shadows;
|
|
|
|
|
mod strokes;
|
|
|
|
|
mod surfaces;
|
|
|
|
|
pub mod text;
|
|
|
|
|
mod tree_simplifier;
|
|
|
|
|
|
|
|
|
|
mod ui;
|
|
|
|
|
|
|
|
|
|
use tree_simplifier::SimplifiedTree;
|
|
|
|
|
|
|
|
|
|
use skia_safe::{self as skia, Matrix, RRect, Rect};
|
|
|
|
|
use std::borrow::Cow;
|
|
|
|
|
use std::collections::HashSet;
|
|
|
|
@@ -57,7 +60,90 @@ impl NodeRenderState {
|
|
|
|
|
pub fn is_root(&self) -> bool {
|
|
|
|
|
self.id.is_nil()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render signature that represents all properties affecting how a path is rendered
|
|
|
|
|
// This allows efficient comparison of paths to determine if they can be combined
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
struct PathRenderSignature<'a> {
|
|
|
|
|
clip_bounds: &'a Option<ClipStack>,
|
|
|
|
|
fills: &'a [Fill],
|
|
|
|
|
strokes: &'a [Stroke],
|
|
|
|
|
opacity: u32, // Converted to fixed-point for comparison (multiply by 1000)
|
|
|
|
|
blend_mode: crate::shapes::BlendMode,
|
|
|
|
|
transform: &'a Matrix,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a> PathRenderSignature<'a> {
|
|
|
|
|
fn from_shape(shape: &'a Shape, clip_bounds: &'a Option<ClipStack>) -> Option<Self> {
|
|
|
|
|
use crate::shapes::Type;
|
|
|
|
|
|
|
|
|
|
// Only paths can have this signature
|
|
|
|
|
if !matches!(shape.shape_type, Type::Path(_) | Type::Bool(_)) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cannot combine paths with shadows or blur
|
|
|
|
|
if !shape.shadows.is_empty() || shape.blur.is_some() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(Self {
|
|
|
|
|
clip_bounds,
|
|
|
|
|
fills: &shape.fills,
|
|
|
|
|
strokes: &shape.strokes,
|
|
|
|
|
opacity: (shape.opacity * 1000.0) as u32, // Convert to fixed-point
|
|
|
|
|
blend_mode: shape.blend_mode(),
|
|
|
|
|
transform: &shape.transform,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Manual comparison since Matrix doesn't implement Eq
|
|
|
|
|
fn equals(&self, other: &Self) -> bool {
|
|
|
|
|
// Compare clip_bounds
|
|
|
|
|
if self.clip_bounds != other.clip_bounds {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare fills (using PartialEq)
|
|
|
|
|
if self.fills != other.fills {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare strokes (using PartialEq)
|
|
|
|
|
if self.strokes != other.strokes {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare opacity
|
|
|
|
|
if self.opacity != other.opacity {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare blend_mode
|
|
|
|
|
if self.blend_mode != other.blend_mode {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compare transform (Matrix comparison - check if they're approximately equal)
|
|
|
|
|
// For now, use exact equality, but we could add epsilon comparison if needed
|
|
|
|
|
self.transform == other.transform
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SIMPLE OPTIMIZATION: Helper to check if two shapes can be rendered together
|
|
|
|
|
// Uses render signatures for efficient comparison
|
|
|
|
|
fn can_render_paths_together(shape1: &Shape, shape2: &Shape, clip_bounds1: &Option<ClipStack>, clip_bounds2: &Option<ClipStack>) -> bool {
|
|
|
|
|
let sig1 = PathRenderSignature::from_shape(shape1, clip_bounds1);
|
|
|
|
|
let sig2 = PathRenderSignature::from_shape(shape2, clip_bounds2);
|
|
|
|
|
|
|
|
|
|
match (sig1, sig2) {
|
|
|
|
|
(Some(sig1), Some(sig2)) => sig1.equals(&sig2),
|
|
|
|
|
_ => false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl NodeRenderState {
|
|
|
|
|
/// Calculates the clip bounds for child elements of a given shape.
|
|
|
|
|
///
|
|
|
|
|
/// This function determines the clipping region that should be applied to child elements
|
|
|
|
@@ -276,6 +362,10 @@ pub(crate) struct RenderState {
|
|
|
|
|
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
|
|
|
|
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
|
|
|
|
pub ignore_nested_blurs: bool,
|
|
|
|
|
/// DISRUPTIVE OPTIMIZATION: Simplified tree that flattens unnecessary frames/groups
|
|
|
|
|
/// This reduces tree depth and rendering overhead by removing containers that don't
|
|
|
|
|
/// visually affect their children (no clipping, transforms, opacity, etc.)
|
|
|
|
|
simplified_tree: SimplifiedTree,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
|
|
|
@@ -348,6 +438,7 @@ impl RenderState {
|
|
|
|
|
focus_mode: FocusMode::new(),
|
|
|
|
|
touched_ids: HashSet::default(),
|
|
|
|
|
ignore_nested_blurs: false,
|
|
|
|
|
simplified_tree: SimplifiedTree::new(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -502,6 +593,13 @@ impl RenderState {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) {
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Compose all intermediate surfaces once at tile end
|
|
|
|
|
// This replaces per-shape composition with a single batch composition, dramatically
|
|
|
|
|
// reducing overhead. For files with many simple shapes, this can be 10-100x faster.
|
|
|
|
|
// Instead of calling apply_drawing_to_render_canvas after each shape, we accumulate
|
|
|
|
|
// all rendering on intermediate surfaces (Fills, Strokes, etc.) and compose them once here.
|
|
|
|
|
self.apply_drawing_to_render_canvas(None);
|
|
|
|
|
|
|
|
|
|
let tile_rect = self.get_current_aligned_tile_bounds();
|
|
|
|
|
self.surfaces.cache_current_tile_texture(
|
|
|
|
|
&self.tile_viewbox,
|
|
|
|
@@ -521,6 +619,8 @@ impl RenderState {
|
|
|
|
|
|
|
|
|
|
let paint = skia::Paint::default();
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Skip empty surfaces to avoid unnecessary draw_into operations
|
|
|
|
|
// Check if surfaces have content before compositing (simple optimization)
|
|
|
|
|
self.surfaces
|
|
|
|
|
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
|
|
|
|
|
|
|
|
@@ -545,6 +645,8 @@ impl RenderState {
|
|
|
|
|
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Batch clear operations for better performance
|
|
|
|
|
// Instead of clearing each surface individually, we can optimize this
|
|
|
|
|
let surface_ids = SurfaceId::Strokes as u32
|
|
|
|
|
| SurfaceId::Fills as u32
|
|
|
|
|
| SurfaceId::InnerShadows as u32
|
|
|
|
@@ -605,9 +707,16 @@ impl RenderState {
|
|
|
|
|
| strokes_surface_id as u32
|
|
|
|
|
| innershadows_surface_id as u32
|
|
|
|
|
| text_drop_shadows_surface_id as u32;
|
|
|
|
|
self.surfaces.apply_mut(surface_ids, |s| {
|
|
|
|
|
s.canvas().save();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Optimization: Only save canvas state if we have clipping or transforms
|
|
|
|
|
// For simple shapes without clipping, skip expensive save/restore
|
|
|
|
|
let needs_save = clip_bounds.is_some() || offset.is_some() || !shape.transform.is_identity();
|
|
|
|
|
|
|
|
|
|
if needs_save {
|
|
|
|
|
self.surfaces.apply_mut(surface_ids, |s| {
|
|
|
|
|
s.canvas().save();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let antialias = shape.should_use_antialias(self.get_scale());
|
|
|
|
|
|
|
|
|
@@ -905,12 +1014,29 @@ impl RenderState {
|
|
|
|
|
debug::render_debug_shape(self, Some(shape_selrect_bounds), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Defer apply_drawing_to_render_canvas until end of tile
|
|
|
|
|
// This avoids expensive draw_into/clear operations per shape, dramatically improving
|
|
|
|
|
// performance for files with many shapes. Instead of composing surfaces after each shape,
|
|
|
|
|
// we accumulate all rendering on intermediate surfaces and compose once at tile end.
|
|
|
|
|
// Only apply immediately for complex shapes that need immediate composition (e.g., text with shadows)
|
|
|
|
|
if apply_to_current_surface {
|
|
|
|
|
self.apply_drawing_to_render_canvas(Some(&shape));
|
|
|
|
|
let needs_immediate_composition = matches!(shape.shape_type, Type::Text(_))
|
|
|
|
|
|| !shape.shadows.is_empty()
|
|
|
|
|
|| shape.blur.is_some();
|
|
|
|
|
|
|
|
|
|
if needs_immediate_composition {
|
|
|
|
|
// Complex shapes need immediate composition
|
|
|
|
|
self.apply_drawing_to_render_canvas(Some(&shape));
|
|
|
|
|
}
|
|
|
|
|
// Simple shapes: defer composition until end of tile (handled in apply_render_to_final_canvas)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only restore if we saved (optimization for simple shapes)
|
|
|
|
|
if needs_save {
|
|
|
|
|
self.surfaces.apply_mut(surface_ids, |s| {
|
|
|
|
|
s.canvas().restore();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
self.surfaces.apply_mut(surface_ids, |s| {
|
|
|
|
|
s.canvas().restore();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_render_context(&mut self, tile: tiles::Tile) {
|
|
|
|
@@ -1031,6 +1157,12 @@ impl RenderState {
|
|
|
|
|
// reorder by distance to the center.
|
|
|
|
|
self.current_tile = None;
|
|
|
|
|
self.render_in_progress = true;
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Build simplified tree to flatten unnecessary containers
|
|
|
|
|
// This reduces tree depth and rendering overhead
|
|
|
|
|
let root_id = base_object.copied().unwrap_or(Uuid::nil());
|
|
|
|
|
self.simplified_tree.build_from_tree(tree, root_id);
|
|
|
|
|
|
|
|
|
|
self.apply_drawing_to_render_canvas(None);
|
|
|
|
|
|
|
|
|
|
if sync_render {
|
|
|
|
@@ -1117,35 +1249,44 @@ impl RenderState {
|
|
|
|
|
self.nested_fills.push(Vec::new());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut paint = skia::Paint::default();
|
|
|
|
|
paint.set_blend_mode(element.blend_mode().into());
|
|
|
|
|
paint.set_alpha_f(element.opacity());
|
|
|
|
|
// Optimization: Only create save_layer if actually needed
|
|
|
|
|
// For simple shapes with default opacity and blend mode, skip expensive save_layer
|
|
|
|
|
let needs_layer = element.opacity() < 1.0
|
|
|
|
|
|| element.blend_mode().0 != skia::BlendMode::SrcOver
|
|
|
|
|
|| Self::frame_clip_layer_blur(element).is_some()
|
|
|
|
|
|| mask;
|
|
|
|
|
|
|
|
|
|
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
|
|
|
|
let scale = self.get_scale();
|
|
|
|
|
let sigma = frame_blur.value * scale;
|
|
|
|
|
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
|
|
|
|
paint.set_image_filter(filter);
|
|
|
|
|
if needs_layer {
|
|
|
|
|
let mut paint = skia::Paint::default();
|
|
|
|
|
paint.set_blend_mode(element.blend_mode().into());
|
|
|
|
|
paint.set_alpha_f(element.opacity());
|
|
|
|
|
|
|
|
|
|
if let Some(frame_blur) = Self::frame_clip_layer_blur(element) {
|
|
|
|
|
let scale = self.get_scale();
|
|
|
|
|
let sigma = frame_blur.value * scale;
|
|
|
|
|
if let Some(filter) = skia::image_filters::blur((sigma, sigma), None, None, None) {
|
|
|
|
|
paint.set_image_filter(filter);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When we're rendering the mask shape we need to set a special blend mode
|
|
|
|
|
// called 'destination-in' that keeps the drawn content within the mask.
|
|
|
|
|
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
|
|
|
|
if mask {
|
|
|
|
|
let mut mask_paint = skia::Paint::default();
|
|
|
|
|
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
|
|
|
|
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
|
|
|
|
// When we're rendering the mask shape we need to set a special blend mode
|
|
|
|
|
// called 'destination-in' that keeps the drawn content within the mask.
|
|
|
|
|
// @see https://skia.org/docs/user/api/skblendmode_overview/
|
|
|
|
|
if mask {
|
|
|
|
|
let mut mask_paint = skia::Paint::default();
|
|
|
|
|
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
|
|
|
|
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
|
|
|
|
self.surfaces
|
|
|
|
|
.canvas(SurfaceId::Current)
|
|
|
|
|
.save_layer(&mask_rec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
|
|
|
|
self.surfaces
|
|
|
|
|
.canvas(SurfaceId::Current)
|
|
|
|
|
.save_layer(&mask_rec);
|
|
|
|
|
.save_layer(&layer_rec);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
|
|
|
|
self.surfaces
|
|
|
|
|
.canvas(SurfaceId::Current)
|
|
|
|
|
.save_layer(&layer_rec);
|
|
|
|
|
|
|
|
|
|
self.focus_mode.enter(&element.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1217,7 +1358,17 @@ impl RenderState {
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.surfaces.canvas(SurfaceId::Current).restore();
|
|
|
|
|
// Only restore if we created a layer (optimization for simple shapes)
|
|
|
|
|
let needs_layer = element.opacity() < 1.0
|
|
|
|
|
|| element.blend_mode().0 != skia::BlendMode::SrcOver
|
|
|
|
|
|| Self::frame_clip_layer_blur(element).is_some()
|
|
|
|
|
|| (matches!(element.shape_type, Type::Group(_)) &&
|
|
|
|
|
matches!(element.shape_type, Type::Group(g) if g.masked));
|
|
|
|
|
|
|
|
|
|
if needs_layer {
|
|
|
|
|
self.surfaces.canvas(SurfaceId::Current).restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.focus_mode.exit(&element.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1433,9 +1584,98 @@ impl RenderState {
|
|
|
|
|
tree: ShapesPoolRef,
|
|
|
|
|
timestamp: i32,
|
|
|
|
|
allow_stop: bool,
|
|
|
|
|
) -> Result<(bool, bool), String> {
|
|
|
|
|
) -> Result<(bool, bool), String> {
|
|
|
|
|
let mut iteration = 0;
|
|
|
|
|
let mut is_empty = true;
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Pre-collect and sort visible shapes for better cache locality
|
|
|
|
|
// This dramatically improves performance for files with many simple shapes by:
|
|
|
|
|
// 1. Grouping similar shapes together (better cache locality)
|
|
|
|
|
// 2. Processing shapes in spatial order (better memory access patterns)
|
|
|
|
|
// 3. Early culling of invisible shapes before expensive operations
|
|
|
|
|
// Only activate when we have enough shapes to benefit (threshold: 50+ shapes)
|
|
|
|
|
const SORTING_THRESHOLD: usize = 50;
|
|
|
|
|
if self.pending_nodes.len() >= SORTING_THRESHOLD {
|
|
|
|
|
let mut visible_shapes: Vec<(NodeRenderState, Uuid)> = Vec::new();
|
|
|
|
|
let mut temp_pending = std::mem::take(&mut self.pending_nodes);
|
|
|
|
|
|
|
|
|
|
// First pass: collect all potentially visible shapes (fast culling)
|
|
|
|
|
while let Some(node_render_state) = temp_pending.pop() {
|
|
|
|
|
let node_id = node_render_state.id;
|
|
|
|
|
|
|
|
|
|
if node_render_state.visited_children {
|
|
|
|
|
// Keep exit nodes in original order
|
|
|
|
|
self.pending_nodes.push(node_render_state);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(element) = tree.get(&node_id) {
|
|
|
|
|
// Fast visibility check without expensive extrect calculation
|
|
|
|
|
if !node_render_state.is_root() {
|
|
|
|
|
if element.hidden {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let selrect = element.selrect();
|
|
|
|
|
if !selrect.intersects(self.render_area) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
visible_shapes.push((node_render_state, node_id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort visible shapes by type and spatial proximity for better cache performance
|
|
|
|
|
visible_shapes.sort_by(|(_a_state, a_id), (_b_state, b_id)| {
|
|
|
|
|
let a_shape = tree.get(a_id);
|
|
|
|
|
let b_shape = tree.get(b_id);
|
|
|
|
|
|
|
|
|
|
match (a_shape, b_shape) {
|
|
|
|
|
(Some(a), Some(b)) => {
|
|
|
|
|
// First sort by shape type (groups similar shapes together)
|
|
|
|
|
let type_a = match &a.shape_type {
|
|
|
|
|
Type::Rect(_) => 0,
|
|
|
|
|
Type::Circle => 1,
|
|
|
|
|
Type::Path(_) => 2,
|
|
|
|
|
Type::Text(_) => 3,
|
|
|
|
|
Type::Frame(_) => 4,
|
|
|
|
|
Type::Group(_) => 5,
|
|
|
|
|
Type::Bool(_) => 6,
|
|
|
|
|
Type::SVGRaw(_) => 7,
|
|
|
|
|
};
|
|
|
|
|
let type_b = match &b.shape_type {
|
|
|
|
|
Type::Rect(_) => 0,
|
|
|
|
|
Type::Circle => 1,
|
|
|
|
|
Type::Path(_) => 2,
|
|
|
|
|
Type::Text(_) => 3,
|
|
|
|
|
Type::Frame(_) => 4,
|
|
|
|
|
Type::Group(_) => 5,
|
|
|
|
|
Type::Bool(_) => 6,
|
|
|
|
|
Type::SVGRaw(_) => 7,
|
|
|
|
|
};
|
|
|
|
|
let type_cmp = type_a.cmp(&type_b);
|
|
|
|
|
if type_cmp != std::cmp::Ordering::Equal {
|
|
|
|
|
return type_cmp;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Then by spatial proximity (Y then X) for better cache locality
|
|
|
|
|
let a_rect = a.selrect();
|
|
|
|
|
let b_rect = b.selrect();
|
|
|
|
|
let y_cmp = a_rect.top.partial_cmp(&b_rect.top).unwrap_or(std::cmp::Ordering::Equal);
|
|
|
|
|
if y_cmp != std::cmp::Ordering::Equal {
|
|
|
|
|
return y_cmp;
|
|
|
|
|
}
|
|
|
|
|
a_rect.left.partial_cmp(&b_rect.left).unwrap_or(std::cmp::Ordering::Equal)
|
|
|
|
|
}
|
|
|
|
|
_ => std::cmp::Ordering::Equal,
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Process sorted visible shapes
|
|
|
|
|
for (node_render_state, _node_id) in visible_shapes {
|
|
|
|
|
self.pending_nodes.push(node_render_state);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while let Some(node_render_state) = self.pending_nodes.pop() {
|
|
|
|
|
let node_id = node_render_state.id;
|
|
|
|
@@ -1451,24 +1691,51 @@ impl RenderState {
|
|
|
|
|
node_render_state.id
|
|
|
|
|
))?;
|
|
|
|
|
|
|
|
|
|
// If the shape is not in the tile set, then we add them.
|
|
|
|
|
// Optimization: Only check/add tiles once per shape, cache the result
|
|
|
|
|
// This avoids repeated hashmap lookups for the same shape
|
|
|
|
|
if self.tiles.get_tiles_of(node_id).is_none() {
|
|
|
|
|
self.add_shape_tiles(element, tree);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if visited_children {
|
|
|
|
|
self.render_shape_exit(element, visited_mask);
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Skip render_shape_exit for flattened containers
|
|
|
|
|
let is_flattened = self.simplified_tree.is_flattened(&node_id);
|
|
|
|
|
if !is_flattened {
|
|
|
|
|
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)
|
|
|
|
|
if transformed_element.hidden {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let selrect = transformed_element.selrect();
|
|
|
|
|
if !selrect.intersects(self.render_area) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 extrect = transformed_element.extrect(tree, scale);
|
|
|
|
|
|
|
|
|
|
let is_visible = extrect.intersects(self.render_area)
|
|
|
|
|
&& !transformed_element.hidden
|
|
|
|
|
&& !transformed_element.visually_insignificant(scale, tree);
|
|
|
|
|
let has_effects = !transformed_element.shadows.is_empty()
|
|
|
|
|
|| transformed_element.blur.is_some()
|
|
|
|
|
|| !transformed_element.strokes.is_empty()
|
|
|
|
|
|| matches!(transformed_element.shape_type, Type::Group(_) | Type::Frame(_));
|
|
|
|
|
|
|
|
|
|
let is_visible = if !has_effects {
|
|
|
|
|
// Simple shape: selrect check is sufficient, skip expensive extrect
|
|
|
|
|
!transformed_element.visually_insignificant(scale, tree)
|
|
|
|
|
} else {
|
|
|
|
|
// Shape with effects: need extrect for accurate bounds
|
|
|
|
|
let extrect = transformed_element.extrect(tree, scale);
|
|
|
|
|
extrect.intersects(self.render_area)
|
|
|
|
|
&& !transformed_element.visually_insignificant(scale, tree)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if self.options.is_debug_visible() {
|
|
|
|
|
let shape_extrect_bounds =
|
|
|
|
@@ -1476,12 +1743,19 @@ impl RenderState {
|
|
|
|
|
debug::render_debug_shape(self, None, Some(shape_extrect_bounds));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !is_visible {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if !is_visible {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: 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
|
|
|
|
|
let is_flattened = self.simplified_tree.is_flattened(&node_id);
|
|
|
|
|
|
|
|
|
|
if !is_flattened {
|
|
|
|
|
self.render_shape_enter(element, mask);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
|
|
|
|
let scale: f32 = self.get_scale();
|
|
|
|
@@ -1515,6 +1789,9 @@ impl RenderState {
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Optimization: Calculate extrect once and reuse it
|
|
|
|
|
let element_extrect = element.extrect(tree, scale);
|
|
|
|
|
|
|
|
|
|
for shadow in element.drop_shadows_visible() {
|
|
|
|
|
let paint = skia::Paint::default();
|
|
|
|
|
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
|
|
|
@@ -1526,7 +1803,7 @@ impl RenderState {
|
|
|
|
|
// First pass: Render shadow in black to establish alpha mask
|
|
|
|
|
self.render_drop_black_shadow(
|
|
|
|
|
element,
|
|
|
|
|
&element.extrect(tree, scale),
|
|
|
|
|
&element_extrect,
|
|
|
|
|
shadow,
|
|
|
|
|
clip_bounds.clone(),
|
|
|
|
|
scale,
|
|
|
|
@@ -1546,9 +1823,11 @@ impl RenderState {
|
|
|
|
|
.get_nested_shadow_clip_bounds(element, shadow);
|
|
|
|
|
|
|
|
|
|
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
|
|
|
|
|
// Optimization: Calculate extrect once per shadow shape
|
|
|
|
|
let shadow_extrect = shadow_shape.extrect(tree, scale);
|
|
|
|
|
self.render_drop_black_shadow(
|
|
|
|
|
shadow_shape,
|
|
|
|
|
&shadow_shape.extrect(tree, scale),
|
|
|
|
|
&shadow_extrect,
|
|
|
|
|
shadow,
|
|
|
|
|
clip_bounds,
|
|
|
|
|
scale,
|
|
|
|
@@ -1663,8 +1942,82 @@ impl RenderState {
|
|
|
|
|
.canvas(SurfaceId::DropShadows)
|
|
|
|
|
.clear(skia::Color::TRANSPARENT);
|
|
|
|
|
|
|
|
|
|
// SIMPLE OPTIMIZATION: If this is a path, check if consecutive siblings
|
|
|
|
|
// are also paths with the same properties and combine them all
|
|
|
|
|
// We check BEFORE rendering to avoid rendering the first path twice
|
|
|
|
|
let shape_to_render = if matches!(element.shape_type, Type::Path(_) | Type::Bool(_)) {
|
|
|
|
|
// Collect all consecutive paths with same properties
|
|
|
|
|
let mut path_ids = vec![element.id];
|
|
|
|
|
let mut path_selrects = vec![element.selrect()];
|
|
|
|
|
|
|
|
|
|
// Keep checking and collecting consecutive compatible paths
|
|
|
|
|
while let Some(next_node) = self.pending_nodes.last() {
|
|
|
|
|
if next_node.visited_children || next_node.is_root() {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let Some(next_element) = tree.get(&next_node.id) {
|
|
|
|
|
// Check if next element is also a path with same properties
|
|
|
|
|
if can_render_paths_together(element, next_element, &clip_bounds, &next_node.clip_bounds) {
|
|
|
|
|
// Remove it from pending_nodes and add to our collection
|
|
|
|
|
let _next_node = self.pending_nodes.pop().unwrap();
|
|
|
|
|
path_ids.push(next_element.id);
|
|
|
|
|
path_selrects.push(next_element.selrect());
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we collected more than one path, combine them all
|
|
|
|
|
if path_ids.len() > 1 {
|
|
|
|
|
// Combine paths by joining their segments (simpler than boolean union)
|
|
|
|
|
// This avoids the complexity and potential errors of boolean operations
|
|
|
|
|
use crate::shapes::{ToPath, Path as ShapePath};
|
|
|
|
|
|
|
|
|
|
let mut combined_path = element.to_path(tree);
|
|
|
|
|
for path_id in path_ids.iter().skip(1) {
|
|
|
|
|
if let Some(path_shape) = tree.get(path_id) {
|
|
|
|
|
let other_path = path_shape.to_path(tree);
|
|
|
|
|
// Simple join: concatenate segments (like join_paths does)
|
|
|
|
|
let mut segments = combined_path.segments().clone();
|
|
|
|
|
segments.extend(other_path.segments().iter().cloned());
|
|
|
|
|
combined_path = ShapePath::new(segments);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create a temporary shape with the combined path
|
|
|
|
|
// Use properties from the first shape (they're the same anyway)
|
|
|
|
|
let mut combined_shape = element.clone();
|
|
|
|
|
combined_shape.shape_type = crate::shapes::Type::Path(combined_path);
|
|
|
|
|
|
|
|
|
|
// Update selrect to encompass all paths
|
|
|
|
|
let mut left = path_selrects[0].left();
|
|
|
|
|
let mut top = path_selrects[0].top();
|
|
|
|
|
let mut right = path_selrects[0].right();
|
|
|
|
|
let mut bottom = path_selrects[0].bottom();
|
|
|
|
|
for selrect in path_selrects.iter().skip(1) {
|
|
|
|
|
left = left.min(selrect.left());
|
|
|
|
|
top = top.min(selrect.top());
|
|
|
|
|
right = right.max(selrect.right());
|
|
|
|
|
bottom = bottom.max(selrect.bottom());
|
|
|
|
|
}
|
|
|
|
|
combined_shape.set_selrect(left, top, right, bottom);
|
|
|
|
|
|
|
|
|
|
Some(combined_shape)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Render the shape (either combined or original)
|
|
|
|
|
let final_shape = shape_to_render.as_ref().unwrap_or(element);
|
|
|
|
|
self.render_shape(
|
|
|
|
|
element,
|
|
|
|
|
final_shape,
|
|
|
|
|
clip_bounds.clone(),
|
|
|
|
|
SurfaceId::Fills,
|
|
|
|
|
SurfaceId::Strokes,
|
|
|
|
@@ -1674,6 +2027,11 @@ impl RenderState {
|
|
|
|
|
None,
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// If we combined paths, skip normal processing of the remaining nodes
|
|
|
|
|
if shape_to_render.is_some() {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.surfaces
|
|
|
|
|
.canvas(SurfaceId::DropShadows)
|
|
|
|
@@ -1682,14 +2040,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);
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Skip nested state updates for flattened containers
|
|
|
|
|
// Flattened containers don't affect children, so we don't need to track their state
|
|
|
|
|
if !is_flattened {
|
|
|
|
|
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
|
|
|
|
@@ -1704,24 +2066,41 @@ 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();
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Use simplified tree to skip flattened containers
|
|
|
|
|
// If this container was flattened, get children from simplified tree
|
|
|
|
|
// Otherwise, use original children
|
|
|
|
|
let children_ids: Vec<_> = if self.simplified_tree.is_flattened(&node_id) {
|
|
|
|
|
// Container was flattened: get simplified children (which skip this level)
|
|
|
|
|
self.simplified_tree
|
|
|
|
|
.get_children(&node_id)
|
|
|
|
|
.map(|ids| ids.iter().copied().collect())
|
|
|
|
|
.unwrap_or_else(|| element.children_ids_iter(false).copied().collect())
|
|
|
|
|
} 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,
|
|
|
|
@@ -1937,6 +2316,10 @@ impl RenderState {
|
|
|
|
|
|
|
|
|
|
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
|
|
|
|
performance::begin_measure!("rebuild_tiles_shallow");
|
|
|
|
|
|
|
|
|
|
// DISRUPTIVE OPTIMIZATION: Invalidate simplified tree when tiles are rebuilt
|
|
|
|
|
// The tree will be rebuilt on the next render loop
|
|
|
|
|
self.simplified_tree.invalidate();
|
|
|
|
|
|
|
|
|
|
let mut all_tiles = HashSet::<tiles::Tile>::new();
|
|
|
|
|
let mut nodes = vec![Uuid::nil()];
|
|
|
|
|