Merge pull request #7782 from penpot/alotor-wasm-thumbnails

 Render WASM dashboard thumbnails
This commit is contained in:
Aitor Moreno
2025-11-20 13:12:26 +01:00
committed by GitHub
11 changed files with 400 additions and 169 deletions

View File

@@ -96,7 +96,7 @@
;; loading all pages into memory for find the frame set for thumbnail. ;; loading all pages into memory for find the frame set for thumbnail.
(defn get-file-data-for-thumbnail (defn get-file-data-for-thumbnail
[{:keys [::db/conn] :as cfg} {:keys [data id] :as file}] [{:keys [::db/conn] :as cfg} {:keys [data id] :as file} strip-frames-with-thumbnails]
(letfn [;; function responsible on finding the frame marked to be (letfn [;; function responsible on finding the frame marked to be
;; used as thumbnail; the returned frame always have ;; used as thumbnail; the returned frame always have
;; the :page-id set to the page that it belongs. ;; the :page-id set to the page that it belongs.
@@ -173,7 +173,7 @@
;; Assoc the available thumbnails and prune not visible shapes ;; Assoc the available thumbnails and prune not visible shapes
;; for avoid transfer unnecessary data. ;; for avoid transfer unnecessary data.
:always strip-frames-with-thumbnails
(update :objects assoc-thumbnails page-id thumbs))))) (update :objects assoc-thumbnails page-id thumbs)))))
(def ^:private (def ^:private
@@ -186,7 +186,8 @@
[:map {:title "PartialFile"} [:map {:title "PartialFile"}
[:id ::sm/uuid] [:id ::sm/uuid]
[:revn {:min 0} ::sm/int] [:revn {:min 0} ::sm/int]
[:page [:map-of :keyword ::sm/any]]]) [:page [:map-of :keyword ::sm/any]]
[:strip-frames-with-thumbnails {:optional true} ::sm/boolean]])
(sv/defmethod ::get-file-data-for-thumbnail (sv/defmethod ::get-file-data-for-thumbnail
"Retrieves the data for generate the thumbnail of the file. Used "Retrieves the data for generate the thumbnail of the file. Used
@@ -195,7 +196,7 @@
::doc/module :files ::doc/module :files
::sm/params schema:get-file-data-for-thumbnail ::sm/params schema:get-file-data-for-thumbnail
::sm/result schema:partial-file} ::sm/result schema:partial-file}
[cfg {:keys [::rpc/profile-id file-id] :as params}] [cfg {:keys [::rpc/profile-id file-id strip-frames-with-thumbnails] :as params}]
(db/run! cfg (fn [{:keys [::db/conn] :as cfg}] (db/run! cfg (fn [{:keys [::db/conn] :as cfg}]
(files/check-read-permissions! conn profile-id file-id) (files/check-read-permissions! conn profile-id file-id)
@@ -205,14 +206,18 @@
file (bfc/get-file cfg file-id file (bfc/get-file cfg file-id
:realize? true :realize? true
:read-only? true)] :read-only? true)
strip-frames-with-thumbnails
(or (nil? strip-frames-with-thumbnails) ;; if not present, default to true
(true? strip-frames-with-thumbnails))]
(-> (cfeat/get-team-enabled-features cf/flags team) (-> (cfeat/get-team-enabled-features cf/flags team)
(cfeat/check-file-features! (:features file))) (cfeat/check-file-features! (:features file)))
{:file-id file-id {:file-id file-id
:revn (:revn file) :revn (:revn file)
:page (get-file-data-for-thumbnail cfg file)})))) :page (get-file-data-for-thumbnail cfg file strip-frames-with-thumbnails)}))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; MUTATION COMMANDS ;; MUTATION COMMANDS

View File

