From 17fefcf0bc1f736eaeb43b22da66195cb787a237 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Oct 2025 15:34:33 +0200 Subject: [PATCH 01/15] :sparkles: Changes WASM serialization mechanism --- common/src/app/common/files/changes.cljc | 3 +- common/src/app/common/types/shape.cljc | 7 +-- frontend/src/app/main/data/changes.cljs | 29 ++++++++- .../app/main/data/workspace/modifiers.cljs | 23 ++++--- .../src/app/main/data/workspace/shapes.cljs | 27 ++++---- .../sidebar/options/menus/measures.cljs | 24 +++----- .../app/main/ui/workspace/viewport_wasm.cljs | 3 +- frontend/src/app/render_wasm/shape.cljs | 61 ++++--------------- 8 files changed, 75 insertions(+), 102 deletions(-) diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index eb196ac848..bfbd8db89f 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -517,8 +517,7 @@ (when verify? (check-changes items)) - (binding [*touched-changes* (volatile! #{}) - cts/*wasm-sync* (not cts/*wasm-sync-override*)] + (binding [*touched-changes* (volatile! #{})] (let [result (reduce #(or (process-change %1 %2) %1) data items) result (reduce process-touched-change result @*touched-changes*)] ;; Validate result shapes (only on the backend) diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index c95bb6e0e5..36ab8ddce9 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -36,12 +36,7 @@ [app.common.uuid :as uuid] [clojure.set :as set])) -(defonce ^:dynamic *wasm-sync* false) - -;; This is a temporary workaround so the changes-builder doesn't generate updates -;; in the WASM model. -(defonce ^:dynamic *wasm-sync-override* false) - +(defonce ^:dynamic *shape-changes* nil) (defonce wasm-enabled? false) (defonce wasm-create-shape (constantly nil)) diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 8e914ec00f..309c96c30d 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -7,14 +7,19 @@ (ns app.main.data.changes (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.files.changes :as cpc] [app.common.logging :as log] [app.common.time :as ct] + [app.common.types.shape :as cts] [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.event :as ev] [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])) @@ -99,7 +104,29 @@ pids (into #{} xf:map-page-id redo-changes)] (reduce #(ctst/update-object-indices %1 %2) fdata pids)))] - (update-in state [:files file-id :data] apply-changes))))) + (if (features/active-feature? state "render-wasm/v1") + ;; Update the wasm model + (let [shape-changes (volatile! {}) + + state + (binding [cts/*shape-changes* shape-changes] + (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") + + state) + + ;; wasm renderer deactivated + (update-in state [:files file-id :data] apply-changes)))))) (defn commit "Create a commit event instance" diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index a25e6b27dd..72d4d60e90 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -19,7 +19,6 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] - [app.common.types.shape :as shape] [app.common.types.shape-tree :as ctst] [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] @@ -232,16 +231,17 @@ (update-vals #(map second %)))] ;; Props are grouped by id and then assoc to the shape the new value - (doseq [[id properties] wasm-props] - (let [shape - (->> properties - (reduce - (fn [shape {:keys [property value]}] - (assoc shape property value)) - (get objects id)))] + (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))))))) + ;; With the new values to the shape change multi props + (wasm.shape/set-wasm-multi-attrs! shape (->> properties (map :property))))) + wasm-props))) (defn clear-local-transform [] (ptk/reify ::clear-local-transform @@ -649,8 +649,7 @@ (let [objects (dsh/lookup-page-objects state) ignore-tree - (binding [shape/*wasm-sync* false] - (calculate-ignore-tree modif-tree objects)) + (calculate-ignore-tree modif-tree objects) options (-> params diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 250ad14ff8..3df0560a32 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -78,20 +78,19 @@ (not-empty)) changes - (binding [cts/*wasm-sync-override* true] - (-> (pcb/empty-changes it page-id) - (pcb/set-save-undo? save-undo?) - (pcb/set-stack-undo? stack-undo?) - (cls/generate-update-shapes ids - update-fn - objects - {:attrs attrs - :changed-sub-attr changed-sub-attr - :ignore-tree ignore-tree - :ignore-touched ignore-touched - :with-objects? with-objects?}) - (cond-> undo-group - (pcb/set-undo-group undo-group)))) + (-> (pcb/empty-changes it page-id) + (pcb/set-save-undo? save-undo?) + (pcb/set-stack-undo? stack-undo?) + (cls/generate-update-shapes ids + update-fn + objects + {:attrs attrs + :changed-sub-attr changed-sub-attr + :ignore-tree ignore-tree + :ignore-touched ignore-touched + :with-objects? with-objects?}) + (cond-> undo-group + (pcb/set-undo-group undo-group))) changes (add-undo-group changes state)] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index b1d36ba01d..03ae048983 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -12,7 +12,6 @@ [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.logic.shapes :as cls] - [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.common.types.token :as tk] [app.main.constants :refer [size-presets]] @@ -295,9 +294,8 @@ (mf/use-fn (mf/deps ids) (fn [value attr] - (binding [cts/*wasm-sync* true] - (st/emit! (udw/trigger-bounding-box-cloaking ids) - (udw/update-dimensions ids attr value))))) + (st/emit! (udw/trigger-bounding-box-cloaking ids) + (udw/update-dimensions ids attr value)))) on-size-change (mf/use-fn @@ -306,16 +304,14 @@ (if (or (string? value) (int? value)) (do (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (binding [cts/*wasm-sync* true] - (run! #(do-size-change value attr) shapes))) + (run! #(do-size-change value attr) shapes)) (do (let [resolved-value (:resolved-value (first value))] (st/emit! (udw/trigger-bounding-box-cloaking ids) (dwta/toggle-token {:token (first value) :attrs #{attr} :shape-ids ids})) - (binding [cts/*wasm-sync* true] - (run! #(do-size-change resolved-value attr) shapes))))))) + (run! #(do-size-change resolved-value attr) shapes)))))) on-proportion-lock-change (mf/use-fn @@ -337,16 +333,14 @@ (if (or (string? value) (int? value)) (do (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (binding [cts/*wasm-sync* true] - (run! #(do-position-change %1 value attr) shapes))) + (run! #(do-position-change %1 value attr) shapes)) (do (let [resolved-value (:resolved-value (first value))] (st/emit! (udw/trigger-bounding-box-cloaking ids) (dwta/toggle-token {:token (first value) :attrs #{attr} :shape-ids ids})) - (binding [cts/*wasm-sync* true] - (run! #(do-position-change %1 resolved-value attr) shapes))))))) + (run! #(do-position-change %1 resolved-value attr) shapes)))))) ;; ROTATION do-rotation-change @@ -362,16 +356,14 @@ (if (or (string? value) (int? value)) (do (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (binding [cts/*wasm-sync* true] - (run! #(do-rotation-change value) shapes))) + (run! #(do-rotation-change value) shapes)) (do (let [resolved-value (:resolved-value (first value))] (st/emit! (udw/trigger-bounding-box-cloaking ids) (dwta/toggle-token {:token (first value) :attrs #{:rotation} :shape-ids ids})) - (binding [cts/*wasm-sync* true] - (run! #(do-rotation-change resolved-value) shapes))))))) + (run! #(do-rotation-change resolved-value) shapes)))))) on-width-change (mf/use-fn (mf/deps on-size-change) #(on-size-change % :width)) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index d1628c2542..8872ce9304 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -113,8 +113,7 @@ objects-modified (mf/with-memo [base-objects wasm-modifiers] - (binding [cts/*wasm-sync* false] - (apply-modifiers-to-selected selected base-objects wasm-modifiers))) + (apply-modifiers-to-selected selected base-objects wasm-modifiers)) selected-shapes (->> selected (into [] (keep (d/getf objects-modified))) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 148096795e..6cc2791c2f 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -6,14 +6,12 @@ (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])) @@ -121,7 +119,7 @@ ;; --- SHAPE IMPL -(defn- set-wasm-single-attr! +(defn set-shape-wasm-attr! [shape k] (let [v (get shape k) id (get shape :id)] @@ -233,51 +231,12 @@ (let [shape-id (dm/get-prop shape :id)] (when (shape-in-current-page? shape-id) (api/use-shape shape-id) - (let [result - (->> properties - (mapcat #(set-wasm-single-attr! shape %))) - pending (-> (d/index-by :key :callback result) vals)] - (if (and pending (seq pending)) - (->> (rx/from pending) - (rx/mapcat (fn [callback] (callback))) - (rx/reduce conj []) - (rx/subs! - (fn [_] - (api/update-shape-tiles) - (api/clear-drawing-cache) - (api/request-render "set-wasm-attrs-pending")))) - (do - (api/update-shape-tiles) - (api/request-render "set-wasm-attrs"))))))) - -(defn set-wasm-attrs! - [shape k v] - (let [shape-id (dm/get-prop shape :id) - old-value (get shape k)] - (when (and (shape-in-current-page? shape-id) - (not (identical? old-value v))) - (let [shape (assoc shape k v)] - (api/use-shape shape-id) - (let [result (set-wasm-single-attr! shape k) - pending (-> (d/index-by :key :callback result) vals)] - (if (and pending (seq pending)) - (->> (rx/from pending) - (rx/mapcat (fn [callback] (callback))) - (rx/reduce conj []) - (rx/subs! - (fn [_] - (api/update-shape-tiles) - (api/clear-drawing-cache) - (api/request-render "set-wasm-attrs-pending")))) - (do - (api/update-shape-tiles) - (api/request-render "set-wasm-attrs")))))))) + (run! (partial set-shape-wasm-attr! shape) properties)))) (defn- impl-assoc [self k v] - (when ^boolean shape/*wasm-sync* - (binding [shape/*wasm-sync* false] - (set-wasm-attrs! self k v))) + (when shape/*shape-changes* + (vswap! shape/*shape-changes* update (:id self) (fnil conj #{}) k)) (case k :id @@ -299,10 +258,14 @@ (defn- impl-dissoc [self k] - (when ^boolean shape/*wasm-sync* - (binding [shape/*wasm-sync* false] - (when (shape-in-current-page? (.-id ^ShapeProxy self)) - (set-wasm-attrs! self k nil)))) + #_(when ^boolean shape/*wasm-sync* + (binding [shape/*wasm-sync* false] + (when (shape-in-current-page? (.-id ^ShapeProxy self)) + (set-wasm-attrs! self k nil)))) + + (when shape/*shape-changes* + (vswap! shape/*shape-changes* update (:id self) (fnil conj #{}) k)) + (case k :id (ShapeProxy. nil From 9d7cac5e73d7c99bb36a75cd702943e2a3876c66 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Oct 2025 15:41:17 +0200 Subject: [PATCH 02/15] :sparkles: Improved performance of children ancestors --- render-wasm/src/render.rs | 19 +++----------- render-wasm/src/shapes.rs | 53 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 66d5dad19e..00c613a54f 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,7 +22,8 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ - Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, StructureEntry, Type, + all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, + StructureEntry, Type, }; use crate::state::ShapesPool; use crate::tiles::{self, PendingTiles, TileRect}; @@ -1819,20 +1820,8 @@ impl RenderState { tree: &mut ShapesPool, modifiers: &HashMap, ) { - let mut ancestors = IndexSet::new(); - for (uuid, matrix) in modifiers { - let mut shape = { - let Some(shape) = tree.get(uuid) else { - panic!("Invalid current shape") - }; - let shape: Cow = Cow::Borrowed(shape); - shape - }; - - shape.to_mut().apply_transform(matrix); - ancestors.insert(*uuid); - ancestors.extend(shape.all_ancestors(tree, false)); - } + let ids: Vec<_> = modifiers.keys().collect(); + let ancestors = all_with_ancestors(&ids, tree, false); self.invalidate_and_update_tiles(&ancestors, tree, modifiers); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 2b22f658fd..33dfb266d4 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -182,6 +182,59 @@ pub struct Shape { pub extrect: OnceCell, } +// Returns all ancestor shapes of this shape, traversing up the parent hierarchy +// +// This function walks up the parent chain starting from this shape's parent, +// collecting all ancestor IDs. It stops when it reaches a nil UUID or when +// an ancestor is hidden (unless include_hidden is true). +// +// # Arguments +// * `shapes` - The shapes pool containing all shapes +// * `include_hidden` - Whether to include hidden ancestors in the result +// +// # Returns +// A set of ancestor UUIDs in traversal order (closest ancestor first) +pub fn all_with_ancestors( + shapes: &[&Uuid], + shapes_pool: &ShapesPool, + include_hidden: bool, +) -> IndexSet { + let mut pending = Vec::from(shapes); + let mut result = IndexSet::new(); + + while !pending.is_empty() { + let Some(current_id) = pending.pop() else { + break; + }; + + result.insert(*current_id); + + let Some(parent_id) = shapes_pool.get(current_id).and_then(|s| s.parent_id) else { + continue; + }; + + if parent_id == Uuid::nil() { + continue; + } + + if result.contains(&parent_id) { + continue; + } + + // Check if the ancestor is hidden + let Some(parent) = shapes_pool.get(&parent_id) else { + continue; + }; + + if !include_hidden && parent.hidden() { + continue; + } + + pending.push(&parent.id); + } + result +} + impl Shape { pub fn new(id: Uuid) -> Self { Self { From e9230b8b545a178c296ba1a602097a386f83cfdd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 20 Oct 2025 17:09:10 +0200 Subject: [PATCH 03/15] :sparkles: Change internal data type for tiles --- render-wasm/src/tiles.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 813386a771..5f9a0dd98b 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -1,6 +1,5 @@ use crate::uuid::Uuid; use crate::view::Viewbox; -use indexmap::IndexSet; use skia_safe as skia; use std::collections::{HashMap, HashSet}; @@ -114,7 +113,7 @@ pub fn get_tile_rect(tile: Tile, scale: f32) -> skia::Rect { // This structure is usseful to keep all the shape uuids by shape id. pub struct TileHashMap { - grid: HashMap>, + grid: HashMap>, index: HashMap>, } @@ -126,13 +125,13 @@ impl TileHashMap { } } - pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&IndexSet> { + pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet> { self.grid.get(&tile) } pub fn remove_shape_at(&mut self, tile: Tile, id: Uuid) { if let Some(shapes) = self.grid.get_mut(&tile) { - shapes.shift_remove(&id); + shapes.remove(&id); } if let Some(tiles) = self.index.get_mut(&id) { From e58b2453b125f66287057c1a5274f9e66debb381 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 21 Oct 2025 09:40:38 +0200 Subject: [PATCH 04/15] :sparkles: Removed method set_selrect_for_current_shape --- render-wasm/src/main.rs | 4 ++-- render-wasm/src/state.rs | 18 ------------------ 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index f2ab215147..d80ae3b0ea 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -253,8 +253,8 @@ pub extern "C" fn set_shape_masked_group(masked: bool) { #[no_mangle] pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) { - with_state_mut!(state, { - state.set_selrect_for_current_shape(left, top, right, bottom); + with_current_shape_mut!(state, |shape: &mut Shape| { + shape.set_selrect(left, top, right, bottom); }); } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 9d641a8626..62ed73053d 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -157,24 +157,6 @@ impl State { } } - /// Sets the selection rectangle for the current shape and processes its ancestors - /// - /// When a shape's selection rectangle changes, all its ancestors need to have their - /// extended rectangles recalculated because the shape's bounds may have changed. - /// This ensures proper rendering of frames and groups containing the modified shape. - // FIXME: PERFORMANCE - pub fn set_selrect_for_current_shape(&mut self, left: f32, top: f32, right: f32, bottom: f32) { - let shape = { - let Some(shape) = self.current_shape_mut() else { - panic!("Invalid current shape") - }; - shape.set_selrect(left, top, right, bottom); - shape.clone() - }; - self.render_state - .process_shape_ancestors(&shape, &mut self.shapes, &self.modifiers); - } - pub fn update_tile_for_shape(&mut self, shape_id: Uuid) { if let Some(shape) = self.shapes.get(&shape_id) { self.render_state From e4b4f1bd0855fbe938ef8070e4d5af7dd8b478cd Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 21 Oct 2025 09:47:08 +0200 Subject: [PATCH 05/15] :sparkles: Removed all_ancestors traversals --- render-wasm/src/render.rs | 17 ---------------- render-wasm/src/shapes.rs | 41 --------------------------------------- 2 files changed, 58 deletions(-) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 00c613a54f..81b10d0624 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1792,23 +1792,6 @@ impl RenderState { } } - /// Processes all ancestors of a shape, invalidating their extended rectangles and updating their tiles - /// - /// When a shape changes, all its ancestors need to have their extended rectangles recalculated - /// because they may contain the changed shape. This function: - /// 1. Computes all ancestors of the shape - /// 2. Invalidates the extrect cache for each ancestor - /// 3. Updates the tiles for each ancestor to ensure proper rendering - pub fn process_shape_ancestors( - &mut self, - shape: &Shape, - tree: &mut ShapesPool, - modifiers: &HashMap, - ) { - let ancestors = shape.all_ancestors(tree, false); - self.invalidate_and_update_tiles(&ancestors, tree, modifiers); - } - /// Rebuilds tiles for shapes with modifiers and processes their ancestors /// /// This function applies transformation modifiers to shapes and updates their tiles. diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 33dfb266d4..79c9b0a93a 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -959,47 +959,6 @@ impl Shape { } } - /// Returns all ancestor shapes of this shape, traversing up the parent hierarchy - /// - /// This function walks up the parent chain starting from this shape's parent, - /// collecting all ancestor IDs. It stops when it reaches a nil UUID or when - /// an ancestor is hidden (unless include_hidden is true). - /// - /// # Arguments - /// * `shapes` - The shapes pool containing all shapes - /// * `include_hidden` - Whether to include hidden ancestors in the result - /// - /// # Returns - /// A set of ancestor UUIDs in traversal order (closest ancestor first) - pub fn all_ancestors(&self, shapes: &ShapesPool, include_hidden: bool) -> IndexSet { - let mut ancestors = IndexSet::new(); - let mut current_id = self.id; - - // Traverse upwards using parent_id - while let Some(parent_id) = shapes.get(¤t_id).and_then(|s| s.parent_id) { - // If the parent_id is the zero UUID, there are no more ancestors - if parent_id == Uuid::nil() { - break; - } - - // Check if the ancestor is hidden - if let Some(parent) = shapes.get(&parent_id) { - if !include_hidden && parent.hidden() { - break; - } - ancestors.insert(parent_id); - current_id = parent_id; - } else { - // FIXME: This should panic! I've removed it temporarily until - // we fix the problems with shapes without parents. - // panic!("Parent can't be found"); - break; - } - } - - ancestors - } - pub fn get_matrix(&self) -> Matrix { let mut matrix = Matrix::new_identity(); matrix.post_translate(self.left_top()); From d4b4d943c6348ff2869ede41e8c3ff9ca5b0314e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 22 Oct 2025 11:30:28 +0200 Subject: [PATCH 06/15] :sparkles: Store bounds inside the shape --- render-wasm/src/shapes.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 79c9b0a93a..654908ceeb 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -180,6 +180,7 @@ pub struct Shape { pub shadows: Vec, pub layout_item: Option, pub extrect: OnceCell, + pub bounds: OnceCell, } // Returns all ancestor shapes of this shape, traversing up the parent hierarchy @@ -260,6 +261,7 @@ impl Shape { shadows: Vec::with_capacity(1), layout_item: None, extrect: OnceCell::new(), + bounds: OnceCell::new(), } } @@ -290,6 +292,10 @@ impl Shape { self.extrect = OnceCell::new(); } + pub fn invalidate_bounds(&mut self) { + self.bounds = OnceCell::new(); + } + pub fn set_parent(&mut self, id: Uuid) { self.parent_id = Some(id); } @@ -319,6 +325,7 @@ impl Shape { pub fn set_selrect(&mut self, left: f32, top: f32, right: f32, bottom: f32) { self.invalidate_extrect(); + self.invalidate_bounds(); self.selrect.set_ltrb(left, top, right, bottom); if let Type::Text(ref mut text) = self.shape_type { text.update_layout(self.selrect); @@ -685,8 +692,7 @@ impl Shape { || self.selrect.height() * scale > ANTIALIAS_THRESHOLD } - // TODO: Maybe store this inside the shape - pub fn bounds(&self) -> Bounds { + pub fn calculate_bounds(&self) -> Bounds { let mut bounds = Bounds::new( Point::new(self.selrect.x(), self.selrect.y()), Point::new(self.selrect.x() + self.selrect.width(), self.selrect.y()), @@ -712,6 +718,11 @@ impl Shape { bounds } + pub fn bounds(&self) -> Bounds { + *self.bounds + .get_or_init(|| self.calculate_bounds()) + } + pub fn selrect(&self) -> math::Rect { self.selrect } @@ -1136,8 +1147,12 @@ impl Shape { } pub fn apply_transform(&mut self, transform: &Matrix) { - self.invalidate_extrect(); self.transform_selrect(transform); + + // We don't need to invalidate this? we can just transform it + 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() { path.transform(transform); From 59e745e9ab3bb0249f59af2876930cb9c923a493 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 22 Oct 2025 11:30:59 +0200 Subject: [PATCH 07/15] :sparkles: Improve performance of group bounds --- render-wasm/src/shapes.rs | 51 +++++++++++++++++++++++++++-- render-wasm/src/shapes/modifiers.rs | 44 +++++++++++++------------ 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 654908ceeb..abe050e16e 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -719,8 +719,7 @@ impl Shape { } pub fn bounds(&self) -> Bounds { - *self.bounds - .get_or_init(|| self.calculate_bounds()) + *self.bounds.get_or_init(|| self.calculate_bounds()) } pub fn selrect(&self) -> math::Rect { @@ -947,6 +946,24 @@ impl Shape { } } + pub fn children_ids_iter(&self, include_hidden: bool) -> Box + '_> { + if include_hidden { + return Box::new(self.children.iter().rev()); + } + + if let Type::Bool(_) = self.shape_type { + Box::new([].iter()) + } else if let Type::Group(group) = self.shape_type { + if group.masked { + Box::new(self.children.iter().rev().take(self.children.len() - 1)) + } else { + Box::new(self.children.iter().rev()) + } + } else { + Box::new(self.children.iter().rev()) + } + } + pub fn all_children( &self, shapes: &ShapesPool, @@ -1259,6 +1276,36 @@ impl Shape { } } + pub fn modified_children_ids_iter<'a>( + &'a self, + structure: Option<&'a Vec>, + include_hidden: bool, + ) -> Box> + 'a> { + if let Some(structure) = structure { + let mut result: Vec> = self + .children_ids_iter(include_hidden) + .map(Cow::Borrowed) + .collect(); + let mut to_remove = HashSet::>::new(); + + for st in structure { + match st.entry_type { + StructureEntryType::AddChild => { + result.insert(result.len() - st.index as usize, Cow::Owned(st.id)); + } + StructureEntryType::RemoveChild => { + to_remove.insert(Cow::Owned(st.id)); + } + _ => {} + } + } + + Box::new(result.into_iter().filter(move |id| !to_remove.contains(id))) + } else { + Box::new(self.children_ids_iter(include_hidden).map(Cow::Borrowed)) + } + } + pub fn drop_shadow_paints(&self) -> Vec { let drop_shadows: Vec<&Shadow> = self.drop_shadows_visible().collect(); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 6dafb1c202..c8411621e1 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -88,7 +88,6 @@ fn propagate_children( result } -// FIXME: PERFORMANCE fn calculate_group_bounds( shape: &Shape, shapes: &ShapesPool, @@ -98,16 +97,14 @@ fn calculate_group_bounds( let shape_bounds = bounds.find(shape); let mut result = Vec::::new(); - let children_ids = shape.modified_children_ids(structure.get(&shape.id), true); - for child_id in children_ids.iter() { - let Some(child) = shapes.get(child_id) else { + for child_id in shape.modified_children_ids_iter(structure.get(&shape.id), true) { + let Some(child) = shapes.get(&child_id) else { continue; }; let child_bounds = bounds.find(child); result.append(&mut child_bounds.points()); } - shape_bounds.with_points(result) } @@ -277,30 +274,32 @@ fn propagate_reflow( let shapes = &state.shapes; let mut reflow_parent = false; + if reflown.contains(&id) { + return; + } + match &shape.shape_type { Type::Frame(Frame { layout: Some(_), .. }) => { - if !reflown.contains(id) { - let mut skip_reflow = false; - if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() { - if let Some(parent_id) = shape.parent_id { - if !reflown.contains(&parent_id) { - // If this is a fill layout but the parent has not been reflown yet - // we wait for the next iteration for reflow - skip_reflow = true; - reflow_parent = true; - } + let mut skip_reflow = false; + if shape.is_layout_horizontal_fill() || shape.is_layout_vertical_fill() { + if let Some(parent_id) = shape.parent_id { + if !reflown.contains(&parent_id) { + // If this is a fill layout but the parent has not been reflown yet + // we wait for the next iteration for reflow + skip_reflow = true; + reflow_parent = true; } } + } - if shape.is_layout_vertical_auto() || shape.is_layout_horizontal_auto() { - reflow_parent = true; - } + if shape.is_layout_vertical_auto() || shape.is_layout_horizontal_auto() { + reflow_parent = true; + } - if !skip_reflow { - layout_reflows.push(*id); - } + if !skip_reflow { + layout_reflows.push(*id); } } Type::Group(Group { masked: true }) => { @@ -310,6 +309,7 @@ fn propagate_reflow( bounds.insert(shape.id, child_bounds); reflow_parent = true; } + reflown.insert(*id); } Type::Group(_) => { if let Some(shape_bounds) = @@ -318,6 +318,7 @@ fn propagate_reflow( bounds.insert(shape.id, shape_bounds); reflow_parent = true; } + reflown.insert(*id); } Type::Bool(_) => { if let Some(shape_bounds) = @@ -326,6 +327,7 @@ fn propagate_reflow( bounds.insert(shape.id, shape_bounds); reflow_parent = true; } + reflown.insert(*id); } _ => { // Other shapes don't have to be reflown From ed4df73e42efd055d40a3173880145df6768af60 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 24 Oct 2025 11:42:53 +0200 Subject: [PATCH 08/15] :sparkles: Changes to modifiers --- frontend/src/app/main/data/changes.cljs | 11 +-- .../app/main/data/workspace/modifiers.cljs | 32 ++++---- frontend/src/app/render_wasm/shape.cljs | 35 ++++++-- render-wasm/src/main.rs | 26 ++++-- render-wasm/src/render.rs | 59 ++++--------- render-wasm/src/shapes.rs | 82 ++++++++++--------- render-wasm/src/shapes/shape_to_path.rs | 19 ++--- render-wasm/src/state.rs | 16 ++-- render-wasm/src/state/shapes_pool.rs | 73 ++++++++++++++++- 9 files changed, 214 insertions(+), 139 deletions(-) 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(); + } } From c66a8f5dc574410d47a31f7cf0c21275b9ea0bfc Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 27 Oct 2025 09:21:15 +0100 Subject: [PATCH 09/15] :sparkles: Improve shapes pool performance --- render-wasm/src/main.rs | 2 +- render-wasm/src/math/bools.rs | 8 +- render-wasm/src/render.rs | 34 ++--- render-wasm/src/render/grid_layout.rs | 4 +- render-wasm/src/render/ui.rs | 4 +- render-wasm/src/shapes.rs | 24 ++-- render-wasm/src/shapes/modifiers.rs | 12 +- .../src/shapes/modifiers/flex_layout.rs | 8 +- .../src/shapes/modifiers/grid_layout.rs | 16 +-- render-wasm/src/shapes/shape_to_path.rs | 6 +- render-wasm/src/state.rs | 20 ++- render-wasm/src/state/shapes_pool.rs | 123 ++++++++++++------ 12 files changed, 159 insertions(+), 102 deletions(-) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 4c76ed78a2..4d03aadf3b 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -24,7 +24,7 @@ use std::collections::HashMap; use utils::uuid_from_u32_quartet; use uuid::Uuid; -pub(crate) static mut STATE: Option> = None; +pub(crate) static mut STATE: Option>> = None; #[macro_export] macro_rules! with_state_mut { diff --git a/render-wasm/src/math/bools.rs b/render-wasm/src/math/bools.rs index 5e43764826..8f3facbe57 100644 --- a/render-wasm/src/math/bools.rs +++ b/render-wasm/src/math/bools.rs @@ -1,7 +1,7 @@ use super::Matrix; use crate::render::{RenderState, SurfaceId}; use crate::shapes::{BoolType, Path, Segment, Shape, StructureEntry, ToPath, Type}; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; use crate::uuid::Uuid; use bezier_rs::{Bezier, BezierHandles, ProjectionOptions, TValue}; use glam::DVec2; @@ -387,7 +387,7 @@ fn beziers_to_segments(beziers: &[(BezierSource, Bezier)]) -> Vec { pub fn bool_from_shapes( bool_type: BoolType, children_ids: &IndexSet, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) -> Path { @@ -424,7 +424,7 @@ pub fn bool_from_shapes( pub fn update_bool_to_path( shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) -> Shape { @@ -449,7 +449,7 @@ pub fn update_bool_to_path( pub fn debug_render_bool_paths( render_state: &mut RenderState, shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 21dcc3fa60..8b5cf55c64 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -25,7 +25,7 @@ use crate::shapes::{ all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, StructureEntry, Type, }; -use crate::state::ShapesPool; +use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; use crate::uuid::Uuid; use crate::view::Viewbox; @@ -277,7 +277,7 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { fn is_modified_child( shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, ) -> bool { if modifiers.is_empty() { @@ -476,7 +476,7 @@ impl RenderState { #[allow(clippy::too_many_arguments)] pub fn render_shape( &mut self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, shape: &Shape, @@ -839,7 +839,7 @@ impl RenderState { pub fn render_from_cache( &mut self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { @@ -884,7 +884,7 @@ impl RenderState { pub fn start_render_loop( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, scale_content: &HashMap, @@ -944,7 +944,7 @@ impl RenderState { pub fn process_animation_frame( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, scale_content: &HashMap, @@ -1031,7 +1031,7 @@ impl RenderState { #[inline] pub fn render_shape_exit( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, element: &Shape, @@ -1141,7 +1141,7 @@ impl RenderState { self.get_rect_bounds(rect) } - pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: &ShapesPool) -> Rect { + pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect { let rect = shape.extrect(tree); self.get_rect_bounds(rect) } @@ -1179,7 +1179,7 @@ impl RenderState { #[allow(clippy::too_many_arguments)] fn render_drop_black_shadow( &mut self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, shape: &Shape, @@ -1253,7 +1253,7 @@ impl RenderState { pub fn render_shape_tree_partial_uncached( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, scale_content: &HashMap, @@ -1537,7 +1537,7 @@ impl RenderState { pub fn render_shape_tree_partial( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, scale_content: &HashMap, @@ -1655,13 +1655,13 @@ impl RenderState { Ok(()) } - pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: &ShapesPool) -> TileRect { + pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect { let extrect = shape.extrect(tree); let tile_size = tiles::get_tile_size(self.get_scale()); tiles::get_tiles_for_rect(extrect, tile_size) } - pub fn update_tile_for(&mut self, shape: &Shape, tree: &ShapesPool) { + pub fn update_tile_for(&mut self, shape: &Shape, tree: ShapesPoolRef) { let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree); let old_tiles: HashSet = self .tiles @@ -1691,7 +1691,7 @@ impl RenderState { pub fn rebuild_tiles_shallow( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { @@ -1721,7 +1721,7 @@ impl RenderState { pub fn rebuild_tiles( &mut self, - tree: &ShapesPool, + tree: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { @@ -1759,7 +1759,7 @@ impl RenderState { pub fn invalidate_and_update_tiles( &mut self, shape_ids: &IndexSet, - tree: &mut ShapesPool, + tree: ShapesPoolMutRef<'_>, ) { for shape_id in shape_ids { if let Some(shape) = tree.get(shape_id) { @@ -1776,7 +1776,7 @@ 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, ids: Vec) { + pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec) { let ancestors = all_with_ancestors(&ids, tree, false); self.invalidate_and_update_tiles(&ancestors, tree); } diff --git a/render-wasm/src/render/grid_layout.rs b/render-wasm/src/render/grid_layout.rs index 2424d1acca..635cff137d 100644 --- a/render-wasm/src/render/grid_layout.rs +++ b/render-wasm/src/render/grid_layout.rs @@ -4,14 +4,14 @@ use std::collections::HashMap; use crate::math::{Matrix, Rect}; use crate::shapes::modifiers::grid_layout::grid_cell_data; use crate::shapes::{Shape, StructureEntry}; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; use crate::uuid::Uuid; pub fn render_overlay( zoom: f32, canvas: &skia::Canvas, shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs index 40420f685e..6b37703fdc 100644 --- a/render-wasm/src/render/ui.rs +++ b/render-wasm/src/render/ui.rs @@ -1,7 +1,7 @@ use skia_safe::{self as skia, Color4f}; use std::collections::HashMap; -use super::{RenderState, ShapesPool, SurfaceId}; +use super::{RenderState, ShapesPoolRef, SurfaceId}; use crate::math::Matrix; use crate::render::grid_layout; use crate::shapes::StructureEntry; @@ -9,7 +9,7 @@ use crate::uuid::Uuid; pub fn render( render_state: &mut RenderState, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) { diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 5dbb8d464a..dcfcdf48bc 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -50,7 +50,7 @@ pub use transform::*; use crate::math::{self, Bounds, Matrix, Point}; use indexmap::IndexSet; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; const MIN_VISIBLE_SIZE: f32 = 2.0; const ANTIALIAS_THRESHOLD: f32 = 15.0; @@ -197,7 +197,7 @@ pub struct Shape { // A set of ancestor UUIDs in traversal order (closest ancestor first) pub fn all_with_ancestors( shapes: &[Uuid], - shapes_pool: &ShapesPool, + shapes_pool: ShapesPoolRef, include_hidden: bool, ) -> IndexSet { let mut pending = Vec::from_iter(shapes.iter()); @@ -677,7 +677,7 @@ impl Shape { self.selrect.width() } - pub fn visually_insignificant(&self, scale: f32, shapes_pool: &ShapesPool) -> bool { + pub fn visually_insignificant(&self, scale: f32, shapes_pool: ShapesPoolRef) -> bool { let extrect = self.extrect(shapes_pool); extrect.width() * scale < MIN_VISIBLE_SIZE && extrect.height() * scale < MIN_VISIBLE_SIZE } @@ -721,7 +721,7 @@ impl Shape { self.selrect } - pub fn extrect(&self, shapes_pool: &ShapesPool) -> math::Rect { + pub fn extrect(&self, shapes_pool: ShapesPoolRef) -> math::Rect { *self .extrect .get_or_init(|| self.calculate_extrect(shapes_pool)) @@ -834,7 +834,11 @@ impl Shape { rect } - fn apply_children_bounds(&self, mut rect: math::Rect, shapes_pool: &ShapesPool) -> math::Rect { + fn apply_children_bounds( + &self, + mut rect: math::Rect, + shapes_pool: ShapesPoolRef, + ) -> math::Rect { let include_children = match self.shape_type { Type::Group(_) => true, Type::Frame(_) => !self.clip_content, @@ -852,7 +856,7 @@ impl Shape { rect } - pub fn calculate_extrect(&self, shapes_pool: &ShapesPool) -> math::Rect { + pub fn calculate_extrect(&self, shapes_pool: ShapesPoolRef) -> math::Rect { let shape = self; let max_stroke = Stroke::max_bounds_width(shape.strokes.iter(), shape.is_open()); @@ -940,7 +944,7 @@ impl Shape { pub fn all_children( &self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, include_hidden: bool, include_self: bool, ) -> IndexSet { @@ -968,7 +972,7 @@ impl Shape { matrix } - pub fn get_concatenated_matrix(&self, shapes: &ShapesPool) -> Matrix { + pub fn get_concatenated_matrix(&self, shapes: ShapesPoolRef) -> Matrix { let mut matrix = Matrix::new_identity(); let mut current_id = self.id; while let Some(parent_id) = shapes.get(¤t_id).and_then(|s| s.parent_id) { @@ -1179,8 +1183,8 @@ impl Shape { pub fn transformed( &self, transform: Option<&Matrix>, - structure: Option<&Vec>) -> Self - { + structure: Option<&Vec>, + ) -> Self { let mut shape: Cow = Cow::Borrowed(self); if let Some(transform) = transform { shape.to_mut().apply_transform(transform); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index c8411621e1..b655342cbc 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -13,13 +13,13 @@ use crate::shapes::{ ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, StructureEntry, TransformEntry, Type, }; -use crate::state::{ShapesPool, State}; +use crate::state::{ShapesPoolRef, State}; use crate::uuid::Uuid; #[allow(clippy::too_many_arguments)] fn propagate_children( shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, parent_bounds_before: &Bounds, parent_bounds_after: &Bounds, transform: Matrix, @@ -90,7 +90,7 @@ fn propagate_children( fn calculate_group_bounds( shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, structure: &HashMap>, ) -> Option { @@ -110,7 +110,7 @@ fn calculate_group_bounds( fn calculate_bool_bounds( shape: &Shape, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, modifiers: &HashMap, structure: &HashMap>, @@ -464,7 +464,7 @@ mod tests { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPool::new(); + let mut shapes = ShapesPoolRef::new(); shapes.initialize(10); let child_id = Uuid::new_v4(); @@ -507,7 +507,7 @@ mod tests { fn test_group_bounds() { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPool::new(); + let mut shapes = ShapesPoolRef::new(); shapes.initialize(10); let child1_id = Uuid::new_v4(); diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index af76d5abc6..dd89fde069 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -4,7 +4,7 @@ use crate::shapes::{ AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem, Modifier, Shape, StructureEntry, }; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; use crate::uuid::Uuid; use std::collections::{HashMap, VecDeque}; @@ -179,7 +179,7 @@ fn initialize_tracks( layout_bounds: &Bounds, layout_axis: &LayoutAxis, flex_data: &FlexData, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, structure: &HashMap>, ) -> Vec { @@ -433,7 +433,7 @@ fn calculate_track_data( layout_data: &LayoutData, flex_data: &FlexData, layout_bounds: &Bounds, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, structure: &HashMap>, ) -> Vec { @@ -574,7 +574,7 @@ pub fn reflow_flex_layout( shape: &Shape, layout_data: &LayoutData, flex_data: &FlexData, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &mut HashMap, structure: &HashMap>, ) -> VecDeque { diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 8e5a1c2de1..96f017fe79 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -4,7 +4,7 @@ use crate::shapes::{ JustifyContent, JustifyItems, JustifySelf, Layout, LayoutData, LayoutItem, Modifier, Shape, StructureEntry, Type, }; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; use crate::uuid::Uuid; use indexmap::IndexSet; use std::collections::{HashMap, VecDeque}; @@ -45,7 +45,7 @@ pub fn calculate_tracks( grid_data: &GridData, layout_bounds: &Bounds, cells: &Vec, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, ) -> Vec { let layout_size = if is_column { @@ -122,7 +122,7 @@ fn set_auto_base_size( column: bool, tracks: &mut [TrackData], cells: &Vec, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, ) { for cell in cells { @@ -173,7 +173,7 @@ fn set_auto_multi_span( column: bool, tracks: &mut [TrackData], cells: &[GridCell], - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, ) { // Remove groups with flex (will be set in flex_multi_span) @@ -248,7 +248,7 @@ fn set_flex_multi_span( layout_data: &LayoutData, tracks: &mut [TrackData], cells: &[GridCell], - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &HashMap, ) { // Remove groups without flex @@ -539,7 +539,7 @@ fn cell_bounds( pub fn create_cell_data<'a>( layout_bounds: &Bounds, children: &IndexSet, - shapes: &'a ShapesPool, + shapes: ShapesPoolRef<'a>, cells: &Vec, column_tracks: &[TrackData], row_tracks: &[TrackData], @@ -602,7 +602,7 @@ pub fn create_cell_data<'a>( pub fn grid_cell_data<'a>( shape: &Shape, - shapes: &'a ShapesPool, + shapes: ShapesPoolRef<'a>, modifiers: &HashMap, structure: &HashMap>, allow_empty: bool, @@ -723,7 +723,7 @@ pub fn reflow_grid_layout( shape: &Shape, layout_data: &LayoutData, grid_data: &GridData, - shapes: &ShapesPool, + shapes: ShapesPoolRef, bounds: &mut HashMap, structure: &HashMap>, ) -> VecDeque { diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index 8bbcd59bda..a798665abf 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -4,7 +4,7 @@ use super::{Corners, Path, Segment, Shape, StructureEntry, Type}; use crate::math; use crate::shapes::text_paths::TextPaths; -use crate::state::ShapesPool; +use crate::state::ShapesPoolRef; use crate::uuid::Uuid; use std::collections::HashMap; @@ -13,7 +13,7 @@ const BEZIER_CIRCLE_C: f32 = 0.551_915_05; pub trait ToPath { fn to_path( &self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) -> Path; @@ -182,7 +182,7 @@ fn transform_segments(segments: Vec, shape: &Shape) -> Vec { impl ToPath for Shape { fn to_path( &self, - shapes: &ShapesPool, + shapes: ShapesPoolRef, modifiers: &HashMap, structure: &HashMap>, ) -> Path { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index a5d7d62b4c..0d82585233 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; mod shapes_pool; mod text_editor; -pub use shapes_pool::*; +pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef}; pub use text_editor::*; use crate::render::RenderState; @@ -19,17 +19,17 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data; /// It is created by [init] and passed to the other exported functions. /// Note that rust-skia data structures are not thread safe, so a state /// must not be shared between different Web Workers. -pub(crate) struct State { +pub(crate) struct State<'a> { pub render_state: RenderState, pub text_editor_state: TextEditorState, pub current_id: Option, - pub shapes: ShapesPool, + pub shapes: ShapesPool<'a>, pub modifiers: HashMap, pub scale_content: HashMap, pub structure: HashMap>, } -impl State { +impl<'a> State<'a> { pub fn new(width: i32, height: i32) -> Self { State { render_state: RenderState::new(width, height), @@ -183,8 +183,16 @@ impl State { } pub fn rebuild_modifier_tiles(&mut self, ids: Vec) { - self.render_state - .rebuild_modifier_tiles(&mut self.shapes, ids); + // SAFETY: We're extending the lifetime of the mutable borrow to 'a. + // This is safe because: + // 1. shapes has lifetime 'a in the struct + // 2. The reference won't outlive the struct + // 3. No other references to shapes exist during this call + unsafe { + let shapes_ptr = &mut self.shapes as *mut ShapesPool<'a>; + self.render_state + .rebuild_modifier_tiles(&mut *shapes_ptr, ids); + } } pub fn font_collection(&self) -> &FontCollection { diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 452c33fbab..31d8d83942 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -14,7 +14,7 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3; /// A pool allocator for `Shape` objects that attempts to minimize memory reallocations. /// -/// `ShapesPool` pre-allocates a contiguous vector of `Shape` instances, +/// `ShapesPoolImpl` pre-allocates a contiguous vector of `Shape` instances, /// which can be reused and indexed efficiently. This design helps avoid /// memory reallocation overhead by reserving enough space in advance. /// @@ -23,20 +23,25 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3; /// Shapes are stored in a `Vec`, which keeps the `Shape` instances /// in a contiguous memory block. /// -pub struct ShapesPool { +pub struct ShapesPoolImpl<'a> { shapes: Vec, counter: usize, - shapes_uuid_to_idx: HashMap, + shapes_uuid_to_idx: HashMap<&'a Uuid, usize>, - modified_shape_cache: HashMap>, - modifiers: HashMap, - structure: HashMap>, + modified_shape_cache: HashMap<&'a Uuid, OnceCell>, + modifiers: HashMap<&'a Uuid, skia::Matrix>, + structure: HashMap<&'a Uuid, Vec>, } -impl ShapesPool { +// Type aliases to avoid writing lifetimes everywhere +pub type ShapesPool<'a> = ShapesPoolImpl<'a>; +pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl<'a>; +pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl<'a>; + +impl<'a> ShapesPoolImpl<'a> { pub fn new() -> Self { - ShapesPool { + ShapesPoolImpl { shapes: vec![], counter: 0, shapes_uuid_to_idx: HashMap::default(), @@ -68,11 +73,19 @@ impl ShapesPool { self.shapes .extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional)); } - let new_shape = &mut self.shapes[self.counter]; + let idx = self.counter; + let new_shape = &mut self.shapes[idx]; new_shape.id = id; - self.shapes_uuid_to_idx.insert(id, self.counter); + + // Get a reference to the id field in the shape + // SAFETY: We need to get a reference with lifetime 'a from the shape's id. + // This is safe because the shapes Vec is stable and won't be reallocated + // (we pre-allocate), and the id field won't move within the Shape. + let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) }; + + self.shapes_uuid_to_idx.insert(id_ref, idx); self.counter += 1; - new_shape + &mut self.shapes[idx] } pub fn len(&self) -> usize { @@ -80,31 +93,47 @@ impl ShapesPool { } pub fn has(&self, id: &Uuid) -> bool { - self.shapes_uuid_to_idx.contains_key(id) + self.shapes_uuid_to_idx.contains_key(&id) } pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut Shape> { - let idx = *self.shapes_uuid_to_idx.get(id)?; + let idx = *self.shapes_uuid_to_idx.get(&id)?; Some(&mut self.shapes[idx]) } - pub fn get(&self, id: &Uuid) -> Option<&Shape> { - let idx = *self.shapes_uuid_to_idx.get(id)?; - 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) - ) - })) + pub fn get(&self, id: &Uuid) -> Option<&'a Shape> { + let idx = *self.shapes_uuid_to_idx.get(&id)?; + + // SAFETY: We're extending the lifetimes to 'a. + // This is safe because: + // 1. All internal HashMaps and the shapes Vec have fields with lifetime 'a + // 2. The shape at idx won't be moved or reallocated (pre-allocated Vec) + // 3. The id is stored in shapes[idx].id which has lifetime 'a + // 4. The references won't outlive the ShapesPoolImpl + unsafe { + let shape_ptr = &self.shapes[idx] as *const Shape; + let modifiers_ptr = &self.modifiers as *const HashMap<&'a Uuid, skia::Matrix>; + let structure_ptr = &self.structure as *const HashMap<&'a Uuid, Vec>; + let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell>; + + // Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id + let id_ref: &'a Uuid = &*(id as *const Uuid); + + if (*modifiers_ptr).contains_key(&id_ref) || (*structure_ptr).contains_key(&id_ref) { + if let Some(cell) = (*cache_ptr).get(&id_ref) { + Some(cell.get_or_init(|| { + let shape = &*shape_ptr; + shape.transformed( + (*modifiers_ptr).get(&id_ref), + (*structure_ptr).get(&id_ref), + ) + })) + } else { + Some(&*shape_ptr) + } } else { - let shape = &self.shapes[idx]; - Some(shape) + Some(&*shape_ptr) } - } else { - Some(&self.shapes[idx]) } } @@ -126,23 +155,30 @@ impl ShapesPool { 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()); + // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and + // Initialize the cache cells because later we don't want to have the mutable pointer + let mut modifiers_with_refs = HashMap::with_capacity(modifiers.len()); + for (uuid, matrix) in modifiers { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + modifiers_with_refs.insert(uuid_ref, matrix); + } } - self.modifiers = modifiers; + self.modifiers = modifiers_with_refs; } #[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()); + // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and + // Initialize the cache cells because later we don't want to have the mutable pointer + let mut structure_with_refs = HashMap::with_capacity(structure.len()); + for (uuid, entries) in structure { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + structure_with_refs.insert(uuid_ref, entries); + } } - self.structure = structure; + self.structure = structure_with_refs; } #[allow(dead_code)] @@ -156,4 +192,13 @@ impl ShapesPool { self.clean_shape_cache(); self.structure = HashMap::default(); } + + /// Get a reference to the Uuid stored in a shape, if it exists + pub fn get_uuid_ref(&self, id: &Uuid) -> Option<&'a Uuid> { + let idx = *self.shapes_uuid_to_idx.get(&id)?; + // SAFETY: We're returning a reference with lifetime 'a to a Uuid stored + // in the shapes Vec. This is safe because the Vec is stable (pre-allocated) + // and won't be reallocated. + unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) } + } } From f6eb4923295e2e52abb47e3cb72614e57f5af7eb Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Mon, 27 Oct 2025 16:24:22 +0100 Subject: [PATCH 10/15] :bug: Fig shapes pool extending size --- render-wasm/src/state/shapes_pool.rs | 101 +++++++++++++++++++++++++-- 1 file changed, 95 insertions(+), 6 deletions(-) diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 31d8d83942..6ecd50b274 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -62,32 +62,121 @@ impl<'a> ShapesPoolImpl<'a> { return; } + // Reserve exact capacity to avoid any future reallocations + // This is critical because we store &'a Uuid references that would be invalidated + let target_capacity = (capacity as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize; + self.shapes + .reserve_exact(target_capacity.saturating_sub(self.shapes.len())); + self.shapes .extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional as usize)); performance::end_measure!("shapes_pool_initialize"); } pub fn add_shape(&mut self, id: Uuid) -> &mut Shape { - if self.counter >= self.shapes.len() { + let did_reallocate = if self.counter >= self.shapes.len() { + // We need more space. Check if we'll need to reallocate the Vec. + let current_capacity = self.shapes.capacity(); let additional = (self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize; + let needed_capacity = self.shapes.len() + additional; + + let will_reallocate = needed_capacity > current_capacity; + + if will_reallocate { + // Reserve extra space to minimize future reallocations + let extra_reserve = (needed_capacity as f32 * 0.5) as usize; + self.shapes + .reserve(needed_capacity + extra_reserve - current_capacity); + } + self.shapes .extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional)); - } + + will_reallocate + } else { + false + }; + let idx = self.counter; let new_shape = &mut self.shapes[idx]; new_shape.id = id; - // Get a reference to the id field in the shape - // SAFETY: We need to get a reference with lifetime 'a from the shape's id. - // This is safe because the shapes Vec is stable and won't be reallocated - // (we pre-allocate), and the id field won't move within the Shape. + // Get a reference to the id field in the shape with lifetime 'a + // SAFETY: This is safe because: + // 1. We pre-allocate enough capacity to avoid Vec reallocation + // 2. The shape and its id field won't move within the Vec + // 3. The reference won't outlive the ShapesPoolImpl let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) }; self.shapes_uuid_to_idx.insert(id_ref, idx); self.counter += 1; + + // If the Vec reallocated, we need to rebuild all references in the HashMaps + // because the old references point to deallocated memory + if did_reallocate { + self.rebuild_references(); + } + &mut self.shapes[idx] } + /// Rebuilds all &'a Uuid references in the HashMaps after a Vec reallocation. + /// This is necessary because Vec reallocation invalidates all existing references. + fn rebuild_references(&mut self) { + // Rebuild shapes_uuid_to_idx with fresh references + let mut new_map = HashMap::with_capacity(self.shapes_uuid_to_idx.len()); + for (_, idx) in self.shapes_uuid_to_idx.drain() { + let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) }; + new_map.insert(id_ref, idx); + } + self.shapes_uuid_to_idx = new_map; + + // Rebuild modifiers with fresh references + if !self.modifiers.is_empty() { + let old_modifiers: Vec<(Uuid, skia::Matrix)> = self + .modifiers + .drain() + .map(|(uuid_ref, matrix)| (*uuid_ref, matrix)) + .collect(); + + for (uuid, matrix) in old_modifiers { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modifiers.insert(uuid_ref, matrix); + } + } + } + + // Rebuild structure with fresh references + if !self.structure.is_empty() { + let old_structure: Vec<(Uuid, Vec)> = self + .structure + .drain() + .map(|(uuid_ref, entries)| (*uuid_ref, entries)) + .collect(); + + for (uuid, entries) in old_structure { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.structure.insert(uuid_ref, entries); + } + } + } + + // Rebuild modified_shape_cache with fresh references + if !self.modified_shape_cache.is_empty() { + let old_cache: Vec<(Uuid, OnceCell)> = self + .modified_shape_cache + .drain() + .map(|(uuid_ref, cell)| (*uuid_ref, cell)) + .collect(); + + for (uuid, cell) in old_cache { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, cell); + } + } + } + } + pub fn len(&self) -> usize { self.shapes_uuid_to_idx.len() } From dbf9bdceb50a6bf5a4d41997c1234d8a1a9e27e2 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 28 Oct 2025 17:11:02 +0100 Subject: [PATCH 11/15] :sparkles: Removed modifiers from code --- .../app/main/data/workspace/modifiers.cljs | 19 +- .../app/main/data/workspace/transforms.cljs | 4 +- frontend/src/app/render_wasm/api.cljs | 29 ++- render-wasm/src/main.rs | 16 +- render-wasm/src/math.rs | 1 + render-wasm/src/math/bools.rs | 39 ++-- render-wasm/src/render.rs | 195 +++--------------- render-wasm/src/render/grid_layout.rs | 17 +- render-wasm/src/render/ui.rs | 13 +- render-wasm/src/shapes.rs | 79 +------ render-wasm/src/shapes/modifiers.rs | 60 ++---- .../src/shapes/modifiers/flex_layout.rs | 18 +- .../src/shapes/modifiers/grid_layout.rs | 27 +-- render-wasm/src/shapes/shape_to_path.rs | 28 +-- render-wasm/src/state.rs | 37 +--- render-wasm/src/state/shapes_pool.rs | 60 +++++- render-wasm/src/wasm/paths.rs | 2 +- render-wasm/src/wasm/paths/bools.rs | 8 +- 18 files changed, 194 insertions(+), 458 deletions(-) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index b1a2714e31..4e175d0009 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -19,6 +19,7 @@ [app.common.types.component :as ctk] [app.common.types.container :as ctn] [app.common.types.modifiers :as ctm] + [app.common.types.path :as path] [app.common.types.shape-tree :as ctst] [app.common.types.shape.attrs :refer [editable-attrs]] [app.common.types.shape.layout :as ctl] @@ -669,6 +670,8 @@ snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid)) + _ (wasm.api/clean-geometry-modifiers) + transforms (into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)) @@ -685,12 +688,24 @@ modifiers (dm/get-in modif-tree [shape-id :modifiers])] (-> shape (gsh/apply-transform transform) - (ctm/apply-structure-modifiers modifiers))))] + (ctm/apply-structure-modifiers modifiers)))) + + bool-ids + (into #{} + (comp + (mapcat (partial cfh/get-parents-with-self objects)) + (filter cfh/bool-shape?) + (map :id)) + ids)] (rx/of (clear-local-transform) (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms}) (ptk/event ::dwcm/move-frame-comment-threads transforms) - (dwsh/update-shapes ids update-shape options)))))) + (dwsh/update-shapes ids update-shape options) + + ;; The update to the bool path needs to be in a different operation because it + ;; needs to have the updated children info + (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true))))))) (def ^:private xf-rotation-shape diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index e0dfddee42..0fdc6665e8 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -314,8 +314,8 @@ :ignore-constraints (contains? layout :scale-text)))))) (rx/take-until stopper)) - ;; The last event we need to use the old method so the elements are correctly positioned until - ;; all the logic is implemented in wasm + ;; The last event we need to use the old method so the elements are correctly + ;; positioned until all the logic is implemented in wasm (->> resize-events-stream (rx/take-until stopper) (rx/last) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 18b2e25bc6..ae634630a9 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -8,7 +8,7 @@ "A WASM based render API" (:require ["react-dom/server" :as rds] - [app.common.data :as d :refer [not-empty?]] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] [app.common.types.fills :as types.fills] @@ -850,20 +850,14 @@ [pending] (let [event (js/CustomEvent. "wasm:set-objects-finished") pending (-> (d/index-by :key :callback pending) vals)] - (if (not-empty? pending) - (->> (rx/from pending) - (rx/merge-map (fn [callback] (callback))) - (rx/tap (fn [_] (request-render "set-objects"))) - (rx/reduce conj []) - (rx/subs! (fn [_] - (clear-drawing-cache) - (request-render "pending-finished") - (h/call wasm/internal-module "_update_shape_text_layout_for_all") - (.dispatchEvent ^js js/document event)))) - (do - (clear-drawing-cache) - (request-render "pending-finished") - (.dispatchEvent ^js js/document event))))) + (->> (rx/from pending) + (rx/merge-map (fn [callback] (callback))) + (rx/reduce conj []) + (rx/subs! (fn [_] + (clear-drawing-cache) + (request-render "pending-finished") + (h/call wasm/internal-module "_update_shape_text_layout_for_all") + (.dispatchEvent ^js js/document event)))))) (defn process-object [shape] @@ -988,6 +982,10 @@ [] (h/call wasm/internal-module "_clean_modifiers")) +(defn clean-geometry-modifiers + [] + (h/call wasm/internal-module "_clean_geometry_modifiers")) + (defn set-modifiers [modifiers] @@ -1116,7 +1114,6 @@ (defn calculate-bool [bool-type ids] - (let [size (mem/get-alloc-size ids UUID-U8-SIZE) heap (mem/get-heap-u32) offset (mem/alloc->offset-32 size)] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 4d03aadf3b..675a6e3ad0 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -495,11 +495,7 @@ pub extern "C" fn get_selection_rect() -> *mut u8 { with_state_mut!(state, { let bbs: Vec<_> = entries .iter() - .flat_map(|id| { - let default = Matrix::default(); - let modifier = state.modifiers.get(id).unwrap_or(&default); - state.shapes.get(id).map(|b| b.bounds().transform(modifier)) - }) + .flat_map(|id| state.shapes.get(id).map(|b| b.bounds())) .collect(); let result_bound = if bbs.len() == 1 { @@ -569,9 +565,15 @@ pub extern "C" fn set_structure_modifiers() { #[no_mangle] pub extern "C" fn clean_modifiers() { with_state_mut!(state, { - state.structure.clear(); state.scale_content.clear(); - // state.modifiers.clear(); + state.shapes.clean_modifiers(); + state.shapes.clean_structure(); + }); +} + +#[no_mangle] +pub extern "C" fn clean_geometry_modifiers() { + with_state_mut!(state, { state.shapes.clean_modifiers(); }); } diff --git a/render-wasm/src/math.rs b/render-wasm/src/math.rs index 58b395fd6a..0e1dddf820 100644 --- a/render-wasm/src/math.rs +++ b/render-wasm/src/math.rs @@ -32,6 +32,7 @@ pub fn are_close_points(a: impl Into<(f32, f32)>, b: impl Into<(f32, f32)>) -> b is_close_to(a_x, b_x) && is_close_to(a_y, b_y) } +#[allow(dead_code)] pub fn is_close_matrix(m: &Matrix, other: &Matrix) -> bool { is_close_to(m.scale_x(), other.scale_x()) && is_close_to(m.scale_y(), other.scale_y()) diff --git a/render-wasm/src/math/bools.rs b/render-wasm/src/math/bools.rs index 8f3facbe57..72f932fb64 100644 --- a/render-wasm/src/math/bools.rs +++ b/render-wasm/src/math/bools.rs @@ -388,8 +388,6 @@ pub fn bool_from_shapes( bool_type: BoolType, children_ids: &IndexSet, shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, ) -> Path { if children_ids.is_empty() { return Path::default(); @@ -399,13 +397,13 @@ pub fn bool_from_shapes( return Path::default(); }; - let mut current_path = child.to_path(shapes, modifiers, structure); + let mut current_path = child.to_path(shapes); for idx in (0..children_ids.len() - 1).rev() { let Some(other) = shapes.get(&children_ids[idx]) else { continue; }; - let other_path = other.to_path(shapes, modifiers, structure); + let other_path = other.to_path(shapes); let (segs_a, segs_b) = split_segments(¤t_path, &other_path); @@ -422,26 +420,15 @@ pub fn bool_from_shapes( current_path } -pub fn update_bool_to_path( - shape: &Shape, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, -) -> Shape { - let mut shape = shape.clone(); - let children_ids = shape.modified_children_ids(structure.get(&shape.id), true); +#[allow(dead_code)] +pub fn update_bool_to_path(shape: &mut Shape, shapes: ShapesPoolRef) { + let children_ids = shape.children_ids(true); let Type::Bool(bool_data) = &mut shape.shape_type else { - return shape; + return; }; - bool_data.path = bool_from_shapes( - bool_data.bool_type, - &children_ids, - shapes, - modifiers, - structure, - ); - shape + + bool_data.path = bool_from_shapes(bool_data.bool_type, &children_ids, shapes); } #[allow(dead_code)] @@ -450,14 +437,14 @@ pub fn debug_render_bool_paths( render_state: &mut RenderState, shape: &Shape, shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, + _modifiers: &HashMap, + _structure: &HashMap>, ) { let canvas = render_state.surfaces.canvas(SurfaceId::Strokes); let mut shape = shape.clone(); - let children_ids = shape.modified_children_ids(structure.get(&shape.id), true); + let children_ids = shape.children_ids(true); let Type::Bool(bool_data) = &mut shape.shape_type else { return; @@ -471,13 +458,13 @@ pub fn debug_render_bool_paths( return; }; - let mut current_path = child.to_path(shapes, modifiers, structure); + let mut current_path = child.to_path(shapes); for idx in (0..children_ids.len() - 1).rev() { let Some(other) = shapes.get(&children_ids[idx]) else { continue; }; - let other_path = other.to_path(shapes, modifiers, structure); + let other_path = other.to_path(shapes); let (segs_a, segs_b) = split_segments(¤t_path, &other_path); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 8b5cf55c64..058454b421 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,8 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ - all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, - StructureEntry, Type, + all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -31,8 +30,6 @@ use crate::uuid::Uuid; use crate::view::Viewbox; use crate::wapi; -use crate::math; -use crate::math::bools; use indexmap::IndexSet; pub use fonts::*; @@ -63,13 +60,11 @@ 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 - /// when rendering. It takes into account the element's selection rectangle, transform, - /// and any additional modifiers. + /// when rendering. It takes into account the element's selection rectangle, transform. /// /// # Parameters /// /// * `element` - The shape element for which to calculate clip bounds - /// * `modifiers` - Optional transformation matrix to apply to the bounds /// * `offset` - Optional offset (x, y) to adjust the bounds position. When provided, /// the bounds are translated by the negative of this offset, effectively moving /// the clipping region to compensate for coordinate system transformations. @@ -78,7 +73,6 @@ impl NodeRenderState { pub fn get_children_clip_bounds( &self, element: &Shape, - modifiers: Option<&Matrix>, offset: Option<(f32, f32)>, ) -> Option<(Rect, Option, Matrix)> { if self.id.is_nil() || !element.clip() { @@ -97,10 +91,6 @@ impl NodeRenderState { transform.post_translate(bounds.center()); transform.pre_translate(-bounds.center()); - if let Some(modifier) = modifiers { - transform.post_concat(modifier); - } - let corners = match &element.shape_type { Type::Rect(data) => data.corners, Type::Frame(data) => data.corners, @@ -119,12 +109,10 @@ impl NodeRenderState { /// # Parameters /// /// * `element` - The shape element for which to calculate shadow clip bounds - /// * `modifiers` - Optional transformation matrix to apply to the bounds /// * `shadow` - The shadow configuration containing blur, offset, and other properties pub fn get_nested_shadow_clip_bounds( &self, element: &Shape, - modifiers: Option<&Matrix>, shadow: &Shadow, ) -> Option<(Rect, Option, Matrix)> { if self.id.is_nil() { @@ -142,10 +130,6 @@ impl NodeRenderState { transform.post_translate(element.center()); transform.pre_translate(-element.center()); - if let Some(modifier) = modifiers { - transform.post_concat(modifier); - } - let corners = match &element.shape_type { Type::Rect(data) => data.corners, Type::Frame(data) => data.corners, @@ -275,28 +259,6 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { .into() } -fn is_modified_child( - shape: &Shape, - shapes: ShapesPoolRef, - modifiers: &HashMap, -) -> bool { - if modifiers.is_empty() { - return false; - } - - let ids = shape.all_children(shapes, true, false); - let default = &Matrix::default(); - let parent_modifier = modifiers.get(&shape.id).unwrap_or(default); - - // Returns true if the transform of any child is different to the parent's - ids.iter().any(|id| { - !math::is_close_matrix( - parent_modifier, - modifiers.get(id).unwrap_or(&Matrix::default()), - ) - }) -} - impl RenderState { pub fn new(width: i32, height: i32) -> RenderState { // This needs to be done once per WebGL context. @@ -476,9 +438,6 @@ impl RenderState { #[allow(clippy::too_many_arguments)] pub fn render_shape( &mut self, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, shape: &Shape, scale_content: Option<&f32>, clip_bounds: Option<(Rect, Option, Matrix)>, @@ -546,10 +505,6 @@ impl RenderState { // We don't want to change the value in the global state let mut shape: Cow = Cow::Borrowed(shape); - if let Some(shape_modifiers) = modifiers.get(&shape.id) { - shape.to_mut().apply_transform(shape_modifiers); - } - let mut nested_blur_value = 0.; for nested_blur in self.nested_blurs.iter().flatten() { if !nested_blur.hidden && nested_blur.blur_type == BlurType::LayerBlur { @@ -580,11 +535,6 @@ impl RenderState { match &shape.shape_type { Type::SVGRaw(sr) => { - if let Some(shape_modifiers) = modifiers.get(&shape.id) { - self.surfaces - .canvas(fills_surface_id) - .concat(shape_modifiers); - } self.surfaces.canvas(fills_surface_id).concat(&matrix); if let Some(svg) = shape.svg.as_ref() { svg.render(self.surfaces.canvas(fills_surface_id)) @@ -751,20 +701,7 @@ impl RenderState { s.canvas().concat(&matrix); }); - // For boolean shapes, there's no need to calculate children because - // when painting the shape, the necessary path is already calculated - let shape = if let Type::Bool(_) = &shape.shape_type { - // If any child transform doesn't match the parent transform means - // that the children is transformed and we need to recalculate the - // boolean - if is_modified_child(&shape, shapes, modifiers) { - &bools::update_bool_to_path(&shape, shapes, modifiers, structure) - } else { - &shape - } - } else { - &shape - }; + let shape = &shape; if shape.fills.is_empty() && !matches!(shape.shape_type, Type::Group(_)) @@ -837,12 +774,7 @@ impl RenderState { } } - pub fn render_from_cache( - &mut self, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, - ) { + pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { let scale = self.get_cached_scale(); if let Some(snapshot) = &self.cached_target_snapshot { let canvas = self.surfaces.canvas(SurfaceId::Target); @@ -875,7 +807,7 @@ impl RenderState { debug::render(self); } - ui::render(self, shapes, modifiers, structure); + ui::render(self, shapes); debug::render_wasm_label(self); self.flush_and_submit(); @@ -885,8 +817,6 @@ impl RenderState { pub fn start_render_loop( &mut self, tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { @@ -937,7 +867,7 @@ impl RenderState { self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); - self.process_animation_frame(tree, modifiers, structure, scale_content, timestamp)?; + self.process_animation_frame(tree, scale_content, timestamp)?; performance::end_measure!("start_render_loop"); Ok(()) } @@ -945,21 +875,13 @@ impl RenderState { pub fn process_animation_frame( &mut self, tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { - self.render_shape_tree_partial( - tree, - modifiers, - structure, - scale_content, - timestamp, - )?; + self.render_shape_tree_partial(tree, scale_content, timestamp)?; } else { println!("Empty tree"); } @@ -1031,9 +953,6 @@ impl RenderState { #[inline] pub fn render_shape_exit( &mut self, - tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, element: &Shape, visited_mask: bool, scale_content: Option<&f32>, @@ -1091,9 +1010,6 @@ impl RenderState { element_strokes.to_mut().clear_shadows(); element_strokes.to_mut().clip_content = false; self.render_shape( - tree, - modifiers, - structure, &element_strokes, scale_content, None, @@ -1179,9 +1095,6 @@ impl RenderState { #[allow(clippy::too_many_arguments)] fn render_drop_black_shadow( &mut self, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, shape: &Shape, shadow: &Shadow, scale_content: Option<&f32>, @@ -1233,9 +1146,6 @@ impl RenderState { .translate(translation); self.render_shape( - shapes, - modifiers, - structure, &plain_shape, scale_content, clip_bounds, @@ -1254,8 +1164,6 @@ impl RenderState { pub fn render_shape_tree_partial_uncached( &mut self, tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, scale_content: &HashMap, timestamp: i32, ) -> Result<(bool, bool), String> { @@ -1272,10 +1180,11 @@ impl RenderState { } = node_render_state; is_empty = false; - let element = tree.get(&node_id).ok_or( - "Error: Element with root_id {node_render_state.id} not found in the tree." - .to_string(), - )?; + + let element = tree.get(&node_id).ok_or(format!( + "Error: Element with root_id {} not found in the tree.", + node_render_state.id + ))?; // If the shape is not in the tile set, then we update // it. @@ -1284,14 +1193,7 @@ impl RenderState { } if visited_children { - self.render_shape_exit( - tree, - modifiers, - structure, - element, - visited_mask, - scale_content.get(&element.id), - ); + self.render_shape_exit(element, visited_mask, scale_content.get(&element.id)); continue; } @@ -1351,9 +1253,6 @@ impl RenderState { // First pass: Render shadow in black to establish alpha mask self.render_drop_black_shadow( - tree, - modifiers, - structure, element, shadow, scale_content.get(&element.id), @@ -1369,17 +1268,11 @@ impl RenderState { if shadow_shape.hidden { continue; } - let clip_bounds = node_render_state.get_nested_shadow_clip_bounds( - element, - modifiers.get(&element.id), - shadow, - ); + let clip_bounds = node_render_state + .get_nested_shadow_clip_bounds(element, shadow); if !matches!(shadow_shape.shape_type, Type::Text(_)) { self.render_drop_black_shadow( - tree, - modifiers, - structure, shadow_shape, shadow, scale_content.get(&element.id), @@ -1415,9 +1308,6 @@ impl RenderState { new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver); self.render_shape( - tree, - modifiers, - structure, shadow_shape, scale_content.get(&element.id), clip_bounds, @@ -1456,9 +1346,6 @@ impl RenderState { .clear(skia::Color::TRANSPARENT); self.render_shape( - tree, - modifiers, - structure, element, scale_content.get(&element.id), clip_bounds, @@ -1497,14 +1384,10 @@ impl RenderState { }); if element.is_recursive() { - let children_clip_bounds = node_render_state.get_children_clip_bounds( - element, - modifiers.get(&element.id), - None, - ); + let children_clip_bounds = + node_render_state.get_children_clip_bounds(element, None); - let mut children_ids = - element.modified_children_ids(structure.get(&element.id), false); + let mut children_ids = element.children_ids(false); // Z-index ordering on Layouts if element.has_layout() { @@ -1538,8 +1421,6 @@ impl RenderState { pub fn render_shape_tree_partial( &mut self, tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { @@ -1566,13 +1447,8 @@ impl RenderState { } } else { performance::begin_measure!("render_shape_tree::uncached"); - let (is_empty, early_return) = self.render_shape_tree_partial_uncached( - tree, - modifiers, - structure, - scale_content, - timestamp, - )?; + let (is_empty, early_return) = + self.render_shape_tree_partial_uncached(tree, scale_content, timestamp)?; if early_return { return Ok(()); } @@ -1606,7 +1482,7 @@ impl RenderState { let Some(root) = tree.get(&Uuid::nil()) else { return Err(String::from("Root shape not found")); }; - let root_ids = root.modified_children_ids(structure.get(&root.id), false); + let root_ids = root.children_ids(false); // If we finish processing every node rendering is complete // let's check if there are more pending nodes @@ -1649,7 +1525,7 @@ impl RenderState { debug::render(self); } - ui::render(self, tree, modifiers, structure); + ui::render(self, tree); debug::render_wasm_label(self); Ok(()) @@ -1678,6 +1554,7 @@ impl RenderState { // Then, add the shape to the new tiles for tile in new_tiles { + self.remove_cached_tile_shape(tile, shape.id); self.tiles.add_shape_at(tile, shape.id); } } @@ -1689,27 +1566,18 @@ impl RenderState { self.tiles.remove_shape_at(tile, id); } - pub fn rebuild_tiles_shallow( - &mut self, - tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, - ) { + pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_tiles_shallow"); self.tiles.invalidate(); self.surfaces.remove_cached_tiles(self.background_color); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { - let mut shape: Cow = Cow::Borrowed(shape); if shape_id != Uuid::nil() { - if let Some(modifier) = modifiers.get(&shape_id) { - shape.to_mut().apply_transform(modifier); - } 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); + let children = shape.children_ids(false); for child_id in children.iter() { nodes.push(*child_id); } @@ -1719,27 +1587,18 @@ impl RenderState { performance::end_measure!("rebuild_tiles_shallow"); } - pub fn rebuild_tiles( - &mut self, - tree: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, - ) { + pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) { performance::begin_measure!("rebuild_tiles"); self.tiles.invalidate(); self.surfaces.remove_cached_tiles(self.background_color); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { - let mut shape: Cow = Cow::Borrowed(shape); if shape_id != Uuid::nil() { - if let Some(modifier) = modifiers.get(&shape_id) { - shape.to_mut().apply_transform(modifier); - } self.update_tile_for(&shape, tree); } - let children = shape.modified_children_ids(structure.get(&shape.id), false); + let children = shape.children_ids(false); for child_id in children.iter() { nodes.push(*child_id); } diff --git a/render-wasm/src/render/grid_layout.rs b/render-wasm/src/render/grid_layout.rs index 635cff137d..c319e5410f 100644 --- a/render-wasm/src/render/grid_layout.rs +++ b/render-wasm/src/render/grid_layout.rs @@ -1,21 +1,12 @@ use skia_safe::{self as skia}; -use std::collections::HashMap; -use crate::math::{Matrix, Rect}; +use crate::math::Rect; use crate::shapes::modifiers::grid_layout::grid_cell_data; -use crate::shapes::{Shape, StructureEntry}; +use crate::shapes::Shape; use crate::state::ShapesPoolRef; -use crate::uuid::Uuid; -pub fn render_overlay( - zoom: f32, - canvas: &skia::Canvas, - shape: &Shape, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, -) { - let cells = grid_cell_data(shape, shapes, modifiers, structure, true); +pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: ShapesPoolRef) { + let cells = grid_cell_data(shape, shapes, true); let mut paint = skia::Paint::default(); paint.set_style(skia::PaintStyle::Stroke); diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs index 6b37703fdc..3f662bc009 100644 --- a/render-wasm/src/render/ui.rs +++ b/render-wasm/src/render/ui.rs @@ -1,18 +1,9 @@ use skia_safe::{self as skia, Color4f}; -use std::collections::HashMap; use super::{RenderState, ShapesPoolRef, SurfaceId}; -use crate::math::Matrix; use crate::render::grid_layout; -use crate::shapes::StructureEntry; -use crate::uuid::Uuid; -pub fn render( - render_state: &mut RenderState, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, -) { +pub fn render(render_state: &mut RenderState, shapes: ShapesPoolRef) { let canvas = render_state.surfaces.canvas(SurfaceId::UI); canvas.clear(Color4f::new(0.0, 0.0, 0.0, 0.0)); @@ -29,7 +20,7 @@ pub fn render( if let Some(id) = render_state.show_grid { if let Some(shape) = shapes.get(&id) { - grid_layout::render_overlay(zoom, canvas, shape, shapes, modifiers, structure); + grid_layout::render_overlay(zoom, canvas, shape, shapes); } } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index dcfcdf48bc..ebe2c97beb 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -47,6 +47,7 @@ pub use svgraw::*; pub use text::*; pub use transform::*; +use crate::math::bools as math_bools; use crate::math::{self, Bounds, Matrix, Point}; use indexmap::IndexSet; @@ -309,6 +310,10 @@ impl Shape { matches!(self.shape_type, Type::Frame(_)) } + pub fn is_bool(&self) -> bool { + matches!(self.shape_type, Type::Bool(_)) + } + pub fn is_group_like(&self) -> bool { matches!(self.shape_type, Type::Group(_)) || matches!(self.shape_type, Type::Bool(_)) } @@ -901,6 +906,7 @@ impl Shape { self.children.first() } + // TODO: Review this to use children_ids_iter instead pub fn children_ids(&self, include_hidden: bool) -> IndexSet { if include_hidden { return self.children.clone().into_iter().rev().collect(); @@ -1164,7 +1170,7 @@ impl Shape { for st in structure { match st.entry_type { StructureEntryType::AddChild => { - result.insert(result.len() - st.index as usize, st.id); + result.insert(st.index as usize, st.id); } StructureEntryType::RemoveChild => { to_remove.insert(&st.id); @@ -1182,6 +1188,7 @@ impl Shape { pub fn transformed( &self, + shapes_pool: ShapesPoolRef, transform: Option<&Matrix>, structure: Option<&Vec>, ) -> Self { @@ -1192,6 +1199,9 @@ impl Shape { if let Some(structure) = structure { shape.to_mut().apply_structure(structure); } + if self.is_bool() { + math_bools::update_bool_to_path(shape.to_mut(), shapes_pool) + } shape.into_owned() } @@ -1247,73 +1257,6 @@ impl Shape { .count() } - /* - Returns the list of children taking into account the structure modifiers - */ - pub fn modified_children_ids( - &self, - structure: Option<&Vec>, - include_hidden: bool, - ) -> IndexSet { - if let Some(structure) = structure { - let mut result: Vec = - Vec::from_iter(self.children_ids(include_hidden).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); - } - _ => {} - } - } - - let ret: IndexSet = result - .iter() - .filter(|id| !to_remove.contains(id)) - .copied() - .collect(); - - ret - } else { - self.children_ids(include_hidden) - } - } - - pub fn modified_children_ids_iter<'a>( - &'a self, - structure: Option<&'a Vec>, - include_hidden: bool, - ) -> Box> + 'a> { - if let Some(structure) = structure { - let mut result: Vec> = self - .children_ids_iter(include_hidden) - .map(Cow::Borrowed) - .collect(); - let mut to_remove = HashSet::>::new(); - - for st in structure { - match st.entry_type { - StructureEntryType::AddChild => { - result.insert(result.len() - st.index as usize, Cow::Owned(st.id)); - } - StructureEntryType::RemoveChild => { - to_remove.insert(Cow::Owned(st.id)); - } - _ => {} - } - } - - Box::new(result.into_iter().filter(move |id| !to_remove.contains(id))) - } else { - Box::new(self.children_ids_iter(include_hidden).map(Cow::Borrowed)) - } - } - pub fn drop_shadow_paints(&self) -> Vec { let drop_shadows: Vec<&Shadow> = self.drop_shadows_visible().collect(); diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index b655342cbc..b43e2b98bf 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -10,8 +10,7 @@ use crate::math::{self as math, bools, identitish, Bounds, Matrix, Point}; use common::GetBounds; use crate::shapes::{ - ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, StructureEntry, - TransformEntry, Type, + ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, Type, }; use crate::state::{ShapesPoolRef, State}; use crate::uuid::Uuid; @@ -24,10 +23,9 @@ fn propagate_children( parent_bounds_after: &Bounds, transform: Matrix, bounds: &HashMap, - structure: &HashMap>, scale_content: &HashMap, ) -> VecDeque { - let children_ids = shape.modified_children_ids(structure.get(&shape.id), true); + let children_ids = shape.children_ids(true); if children_ids.is_empty() || identitish(&transform) { return VecDeque::new(); @@ -92,12 +90,11 @@ fn calculate_group_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, - structure: &HashMap>, ) -> Option { let shape_bounds = bounds.find(shape); let mut result = Vec::::new(); - for child_id in shape.modified_children_ids_iter(structure.get(&shape.id), true) { + for child_id in shape.children_ids_iter(true) { let Some(child) = shapes.get(&child_id) else { continue; }; @@ -112,23 +109,15 @@ fn calculate_bool_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, - modifiers: &HashMap, - structure: &HashMap>, ) -> Option { let shape_bounds = bounds.find(shape); - let children_ids = shape.modified_children_ids(structure.get(&shape.id), true); + let children_ids = shape.children_ids(true); let Type::Bool(bool_data) = &shape.shape_type else { return Some(shape_bounds); }; - let path = bools::bool_from_shapes( - bool_data.bool_type, - &children_ids, - shapes, - modifiers, - structure, - ); + let path = bools::bool_from_shapes(bool_data.bool_type, &children_ids, shapes); Some(path.bounds()) } @@ -235,7 +224,6 @@ fn propagate_transform( &shape_bounds_after, transform, bounds, - &state.structure, &state.scale_content, ); entries.append(&mut children); @@ -265,7 +253,6 @@ fn propagate_reflow( bounds: &mut HashMap, layout_reflows: &mut Vec, reflown: &mut HashSet, - modifiers: &HashMap, ) { let Some(shape) = state.shapes.get(id) else { return; @@ -303,7 +290,7 @@ fn propagate_reflow( } } Type::Group(Group { masked: true }) => { - let children_ids = shape.modified_children_ids(state.structure.get(&shape.id), true); + let children_ids = shape.children_ids(true); if let Some(child) = shapes.get(&children_ids[0]) { let child_bounds = bounds.find(child); bounds.insert(shape.id, child_bounds); @@ -312,18 +299,14 @@ fn propagate_reflow( reflown.insert(*id); } Type::Group(_) => { - if let Some(shape_bounds) = - calculate_group_bounds(shape, shapes, bounds, &state.structure) - { + if let Some(shape_bounds) = calculate_group_bounds(shape, shapes, bounds) { bounds.insert(shape.id, shape_bounds); reflow_parent = true; } reflown.insert(*id); } Type::Bool(_) => { - if let Some(shape_bounds) = - calculate_bool_bounds(shape, shapes, bounds, modifiers, &state.structure) - { + if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds) { bounds.insert(shape.id, shape_bounds); reflow_parent = true; } @@ -365,24 +348,12 @@ fn reflow_shape( }; if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout { - let mut children = flex_layout::reflow_flex_layout( - shape, - layout_data, - flex_data, - shapes, - bounds, - &state.structure, - ); + let mut children = + flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds); entries.append(&mut children); } else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout { - let mut children = grid_layout::reflow_grid_layout( - shape, - layout_data, - grid_data, - shapes, - bounds, - &state.structure, - ); + let mut children = + grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds); entries.append(&mut children); } reflown.insert(*id); @@ -398,12 +369,6 @@ pub fn propagate_modifiers( .map(|entry| Modifier::Transform(entry.clone())) .collect(); - for id in state.structure.keys() { - if id != &Uuid::nil() { - entries.push_back(Modifier::Reflow(*id)); - } - } - let mut modifiers = HashMap::::new(); let mut bounds = HashMap::::new(); let mut reflown = HashSet::::new(); @@ -432,7 +397,6 @@ pub fn propagate_modifiers( &mut bounds, &mut layout_reflows, &mut reflown, - &modifiers, ), } } diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index dd89fde069..b3202c43f3 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -2,7 +2,7 @@ use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt}; use crate::shapes::{ AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem, - Modifier, Shape, StructureEntry, + Modifier, Shape, }; use crate::state::ShapesPoolRef; use crate::uuid::Uuid; @@ -181,11 +181,10 @@ fn initialize_tracks( flex_data: &FlexData, shapes: ShapesPoolRef, bounds: &HashMap, - structure: &HashMap>, ) -> Vec { let mut tracks = Vec::::new(); let mut current_track = TrackData::default(); - let mut children = shape.modified_children_ids(structure.get(&shape.id), true); + let mut children = shape.children_ids(true); let mut first = true; if flex_data.is_reverse() { @@ -435,7 +434,6 @@ fn calculate_track_data( layout_bounds: &Bounds, shapes: ShapesPoolRef, bounds: &HashMap, - structure: &HashMap>, ) -> Vec { let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); let mut tracks = initialize_tracks( @@ -445,7 +443,6 @@ fn calculate_track_data( flex_data, shapes, bounds, - structure, ); distribute_fill_main_space(&layout_axis, &mut tracks); @@ -576,20 +573,11 @@ pub fn reflow_flex_layout( flex_data: &FlexData, shapes: ShapesPoolRef, bounds: &mut HashMap, - structure: &HashMap>, ) -> VecDeque { let mut result = VecDeque::new(); let layout_bounds = &bounds.find(shape); let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); - let tracks = calculate_track_data( - shape, - layout_data, - flex_data, - layout_bounds, - shapes, - bounds, - structure, - ); + let tracks = calculate_track_data(shape, layout_data, flex_data, layout_bounds, shapes, bounds); for track in tracks.iter() { let total_shapes_size = track.shapes.iter().map(|s| s.main_size).sum::(); diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 96f017fe79..f1ceacdccb 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -2,7 +2,7 @@ use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vect use crate::shapes::{ AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType, JustifyContent, JustifyItems, JustifySelf, Layout, LayoutData, LayoutItem, Modifier, Shape, - StructureEntry, Type, + Type, }; use crate::state::ShapesPoolRef; use crate::uuid::Uuid; @@ -603,8 +603,6 @@ pub fn create_cell_data<'a>( pub fn grid_cell_data<'a>( shape: &Shape, shapes: ShapesPoolRef<'a>, - modifiers: &HashMap, - structure: &HashMap>, allow_empty: bool, ) -> Vec> { let Type::Frame(Frame { @@ -616,26 +614,8 @@ pub fn grid_cell_data<'a>( }; let bounds = &mut HashMap::::new(); - - let shape = &mut shape.clone(); - if let Some(modifiers) = modifiers.get(&shape.id) { - shape.apply_transform(modifiers); - } - let layout_bounds = shape.bounds(); - let children = shape.modified_children_ids(structure.get(&shape.id), false); - - for child_id in children.iter() { - let Some(child) = shapes.get(child_id) else { - continue; - }; - - if let Some(modifier) = modifiers.get(child_id) { - let mut b = bounds.find(child); - b.transform_mut(modifier); - bounds.insert(*child_id, b); - } - } + let children = shape.children_ids(false); let column_tracks = calculate_tracks( true, @@ -725,11 +705,10 @@ pub fn reflow_grid_layout( grid_data: &GridData, shapes: ShapesPoolRef, bounds: &mut HashMap, - structure: &HashMap>, ) -> VecDeque { let mut result = VecDeque::new(); let layout_bounds = bounds.find(shape); - let children = shape.modified_children_ids(structure.get(&shape.id), true); + let children = shape.children_ids(true); let column_tracks = calculate_tracks( true, diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index a798665abf..d641a2dc86 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -1,22 +1,13 @@ -use skia_safe::Matrix; - -use super::{Corners, Path, Segment, Shape, StructureEntry, Type}; +use super::{Corners, Path, Segment, Shape, Type}; use crate::math; use crate::shapes::text_paths::TextPaths; use crate::state::ShapesPoolRef; -use crate::uuid::Uuid; -use std::collections::HashMap; const BEZIER_CIRCLE_C: f32 = 0.551_915_05; pub trait ToPath { - fn to_path( - &self, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, - ) -> Path; + fn to_path(&self, shapes: ShapesPoolRef) -> Path; } enum CornerType { @@ -180,33 +171,28 @@ fn transform_segments(segments: Vec, shape: &Shape) -> Vec { } impl ToPath for Shape { - fn to_path( - &self, - shapes: ShapesPoolRef, - modifiers: &HashMap, - structure: &HashMap>, - ) -> Path { + fn to_path(&self, shapes: ShapesPoolRef) -> Path { match &self.shape_type { Type::Frame(ref frame) => { - let children = self.modified_children_ids(structure.get(&self.id), true); + let children = self.children_ids(true); let mut result = Path::new(rect_segments(&self, frame.corners)); for id in children { let Some(shape) = shapes.get(&id) else { continue; }; - result = join_paths(result, shape.to_path(shapes, modifiers, structure)); + result = join_paths(result, shape.to_path(shapes)); } result } Type::Group(_) => { - let children = self.modified_children_ids(structure.get(&self.id), true); + let children = self.children_ids(true); let mut result = Path::default(); for id in children { let Some(shape) = shapes.get(&id) else { continue; }; - result = join_paths(result, shape.to_path(shapes, modifiers, structure)); + result = join_paths(result, shape.to_path(shapes)); } // Force closure of the group path let mut segments = result.segments().clone(); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 0d82585233..85998b1bb3 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -8,7 +8,6 @@ pub use text_editor::*; use crate::render::RenderState; use crate::shapes::Shape; -use crate::shapes::StructureEntry; use crate::tiles; use crate::uuid::Uuid; @@ -24,9 +23,7 @@ pub(crate) struct State<'a> { pub text_editor_state: TextEditorState, pub current_id: Option, pub shapes: ShapesPool<'a>, - pub modifiers: HashMap, pub scale_content: HashMap, - pub structure: HashMap>, } impl<'a> State<'a> { @@ -36,9 +33,7 @@ impl<'a> State<'a> { text_editor_state: TextEditorState::new(), current_id: None, shapes: ShapesPool::new(), - modifiers: HashMap::new(), scale_content: HashMap::new(), - structure: HashMap::new(), } } @@ -65,29 +60,18 @@ impl<'a> State<'a> { } pub fn render_from_cache(&mut self) { - self.render_state - .render_from_cache(&self.shapes, &self.modifiers, &self.structure); + self.render_state.render_from_cache(&self.shapes); } pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { - self.render_state.start_render_loop( - &self.shapes, - &self.modifiers, - &self.structure, - &self.scale_content, - timestamp, - )?; + self.render_state + .start_render_loop(&self.shapes, &self.scale_content, timestamp)?; Ok(()) } pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { - self.render_state.process_animation_frame( - &self.shapes, - &self.modifiers, - &self.structure, - &self.scale_content, - timestamp, - )?; + self.render_state + .process_animation_frame(&self.shapes, &self.scale_content, timestamp)?; Ok(()) } @@ -147,6 +131,8 @@ impl<'a> State<'a> { panic!("Invalid current shape") }; shape.set_parent(id); + + // TODO this clone doesn't seem necessary shape.clone() }; @@ -166,6 +152,7 @@ impl<'a> State<'a> { let Some(shape) = self.current_shape() else { panic!("Invalid current shape") }; + // TODO: Remove this clone if !shape.id.is_nil() { self.render_state .update_tile_for(&shape.clone(), &self.shapes); @@ -173,13 +160,11 @@ impl<'a> State<'a> { } pub fn rebuild_tiles_shallow(&mut self) { - self.render_state - .rebuild_tiles_shallow(&self.shapes, &self.modifiers, &self.structure); + self.render_state.rebuild_tiles_shallow(&self.shapes); } pub fn rebuild_tiles(&mut self) { - self.render_state - .rebuild_tiles(&self.shapes, &self.modifiers, &self.structure); + self.render_state.rebuild_tiles(&self.shapes); } pub fn rebuild_modifier_tiles(&mut self, ids: Vec) { @@ -204,7 +189,7 @@ impl<'a> State<'a> { let bounds = shape.bounds(); let position = Point::new(pos_x, pos_y); - let cells = grid_cell_data(shape, &self.shapes, &self.modifiers, &self.structure, true); + let cells = grid_cell_data(shape, &self.shapes, true); for cell in cells { let points = &[ diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 6ecd50b274..d4ca32af9a 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -2,6 +2,7 @@ use std::collections::HashMap; use std::iter; use crate::performance; +use crate::shapes; use crate::shapes::Shape; use crate::uuid::Uuid; @@ -208,11 +209,15 @@ impl<'a> ShapesPoolImpl<'a> { // Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id let id_ref: &'a Uuid = &*(id as *const Uuid); - if (*modifiers_ptr).contains_key(&id_ref) || (*structure_ptr).contains_key(&id_ref) { + if self.to_update_bool(&*shape_ptr) + || (*modifiers_ptr).contains_key(&id_ref) + || (*structure_ptr).contains_key(&id_ref) + { if let Some(cell) = (*cache_ptr).get(&id_ref) { Some(cell.get_or_init(|| { let shape = &*shape_ptr; shape.transformed( + &self, (*modifiers_ptr).get(&id_ref), (*structure_ptr).get(&id_ref), ) @@ -246,14 +251,25 @@ impl<'a> ShapesPoolImpl<'a> { // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and // Initialize the cache cells because later we don't want to have the mutable pointer + + let mut ids = Vec::::new(); + let mut modifiers_with_refs = HashMap::with_capacity(modifiers.len()); for (uuid, matrix) in modifiers { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { - self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + // self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); modifiers_with_refs.insert(uuid_ref, matrix); + ids.push(*uuid_ref); } } self.modifiers = modifiers_with_refs; + + let all_ids = shapes::all_with_ancestors(&ids, &self, true); + for uuid in all_ids { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + } + } } #[allow(dead_code)] @@ -261,13 +277,22 @@ impl<'a> ShapesPoolImpl<'a> { // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and // Initialize the cache cells because later we don't want to have the mutable pointer let mut structure_with_refs = HashMap::with_capacity(structure.len()); + let mut ids = Vec::::new(); + for (uuid, entries) in structure { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { - self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); structure_with_refs.insert(uuid_ref, entries); + ids.push(*uuid_ref); } } self.structure = structure_with_refs; + + let all_ids = shapes::all_with_ancestors(&ids, &self, true); + for uuid in all_ids { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + } + } } #[allow(dead_code)] @@ -290,4 +315,33 @@ impl<'a> ShapesPoolImpl<'a> { // and won't be reallocated. unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) } } + + fn to_update_bool(&self, shape: &Shape) -> bool { + // TODO: Check if any of the children is in the modifiers with a + // different matrix than the current one. + shape.is_bool() + } } + +// fn is_modified_child( +// shape: &Shape, +// shapes: ShapesPoolRef, +// modifiers: &HashMap, +// ) -> bool { +// if modifiers.is_empty() { +// return false; +// } +// +// let ids = shape.all_children(shapes, true, false); +// let default = &Matrix::default(); +// let parent_modifier = modifiers.get(&shape.id).unwrap_or(default); +// +// // Returns true if the transform of any child is different to the parent's +// ids.iter().any(|id| { +// !math::is_close_matrix( +// parent_modifier, +// modifiers.get(id).unwrap_or(&Matrix::default()), +// ) +// }) +// } + diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index 4f0fc34ebb..db69f1b21a 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -213,7 +213,7 @@ pub extern "C" fn set_shape_path_content() { pub extern "C" fn current_to_path() -> *mut u8 { let mut result = Vec::::default(); with_current_shape!(state, |shape: &Shape| { - let path = shape.to_path(&state.shapes, &state.modifiers, &state.structure); + let path = shape.to_path(&state.shapes); result = path .segments() .iter() diff --git a/render-wasm/src/wasm/paths/bools.rs b/render-wasm/src/wasm/paths/bools.rs index cc72543d63..08b852d511 100644 --- a/render-wasm/src/wasm/paths/bools.rs +++ b/render-wasm/src/wasm/paths/bools.rs @@ -57,13 +57,7 @@ pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 { let bool_type = RawBoolType::from(raw_bool_type).into(); let result; with_state!(state, { - let path = math::bools::bool_from_shapes( - bool_type, - &entries, - &state.shapes, - &state.modifiers, - &state.structure, - ); + let path = math::bools::bool_from_shapes(bool_type, &entries, &state.shapes); result = path .segments() .iter() From 122619b197868be773feb63726526a013863df85 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 29 Oct 2025 09:54:51 +0100 Subject: [PATCH 12/15] :sparkles: Support for booleans dynamic transforms --- render-wasm/src/shapes/modifiers.rs | 13 ++++++++--- render-wasm/src/state/shapes_pool.rs | 33 +++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index b43e2b98bf..df5839e888 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -109,6 +109,7 @@ fn calculate_bool_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, + modifiers: &HashMap ) -> Option { let shape_bounds = bounds.find(shape); let children_ids = shape.children_ids(true); @@ -117,9 +118,13 @@ fn calculate_bool_bounds( return Some(shape_bounds); }; - let path = bools::bool_from_shapes(bool_data.bool_type, &children_ids, shapes); + let mut subtree = shapes.subtree(&shape.id); + subtree.set_modifiers(modifiers.clone()); - Some(path.bounds()) + let path = bools::bool_from_shapes(bool_data.bool_type, &children_ids, &subtree); + let result = path.bounds(); + + Some(result) } fn set_pixel_precision(transform: &mut Matrix, bounds: &mut Bounds) { @@ -253,6 +258,7 @@ fn propagate_reflow( bounds: &mut HashMap, layout_reflows: &mut Vec, reflown: &mut HashSet, + modifiers: &HashMap ) { let Some(shape) = state.shapes.get(id) else { return; @@ -306,7 +312,7 @@ fn propagate_reflow( reflown.insert(*id); } Type::Bool(_) => { - if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds) { + if let Some(shape_bounds) = calculate_bool_bounds(shape, shapes, bounds, modifiers) { bounds.insert(shape.id, shape_bounds); reflow_parent = true; } @@ -397,6 +403,7 @@ pub fn propagate_modifiers( &mut bounds, &mut layout_reflows, &mut reflown, + &mut modifiers, ), } } diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index d4ca32af9a..d286d6955c 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -316,6 +316,38 @@ impl<'a> ShapesPoolImpl<'a> { unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) } } + pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> { + let Some(shape) = self.get(id) else { panic!("Subtree not found"); }; + + // TODO: Maybe create all_children_iter + let all_children = shape.all_children(self, true, true); + + let mut shapes = vec![]; + let mut idx = 0; + let mut shapes_uuid_to_idx = HashMap::default(); + + for id in all_children.iter() { + let Some(shape) = self.get(id) else { panic!("Not found"); }; + shapes.push(shape.clone()); + + let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) }; + shapes_uuid_to_idx.insert(id_ref, idx); + idx += 1; + } + + let mut result = ShapesPoolImpl { + shapes, + counter: idx, + shapes_uuid_to_idx, + modified_shape_cache: HashMap::default(), + modifiers: HashMap::default(), + structure: HashMap::default(), + }; + result.rebuild_references(); + + result + } + fn to_update_bool(&self, shape: &Shape) -> bool { // TODO: Check if any of the children is in the modifiers with a // different matrix than the current one. @@ -344,4 +376,3 @@ impl<'a> ShapesPoolImpl<'a> { // ) // }) // } - From fcc928230464cdbd55b23d709f537945faf84668 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Oct 2025 12:20:45 +0100 Subject: [PATCH 13/15] :sparkles: Fix problems with SVGraw and modifiers --- .../app/main/data/workspace/modifiers.cljs | 38 ++++++++++++------- .../app/main/data/workspace/transforms.cljs | 38 ++++++++++--------- frontend/src/app/render_wasm/shape.cljs | 5 ++- render-wasm/src/main.rs | 8 +++- render-wasm/src/render.rs | 9 ++++- render-wasm/src/shapes.rs | 10 ++++- render-wasm/src/shapes/modifiers.rs | 18 ++++----- render-wasm/src/shapes/shape_to_path.rs | 8 ++-- render-wasm/src/state.rs | 9 ++++- render-wasm/src/state/shapes_pool.rs | 14 ++++--- 10 files changed, 101 insertions(+), 56 deletions(-) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index 4e175d0009..3d3831062d 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -645,12 +645,16 @@ #_:clj-kondo/ignore (defn apply-wasm-modifiers - [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-group] - :or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-group nil} + [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-transation?] + :or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true} :as params}] (ptk/reify ::apply-wasm-modifiesr ptk/WatchEvent (watch [_ state _] + (wasm.api/clean-modifiers) + (let [structure-entries (parse-structure-modifiers modif-tree)] + (wasm.api/set-structure-modifiers structure-entries)) + (let [objects (dsh/lookup-page-objects state) ignore-tree @@ -670,8 +674,6 @@ snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid)) - _ (wasm.api/clean-geometry-modifiers) - transforms (into {} (wasm.api/propagate-modifiers geometry-entries snap-pixel?)) @@ -696,16 +698,26 @@ (mapcat (partial cfh/get-parents-with-self objects)) (filter cfh/bool-shape?) (map :id)) - ids)] - (rx/of - (clear-local-transform) - (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms}) - (ptk/event ::dwcm/move-frame-comment-threads transforms) - (dwsh/update-shapes ids update-shape options) + ids) - ;; The update to the bool path needs to be in a different operation because it - ;; needs to have the updated children info - (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true))))))) + undo-id (js/Symbol)] + (rx/concat + (if undo-transation? + (rx/of (dwu/start-undo-transaction undo-id)) + (rx/empty)) + (rx/of + (clear-local-transform) + (ptk/event ::dwg/move-frame-guides {:ids ids :transforms transforms}) + (ptk/event ::dwcm/move-frame-comment-threads transforms) + (dwsh/update-shapes ids update-shape options) + + ;; The update to the bool path needs to be in a different operation because it + ;; needs to have the updated children info + (dwsh/update-shapes bool-ids path/update-bool-shape (assoc options :with-objects? true))) + + (if undo-transation? + (rx/of (dwu/commit-undo-transaction undo-id)) + (rx/empty))))))) (def ^:private xf-rotation-shape diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 0fdc6665e8..13fa6c3356 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -146,7 +146,7 @@ (defn start-resize "Enter mouse resize mode, until mouse button is released." [handler ids shape] - (letfn [(resize [shape initial layout [point lock? center? point-snap]] + (letfn [(resize [shape initial layout objects [point lock? center? point-snap]] (let [selrect (dm/get-prop shape :selrect) width (dm/get-prop selrect :width) height (dm/get-prop selrect :height) @@ -243,10 +243,14 @@ :always (ctm/resize scalev resize-origin shape-transform shape-transform-inverse) - ^boolean change-width? + (and (ctl/any-layout-immediate-child? objects shape) + (not= (:layout-item-h-sizing shape) :fix) + ^boolean change-width?) (ctm/change-property :layout-item-h-sizing :fix) - ^boolean change-height? + (and (ctl/any-layout-immediate-child? objects shape) + (not= (:layout-item-v-sizing shape) :fix) + ^boolean change-height?) (ctm/change-property :layout-item-v-sizing :fix) ;; Set grow-type if it should change @@ -298,7 +302,7 @@ (fn [[point _ _ :as current]] (->> (snap/closest-snap-point page-id shapes objects layout zoom focus point) (rx/map #(conj current %))))) - (rx/map #(resize shape initial-position layout %)) + (rx/map #(resize shape initial-position layout objects %)) (rx/share)) modifiers-stream @@ -332,8 +336,8 @@ (rx/take-until stopper)))] (rx/concat - ;; This initial stream waits for some pixels to be move before making the resize - ;; if you make a click in the border will not make a resize + ;; This initial stream waits for some pixels to be move before making the resize + ;; if you make a click in the border will not make a resize (->> ms/mouse-position (rx/map #(gpt/to-vec initial-position %)) (rx/map #(gpt/length %)) @@ -745,12 +749,6 @@ (fn [[modifiers snap-ignore-axis]] (dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis)))) - (->> modifiers-stream - (rx/last) - (rx/map - (fn [[modifiers snap-ignore-axis]] - (dwm/apply-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis)))) - (->> move-stream (rx/with-latest-from ms/mouse-position-alt) (rx/filter (fn [[_ alt?]] alt?)) @@ -765,14 +763,18 @@ ;; Last event will write the modifiers creating the changes (->> move-stream (rx/last) + (rx/with-latest-from modifiers-stream) (rx/mapcat - (fn [[_ target-frame drop-index drop-cell]] + (fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]] (let [undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - ;; (dwm/apply-modifiers {:undo-transation? false}) - (move-shapes-to-frame ids target-frame drop-index drop-cell) - (finish-transform) - (dwu/commit-undo-transaction undo-id))))))) + (rx/of + (dwu/start-undo-transaction undo-id) + (dwm/apply-wasm-modifiers modifiers + :snap-ignore-axis snap-ignore-axis + :undo-transation? false) + (move-shapes-to-frame ids target-frame drop-index drop-cell) + (finish-transform) + (dwu/commit-undo-transaction undo-id))))))) (rx/merge (->> modifiers-stream diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 7e0ffcc7a2..922240dd29 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -135,7 +135,10 @@ (when (or (= v :path) (= v :bool)) (api/set-shape-path-content (:content shape)))) :bool-type (api/set-shape-bool-type v) - :selrect (api/set-shape-selrect v) + :selrect (do + (api/set-shape-selrect v) + (when (= (:type shape) :svg-raw) + (api/set-shape-svg-raw-content (api/get-static-markup shape)))) :show-content (if (= (:type shape) :frame) (api/set-shape-clip-content (not v)) (api/set-shape-clip-content false)) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 675a6e3ad0..694c0c9901 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -290,15 +290,21 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) { fn set_children_set(entries: IndexSet) { let mut deleted = IndexSet::new(); + let mut parent_id = None; with_current_shape_mut!(state, |shape: &mut Shape| { + parent_id = Some(shape.id); (_, deleted) = shape.compute_children_differences(&entries); shape.children = entries.clone(); }); with_state_mut!(state, { + let Some(parent_id) = parent_id else { + return; + }; + for id in deleted { - state.delete_shape(id); + state.delete_shape_children(parent_id, id); } }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 058454b421..78e032caaf 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -535,7 +535,12 @@ impl RenderState { match &shape.shape_type { Type::SVGRaw(sr) => { + if let Some(svg_transform) = shape.svg_transform() { + matrix.pre_concat(&svg_transform); + } + self.surfaces.canvas(fills_surface_id).concat(&matrix); + if let Some(svg) = shape.svg.as_ref() { svg.render(self.surfaces.canvas(fills_surface_id)) } else { @@ -1574,7 +1579,7 @@ impl RenderState { while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { - self.update_tile_for(&shape, tree); + self.update_tile_for(shape, tree); } else { // We only need to rebuild tiles from the first level. let children = shape.children_ids(false); @@ -1595,7 +1600,7 @@ impl RenderState { while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { - self.update_tile_for(&shape, tree); + self.update_tile_for(shape, tree); } let children = shape.children_ids(false); diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index ebe2c97beb..6a9d0bddb5 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -182,6 +182,7 @@ pub struct Shape { pub layout_item: Option, pub extrect: OnceCell, pub bounds: OnceCell, + pub svg_transform: Option, } // Returns all ancestor shapes of this shape, traversing up the parent hierarchy @@ -263,6 +264,7 @@ impl Shape { layout_item: None, extrect: OnceCell::new(), bounds: OnceCell::new(), + svg_transform: None, } } @@ -393,6 +395,10 @@ impl Shape { self.hidden = value; } + pub fn svg_transform(&self) -> Option { + self.svg_transform + } + // FIXME: These arguments could be grouped or simplified #[allow(clippy::too_many_arguments)] pub fn set_flex_layout_child_data( @@ -876,7 +882,7 @@ impl Shape { } Type::Text(text_content) => { // FIXME: we need to recalculate the text bounds here because the shape's selrect - let text_bounds = text_content.calculate_bounds(&shape); + let text_bounds = text_content.calculate_bounds(shape); text_bounds.to_rect() } _ => shape.bounds().to_rect(), @@ -1160,6 +1166,8 @@ impl Shape { } } else if let Type::Text(text) = &mut self.shape_type { text.transform(transform); + } else if let Type::SVGRaw(_) = &mut self.shape_type { + self.svg_transform = Some(*transform); } } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index df5839e888..5bfd4781ca 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -95,7 +95,7 @@ fn calculate_group_bounds( let mut result = Vec::::new(); for child_id in shape.children_ids_iter(true) { - let Some(child) = shapes.get(&child_id) else { + let Some(child) = shapes.get(child_id) else { continue; }; @@ -109,7 +109,7 @@ fn calculate_bool_bounds( shape: &Shape, shapes: ShapesPoolRef, bounds: &HashMap, - modifiers: &HashMap + modifiers: &HashMap, ) -> Option { let shape_bounds = bounds.find(shape); let children_ids = shape.children_ids(true); @@ -258,7 +258,7 @@ fn propagate_reflow( bounds: &mut HashMap, layout_reflows: &mut Vec, reflown: &mut HashSet, - modifiers: &HashMap + modifiers: &HashMap, ) { let Some(shape) = state.shapes.get(id) else { return; @@ -267,7 +267,7 @@ fn propagate_reflow( let shapes = &state.shapes; let mut reflow_parent = false; - if reflown.contains(&id) { + if reflown.contains(id) { return; } @@ -403,7 +403,7 @@ pub fn propagate_modifiers( &mut bounds, &mut layout_reflows, &mut reflown, - &mut modifiers, + &modifiers, ), } } @@ -429,13 +429,14 @@ mod tests { use crate::math::{Matrix, Point}; use crate::shapes::*; + use crate::state::ShapesPool; #[test] fn test_propagate_shape() { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPoolRef::new(); + let mut shapes = ShapesPool::new(); shapes.initialize(10); let child_id = Uuid::new_v4(); @@ -468,7 +469,6 @@ mod tests { transform, &HashMap::new(), &HashMap::new(), - &HashMap::new(), ); assert_eq!(result.len(), 1); @@ -478,7 +478,7 @@ mod tests { fn test_group_bounds() { let parent_id = Uuid::new_v4(); let shapes = { - let mut shapes = ShapesPoolRef::new(); + let mut shapes = ShapesPool::new(); shapes.initialize(10); let child1_id = Uuid::new_v4(); @@ -500,7 +500,7 @@ mod tests { let parent = shapes.get(&parent_id).unwrap(); let bounds = - calculate_group_bounds(parent, &shapes, &HashMap::new(), &HashMap::new()).unwrap(); + calculate_group_bounds(parent, &shapes, &HashMap::new()).unwrap(); assert_eq!(bounds.width(), 3.0); assert_eq!(bounds.height(), 3.0); diff --git a/render-wasm/src/shapes/shape_to_path.rs b/render-wasm/src/shapes/shape_to_path.rs index d641a2dc86..db241fe44b 100644 --- a/render-wasm/src/shapes/shape_to_path.rs +++ b/render-wasm/src/shapes/shape_to_path.rs @@ -175,7 +175,7 @@ impl ToPath for Shape { match &self.shape_type { Type::Frame(ref frame) => { let children = self.children_ids(true); - let mut result = Path::new(rect_segments(&self, frame.corners)); + let mut result = Path::new(rect_segments(self, frame.corners)); for id in children { let Some(shape) = shapes.get(&id) else { continue; @@ -202,11 +202,11 @@ impl ToPath for Shape { Type::Bool(bool_data) => bool_data.path.clone(), - Type::Rect(ref rect) => Path::new(rect_segments(&self, rect.corners)), + Type::Rect(ref rect) => Path::new(rect_segments(self, rect.corners)), Type::Path(path_data) => path_data.clone(), - Type::Circle => Path::new(circle_segments(&self)), + Type::Circle => Path::new(circle_segments(self)), Type::SVGRaw(_) => Path::default(), @@ -217,7 +217,7 @@ impl ToPath for Shape { result = join_paths(result, Path::from_skia_path(path)); } - Path::new(transform_segments(result.segments().clone(), &self)) + Path::new(transform_segments(result.segments().clone(), self)) } } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 85998b1bb3..54059ba70d 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -94,9 +94,14 @@ impl<'a> State<'a> { self.current_id = Some(id); } - pub fn delete_shape(&mut self, id: Uuid) { + pub fn delete_shape_children(&mut self, parent_id: Uuid, id: Uuid) { // We don't really do a self.shapes.remove so that redo/undo keep working - if let Some(shape) = self.shapes.get(&id) { + let Some(shape) = self.shapes.get(&id) else { + return; + }; + + // Only remove the children when is being deleted from the owner + if shape.parent_id.is_none() || shape.parent_id == Some(parent_id) { let tiles::TileRect(rsx, rsy, rex, rey) = self.render_state.get_tiles_for_shape(shape, &self.shapes); for x in rsx..=rex { diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index d286d6955c..9c0b8ff450 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -217,7 +217,7 @@ impl<'a> ShapesPoolImpl<'a> { Some(cell.get_or_init(|| { let shape = &*shape_ptr; shape.transformed( - &self, + self, (*modifiers_ptr).get(&id_ref), (*structure_ptr).get(&id_ref), ) @@ -264,7 +264,7 @@ impl<'a> ShapesPoolImpl<'a> { } self.modifiers = modifiers_with_refs; - let all_ids = shapes::all_with_ancestors(&ids, &self, true); + let all_ids = shapes::all_with_ancestors(&ids, self, true); for uuid in all_ids { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); @@ -287,7 +287,7 @@ impl<'a> ShapesPoolImpl<'a> { } self.structure = structure_with_refs; - let all_ids = shapes::all_with_ancestors(&ids, &self, true); + let all_ids = shapes::all_with_ancestors(&ids, self, true); for uuid in all_ids { if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); @@ -317,7 +317,9 @@ impl<'a> ShapesPoolImpl<'a> { } pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> { - let Some(shape) = self.get(id) else { panic!("Subtree not found"); }; + let Some(shape) = self.get(id) else { + panic!("Subtree not found"); + }; // TODO: Maybe create all_children_iter let all_children = shape.all_children(self, true, true); @@ -327,7 +329,9 @@ impl<'a> ShapesPoolImpl<'a> { let mut shapes_uuid_to_idx = HashMap::default(); for id in all_children.iter() { - let Some(shape) = self.get(id) else { panic!("Not found"); }; + let Some(shape) = self.get(id) else { + panic!("Not found"); + }; shapes.push(shape.clone()); let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) }; From f3b914534fb4deeefd58505f141318d75a7be181 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 30 Oct 2025 17:47:20 +0100 Subject: [PATCH 14/15] :sparkles: Add scale_content to shapes_pool --- frontend/src/app/render_wasm/api.cljs | 4 -- render-wasm/src/main.rs | 17 +++----- render-wasm/src/render.rs | 41 ++++-------------- render-wasm/src/shapes.rs | 25 +++++------ render-wasm/src/shapes/modifiers.rs | 16 +------ render-wasm/src/state.rs | 6 +-- render-wasm/src/state/shapes_pool.rs | 60 +++++++++++++++++++++------ 7 files changed, 75 insertions(+), 94 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index ae634630a9..36a00a67fb 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -982,10 +982,6 @@ [] (h/call wasm/internal-module "_clean_modifiers")) -(defn clean-geometry-modifiers - [] - (h/call wasm/internal-module "_clean_geometry_modifiers")) - (defn set-modifiers [modifiers] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 694c0c9901..9ecb93d6d6 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -541,6 +541,7 @@ pub extern "C" fn set_structure_modifiers() { with_state_mut!(state, { let mut structure = HashMap::new(); + let mut scale_content = HashMap::new(); for entry in entries { match entry.entry_type { StructureEntryType::ScaleContent => { @@ -548,7 +549,7 @@ pub extern "C" fn set_structure_modifiers() { continue; }; for id in shape.all_children(&state.shapes, true, true) { - state.scale_content.insert(id, entry.value); + scale_content.insert(id, entry.value); } } _ => { @@ -560,6 +561,9 @@ pub extern "C" fn set_structure_modifiers() { } } } + if !scale_content.is_empty() { + state.shapes.set_scale_content(scale_content); + } if !structure.is_empty() { state.shapes.set_structure(structure); } @@ -571,16 +575,7 @@ pub extern "C" fn set_structure_modifiers() { #[no_mangle] pub extern "C" fn clean_modifiers() { with_state_mut!(state, { - state.scale_content.clear(); - state.shapes.clean_modifiers(); - state.shapes.clean_structure(); - }); -} - -#[no_mangle] -pub extern "C" fn clean_geometry_modifiers() { - with_state_mut!(state, { - state.shapes.clean_modifiers(); + state.shapes.clean_all(); }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 78e032caaf..02dc4768fa 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -14,7 +14,7 @@ mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use gpu_state::GpuState; use options::RenderOptions; @@ -439,7 +439,6 @@ impl RenderState { pub fn render_shape( &mut self, shape: &Shape, - scale_content: Option<&f32>, clip_bounds: Option<(Rect, Option, Matrix)>, fills_surface_id: SurfaceId, strokes_surface_id: SurfaceId, @@ -449,12 +448,6 @@ impl RenderState { offset: Option<(f32, f32)>, parent_shadows: Option>, ) { - let shape = if let Some(scale_content) = scale_content { - &shape.scale_content(*scale_content) - } else { - shape - }; - let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32 @@ -819,12 +812,7 @@ impl RenderState { } } - pub fn start_render_loop( - &mut self, - tree: ShapesPoolRef, - scale_content: &HashMap, - timestamp: i32, - ) -> Result<(), String> { + pub fn start_render_loop(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> { let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); @@ -872,7 +860,7 @@ impl RenderState { self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); - self.process_animation_frame(tree, scale_content, timestamp)?; + self.process_animation_frame(tree, timestamp)?; performance::end_measure!("start_render_loop"); Ok(()) } @@ -880,13 +868,12 @@ impl RenderState { pub fn process_animation_frame( &mut self, tree: ShapesPoolRef, - scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { - self.render_shape_tree_partial(tree, scale_content, timestamp)?; + self.render_shape_tree_partial(tree, timestamp)?; } else { println!("Empty tree"); } @@ -956,12 +943,7 @@ impl RenderState { } #[inline] - pub fn render_shape_exit( - &mut self, - element: &Shape, - visited_mask: bool, - scale_content: Option<&f32>, - ) { + pub fn render_shape_exit(&mut self, element: &Shape, visited_mask: bool) { if visited_mask { // Because masked groups needs two rendering passes (first drawing // the content and then drawing the mask), we need to do an @@ -1016,7 +998,6 @@ impl RenderState { element_strokes.to_mut().clip_content = false; self.render_shape( &element_strokes, - scale_content, None, SurfaceId::Fills, SurfaceId::Strokes, @@ -1102,7 +1083,6 @@ impl RenderState { &mut self, shape: &Shape, shadow: &Shadow, - scale_content: Option<&f32>, clip_bounds: Option<(Rect, Option, Matrix)>, scale: f32, translation: (f32, f32), @@ -1152,7 +1132,6 @@ impl RenderState { self.render_shape( &plain_shape, - scale_content, clip_bounds, SurfaceId::DropShadows, SurfaceId::DropShadows, @@ -1169,7 +1148,6 @@ impl RenderState { pub fn render_shape_tree_partial_uncached( &mut self, tree: ShapesPoolRef, - scale_content: &HashMap, timestamp: i32, ) -> Result<(bool, bool), String> { let mut iteration = 0; @@ -1198,7 +1176,7 @@ impl RenderState { } if visited_children { - self.render_shape_exit(element, visited_mask, scale_content.get(&element.id)); + self.render_shape_exit(element, visited_mask); continue; } @@ -1260,7 +1238,6 @@ impl RenderState { self.render_drop_black_shadow( element, shadow, - scale_content.get(&element.id), clip_bounds, scale, translation, @@ -1280,7 +1257,6 @@ impl RenderState { self.render_drop_black_shadow( shadow_shape, shadow, - scale_content.get(&element.id), clip_bounds, scale, translation, @@ -1314,7 +1290,6 @@ impl RenderState { self.render_shape( shadow_shape, - scale_content.get(&element.id), clip_bounds, SurfaceId::DropShadows, SurfaceId::DropShadows, @@ -1352,7 +1327,6 @@ impl RenderState { self.render_shape( element, - scale_content.get(&element.id), clip_bounds, SurfaceId::Fills, SurfaceId::Strokes, @@ -1426,7 +1400,6 @@ impl RenderState { pub fn render_shape_tree_partial( &mut self, tree: ShapesPoolRef, - scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { let mut should_stop = false; @@ -1453,7 +1426,7 @@ impl RenderState { } else { performance::begin_measure!("render_shape_tree::uncached"); let (is_empty, early_return) = - self.render_shape_tree_partial_uncached(tree, scale_content, timestamp)?; + self.render_shape_tree_partial_uncached(tree, timestamp)?; if early_return { return Ok(()); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 6a9d0bddb5..adc8235b83 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -183,6 +183,7 @@ pub struct Shape { pub extrect: OnceCell, pub bounds: OnceCell, pub svg_transform: Option, + pub ignore_constraints: bool, } // Returns all ancestor shapes of this shape, traversing up the parent hierarchy @@ -265,30 +266,24 @@ impl Shape { extrect: OnceCell::new(), bounds: OnceCell::new(), svg_transform: None, + ignore_constraints: false, } } - pub fn scale_content(&self, value: f32) -> Self { - let mut result = self.clone(); - result.shape_type.scale_content(value); - result - .strokes - .iter_mut() - .for_each(|s| s.scale_content(value)); - result - .shadows - .iter_mut() - .for_each(|s| s.scale_content(value)); + pub fn scale_content(&mut self, value: f32) { + self.ignore_constraints = true; + self.shape_type.scale_content(value); + self.strokes.iter_mut().for_each(|s| s.scale_content(value)); - if let Some(blur) = result.blur.as_mut() { + self.shadows.iter_mut().for_each(|s| s.scale_content(value)); + + if let Some(blur) = self.blur.as_mut() { blur.scale_content(value); } - result - .layout_item + self.layout_item .iter_mut() .for_each(|i| i.scale_content(value)); - result } pub fn invalidate_extrect(&mut self) { diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 5bfd4781ca..f6c456ae59 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -23,7 +23,6 @@ fn propagate_children( parent_bounds_after: &Bounds, transform: Matrix, bounds: &HashMap, - scale_content: &HashMap, ) -> VecDeque { let children_ids = shape.children_ids(true); @@ -38,8 +37,6 @@ fn propagate_children( continue; }; - let ignore_constraints = scale_content.contains_key(child_id); - let child_bounds = bounds.find(child); let constraint_h = match &shape.shape_type { @@ -77,7 +74,7 @@ fn propagate_children( constraint_h, constraint_v, transform, - ignore_constraints, + child.ignore_constraints, ); result.push_back(Modifier::transform(*child_id, transform)); @@ -229,7 +226,6 @@ fn propagate_transform( &shape_bounds_after, transform, bounds, - &state.scale_content, ); entries.append(&mut children); } @@ -343,12 +339,6 @@ fn reflow_shape( let shapes = &state.shapes; - let shape = if let Some(scale_content) = state.scale_content.get(id) { - &shape.scale_content(*scale_content) - } else { - shape - }; - let Type::Frame(frame_data) = &shape.shape_type else { return; }; @@ -468,7 +458,6 @@ mod tests { &bounds_after, transform, &HashMap::new(), - &HashMap::new(), ); assert_eq!(result.len(), 1); @@ -499,8 +488,7 @@ mod tests { let parent = shapes.get(&parent_id).unwrap(); - let bounds = - calculate_group_bounds(parent, &shapes, &HashMap::new()).unwrap(); + let bounds = calculate_group_bounds(parent, &shapes, &HashMap::new()).unwrap(); assert_eq!(bounds.width(), 3.0); assert_eq!(bounds.height(), 3.0); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 54059ba70d..7cf42d3c74 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -23,7 +23,6 @@ pub(crate) struct State<'a> { pub text_editor_state: TextEditorState, pub current_id: Option, pub shapes: ShapesPool<'a>, - pub scale_content: HashMap, } impl<'a> State<'a> { @@ -33,7 +32,6 @@ impl<'a> State<'a> { text_editor_state: TextEditorState::new(), current_id: None, shapes: ShapesPool::new(), - scale_content: HashMap::new(), } } @@ -65,13 +63,13 @@ impl<'a> State<'a> { pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .start_render_loop(&self.shapes, &self.scale_content, timestamp)?; + .start_render_loop(&self.shapes, timestamp)?; Ok(()) } pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { self.render_state - .process_animation_frame(&self.shapes, &self.scale_content, timestamp)?; + .process_animation_frame(&self.shapes, timestamp)?; Ok(()) } diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index 9c0b8ff450..ca91b833d3 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -33,6 +33,7 @@ pub struct ShapesPoolImpl<'a> { modified_shape_cache: HashMap<&'a Uuid, OnceCell>, modifiers: HashMap<&'a Uuid, skia::Matrix>, structure: HashMap<&'a Uuid, Vec>, + scale_content: HashMap<&'a Uuid, f32>, } // Type aliases to avoid writing lifetimes everywhere @@ -50,6 +51,7 @@ impl<'a> ShapesPoolImpl<'a> { modified_shape_cache: HashMap::default(), modifiers: HashMap::default(), structure: HashMap::default(), + scale_content: HashMap::default(), } } @@ -162,6 +164,20 @@ impl<'a> ShapesPoolImpl<'a> { } } + // Rebuild scale_content with fresh references + if !self.scale_content.is_empty() { + let old_scale_content: Vec<(Uuid, f32)> = self + .scale_content + .drain() + .map(|(uuid_ref, scale)| (*uuid_ref, scale)) + .collect(); + + for (uuid, scale) in old_scale_content { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.scale_content.insert(uuid_ref, scale); + } + } + } // Rebuild modified_shape_cache with fresh references if !self.modified_shape_cache.is_empty() { let old_cache: Vec<(Uuid, OnceCell)> = self @@ -204,6 +220,7 @@ impl<'a> ShapesPoolImpl<'a> { let shape_ptr = &self.shapes[idx] as *const Shape; let modifiers_ptr = &self.modifiers as *const HashMap<&'a Uuid, skia::Matrix>; let structure_ptr = &self.structure as *const HashMap<&'a Uuid, Vec>; + let scale_content_ptr = &self.scale_content as *const HashMap<&'a Uuid, f32>; let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell>; // Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id @@ -212,15 +229,19 @@ impl<'a> ShapesPoolImpl<'a> { if self.to_update_bool(&*shape_ptr) || (*modifiers_ptr).contains_key(&id_ref) || (*structure_ptr).contains_key(&id_ref) + || (*scale_content_ptr).contains_key(&id_ref) { if let Some(cell) = (*cache_ptr).get(&id_ref) { Some(cell.get_or_init(|| { - let shape = &*shape_ptr; - shape.transformed( + let mut shape = (*shape_ptr).transformed( self, (*modifiers_ptr).get(&id_ref), (*structure_ptr).get(&id_ref), - ) + ); + if let Some(scale) = (*scale_content_ptr).get(&id_ref) { + shape.scale_content(*scale); + } + shape })) } else { Some(&*shape_ptr) @@ -240,12 +261,10 @@ impl<'a> ShapesPoolImpl<'a> { 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(); @@ -272,7 +291,6 @@ impl<'a> ShapesPoolImpl<'a> { } } - #[allow(dead_code)] pub fn set_structure(&mut self, structure: HashMap>) { // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and // Initialize the cache cells because later we don't want to have the mutable pointer @@ -295,16 +313,33 @@ impl<'a> ShapesPoolImpl<'a> { } } - #[allow(dead_code)] - pub fn clean_modifiers(&mut self) { - self.clean_shape_cache(); - self.modifiers = HashMap::default(); + pub fn set_scale_content(&mut self, scale_content: HashMap) { + // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and + // Initialize the cache cells because later we don't want to have the mutable pointer + let mut scale_content_with_refs = HashMap::with_capacity(scale_content.len()); + let mut ids = Vec::::new(); + + for (uuid, value) in scale_content { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + scale_content_with_refs.insert(uuid_ref, value); + ids.push(*uuid_ref); + } + } + self.scale_content = scale_content_with_refs; + + let all_ids = shapes::all_with_ancestors(&ids, self, true); + for uuid in all_ids { + if let Some(uuid_ref) = self.get_uuid_ref(&uuid) { + self.modified_shape_cache.insert(uuid_ref, OnceCell::new()); + } + } } - #[allow(dead_code)] - pub fn clean_structure(&mut self) { + pub fn clean_all(&mut self) { self.clean_shape_cache(); + self.modifiers = HashMap::default(); self.structure = HashMap::default(); + self.scale_content = HashMap::default(); } /// Get a reference to the Uuid stored in a shape, if it exists @@ -346,6 +381,7 @@ impl<'a> ShapesPoolImpl<'a> { modified_shape_cache: HashMap::default(), modifiers: HashMap::default(), structure: HashMap::default(), + scale_content: HashMap::default(), }; result.rebuild_references(); From de04026dc8102e58b37a9c3ea191ea31ec609432 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Fri, 31 Oct 2025 11:56:07 +0100 Subject: [PATCH 15/15] :sparkles: After review changes --- frontend/src/app/render_wasm/shape.cljs | 12 +++++------- render-wasm/src/shapes.rs | 2 +- render-wasm/src/state/shapes_pool.rs | 24 ------------------------ 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 922240dd29..3af716f0f2 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -259,10 +259,13 @@ (api/update-shape-tiles) (api/request-render "set-wasm-attrs"))))) +;; `conj` empty set initialization +(def conj* (fnil conj #{})) + (defn- impl-assoc [self k v] (when shape/*shape-changes* - (vswap! shape/*shape-changes* update (:id self) (fnil conj #{}) k)) + (vswap! shape/*shape-changes* update (:id self) conj* k)) (case k :id @@ -284,13 +287,8 @@ (defn- impl-dissoc [self k] - #_(when ^boolean shape/*wasm-sync* - (binding [shape/*wasm-sync* false] - (when (shape-in-current-page? (.-id ^ShapeProxy self)) - (set-wasm-attrs! self k nil)))) - (when shape/*shape-changes* - (vswap! shape/*shape-changes* update (:id self) (fnil conj #{}) k)) + (vswap! shape/*shape-changes* update (:id self) conj* k)) (case k :id diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index adc8235b83..c834d778f6 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1151,7 +1151,7 @@ impl Shape { pub fn apply_transform(&mut self, transform: &Matrix) { self.transform_selrect(transform); - // We don't need to invalidate this? we can just transform it + // TODO: See if we can change this invalidation to a transformation self.invalidate_extrect(); self.invalidate_bounds(); diff --git a/render-wasm/src/state/shapes_pool.rs b/render-wasm/src/state/shapes_pool.rs index ca91b833d3..f778f5faab 100644 --- a/render-wasm/src/state/shapes_pool.rs +++ b/render-wasm/src/state/shapes_pool.rs @@ -266,8 +266,6 @@ impl<'a> ShapesPoolImpl<'a> { } pub fn set_modifiers(&mut self, modifiers: HashMap) { - // self.clean_shape_cache(); - // Convert HashMap to HashMap<&'a Uuid, V> using references from shapes and // Initialize the cache cells because later we don't want to have the mutable pointer @@ -394,25 +392,3 @@ impl<'a> ShapesPoolImpl<'a> { shape.is_bool() } } - -// fn is_modified_child( -// shape: &Shape, -// shapes: ShapesPoolRef, -// modifiers: &HashMap, -// ) -> bool { -// if modifiers.is_empty() { -// return false; -// } -// -// let ids = shape.all_children(shapes, true, false); -// let default = &Matrix::default(); -// let parent_modifier = modifiers.get(&shape.id).unwrap_or(default); -// -// // Returns true if the transform of any child is different to the parent's -// ids.iter().any(|id| { -// !math::is_close_matrix( -// parent_modifier, -// modifiers.get(id).unwrap_or(&Matrix::default()), -// ) -// }) -// }