This commit is contained in:
Elena Torro 2026-05-08 13:46:26 +02:00
parent 4e98dfb99f
commit 6d6f961b89
8 changed files with 227 additions and 34 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(())
}

View File

@ -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");