@@ -83,7 +83,7 @@
:source-map-detail-level :all}}} :source-map-detail-level :all}}}
:worker :worker
{:target :browser {:target :esm
:output-dir "resources/public/js/worker/" :output-dir "resources/public/js/worker/"
:asset-path "/js/worker" :asset-path "/js/worker"
:devtools {:browser-inject :main :devtools {:browser-inject :main

View File

@@ -67,7 +67,7 @@
:height (:height vbox) :height (:height vbox)
:fill color}]) :fill color}])
(defn- calculate-dimensions (defn calculate-dimensions
[objects aspect-ratio] [objects aspect-ratio]
(let [root-objects (ctst/get-root-objects objects)] (let [root-objects (ctst/get-root-objects objects)]
(if (empty? root-objects) (if (empty? root-objects)

View File

@@ -18,6 +18,7 @@
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.project :as dpj] [app.main.data.project :as dpj]
[app.main.data.team :as dtm] [app.main.data.team :as dtm]
[app.main.features :as features]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.rasterizer :as thr] [app.main.rasterizer :as thr]
[app.main.refs :as refs] [app.main.refs :as refs]
@@ -46,6 +47,8 @@
(log/set-level! :debug) (log/set-level! :debug)
(def thumbnail-width 252)
;; --- Grid Item Thumbnail ;; --- Grid Item Thumbnail
(defn- persist-thumbnail (defn- persist-thumbnail
@@ -56,15 +59,22 @@
(defn render-thumbnail (defn render-thumbnail
[file-id revn] [file-id revn]
(->> (mw/ask! {:cmd :thumbnails/generate-for-file (if (features/active-feature? @st/state "render-wasm/v1")
:revn revn (->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
:file-id file-id}) :revn revn
(rx/mapcat (fn [{:keys [fonts] :as result}] :file-id file-id
(->> (fonts/render-font-styles fonts) :width thumbnail-width}))
(rx/map (fn [styles] (->> (mw/ask! {:cmd :thumbnails/generate-for-file
(assoc result :revn revn
:styles styles :file-id file-id
:width 252)))))))) :width thumbnail-width})
(rx/mapcat
(fn [{:keys [fonts] :as result}]
(->> (fonts/render-font-styles fonts)
(rx/map (fn [styles]
(-> result
(assoc :styles styles
:width thumbnail-width))))))))))
(defn- ask-for-thumbnail (defn- ask-for-thumbnail
"Creates some hooks to handle the files thumbnails cache" "Creates some hooks to handle the files thumbnails cache"

View File

@@ -94,6 +94,23 @@
(set! wasm/internal-frame-id nil) (set! wasm/internal-frame-id nil)
(ug/dispatch! (ug/event "penpot:wasm:render")))) (ug/dispatch! (ug/event "penpot:wasm:render"))))
(defn render-sync
[]
(when wasm/context-initialized?
(h/call wasm/internal-module "_render_sync")
(set! wasm/internal-frame-id nil)))
(defn render-sync-shape
[id]
(when wasm/context-initialized?
(let [buffer (uuid/get-u32 id)]
(h/call wasm/internal-module "_render_sync_shape"
(aget buffer 0)
(aget buffer 1)
(aget buffer 2)
(aget buffer 3))
(set! wasm/internal-frame-id nil))))
(def set-view-render (def set-view-render
(fns/debounce (fns/debounce
(fn [ts] (fn [ts]
@@ -290,6 +307,13 @@
(aset textures new-id texture) (aset textures new-id texture)
new-id)) new-id))
(defn- retrieve-image
[url]
(rx/from
(-> (js/fetch url)
(p/then (fn [^js response] (.blob response)))
(p/then (fn [^js image] (js/createImageBitmap image))))))
(defn- fetch-image (defn- fetch-image
"Loads an image and creates a WebGL texture from it, passing the texture ID to WASM. "Loads an image and creates a WebGL texture from it, passing the texture ID to WASM.
This avoids decoding the image twice (once in browser, once in WASM)." This avoids decoding the image twice (once in browser, once in WASM)."
@@ -297,56 +321,50 @@
(let [url (cf/resolve-file-media {:id image-id} thumbnail?)] (let [url (cf/resolve-file-media {:id image-id} thumbnail?)]
{:key url {:key url
:thumbnail? thumbnail? :thumbnail? thumbnail?
:callback #(->> (p/create :callback
(fn [resolve reject] (fn []
(let [img (js/Image.) (->> (retrieve-image url)
on-load (fn [] (rx/map
(resolve img)) (fn [img]
on-error (fn [err] (when-let [gl (get-webgl-context)]
(reject err))] (let [texture (create-webgl-texture-from-image gl img)
(set! (.-crossOrigin img) "anonymous") texture-id (get-texture-id-for-gl-object texture)
(.addEventListener img "load" on-load) width (.-width ^js img)
(.addEventListener img "error" on-error) height (.-height ^js img)
(set! (.-src img) url)))) ;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail)
(rx/from) ;; + 4 bytes (texture ID) + 8 bytes (dimensions)
(rx/map (fn [img] total-bytes 48
(when-let [gl (get-webgl-context)] offset (mem/alloc->offset-32 total-bytes)
(let [texture (create-webgl-texture-from-image gl img) heap32 (mem/get-heap-u32)]
texture-id (get-texture-id-for-gl-object texture)
width (.-width ^js img)
height (.-height ^js img)
;; Header: 32 bytes (2 UUIDs) + 4 bytes (thumbnail) + 4 bytes (texture ID) + 8 bytes (dimensions)
total-bytes 48
offset (mem/alloc->offset-32 total-bytes)
heap32 (mem/get-heap-u32)]
;; 1. Set shape id (offset + 0 to offset + 3) ;; 1. Set shape id (offset + 0 to offset + 3)
(mem.h32/write-uuid offset heap32 shape-id) (mem.h32/write-uuid offset heap32 shape-id)
;; 2. Set image id (offset + 4 to offset + 7) ;; 2. Set image id (offset + 4 to offset + 7)
(mem.h32/write-uuid (+ offset 4) heap32 image-id) (mem.h32/write-uuid (+ offset 4) heap32 image-id)
;; 3. Set thumbnail flag as u32 (offset + 8) ;; 3. Set thumbnail flag as u32 (offset + 8)
(aset heap32 (+ offset 8) (if thumbnail? 1 0)) (aset heap32 (+ offset 8) (if thumbnail? 1 0))
;; 4. Set texture ID (offset + 9) ;; 4. Set texture ID (offset + 9)
(aset heap32 (+ offset 9) texture-id) (aset heap32 (+ offset 9) texture-id)
;; 5. Set width (offset + 10) ;; 5. Set width (offset + 10)
(aset heap32 (+ offset 10) width) (aset heap32 (+ offset 10) width)
;; 6. Set height (offset + 11) ;; 6. Set height (offset + 11)
(aset heap32 (+ offset 11) height) (aset heap32 (+ offset 11) height)
(h/call wasm/internal-module "_store_image_from_texture") (h/call wasm/internal-module "_store_image_from_texture")
true)))) true))))
(rx/catch (fn [cause] (rx/catch
(log/error :hint "Could not fetch image" (fn [cause]
:image-id image-id (log/error :hint "Could not fetch image"
:thumbnail? thumbnail? :image-id image-id
:url url :thumbnail? thumbnail?
:cause cause) :url url
(rx/empty))))})) :cause cause)
(rx/empty)))))}))
(defn- get-fill-images (defn- get-fill-images
[leaf] [leaf]
@@ -961,26 +979,30 @@
:dimensions (get-text-dimensions id)}))))) :dimensions (get-text-dimensions id)})))))
(defn process-pending (defn process-pending
[shapes thumbnails full on-complete] ([shapes thumbnails full on-complete]
(let [pending-thumbnails (process-pending shapes thumbnails full nil on-complete))
(d/index-by :key :callback thumbnails) ([shapes thumbnails full on-render on-complete]
(let [pending-thumbnails
(d/index-by :key :callback thumbnails)
pending-full pending-full
(d/index-by :key :callback full)] (d/index-by :key :callback full)]
(->> (rx/concat (->> (rx/concat
(->> (rx/from (vals pending-thumbnails)) (->> (rx/from (vals pending-thumbnails))
(rx/merge-map (fn [callback] (callback))) (rx/merge-map (fn [callback] (callback)))
(rx/reduce conj [])) (rx/reduce conj []))
(->> (rx/from (vals pending-full)) (->> (rx/from (vals pending-full))
(rx/mapcat (fn [callback] (callback))) (rx/mapcat (fn [callback] (callback)))
(rx/reduce conj []))) (rx/reduce conj [])))
(rx/subs! (rx/subs!
(fn [_] (fn [_]
(update-text-layouts shapes) (update-text-layouts shapes)
(request-render "pending-finished")) (if on-render
noop-fn (on-render)
on-complete)))) (request-render "pending-finished")))
noop-fn
on-complete)))))
(defn process-object (defn process-object
[shape] [shape]
@@ -988,24 +1010,26 @@
(process-pending [shape] thumbnails full noop-fn))) (process-pending [shape] thumbnails full noop-fn)))
(defn set-objects (defn set-objects
[objects] ([objects]
(perf/begin-measure "set-objects") (set-objects objects nil))
(let [shapes (into [] (vals objects)) ([objects render-callback]
total-shapes (count shapes) (perf/begin-measure "set-objects")
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]} (let [shapes (into [] (vals objects))
{:keys [thumbnails full]} total-shapes (count shapes)
(loop [index 0 thumbnails-acc [] full-acc []] ;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
(if (< index total-shapes) {:keys [thumbnails full]}
(let [shape (nth shapes index) (loop [index 0 thumbnails-acc [] full-acc []]
{:keys [thumbnails full]} (set-object objects shape)] (if (< index total-shapes)
(recur (inc index) (let [shape (nth shapes index)
(into thumbnails-acc thumbnails) {:keys [thumbnails full]} (set-object objects shape)]
(into full-acc full))) (recur (inc index)
{:thumbnails thumbnails-acc :full full-acc}))] (into thumbnails-acc thumbnails)
(perf/end-measure "set-objects") (into full-acc full)))
(process-pending shapes thumbnails full {:thumbnails thumbnails-acc :full full-acc}))]
(fn [] (perf/end-measure "set-objects")
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))) (process-pending shapes thumbnails full render-callback
(fn []
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode (defn clear-focus-mode
[] []
@@ -1132,14 +1156,16 @@
(request-render "set-modifiers"))))) (request-render "set-modifiers")))))
(defn initialize-viewport (defn initialize-viewport
[base-objects zoom vbox background] ([base-objects zoom vbox background]
(let [rgba (sr-clr/hex->u32argb background 1) (initialize-viewport base-objects zoom vbox background nil))
shapes (into [] (vals base-objects)) ([base-objects zoom vbox background callback]
total-shapes (count shapes)] (let [rgba (sr-clr/hex->u32argb background 1)
(h/call wasm/internal-module "_set_canvas_background" rgba) shapes (into [] (vals base-objects))
(h/call wasm/internal-module "_set_view" zoom (- (:x vbox)) (- (:y vbox))) total-shapes (count shapes)]
(h/call wasm/internal-module "_init_shapes_pool" total-shapes) (h/call wasm/internal-module "_set_canvas_background" rgba)
(set-objects base-objects))) (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))))
(def ^:private default-context-options (def ^:private default-context-options
#js {:antialias false #js {:antialias false
@@ -1160,8 +1186,10 @@
(defn set-canvas-size (defn set-canvas-size
[canvas] [canvas]
(set! (.-width canvas) (* dpr (.-clientWidth ^js canvas))) (let [width (or (.-clientWidth ^js canvas) (.-width ^js canvas))
(set! (.-height canvas) (* dpr (.-clientHeight ^js canvas)))) height (or (.-clientHeight ^js canvas) (.-height ^js canvas))]
(set! (.-width canvas) (* dpr width))
(set! (.-height canvas) (* dpr height))))
(defn- get-browser (defn- get-browser
[] []
@@ -1274,51 +1302,59 @@
(mem/free) (mem/free)
content))) content)))
(defn init-wasm-module
[module]
(let [default-fn (unchecked-get module "default")
serializers
#js
{:blur-type (unchecked-get module "RawBlurType")
:blend-mode (unchecked-get module "RawBlendMode")
:bool-type (unchecked-get module "RawBoolType")
:font-style (unchecked-get module "RawFontStyle")
:flex-direction (unchecked-get module "RawFlexDirection")
:grid-direction (unchecked-get module "RawGridDirection")
:grow-type (unchecked-get module "RawGrowType")
:align-items (unchecked-get module "RawAlignItems")
:align-self (unchecked-get module "RawAlignSelf")
:align-content (unchecked-get module "RawAlignContent")
:justify-items (unchecked-get module "RawJustifyItems")
:justify-content (unchecked-get module "RawJustifyContent")
:justify-self (unchecked-get module "RawJustifySelf")
:wrap-type (unchecked-get module "RawWrapType")
:grid-track-type (unchecked-get module "RawGridTrackType")
:shadow-style (unchecked-get module "RawShadowStyle")
:stroke-style (unchecked-get module "RawStrokeStyle")
:stroke-cap (unchecked-get module "RawStrokeCap")
:shape-type (unchecked-get module "RawShapeType")
:constraint-h (unchecked-get module "RawConstraintH")
:constraint-v (unchecked-get module "RawConstraintV")
:sizing (unchecked-get module "RawSizing")
:vertical-align (unchecked-get module "RawVerticalAlign")
:fill-data (unchecked-get module "RawFillData")
:text-align (unchecked-get module "RawTextAlign")
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default-fn)))
(defonce module (defonce module
(delay (delay
(if (exists? js/dynamicImport) (if (exists? js/dynamicImport)
(let [uri (cf/resolve-static-asset "js/render_wasm.js")] (let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(->> (js/dynamicImport (str uri)) (->> (js/dynamicImport (str uri))
(p/mcat (fn [module] (p/mcat init-wasm-module)
(let [default (unchecked-get module "default") (p/fmap
serializers #js{:blur-type (unchecked-get module "RawBlurType") (fn [default]
:blend-mode (unchecked-get module "RawBlendMode") (set! wasm/internal-module default)
:bool-type (unchecked-get module "RawBoolType") true))
:font-style (unchecked-get module "RawFontStyle") (p/merr
:flex-direction (unchecked-get module "RawFlexDirection") (fn [cause]
:grid-direction (unchecked-get module "RawGridDirection") (js/console.error cause)
:grow-type (unchecked-get module "RawGrowType") (p/resolved false)))))
:align-items (unchecked-get module "RawAlignItems")
:align-self (unchecked-get module "RawAlignSelf")
:align-content (unchecked-get module "RawAlignContent")
:justify-items (unchecked-get module "RawJustifyItems")
:justify-content (unchecked-get module "RawJustifyContent")
:justify-self (unchecked-get module "RawJustifySelf")
:wrap-type (unchecked-get module "RawWrapType")
:grid-track-type (unchecked-get module "RawGridTrackType")
:shadow-style (unchecked-get module "RawShadowStyle")
:stroke-style (unchecked-get module "RawStrokeStyle")
:stroke-cap (unchecked-get module "RawStrokeCap")
:shape-type (unchecked-get module "RawShapeType")
:constraint-h (unchecked-get module "RawConstraintH")
:constraint-v (unchecked-get module "RawConstraintV")
:sizing (unchecked-get module "RawSizing")
:vertical-align (unchecked-get module "RawVerticalAlign")
:fill-data (unchecked-get module "RawFillData")
:text-align (unchecked-get module "RawTextAlign")
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default))))
(p/fmap (fn [default]
(set! wasm/internal-module default)
true))
(p/merr (fn [cause]
(js/console.error cause)
(p/resolved false)))))
(p/resolved false)))) (p/resolved false))))

