From f00a334e9d3fd32119477342f13b95876c334180 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Fri, 8 May 2026 09:20:21 +0200 Subject: [PATCH] wip --- .../app/main/data/workspace/modifiers.cljs | 26 +++---- .../app/main/data/workspace/transforms.cljs | 68 ++++++++++++------- frontend/src/app/main/refs.cljs | 45 +++++++++++- render-wasm/src/render.rs | 17 +++++ 4 files changed, 118 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index cf954eee54..1ace91e3f5 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -46,6 +46,13 @@ ;; `set-wasm-modifiers` calls fire in between. (defonce ^:private interactive-transform-active? (atom false)) +;; Holds the property changes (`extract-property-changes` output) +;; applied during the in-flight gesture. Written per drag frame in +;; `set-wasm-modifiers`, read at gesture-end (`clear-local-transform`) +;; to revert. Kept out of Redux to avoid root-atom churn at drag +;; cadence (~62.5 Hz) and the re-frame subscription scan that follows. +(defonce ^:private gesture-wasm-props (volatile! nil)) + (defn- ensure-interactive-transform-start! [] (when (compare-and-set! interactive-transform-active? false true) @@ -303,12 +310,14 @@ ;; skip shadows / blur). (ensure-interactive-transform-end!) (wasm.api/clean-modifiers) - (set-wasm-props! (dsh/lookup-page-objects state) (:wasm-props state) []))) + (let [last-wasm-props @gesture-wasm-props] + (vreset! gesture-wasm-props nil) + (set-wasm-props! (dsh/lookup-page-objects state) last-wasm-props [])))) ptk/UpdateEvent (update [_ state] (-> state - (dissoc :workspace-modifiers :wasm-props :prev-wasm-props) + (dissoc :workspace-modifiers) (dissoc :app.main.data.workspace.transforms/current-move-selected))))) (defn create-modif-tree @@ -684,14 +693,6 @@ :or {ignore-constraints false ignore-snap-pixel false} :as params}] (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)))) - ptk/WatchEvent (watch [_ state _] ;; Entering an interactive transform (drag/resize/rotate). Flip @@ -701,8 +702,9 @@ ;; `clear-local-transform`. (ensure-interactive-transform-start!) (wasm.api/clean-modifiers) - (let [prev-wasm-props (:prev-wasm-props state) - wasm-props (:wasm-props state) + (let [prev-wasm-props @gesture-wasm-props + wasm-props (extract-property-changes modif-tree) + _ (vreset! gesture-wasm-props wasm-props) objects (dsh/lookup-page-objects state) snap-pixel? (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid)) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 1dcf6a2688..0b9e0098fc 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -743,31 +743,44 @@ (rx/take-until stopper) (rx/share)) - modifiers-stream + ;; Per-pointermove: combine inputs and cache the most-recent + ;; non-nil grid cell. The tap fires per move so the cache stays + ;; fresh regardless of where the WASM render branch samples. + move-stream-with-cell-data (->> move-stream (rx/with-latest-from array/conj ms/mouse-position-shift) (rx/tap (fn [[_ _ _ cell-data _]] (when (some? cell-data) (vreset! prev-cell-data cell-data)))) + (rx/share)) - (rx/map - (fn [[move-vector target-frame drop-index cell-data shift?]] - (let [cell-data (or cell-data @prev-cell-data) - x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) - [move-vector snap-ignore-axis] - (cond - (and shift? x-disp?) - [(assoc move-vector :y 0) :y] + ;; Heavy build (modifier tree construction). Pulled out as a + ;; standalone fn so the WASM render branch can apply it post- + ;; sample and the commit branch can apply it once on the final + ;; event, rather than running it per pointermove via the shared + ;; modifiers-stream below. + build-modifier + (fn [[move-vector target-frame drop-index cell-data shift?]] + (let [cell-data (or cell-data @prev-cell-data) + x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) + [move-vector snap-ignore-axis] + (cond + (and shift? x-disp?) + [(assoc move-vector :y 0) :y] - shift? - [(assoc move-vector :x 0) :x] + shift? + [(assoc move-vector :x 0) :x] - :else - [move-vector nil])] - [(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) - (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)) - snap-ignore-axis]))) + :else + [move-vector nil])] + [(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) + (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)) + snap-ignore-axis])) + + modifiers-stream + (->> move-stream-with-cell-data + (rx/map build-modifier) (rx/share))] (if (features/active-feature? state "render-wasm/v1") @@ -779,11 +792,14 @@ (rx/of true) (rx/empty)))))] (rx/merge - (->> modifiers-stream + ;; Sampled render path: `build-modifier` runs only for sampled + ;; events (~62.5 Hz), not per pointermove. Modifier-tree + ;; construction and frame-modifier propagation are skipped on + ;; dropped frames. + (->> move-stream-with-cell-data (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 build-modifier) (rx/map (fn [[modifiers snap-ignore-axis]] (dwm/set-wasm-modifiers modifiers @@ -802,14 +818,18 @@ (dws/duplicate-selected false true)) (rx/empty))))) - ;; Last event will write the modifiers creating the changes - (->> move-stream + ;; Last event writes the modifiers creating the changes. + ;; Build from the final move event directly so the commit + ;; reflects the user's actual release position rather than + ;; the latest sampled value (which can be ~16 ms stale). + (->> move-stream-with-cell-data (rx/last) (rx/take-until duplicate-stopper) - (rx/with-latest-from modifiers-stream) (rx/mapcat - (fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]] - (let [undo-id (js/Symbol)] + (fn [args] + (let [[_ target-frame drop-index drop-cell _] args + [modifiers snap-ignore-axis] (build-modifier args) + undo-id (js/Symbol)] (rx/of (dwu/start-undo-transaction undo-id) (dwm/apply-wasm-modifiers modifiers diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 01bb43a0cb..a42f25e51b 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -388,9 +388,50 @@ (def workspace-wasm-editor-styles (l/derived :workspace-wasm-editor-styles st/state)) +(def ^:private wasm-modifier-jitter-threshold-px + ;; Suppress drag-preview pushes whose translation delta is below half a + ;; pixel. The Skia renderer keeps running at full sample cadence; this + ;; only coarsens the JS-side outline geometry recompute that the memoed + ;; consumers in `viewport_wasm` derive from `wasm-modifiers`. + 0.5) + +(defn- ^boolean wasm-modifiers-significant-change? + "Cheap predicate: did `new` move any shape by ≥ threshold pixels vs `prev`. + Only short-circuits for *pure-translation* matrices (drag); resize/rotate + matrices always pass through." + [prev new threshold] + (cond + (or (nil? prev) (nil? new)) true + (not= (count prev) (count new)) true + :else + (let [prev-map (into {} prev)] + (loop [pairs new] + (if-let [pair (first pairs)] + (let [id (nth pair 0) + ^js m-new (nth pair 1) + ^js m-prev (get prev-map id)] + (cond + (nil? m-prev) true + (or (not (== 1 (.-a m-new))) (not (== 0 (.-b m-new))) + (not (== 0 (.-c m-new))) (not (== 1 (.-d m-new))) + (not (== 1 (.-a m-prev))) (not (== 0 (.-b m-prev))) + (not (== 0 (.-c m-prev))) (not (== 1 (.-d m-prev)))) + true + (or (> (js/Math.abs (- (.-e m-new) (.-e m-prev))) threshold) + (> (js/Math.abs (- (.-f m-new) (.-f m-prev))) threshold)) + true + :else (recur (rest pairs)))) + false))))) + (def workspace-wasm-modifiers - (let [a (atom nil)] - (rx/sub! ms/wasm-modifiers #(reset! a %)) + (let [a (atom nil) + last-emitted (volatile! nil)] + (rx/sub! ms/wasm-modifiers + (fn [val] + (when (wasm-modifiers-significant-change? + @last-emitted val wasm-modifier-jitter-threshold-px) + (vreset! last-emitted val) + (reset! a val)))) a)) (def ^:private workspace-modifiers-with-objects diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 9ab1436606..c6b391fc48 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -3270,6 +3270,23 @@ impl RenderState { .get_tiles_of(shape.id) .map_or(Vec::new(), |t| t.iter().copied().collect()); + // Drag fast path: when the shape's tile footprint is identical to + // last frame, the tile *content* still changed (the shape moved + // within those tiles), so callers must still invalidate the tile + // cache for that set — but the index `remove_shape_at` + + // `add_shape_at` shuffle is pure churn. + if self.options.is_interactive_transform() { + let new_count = ((rex - rsx + 1) * (rey - rsy + 1)) as usize; + if old_tiles.len() == new_count + && old_tiles.iter().all(|t| { + let (x, y) = (t.x(), t.y()); + x >= rsx && x <= rex && y >= rsy && y <= rey + }) + { + return old_tiles.into_iter().collect(); + } + } + let mut result = HashSet::::with_capacity(old_tiles.len()); // First, remove the shape from all tiles where it was previously located