diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index ffa3181a4e..cce984b413 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -96,12 +96,14 @@ ptk/UpdateEvent (update [_ state] - (update state :thumbnails - (fn [thumbs] - (if-let [uri (get thumbs object-id)] - (do (vreset! pending uri) - (dissoc thumbs object-id)) - thumbs)))) + (-> state + (update :thumbnails + (fn [thumbs] + (if-let [uri (get thumbs object-id)] + (do (vreset! pending uri) + (dissoc thumbs object-id)) + thumbs))) + (update :thumbnails-meta dissoc object-id))) ptk/WatchEvent (watch [_ _ _] @@ -124,10 +126,13 @@ (ptk/reify ::assoc-thumbnail ptk/UpdateEvent (update [_ state] - (let [prev-uri (dm/get-in state [:thumbnails object-id])] + (let [prev-uri (dm/get-in state [:thumbnails object-id]) + now (.now js/Date)] (some->> prev-uri (vreset! prev-uri*)) (l/trc :hint "assoc thumbnail" :object-id object-id :uri uri) - (update state :thumbnails assoc object-id uri))) + (-> state + (update :thumbnails assoc object-id uri) + (update :thumbnails-meta assoc object-id {:rendered-at now})))) ptk/EffectEvent (effect [_ _ _] diff --git a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs index ff82e15972..507714578c 100644 --- a/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails_wasm.cljs @@ -63,8 +63,9 @@ (try (let [objects (dsh/lookup-page-objects @st/state file-id page-id)] (if-let [frame (get objects frame-id)] - (let [{:keys [width height]} (:selrect frame) - max-size (mth/max width height) + (let [{ext-w :width ext-h :height} (wasm.api/get-shape-extrect frame-id) + {sel-w :width sel-h :height} (:selrect frame) + max-size (mth/max (or ext-w sel-w) (or ext-h sel-h)) scale (mth/max 1 (/ target-size max-size)) png-bytes (wasm.api/render-shape-pixels frame-id scale)] (if (or (nil? png-bytes) (zero? (.-length png-bytes))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 01bb43a0cb..a68f577b6b 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -588,6 +588,12 @@ (cf/resolve-media))) st/state)) +(defn workspace-thumbnail-rendered-at + [object-id] + (l/derived + #(dm/get-in % [:thumbnails-meta object-id :rendered-at]) + st/state)) + (def workspace-text-modifier (l/derived :workspace-text-modifier st/state)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs index 28eb7e4699..419ab73531 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/common.cljs @@ -324,14 +324,35 @@ current-page-id (mf/deref refs/current-page-id) thumbnail-requested? (mf/use-ref false) - thumbnail-uri* + object-id (mf/with-memo [file-id page-id root-id] - (let [object-id (thc/fmt-object-id file-id page-id root-id "component")] - (refs/workspace-thumbnail-by-id object-id))) + (thc/fmt-object-id file-id page-id root-id "component")) + + thumbnail-uri* + (mf/with-memo [object-id] + (refs/workspace-thumbnail-by-id object-id)) thumbnail-uri (mf/deref thumbnail-uri*) + rendered-at* + (mf/with-memo [object-id] + (refs/workspace-thumbnail-rendered-at object-id)) + + rendered-at + (mf/deref rendered-at*) + + modified-at + (some-> (:modified-at component) (.getTime)) + + ;; Stale if there's no in-session render record + ;; or the component was modified after the last render + stale? + (and (some? thumbnail-uri) + (or (nil? rendered-at) + (and (some? modified-at) + (> modified-at rendered-at)))) + on-error (mf/use-fn (mf/deps @retry) @@ -340,20 +361,21 @@ (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. + ;; visible and either has no cached thumbnail or the cached one is + ;; stale relative to the last recorded edit, trigger a render. Ref + ;; is used to avoid triggering multiple renders while the previous + ;; render is in flight. (mf/use-effect - (mf/deps is-hidden thumbnail-uri wasm? current-page-id file-id page-id) + (mf/deps is-hidden thumbnail-uri stale? wasm? current-page-id file-id page-id) (fn [] - (if (some? thumbnail-uri) + (if (and (some? thumbnail-uri) (not stale?)) (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) + (not stale?) (or (contains? cf/flags :component-thumbnails) wasm?)) [:& component-svg-thumbnail diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index a9d9380ede..edb1a4f0bf 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -2172,6 +2172,24 @@ (mem/free) result)) +(defn get-shape-extrect + [shape-id] + (let [buffer (uuid/get-u32 shape-id) + offset (h/call wasm/internal-module "_get_shape_extrect" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))] + (when (and (number? offset) (pos? offset)) + (let [heapf32 (mem/get-heap-f32) + base (mem/->offset-32 offset) + x (aget heapf32 base) + y (aget heapf32 (+ base 1)) + w (aget heapf32 (+ base 2)) + h (aget heapf32 (+ base 3))] + (mem/free) + {:x x :y y :width w :height h})))) + (defn init-wasm-module [module] (let [default-fn (unchecked-get module "default") diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index bbec95e81f..74cc7478fe 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -878,6 +878,25 @@ pub extern "C" fn end_temp_objects() -> Result<()> { Ok(()) } +#[no_mangle] +#[wasm_error] +pub extern "C" fn get_shape_extrect(a: u32, b: u32, c: u32, d: u32) -> Result<*mut u8> { + let id = uuid_from_u32_quartet(a, b, c, d); + + with_state!(state, { + let Some(shape) = state.shapes.get(&id) else { + return Err(Error::CriticalError("Shape not found".to_string())); + }; + let extrect = get_render_state().get_cached_extrect(shape, &state.shapes, 1.0); + let mut buf = Vec::with_capacity(16); + buf.extend_from_slice(&extrect.x().to_le_bytes()); + buf.extend_from_slice(&extrect.y().to_le_bytes()); + buf.extend_from_slice(&extrect.width().to_le_bytes()); + buf.extend_from_slice(&extrect.height().to_le_bytes()); + Ok(mem::write_bytes(buf)) + }) +} + #[no_mangle] #[wasm_error] pub extern "C" fn render_shape_pixels(