mirror of
https://github.com/penpot/penpot.git
synced 2025-12-12 06:24:17 +01:00
🐛 Fix extrect calculation for shadows and blurs depending on the scale
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user