Merge pull request #8823 from penpot/elenatorro-13350-add-components-preview-using-render

🔧 Use wasm render for components thumbnail
This commit is contained in:
Alejandro Alonso 2026-03-31 09:45:53 +02:00 committed by GitHub
commit 153277d152
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 182 additions and 22 deletions

View File

@ -44,6 +44,7 @@
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.specialized-panel :as dwsp] [app.main.data.workspace.specialized-panel :as dwsp]
[app.main.data.workspace.thumbnails :as dwt] [app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.thumbnails-wasm :as dwt.wasm]
[app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt] [app.main.data.workspace.wasm-text :as dwwt]
@ -945,7 +946,12 @@
component (ctkl/get-component data component-id) component (ctkl/get-component data component-id)
page-id (:main-instance-page component) page-id (:main-instance-page component)
root-id (:main-instance-id component)] root-id (:main-instance-id component)]
(dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))) (if (and (= tag "component")
(features/active-feature? state "render-wasm/v1"))
;; WASM: render immediately, UI only — server persist happens
;; on the debounced path (update-component-thumbnail)
(dwt.wasm/render-thumbnail file-id page-id root-id)
(dwt/update-thumbnail file-id page-id root-id tag "update-component-thumbnail-sync"))))
(defn update-component-sync (defn update-component-sync
([shape-id file-id] (update-component-sync shape-id file-id nil)) ([shape-id file-id] (update-component-sync shape-id file-id nil))
@ -964,9 +970,12 @@
(dwu/start-undo-transaction undo-id) (dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group) (update-component shape-id undo-group)
;; These two calls are necessary for properly sync thumbnails ;; These calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page ;; when a main component does not live in the same page.
(update-component-thumbnail-sync state component-id file-id "frame") ;; When WASM is active, skip the "frame" tag (SVG-based) since
;; component previews are rendered locally via WASM.
(when-not (features/active-feature? state "render-wasm/v1")
(update-component-thumbnail-sync state component-id file-id "frame"))
(update-component-thumbnail-sync state component-id file-id "component") (update-component-thumbnail-sync state component-id file-id "component")
(sync-file current-file-id file-id :components component-id undo-group) (sync-file current-file-id file-id :components component-id undo-group)
@ -991,13 +1000,21 @@
(dwu/commit-undo-transaction undo-id))))))) (dwu/commit-undo-transaction undo-id)))))))
(defn update-component-thumbnail (defn update-component-thumbnail
"Update the thumbnail of the component with the given id, in the "Persist the thumbnail of the component to the server.
current file and in the imported libraries." For WASM, the UI is already up-to-date from the immediate render in
update-component-thumbnail-sync, so this only persists.
For SVG, this does the full render + persist."
[component-id file-id] [component-id file-id]
(ptk/reify ::update-component-thumbnail (ptk/reify ::update-component-thumbnail
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))) (if (features/active-feature? state "render-wasm/v1")
(let [data (dsh/lookup-file-data state file-id)
component (ctkl/get-component data component-id)
page-id (:main-instance-page component)
root-id (:main-instance-id component)]
(rx/of (dwt.wasm/persist-thumbnail file-id page-id root-id)))
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))))
(defn- find-shape-index (defn- find-shape-index
[objects id shape-id] [objects id shape-id]
@ -1373,9 +1390,18 @@
(->> (rx/from changed-components) (->> (rx/from changed-components)
(rx/map #(component-changed % (:id old-data) undo-group)))) (rx/map #(component-changed % (:id old-data) undo-group))))
;; even if save-undo? is false, we need to update the :modified-date of the component ;; even if save-undo? is false, we need to update the :modified-date of the component
;; (for example, for undos) ;; (for example, for undos). When WASM is active, also re-render the thumbnail
(->> (rx/from changed-components) ;; so undo/redo visually updates component previews.
(rx/map touch-component))) (->> (mapcat (fn [component-id]
(if (features/active-feature? @st/state "render-wasm/v1")
(let [component (ctkl/get-component old-data component-id)]
[(touch-component component-id)
(dwt.wasm/render-thumbnail (:id old-data)
(:main-instance-page component)
(:main-instance-id component))])
[(touch-component component-id)]))
changed-components)
(rx/from)))
(rx/empty))))) (rx/empty)))))
@ -1390,18 +1416,32 @@
(rx/debounce 5000) (rx/debounce 5000)
(rx/tap #(log/trc :hint "buffer initialized")))] (rx/tap #(log/trc :hint "buffer initialized")))]
(when (contains? cf/flags :component-thumbnails) (when (or (contains? cf/flags :component-thumbnails)
(->> (rx/merge (features/active-feature? @st/state "render-wasm/v1"))
changes-s (let [wasm? (features/active-feature? @st/state "render-wasm/v1")]
(->> (rx/merge
changes-s
(->> changes-s ;; WASM: render thumbnails immediately for instant UI feedback
(rx/map deref) (if wasm?
(rx/buffer-until notifier-s) (->> changes-s
(rx/mapcat #(into #{} %)) (rx/filter (ptk/type? ::component-changed))
(rx/map (fn [[component-id file-id]] (rx/map deref)
(update-component-thumbnail component-id file-id))))) (rx/map (fn [[component-id file-id]]
(update-component-thumbnail-sync @st/state component-id file-id "component"))))
(rx/empty))
(rx/take-until stopper-s))))))) ;; Persist thumbnails to the server in batches after user
;; becomes inactive for 5 seconds.
(->> changes-s
(rx/filter (ptk/type? ::component-changed))
(rx/map deref)
(rx/buffer-until notifier-s)
(rx/mapcat #(into #{} %))
(rx/map (fn [[component-id file-id]]
(update-component-thumbnail component-id file-id)))))
(rx/take-until stopper-s))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Backend interactions ;; Backend interactions

View File

@ -118,7 +118,7 @@
(rx/ignore)))) (rx/ignore))))
(rx/empty))))))) (rx/empty)))))))
(defn- assoc-thumbnail (defn assoc-thumbnail
[object-id uri] [object-id uri]
(let [prev-uri* (volatile! nil)] (let [prev-uri* (volatile! nil)]
(ptk/reify ::assoc-thumbnail (ptk/reify ::assoc-thumbnail

View File

@ -0,0 +1,118 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.thumbnails-wasm
"WASM-based component thumbnail rendering.
Renders component previews using the existing workspace WASM context
via render-shape-pixels (SurfaceId::Export), avoiding a separate
WASM module in the worker.
Two-phase design:
- render-thumbnail: immediate WASM render → UI update (no server)
- persist-thumbnail: pushes current data-uri to the server (debounced)"
(:require
[app.common.data.macros :as dm]
[app.common.logging :as l]
[app.common.thumbnails :as thc]
[app.common.time :as ct]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.repo :as rp]
[app.render-wasm.api :as wasm.api]
[app.util.webapi :as wapi]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(l/set-level! :warn)
(defn- png-bytes->data-uri
"Converts a Uint8Array of PNG bytes to a data:image/png;base64 URI."
[png-bytes]
(let [blob (wapi/create-blob png-bytes "image/png")
reader (js/FileReader.)]
(js/Promise.
(fn [resolve reject]
(set! (.-onload reader)
(fn [] (resolve (.-result reader))))
(set! (.-onerror reader)
(fn [e] (reject e)))
(.readAsDataURL reader blob)))))
(defn- render-component-pixels
"Renders a component frame using the workspace WASM context.
Returns an observable that emits a data-uri string.
Deferred by one animation frame so that process-shape-changes!
has time to sync all child shapes to WASM memory first."
[frame-id]
(rx/create
(fn [subs]
(js/requestAnimationFrame
(fn [_]
(try
(let [png-bytes (wasm.api/render-shape-pixels frame-id 1)]
(if (or (nil? png-bytes) (zero? (.-length png-bytes)))
(do (js/console.error "[thumbnails] render-shape-pixels returned empty for" (str frame-id))
(rx/end! subs))
(.then (png-bytes->data-uri png-bytes)
(fn [data-uri]
(rx/push! subs data-uri)
(rx/end! subs))
(fn [err]
(rx/error! subs err)))))
(catch :default err
(rx/error! subs err)))))
nil)))
(defn render-thumbnail
"Renders a component thumbnail via WASM and updates the UI immediately.
Does NOT persist to the server — persistence is handled separately
by `persist-thumbnail` on a debounced schedule."
[file-id page-id frame-id]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
(ptk/reify ::render-thumbnail
cljs.core/IDeref
(-deref [_] object-id)
ptk/WatchEvent
(watch [_ _ stream]
(let [tp (ct/tpoint-ms)]
(->> (render-component-pixels frame-id)
(rx/map
(fn [data-uri]
(l/dbg :hint "component thumbnail rendered (wasm)"
:elapsed (dm/str (tp) "ms"))
(dwt/assoc-thumbnail object-id data-uri)))
(rx/catch (fn [err]
(js/console.error "[thumbnails] error rendering component thumbnail" err)
(rx/empty)))
(rx/take-until
(->> stream
(rx/filter (ptk/type? ::dwt/clear-thumbnail))
(rx/filter #(= (deref %) object-id))))))))))
(defn persist-thumbnail
"Persists the current component thumbnail data-uri to the server.
Expects that `render-thumbnail` has already been called so the
data-uri is present in app state. If not, this is a no-op."
[file-id page-id frame-id]
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
(ptk/reify ::persist-thumbnail
ptk/WatchEvent
(watch [_ state _]
(let [data-uri (dm/get-in state [:thumbnails object-id])]
(if (and (some? data-uri)
(str/starts-with? data-uri "data:"))
(let [blob (wapi/data-uri->blob data-uri)]
(->> (rp/cmd! :create-file-object-thumbnail
{:file-id file-id
:object-id object-id
:media blob
:tag "component"})
(rx/catch rx/empty)
(rx/ignore)))
(rx/empty)))))))

View File

@ -24,6 +24,7 @@
[app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.variants :as dwv] [app.main.data.workspace.variants :as dwv]
[app.main.features :as features]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.render :refer [component-svg component-svg-thumbnail]] [app.main.render :refer [component-svg component-svg-thumbnail]]
[app.main.store :as st] [app.main.store :as st]
@ -299,7 +300,8 @@
(inc retry))))] (inc retry))))]
(if (and (some? thumbnail-uri) (if (and (some? thumbnail-uri)
(contains? cf/flags :component-thumbnails)) (or (contains? cf/flags :component-thumbnails)
(features/active-feature? @st/state "render-wasm/v1")))
[:& component-svg-thumbnail [:& component-svg-thumbnail
{:thumbnail-uri thumbnail-uri {:thumbnail-uri thumbnail-uri
:class class :class class