mirror of
https://github.com/penpot/penpot.git
synced 2026-05-19 23:13:39 +00:00
⚡ Improve drag performance avoiding unnecessary modifiers
This commit is contained in:
parent
44f4c43f15
commit
8dd4b486e7
@ -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))))))))
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user