From 6d6f961b89dc32f955942542f977e2fa315c2fdb Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Fri, 8 May 2026 13:46:26 +0200 Subject: [PATCH] wip --- .../src/app/main/ui/workspace/viewport.cljs | 16 ++-- .../main/ui/workspace/viewport/actions.cljs | 93 ++++++++++++++++++- .../app/main/ui/workspace/viewport/hooks.cljs | 6 +- .../ui/workspace/viewport/viewport_ref.cljs | 6 +- .../app/main/ui/workspace/viewport_wasm.cljs | 16 ++-- frontend/src/app/render_wasm/api.cljs | 75 +++++++++++---- render-wasm/src/main.rs | 8 ++ render-wasm/src/render.rs | 41 ++++++++ 8 files changed, 227 insertions(+), 34 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 9cd8c4ef6e..7544308fed 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -210,7 +210,9 @@ on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? path-editing? grid-editing? path-drawing? create-comment? space? panning z? read-only?) - on-pointer-up (actions/on-pointer-up disable-paste-ref) + on-pointer-up (actions/on-pointer-up disable-paste-ref) + on-pointer-cancel (actions/on-pointer-cancel) + on-lost-pointer-capture (actions/on-lost-pointer-capture) on-pointer-enter (actions/on-pointer-enter in-viewport-ref) on-pointer-leave (actions/on-pointer-leave in-viewport-ref) @@ -430,11 +432,13 @@ :on-drag-over on-drag-over :on-drag-end on-drag-end :on-drop on-drop - :on-pointer-down on-pointer-down - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-pointer-move on-pointer-move - :on-pointer-up on-pointer-up} + :on-pointer-down on-pointer-down + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-move on-pointer-move + :on-pointer-up on-pointer-up + :on-pointer-cancel on-pointer-cancel + :on-lost-pointer-capture on-lost-pointer-capture} [:defs ;; This clip is so the handlers are not over the rulers diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index fb6837b70c..c5e6992015 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -39,6 +39,54 @@ (def scale-per-pixel -0.0057) +;; FIXME(drag-debug): temporary instrumentation to diagnose Firefox firing +;; pointercancel/lostpointercapture mid-drag. Remove once fixed. +(defonce ^:private drag-debug-state + #js {:downTs 0 + :moveCount 0 + :pointerId nil + :captureNode nil + :captureTag nil + :captureClass nil}) + +(defn- log-drag! + [phase event] + (let [state drag-debug-state + target (dom/get-target event) + captured (.-captureNode state) + cap-conn (when captured (.-isConnected captured)) + cap-same (and captured (= captured target))] + (js/console.log + (str "[drag-debug] " phase) + #js {:pointerId (.-pointerId event) + :pointerType (.-pointerType event) + :button (.-button event) + :buttons (.-buttons event) + :isPrimary (.-isPrimary event) + :tag (some-> target .-tagName) + :class (some-> target .-className) + :captureTag (.-captureTag state) + :captureClass (.-captureClass state) + :captureConnected cap-conn + :captureIsTarget cap-same + :movesSinceDown (.-moveCount state) + :timeFromDownMs (when (pos? (.-downTs state)) + (- (.-timeStamp event) (.-downTs state)))}))) + +(defn- drag-debug-record-down! + [event capture-node] + (let [state drag-debug-state] + (set! (.-downTs state) (.-timeStamp event)) + (set! (.-moveCount state) 0) + (set! (.-pointerId state) (.-pointerId event)) + (set! (.-captureNode state) capture-node) + (set! (.-captureTag state) (some-> capture-node .-tagName)) + (set! (.-captureClass state) (some-> capture-node .-className)))) + +(defn- drag-debug-bump-move! [] + (set! (.-moveCount drag-debug-state) + (inc (.-moveCount drag-debug-state)))) + (defn on-pointer-down [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? node-editing? grid-editing? drawing-path? create-comment? space? panning z? read-only?] @@ -50,13 +98,14 @@ ;; We need to handle editor related stuff here because ;; handling on editor dom node does not works properly. (let [target (dom/get-target event) - editor (txu/closest-text-editor-content target)] + editor (txu/closest-text-editor-content target) + capture-node (or editor target)] ;; Capture mouse pointer to detect the movements even if cursor ;; leaves the viewport or the browser itself ;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture - (if editor - (.setPointerCapture editor (.-pointerId event)) - (.setPointerCapture target (.-pointerId event)))) + (.setPointerCapture capture-node (.-pointerId event)) + (drag-debug-record-down! event capture-node) + (log-drag! "down" event)) (when (or (dom/class? (dom/get-target event) "viewport-controls") (dom/class? (dom/get-target event) "viewport-selrect") @@ -281,6 +330,7 @@ [disable-paste-ref] (mf/use-callback (fn [event] + (log-drag! "up" event) (dom/stop-propagation event) (let [target (dom/get-target event)] @@ -309,6 +359,40 @@ (st/emit! (dw/finish-panning) (dw/finish-zooming)))))) +(defn on-pointer-cancel + [] + (mf/use-fn + (fn [event] + (log-drag! "cancel" event) + (dom/stop-propagation event) + + (let [target (dom/get-target event)] + (.releasePointerCapture target (.-pointerId event))) + + ;; `pointercancel` fires when the browser takes over the gesture + ;; (system swipe, stylus disconnect, touch cancelled). Emit `:interrupt` + ;; so any in-flight `drag-stopper` stream terminates instead of waiting + ;; for a `mouseup` that will never come, and clear pan/zoom state which + ;; isn't gated by drag-stopper. + (st/emit! :interrupt + (dw/finish-panning) + (dw/finish-zooming))))) + +(defn on-lost-pointer-capture + [] + (mf/use-fn + (fn [event] + (log-drag! "lostcapture" event) + ;; Backstop: `lostpointercapture` fires whenever the captured pointer + ;; is released — after `pointerup`, after `pointercancel`, or when the + ;; browser releases capture unilaterally (capturing element removed, + ;; another element stealing capture, navigation). This is the W3C + ;; signal for "the gesture is definitively over", so it's the safest + ;; place to ensure no drag stream is left hanging. + (st/emit! :interrupt + (dw/finish-panning) + (dw/finish-zooming))))) + (defn on-pointer-enter [in-viewport-ref] (mf/use-fn #(mf/set-ref-val! in-viewport-ref true))) @@ -358,6 +442,7 @@ (let [last-position (mf/use-var nil)] (mf/use-fn (fn [event] + (drag-debug-bump-move!) (let [raw-pt (dom/get-client-position event) pt (uwvv/point->viewport raw-pt) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 140b5d5dd1..6a4e51d4b7 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -58,7 +58,11 @@ parent? (dom/get-parent-with-data target "dont-clear-path")] (when (and drawing-path? (not parent?)) (st/emit! (dwe/clear-edition-mode)))))) - on-blur (mf/use-fn #(st/emit! (mse/->BlurEvent)))] + on-blur (mf/use-fn + (fn [] + ;; FIXME(drag-debug): remove with rest of drag-debug + (js/console.log "[drag-debug] window-blur -> BlurEvent") + (st/emit! (mse/->BlurEvent))))] (mf/with-effect [drawing-tool drawing-path?] (let [key (events/listen js/window EventType.POINTERDOWN on-pointer-down)] diff --git a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs index 41894c8d21..8198bc52d5 100644 --- a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs @@ -49,7 +49,11 @@ (mf/set-ref-val! ref node) (reset! viewport-ref node) (when (some? node) - (events/listen node EventType.MOUSELEAVE (fn [] (st/emit! (mse/->BlurEvent))))) + (events/listen node EventType.MOUSELEAVE + (fn [] + ;; FIXME(drag-debug): remove with rest of drag-debug + (js/console.log "[drag-debug] viewport-mouseleave -> BlurEvent") + (st/emit! (mse/->BlurEvent))))) (init-observer node on-change-bounds)))])) (defn point->viewport diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 1420030013..060fdc92d6 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -267,7 +267,9 @@ on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? path-editing? grid-editing? path-drawing? create-comment? space? panning z? read-only?) - on-pointer-up (actions/on-pointer-up disable-paste-ref) + on-pointer-up (actions/on-pointer-up disable-paste-ref) + on-pointer-cancel (actions/on-pointer-cancel) + on-lost-pointer-capture (actions/on-lost-pointer-capture) on-pointer-enter (actions/on-pointer-enter in-viewport-ref) on-pointer-leave (actions/on-pointer-leave in-viewport-ref) @@ -577,11 +579,13 @@ :on-drag-over on-drag-over :on-drag-end on-drag-end :on-drop on-drop - :on-pointer-down on-pointer-down - :on-pointer-enter on-pointer-enter - :on-pointer-leave on-pointer-leave - :on-pointer-move on-pointer-move - :on-pointer-up on-pointer-up} + :on-pointer-down on-pointer-down + :on-pointer-enter on-pointer-enter + :on-pointer-leave on-pointer-leave + :on-pointer-move on-pointer-move + :on-pointer-up on-pointer-up + :on-pointer-cancel on-pointer-cancel + :on-lost-pointer-capture on-lost-pointer-capture} [:defs ;; This clip is so the handlers are not over the rulers diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 737131d243..62ae893dda 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1091,26 +1091,69 @@ (= result 1)) false)) +(defn- gesture-active? + "True while the user is actively panning, zooming, or transforming. We + suppress the debounced full-quality render in this state because each + firing kicks off an async _render that ties up rAFs and the main thread, + delaying the next set-view-box and causing the debounce to expire again + mid-gesture (a feedback loop visible as multiple full renders during a + single continuous pan, especially on Firefox)." + [] + (let [ws (:workspace-local @st/state)] + (or (:panning ws) + (:zooming ws) + (some? (:transform ws))))) + +(defn- do-render-finish! [] + (when (and wasm/context-initialized? (not @wasm/context-lost?)) + (perf/begin-measure "render-finish") + ;; FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + (js/console.log "[pan-end-debug] render-finish FIRED") + (h/call wasm/internal-module "_set_view_end") + (perf/end-measure "render-finish") + ;; Use async _render: visible tiles render synchronously + ;; (no yield), interest-area tiles render progressively + ;; via rAF. _set_view_end already rebuilt the tile + ;; index. For pan, most tiles are cached so the render + ;; completes in the first frame. For zoom, interest- + ;; area tiles (~3 tile margin) don't block the main + ;; thread. + (h/call wasm/internal-module "_render" 0))) + (def render-finish - (letfn [(do-render [] - ;; Check if context is still initialized before executing - ;; to prevent errors when navigating quickly - (when (and wasm/context-initialized? (not @wasm/context-lost?)) - (perf/begin-measure "render-finish") - (h/call wasm/internal-module "_set_view_end") - (perf/end-measure "render-finish") - ;; Use async _render: visible tiles render synchronously - ;; (no yield), interest-area tiles render progressively - ;; via rAF. _set_view_end already rebuilt the tile - ;; index. For pan, most tiles are cached so the render - ;; completes in the first frame. For zoom, interest- - ;; area tiles (~3 tile margin) don't block the main - ;; thread. - (h/call wasm/internal-module "_render" 0)))] - (fns/debounce do-render DEBOUNCE_DELAY_MS))) + (let [debounced (fns/debounce do-render-finish! DEBOUNCE_DELAY_MS)] + (fn [] + ;; While a gesture is active, suppress the debounced fire. The + ;; gesture-end watcher below will trigger do-render-finish! exactly + ;; once when the gesture ends, eliminating the mid-gesture feedback + ;; loop entirely. + (when-not (gesture-active?) + (debounced))))) + +;; Fire the deferred full render the moment the gesture ends. We watch the +;; store rather than coupling viewport.cljs to api.cljs (which would risk a +;; circular dep) — render-wasm already depends on app.main.store. +(defonce ^:private _gesture-end-watcher + (add-watch st/state + ::gesture-end-watcher + (fn [_ _ old-state new-state] + (let [old-ws (:workspace-local old-state) + new-ws (:workspace-local new-state) + was-active? (or (:panning old-ws) + (:zooming old-ws) + (some? (:transform old-ws))) + now-active? (or (:panning new-ws) + (:zooming new-ws) + (some? (:transform new-ws)))] + (when (and was-active? (not now-active?)) + ;; FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + (js/console.log "[pan-end-debug] gesture ended -> flushing render") + (do-render-finish!)))))) (defn set-view-box [zoom vbox] + ;; FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + (js/console.log "[pan-end-debug] set-view-box CALLED (vbox:" (:x vbox) (:y vbox) ")") (perf/begin-measure "set-view-box") (h/call wasm/internal-module "_set_view_start") (h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index bd6e26d1fa..2f5d7ef175 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -443,6 +443,9 @@ pub extern "C" fn set_view_start() -> Result<()> { pub extern "C" fn set_view_end() -> Result<()> { with_state_mut!(state, { performance::begin_measure!("set_view_end"); + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + let _start_set_view_end = performance::get_time(); + println!("[pan-end-debug] set_view_end: enter"); let render_state = get_render_state(); render_state.options.set_fast_mode(false); render_state.cancel_animation_frame(); @@ -471,6 +474,11 @@ pub extern "C" fn set_view_end() -> Result<()> { render_state.rebuild_tile_index(&state.shapes); } performance::end_measure!("set_view_end"); + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + println!( + "[pan-end-debug] set_view_end: {}ms", + performance::get_time() - _start_set_view_end + ); }); Ok(()) } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 3df32255b5..33c71b7e2e 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -1689,6 +1689,14 @@ impl RenderState { non_overlapping.push((*id, *bounds, *selrect)); } + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + println!( + "[pan-end-debug] rebuild candidates={} non_overlapping={}", + candidates.len(), + non_overlapping.len() + ); + let _start_snapshots = performance::get_time(); + // Snapshot from Backbuffer for each accepted shape. let scale = self.get_scale(); let vb_left = self.viewbox.area.left; @@ -1733,6 +1741,15 @@ impl RenderState { }, ); } + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + println!( + "[pan-end-debug] rebuild_crop_snapshots_total: {}ms", + performance::get_time() - _start_snapshots + ); + println!( + "[pan-end-debug] rebuild cached={}", + self.backbuffer_crop_cache.len() + ); } pub fn render_from_cache(&mut self, shapes: ShapesPoolRef) { @@ -2103,8 +2120,32 @@ impl RenderState { // A full-quality frame is now complete. Refresh Backbuffer and regenerate // the per-shape crop cache so interactive drags can reuse pixels. if !self.options.is_fast_mode() && !self.options.is_interactive_transform() { + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + println!("[pan-end-debug] post_full_render_block: enter"); + let _start_post_render = performance::get_time(); + + let _start_copy = performance::get_time(); self.surfaces.copy_target_to_backbuffer(); + println!( + "[pan-end-debug] copy_target_to_backbuffer: {}ms", + performance::get_time() - _start_copy + ); + + let _start_rebuild = performance::get_time(); self.rebuild_backbuffer_crop_cache(tree); + println!( + "[pan-end-debug] rebuild_backbuffer_crop_cache: {}ms", + performance::get_time() - _start_rebuild + ); + println!( + "[pan-end-debug] post_full_render_block: {}ms", + performance::get_time() - _start_post_render + ); + } else { + // FIXME(pan-end-debug): remove with rest of pan-end debug instrumentation + println!( + "[pan-end-debug] post_full_render_block: SKIPPED (fast_mode or interactive_transform still active)" + ); } wapi::notify_tiles_render_complete!(); performance::end_measure!("render");