mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
✨ Add lazy async rendering for component thumbnails
This commit is contained in:
parent
3f0d103cb3
commit
448390f8c9
@ -979,10 +979,7 @@
|
||||
|
||||
;; 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 "frame")
|
||||
(update-component-thumbnail-sync state component-id file-id "component")
|
||||
|
||||
(sync-file current-file-id file-id :components component-id undo-group)
|
||||
@ -1007,10 +1004,10 @@
|
||||
(dwu/commit-undo-transaction undo-id)))))))
|
||||
|
||||
(defn update-component-thumbnail
|
||||
"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."
|
||||
"Update the thumbnail of the component with the given id, in the
|
||||
current file and in the imported libraries.
|
||||
For WASM, re-renders and persists to the server in one step.
|
||||
For SVG, update-thumbnail already handles both render + persist."
|
||||
[component-id file-id]
|
||||
(ptk/reify ::update-component-thumbnail
|
||||
ptk/WatchEvent
|
||||
@ -1020,7 +1017,7 @@
|
||||
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 (dwt.wasm/render-thumbnail file-id page-id root-id :persist? true)))
|
||||
(rx/of (update-component-thumbnail-sync state component-id file-id "component"))))))
|
||||
|
||||
(defn- find-shape-index
|
||||
@ -1379,7 +1376,8 @@
|
||||
|
||||
check-changes
|
||||
(fn [[event [old-data _mid_data _new-data]]]
|
||||
(when old-data
|
||||
(if (nil? old-data)
|
||||
(rx/empty)
|
||||
(let [{:keys [file-id changes save-undo? undo-group]} event
|
||||
|
||||
changed-components
|
||||
@ -1397,18 +1395,9 @@
|
||||
(->> (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). 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)))
|
||||
;; (for example, for undos)
|
||||
(->> (rx/from changed-components)
|
||||
(rx/map touch-component)))
|
||||
|
||||
(rx/empty)))))
|
||||
|
||||
@ -1425,30 +1414,30 @@
|
||||
|
||||
(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
|
||||
(->> (rx/merge
|
||||
changes-s
|
||||
|
||||
;; 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))
|
||||
;; 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))))
|
||||
|
||||
;; 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)))))
|
||||
;; Immediately update the component thumbnail on undos,
|
||||
;; which emit touch-component instead of component-changed.
|
||||
(->> changes-s
|
||||
(rx/filter (ptk/type? ::touch-component))
|
||||
(rx/map deref)
|
||||
(rx/map (fn [[component-id file-id]]
|
||||
(let [file-id (or file-id (:current-file-id @st/state))]
|
||||
(update-component-thumbnail-sync
|
||||
@st/state component-id file-id "component"))))))
|
||||
|
||||
(rx/take-until stopper-s))))))))
|
||||
(rx/take-until stopper-s)))))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Backend interactions
|
||||
|
||||
@ -84,9 +84,9 @@
|
||||
|
||||
(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]
|
||||
When `persist?` is true, also persists the rendered thumbnail to the
|
||||
server in the same observable chain (guaranteeing correct ordering)."
|
||||
[file-id page-id frame-id & {:keys [persist?] :or {persist? false}}]
|
||||
|
||||
(let [object-id (thc/fmt-object-id file-id page-id frame-id "component")]
|
||||
(ptk/reify ::render-thumbnail
|
||||
@ -115,15 +115,30 @@
|
||||
(catch :default err
|
||||
(rx/error! subs err)))))))
|
||||
|
||||
(persist-to-server
|
||||
[data-uri]
|
||||
(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))))
|
||||
|
||||
(do-render-thumbnail
|
||||
[]
|
||||
(let [tp (ct/tpoint-ms)]
|
||||
(->> (render-component-pixels file-id page-id frame-id)
|
||||
(rx/map
|
||||
(rx/mapcat
|
||||
(fn [data-uri]
|
||||
(l/dbg :hint "component thumbnail rendered (wasm)"
|
||||
:elapsed (dm/str (tp) "ms"))
|
||||
(dwt/assoc-thumbnail object-id data-uri)))
|
||||
(if persist?
|
||||
(rx/merge
|
||||
(rx/of (dwt/assoc-thumbnail object-id data-uri))
|
||||
(persist-to-server data-uri))
|
||||
(rx/of (dwt/assoc-thumbnail object-id data-uri)))))
|
||||
|
||||
(rx/catch (fn [err]
|
||||
(js/console.error err)
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.libraries :as dwl]
|
||||
[app.main.data.workspace.thumbnails-wasm :as dwt.wasm]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.features :as features]
|
||||
@ -283,6 +284,9 @@
|
||||
(let [page-id (:main-instance-page component)
|
||||
root-id (:main-instance-id component)
|
||||
retry (mf/use-state 0)
|
||||
wasm? (features/active-feature? @st/state "render-wasm/v1")
|
||||
current-page-id (mf/deref refs/current-page-id)
|
||||
thumbnail-requested? (mf/use-ref false)
|
||||
|
||||
thumbnail-uri*
|
||||
(mf/with-memo [file-id page-id root-id]
|
||||
@ -299,9 +303,23 @@
|
||||
(when (< @retry 3)
|
||||
(inc retry))))]
|
||||
|
||||
;; Lazy WASM thumbnail rendering: when the component becomes
|
||||
;; visible, has no cached thumbnail, and lives on the current page
|
||||
;; trigger a render. Ref is used to avoid triggering multiple renders
|
||||
;; while the component is still not rendered and the thumbnail URI
|
||||
;; is not available.
|
||||
(mf/use-effect
|
||||
(mf/deps is-hidden thumbnail-uri wasm? current-page-id)
|
||||
(fn []
|
||||
(if (some? thumbnail-uri)
|
||||
(mf/set-ref-val! thumbnail-requested? false)
|
||||
(when (and wasm? (not is-hidden) (not (mf/ref-val thumbnail-requested?)) (= page-id current-page-id))
|
||||
(mf/set-ref-val! thumbnail-requested? true)
|
||||
(st/emit! (dwt.wasm/render-thumbnail file-id page-id root-id))))))
|
||||
|
||||
(if (and (some? thumbnail-uri)
|
||||
(or (contains? cf/flags :component-thumbnails)
|
||||
(features/active-feature? @st/state "render-wasm/v1")))
|
||||
wasm?))
|
||||
[:& component-svg-thumbnail
|
||||
{:thumbnail-uri thumbnail-uri
|
||||
:class class
|
||||
|
||||
@ -2,7 +2,7 @@ use crate::math::Rect as MathRect;
|
||||
use crate::shapes::ImageFill;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::error::Result;
|
||||
use skia_safe::gpu::{surfaces, Budgeted, DirectContext};
|
||||
use skia_safe::{self as skia, Codec, ISize};
|
||||
use std::collections::HashMap;
|
||||
@ -159,7 +159,7 @@ impl ImageStore {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let raw_data = image_data.to_vec();
|
||||
@ -186,7 +186,7 @@ impl ImageStore {
|
||||
let key = (id, is_thumbnail);
|
||||
|
||||
if self.images.contains_key(&key) {
|
||||
return Err(Error::RecoverableError("Image already exists".to_string()));
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create a Skia image from the existing GL texture
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user