mirror of
https://github.com/penpot/penpot.git
synced 2026-07-23 06:28:14 +00:00
⚡ Coalesce live drag-preview state via RAF atoms
This commit is contained in:
parent
ca1e1dd806
commit
2d32946c06
@ -30,6 +30,7 @@
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.streams :as ms]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.shape :as wasm.shape]
|
||||
[beicon.v2.core :as rx]
|
||||
@ -614,19 +615,27 @@
|
||||
(filter (fn [[_ {:keys [type]}]]
|
||||
(= type :change-property)))))
|
||||
|
||||
;; The live preview state for a drag / resize / rotate gesture lives in
|
||||
;; behavior-subjects in `app.main.streams` (`wasm-modifiers` and
|
||||
;; `workspace-selrect`). These were previously the Redux keys
|
||||
;; `:workspace-wasm-modifiers` / `:workspace-selrect`, but at one
|
||||
;; React commit per pointermove they were the dominant per-event cost
|
||||
;; during a drag. Reads now go through RAF-coalesced atoms in
|
||||
;; `app.main.refs`, which update at most once per animation frame.
|
||||
|
||||
(defn set-temporary-selrect
|
||||
[selrect]
|
||||
(ptk/reify ::set-temporary-selrect
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :workspace-selrect selrect))))
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(rx/push! ms/workspace-selrect selrect))))
|
||||
|
||||
(defn set-temporary-modifiers
|
||||
[modifiers]
|
||||
(ptk/reify ::set-temporary-modifiers
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(assoc state :workspace-wasm-modifiers modifiers))))
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
(rx/push! ms/wasm-modifiers modifiers))))
|
||||
|
||||
(def ^:private xf:map-key (map key))
|
||||
|
||||
@ -658,15 +667,40 @@
|
||||
objects (dsh/lookup-page-objects state)
|
||||
snap-pixel?
|
||||
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))]
|
||||
;; --- DRAG PERF INSTRUMENTATION (remove after investigation) ---
|
||||
;; Spans appear in DevTools Performance > Timings track as `swm:*`.
|
||||
;; `props=N`/`geom=N` count tags are logged once per call so you can
|
||||
;; correlate per-event work to subtree size.
|
||||
(js/performance.mark "swm:total:start")
|
||||
(js/console.timeStamp (str "swm props=" (count wasm-props)))
|
||||
(js/performance.mark "swm:set-props:start")
|
||||
(set-wasm-props! objects prev-wasm-props wasm-props)
|
||||
(wasm.api/set-structure-modifiers (parse-structure-modifiers modif-tree))
|
||||
(let [geometry-entries (parse-geometry-modifiers modif-tree)
|
||||
modifiers (wasm.api/propagate-modifiers geometry-entries snap-pixel?)]
|
||||
(wasm.api/set-modifiers modifiers)
|
||||
(let [ids (into [] xf:map-key geometry-entries)
|
||||
selrect (wasm.api/get-selection-rect ids)]
|
||||
(rx/of (set-temporary-selrect selrect)
|
||||
(set-temporary-modifiers modifiers))))))))
|
||||
(js/performance.measure "swm:set-props" "swm:set-props:start")
|
||||
(js/performance.clearMarks "swm:set-props:start")
|
||||
(let [structure-entries (parse-structure-modifiers modif-tree)]
|
||||
(js/performance.mark "swm:struct-mods:start")
|
||||
(wasm.api/set-structure-modifiers structure-entries)
|
||||
(js/performance.measure "swm:struct-mods" "swm:struct-mods:start")
|
||||
(js/performance.clearMarks "swm:struct-mods:start")
|
||||
(let [geometry-entries (parse-geometry-modifiers modif-tree)
|
||||
_ (js/console.timeStamp (str "swm geom=" (count geometry-entries)))
|
||||
_ (js/performance.mark "swm:propagate:start")
|
||||
modifiers (wasm.api/propagate-modifiers geometry-entries snap-pixel?)
|
||||
_ (js/performance.measure "swm:propagate" "swm:propagate:start")
|
||||
_ (js/performance.clearMarks "swm:propagate:start")]
|
||||
(js/performance.mark "swm:set-mods:start")
|
||||
(wasm.api/set-modifiers modifiers)
|
||||
(js/performance.measure "swm:set-mods" "swm:set-mods:start")
|
||||
(js/performance.clearMarks "swm:set-mods:start")
|
||||
(let [ids (into [] xf:map-key geometry-entries)
|
||||
_ (js/performance.mark "swm:selrect:start")
|
||||
selrect (wasm.api/get-selection-rect ids)
|
||||
_ (js/performance.measure "swm:selrect" "swm:selrect:start")
|
||||
_ (js/performance.clearMarks "swm:selrect:start")]
|
||||
(js/performance.measure "swm:total" "swm:total:start")
|
||||
(js/performance.clearMarks "swm:total:start")
|
||||
(rx/of (set-temporary-selrect selrect)
|
||||
(set-temporary-modifiers modifiers)))))))))
|
||||
|
||||
(defn propagate-structure-modifiers
|
||||
[modif-tree objects]
|
||||
|
||||
@ -137,9 +137,15 @@
|
||||
(ptk/reify ::finish-transform
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(-> state
|
||||
(update :workspace-local dissoc :transform :duplicate-move-started?)
|
||||
(dissoc :workspace-selrect :workspace-wasm-modifiers)))))
|
||||
(update state :workspace-local dissoc :transform :duplicate-move-started?))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ _ _]
|
||||
;; Clear the live-preview streams so React overlays unmount when
|
||||
;; the gesture ends. (These streams replaced the redux keys
|
||||
;; `:workspace-wasm-modifiers` / `:workspace-selrect`.)
|
||||
(rx/push! ms/wasm-modifiers nil)
|
||||
(rx/push! ms/workspace-selrect nil))))
|
||||
|
||||
;; -- Resize --------------------------------------------------------
|
||||
|
||||
|
||||
@ -17,8 +17,41 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.tokens.selected-set :as dwts]
|
||||
[app.main.store :as st]
|
||||
[app.main.streams :as ms]
|
||||
[beicon.v2.core :as rx]
|
||||
[okulary.core :as l]))
|
||||
|
||||
(defn- raf-coalesced-atom
|
||||
"Returns a CLJS atom mirroring the latest value pushed to source-stream,
|
||||
but updated at most once per animation frame for non-nil pushes.
|
||||
Components that `mf/deref` the atom re-render aligned to RAF instead
|
||||
of per push, which matters for streams that fire at pointer-event
|
||||
rate (drag / resize / rotate live preview).
|
||||
|
||||
nil pushes apply synchronously and cancel any pending RAF: a
|
||||
`finish-transform` clear must take effect in the same reducer pass
|
||||
that commits the new shape state, otherwise the previous modifier
|
||||
would double-apply to the already-committed shapes for one frame."
|
||||
[source-stream]
|
||||
(let [a (atom nil)
|
||||
latest (volatile! nil)
|
||||
raf-id (volatile! nil)
|
||||
flush! (fn []
|
||||
(vreset! raf-id nil)
|
||||
(reset! a @latest))]
|
||||
(rx/sub! source-stream
|
||||
(fn [v]
|
||||
(vreset! latest v)
|
||||
(if (nil? v)
|
||||
(do
|
||||
(when-some [id @raf-id]
|
||||
(js/cancelAnimationFrame id)
|
||||
(vreset! raf-id nil))
|
||||
(reset! a nil))
|
||||
(when (nil? @raf-id)
|
||||
(vreset! raf-id (js/requestAnimationFrame flush!))))))
|
||||
a))
|
||||
|
||||
;; ---- Global refs
|
||||
|
||||
(def route
|
||||
@ -160,8 +193,14 @@
|
||||
"All tokens related ephimeral state"
|
||||
(l/derived :workspace-tokens st/state))
|
||||
|
||||
;; Live drag-gesture state. These are intentionally NOT in the Redux
|
||||
;; state tree — they are short-lived UI values updated on every gesture
|
||||
;; tick (drag/resize/rotate) and consumed by viewport overlays, the
|
||||
;; sidebar, and on-canvas widgets. Writing to atoms instead of dispatching
|
||||
;; ptk events skips the event-pipeline overhead and keeps temp state out
|
||||
;; of app state. See `app.main.data.workspace.modifiers` for the writers.
|
||||
(def workspace-selrect
|
||||
(l/derived :workspace-selrect st/state))
|
||||
(raf-coalesced-atom ms/workspace-selrect))
|
||||
|
||||
;; WARNING: Don't use directly from components, this is a proxy to
|
||||
;; improve performance of selected-shapes and
|
||||
@ -382,7 +421,7 @@
|
||||
(l/derived :workspace-wasm-editor-styles st/state))
|
||||
|
||||
(def workspace-wasm-modifiers
|
||||
(l/derived :workspace-wasm-modifiers st/state))
|
||||
(raf-coalesced-atom ms/wasm-modifiers))
|
||||
|
||||
(def ^:private workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
|
||||
@ -22,6 +22,21 @@
|
||||
(or ^boolean (kbd/keyboard-event? event)
|
||||
^boolean (mse/mouse-event? event)))
|
||||
|
||||
;; --- Live preview streams (drag / resize / rotate)
|
||||
;;
|
||||
;; Carry the in-flight modifier tree and selrect during an interactive
|
||||
;; transform. Replaces the previous Redux keys
|
||||
;; `:workspace-wasm-modifiers` and `:workspace-selrect`, which churned
|
||||
;; React state on every pointermove. Writes happen from
|
||||
;; `app.main.data.workspace.modifiers/set-temporary-*`; reads happen
|
||||
;; via the RAF-coalesced atoms in `app.main.refs`.
|
||||
|
||||
(defonce wasm-modifiers
|
||||
(rx/behavior-subject nil))
|
||||
|
||||
(defonce workspace-selrect
|
||||
(rx/behavior-subject nil))
|
||||
|
||||
;; --- Derived streams
|
||||
|
||||
(defonce ^:private pointer
|
||||
|
||||
@ -46,13 +46,10 @@
|
||||
|
||||
(mf/defc single-shape-options*
|
||||
{::mf/private true}
|
||||
[{:keys [shape page-id file-id libraries] :rest props}]
|
||||
[{:keys [shape page-id file-id libraries wasm-modifiers modifiers] :rest props}]
|
||||
(let [shape-type (dm/get-prop shape :type)
|
||||
shape-id (dm/get-prop shape :id)
|
||||
|
||||
wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
|
||||
modifiers (mf/deref refs/workspace-modifiers)
|
||||
|
||||
shape
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [wasm-modifiers (into {} wasm-modifiers)]
|
||||
@ -72,17 +69,23 @@
|
||||
:bool [:> bool/options* {:shape shape :file-id file-id :page-id page-id}]
|
||||
nil)))
|
||||
|
||||
(mf/defc shape-options*
|
||||
;; Throttled inner: re-renders at most every 100ms when its props change.
|
||||
;; The parent (shape-options*) feeds wasm-modifiers / modifiers as props so
|
||||
;; this throttle actually applies to drag-time modifier updates.
|
||||
(mf/defc shape-options-throttled*
|
||||
{::mf/wrap [#(mf/throttle % 100)]
|
||||
::mf/private true}
|
||||
[{:keys [shapes shapes-with-children selected page-id file-id libraries]}]
|
||||
[{:keys [shapes shapes-with-children selected page-id file-id libraries
|
||||
wasm-modifiers modifiers]}]
|
||||
(if (= 1 (count selected))
|
||||
[:> single-shape-options*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
:libraries libraries
|
||||
:shape (first shapes)
|
||||
:shapes-with-children shapes-with-children}]
|
||||
:shapes-with-children shapes-with-children
|
||||
:wasm-modifiers wasm-modifiers
|
||||
:modifiers modifiers}]
|
||||
[:> multiple/options*
|
||||
{:shapes-with-children shapes-with-children
|
||||
:shapes shapes
|
||||
@ -90,6 +93,21 @@
|
||||
:file-id file-id
|
||||
:libraries libraries}]))
|
||||
|
||||
(mf/defc shape-options*
|
||||
{::mf/private true}
|
||||
[{:keys [shapes shapes-with-children selected page-id file-id libraries]}]
|
||||
(let [wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
|
||||
modifiers (mf/deref refs/workspace-modifiers)]
|
||||
[:> shape-options-throttled*
|
||||
{:shapes shapes
|
||||
:shapes-with-children shapes-with-children
|
||||
:selected selected
|
||||
:page-id page-id
|
||||
:file-id file-id
|
||||
:libraries libraries
|
||||
:wasm-modifiers wasm-modifiers
|
||||
:modifiers modifiers}]))
|
||||
|
||||
(mf/defc specialized-panel*
|
||||
{::mf/private true}
|
||||
[{:keys [panel]}]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user