diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index ae616f29a6..e5fc20de1d 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -58,7 +58,7 @@ ([{:keys [id points selrect] :as shape} content] (wasm.api/use-shape id) - (wasm.api/set-shape-text id content) + (wasm.api/set-shape-text id content false) (let [dimension (wasm.api/get-text-dimensions) resize-v (gpt/point (/ (:width dimension) (-> selrect :width)) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 36a00a67fb..b04c94d728 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -228,9 +228,10 @@ (defn- fetch-image - [shape-id image-id] - (let [url (cf/resolve-file-media {:id image-id})] + [shape-id image-id thumbnail?] + (let [url (cf/resolve-file-media {:id image-id} thumbnail?)] {:key url + :thumbnail? thumbnail? :callback #(->> (http/send! {:method :get :uri url :response-type :blob}) @@ -239,27 +240,31 @@ (rx/map (fn [image] (let [size (.-byteLength image) padded-size (if (zero? (mod size 4)) size (+ size (- 4 (mod size 4)))) - total-bytes (+ 32 padded-size) ; UUID size + padded size + ;; 36 bytes header (32 for UUIDs + 4 for thumbnail flag) + padded image + total-bytes (+ 36 padded-size) offset (mem/alloc->offset-32 total-bytes) heap32 (mem/get-heap-u32) data (js/Uint8Array. image) padded (js/Uint8Array. padded-size)] - ;; 1. Set shape id + ;; 1. Set shape id (offset + 0 to offset + 3) (mem.h32/write-uuid offset heap32 shape-id) - ;; 2. Set image id + ;; 2. Set image id (offset + 4 to offset + 7) (mem.h32/write-uuid (+ offset 4) heap32 image-id) - ;; 3. Adjust padding on image data + ;; 3. Set thumbnail flag as u32 (offset + 8) + (aset heap32 (+ offset 8) thumbnail?) + + ;; 4. Adjust padding on image data (.set padded data) (when (< size padded-size) (dotimes [i (- padded-size size)] (aset padded (+ size i) 0))) - ;; 4. Set image data + ;; 5. Set image data (starting at offset + 9) (let [u32view (js/Uint32Array. (.-buffer padded)) - image-u32-offset (+ offset 8)] + image-u32-offset (+ offset 9)] (.set heap32 u32view image-u32-offset)) (h/call wasm/internal-module "_store_image") @@ -270,7 +275,7 @@ (filter :fill-image (:fills leaf))) (defn- process-fill-image - [shape-id fill] + [shape-id fill thumbnail?] (when-let [image (:fill-image fill)] (let [id (get image :id) buffer (uuid/get-u32 id) @@ -278,22 +283,22 @@ (aget buffer 0) (aget buffer 1) (aget buffer 2) - (aget buffer 3))] + (aget buffer 3) + thumbnail?)] (when (zero? cached-image?) - (fetch-image shape-id id))))) + (fetch-image shape-id id thumbnail?))))) (defn set-shape-text-images - [shape-id content] - + [shape-id content thumbnail?] (let [paragraph-set (first (get content :children)) paragraphs (get paragraph-set :children)] (->> paragraphs (mapcat :children) (mapcat get-fill-images) - (map #(process-fill-image shape-id %))))) + (map #(process-fill-image shape-id % thumbnail?))))) (defn set-shape-fills - [shape-id fills] + [shape-id fills thumbnail?] (if (empty? fills) (h/call wasm/internal-module "_clear_shape_fills") (let [fills (types.fills/coerce fills) @@ -313,14 +318,15 @@ (aget buffer 0) (aget buffer 1) (aget buffer 2) - (aget buffer 3))] + (aget buffer 3) + thumbnail?)] (when (zero? cached-image?) - (fetch-image shape-id id)))) + (fetch-image shape-id id thumbnail?)))) (types.fills/get-image-ids fills))))) (defn set-shape-strokes - [shape-id strokes] + [shape-id strokes thumbnail?] (h/call wasm/internal-module "_clear_shape_strokes") (keep (fn [stroke] (let [opacity (or (:stroke-opacity stroke) 1.0) @@ -349,11 +355,14 @@ (some? image) (let [image-id (get image :id) buffer (uuid/get-u32 image-id) - cached-image? (h/call wasm/internal-module "_is_image_cached" (aget buffer 0) (aget buffer 1) (aget buffer 2) (aget buffer 3))] + cached-image? (h/call wasm/internal-module "_is_image_cached" + (aget buffer 0) (aget buffer 1) + (aget buffer 2) (aget buffer 3) + thumbnail?)] (types.fills.impl/write-image-fill offset dview opacity image) (h/call wasm/internal-module "_add_shape_stroke_fill") (when (== cached-image? 0) - (fetch-image shape-id image-id))) + (fetch-image shape-id image-id thumbnail?))) (some? color) (do @@ -725,9 +734,9 @@ result))))) (defn set-shape-text - [shape-id content] + [shape-id content thumbnail?] (concat - (set-shape-text-images shape-id content) + (set-shape-text-images shape-id content thumbnail?) (set-shape-text-content shape-id content))) (defn set-shape-grow-type @@ -839,45 +848,61 @@ (set-shape-selrect selrect) - (let [pending (into [] (concat - (set-shape-text id content) - (set-shape-fills id fills) - (set-shape-strokes id strokes)))] + (let [pending_thumbnails (into [] (concat + (set-shape-text id content true) + (set-shape-fills id fills true) + (set-shape-strokes id strokes true))) + pending_full (into [] (concat + (set-shape-text id content false) + (set-shape-fills id fills false) + (set-shape-strokes id strokes false)))] (perf/end-measure "set-object") - pending))) + {:thumbnails pending_thumbnails + :full pending_full}))) (defn process-pending - [pending] + [thumbnails full] (let [event (js/CustomEvent. "wasm:set-objects-finished") - pending (-> (d/index-by :key :callback pending) vals)] - (->> (rx/from pending) + pending-thumbnails (-> (d/index-by :key :callback thumbnails) vals) + pending-full (-> (d/index-by :key :callback full) vals)] + (->> (rx/from pending-thumbnails) + (rx/merge-map (fn [callback] (callback))) + (rx/reduce conj []) + (rx/merge-map (fn [_] + (clear-drawing-cache) + (request-render "pending-thumbnails-finished") + (h/call wasm/internal-module "_update_shape_text_layout_for_all") + (.dispatchEvent ^js js/document event) + ;; After thumbnails are done, process full images + (rx/from pending-full))) (rx/merge-map (fn [callback] (callback))) (rx/reduce conj []) (rx/subs! (fn [_] (clear-drawing-cache) - (request-render "pending-finished") - (h/call wasm/internal-module "_update_shape_text_layout_for_all") - (.dispatchEvent ^js js/document event)))))) + (request-render "pending-full-finished")))))) (defn process-object [shape] - (let [pending (set-object [] shape)] - (process-pending pending))) + (let [{:keys [thumbnails full]} (set-object [] shape)] + (process-pending thumbnails full))) (defn set-objects [objects] (perf/begin-measure "set-objects") (let [shapes (into [] (vals objects)) total-shapes (count shapes) - pending - (loop [index 0 pending []] + ;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]} + {:keys [thumbnails full]} + (loop [index 0 thumbnails-acc [] full-acc []] (if (< index total-shapes) (let [shape (nth shapes index) - pending' (set-object objects shape)] - (recur (inc index) (into pending pending'))) - pending))] + {:keys [thumbnails full]} (set-object objects shape)] + (recur (inc index) + (into thumbnails-acc thumbnails) + (into full-acc full))) + {:thumbnails thumbnails-acc :full full-acc}))] (perf/end-measure "set-objects") - (process-pending pending))) + (process-pending thumbnails full))) (defn clear-focus-mode [] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 3af716f0f2..8a30c95bc8 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -144,8 +144,8 @@ (api/set-shape-clip-content false)) :rotation (api/set-shape-rotation v) :transform (api/set-shape-transform v) - :fills (into [] (api/set-shape-fills id v)) - :strokes (into [] (api/set-shape-strokes id v)) + :fills (into [] (api/set-shape-fills id v false)) + :strokes (into [] (api/set-shape-strokes id v false)) :blend-mode (api/set-shape-blend-mode v) :opacity (api/set-shape-opacity v) :hidden (api/set-shape-hidden v) @@ -185,7 +185,7 @@ (api/set-shape-svg-raw-content (api/get-static-markup shape)) (= (:type shape) :text) - (api/set-shape-text id v)) + (api/set-shape-text id v false)) :grow-type (api/set-shape-grow-type v) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 9ecb93d6d6..38e447e07a 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -440,10 +440,10 @@ pub extern "C" fn set_children() { } #[no_mangle] -pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32) -> bool { +pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32, is_thumbnail: bool) -> bool { with_state_mut!(state, { let id = uuid_from_u32_quartet(a, b, c, d); - state.render_state().has_image(&id) + state.render_state().has_image(&id, is_thumbnail) }) } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 78dbb4c806..3a131571f3 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -318,12 +318,17 @@ impl RenderState { &mut self.fonts } - pub fn add_image(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { - self.images.add(id, image_data) + pub fn add_image( + &mut self, + id: Uuid, + is_thumbnail: bool, + image_data: &[u8], + ) -> Result<(), String> { + self.images.add(id, is_thumbnail, image_data) } - pub fn has_image(&self, id: &Uuid) -> bool { - self.images.contains(id) + pub fn has_image(&self, id: &Uuid, is_thumbnail: bool) -> bool { + self.images.contains(id, is_thumbnail) } pub fn set_debug_flags(&mut self, debug: u32) { diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 0b3e2283f0..57b7675312 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -58,7 +58,7 @@ enum StoredImage { } pub struct ImageStore { - images: HashMap, + images: HashMap<(Uuid, bool), StoredImage>, context: Box, } @@ -70,23 +70,36 @@ impl ImageStore { } } - pub fn add(&mut self, id: Uuid, image_data: &[u8]) -> Result<(), String> { - if self.images.contains_key(&id) { + pub fn add(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<(), String> { + let key = (id, is_thumbnail); + + if self.images.contains_key(&key) { return Err("Image already exists".to_string()); } self.images - .insert(id, StoredImage::Raw(image_data.to_vec())); + .insert(key, StoredImage::Raw(image_data.to_vec())); Ok(()) } - pub fn contains(&self, id: &Uuid) -> bool { - self.images.contains_key(id) + pub fn contains(&self, id: &Uuid, is_thumbnail: bool) -> bool { + self.images.contains_key(&(*id, is_thumbnail)) } pub fn get(&mut self, id: &Uuid) -> Option<&Image> { + // Try to get full image first, fallback to thumbnail + let has_full = self.images.contains_key(&(*id, false)); + if has_full { + self.get_internal(id, false) + } else { + self.get_internal(id, true) + } + } + + fn get_internal(&mut self, id: &Uuid, is_thumbnail: bool) -> Option<&Image> { + let key = (*id, is_thumbnail); // Use entry API to mutate the HashMap in-place if needed - if let Some(entry) = self.images.get_mut(id) { + if let Some(entry) = self.images.get_mut(&key) { match entry { StoredImage::Gpu(ref img) => Some(img), StoredImage::Raw(raw_data) => { diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index b7075899ff..ea1fc2267a 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -7,6 +7,7 @@ use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0; const IMAGE_IDS_SIZE: usize = 32; +const IMAGE_HEADER_SIZE: usize = 36; // 32 bytes for IDs + 4 bytes for is_thumbnail flag #[derive(Debug, Clone, Copy, PartialEq)] #[repr(C)] @@ -67,12 +68,19 @@ impl TryFrom> for ShapeImageIds { pub extern "C" fn store_image() { let bytes = mem::bytes(); let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap(); - let image_bytes = &bytes[IMAGE_IDS_SIZE..]; + + // Read is_thumbnail flag (4 bytes as u32) + let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE]; + let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap()); + let is_thumbnail = is_thumbnail_value != 0; + + let image_bytes = &bytes[IMAGE_HEADER_SIZE..]; with_state_mut!(state, { - if let Err(msg) = state - .render_state_mut() - .add_image(ids.image_id, image_bytes) + if let Err(msg) = + state + .render_state_mut() + .add_image(ids.image_id, is_thumbnail, image_bytes) { eprintln!("{}", msg); }