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.
(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))

View File

@@ -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

View File

@@ -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

View File

@@ -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::<tiles::Tile>::with_capacity(old_tiles.len());
// First, remove the shape from all tiles where it was previously located