Improve drag performance avoiding unnecessary modifiers

This commit is contained in:
Elena Torró 2026-05-19 09:44:58 +02:00 committed by GitHub
parent 44f4c43f15
commit 8dd4b486e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 125 additions and 67 deletions

View File

@ -629,34 +629,10 @@
(ptk/reify ::set-temporary-modifiers
ptk/EffectEvent
(effect [_ _ _]
(rx/push! ms/wasm-modifiers modifiers))))
(rx/push! ms/wasm-modifiers (into {} modifiers)))))
(def ^:private xf:map-key (map key))
(defn- expand-translation-entry
"Expand one translation-only geometry entry into [descendant-id matrix]
pairs covering the moved shape's full subtree (every descendant gets
the same matrix)."
[[id data] objects subtree-ids-by-id]
(let [m (:transform data)
sub (or (get subtree-ids-by-id id)
(cfh/get-children-ids-with-self objects id))]
(map (fn [sid] [sid m]) sub)))
(defn- expand-translation-modifiers
"Pure translation propagates as identity to descendants: every shape in
the subtree gets the same matrix. Builds the flat [id matrix] list
directly, skipping the WASM tree walk + FFI roundtrip used by
`propagate-modifiers` for the general (resize/rotate) case.
Only safe when pixel-snap is off: WASM applies pixel correction
per-shape (different scale/translation per descendant), which we
can't replicate cheaply on the CLJS side."
[geometry-entries objects subtree-ids-by-id]
(into []
(mapcat #(expand-translation-entry % objects subtree-ids-by-id))
geometry-entries))
(defn- translate-selrect
"Shift `selrect`'s center by (tx, ty). Width/height/transform are
invariant under pure translation, so only `:center` moves."
@ -688,11 +664,12 @@
(ptk/reify ::set-wasm-modifiers
ptk/UpdateEvent
(update [_ state]
(let [property-changes
(extract-property-changes modif-tree)]
(-> state
(assoc :prev-wasm-props (:wasm-props state))
(assoc :wasm-props property-changes))))
(let [property-changes (extract-property-changes modif-tree)]
(if (d/not-empty? property-changes)
(-> state
(assoc :prev-wasm-props (:wasm-props state))
(assoc :wasm-props property-changes))
state)))
ptk/WatchEvent
(watch [_ state _]
@ -702,29 +679,26 @@
;; thread is not blocked. The pair is closed in
;; `clear-local-transform`.
(ensure-interactive-transform-start!)
(wasm.api/clean-modifiers)
(let [prev-wasm-props (:prev-wasm-props state)
wasm-props (:wasm-props state)
objects (dsh/lookup-page-objects state)
snap-pixel?
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
(let [snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))
translation? (every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
translation?
(every? #(ctm/only-move? (:modifiers %)) (vals modif-tree))]
(set-wasm-props! objects prev-wasm-props wasm-props)
;; Only geometry (transform matrix) changes during drag.
(when-not translation?
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree)))
(let [objects (dsh/lookup-page-objects state)]
(set-wasm-props! objects (:prev-wasm-props state) (:wasm-props state))
(wasm.api/clean-modifiers)
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree))))
(let [geometry-entries (parse-geometry-modifiers modif-tree)
root-modifiers (into [] (map (fn [[id data]] [id (:transform data)])) geometry-entries)
modifiers
(if (and translation? (not snap-pixel?))
(expand-translation-modifiers geometry-entries objects subtree-ids-by-id)
root-modifiers
(wasm.api/propagate-modifiers geometry-entries snap-pixel?))]
(wasm.api/set-modifiers modifiers)
(let [ids (into [] xf:map-key geometry-entries)
selrect
(if (and translation? (not snap-pixel?) selection-rect-cache (seq modifiers))
(cached-translation-selrect ids (second (first modifiers)) selection-rect-cache)
(wasm.api/get-selection-rect ids))]
(let [ids (into [] xf:map-key geometry-entries)
selrect (if (and translation? (not snap-pixel?) selection-rect-cache (seq modifiers))
(cached-translation-selrect ids (second (first modifiers)) selection-rect-cache)
(wasm.api/get-selection-rect ids))]
(rx/of (set-temporary-selrect selrect)
(set-temporary-modifiers modifiers))))))))

