diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index ed8e44e3ca..f92cf5b516 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -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.refs :as refs] [app.render-wasm.api :as wasm.api] [app.render-wasm.shape :as wasm.shape] [beicon.v2.core :as rx] @@ -55,6 +56,52 @@ (when (compare-and-set! interactive-transform-active? true false) (wasm.api/set-modifiers-end))) +;; ────────────────────────────────────────────────────────────────────── +;; Live drag-gesture state. +;; +;; `set-wasm-modifiers` runs on every drag pointermove and writes: +;; 1. WASM canvas state (synchronous, cheap) +;; 2. Live drag values (selrect + per-shape transforms) consumed by +;; viewport overlays, the sidebar, and on-canvas widgets. +;; +;; Step 2 lives in dedicated atoms (refs/workspace-selrect, +;; refs/workspace-wasm-modifiers) — NOT in Redux state. Atom writes are +;; coalesced to one per animation frame so React subscribers reconcile +;; at most once per frame regardless of pointermove arrival rate, and we +;; skip the ptk event-pipeline overhead for these high-frequency updates. +(defonce ^:private pending-temp-payload (atom nil)) +(defonce ^:private pending-temp-frame-id (volatile! nil)) + +(defn- flush-pending-temp! + [] + (vreset! pending-temp-frame-id nil) + (when-some [payload @pending-temp-payload] + (reset! pending-temp-payload nil) + (reset! refs/workspace-selrect (:selrect payload)) + (reset! refs/workspace-wasm-modifiers (:modifiers payload)))) + +(defn- schedule-temp-flush! + [selrect modifiers] + (reset! pending-temp-payload {:selrect selrect :modifiers modifiers}) + (when (nil? @pending-temp-frame-id) + (vreset! pending-temp-frame-id + (js/requestAnimationFrame flush-pending-temp!)))) + +(defn- cancel-pending-temp-flush! + [] + (when-some [frame-id @pending-temp-frame-id] + (js/cancelAnimationFrame frame-id) + (vreset! pending-temp-frame-id nil)) + (reset! pending-temp-payload nil)) + +(defn clear-temp-state! + "Reset the live drag-gesture state. Cancels any rAF-queued write so it + cannot resurrect the values after they are cleared." + [] + (cancel-pending-temp-flush!) + (reset! refs/workspace-selrect nil) + (reset! refs/workspace-wasm-modifiers nil)) + (def ^:private transform-attrs #{:selrect :points @@ -296,6 +343,10 @@ ptk/EffectEvent (effect [_ state _] (when (features/active-feature? state "render-wasm/v1") + ;; Drop any rAF-queued temp dispatch and reset the live drag-gesture + ;; state — its values are about to be replaced by the committed + ;; update. + (clear-temp-state!) ;; End interactive transform mode BEFORE cleaning modifiers so ;; the final full-quality render triggered by subsequent shape ;; updates is not still classified as "interactive" (which would @@ -614,20 +665,6 @@ (filter (fn [[_ {:keys [type]}]] (= type :change-property))))) -(defn set-temporary-selrect - [selrect] - (ptk/reify ::set-temporary-selrect - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-selrect selrect)))) - -(defn set-temporary-modifiers - [modifiers] - (ptk/reify ::set-temporary-modifiers - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-wasm-modifiers modifiers)))) - (def ^:private xf:map-key (map key)) #_:clj-kondo/ignore @@ -666,8 +703,13 @@ (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))))))))) + ;; Coalesce the Redux dispatch to one per animation frame so + ;; React subscribers (sidebar, viewport overlays, flex + ;; controls) reconcile at most once per frame regardless of + ;; pointermove arrival rate. The WASM canvas update above + ;; already happened synchronously. + (schedule-temp-flush! selrect modifiers) + (rx/empty)))))))) (defn propagate-structure-modifiers [modif-tree objects] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 58982b33f0..f70e5fdedd 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -135,11 +135,16 @@ (defn finish-transform [] (ptk/reify ::finish-transform + ptk/EffectEvent + (effect [_ _ _] + ;; Live drag-gesture state lives in dedicated atoms (see + ;; app.main.data.workspace.modifiers); reset them here so the + ;; viewport overlays and sidebar drop the in-flight values. + (dwm/clear-temp-state!)) + 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?)))) ;; -- Resize -------------------------------------------------------- diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index c61307cf93..9fd8b9223b 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -160,8 +160,17 @@ "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)) + (l/atom nil)) + +(def workspace-wasm-modifiers + (l/atom nil)) ;; WARNING: Don't use directly from components, this is a proxy to ;; improve performance of selected-shapes and @@ -381,8 +390,9 @@ (def workspace-wasm-editor-styles (l/derived :workspace-wasm-editor-styles st/state)) -(def workspace-wasm-modifiers - (l/derived :workspace-wasm-modifiers st/state)) +;; `workspace-wasm-modifiers` is defined alongside `workspace-selrect` +;; near the top of this file as a plain atom (not derived from app +;; state). Keep it in mind when grepping. (def ^:private workspace-modifiers-with-objects (l/derived diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index c7413af9ac..23181957c0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -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]}]