This commit is contained in:
Elena Torro 2026-05-08 09:20:21 +02:00
parent 790dfb04f0
commit f00a334e9d
4 changed files with 118 additions and 38 deletions

View File

@ -46,6 +46,13 @@
;; `set-wasm-modifiers` calls fire in between. ;; `set-wasm-modifiers` calls fire in between.
(defonce ^:private interactive-transform-active? (atom false)) (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! (defn- ensure-interactive-transform-start!
[] []
(when (compare-and-set! interactive-transform-active? false true) (when (compare-and-set! interactive-transform-active? false true)
@ -303,12 +310,14 @@
;; skip shadows / blur). ;; skip shadows / blur).
(ensure-interactive-transform-end!) (ensure-interactive-transform-end!)
(wasm.api/clean-modifiers) (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 ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(dissoc :workspace-modifiers :wasm-props :prev-wasm-props) (dissoc :workspace-modifiers)
(dissoc :app.main.data.workspace.transforms/current-move-selected))))) (dissoc :app.main.data.workspace.transforms/current-move-selected)))))
(defn create-modif-tree (defn create-modif-tree
@ -684,14 +693,6 @@
:or {ignore-constraints false ignore-snap-pixel false} :or {ignore-constraints false ignore-snap-pixel false}
:as params}] :as params}]
(ptk/reify ::set-wasm-modifiers (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 ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
;; Entering an interactive transform (drag/resize/rotate). Flip ;; Entering an interactive transform (drag/resize/rotate). Flip
@ -701,8 +702,9 @@
;; `clear-local-transform`. ;; `clear-local-transform`.
(ensure-interactive-transform-start!) (ensure-interactive-transform-start!)
(wasm.api/clean-modifiers) (wasm.api/clean-modifiers)
(let [prev-wasm-props (:prev-wasm-props state) (let [prev-wasm-props @gesture-wasm-props
wasm-props (:wasm-props state) wasm-props (extract-property-changes modif-tree)
_ (vreset! gesture-wasm-props wasm-props)
objects (dsh/lookup-page-objects state) objects (dsh/lookup-page-objects state)
snap-pixel? snap-pixel?
(and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid)) (and (not ignore-snap-pixel) (contains? (:workspace-layout state) :snap-pixel-grid))

View File

@ -743,31 +743,44 @@
(rx/take-until stopper) (rx/take-until stopper)
(rx/share)) (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 (->> move-stream
(rx/with-latest-from array/conj ms/mouse-position-shift) (rx/with-latest-from array/conj ms/mouse-position-shift)
(rx/tap (rx/tap
(fn [[_ _ _ cell-data _]] (fn [[_ _ _ cell-data _]]
(when (some? cell-data) (when (some? cell-data)
(vreset! prev-cell-data cell-data)))) (vreset! prev-cell-data cell-data))))
(rx/share))
(rx/map ;; Heavy build (modifier tree construction). Pulled out as a
(fn [[move-vector target-frame drop-index cell-data shift?]] ;; standalone fn so the WASM render branch can apply it post-
(let [cell-data (or cell-data @prev-cell-data) ;; sample and the commit branch can apply it once on the final
x-disp? (> (mth/abs (:x move-vector)) (mth/abs (:y move-vector))) ;; event, rather than running it per pointermove via the shared
[move-vector snap-ignore-axis] ;; modifiers-stream below.
(cond build-modifier
(and shift? x-disp?) (fn [[move-vector target-frame drop-index cell-data shift?]]
[(assoc move-vector :y 0) :y] (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? shift?
[(assoc move-vector :x 0) :x] [(assoc move-vector :x 0) :x]
:else :else
[move-vector nil])] [move-vector nil])]
[(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector)) [(-> (dwm/create-modif-tree ids (ctm/move-modifiers move-vector))
(dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data)) (dwm/build-change-frame-modifiers objects selected target-frame drop-index cell-data))
snap-ignore-axis]))) snap-ignore-axis]))
modifiers-stream
(->> move-stream-with-cell-data
(rx/map build-modifier)
(rx/share))] (rx/share))]
(if (features/active-feature? state "render-wasm/v1") (if (features/active-feature? state "render-wasm/v1")
@ -779,11 +792,14 @@
(rx/of true) (rx/of true)
(rx/empty)))))] (rx/empty)))))]
(rx/merge (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) (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/sample 16)
(rx/map build-modifier)
(rx/map (rx/map
(fn [[modifiers snap-ignore-axis]] (fn [[modifiers snap-ignore-axis]]
(dwm/set-wasm-modifiers modifiers (dwm/set-wasm-modifiers modifiers
@ -802,14 +818,18 @@
(dws/duplicate-selected false true)) (dws/duplicate-selected false true))
(rx/empty))))) (rx/empty)))))
;; Last event will write the modifiers creating the changes ;; Last event writes the modifiers creating the changes.
(->> move-stream ;; 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/last)
(rx/take-until duplicate-stopper) (rx/take-until duplicate-stopper)
(rx/with-latest-from modifiers-stream)
(rx/mapcat (rx/mapcat
(fn [[[_ target-frame drop-index drop-cell] [modifiers snap-ignore-axis]]] (fn [args]
(let [undo-id (js/Symbol)] (let [[_ target-frame drop-index drop-cell _] args
[modifiers snap-ignore-axis] (build-modifier args)
undo-id (js/Symbol)]
(rx/of (rx/of
(dwu/start-undo-transaction undo-id) (dwu/start-undo-transaction undo-id)
(dwm/apply-wasm-modifiers modifiers (dwm/apply-wasm-modifiers modifiers

View File

@ -388,9 +388,50 @@
(def workspace-wasm-editor-styles (def workspace-wasm-editor-styles
(l/derived :workspace-wasm-editor-styles st/state)) (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 (def workspace-wasm-modifiers
(let [a (atom nil)] (let [a (atom nil)
(rx/sub! ms/wasm-modifiers #(reset! a %)) 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)) a))
(def ^:private workspace-modifiers-with-objects (def ^:private workspace-modifiers-with-objects

View File

@ -3270,6 +3270,23 @@ impl RenderState {
.get_tiles_of(shape.id) .get_tiles_of(shape.id)
.map_or(Vec::new(), |t| t.iter().copied().collect()); .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::<tiles::Tile>::with_capacity(old_tiles.len()); let mut result = HashSet::<tiles::Tile>::with_capacity(old_tiles.len());
// First, remove the shape from all tiles where it was previously located // First, remove the shape from all tiles where it was previously located