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.specialized-panel :as dwsp]
[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.undo :as dwu]
[app.main.data.workspace.wasm-text :as dwwt]
@ -945,7 +946,12 @@
component (ctkl/get-component data component-id)
page-id (:main-instance-page 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
([shape-id file-id] (update-component-sync shape-id file-id nil))
@ -964,9 +970,12 @@
(dwu/start-undo-transaction undo-id)
(update-component shape-id undo-group)
;; These two calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page
(update-component-thumbnail-sync state component-id file-id "frame")
;; These calls are necessary for properly sync thumbnails
;; when a main component does not live in the same page.
;; 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")
(sync-file current-file-id file-id :components component-id undo-group)
@ -991,13 +1000,21 @@
(dwu/commit-undo-transaction undo-id)))))))
(defn update-component-thumbnail
"Update the thumbnail of the component with the given id, in the
current file and in the imported libraries."
"Persist the thumbnail of the component to the server.
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]
(ptk/reify ::update-component-thumbnail
ptk/WatchEvent
(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
[objects id shape-id]
@ -1373,9 +1390,18 @@
(->> (rx/from changed-components)
(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
;; (for example, for undos)
(->> (rx/from changed-components)
(rx/map touch-component)))
;; (for example, for undos). When WASM is active, also re-render the thumbnail
;; so undo/redo visually updates component previews.
(->> (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)))))
@ -1390,18 +1416,32 @@
(rx/debounce 5000)
(rx/tap #(log/trc :hint "buffer initialized")))]
(when (contains? cf/flags :component-thumbnails)
(->> (rx/merge
changes-s
(when (or (contains? cf/flags :component-thumbnails)
(features/active-feature? @st/state "render-wasm/v1"))
(let [wasm? (features/active-feature? @st/state "render-wasm/v1")]
(->> (rx/merge
changes-s
(->> changes-s
(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)))))
;; WASM: render thumbnails immediately for instant UI feedback
(if wasm?
(->> changes-s
(rx/filter (ptk/type? ::component-changed))
(rx/map deref)
(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

View File

@ -118,7 +118,7 @@
(rx/ignore))))
(rx/empty)))))))
(defn- assoc-thumbnail
(defn assoc-thumbnail
[object-id uri]
(let [prev-uri* (volatile! nil)]
(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.undo :as dwu]
[app.main.data.workspace.variants :as dwv]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.render :refer [component-svg component-svg-thumbnail]]
[app.main.store :as st]
@ -299,7 +300,8 @@
(inc retry))))]
(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
{:thumbnail-uri thumbnail-uri
:class class