mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🔧 Improve page loading
This commit is contained in:
parent
83e9f85ccf
commit
d85d63ef3c
@ -289,7 +289,6 @@
|
||||
;; This prevents errors when processing changes from other pages
|
||||
(when shape
|
||||
(wasm.api/process-object shape))))))
|
||||
|
||||
(defn initialize-workspace
|
||||
([team-id file-id]
|
||||
(initialize-workspace team-id file-id nil))
|
||||
|
||||
@ -310,7 +310,8 @@
|
||||
(:y selected-frame))
|
||||
rule-area-size (/ rulers/ruler-area-size zoom)
|
||||
preview-blend (-> refs/workspace-preview-blend
|
||||
(mf/deref))]
|
||||
(mf/deref))
|
||||
shapes-loading? (mf/deref wasm.api/shapes-loading?)]
|
||||
|
||||
;; NOTE: We need this page-id dependency to react to it and reset the
|
||||
;; canvas, even though we are not using `page-id` inside the hook.
|
||||
@ -341,10 +342,14 @@
|
||||
init?
|
||||
(do
|
||||
(reset! canvas-init? true)
|
||||
;; Restore previous canvas pixels immediately after context initialization
|
||||
;; This happens before initialize-viewport is called
|
||||
(wasm.api/apply-canvas-blur)
|
||||
(wasm.api/restore-previous-canvas-pixels))
|
||||
(if (wasm.api/has-captured-pixels?)
|
||||
;; Page switch: restore previously captured pixels (blurred)
|
||||
(wasm.api/restore-previous-canvas-pixels)
|
||||
;; First load: try to draw a blurred page thumbnail
|
||||
(when-let [frame-id (get page :thumbnail-frame-id)]
|
||||
(when-let [uri (dm/get-in @st/state [:thumbnails frame-id])]
|
||||
(wasm.api/draw-thumbnail-to-canvas uri)))))
|
||||
|
||||
(pos? retries)
|
||||
(vreset! timeout-id-ref
|
||||
@ -387,8 +392,14 @@
|
||||
(when @canvas-init?
|
||||
(if (not @initialized?)
|
||||
(do
|
||||
(wasm.api/clear-canvas-pixels)
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox background)
|
||||
;; Keep the blurred previous-page preview (page switch) or
|
||||
;; blank canvas (first load) visible while shapes load.
|
||||
;; The loading overlay is suppressed because on-shapes-ready
|
||||
;; is set.
|
||||
(wasm.api/initialize-viewport
|
||||
base-objects zoom vbox background 1 nil
|
||||
(fn []
|
||||
(wasm.api/clear-canvas-pixels)))
|
||||
(reset! initialized? true)
|
||||
(mf/set-ref-val! last-file-version-id-ref file-version-id))
|
||||
(when (and (some? file-version-id)
|
||||
@ -497,7 +508,7 @@
|
||||
:width (max 0 (- (:width vbox) rule-area-size))
|
||||
:height (max 0 (- (:height vbox) rule-area-size))}]]]
|
||||
|
||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||
[:g {:style {:pointer-events (if (or disable-events? shapes-loading?) "none" "auto")}}
|
||||
;; Text editor handling:
|
||||
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
||||
(when show-text-editor?
|
||||
@ -596,15 +607,16 @@
|
||||
:alt? @alt?
|
||||
:shift? @shift?}])
|
||||
|
||||
[:> widgets/frame-titles*
|
||||
{:objects objects-modified
|
||||
:selected selected
|
||||
:zoom zoom
|
||||
:is-show-artboard-names show-artboard-names?
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select
|
||||
:focus focus}]
|
||||
(when-not shapes-loading?
|
||||
[:> widgets/frame-titles*
|
||||
{:objects objects-modified
|
||||
:selected selected
|
||||
:zoom zoom
|
||||
:is-show-artboard-names show-artboard-names?
|
||||
:on-frame-enter on-frame-enter
|
||||
:on-frame-leave on-frame-leave
|
||||
:on-frame-select on-frame-select
|
||||
:focus focus}])
|
||||
|
||||
(when show-prototypes?
|
||||
[:> widgets/frame-flows*
|
||||
|
||||
@ -80,10 +80,9 @@
|
||||
(def ^:const MAX_BUFFER_CHUNK_SIZE (* 256 1024))
|
||||
|
||||
(def ^:const DEBOUNCE_DELAY_MS 100)
|
||||
(def ^:const THROTTLE_DELAY_MS 10)
|
||||
|
||||
;; Number of shapes to process before yielding to browser
|
||||
(def ^:const SHAPES_CHUNK_SIZE 100)
|
||||
;; Time budget (ms) per chunk of shape processing before yielding to browser
|
||||
(def ^:private ^:const CHUNK_TIME_BUDGET_MS 8)
|
||||
;; Threshold below which we use synchronous processing (no chunking overhead)
|
||||
(def ^:const ASYNC_THRESHOLD 100)
|
||||
|
||||
@ -98,6 +97,12 @@
|
||||
(def capture-canvas-pixels webgl/capture-canvas-pixels)
|
||||
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
|
||||
(def clear-canvas-pixels webgl/clear-canvas-pixels)
|
||||
(def draw-thumbnail-to-canvas webgl/draw-thumbnail-to-canvas)
|
||||
|
||||
(defn has-captured-pixels?
|
||||
"Returns true if there are saved canvas pixels from a previous page."
|
||||
[]
|
||||
(some? wasm/canvas-pixels))
|
||||
|
||||
;; Re-export public text editor functions
|
||||
(def text-editor-focus text-editor/text-editor-focus)
|
||||
@ -123,13 +128,17 @@
|
||||
;;
|
||||
(def shape-wrapper-factory nil)
|
||||
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||
[]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(js/requestAnimationFrame (fn [_] (resolve nil))))))
|
||||
(let [^js ch (js/MessageChannel.)]
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses MessageChannel for near-zero delay (avoids setTimeout's 4ms minimum
|
||||
after nesting depth > 5). Same technique used by React's scheduler."
|
||||
[]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(set! (.-onmessage (.-port1 ch))
|
||||
(fn [_] (resolve nil)))
|
||||
(.postMessage (.-port2 ch) nil)))))
|
||||
|
||||
;; Based on app.main.render/object-svg
|
||||
(mf/defc object-svg
|
||||
@ -982,11 +991,18 @@
|
||||
(letfn [(do-render []
|
||||
;; Check if context is still initialized before executing
|
||||
;; to prevent errors when navigating quickly
|
||||
(when wasm/context-initialized?
|
||||
(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")
|
||||
(render (js/performance.now))))]
|
||||
;; 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)))
|
||||
|
||||
(defn set-view-box
|
||||
@ -1064,54 +1080,62 @@
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
(set-shape-text-images id content true)
|
||||
(let [is-text? (= type :text)
|
||||
pending_thumbnails (into [] (concat
|
||||
(when is-text? (set-shape-text-content id content))
|
||||
(when is-text? (set-shape-text-images id content true))
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending_full (into [] (concat
|
||||
(set-shape-text-images id content false)
|
||||
(when is-text? (set-shape-text-images id content false))
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending_thumbnails
|
||||
:full pending_full}))))
|
||||
|
||||
(defn update-text-layouts
|
||||
[shapes]
|
||||
(->> shapes
|
||||
(filter cfh/text-shape?)
|
||||
(map :id)
|
||||
(run!
|
||||
(fn [id]
|
||||
(defn- update-text-layouts
|
||||
"Synchronously update text layouts for all shapes and send rect updates
|
||||
to the worker index."
|
||||
[text-ids]
|
||||
(run! (fn [id]
|
||||
(f/update-text-layout id)
|
||||
(update-text-rect! id)))))
|
||||
(update-text-rect! id))
|
||||
text-ids))
|
||||
|
||||
(defn process-pending
|
||||
([shapes thumbnails full on-complete]
|
||||
(process-pending shapes thumbnails full nil on-complete))
|
||||
([shapes thumbnails full on-render on-complete]
|
||||
(let [pending-thumbnails
|
||||
(d/index-by :key :callback thumbnails)
|
||||
[shapes thumbnails full on-complete]
|
||||
(let [pending-thumbnails
|
||||
(d/index-by :key :callback thumbnails)
|
||||
|
||||
pending-full
|
||||
(d/index-by :key :callback full)]
|
||||
pending-full
|
||||
(d/index-by :key :callback full)]
|
||||
|
||||
(->> (rx/concat
|
||||
(->> (rx/from (vals pending-thumbnails))
|
||||
(rx/merge-map (fn [callback] (callback)))
|
||||
(rx/reduce conj []))
|
||||
(->> (rx/from (vals pending-full))
|
||||
(rx/mapcat (fn [callback] (callback)))
|
||||
(rx/reduce conj [])))
|
||||
(rx/subs!
|
||||
(fn [_]
|
||||
(update-text-layouts shapes)
|
||||
(if on-render
|
||||
(on-render)
|
||||
(request-render "pending-finished")))
|
||||
noop-fn
|
||||
on-complete)))))
|
||||
;; Run text layouts synchronously so shapes are immediately correct.
|
||||
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||
(when (seq text-ids)
|
||||
(update-text-layouts text-ids)))
|
||||
|
||||
(if (or (seq pending-thumbnails) (seq pending-full))
|
||||
(->> (rx/concat
|
||||
(->> (rx/from (vals pending-thumbnails))
|
||||
(rx/merge-map (fn [callback] (callback)))
|
||||
(rx/reduce conj []))
|
||||
(->> (rx/from (vals pending-full))
|
||||
(rx/mapcat (fn [callback] (callback)))
|
||||
(rx/reduce conj [])))
|
||||
(rx/subs!
|
||||
(fn [_]
|
||||
;; Fonts are now loaded — recompute text layouts so Skia
|
||||
;; uses the real metrics instead of fallback-font estimates.
|
||||
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||
(when (seq text-ids)
|
||||
(update-text-layouts text-ids)))
|
||||
(request-render "images-loaded"))
|
||||
noop-fn
|
||||
(fn [] (when on-complete (on-complete)))))
|
||||
;; No pending images — complete immediately.
|
||||
(when on-complete (on-complete)))))
|
||||
|
||||
(defn process-object
|
||||
[shape]
|
||||
@ -1119,86 +1143,115 @@
|
||||
(process-pending [shape] thumbnails full noop-fn)))
|
||||
|
||||
(defn- process-shapes-chunk
|
||||
"Process a chunk of shapes synchronously, returning accumulated pending operations.
|
||||
"Process shapes starting at `start-index` until the time budget is exhausted.
|
||||
Returns {:thumbnails [...] :full [...] :next-index n}"
|
||||
[shapes start-index chunk-size thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
end-index (min total (+ start-index chunk-size))]
|
||||
(loop [index start-index
|
||||
t-acc thumbnails-acc
|
||||
f-acc full-acc]
|
||||
(if (< index end-index)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into t-acc thumbnails)
|
||||
(into f-acc full)))
|
||||
{:thumbnails t-acc
|
||||
:full f-acc
|
||||
:next-index end-index}))))
|
||||
[shapes start-index thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
deadline (+ (js/performance.now) CHUNK_TIME_BUDGET_MS)]
|
||||
(let [result
|
||||
(loop [index start-index
|
||||
t-acc (transient thumbnails-acc)
|
||||
f-acc (transient full-acc)]
|
||||
(if (and (< index total)
|
||||
;; Check performance.now every 8 shapes to reduce overhead
|
||||
(or (pos? (bit-and (- index start-index) 7))
|
||||
(<= (js/performance.now) deadline)))
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(reduce conj! t-acc thumbnails)
|
||||
(reduce conj! f-acc full)))
|
||||
{:thumbnails (persistent! t-acc)
|
||||
:full (persistent! f-acc)
|
||||
:next-index index}))]
|
||||
result)))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in chunks, yielding to the browser between chunks.
|
||||
Returns a promise that resolves when all shapes are processed.
|
||||
|
||||
Renders a preview only periodically during loading to show progress,
|
||||
then does a full tile-based render at the end."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE))
|
||||
;; Render at 25%, 50%, 75% of loading
|
||||
render-at-chunks (set [(mth/floor (* total-chunks 0.25))
|
||||
(mth/floor (* total-chunks 0.5))
|
||||
(mth/floor (* total-chunks 0.75))])]
|
||||
"Asynchronously process shapes in time-budgeted chunks, yielding to the
|
||||
browser between chunks so the UI stays responsive.
|
||||
Returns a promise that resolves when all shapes are processed."
|
||||
[shapes render-callback on-shapes-ready]
|
||||
(let [total-shapes (count shapes)]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count]
|
||||
(letfn [(process-next-chunk [index thumbnails-acc full-acc]
|
||||
(if (< index total-shapes)
|
||||
;; Process one chunk
|
||||
;; Process one time-budgeted chunk
|
||||
(let [{:keys [thumbnails full next-index]}
|
||||
(process-shapes-chunk shapes index SHAPES_CHUNK_SIZE
|
||||
thumbnails-acc full-acc)
|
||||
new-chunk-count (inc chunk-count)]
|
||||
;; Only render at specific progress milestones
|
||||
(when (contains? render-at-chunks new-chunk-count)
|
||||
(render-preview!))
|
||||
|
||||
(process-shapes-chunk shapes index
|
||||
thumbnails-acc full-acc)]
|
||||
;; Yield to browser, then continue with next chunk
|
||||
(-> (yield-to-browser)
|
||||
(p/then (fn [_]
|
||||
(process-next-chunk next-index thumbnails full new-chunk-count)))))
|
||||
(process-next-chunk next-index thumbnails full)))))
|
||||
;; All chunks done - finalize
|
||||
(do
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails-acc full-acc noop-fn
|
||||
(fn []
|
||||
(end-shapes-loading!)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
|
||||
(resolve nil))))))]
|
||||
(process-next-chunk 0 [] [] 0))))))
|
||||
|
||||
;; Notify that shapes are loaded and tiles rebuilt
|
||||
(when on-shapes-ready (on-shapes-ready))
|
||||
;; Show shapes immediately: end loading overlay + unblock rendering
|
||||
(h/call wasm/internal-module "_end_loading")
|
||||
(end-shapes-loading!)
|
||||
|
||||
;; Text layouts must run after _end_loading (they
|
||||
;; depend on state that is only correct when loading
|
||||
;; is false). Each call touch_shape → touched_ids.
|
||||
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||
(when (seq text-ids)
|
||||
(update-text-layouts text-ids)))
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(request-render "set-objects-complete"))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
|
||||
(resolve nil)
|
||||
|
||||
;; Kick off image fetches in the background.
|
||||
;; The promise is already resolved so these don't
|
||||
;; block the caller.
|
||||
(let [pending-thumbnails (d/index-by :key :callback thumbnails-acc)
|
||||
pending-full (d/index-by :key :callback full-acc)]
|
||||
(when (or (seq pending-thumbnails) (seq pending-full))
|
||||
(->> (rx/concat
|
||||
(->> (rx/from (vals pending-thumbnails))
|
||||
(rx/merge-map (fn [callback] (callback)))
|
||||
(rx/reduce conj []))
|
||||
(->> (rx/from (vals pending-full))
|
||||
(rx/mapcat (fn [callback] (callback)))
|
||||
(rx/reduce conj [])))
|
||||
(rx/subs!
|
||||
(fn [_]
|
||||
;; Fonts are now loaded — recompute text
|
||||
;; layouts so Skia uses the real metrics
|
||||
;; instead of fallback-font estimates.
|
||||
(let [text-ids (into [] (comp (filter cfh/text-shape?) (map :id)) shapes)]
|
||||
(when (seq text-ids)
|
||||
(update-text-layouts text-ids)))
|
||||
(request-render "images-loaded"))
|
||||
noop-fn
|
||||
noop-fn)))))))]
|
||||
(process-next-chunk 0 [] []))))))
|
||||
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback]
|
||||
[shapes render-callback on-shapes-ready]
|
||||
(let [total-shapes (count shapes)
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(loop [index 0 thumbnails-acc (transient []) full-acc (transient [])]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(reduce conj! thumbnails-acc thumbnails)
|
||||
(reduce conj! full-acc full)))
|
||||
{:thumbnails (persistent! thumbnails-acc) :full (persistent! full-acc)}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(when on-shapes-ready (on-shapes-ready))
|
||||
(process-pending shapes thumbnails full
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(request-render "set-objects-sync-complete"))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
|
||||
|
||||
(defn- shapes-in-tree-order
|
||||
@ -1234,23 +1287,36 @@
|
||||
"Set all shape objects for rendering.
|
||||
|
||||
Shapes are processed in tree order (parents before children)
|
||||
to maintain proper shape reference consistency in WASM."
|
||||
to maintain proper shape reference consistency in WASM.
|
||||
|
||||
on-shapes-ready is an optional callback invoked right after shapes are
|
||||
loaded into WASM (and tiles rebuilt for async). It fires before image
|
||||
loading begins, allowing callers to reveal the page content during
|
||||
transitions."
|
||||
([objects]
|
||||
(set-objects objects nil))
|
||||
(set-objects objects nil nil))
|
||||
([objects render-callback]
|
||||
(set-objects objects render-callback nil))
|
||||
([objects render-callback on-shapes-ready]
|
||||
(perf/begin-measure "set-objects")
|
||||
(let [shapes (shapes-in-tree-order objects)
|
||||
total-shapes (count shapes)]
|
||||
(if (< total-shapes ASYNC_THRESHOLD)
|
||||
(set-objects-sync shapes render-callback)
|
||||
(set-objects-sync shapes render-callback on-shapes-ready)
|
||||
(do
|
||||
(begin-shapes-loading!)
|
||||
(h/call wasm/internal-module "_begin_loading")
|
||||
;; NOTE: to render a loading overlay in the future
|
||||
;; (when-not on-shapes-ready
|
||||
;; (h/call wasm/internal-module "_render_loading_overlay"))
|
||||
(try
|
||||
(-> (set-objects-async shapes render-callback)
|
||||
(-> (set-objects-async shapes render-callback on-shapes-ready)
|
||||
(p/catch (fn [error]
|
||||
(h/call wasm/internal-module "_end_loading")
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error))))
|
||||
(catch :default error
|
||||
(h/call wasm/internal-module "_end_loading")
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error)
|
||||
(throw error)))
|
||||
@ -1385,17 +1451,18 @@
|
||||
|
||||
(defn initialize-viewport
|
||||
([base-objects zoom vbox background]
|
||||
(initialize-viewport base-objects zoom vbox background 1 nil))
|
||||
(initialize-viewport base-objects zoom vbox background 1 nil nil))
|
||||
([base-objects zoom vbox background callback]
|
||||
(initialize-viewport base-objects zoom vbox background 1 callback))
|
||||
(initialize-viewport base-objects zoom vbox background 1 callback nil))
|
||||
([base-objects zoom vbox background background-opacity callback]
|
||||
(initialize-viewport base-objects zoom vbox background background-opacity callback nil))
|
||||
([base-objects zoom vbox background background-opacity callback on-shapes-ready]
|
||||
(let [rgba (sr-clr/hex->u32argb background background-opacity)
|
||||
shapes (into [] (vals base-objects))
|
||||
total-shapes (count shapes)]
|
||||
total-shapes (count (vals base-objects))]
|
||||
(h/call wasm/internal-module "_set_canvas_background" rgba)
|
||||
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox)))
|
||||
(h/call wasm/internal-module "_init_shapes_pool" total-shapes)
|
||||
(set-objects base-objects callback))))
|
||||
(set-objects base-objects callback on-shapes-ready))))
|
||||
|
||||
(def ^:private default-context-options
|
||||
#js {:antialias false
|
||||
|
||||
@ -9,7 +9,8 @@
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.util.dom :as dom]))
|
||||
[app.util.dom :as dom]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
@ -166,3 +167,32 @@ void main() {
|
||||
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
|
||||
image-data (js/ImageData. buffer width height)]
|
||||
(set! wasm/canvas-pixels image-data))))
|
||||
|
||||
(defn draw-thumbnail-to-canvas
|
||||
"Loads an image from `uri` and draws it stretched to fill the WebGL canvas.
|
||||
Returns a promise that resolves to true if drawn, false otherwise."
|
||||
[uri]
|
||||
(if (and uri wasm/canvas wasm/gl-context)
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(let [img (js/Image.)]
|
||||
(set! (.-crossOrigin img) "anonymous")
|
||||
(set! (.-onload img)
|
||||
(fn []
|
||||
(if wasm/gl-context
|
||||
(let [gl wasm/gl-context
|
||||
width (.-width wasm/canvas)
|
||||
height (.-height wasm/canvas)
|
||||
;; Draw image to an offscreen 2D canvas, scaled to fill
|
||||
canvas2d (js/OffscreenCanvas. width height)
|
||||
ctx2d (.getContext canvas2d "2d")]
|
||||
(.drawImage ctx2d img 0 0 width height)
|
||||
(let [image-data (.getImageData ctx2d 0 0 width height)]
|
||||
(draw-imagedata-to-webgl gl image-data))
|
||||
(resolve true))
|
||||
(resolve false))))
|
||||
(set! (.-onerror img)
|
||||
(fn [_]
|
||||
(resolve false)))
|
||||
(set! (.-src img) uri))))
|
||||
(p/resolved false)))
|
||||
|
||||
@ -244,6 +244,42 @@ pub extern "C" fn render_preview() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enter bulk-loading mode. While active, `state.loading` is `true`.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn begin_loading() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.loading = true;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Leave bulk-loading mode. Should be called after the first
|
||||
/// render so the loading flag is available during that render.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn end_loading() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.loading = false;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Draw a full-screen loading overlay (background + "Loading…" text).
|
||||
/// Called from CLJS right after begin_loading so the user sees
|
||||
/// immediate feedback while shapes are being processed.
|
||||
/// NOTE:
|
||||
/// This is currently not being used, but it's set there for testing purposes on
|
||||
/// upcoming tasks
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_loading_overlay() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.render_loading_overlay();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
@ -305,8 +341,10 @@ pub extern "C" fn set_view_start() -> Result<()> {
|
||||
}
|
||||
|
||||
/// Finishes a view interaction (zoom or pan). Rebuilds the tile index
|
||||
/// and invalidates the tile texture cache so the subsequent render
|
||||
/// re-draws all tiles at full quality (fast_mode is off at this point).
|
||||
/// and, for zoom changes, invalidates the tile texture cache so the
|
||||
/// subsequent render re-draws tiles at full quality.
|
||||
/// For pure pan (same zoom), cached tiles are preserved so only
|
||||
/// newly-visible tiles need rendering.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
@ -323,11 +361,21 @@ pub extern "C" fn set_view_end() -> Result<()> {
|
||||
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else if state.render_state.zoom_changed() {
|
||||
// Zoom changed: tile sizes differ so all cached tile
|
||||
// textures are invalid (wrong scale). Rebuild the tile
|
||||
// index and clear the tile texture cache, but *preserve*
|
||||
// the cache canvas so render_from_cache can show a scaled
|
||||
// preview of the old content while new tiles render.
|
||||
state.render_state.rebuild_tile_index(&state.shapes);
|
||||
state.render_state.surfaces.invalidate_tile_cache();
|
||||
} else {
|
||||
// Rebuild tile index + invalidate tile texture cache.
|
||||
// Cache canvas is preserved so render_from_cache can still
|
||||
// show a scaled preview during zoom.
|
||||
state.rebuild_tiles_shallow();
|
||||
// Pure pan at the same zoom level: tile contents have not
|
||||
// changed — only the viewport position moved. Update the
|
||||
// tile index (which tiles are in the interest area) but
|
||||
// keep cached tile textures so the render can blit them
|
||||
// instead of re-drawing every visible tile from scratch.
|
||||
state.render_state.rebuild_tile_index(&state.shapes);
|
||||
}
|
||||
|
||||
performance::end_measure!("set_view_end");
|
||||
|
||||
@ -658,6 +658,41 @@ impl RenderState {
|
||||
self.surfaces.reset(self.background_color);
|
||||
}
|
||||
|
||||
/// NOTE:
|
||||
/// This is currently not being used, but it's set there for testing purposes on
|
||||
/// upcoming tasks
|
||||
pub fn render_loading_overlay(&mut self) {
|
||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||
let skia::ISize { width, height } = canvas.base_layer_size();
|
||||
|
||||
canvas.save();
|
||||
|
||||
// Full-screen background rect
|
||||
let rect = skia::Rect::from_wh(width as f32, height as f32);
|
||||
let mut bg_paint = skia::Paint::default();
|
||||
bg_paint.set_color(self.background_color);
|
||||
bg_paint.set_style(skia::PaintStyle::Fill);
|
||||
canvas.draw_rect(rect, &bg_paint);
|
||||
|
||||
// Centered "Loading…" text
|
||||
let mut text_paint = skia::Paint::default();
|
||||
text_paint.set_color(skia::Color::GRAY);
|
||||
text_paint.set_anti_alias(true);
|
||||
|
||||
let font = self.fonts.debug_font();
|
||||
// FIXME
|
||||
let text = "Loading…";
|
||||
let (text_width, _) = font.measure_str(text, None);
|
||||
let metrics = font.metrics();
|
||||
let text_height = metrics.1.cap_height;
|
||||
let x = (width as f32 - text_width) / 2.0;
|
||||
let y = (height as f32 + text_height) / 2.0;
|
||||
canvas.draw_str(text, skia::Point::new(x, y), font, &text_paint);
|
||||
|
||||
canvas.restore();
|
||||
self.flush_and_submit();
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_canvas_at(&mut self, surface_id: SurfaceId) -> &skia::Canvas {
|
||||
self.surfaces.canvas(surface_id)
|
||||
@ -1559,6 +1594,16 @@ impl RenderState {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
} else {
|
||||
self.process_animation_frame(base_object, tree, timestamp)?;
|
||||
// Update cached_viewbox after visible tiles render
|
||||
// synchronously so that render_from_cache uses the correct
|
||||
// zoom ratio even if interest-area tiles are still rendering
|
||||
// asynchronously. Without this, panning right after a zoom
|
||||
// would keep scaling the Cache surface by the old zoom ratio
|
||||
// (pixelated/wrong-scale tiles) because the async render
|
||||
// never completes — each pan frame cancels it.
|
||||
if self.cache_cleared_this_render {
|
||||
self.cached_viewbox = self.viewbox;
|
||||
}
|
||||
}
|
||||
|
||||
performance::end_measure!("start_render_loop");
|
||||
@ -2576,6 +2621,23 @@ impl RenderState {
|
||||
tile_rect,
|
||||
self.background_color,
|
||||
);
|
||||
|
||||
// Also draw the cached tile to the Cache surface so
|
||||
// render_from_cache (used during pan) has the full scene.
|
||||
// apply_render_to_final_canvas clears Cache on the first
|
||||
// uncached tile, but cached tiles must also be present.
|
||||
if !self.options.is_fast_mode() {
|
||||
if !self.cache_cleared_this_render {
|
||||
self.surfaces.clear_cache(self.background_color);
|
||||
self.cache_cleared_this_render = true;
|
||||
}
|
||||
let aligned_rect = self.get_aligned_tile_bounds(current_tile);
|
||||
self.surfaces.draw_cached_tile_to_cache(
|
||||
current_tile,
|
||||
&aligned_rect,
|
||||
self.background_color,
|
||||
);
|
||||
}
|
||||
performance::end_measure!("render_shape_tree::cached");
|
||||
|
||||
if self.options.is_debug_visible() {
|
||||
|
||||
@ -594,6 +594,26 @@ impl Surfaces {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws a cached tile texture to the Cache surface at the given
|
||||
/// cache-aligned rect. This keeps the Cache surface in sync with
|
||||
/// Target so that `render_from_cache` (used during pan) has the
|
||||
/// full scene including tiles served from the texture cache.
|
||||
pub fn draw_cached_tile_to_cache(
|
||||
&mut self,
|
||||
tile: Tile,
|
||||
aligned_rect: &skia::Rect,
|
||||
color: skia::Color,
|
||||
) {
|
||||
if let Some(image) = self.tiles.get(tile) {
|
||||
let mut bg = skia::Paint::default();
|
||||
bg.set_color(color);
|
||||
self.cache.canvas().draw_rect(aligned_rect, &bg);
|
||||
self.cache
|
||||
.canvas()
|
||||
.draw_image_rect(&image, None, aligned_rect, &skia::Paint::default());
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the current tile directly to the target and cache surfaces without
|
||||
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||
|
||||
@ -26,6 +26,8 @@ pub(crate) struct State {
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool,
|
||||
pub saved_shapes: Option<ShapesPool>,
|
||||
/// True while the first bulk load of shapes is in progress.
|
||||
pub loading: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@ -36,8 +38,8 @@ impl State {
|
||||
current_id: None,
|
||||
current_browser: 0,
|
||||
shapes: ShapesPool::new(),
|
||||
// TODO: Maybe this can be moved to a different object
|
||||
saved_shapes: None,
|
||||
loading: false,
|
||||
})
|
||||
}
|
||||
|
||||
@ -285,12 +287,16 @@ impl State {
|
||||
}
|
||||
|
||||
pub fn touch_current(&mut self) {
|
||||
if let Some(current_id) = self.current_id {
|
||||
self.render_state.mark_touched(current_id);
|
||||
if !self.loading {
|
||||
if let Some(current_id) = self.current_id {
|
||||
self.render_state.mark_touched(current_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn touch_shape(&mut self, id: Uuid) {
|
||||
self.render_state.mark_touched(id);
|
||||
if !self.loading {
|
||||
self.render_state.mark_touched(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user