View File

@@ -89,7 +89,7 @@
(defn init (defn init
"Return a initialized webworker instance." "Return a initialized webworker instance."
[path on-error] [path on-error]
(let [instance (js/Worker. path) (let [instance (js/Worker. path #js {:type "module"})
bus (rx/subject) bus (rx/subject)
worker (Worker. instance (rx/to-observable bus)) worker (Worker. instance (rx/to-observable bus))

View File

@@ -7,16 +7,24 @@
(ns app.worker.thumbnails (ns app.worker.thumbnails
(:require (:require
["react-dom/server" :as rds] ["react-dom/server" :as rds]
[app.common.data.macros :as dm]
[app.common.geom.rect :as grc]
[app.common.geom.shapes.bounds :as gsb]
[app.common.logging :as log] [app.common.logging :as log]
[app.common.types.color :as cc]
[app.common.uri :as u] [app.common.uri :as u]
[app.config :as cf] [app.config :as cf]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.render :as render] [app.main.render :as render]
[app.render-wasm.api :as wasm.api]
[app.render-wasm.wasm :as wasm]
[app.util.http :as http] [app.util.http :as http]
[app.worker.impl :as impl] [app.worker.impl :as impl]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [promesa.core :as p]
[rumext.v2 :as mf]
[shadow.esm :refer (dynamic-import)]))
(log/set-level! :trace) (log/set-level! :trace)
@@ -42,11 +50,11 @@
:http-body body}))) :http-body body})))
(defn- request-data-for-thumbnail (defn- request-data-for-thumbnail
[file-id revn] [file-id revn strip-frames-with-thumbnails]
(let [path "api/main/methods/get-file-data-for-thumbnail" (let [path "api/main/methods/get-file-data-for-thumbnail"
params {:file-id file-id params {:file-id file-id
:revn revn :revn revn
:strip-frames-with-thumbnails true} :strip-frames-with-thumbnails strip-frames-with-thumbnails}
request {:method :get request {:method :get
:uri (u/join cf/public-uri path) :uri (u/join cf/public-uri path)
:credentials "include" :credentials "include"
@@ -86,5 +94,89 @@
(defmethod impl/handler :thumbnails/generate-for-file (defmethod impl/handler :thumbnails/generate-for-file
[{:keys [file-id revn] :as message} _] [{:keys [file-id revn] :as message} _]
(->> (request-data-for-thumbnail file-id revn) (->> (request-data-for-thumbnail file-id revn true)
(rx/map render-thumbnail))) (rx/map render-thumbnail)))
(def init-wasm
(delay
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(-> (dynamic-import (str uri))
(p/then #(wasm.api/init-wasm-module %))
(p/then #(set! wasm/internal-module %))))))
(mf/defc svg-wrapper
[{:keys [data-uri background width height]}]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
:style {:width "100%"
:height "100%"
:background background}
:fill "none"
:viewBox (dm/str "0 0 " width " " height)}
[:image {:xlinkHref data-uri
:width width
:height height}]])
(defn blob->uri
[blob]
(.readAsDataURL (js/FileReaderSync.) blob))
(def thumbnail-aspect-ratio (/ 2 3))
(defmethod impl/handler :thumbnails/generate-for-file-wasm
[{:keys [file-id revn width] :as message} _]
(->> (rx/from @init-wasm)
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
(rx/mapcat
(fn [{:keys [page] :as file}]
(rx/create
(fn [subs]
(try
(let [background-color (or (:background page) cc/canvas)
height (* width thumbnail-aspect-ratio)
canvas (js/OffscreenCanvas. width height)
init? (wasm.api/init-canvas-context canvas)]
(if init?
(let [objects (:objects page)
frame (some->> page :thumbnail-frame-id (get objects))
vbox (if frame
(-> (gsb/get-object-bounds objects frame)
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
(render/calculate-dimensions objects thumbnail-aspect-ratio))
zoom (/ width (:width vbox))]
(wasm.api/initialize-viewport
objects zoom vbox background-color
(fn []
(if frame
(wasm.api/render-sync-shape (:id frame))
(wasm.api/render-sync))
(-> (.convertToBlob canvas)
(p/then
(fn [blob]
(let [data
(rds/renderToStaticMarkup
(mf/element
svg-wrapper
#js {:data-uri (blob->uri blob)
:width width
:height height
:background background-color}))]
(rx/push! subs {:data data :file-id file-id :revn revn}))))
(p/catch #(do (.error js/console %)
(rx/error! subs %)))
(p/finally #(rx/end! subs))))))
(do (rx/error! subs "Error loading webgl context")
(rx/end! subs)))
nil)
(catch :default err
(.error js/console err)
(rx/error! subs err)
(rx/end! subs)))))))))

View File

@@ -1,8 +1,16 @@
addToLibrary({ addToLibrary({
wapi_requestAnimationFrame: function wapi_requestAnimationFrame() { wapi_requestAnimationFrame: function wapi_requestAnimationFrame() {
return window.requestAnimationFrame(Module._process_animation_frame); if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
setTimeout(Module._process_animation_frame);
} else {
return window.requestAnimationFrame(Module._process_animation_frame);
}
}, },
wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) { wapi_cancelAnimationFrame: function wapi_cancelAnimationFrame(frameId) {
return window.cancelAnimationFrame(frameId); if (typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope) {
clearTimeout(frameId);
} else {
return window.cancelAnimationFrame(frameId);
}
} }
}); });

View File

@@ -149,6 +149,27 @@ pub extern "C" fn render(_: i32) {
}); });
} }
#[no_mangle]
pub extern "C" fn render_sync() {
with_state_mut!(state, {
state.rebuild_tiles();
state
.render_sync(performance::get_time())
.expect("Error rendering");
});
}
#[no_mangle]
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
with_state_mut!(state, {
let id = uuid_from_u32_quartet(a, b, c, d);
state.rebuild_tiles_from(Some(&id));
state
.render_sync_shape(&id, performance::get_time())
.expect("Error rendering");
});
}
#[no_mangle] #[no_mangle]
pub extern "C" fn render_from_cache(_: i32) { pub extern "C" fn render_from_cache(_: i32) {
with_state_mut!(state, { with_state_mut!(state, {

View File

@@ -867,7 +867,13 @@ impl RenderState {
} }
} }
pub fn start_render_loop(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> { pub fn start_render_loop(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
sync_render: bool,
) -> Result<(), String> {
let scale = self.get_scale(); let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale); self.tile_viewbox.update(self.viewbox, scale);
@@ -917,20 +923,27 @@ impl RenderState {
self.current_tile = None; self.current_tile = None;
self.render_in_progress = true; self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None); self.apply_drawing_to_render_canvas(None);
self.process_animation_frame(tree, timestamp)?;
if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?;
} else {
self.process_animation_frame(base_object, tree, timestamp)?;
}
performance::end_measure!("start_render_loop"); performance::end_measure!("start_render_loop");
Ok(()) Ok(())
} }
pub fn process_animation_frame( pub fn process_animation_frame(
&mut self, &mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
) -> Result<(), String> { ) -> Result<(), String> {
performance::begin_measure!("process_animation_frame"); performance::begin_measure!("process_animation_frame");
if self.render_in_progress { if self.render_in_progress {
if tree.len() != 0 { if tree.len() != 0 {
self.render_shape_tree_partial(tree, timestamp)?; self.render_shape_tree_partial(base_object, tree, timestamp, true)?;
} else { } else {
println!("Empty tree"); println!("Empty tree");
} }
@@ -947,6 +960,22 @@ impl RenderState {
Ok(()) Ok(())
} }
pub fn render_shape_tree_sync(
&mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef,
timestamp: i32,
) -> Result<(), String> {
if tree.len() != 0 {
self.render_shape_tree_partial(base_object, tree, timestamp, false)?;
} else {
println!("Empty tree");
}
self.flush_and_submit();
Ok(())
}
#[inline] #[inline]
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool { pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
iteration % NODE_BATCH_THRESHOLD == 0 iteration % NODE_BATCH_THRESHOLD == 0
@@ -1215,6 +1244,7 @@ impl RenderState {
&mut self, &mut self,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
allow_stop: bool,
) -> Result<(bool, bool), String> { ) -> Result<(bool, bool), String> {
let mut iteration = 0; let mut iteration = 0;
let mut is_empty = true; let mut is_empty = true;
@@ -1495,7 +1525,7 @@ impl RenderState {
} }
// We try to avoid doing too many calls to get_time // We try to avoid doing too many calls to get_time
if self.should_stop_rendering(iteration, timestamp) { if allow_stop && self.should_stop_rendering(iteration, timestamp) {
return Ok((is_empty, true)); return Ok((is_empty, true));
} }
iteration += 1; iteration += 1;
@@ -1505,8 +1535,10 @@ impl RenderState {
pub fn render_shape_tree_partial( pub fn render_shape_tree_partial(
&mut self, &mut self,
base_object: Option<&Uuid>,
tree: ShapesPoolRef, tree: ShapesPoolRef,
timestamp: i32, timestamp: i32,
allow_stop: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let mut should_stop = false; let mut should_stop = false;
while !should_stop { while !should_stop {
@@ -1532,7 +1564,7 @@ impl RenderState {
} else { } else {
performance::begin_measure!("render_shape_tree::uncached"); performance::begin_measure!("render_shape_tree::uncached");
let (is_empty, early_return) = let (is_empty, early_return) =
self.render_shape_tree_partial_uncached(tree, timestamp)?; self.render_shape_tree_partial_uncached(tree, timestamp, allow_stop)?;
if early_return { if early_return {
return Ok(()); return Ok(());
@@ -1564,10 +1596,16 @@ impl RenderState {
.canvas(SurfaceId::Current) .canvas(SurfaceId::Current)
.clear(self.background_color); .clear(self.background_color);
let Some(root) = tree.get(&Uuid::nil()) else { let root_ids = {
return Err(String::from("Root shape not found")); if let Some(shape_id) = base_object {
vec![*shape_id]
} else {
let Some(root) = tree.get(&Uuid::nil()) else {
return Err(String::from("Root shape not found"));
};
root.children_ids(false)
}
}; };
let root_ids = root.children_ids(false);
// If we finish processing every node rendering is complete // If we finish processing every node rendering is complete
// let's check if there are more pending nodes // let's check if there are more pending nodes
@@ -1711,13 +1749,19 @@ impl RenderState {
performance::end_measure!("rebuild_tiles_shallow"); performance::end_measure!("rebuild_tiles_shallow");
} }
pub fn rebuild_tiles(&mut self, tree: ShapesPoolRef) { pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
performance::begin_measure!("rebuild_tiles"); performance::begin_measure!("rebuild_tiles");
self.tiles.invalidate(); self.tiles.invalidate();
let mut all_tiles = HashSet::<tiles::Tile>::new(); let mut all_tiles = HashSet::<tiles::Tile>::new();
let mut nodes = vec![Uuid::nil()]; let mut nodes = {
if let Some(base_id) = base_id {
vec![*base_id]
} else {
vec![Uuid::nil()]
}
};
while let Some(shape_id) = nodes.pop() { while let Some(shape_id) = nodes.pop() {
if let Some(shape) = tree.get(&shape_id) { if let Some(shape) = tree.get(&shape_id) {
@@ -1737,7 +1781,6 @@ impl RenderState {
for tile in all_tiles { for tile in all_tiles {
self.remove_cached_tile(tile); self.remove_cached_tile(tile);
} }
performance::end_measure!("rebuild_tiles"); performance::end_measure!("rebuild_tiles");
} }

View File

@@ -63,15 +63,27 @@ impl<'a> State<'a> {
self.render_state.render_from_cache(&self.shapes); self.render_state.render_from_cache(&self.shapes);
} }
pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state
.start_render_loop(None, &self.shapes, timestamp, true)?;
Ok(())
}
pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> {
self.render_state
.start_render_loop(Some(id), &self.shapes, timestamp, true)?;
Ok(())
}
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state self.render_state
.start_render_loop(&self.shapes, timestamp)?; .start_render_loop(None, &self.shapes, timestamp, false)?;
Ok(()) Ok(())
} }
pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> {
self.render_state self.render_state
.process_animation_frame(&self.shapes, timestamp)?; .process_animation_frame(None, &self.shapes, timestamp)?;
Ok(()) Ok(())
} }
@@ -162,7 +174,11 @@ impl<'a> State<'a> {
} }
pub fn rebuild_tiles(&mut self) { pub fn rebuild_tiles(&mut self) {
self.render_state.rebuild_tiles(&self.shapes); self.render_state.rebuild_tiles_from(&self.shapes, None);
}
pub fn rebuild_tiles_from(&mut self, base_id: Option<&Uuid>) {
self.render_state.rebuild_tiles_from(&self.shapes, base_id);
} }
pub fn rebuild_touched_tiles(&mut self) { pub fn rebuild_touched_tiles(&mut self) {