🐛 Fix extrect calculation for shadows and blurs depending on the scale

This commit is contained in:
Elena Torro
2025-11-11 12:36:57 +01:00
parent 1b50c13c4d
commit 23baf6d18b
2 changed files with 91 additions and 50 deletions

View File

@@ -1105,7 +1105,8 @@ impl RenderState {
} }
pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect { pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect {
let rect = shape.extrect(tree); let scale = self.get_scale();
let rect = self.get_cached_extrect(shape, tree, scale);
self.get_rect_bounds(rect) self.get_rect_bounds(rect)
} }
@@ -1149,9 +1150,13 @@ impl RenderState {
translation: (f32, f32), translation: (f32, f32),
) { ) {
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow); let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
transformed_shadow.to_mut().offset = (0., 0.); transformed_shadow.to_mut().offset = (0.0, 0.0);
transformed_shadow.to_mut().color = skia::Color::BLACK; transformed_shadow.to_mut().color = skia::Color::BLACK;
transformed_shadow.to_mut().blur = transformed_shadow.blur * scale;
// Scale blur to maintain consistent appearance across zoom levels
// When canvas is scaled down (zoom out), blur should be scaled down too
transformed_shadow.to_mut().blur = shadow.blur * scale;
transformed_shadow.to_mut().spread = shadow.spread * scale;
let mut plain_shape = Cow::Borrowed(shape); let mut plain_shape = Cow::Borrowed(shape);
@@ -1242,13 +1247,12 @@ impl RenderState {
if !node_render_state.is_root() { if !node_render_state.is_root() {
let transformed_element: Cow<Shape> = Cow::Borrowed(element); let transformed_element: Cow<Shape> = Cow::Borrowed(element);
let extrect = element.extrect(tree); let scale = self.get_scale();
// FIXME: we need to find a way to update the extrect properly instead let extrect = transformed_element.extrect(tree, scale);
let bounds = transformed_element.apply_children_blur(extrect, tree);
let is_visible = bounds.intersects(self.render_area) let is_visible = extrect.intersects(self.render_area)
&& !transformed_element.hidden && !transformed_element.hidden
&& !transformed_element.visually_insignificant(self.get_scale(), tree); && !transformed_element.visually_insignificant(scale, tree);
if self.options.is_debug_visible() { if self.options.is_debug_visible() {
let shape_extrect_bounds = let shape_extrect_bounds =
@@ -1339,10 +1343,12 @@ impl RenderState {
.translate(translation); .translate(translation);
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow); let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
// transformed_shadow.to_mut().offset = (0., 0.);
transformed_shadow.to_mut().color = skia::Color::BLACK; transformed_shadow.to_mut().color = skia::Color::BLACK;
transformed_shadow.to_mut().blur = transformed_shadow.to_mut().blur =
transformed_shadow.blur * scale; transformed_shadow.blur * scale;
transformed_shadow.to_mut().spread =
transformed_shadow.spread * scale;
let mut new_shadow_paint = skia::Paint::default(); let mut new_shadow_paint = skia::Paint::default();
new_shadow_paint.set_image_filter( new_shadow_paint.set_image_filter(
@@ -1578,8 +1584,9 @@ impl RenderState {
* Given a shape returns the TileRect with the range of tiles that the shape is in * Given a shape returns the TileRect with the range of tiles that the shape is in
*/ */
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect { pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
let extrect = shape.extrect(tree); let scale = self.get_scale();
let tile_size = tiles::get_tile_size(self.get_scale()); let extrect = self.get_cached_extrect(shape, tree, scale);
let tile_size = tiles::get_tile_size(scale);
tiles::get_tiles_for_rect(extrect, tile_size) tiles::get_tiles_for_rect(extrect, tile_size)
} }
@@ -1770,4 +1777,8 @@ impl RenderState {
pub fn clean_touched(&mut self) { pub fn clean_touched(&mut self) {
self.touched_ids.clear(); self.touched_ids.clear();
} }
pub fn get_cached_extrect(&mut self, shape: &Shape, tree: ShapesPoolRef, scale: f32) -> Rect {
shape.extrect(tree, scale)
}
} }

View File

@@ -2,7 +2,7 @@ use skia_safe::{self as skia};
use crate::uuid::Uuid; use crate::uuid::Uuid;
use std::borrow::Cow; use std::borrow::Cow;
use std::cell::OnceCell; use std::cell::{OnceCell, RefCell};
use std::collections::HashSet; use std::collections::HashSet;
use std::iter::once; use std::iter::once;
@@ -178,8 +178,8 @@ pub struct Shape {
pub svg_attrs: Option<SvgAttrs>, pub svg_attrs: Option<SvgAttrs>,
pub shadows: Vec<Shadow>, pub shadows: Vec<Shadow>,
pub layout_item: Option<LayoutItem>, pub layout_item: Option<LayoutItem>,
pub extrect: OnceCell<math::Rect>,
pub bounds: OnceCell<math::Bounds>, pub bounds: OnceCell<math::Bounds>,
pub extrect_cache: RefCell<Option<(math::Rect, u32)>>,
pub svg_transform: Option<Matrix>, pub svg_transform: Option<Matrix>,
pub ignore_constraints: bool, pub ignore_constraints: bool,
} }
@@ -266,8 +266,8 @@ impl Shape {
svg_attrs: None, svg_attrs: None,
shadows: Vec::with_capacity(1), shadows: Vec::with_capacity(1),
layout_item: None, layout_item: None,
extrect: OnceCell::new(),
bounds: OnceCell::new(), bounds: OnceCell::new(),
extrect_cache: RefCell::new(None),
svg_transform: None, svg_transform: None,
ignore_constraints: false, ignore_constraints: false,
} }
@@ -289,14 +289,14 @@ impl Shape {
.for_each(|i| i.scale_content(value)); .for_each(|i| i.scale_content(value));
} }
pub fn invalidate_extrect(&mut self) {
self.extrect = OnceCell::new();
}
pub fn invalidate_bounds(&mut self) { pub fn invalidate_bounds(&mut self) {
self.bounds = OnceCell::new(); self.bounds = OnceCell::new();
} }
pub fn invalidate_extrect(&mut self) {
*self.extrect_cache.borrow_mut() = None;
}
pub fn set_parent(&mut self, id: Uuid) { pub fn set_parent(&mut self, id: Uuid) {
self.parent_id = Some(id); self.parent_id = Some(id);
} }
@@ -329,8 +329,8 @@ impl Shape {
} }
pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) {
self.invalidate_extrect();
self.invalidate_bounds(); self.invalidate_bounds();
self.invalidate_extrect();
self.selrect.set_ltrb(left, top, right, bottom); self.selrect.set_ltrb(left, top, right, bottom);
if let Type::Text(ref mut text) = self.shape_type { if let Type::Text(ref mut text) = self.shape_type {
text.update_layout(self.selrect); text.update_layout(self.selrect);
@@ -350,10 +350,12 @@ impl Shape {
pub fn set_rotation(&mut self, angle: f32) { pub fn set_rotation(&mut self, angle: f32) {
self.rotation = angle; self.rotation = angle;
self.invalidate_extrect();
} }
pub fn set_transform(&mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) { pub fn set_transform(&mut self, a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) {
self.transform = Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0); self.transform = Matrix::new_all(a, c, e, b, d, f, 0.0, 0.0, 1.0);
self.invalidate_extrect();
} }
pub fn set_opacity(&mut self, opacity: f32) { pub fn set_opacity(&mut self, opacity: f32) {
@@ -621,7 +623,6 @@ impl Shape {
} }
pub fn set_path_segments(&mut self, segments: Vec<Segment>) { pub fn set_path_segments(&mut self, segments: Vec<Segment>) {
self.invalidate_extrect();
let path = Path::new(segments); let path = Path::new(segments);
match &mut self.shape_type { match &mut self.shape_type {
Type::Bool(Bool { bool_type, .. }) => { Type::Bool(Bool { bool_type, .. }) => {
@@ -635,6 +636,8 @@ impl Shape {
} }
_ => {} _ => {}
}; };
self.invalidate_bounds();
self.invalidate_extrect();
} }
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> { pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
@@ -662,6 +665,8 @@ impl Shape {
pub fn set_corners(&mut self, raw_corners: (f32, f32, f32, f32)) { pub fn set_corners(&mut self, raw_corners: (f32, f32, f32, f32)) {
if let Some(corners) = make_corners(raw_corners) { if let Some(corners) = make_corners(raw_corners) {
self.shape_type.set_corners(corners); self.shape_type.set_corners(corners);
self.invalidate_bounds();
self.invalidate_extrect();
} }
} }
@@ -686,8 +691,12 @@ impl Shape {
self.selrect.width() self.selrect.width()
} }
pub fn extrect(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
self.calculate_extrect(shapes_pool, scale)
}
pub fn visually_insignificant(&self, scale: f32, shapes_pool: ShapesPoolRef) -> bool { pub fn visually_insignificant(&self, scale: f32, shapes_pool: ShapesPoolRef) -> bool {
let extrect = self.extrect(shapes_pool); let extrect = self.extrect(shapes_pool, scale);
extrect.width() * scale < MIN_VISIBLE_SIZE && extrect.height() * scale < MIN_VISIBLE_SIZE extrect.width() * scale < MIN_VISIBLE_SIZE && extrect.height() * scale < MIN_VISIBLE_SIZE
} }
@@ -730,12 +739,6 @@ impl Shape {
self.selrect self.selrect
} }
pub fn extrect(&self, shapes_pool: ShapesPoolRef) -> math::Rect {
*self
.extrect
.get_or_init(|| self.calculate_extrect(shapes_pool))
}
pub fn get_text_content(&self) -> &TextContent { pub fn get_text_content(&self) -> &TextContent {
match &self.shape_type { match &self.shape_type {
crate::shapes::Type::Text(text_content) => text_content, crate::shapes::Type::Text(text_content) => text_content,
@@ -781,10 +784,10 @@ impl Shape {
shadow_rect.top += y; shadow_rect.top += y;
shadow_rect.bottom += y; shadow_rect.bottom += y;
shadow_rect.left += shadow.blur; shadow_rect.left -= shadow.blur;
shadow_rect.top += shadow.blur; shadow_rect.top -= shadow.blur;
shadow_rect.right -= shadow.blur; shadow_rect.right += shadow.blur;
shadow_rect.bottom -= shadow.blur; shadow_rect.bottom += shadow.blur;
if let Some(max_stroke) = max_stroke { if let Some(max_stroke) = max_stroke {
shadow_rect.left -= max_stroke; shadow_rect.left -= max_stroke;
@@ -809,20 +812,23 @@ impl Shape {
result result
} }
fn apply_shadow_bounds(&self, mut rect: math::Rect) -> math::Rect { fn apply_shadow_bounds(&self, mut rect: math::Rect, scale: f32) -> math::Rect {
for shadow in self.shadows_visible() { for shadow in self.shadows_visible() {
if !shadow.hidden() { if !shadow.hidden() {
let (x, y) = shadow.offset; let (x, y) = shadow.offset;
let mut shadow_rect = rect; let mut shadow_rect = rect;
shadow_rect.left += x; shadow_rect.left += x;
shadow_rect.right += x; shadow_rect.right += x;
shadow_rect.top += y; shadow_rect.top += y;
shadow_rect.bottom += y; shadow_rect.bottom += y;
shadow_rect.left -= shadow.blur; let safe_margin = 2.0 / scale.max(0.1);
shadow_rect.top -= shadow.blur; let total_expansion = shadow.blur + shadow.spread + safe_margin;
shadow_rect.right += shadow.blur; shadow_rect.left -= total_expansion;
shadow_rect.bottom += shadow.blur; shadow_rect.top -= total_expansion;
shadow_rect.right += total_expansion;
shadow_rect.bottom += total_expansion;
rect.join(shadow_rect); rect.join(shadow_rect);
} }
@@ -830,14 +836,16 @@ impl Shape {
rect rect
} }
fn apply_blur_bounds(&self, mut rect: math::Rect) -> math::Rect { fn apply_blur_bounds(&self, mut rect: math::Rect, scale: f32) -> math::Rect {
let blur = self.blur.as_ref(); let blur = self.blur.as_ref();
if let Some(blur) = blur { if let Some(blur) = blur {
if !blur.hidden { if !blur.hidden {
rect.left -= blur.value; let safe_margin = 1.0 / scale.max(0.1);
rect.top -= blur.value; let scaled_blur = blur.value + safe_margin;
rect.right += blur.value; rect.left -= scaled_blur;
rect.bottom += blur.value; rect.top -= scaled_blur;
rect.right += scaled_blur;
rect.bottom += scaled_blur;
} }
} }
rect rect
@@ -847,6 +855,7 @@ impl Shape {
&self, &self,
mut rect: math::Rect, mut rect: math::Rect,
shapes_pool: ShapesPoolRef, shapes_pool: ShapesPoolRef,
scale: f32,
) -> math::Rect { ) -> math::Rect {
let include_children = match self.shape_type { let include_children = match self.shape_type {
Type::Group(_) => true, Type::Group(_) => true,
@@ -857,7 +866,8 @@ impl Shape {
if include_children { if include_children {
for child_id in self.children_ids_iter(false) { for child_id in self.children_ids_iter(false) {
if let Some(child_shape) = shapes_pool.get(child_id) { if let Some(child_shape) = shapes_pool.get(child_id) {
rect.join(child_shape.extrect(shapes_pool)); let child_extrect = child_shape.calculate_extrect(shapes_pool, scale);
rect.join(child_extrect);
} }
} }
} }
@@ -865,7 +875,12 @@ impl Shape {
rect rect
} }
pub fn apply_children_blur(&self, mut rect: math::Rect, tree: ShapesPoolRef) -> math::Rect { pub fn apply_children_blur(
&self,
mut rect: math::Rect,
tree: ShapesPoolRef,
scale: f32,
) -> math::Rect {
let mut children_blur = 0.0; let mut children_blur = 0.0;
let mut current_parent_id = self.parent_id; let mut current_parent_id = self.parent_id;
@@ -892,7 +907,8 @@ impl Shape {
} }
} }
let blur = children_blur; let safe_margin = 1.0 / scale.max(0.1);
let blur = children_blur + safe_margin;
if blur > 0.0 { if blur > 0.0 {
rect.left -= blur; rect.left -= blur;
@@ -904,7 +920,22 @@ impl Shape {
rect rect
} }
pub fn calculate_extrect(&self, shapes_pool: ShapesPoolRef) -> math::Rect { pub fn calculate_extrect(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
let scale_key = (scale * 1000.0).round() as u32;
if let Some((cached_extrect, cached_scale)) = *self.extrect_cache.borrow() {
if cached_scale == scale_key {
return cached_extrect;
}
}
let extrect = self.calculate_extrect_uncached(shapes_pool, scale);
*self.extrect_cache.borrow_mut() = Some((extrect, scale_key));
extrect
}
fn calculate_extrect_uncached(&self, shapes_pool: ShapesPoolRef, scale: f32) -> math::Rect {
let shape = self; let shape = self;
let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open()); let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open());
@@ -926,10 +957,10 @@ impl Shape {
}; };
rect = self.apply_stroke_bounds(rect, max_stroke); rect = self.apply_stroke_bounds(rect, max_stroke);
rect = self.apply_shadow_bounds(rect); rect = self.apply_shadow_bounds(rect, scale);
rect = self.apply_blur_bounds(rect); rect = self.apply_blur_bounds(rect, scale);
rect = self.apply_children_bounds(rect, shapes_pool); rect = self.apply_children_bounds(rect, shapes_pool, scale);
rect = self.apply_children_blur(rect, shapes_pool); rect = self.apply_children_blur(rect, shapes_pool, scale);
rect rect
} }
@@ -1154,7 +1185,6 @@ impl Shape {
} }
pub fn add_paragraph(&mut self, paragraph: Paragraph) -> Result<(), String> { pub fn add_paragraph(&mut self, paragraph: Paragraph) -> Result<(), String> {
self.invalidate_extrect();
match self.shape_type { match self.shape_type {
Type::Text(ref mut text) => { Type::Text(ref mut text) => {
text.add_paragraph(paragraph); text.add_paragraph(paragraph);