Compare commits

...

10 Commits

Author SHA1 Message Date
Alejandro Alonso
f5eb09ca58 🐛 Fig shapes pool extending size 2025-10-27 16:24:22 +01:00
Alejandro Alonso
78f4787e22 Improve shapes pool performance 2025-10-27 13:17:00 +01:00
alonso.torres
621aee75e1 Changes to modifiers 2025-10-27 13:15:57 +01:00
alonso.torres
513e7374cb Improve performance of group bounds 2025-10-24 12:07:16 +02:00
alonso.torres
7459239639 Store bounds inside the shape 2025-10-24 12:07:16 +02:00
alonso.torres
4b8f4d2d5b Removed all_ancestors traversals 2025-10-24 12:07:16 +02:00
alonso.torres
d50da7ed66 Removed method set_selrect_for_current_shape 2025-10-24 12:07:16 +02:00
alonso.torres
eb735c4cd1 Change internal data type for tiles 2025-10-24 12:07:16 +02:00
alonso.torres
caf250a84e Improved performance of children ancestors 2025-10-24 12:07:16 +02:00
alonso.torres
48ecee8c35 Changes WASM serialization mechanism 2025-10-24 12:07:16 +02:00
21 changed files with 617 additions and 394 deletions

View File

@@ -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)

View File

@@ -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))

View File

@@ -7,14 +7,18 @@
(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.shape :as wasm.shape]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
@@ -99,7 +103,21 @@
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])]
(wasm.shape/process-shape-changes! objects @shape-changes))
state)
;; wasm renderer deactivated
(update-in state [:files file-id :data] apply-changes))))))
(defn commit
"Create a commit event instance"

View File

@@ -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]
@@ -227,21 +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
(doseq [[id properties] wasm-props]
(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)))))))
;; 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
@@ -649,8 +653,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

View File

@@ -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)]

View File

@@ -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))

View File

@@ -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)))

View File

@@ -120,8 +120,11 @@
(-write writer (str "#penpot/shape " (:id delegate)))))
;; --- SHAPE IMPL
(defn- set-wasm-single-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)]
@@ -226,58 +229,37 @@
(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)
(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)]
(if (shape-in-current-page? shape-id)
(do
(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"))))))))
(->> 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]
(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 +281,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

View File

@@ -20,10 +20,11 @@ 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;
pub(crate) static mut STATE: Option<Box<State>> = None;
pub(crate) static mut STATE: Option<Box<State<'static>>> = None;
#[macro_export]
macro_rules! with_state_mut {
@@ -253,8 +254,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);
});
}
@@ -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::<Uuid>::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);
});
}

View File

