From 528d006b8d2ba705a2912a9323c63ff0f4c0249f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Wed, 6 May 2026 12:16:09 +0200 Subject: [PATCH] :zap: Events enhancements * :zap: Compute moved-subtree data once in find-valid-parent-and-frame-ids * :zap: Smooth WASM drag preview stream * :zap: Limit WASM outline modifier application --- common/src/app/common/types/container.cljc | 92 +++++++++---------- .../app/main/data/workspace/transforms.cljs | 20 +++- .../app/main/ui/workspace/viewport_wasm.cljs | 49 +++++++++- 3 files changed, 103 insertions(+), 58 deletions(-) diff --git a/common/src/app/common/types/container.cljc b/common/src/app/common/types/container.cljc index c3aab0df4e..1689386e36 100644 --- a/common/src/app/common/types/container.cljc +++ b/common/src/app/common/types/container.cljc @@ -491,60 +491,50 @@ ([parent-id objects children] (find-valid-parent-and-frame-ids parent-id objects children false nil)) ([parent-id objects children pasting? libraries] - (letfn [(get-frame [parent-id] - (if (cfh/frame-shape? objects parent-id) parent-id (get-in objects [parent-id :frame-id])))] - (let [parent (get objects parent-id) - - ;; We need to check only the top shapes - children-ids (set (map :id children)) + (letfn [(get-frame [pid] + (if (cfh/frame-shape? objects pid) pid (get-in objects [pid :frame-id])))] + ;; `descendants`, variant-id set, etc. depend only on the moved shapes, not on the + ;; candidate parent. Computing them once per drag (this fn is hot during move) + ;; avoids O(depth * subtree) work when walking invalid ancestors — common with + ;; variants and nested components. + (let [children-ids (into #{} (map :id) children) top-children (remove #(contains? children-ids (:parent-id %)) children) - - ;; We can always move the children to the parent they already have. - ;; But if we are pasting, those are new items, so it is considered a change - no-changes? - (and (every? #(= parent-id (:parent-id %)) top-children) - (not pasting?)) - - ;; Are all the top-children a main-instance of a component? - all-main? - (every? ctk/main-instance? top-children) - - ascendants (cfh/get-parents-with-self objects parent-id) - any-main-ascendant (some ctk/main-instance? ascendants) - any-variant-container-ascendant (some ctk/is-variant-container? ascendants) - - get-variant-id (fn [shape] - (when (:component-id shape) - (-> (get-component-from-shape shape libraries) - :variant-id))) - + all-main? (every? ctk/main-instance? top-children) + get-variant-id + (fn [shape] + (when (:component-id shape) + (-> (get-component-from-shape shape libraries) + :variant-id))) descendants (mapcat #(cfh/get-children-with-self objects %) children-ids) any-variant-container-descendant (some ctk/is-variant-container? descendants) - descendants-variant-ids-set (->> descendants - (map get-variant-id) - set) - any-main-descendant - (some - (fn [shape] - (some ctk/main-instance? (cfh/get-children-with-self objects (:id shape)))) - children)] - - (if (or no-changes? - (and (not (invalid-structure-for-component? objects parent children pasting? libraries)) - ;; If we are moving (not pasting) into a main component, no descendant can be main - (or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent))) - ;; Don't allow variant-container inside variant container nor main - (or (not any-variant-container-descendant) - (and (not any-variant-container-ascendant) (not any-main-ascendant))) - ;; If the parent is a variant-container, all the items should be main - (or (not (ctk/is-variant-container? parent)) all-main?) - ;; If we are pasting, the parent can't be a "brother" of any of the pasted items, - ;; so not have the same variant-id of any descendant - (or (not pasting?) - (not (ctk/is-variant? parent)) - (not (contains? descendants-variant-ids-set (:variant-id parent)))))) - [parent-id (get-frame parent-id)] - (recur (:parent-id parent) objects children pasting? libraries)))))) + descendants-variant-ids-set (into #{} (map get-variant-id) descendants) + ;; Same as (some #(some ctk/main-instance? (cfh/get-children-with-self objects (:id %))) + ;; children) but a single walk over `descendants`. + any-main-descendant (some ctk/main-instance? descendants)] + (loop [parent-id parent-id] + (let [parent (get objects parent-id) + no-changes? + (and (every? #(= parent-id (:parent-id %)) top-children) + (not pasting?)) + ascendants (cfh/get-parents-with-self objects parent-id) + any-main-ascendant (some ctk/main-instance? ascendants) + any-variant-container-ascendant (some ctk/is-variant-container? ascendants)] + (if (or no-changes? + (and (not (invalid-structure-for-component? objects parent children pasting? libraries)) + ;; If we are moving (not pasting) into a main component, no descendant can be main + (or pasting? (nil? any-main-descendant) (not (ctk/main-instance? parent))) + ;; Don't allow variant-container inside variant container nor main + (or (not any-variant-container-descendant) + (and (not any-variant-container-ascendant) (not any-main-ascendant))) + ;; If the parent is a variant-container, all the items should be main + (or (not (ctk/is-variant-container? parent)) all-main?) + ;; If we are pasting, the parent can't be a "brother" of any of the pasted items, + ;; so not have the same variant-id of any descendant + (or (not pasting?) + (not (ctk/is-variant? parent)) + (not (contains? descendants-variant-ids-set (:variant-id parent)))))) + [parent-id (get-frame parent-id)] + (recur (:parent-id parent))))))))) ;; --- SHAPE UPDATE diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 58982b33f0..540a0e2362 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -653,6 +653,10 @@ ptk/WatchEvent (watch [_ state stream] (let [prev-cell-data (volatile! nil) + ;; Cache the resolved valid parent while hovering the same raw target frame. + ;; `find-valid-parent-and-frame-ids` may walk many ancestors for variants/components, + ;; and the result is stable during the gesture (objects/libraries are constant here). + find-valid-for-raw-cache (volatile! {:raw nil :pair nil}) page-id (:current-page-id state) libraries (dsh/lookup-libraries state) objects (dsh/lookup-page-objects state page-id) @@ -713,15 +717,22 @@ (fn [[move-vector mod?]] (let [position (gpt/add from-position move-vector) exclude-frames (if mod? exclude-frames exclude-frames-siblings) - target-frame (ctst/top-nested-frame objects position exclude-frames) - [target-frame _] (ctn/find-valid-parent-and-frame-ids target-frame objects shapes false libraries) + raw-target (ctst/top-nested-frame objects position exclude-frames) + cache @find-valid-for-raw-cache + [target-frame _] + (if (= raw-target (:raw cache)) + (:pair cache) + (let [pair (ctn/find-valid-parent-and-frame-ids raw-target objects shapes false libraries)] + (vreset! find-valid-for-raw-cache {:raw raw-target :pair pair}) + pair)) flex-layout? (ctl/flex-layout? objects target-frame) grid-layout? (ctl/grid-layout? objects target-frame) drop-index (when flex-layout? (gslf/get-drop-index target-frame objects position)) cell-data (when (and grid-layout? (not mod?)) (get-drop-cell target-frame objects position))] (array move-vector target-frame drop-index cell-data)))) - (rx/take-until stopper)) + (rx/take-until stopper) + (rx/share)) modifiers-stream (->> move-stream @@ -761,6 +772,9 @@ (rx/merge (->> modifiers-stream (rx/take-until duplicate-stopper) + ;; Sample at a fixed cadence to keep preview smooth. Unlike a throttle, + ;; this tends to avoid perceptible "jumps" while still capping WASM work. + (rx/sample 16) (rx/map (fn [[modifiers snap-ignore-axis]] (dwm/set-wasm-modifiers modifiers :snap-ignore-axis snap-ignore-axis)))) diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 837c72f598..915fc01f81 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -78,6 +78,39 @@ [selected objects modifiers] (apply-modifiers-to-objects objects (select-keys (into {} modifiers) selected))) +(defn- apply-wasm-modifiers-to-ids + "Like `apply-modifiers-to-objects`, but only updates ids in `id-set`. During WASM + drag, `wasm-modifiers` can list every propagated descendant (large variants); SVG + outlines only need geometry for `selected` / hover / highlight — not the whole page." + [objects wasm-modifiers id-set] + (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)) + (update objs id gsh/apply-transform t) + objs))) + objects + wasm-modifiers))) + +(defn- outline-wasm-source-ids + "Superset of shape ids that `shape-outlines` may look up (all outline usages here)." + [base-objects selected highlighted edition hover-ids hover frame-hover] + (let [outlined-frame-id (->> hover-ids + (filter #(cfh/frame-shape? (get base-objects %))) + (remove selected) + (last)) + ids (-> #{} + (into (or selected #{})) + (into (or highlighted #{})) + (into (or hover-ids #{})))] + (cond-> ids + (uuid? (:id hover)) (conj (:id hover)) + (uuid? frame-hover) (conj frame-hover) + (uuid? outlined-frame-id) (conj outlined-frame-id) + (uuid? edition) (disj edition)))) + (mf/defc viewport* [{:keys [selected wglobal layout file page palete-size]}] (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check @@ -129,10 +162,6 @@ (into [] (keep (d/getf objects-modified))) (not-empty)) - objects-for-outlines - (mf/with-memo [base-objects wasm-modifiers] - (apply-modifiers-to-objects base-objects wasm-modifiers)) - ;; STATE alt? (mf/use-state false) shift? (mf/use-state false) @@ -178,6 +207,18 @@ (when (some? parent-id) (get base-objects parent-id))))) + outline-wasm-ids + (mf/with-memo + [base-objects selected highlighted edition @hover-ids @hover @frame-hover] + (outline-wasm-source-ids base-objects selected highlighted edition @hover-ids @hover @frame-hover)) + + objects-for-outlines + (mf/with-memo + [base-objects wasm-modifiers outline-wasm-ids] + (if (seq wasm-modifiers) + (apply-wasm-modifiers-to-ids base-objects wasm-modifiers outline-wasm-ids) + base-objects)) + zoom (d/check-num zoom 1) drawing-tool (:tool drawing)