Merge pull request #7648 from penpot/alotor-performance-improvements

 Add performance improvements for wasm render
This commit is contained in:
Alejandro Alonso
2025-10-31 12:22:14 +01:00
committed by GitHub
26 changed files with 883 additions and 886 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,7 @@
[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.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]
@@ -203,21 +203,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
@@ -616,17 +621,20 @@
#_: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
(binding [shape/*wasm-sync* false]
(calculate-ignore-tree modif-tree objects))
(calculate-ignore-tree modif-tree objects)
options
(-> params
@@ -658,12 +666,34 @@
modifiers (dm/get-in modif-tree [shape-id :modifiers])]
(-> shape
(gsh/apply-transform transform)
(ctm/apply-structure-modifiers modifiers))))]
(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))))))
(ctm/apply-structure-modifiers modifiers))))
bool-ids
(into #{}
(comp
(mapcat (partial cfh/get-parents-with-self objects))
(filter cfh/bool-shape?)
(map :id))
ids)
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

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

@@ -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)
@@ -238,10 +238,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
@@ -293,7 +297,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
@@ -309,8 +313,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)
@@ -327,8 +331,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 %))
@@ -752,12 +756,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?))
@@ -772,14 +770,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

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

@@ -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]
@@ -1116,7 +1110,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)]

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)]
@@ -132,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))
@@ -226,58 +232,40 @@
(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")))))
;; `conj` empty set initialization
(def conj* (fnil conj #{}))
(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) conj* k))
(case k
:id
@@ -299,10 +287,9 @@
(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) 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);
});
}
@@ -289,15 +290,21 @@ pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) {
fn set_children_set(entries: IndexSet<Uuid>) {
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);
}
});
}
@@ -494,11 +501,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 {
@@ -537,6 +540,8 @@ pub extern "C" fn set_structure_modifiers() {
.collect();
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 => {
@@ -544,19 +549,24 @@ 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);
}
}
_ => {
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 !scale_content.is_empty() {
state.shapes.set_scale_content(scale_content);
}
if !structure.is_empty() {
state.shapes.set_structure(structure);
}
});
mem::free_bytes();
@@ -565,9 +575,7 @@ 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_all();
});
}
@@ -595,11 +603,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

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

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,9 +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,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shapes: ShapesPoolRef,
) -> 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(&current_path, &other_path);
@@ -422,26 +420,15 @@ pub fn bool_from_shapes(
current_path
}
pub fn update_bool_to_path(
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> 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)]
@@ -449,15 +436,15 @@ pub fn update_bool_to_path(
pub fn debug_render_bool_paths(
render_state: &mut RenderState,
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shapes: ShapesPoolRef,
_modifiers: &HashMap<Uuid, Matrix>,
_structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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(&current_path, &other_path);

View File

@@ -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;
@@ -22,16 +22,14 @@ 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, Type,
};
use crate::state::ShapesPool;
use crate::state::{ShapesPoolMutRef, ShapesPoolRef};
use crate::tiles::{self, PendingTiles, TileRect};
use crate::uuid::Uuid;
use crate::view::Viewbox;
use crate::wapi;
use crate::math;
use crate::math::bools;
use indexmap::IndexSet;
pub use fonts::*;
@@ -62,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.
@@ -77,7 +73,6 @@ impl NodeRenderState {
pub fn get_children_clip_bounds(
&self,
element: &Shape,
modifiers: Option<&Matrix>,
offset: Option<(f32, f32)>,
) -> Option<(Rect, Option<Corners>, Matrix)> {
if self.id.is_nil() || !element.clip() {
@@ -96,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,
@@ -118,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<Corners>, Matrix)> {
if self.id.is_nil() {
@@ -141,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,
@@ -274,28 +259,6 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
.into()
}
fn is_modified_child(
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
) -> 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.
@@ -475,11 +438,7 @@ impl RenderState {
#[allow(clippy::too_many_arguments)]
pub fn render_shape(
&mut self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
scale_content: Option<&f32>,
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
fills_surface_id: SurfaceId,
strokes_surface_id: SurfaceId,
@@ -489,12 +448,6 @@ impl RenderState {
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
) {
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
@@ -545,10 +498,6 @@ impl RenderState {
// We don't want to change the value in the global state
let mut shape: Cow<Shape> = 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 {
@@ -579,12 +528,12 @@ 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);
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 {
@@ -750,20 +699,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(_))
@@ -836,12 +772,7 @@ impl RenderState {
}
}
pub fn render_from_cache(
&mut self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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);
@@ -874,21 +805,14 @@ impl RenderState {
debug::render(self);
}
ui::render(self, shapes, modifiers, structure);
ui::render(self, shapes);
debug::render_wasm_label(self);
self.flush_and_submit();
}
}
pub fn start_render_loop(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
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);
@@ -936,29 +860,20 @@ 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, timestamp)?;
performance::end_measure!("start_render_loop");
Ok(())
}
pub fn process_animation_frame(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
tree: ShapesPoolRef,
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, timestamp)?;
} else {
println!("Empty tree");
}
@@ -1028,15 +943,7 @@ impl RenderState {
}
#[inline]
pub fn render_shape_exit(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
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
@@ -1090,11 +997,7 @@ 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,
SurfaceId::Fills,
SurfaceId::Strokes,
@@ -1140,13 +1043,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)
}
@@ -1183,12 +1081,8 @@ impl RenderState {
#[allow(clippy::too_many_arguments)]
fn render_drop_black_shadow(
&mut self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shape: &Shape,
shadow: &Shadow,
scale_content: Option<&f32>,
clip_bounds: Option<(Rect, Option<Corners>, Matrix)>,
scale: f32,
translation: (f32, f32),
@@ -1237,11 +1131,7 @@ impl RenderState {
.translate(translation);
self.render_shape(
shapes,
modifiers,
structure,
&plain_shape,
scale_content,
clip_bounds,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
@@ -1257,10 +1147,7 @@ impl RenderState {
pub fn render_shape_tree_partial_uncached(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(bool, bool), String> {
let mut iteration = 0;
@@ -1276,26 +1163,20 @@ 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.
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 {
self.render_shape_exit(
tree,
modifiers,
structure,
element,
visited_mask,
scale_content.get(&element.id),
);
self.render_shape_exit(element, visited_mask);
continue;
}
@@ -1303,18 +1184,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));
}
@@ -1359,12 +1236,8 @@ 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),
clip_bounds,
scale,
translation,
@@ -1377,20 +1250,13 @@ 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),
clip_bounds,
scale,
translation,
@@ -1423,11 +1289,7 @@ 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,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
@@ -1464,11 +1326,7 @@ impl RenderState {
.clear(skia::Color::TRANSPARENT);
self.render_shape(
tree,
modifiers,
structure,
element,
scale_content.get(&element.id),
clip_bounds,
SurfaceId::Fills,
SurfaceId::Strokes,
@@ -1505,14 +1363,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() {
@@ -1545,10 +1399,7 @@ impl RenderState {
pub fn render_shape_tree_partial(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
let mut should_stop = false;
@@ -1574,13 +1425,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, timestamp)?;
if early_return {
return Ok(());
}
@@ -1614,7 +1460,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
@@ -1657,29 +1503,20 @@ impl RenderState {
debug::render(self);
}
ui::render(self, tree, modifiers, structure);
ui::render(self, tree);
debug::render_wasm_label(self);
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)
@@ -1695,6 +1532,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);
}
}
@@ -1706,27 +1544,18 @@ impl RenderState {
self.tiles.remove_shape_at(tile, id);
}
pub fn rebuild_tiles_shallow(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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<Shape> = 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, 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);
let children = shape.children_ids(false);
for child_id in children.iter() {
nodes.push(*child_id);
}
@@ -1736,27 +1565,18 @@ impl RenderState {
performance::end_measure!("rebuild_tiles_shallow");
}
pub fn rebuild_tiles(
&mut self,
tree: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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<Shape> = 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, modifiers);
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);
}
@@ -1776,64 +1596,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

@@ -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::state::ShapesPool;
use crate::uuid::Uuid;
use crate::shapes::Shape;
use crate::state::ShapesPoolRef;
pub fn render_overlay(
zoom: f32,
canvas: &skia::Canvas,
shape: &Shape,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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);

View File

@@ -1,18 +1,9 @@
use skia_safe::{self as skia, Color4f};
use std::collections::HashMap;
use super::{RenderState, ShapesPool, SurfaceId};
use crate::math::Matrix;
use super::{RenderState, ShapesPoolRef, SurfaceId};
use crate::render::grid_layout;
use crate::shapes::StructureEntry;
use crate::uuid::Uuid;
pub fn render(
render_state: &mut RenderState,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) {
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);
}
}

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;
@@ -47,10 +47,11 @@ 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;
use crate::state::ShapesPool;
use crate::state::ShapesPoolRef;
const MIN_VISIBLE_SIZE: f32 = 2.0;
const ANTIALIAS_THRESHOLD: f32 = 15.0;
@@ -180,6 +181,62 @@ pub struct Shape {
pub shadows: Vec<Shadow>,
pub layout_item: Option<LayoutItem>,
pub extrect: OnceCell<math::Rect>,
pub bounds: OnceCell<math::Bounds>,
pub svg_transform: Option<Matrix>,
pub ignore_constraints: bool,
}
// 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,36 +264,36 @@ impl Shape {
shadows: Vec::with_capacity(1),
layout_item: None,
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) {
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);
}
@@ -250,6 +307,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(_))
}
@@ -266,6 +327,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);
@@ -328,6 +390,10 @@ impl Shape {
self.hidden = value;
}
pub fn svg_transform(&self) -> Option<Matrix> {
self.svg_transform
}
// FIXME: These arguments could be grouped or simplified
#[allow(clippy::too_many_arguments)]
pub fn set_flex_layout_child_data(
@@ -617,13 +683,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 +693,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 +719,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 +843,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 +854,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 +862,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 {
@@ -830,7 +877,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(),
@@ -839,7 +886,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
}
@@ -860,6 +907,7 @@ impl Shape {
self.children.first()
}
// TODO: Review this to use children_ids_iter instead
pub fn children_ids(&self, include_hidden: bool) -> IndexSet<Uuid> {
if include_hidden {
return self.children.clone().into_iter().rev().collect();
@@ -883,9 +931,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 +972,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 +979,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) {
@@ -1124,22 +1149,62 @@ impl Shape {
}
pub fn apply_transform(&mut self, transform: &Matrix) {
self.invalidate_extrect();
self.transform_selrect(transform);
// TODO: See if we can change this invalidation to a transformation
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);
}
} 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);
}
}
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(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,
shapes_pool: ShapesPoolRef,
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);
}
if self.is_bool() {
math_bools::update_bool_to_path(shape.to_mut(), shapes_pool)
}
shape.into_owned()
}
@@ -1195,43 +1260,6 @@ impl Shape {
.count()
}
/*
Returns the list of children taking into account the structure modifiers
*/
pub fn modified_children_ids(
&self,
structure: Option<&Vec<StructureEntry>>,
include_hidden: bool,
) -> IndexSet<Uuid> {
if let Some(structure) = structure {
let mut result: Vec<Uuid> =
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<Uuid> = result
.iter()
.filter(|id| !to_remove.contains(id))
.copied()
.collect();
ret
} else {
self.children_ids(include_hidden)
}
}
pub fn drop_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let drop_shadows: Vec<&Shadow> = self.drop_shadows_visible().collect();

View File

@@ -10,24 +10,21 @@ 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::{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,
bounds: &HashMap<Uuid, Bounds>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
scale_content: &HashMap<Uuid, f32>,
) -> VecDeque<Modifier> {
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();
@@ -40,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 {
@@ -79,7 +74,7 @@ fn propagate_children(
constraint_h,
constraint_v,
transform,
ignore_constraints,
child.ignore_constraints,
);
result.push_back(Modifier::transform(*child_id, transform));
@@ -88,18 +83,15 @@ 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() {
for child_id in shape.children_ids_iter(true) {
let Some(child) = shapes.get(child_id) else {
continue;
};
@@ -107,33 +99,29 @@ fn calculate_group_bounds(
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>>,
) -> Option<Bounds> {
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 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) {
@@ -238,8 +226,6 @@ fn propagate_transform(
&shape_bounds_after,
transform,
bounds,
&state.structure,
&state.scale_content,
);
entries.append(&mut children);
}
@@ -277,55 +263,56 @@ 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 }) => {
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);
reflow_parent = true;
}
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, modifiers) {
bounds.insert(shape.id, shape_bounds);
reflow_parent = true;
}
reflown.insert(*id);
}
_ => {
// Other shapes don't have to be reflown
@@ -352,35 +339,17 @@ 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;
};
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);
@@ -396,12 +365,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::<Uuid, Matrix>::new();
let mut bounds = HashMap::<Uuid, Bounds>::new();
let mut reflown = HashSet::<Uuid>::new();
@@ -456,6 +419,7 @@ mod tests {
use crate::math::{Matrix, Point};
use crate::shapes::*;
use crate::state::ShapesPool;
#[test]
fn test_propagate_shape() {
@@ -494,8 +458,6 @@ mod tests {
&bounds_after,
transform,
&HashMap::new(),
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(result.len(), 1);
@@ -526,8 +488,7 @@ mod tests {
let parent = shapes.get(&parent_id).unwrap();
let bounds =
calculate_group_bounds(parent, &shapes, &HashMap::new(), &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);

View File

@@ -2,9 +2,9 @@
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::ShapesPool;
use crate::state::ShapesPoolRef;
use crate::uuid::Uuid;
use std::collections::{HashMap, VecDeque};
@@ -179,13 +179,12 @@ 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> {
let mut tracks = Vec::<TrackData>::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() {
@@ -433,9 +432,8 @@ 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> {
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);
@@ -574,22 +571,13 @@ 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> {
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::<f32>();

View File

@@ -2,9 +2,9 @@ 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::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,9 +602,7 @@ pub fn create_cell_data<'a>(
pub fn grid_cell_data<'a>(
shape: &Shape,
shapes: &'a ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
shapes: ShapesPoolRef<'a>,
allow_empty: bool,
) -> Vec<CellData<'a>> {
let Type::Frame(Frame {
@@ -616,26 +614,8 @@ pub fn grid_cell_data<'a>(
};
let bounds = &mut HashMap::<Uuid, Bounds>::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,
@@ -723,13 +703,12 @@ 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> {
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,

View File

@@ -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::ShapesPool;
use crate::uuid::Uuid;
use std::collections::HashMap;
use crate::state::ShapesPoolRef;
const BEZIER_CIRCLE_C: f32 = 0.551_915_05;
pub trait ToPath {
fn to_path(
&self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path;
fn to_path(&self, shapes: ShapesPoolRef) -> Path;
}
enum CornerType {
@@ -180,34 +171,28 @@ fn transform_segments(segments: Vec<Segment>, shape: &Shape) -> Vec<Segment> {
}
impl ToPath for Shape {
fn to_path(
&self,
shapes: &ShapesPool,
modifiers: &HashMap<Uuid, Matrix>,
structure: &HashMap<Uuid, Vec<StructureEntry>>,
) -> Path {
let shape = self.transformed(modifiers.get(&self.id));
match shape.shape_type {
fn to_path(&self, shapes: ShapesPoolRef) -> Path {
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.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 = shape.modified_children_ids(structure.get(&shape.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();
@@ -215,13 +200,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 +217,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,12 +3,11 @@ 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;
use crate::shapes::Shape;
use crate::shapes::StructureEntry;
use crate::tiles;
use crate::uuid::Uuid;
@@ -19,26 +18,20 @@ 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 modifiers: HashMap<Uuid, skia::Matrix>,
pub scale_content: HashMap<Uuid, f32>,
pub structure: HashMap<Uuid, Vec<StructureEntry>>,
pub shapes: ShapesPool<'a>,
}
impl State {
impl<'a> State<'a> {
pub fn new(width: i32, height: i32) -> Self {
State {
render_state: RenderState::new(width, height),
text_editor_state: TextEditorState::new(),
current_id: None,
shapes: ShapesPool::new(),
modifiers: HashMap::new(),
scale_content: HashMap::new(),
structure: HashMap::new(),
}
}
@@ -65,29 +58,18 @@ impl State {
}
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, 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, timestamp)?;
Ok(())
}
@@ -110,12 +92,16 @@ impl State {
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, &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);
@@ -148,6 +134,8 @@ impl State {
panic!("Invalid current shape")
};
shape.set_parent(id);
// TODO this clone doesn't seem necessary
shape.clone()
};
@@ -157,28 +145,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);
}
}
@@ -186,25 +155,32 @@ impl State {
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, &self.modifiers);
.update_tile_for(&shape.clone(), &self.shapes);
}
}
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) {
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 {
@@ -216,7 +192,7 @@ impl State {
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 = &[
@@ -235,4 +211,8 @@ impl State {
None
}
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
self.shapes.set_modifiers(modifiers);
}
}

View File

@@ -2,14 +2,20 @@ use std::collections::HashMap;
use std::iter;
use crate::performance;
use crate::shapes;
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 +24,34 @@ 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>>,
scale_content: HashMap<&'a Uuid, f32>,
}
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(),
scale_content: HashMap::default(),
}
}
@@ -43,22 +65,133 @@ 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 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<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 +199,57 @@ 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 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<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 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 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)
}
} else {
Some(&*shape_ptr)
}
}
}
#[allow(dead_code)]
@@ -87,4 +260,135 @@ impl ShapesPool {
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Shape> {
self.shapes.iter_mut()
}
fn clean_shape_cache(&mut self) {
self.modified_shape_cache.clear()
}
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
// 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 ids = Vec::<Uuid>::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());
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());
}
}
}
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());
let mut ids = Vec::<Uuid>::new();
for (uuid, entries) in structure {
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
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());
}
}
}
pub fn set_scale_content(&mut self, scale_content: HashMap<Uuid, f32>) {
// 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 scale_content_with_refs = HashMap::with_capacity(scale_content.len());
let mut ids = Vec::<Uuid>::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());
}
}
}
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
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)) }
}
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(),
scale_content: 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.
shape.is_bool()
}
}

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

View File

@@ -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::<RawSegmentData>::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()

View File

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