mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 19:28:12 +00:00
Merge pull request #8823 from penpot/elenatorro-13350-add-components-preview-using-render
🔧 Use wasm render for components thumbnail
This commit is contained in:
commit
153277d152
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
118
frontend/src/app/main/data/workspace/thumbnails_wasm.cljs
Normal file
118
frontend/src/app/main/data/workspace/thumbnails_wasm.cljs
Normal 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)))))))
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user