View File

@ -310,7 +310,7 @@
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
(hooks/setup-keyboard alt? mod? space? z? shift?)
(hooks/setup-hover-shapes page-id move-stream base-objects selected mod? hover measure-hover
hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?)
hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only? transform)
(hooks/setup-viewport-modifiers modifiers base-objects)
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)

View File

@ -177,13 +177,14 @@
(dw/increase-zoom)))))))
(defn setup-hover-shapes
[page-id move-stream objects selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures? read-only?]
[page-id move-stream objects selected mod? hover measure-hover hover-ids hover-top-frame-id hover-disabled? focus zoom show-measures? read-only? transform]
(let [;; We use ref so we don't recreate the stream on a change
zoom-ref (mf/use-ref zoom)
mod-ref (mf/use-ref @mod?)
selected-ref (mf/use-ref selected)
hover-disabled-ref (mf/use-ref hover-disabled?)
focus-ref (mf/use-ref focus)
transform-ref (mf/use-ref transform)
last-point-ref (mf/use-var nil)
mod-str (mf/use-memo #(rx/subject))
@ -251,6 +252,10 @@
(mf/deps focus)
#(mf/set-ref-val! focus-ref focus))
(mf/use-effect
(mf/deps transform)
#(mf/set-ref-val! transform-ref transform))
(hooks/use-stream
over-shapes-stream-debounced
(mf/deps objects)
@ -361,7 +366,9 @@
(get objects)))]
(reset! hover hover-shape)
(reset! measure-hover measure-hover-shape)
(reset! hover-ids ids)))
;; Skip hover-ids update during drag
(when (not= :move (mf/ref-val transform-ref))
(reset! hover-ids ids))))
(fn []
;; Clean the cache

View File

@ -77,7 +77,7 @@
(defn apply-modifiers-to-selected
[selected objects modifiers]
(apply-modifiers-to-objects objects (select-keys (into {} modifiers) selected)))
(apply-modifiers-to-objects objects (select-keys modifiers selected)))
(defn- apply-wasm-modifiers-to-ids
"Like `apply-modifiers-to-objects`, but only updates ids in `id-set`. During WASM
@ -87,13 +87,15 @@
(if (or (empty? wasm-modifiers) (empty? id-set))
objects
(reduce
(fn [objs pair]
(let [[id t] pair]
(if (and (contains? id-set id) (contains? objs id))
(fn [objs id]
(if-let [t (get wasm-modifiers id)]
(if (contains? objs id)
(update objs id gsh/apply-transform t)
objs)))
objs)
objs))
objects
wasm-modifiers)))
id-set)))
(defn- outline-wasm-source-ids
"Superset of shape ids that `shape-outlines` may look up (all outline usages here)."
@ -142,7 +144,6 @@
drawing (mf/deref refs/workspace-drawing)
focus (mf/deref refs/workspace-focus-selected)
wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
workspace-editor-state (mf/deref refs/workspace-editor-state)
file-id (get file :id)
@ -162,7 +163,6 @@
selected-shapes (->> selected
(into [] (keep (d/getf objects-modified)))
(not-empty))
;; STATE
alt? (mf/use-state false)
shift? (mf/use-state false)
@ -370,6 +370,7 @@
offset-y (if selecting-first-level-frame?
(:y first-shape)
(:y selected-frame))
rule-area-size (/ rulers/ruler-area-size zoom)
preview-blend (-> refs/workspace-preview-blend
(mf/deref))
@ -495,7 +496,7 @@
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool path-drawing? path-editing? z? read-only?)
(hooks/setup-keyboard alt? mod? space? z? shift?)
(hooks/setup-hover-shapes page-id move-stream base-objects selected mod? hover measure-hover
hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only?)
hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures? read-only? transform)
(hooks/setup-shortcuts path-editing? path-drawing? text-editing? grid-editing?)
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
@ -613,11 +614,10 @@
:ref text-editor-ref}]))
(when show-frame-outline?
(let [outlined-frame-id
(->> @hover-ids
(filter #(cfh/frame-shape? (get base-objects %)))
(remove selected)
(last))
(let [outlined-frame-id (->> @hover-ids
(filter #(cfh/frame-shape? (get base-objects %)))
(remove selected)
(last))
outlined-frame (get objects outlined-frame-id)]
[:*
[:& outline/shape-outlines

View File

@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::collections::VecDeque;
use std::iter;
use crate::performance;
@ -191,6 +192,15 @@ impl ShapesPoolImpl {
Some(shape)
}
} else {
if let Some(cell) = self.modified_shape_cache.get(&idx) {
return Some(cell.get_or_init(|| {
if let Some(m) = self.find_nearest_ancestor_modifier(idx) {
shape.transformed(Some(&m), None)
} else {
shape.clone()
}
}));
}
Some(shape)
}
}
@ -230,9 +240,6 @@ impl ShapesPoolImpl {
}
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
// Initialize the cache cells for affected shapes
let mut ids = Vec::<Uuid>::new();
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
@ -242,12 +249,47 @@ impl ShapesPoolImpl {
ids.push(uuid);
}
}
// Expand every root modifier to its full descendant subtree.
// When CLJS sends only root shapes (translation on drag), descendants
// need the same matrix.
// For resize/rotate, propagate-modifiers already includes all descendants.
// Descendants are NOT pushed into `ids` / `modifier_uuids`: tile invalidation
// via rebuild_modifier_tiles only runs for roots, which is sufficient because
// descendants always lie inside the parent's bounding box and are therefore
// covered by the parent's old/new tile ranges.
let root_pairs: Vec<(usize, skia::Matrix)> = ids
.iter()
.filter_map(|uuid| {
let idx = self.uuid_to_idx.get(uuid).copied()?;
let matrix = modifiers_with_idx.get(&idx).copied()?;
Some((idx, matrix))
})
.collect();
let mut descendants_idxs: Vec<usize> = Vec::new();
for (root_idx, matrix) in root_pairs {
for descendant_idx in self.collect_all_descendants(root_idx) {
if let std::collections::hash_map::Entry::Vacant(e) =
modifiers_with_idx.entry(descendant_idx)
{
e.insert(matrix);
descendants_idxs.push(descendant_idx);
}
}
}
self.modifiers = modifiers_with_idx;
for descendant_idx in descendants_idxs {
self.modified_shape_cache
.insert(descendant_idx, OnceCell::new());
}
// Compute ancestors before consuming `ids` so we can move it into
// `modifier_uuids` without a clone.
let all_ids = shapes::all_with_ancestors(&ids, self, true);
// Keep modifier_uuids in sync so modifier_ids() is O(K) not O(N_shapes).
// rebuild_modifier_tiles doesn't process every descendant individually.
self.modifier_uuids = ids;
for uuid in all_ids {
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
@ -363,6 +405,41 @@ impl ShapesPoolImpl {
}
}
fn collect_all_descendants(&self, idx: usize) -> Vec<usize> {
let mut result = Vec::new();
let mut queue: VecDeque<&Uuid> = VecDeque::new();
let shape = &self.shapes[idx];
for child_id in shape.children_ids_iter(false) {
queue.push_back(child_id);
}
while let Some(child_id) = queue.pop_front() {
if let Some(&child_idx) = self.uuid_to_idx.get(child_id) {
result.push(child_idx);
let child_shape = &self.shapes[child_idx];
for grandchild_id in child_shape.children_ids_iter(false) {
queue.push_back(grandchild_id);
}
}
}
result
}
fn find_nearest_ancestor_modifier(&self, idx: usize) -> Option<Matrix> {
let mut current_idx = idx;
loop {
let shape = &self.shapes[current_idx];
let parent_id = shape.parent_id?;
if parent_id == Uuid::nil() {
return None;
}
let &parent_idx = self.uuid_to_idx.get(&parent_id)?;
if let Some(matrix) = self.modifiers.get(&parent_idx) {
return Some(*matrix);
}
current_idx = parent_idx;
}
}
fn to_update_bool(&self, shape: &Shape) -> bool {
if !shape.is_bool() {
return false;