@@ -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<Segment> {
pub fn bool_from_shapes(
bool_type: BoolType,
children_ids: &IndexSet<Uuid>,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path {
@@ -424,7 +424,7 @@ pub fn bool_from_shapes(
pub fn update_bool_to_path(
shape: &Shape,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> 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<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {

View File

@@ -22,9 +22,10 @@ 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::state::{ShapesPoolMutRef, ShapesPoolRef};
use crate::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid;
use crate::view::Viewbox;
@@ -275,7 +276,7 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
fn is_modified_child(
shape: &Shape,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
) -> bool {
if modifiers.is_empty() {
@@ -474,7 +475,7 @@ impl RenderState {
#[allow(clippy::too_many_arguments)]
pub fn render_shape(
&mut self,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
@@ -833,7 +834,7 @@ impl RenderState {
pub fn render_from_cache(
&mut self,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
@@ -878,7 +879,7 @@ impl RenderState {
pub fn start_render_loop(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
@@ -938,7 +939,7 @@ impl RenderState {
pub fn process_animation_frame(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
@@ -1021,7 +1022,7 @@ impl RenderState {
#[inline]
pub fn render_shape_exit(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
element: &Shape,
@@ -1133,13 +1134,8 @@ impl RenderState {
self.get_rect_bounds(rect)
}
pub fn get_shape_extrect_bounds(
&mut self,
shape: &Shape,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> Rect {
let rect = shape.extrect(tree, modifiers);
pub fn get_shape_extrect_bounds(&mut self, shape: &Shape, tree: ShapesPoolRef) -> Rect {
let rect = shape.extrect(tree);
self.get_rect_bounds(rect)
}
@@ -1176,7 +1172,7 @@ impl RenderState {
#[allow(clippy::too_many_arguments)]
fn render_drop_black_shadow(
&mut self,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
@@ -1250,7 +1246,7 @@ impl RenderState {
pub fn render_shape_tree_partial_uncached(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
@@ -1277,7 +1273,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 {
@@ -1296,18 +1292,14 @@ impl RenderState {
let transformed_element: Cow<Shape> = 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));
}
@@ -1538,7 +1530,7 @@ impl RenderState {
pub fn render_shape_tree_partial(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
@@ -1656,23 +1648,14 @@ impl RenderState {
Ok(())
}
pub fn get_tiles_for_shape(
&mut self,
shape: &Shape,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> 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(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<Uuid, Matrix>,
) {
let TileRect(rsx, rsy, rex, rey) = self.get_tiles_for_shape(shape, tree, modifiers);
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<tiles::Tile> = self
.tiles
.get_tiles_of(shape.id)
@@ -1701,7 +1684,7 @@ impl RenderState {
pub fn rebuild_tiles_shallow(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
@@ -1716,7 +1699,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);
@@ -1731,7 +1714,7 @@ impl RenderState {
pub fn rebuild_tiles(
&mut self,
tree: &ShapesPool,
tree: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
@@ -1746,7 +1729,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);
@@ -1769,64 +1752,26 @@ impl RenderState {
pub fn invalidate_and_update_tiles(
&mut self,
shape_ids: &IndexSet<Uuid>,
tree: &mut ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
tree: ShapesPoolMutRef<'_>,
) {
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);
}
}
}
}
/// 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<Uuid, Matrix>,
) {
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.
/// 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<Uuid, Matrix>,
) {
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<Shape> = Cow::Borrowed(shape);
shape
};
shape.to_mut().apply_transform(matrix);
ancestors.insert(*uuid);
ancestors.extend(shape.all_ancestors(tree, false));
}
self.invalidate_and_update_tiles(&ancestors, tree, modifiers);
pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec<Uuid>) {
let ancestors = all_with_ancestors(&ids, tree, false);
self.invalidate_and_update_tiles(&ancestors, tree);
}
pub fn get_scale(&self) -> f32 {

View File

@@ -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<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {

View File

@@ -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<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {

View File

@@ -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;
@@ -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;
@@ -180,6 +180,60 @@ pub struct Shape {
pub shadows: Vec<Shadow>,
pub layout_item: Option<LayoutItem>,
pub extrect: OnceCell<math::Rect>,
pub bounds: OnceCell<math::Bounds>,
}
// 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: ShapesPoolRef,
include_hidden: bool,
) -> IndexSet<Uuid> {
let mut pending = Vec::from_iter(shapes.iter());
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 {
@@ -207,6 +261,7 @@ impl Shape {
shadows: Vec::with_capacity(1),
layout_item: None,
extrect: OnceCell::new(),
bounds: OnceCell::new(),
}
}
@@ -237,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);
}
@@ -266,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);
@@ -617,13 +677,8 @@ impl Shape {
self.selrect.width()
}
pub fn visually_insignificant(
&self,
scale: f32,
shapes_pool: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> bool {
let extrect = self.extrect(shapes_pool, modifiers);
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
}
@@ -632,8 +687,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()),
@@ -659,18 +713,18 @@ impl Shape {
bounds
}
pub fn bounds(&self) -> Bounds {
*self.bounds.get_or_init(|| self.calculate_bounds())
}
pub fn selrect(&self) -> math::Rect {
self.selrect
}
pub fn extrect(
&self,
shapes_pool: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> math::Rect {
pub fn extrect(&self, shapes_pool: ShapesPoolRef) -> 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 {
@@ -783,8 +837,7 @@ impl Shape {
fn apply_children_bounds(
&self,
mut rect: math::Rect,
shapes_pool: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
shapes_pool: ShapesPoolRef,
) -> math::Rect {
let include_children = match self.shape_type {
Type::Group(_) => true,
@@ -795,15 +848,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<Shape> = 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));
}
}
}
@@ -811,12 +856,8 @@ impl Shape {
rect
}
pub fn calculate_extrect(
&self,
shapes_pool: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> math::Rect {
let shape = self.transformed(modifiers.get(&self.id));
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());
let mut rect = match &shape.shape_type {
@@ -839,7 +880,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
}
@@ -883,9 +924,27 @@ impl Shape {
}
}
pub fn children_ids_iter(&self, include_hidden: bool) -> Box<dyn Iterator<Item = &Uuid> + '_> {
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,
shapes: ShapesPoolRef,
include_hidden: bool,
include_self: bool,
) -> IndexSet<Uuid> {
@@ -906,47 +965,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<Uuid> {
let mut ancestors = IndexSet::new();
let mut current_id = self.id;
// Traverse upwards using parent_id
while let Some(parent_id) = shapes.get(&current_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());
@@ -954,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(&current_id).and_then(|s| s.parent_id) {
@@ -1122,8 +1140,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);
@@ -1133,11 +1155,41 @@ impl Shape {
}
}
pub fn transformed(&self, transform: Option<&Matrix>) -> Self {
pub fn apply_structure(&mut self, structure: &Vec<StructureEntry>) {
let mut result: Vec<Uuid> = 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<StructureEntry>>,
) -> Self {
let mut shape: Cow<Shape> = 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()
}
@@ -1230,6 +1282,36 @@ impl Shape {
}
}
pub fn modified_children_ids_iter<'a>(
&'a self,
structure: Option<&'a Vec<StructureEntry>>,
include_hidden: bool,
) -> Box<dyn Iterator<Item = Cow<'a, Uuid>> + 'a> {
if let Some(structure) = structure {
let mut result: Vec<Cow<'a, Uuid>> = self
.children_ids_iter(include_hidden)
.map(Cow::Borrowed)
.collect();
let mut to_remove = HashSet::<Cow<'a, Uuid>>::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<skia_safe::Paint> {
let drop_shadows: Vec<&Shadow> = self.drop_shadows_visible().collect();

View File

@@ -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,
@@ -88,32 +88,29 @@ fn propagate_children(
result
}
// FIXME: PERFORMANCE
fn calculate_group_bounds(
shape: &Shape,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Option<Bounds> {
let shape_bounds = bounds.find(shape);
let mut result = Vec::<Point>::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)
}
fn calculate_bool_bounds(
shape: &Shape,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
@@ -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
@@ -462,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();
@@ -505,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();

View File

@@ -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<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Vec<TrackData> {
@@ -433,7 +433,7 @@ fn calculate_track_data(
layout_data: &LayoutData,
flex_data: &FlexData,
layout_bounds: &Bounds,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Vec<TrackData> {
@@ -574,7 +574,7 @@ pub fn reflow_flex_layout(
shape: &Shape,
layout_data: &LayoutData,
flex_data: &FlexData,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &mut HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> VecDeque<Modifier> {

View File

@@ -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<GridCell>,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) -> Vec<TrackData> {
let layout_size = if is_column {
@@ -122,7 +122,7 @@ fn set_auto_base_size(
column: bool,
tracks: &mut [TrackData],
cells: &Vec<GridCell>,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
bounds: &HashMap<Uuid, Bounds>,
) {
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<Uuid, Bounds>,
) {
// 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<Uuid, Bounds>,
) {
// Remove groups without flex
@@ -539,7 +539,7 @@ fn cell_bounds(
pub fn create_cell_data<'a>(
layout_bounds: &Bounds,
children: &IndexSet<Uuid>,
shapes: &'a ShapesPool,
shapes: ShapesPoolRef<'a>,
cells: &Vec<GridCell>,
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<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
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<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> VecDeque<Modifier> {

View File

@@ -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<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path;
@@ -182,15 +182,14 @@ fn transform_segments(segments: Vec<Segment>, shape: &Shape) -> Vec<Segment> {
impl ToPath for Shape {
fn to_path(
&self,
shapes: &ShapesPool,
shapes: ShapesPoolRef,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> 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))
}
}
}

View File

@@ -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<Uuid>,
pub shapes: ShapesPool,
pub shapes: ShapesPool<'a>,
pub modifiers: HashMap<Uuid, skia::Matrix>,
pub scale_content: HashMap<Uuid, f32>,
pub structure: HashMap<Uuid, Vec<StructureEntry>>,
}
impl State {
impl<'a> State<'a> {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
@@ -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);
@@ -157,28 +156,9 @@ 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
.update_tile_for(shape, &self.shapes, &self.modifiers);
self.render_state.update_tile_for(shape, &self.shapes);
}
}
@@ -188,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);
}
}
@@ -202,9 +182,17 @@ impl State {
.rebuild_tiles(&self.shapes, &self.modifiers, &self.structure);
}
pub fn rebuild_modifier_tiles(&mut self) {
self.render_state
.rebuild_modifier_tiles(&mut self.shapes, &self.modifiers);
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
// 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 {
@@ -235,4 +223,8 @@ impl State {
None
}
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
self.shapes.set_modifiers(modifiers);
}
}

View File

@@ -5,11 +5,16 @@ 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.
///
/// `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.
///
@@ -18,18 +23,32 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
/// Shapes are stored in a `Vec<Shape>`, which keeps the `Shape` instances
/// in a contiguous memory block.
///
pub struct ShapesPool {
pub struct ShapesPoolImpl<'a> {
shapes: Vec<Shape>,
shapes_uuid_to_idx: HashMap<Uuid, usize>,
counter: usize,
shapes_uuid_to_idx: HashMap<&'a Uuid, usize>,
modified_shape_cache: HashMap<&'a Uuid, OnceCell<Shape>>,
modifiers: HashMap<&'a Uuid, skia::Matrix>,
structure: HashMap<&'a Uuid, Vec<StructureEntry>>,
}
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(),
modified_shape_cache: HashMap::default(),
modifiers: HashMap::default(),
structure: HashMap::default(),
}
}
@@ -43,22 +62,119 @@ impl ShapesPool {
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));
}
let new_shape = &mut self.shapes[self.counter];
will_reallocate
} else {
false
};
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 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;
new_shape
// 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<StructureEntry>)> = 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<Shape>)> = 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 {
@@ -66,17 +182,48 @@ 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)?;
Some(&self.shapes[idx])
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<StructureEntry>>;
let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell<Shape>>;
// 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 {
Some(&*shape_ptr)
}
}
}
#[allow(dead_code)]
@@ -87,4 +234,60 @@ 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<Uuid, skia::Matrix>) {
// self.clean_shape_cache();
// Convert HashMap<Uuid, V> 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_with_refs;
}
#[allow(dead_code)]
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
// Convert HashMap<Uuid, V> 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_with_refs;
}
#[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();
}
/// 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)) }
}
}

View File

@@ -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<Tile, IndexSet<Uuid>>,
grid: HashMap<Tile, HashSet<Uuid>>,
index: HashMap<Uuid, HashSet<Tile>>,
}
@@ -126,13 +125,13 @@ impl TileHashMap {
}
}
pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&IndexSet<Uuid>> {
pub fn get_shapes_at(&mut self, tile: Tile) -> Option<&HashSet<Uuid>> {
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) {