diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 309c96c30d..6839adc96b 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -18,7 +18,6 @@ [app.main.data.helpers :as dsh] [app.main.features :as features] [app.main.worker :as mw] - [app.render-wasm.api :as wasm.api] [app.render-wasm.shape :as wasm.shape] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -113,15 +112,7 @@ (update-in state [:files file-id :data] apply-changes))] (let [objects (dm/get-in state [:files file-id :data :pages-index (:current-page-id state) :objects])] - (run! - (fn [[shape-id props]] - (wasm.api/use-shape shape-id) - (let [shape (get objects shape-id)] - (run! (partial wasm.shape/set-shape-wasm-attr! shape) props))) - @shape-changes)) - - (wasm.api/update-shape-tiles) - (wasm.api/request-render "set-wasm-attrs") + (wasm.shape/process-shape-changes! objects @shape-changes)) state) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 72d4d60e90..b1a2714e31 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -226,22 +226,26 @@ wasm-props (concat clean-props wasm-props) - wasm-props + ;; Stores a map shape -> set of properties changed + ;; this is the standard format used by process-shape-changes + shape-changes (-> (group-by first wasm-props) - (update-vals #(map second %)))] + (update-vals #(into #{} (map (comp :property second)) %))) - ;; Props are grouped by id and then assoc to the shape the new value - (run! (fn [[id properties]] - (let [shape - (->> properties - (reduce - (fn [shape {:keys [property value]}] - (assoc shape property value)) - (get objects id)))] - - ;; With the new values to the shape change multi props - (wasm.shape/set-wasm-multi-attrs! shape (->> properties (map :property))))) - wasm-props))) + ;; Create a new objects only with the temporary modifications + objects-changed + (->> wasm-props + (reduce + (fn [objects [id properties]] + (let [shape + (->> properties + (reduce + (fn [shape {:keys [property value]}] + (assoc shape property value)) + (get objects id)))] + (assoc objects id shape))) + objects))] + (wasm.shape/process-shape-changes! objects-changed shape-changes))) (defn clear-local-transform [] (ptk/reify ::clear-local-transform diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 6cc2791c2f..7e0ffcc7a2 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -6,12 +6,14 @@ (ns app.render-wasm.shape (:require + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.transit :as t] [app.common.types.shape :as shape] [app.common.types.shape.layout :as ctl] [app.main.refs :as refs] [app.render-wasm.api :as api] + [beicon.v2.core :as rx] [cljs.core :as c] [cuerdas.core :as str])) @@ -118,8 +120,11 @@ (-write writer (str "#penpot/shape " (:id delegate))))) ;; --- SHAPE IMPL - -(defn set-shape-wasm-attr! +;; When an attribute is sent to WASM it could still be pending some side operations +;; for example: font loading when changing a text, this is an async operation that will +;; resolve eventually. +;; The `set-wasm-attr!` can return a list of callbacks to be executed in a second pass. +(defn- set-wasm-attr! [shape k] (let [v (get shape k) id (get shape :id)] @@ -224,14 +229,32 @@ (ctl/flex-layout? shape) (api/set-flex-layout shape))) + ;; Property not in WASM nil))) -(defn set-wasm-multi-attrs! +(defn process-shape! [shape properties] (let [shape-id (dm/get-prop shape :id)] - (when (shape-in-current-page? shape-id) - (api/use-shape shape-id) - (run! (partial set-shape-wasm-attr! shape) properties)))) + (if (shape-in-current-page? shape-id) + (do + (api/use-shape shape-id) + (->> properties + (mapcat #(set-wasm-attr! shape %)) + (d/index-by :key :callback) + (vals) + (rx/from) + (rx/mapcat (fn [callback] (callback))) + (rx/reduce conj []))) + (rx/empty)))) + +(defn process-shape-changes! + [objects shape-changes] + (->> (rx/from shape-changes) + (rx/mapcat (fn [[shape-id props]] (process-shape! (get objects shape-id) props))) + (rx/subs! + (fn [_] + (api/update-shape-tiles) + (api/request-render "set-wasm-attrs"))))) (defn- impl-assoc [self k v] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index d80ae3b0ea..4c76ed78a2 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -20,6 +20,7 @@ use mem::SerializableResult; use shapes::{StructureEntry, StructureEntryType, TransformEntry}; use skia_safe as skia; use state::State; +use std::collections::HashMap; use utils::uuid_from_u32_quartet; use uuid::Uuid; @@ -537,6 +538,7 @@ pub extern "C" fn set_structure_modifiers() { .collect(); with_state_mut!(state, { + let mut structure = HashMap::new(); for entry in entries { match entry.entry_type { StructureEntryType::ScaleContent => { @@ -548,15 +550,17 @@ pub extern "C" fn set_structure_modifiers() { } } _ => { - state.structure.entry(entry.parent).or_insert_with(Vec::new); - state - .structure + structure.entry(entry.parent).or_insert_with(Vec::new); + structure .get_mut(&entry.parent) .expect("Parent not found for entry") .push(entry); } } } + if !structure.is_empty() { + state.shapes.set_structure(structure); + } }); mem::free_bytes(); @@ -567,7 +571,8 @@ pub extern "C" fn clean_modifiers() { with_state_mut!(state, { state.structure.clear(); state.scale_content.clear(); - state.modifiers.clear(); + // state.modifiers.clear(); + state.shapes.clean_modifiers(); }); } @@ -595,11 +600,16 @@ pub extern "C" fn set_modifiers() { .map(|data| TransformEntry::from_bytes(data.try_into().unwrap())) .collect(); + let mut modifiers = HashMap::new(); + let mut ids = Vec::::new(); + for entry in entries { + modifiers.insert(entry.id, entry.transform); + ids.push(entry.id); + } + with_state_mut!(state, { - for entry in entries { - state.modifiers.insert(entry.id, entry.transform); - } - state.rebuild_modifier_tiles(); + state.set_modifiers(modifiers); + state.rebuild_modifier_tiles(ids); }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 81b10d0624..21dcc3fa60 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1141,13 +1141,8 @@ impl RenderState { self.get_rect_bounds(rect) } - pub fn get_shape_extrect_bounds( - &mut self, - shape: &Shape, - tree: &ShapesPool, - modifiers: &HashMap, - ) -> Rect { - let rect = shape.extrect(tree, modifiers); + pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: &ShapesPool) -> Rect { + let rect = shape.extrect(tree); self.get_rect_bounds(rect) } @@ -1285,7 +1280,7 @@ impl RenderState { // If the shape is not in the tile set, then we update // it. if self.tiles.get_tiles_of(node_id).is_none() { - self.update_tile_for(element, tree, modifiers); + self.update_tile_for(element, tree); } if visited_children { @@ -1304,18 +1299,14 @@ impl RenderState { let transformed_element: Cow = Cow::Borrowed(element); let is_visible = transformed_element - .extrect(tree, modifiers) + .extrect(tree) .intersects(self.render_area) && !transformed_element.hidden - && !transformed_element.visually_insignificant( - self.get_scale(), - tree, - modifiers, - ); + && !transformed_element.visually_insignificant(self.get_scale(), tree); if self.options.is_debug_visible() { let shape_extrect_bounds = - self.get_shape_extrect_bounds(&transformed_element, tree, modifiers); + self.get_shape_extrect_bounds(&transformed_element, tree); debug::render_debug_shape(self, None, Some(shape_extrect_bounds)); } @@ -1664,23 +1655,14 @@ impl RenderState { Ok(()) } - pub fn get_tiles_for_shape( - &mut self, - shape: &Shape, - tree: &ShapesPool, - modifiers: &HashMap, - ) -> TileRect { + pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: &ShapesPool) -> TileRect { + let extrect = shape.extrect(tree); let tile_size = tiles::get_tile_size(self.get_scale()); - tiles::get_tiles_for_rect(shape.extrect(tree, modifiers), tile_size) + tiles::get_tiles_for_rect(extrect, tile_size) } - pub fn update_tile_for( - &mut self, - shape: &Shape, - tree: &ShapesPool, - modifiers: &HashMap, - ) { - let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree, modifiers); + pub fn update_tile_for(&mut self, shape: &Shape, tree: &ShapesPool) { + let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); let old_tiles: HashSet = self .tiles .get_tiles_of(shape.id) @@ -1724,7 +1706,7 @@ impl RenderState { if let Some(modifier) = modifiers.get(&shape_id) { shape.to_mut().apply_transform(modifier); } - self.update_tile_for(&shape, tree, modifiers); + self.update_tile_for(&shape, tree); } else { // We only need to rebuild tiles from the first level. let children = shape.modified_children_ids(structure.get(&shape.id), false); @@ -1754,7 +1736,7 @@ impl RenderState { if let Some(modifier) = modifiers.get(&shape_id) { shape.to_mut().apply_transform(modifier); } - self.update_tile_for(&shape, tree, modifiers); + self.update_tile_for(&shape, tree); } let children = shape.modified_children_ids(structure.get(&shape.id), false); @@ -1778,15 +1760,11 @@ impl RenderState { &mut self, shape_ids: &IndexSet, tree: &mut ShapesPool, - modifiers: &HashMap, ) { for shape_id in shape_ids { - if let Some(shape) = tree.get_mut(shape_id) { - shape.invalidate_extrect(); - } if let Some(shape) = tree.get(shape_id) { if !shape.id.is_nil() { - self.update_tile_for(shape, tree, modifiers); + self.update_tile_for(shape, tree); } } } @@ -1798,14 +1776,9 @@ impl RenderState { /// Additionally, it processes all ancestors of modified shapes to ensure their /// extended rectangles are properly recalculated and their tiles are updated. /// This is crucial for frames and groups that contain transformed children. - pub fn rebuild_modifier_tiles( - &mut self, - tree: &mut ShapesPool, - modifiers: &HashMap, - ) { - let ids: Vec<_> = modifiers.keys().collect(); + pub fn rebuild_modifier_tiles(&mut self, tree: &mut ShapesPool, ids: Vec) { let ancestors = all_with_ancestors(&ids, tree, false); - self.invalidate_and_update_tiles(&ancestors, tree, modifiers); + self.invalidate_and_update_tiles(&ancestors, tree); } pub fn get_scale(&self) -> f32 { diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index abe050e16e..5dbb8d464a 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -3,7 +3,7 @@ use skia_safe::{self as skia}; use crate::uuid::Uuid; use std::borrow::Cow; use std::cell::OnceCell; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::iter::once; mod blend; @@ -196,11 +196,11 @@ pub struct Shape { // # Returns // A set of ancestor UUIDs in traversal order (closest ancestor first) pub fn all_with_ancestors( - shapes: &[&Uuid], + shapes: &[Uuid], shapes_pool: &ShapesPool, include_hidden: bool, ) -> IndexSet { - let mut pending = Vec::from(shapes); + let mut pending = Vec::from_iter(shapes.iter()); let mut result = IndexSet::new(); while !pending.is_empty() { @@ -677,13 +677,8 @@ impl Shape { self.selrect.width() } - pub fn visually_insignificant( - &self, - scale: f32, - shapes_pool: &ShapesPool, - modifiers: &HashMap, - ) -> bool { - let extrect = self.extrect(shapes_pool, modifiers); + pub fn visually_insignificant(&self, scale: f32, shapes_pool: &ShapesPool) -> bool { + let extrect = self.extrect(shapes_pool); extrect.width() * scale < MIN_VISIBLE_SIZE && extrect.height() * scale < MIN_VISIBLE_SIZE } @@ -726,14 +721,10 @@ impl Shape { self.selrect } - pub fn extrect( - &self, - shapes_pool: &ShapesPool, - modifiers: &HashMap, - ) -> math::Rect { + pub fn extrect(&self, shapes_pool: &ShapesPool) -> math::Rect { *self .extrect - .get_or_init(|| self.calculate_extrect(shapes_pool, modifiers)) + .get_or_init(|| self.calculate_extrect(shapes_pool)) } pub fn get_text_content(&self) -> &TextContent { @@ -843,12 +834,7 @@ impl Shape { rect } - fn apply_children_bounds( - &self, - mut rect: math::Rect, - shapes_pool: &ShapesPool, - modifiers: &HashMap, - ) -> math::Rect { + fn apply_children_bounds(&self, mut rect: math::Rect, shapes_pool: &ShapesPool) -> math::Rect { let include_children = match self.shape_type { Type::Group(_) => true, Type::Frame(_) => !self.clip_content, @@ -858,15 +844,7 @@ impl Shape { if include_children { for child_id in self.children_ids(false) { if let Some(child_shape) = shapes_pool.get(&child_id) { - // Create a copy of the child shape to apply any transformations - let mut transformed_element: Cow = Cow::Borrowed(child_shape); - if let Some(modifier) = modifiers.get(&child_id) { - transformed_element.to_mut().apply_transform(modifier); - } - - // Get the child's extended rectangle and join it with the container's rectangle - let child_extrect = transformed_element.extrect(shapes_pool, modifiers); - rect.join(child_extrect); + rect.join(child_shape.extrect(shapes_pool)); } } } @@ -874,12 +852,8 @@ impl Shape { rect } - pub fn calculate_extrect( - &self, - shapes_pool: &ShapesPool, - modifiers: &HashMap, - ) -> math::Rect { - let shape = self.transformed(modifiers.get(&self.id)); + pub fn calculate_extrect(&self, shapes_pool: &ShapesPool) -> math::Rect { + let shape = self; let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open()); let mut rect = match &shape.shape_type { @@ -902,7 +876,7 @@ impl Shape { rect = self.apply_stroke_bounds(rect, max_stroke); rect = self.apply_shadow_bounds(rect); rect = self.apply_blur_bounds(rect); - rect = self.apply_children_bounds(rect, shapes_pool, modifiers); + rect = self.apply_children_bounds(rect, shapes_pool); rect } @@ -1179,11 +1153,41 @@ impl Shape { } } - pub fn transformed(&self, transform: Option<&Matrix>) -> Self { + pub fn apply_structure(&mut self, structure: &Vec) { + let mut result: Vec = Vec::from_iter(self.children.iter().copied()); + let mut to_remove = HashSet::<&Uuid>::new(); + + for st in structure { + match st.entry_type { + StructureEntryType::AddChild => { + result.insert(result.len() - st.index as usize, st.id); + } + StructureEntryType::RemoveChild => { + to_remove.insert(&st.id); + } + _ => {} + } + } + + self.children = result + .iter() + .filter(|id| !to_remove.contains(id)) + .copied() + .collect(); + } + + pub fn transformed( + &self, + transform: Option<&Matrix>, + structure: Option<&Vec>) -> Self + { let mut shape: Cow = Cow::Borrowed(self); if let Some(transform) = transform { shape.to_mut().apply_transform(transform); } + if let Some(structure) = structure { + shape.to_mut().apply_structure(structure); + } shape.into_owned() } diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index 3996f12e7c..8bbcd59bda 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -186,11 +186,10 @@ impl ToPath for Shape { modifiers: &HashMap, structure: &HashMap>, ) -> Path { - let shape = self.transformed(modifiers.get(&self.id)); - match shape.shape_type { + match &self.shape_type { Type::Frame(ref frame) => { - let children = shape.modified_children_ids(structure.get(&shape.id), true); - let mut result = Path::new(rect_segments(&shape, frame.corners)); + let children = self.modified_children_ids(structure.get(&self.id), true); + let mut result = Path::new(rect_segments(&self, frame.corners)); for id in children { let Some(shape) = shapes.get(&id) else { continue; @@ -201,7 +200,7 @@ impl ToPath for Shape { } Type::Group(_) => { - let children = shape.modified_children_ids(structure.get(&shape.id), true); + let children = self.modified_children_ids(structure.get(&self.id), true); let mut result = Path::default(); for id in children { let Some(shape) = shapes.get(&id) else { @@ -215,13 +214,13 @@ impl ToPath for Shape { Path::new(segments) } - Type::Bool(bool_data) => bool_data.path, + Type::Bool(bool_data) => bool_data.path.clone(), - Type::Rect(ref rect) => Path::new(rect_segments(&shape, rect.corners)), + Type::Rect(ref rect) => Path::new(rect_segments(&self, rect.corners)), - Type::Path(path_data) => path_data, + Type::Path(path_data) => path_data.clone(), - Type::Circle => Path::new(circle_segments(&shape)), + Type::Circle => Path::new(circle_segments(&self)), Type::SVGRaw(_) => Path::default(), @@ -232,7 +231,7 @@ impl ToPath for Shape { result = join_paths(result, Path::from_skia_path(path)); } - Path::new(transform_segments(result.segments().clone(), &shape)) + Path::new(transform_segments(result.segments().clone(), &self)) } } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 62ed73053d..a5d7d62b4c 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -114,8 +114,7 @@ impl State { // We don't really do a self.shapes.remove so that redo/undo keep working if let Some(shape) = self.shapes.get(&id) { let tiles::TileRect(rsx, rsy, rex, rey) = - self.render_state - .get_tiles_for_shape(shape, &self.shapes, &self.modifiers); + self.render_state.get_tiles_for_shape(shape, &self.shapes); for x in rsx..=rex { for y in rsy..=rey { let tile = tiles::Tile(x, y); @@ -159,8 +158,7 @@ impl State { pub fn update_tile_for_shape(&mut self, shape_id: Uuid) { if let Some(shape) = self.shapes.get(&shape_id) { - self.render_state - .update_tile_for(shape, &self.shapes, &self.modifiers); + self.render_state.update_tile_for(shape, &self.shapes); } } @@ -170,7 +168,7 @@ impl State { }; if !shape.id.is_nil() { self.render_state - .update_tile_for(&shape.clone(), &self.shapes, &self.modifiers); + .update_tile_for(&shape.clone(), &self.shapes); } } @@ -184,9 +182,9 @@ impl State { .rebuild_tiles(&self.shapes, &self.modifiers, &self.structure); } - pub fn rebuild_modifier_tiles(&mut self) { + pub fn rebuild_modifier_tiles(&mut self, ids: Vec) { self.render_state - .rebuild_modifier_tiles(&mut self.shapes, &self.modifiers); + .rebuild_modifier_tiles(&mut self.shapes, ids); } pub fn font_collection(&self) -> &FontCollection { @@ -217,4 +215,8 @@ impl State { None } + + pub fn set_modifiers(&mut self, modifiers: HashMap) { + self.shapes.set_modifiers(modifiers); + } } diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index e349594742..452c33fbab 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -5,6 +5,11 @@ use crate::performance; use crate::shapes::Shape; use crate::uuid::Uuid; +use crate::shapes::StructureEntry; +use crate::skia; + +use std::cell::OnceCell; + const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3; /// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. @@ -20,8 +25,13 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3; /// pub struct ShapesPool { shapes: Vec, - shapes_uuid_to_idx: HashMap, counter: usize, + + shapes_uuid_to_idx: HashMap, + + modified_shape_cache: HashMap>, + modifiers: HashMap, + structure: HashMap>, } impl ShapesPool { @@ -30,6 +40,10 @@ impl ShapesPool { shapes: vec![], counter: 0, shapes_uuid_to_idx: HashMap::default(), + + modified_shape_cache: HashMap::default(), + modifiers: HashMap::default(), + structure: HashMap::default(), } } @@ -76,7 +90,22 @@ impl ShapesPool { pub fn get(&self, id: &Uuid) -> Option<&Shape> { let idx = *self.shapes_uuid_to_idx.get(id)?; - Some(&self.shapes[idx]) + if self.modifiers.contains_key(id) || self.structure.contains_key(id) { + if let Some(cell) = self.modified_shape_cache.get(id) { + Some(cell.get_or_init(|| { + let shape = &self.shapes[idx]; + shape.transformed( + self.modifiers.get(id), + self.structure.get(id) + ) + })) + } else { + let shape = &self.shapes[idx]; + Some(shape) + } + } else { + Some(&self.shapes[idx]) + } } #[allow(dead_code)] @@ -87,4 +116,44 @@ impl ShapesPool { pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Shape> { self.shapes.iter_mut() } + + #[allow(dead_code)] + fn clean_shape_cache(&mut self) { + self.modified_shape_cache.clear() + } + + #[allow(dead_code)] + pub fn set_modifiers(&mut self, modifiers: HashMap) { + // self.clean_shape_cache(); + + // Initialize the cache cells because + // later we don't want to have the mutable pointer + for key in modifiers.keys() { + self.modified_shape_cache.insert(*key, OnceCell::new()); + } + self.modifiers = modifiers; + } + + #[allow(dead_code)] + pub fn set_structure(&mut self, structure: HashMap>) { + // self.clean_shape_cache(); + // Initialize the cache cells because + // later we don't want to have the mutable pointer + for key in structure.keys() { + self.modified_shape_cache.insert(*key, OnceCell::new()); + } + self.structure = structure; + } + + #[allow(dead_code)] + pub fn clean_modifiers(&mut self) { + self.clean_shape_cache(); + self.modifiers = HashMap::default(); + } + + #[allow(dead_code)] + pub fn clean_structure(&mut self) { + self.clean_shape_cache(); + self.structure = HashMap::default(); + } }