Add lazy async rendering for component thumbnails

This commit is contained in:
Elena Torro 2026-04-20 12:28:42 +02:00
parent 3f0d103cb3
commit 448390f8c9
4 changed files with 74 additions and 52 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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