mirror of
https://github.com/penpot/penpot.git
synced 2026-09-19 02:16:14 +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.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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
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.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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user