diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 897fb733af..1b384e235d 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -26,7 +26,7 @@ use crate::error::{Error, Result}; use crate::performance; use crate::shapes::{ all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, - Stroke, StrokeKind, Type, + Stroke, StrokeKind, TextContent, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -1229,12 +1229,23 @@ impl RenderState { } } - Type::Text(text_content) => { + Type::Text(stored_text_content) => { self.surfaces.apply_mut(surface_ids, |s| { s.canvas().concat(&matrix); }); - let text_content = text_content.new_bounds(shape.selrect()); + // Skip the paragraph-cloning `new_bounds` when shape size is unchanged. + let selrect = shape.selrect(); + let stored_bounds = stored_text_content.bounds(); + let bounds_match = (stored_bounds.width() - selrect.width()).abs() < 0.01 + && (stored_bounds.height() - selrect.height()).abs() < 0.01; + let rebound_text_content = if bounds_match { + None + } else { + Some(stored_text_content.new_bounds(selrect)) + }; + let text_content: &TextContent = + rebound_text_content.as_ref().unwrap_or(stored_text_content); let count_inner_strokes = shape.count_visible_inner_strokes(); // Erode the main text fill by 1px when there are inner strokes, to avoid a visible seam at the glyph edge. let text_fill_inset = (count_inner_strokes > 0).then(|| 1.0 / self.get_scale()); @@ -1248,7 +1259,7 @@ impl RenderState { .rev() .map(|stroke| { text::stroke_paragraph_builder_group_from_text( - &text_content, + text_content, stroke, &shape.selrect(), None, @@ -1324,7 +1335,7 @@ impl RenderState { .rev() .map(|stroke| { text::stroke_paragraph_builder_group_from_text( - &text_content, + text_content, stroke, &shape.selrect(), Some(true), @@ -1357,7 +1368,7 @@ impl RenderState { &parent_shadows, &blur_filter, &stroke_kinds, - &text_content, + text_content, )?; } } else { @@ -1401,7 +1412,7 @@ impl RenderState { &drop_shadows, &blur_filter, &stroke_kinds, - &text_content, + text_content, )?; // 4. Stroke fills @@ -1453,7 +1464,7 @@ impl RenderState { &inner_shadows, &blur_filter, &stroke_kinds, - &text_content, + text_content, )?; // 6. Fill Inner shadows diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index a122b1d8f6..ec99e7fef0 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1015,6 +1015,7 @@ impl Shape { } pub fn calculate_extrect(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect { + // `scale` is forwarded to children but intentionally NOT part of the cache key. if let Some(cached_extrect) = *self.extrect_cache.borrow() { return cached_extrect; } @@ -1346,8 +1347,10 @@ impl Shape { pub fn get_skia_path(&self) -> Option { if let Some(path) = self.shape_type.path() { let mut skia_path = path.to_skia_path(self.svg_attrs.as_ref()); - if let Some(path_transform) = self.to_path_transform() { - skia_path = skia_path.make_transform(&path_transform); + if !math::identitish(&self.transform) { + if let Some(path_transform) = self.to_path_transform() { + skia_path = skia_path.make_transform(&path_transform); + } } Some(skia_path) } else { @@ -1356,6 +1359,19 @@ impl Shape { } fn transform_selrect(&mut self, transform: &Matrix) { + if math::is_move_only_matrix(transform) { + let tx = transform.translate_x(); + let ty = transform.translate_y(); + // `self.transform` (rotation/scale around center) is unchanged by translation. + self.selrect = math::Rect::from_xywh( + self.selrect.left + tx, + self.selrect.top + ty, + self.selrect.width(), + self.selrect.height(), + ); + return; + } + let mut center = self.selrect.center(); center = transform.map_point(center); @@ -1377,9 +1393,23 @@ impl Shape { pub fn apply_transform(&mut self, transform: &Matrix) { self.transform_selrect(transform); - // TODO: See if we can change this invalidation to a transformation - self.invalidate_extrect(); - self.invalidate_bounds(); + // Outsets (strokes, shadows, blur, children) are translation-invariant, + // so the cached extrect can be shifted instead of invalidated. + if math::is_move_only_matrix(transform) { + let tx = transform.translate_x(); + let ty = transform.translate_y(); + if let Some(rect) = self.extrect_cache.borrow_mut().as_mut() { + *rect = math::Rect::from_xywh( + rect.left + tx, + rect.top + ty, + rect.width(), + rect.height(), + ); + } + } else { + self.invalidate_extrect(); + self.invalidate_bounds(); + } if let shape_type @ (Type::Path(_) | Type::Bool(_)) = &mut self.shape_type { if let Some(path) = shape_type.path_mut() { diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index 5b61b3ee2a..87da3881bf 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -259,15 +259,21 @@ pub fn get_fill_shader(fill: &Fill, bounding_box: &Rect) -> Option } pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint { - let mut combined_shader: Option = None; let mut fills_paint = skia::Paint::default(); if fills.is_empty() { - combined_shader = Some(skia::shaders::color(skia::Color::TRANSPARENT)); - fills_paint.set_shader(combined_shader); + fills_paint.set_color(skia::Color::TRANSPARENT); return fills_paint; } + if fills.len() == 1 { + if let Fill::Solid(SolidColor(color)) = &fills[0] { + fills_paint.set_color(*color); + return fills_paint; + } + } + + let mut combined_shader: Option = None; for fill in fills { let shader = get_fill_shader(fill, &bounding_box); @@ -287,7 +293,7 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint { } } - fills_paint.set_shader(combined_shader.clone()); + fills_paint.set_shader(combined_shader); fills_paint } diff --git a/render-wasm/src/shapes/paths.rs b/render-wasm/src/shapes/paths.rs index ab7dc5acbf..aa14953341 100644 --- a/render-wasm/src/shapes/paths.rs +++ b/render-wasm/src/shapes/paths.rs @@ -236,6 +236,28 @@ impl Path { } pub fn transform(&mut self, mtx: &Matrix) { + if math::is_move_only_matrix(mtx) { + let tx = mtx.translate_x(); + let ty = mtx.translate_y(); + self.segments.iter_mut().for_each(|s| match s { + Segment::MoveTo(p) | Segment::LineTo(p) => { + p.0 += tx; + p.1 += ty; + } + Segment::CurveTo((c1, c2, p)) => { + c1.0 += tx; + c1.1 += ty; + c2.0 += tx; + c2.1 += ty; + p.0 += tx; + p.1 += ty; + } + _ => {} + }); + self.skia_path = self.skia_path.with_offset((tx, ty)); + return; + } + self.segments.iter_mut().for_each(|s| match s { Segment::MoveTo(p) => { let np = mtx.map_point(skia::Point::new(p.0, p.1)); diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index 541747229a..1a99000692 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -1424,11 +1424,14 @@ pub fn calculate_text_layout_data( let mut previous_line_height = text_content.normalized_line_height(); let text_paragraphs = text_content.paragraphs(); - // 1. Calculate paragraph heights + // 1. Build + layout each paragraph once, recording heights as we go. let mut paragraph_heights: Vec = Vec::new(); + let mut built_groups: Vec> = + Vec::with_capacity(paragraph_builder_groups.len()); for paragraph_builder_group in paragraph_builder_groups.iter_mut() { let group_len = paragraph_builder_group.len(); let mut paragraph_offset_y = previous_line_height; + let mut group_paragraphs: Vec = Vec::with_capacity(group_len); for (builder_index, paragraph_builder) in paragraph_builder_group.iter_mut().enumerate() { let mut skia_paragraph = paragraph_builder.build(); skia_paragraph.layout(text_width); @@ -1442,11 +1445,13 @@ pub fn calculate_text_layout_data( if builder_index == 0 { paragraph_heights.push(skia_paragraph.height()); } + group_paragraphs.push(skia_paragraph); } previous_line_height = paragraph_offset_y; + built_groups.push(group_paragraphs); } - // 2. Calculate vertical offset and build paragraphs with positions + // 2. Position each built paragraph using the heights from step 1. let total_text_height: f32 = paragraph_heights.iter().sum(); let vertical_offset = match shape.vertical_align() { VerticalAlign::Center => (selrect_height - total_text_height) / 2.0, @@ -1455,12 +1460,9 @@ pub fn calculate_text_layout_data( }; let mut paragraph_layouts: Vec = Vec::new(); let mut y_accum = base_y + vertical_offset; - for (i, paragraph_builder_group) in paragraph_builder_groups.iter_mut().enumerate() { + for (i, group_paragraphs) in built_groups.into_iter().enumerate() { // For each paragraph in the group (e.g., fill, stroke, etc.) - for paragraph_builder in paragraph_builder_group.iter_mut() { - let mut skia_paragraph = paragraph_builder.build(); - skia_paragraph.layout(text_width); - + for skia_paragraph in group_paragraphs.into_iter() { let spans = if let Some(text_para) = text_paragraphs.get(i) { text_para.children().to_vec() } else {