From 5a3a855b247e655db681106d59175b75cae418d1 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Tue, 26 May 2026 09:50:23 +0200 Subject: [PATCH] :bug: Fix problem with position-data not present * :bug: Fix problem with position-data not present * :bug: Async set-objects wait before calculate-position-data --- frontend/src/app/main/data/viewer.cljs | 91 ++++++++++++++++++- frontend/src/app/main/data/workspace.cljs | 64 +++++++------ .../app/main/ui/workspace/viewport_wasm.cljs | 8 +- frontend/src/app/render_wasm/api.cljs | 60 ++++++++++++ render-wasm/src/main.rs | 9 ++ render-wasm/src/state.rs | 4 + 6 files changed, 201 insertions(+), 35 deletions(-) diff --git a/frontend/src/app/main/data/viewer.cljs b/frontend/src/app/main/data/viewer.cljs index d2ebf4c9dc..dd40fc5cdd 100644 --- a/frontend/src/app/main/data/viewer.cljs +++ b/frontend/src/app/main/data/viewer.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.features :as cfeat] + [app.common.files.changes :as cpc] [app.common.files.helpers :as cfh] [app.common.geom.point :as gpt] [app.common.schema :as sm] @@ -20,9 +21,11 @@ [app.main.data.common :as dcm] [app.main.data.event :as ev] [app.main.data.fonts :as df] + [app.main.data.helpers :as dsh] [app.main.features :as features] [app.main.repo :as rp] [app.main.router :as rt] + [app.render-wasm.api :as wasm.api] [app.util.globals :as ug] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -171,6 +174,88 @@ (declare go-to-frame-by-index) (declare go-to-frame-auto) +;; Applies to the viewer the changes passed as parameters +;; will not save the data but just modify the data localy +(defn- apply-changes-viewer + [changes] + (ptk/reify ::apply-changes-viewer + ptk/UpdateEvent + (update [_ state] + (let [file (-> (dm/get-in state [:viewer :file]) + (update :data cpc/process-changes changes false)) + + pages + (->> (dm/get-in file [:data :pages]) + (map (fn [page-id] + (let [data (get-in file [:data :pages-index page-id])] + [page-id (assoc data + :frames (ctt/get-viewer-frames (:objects data)) + :all-frames (ctt/get-viewer-frames (:objects data) {:all-frames? true}))]))) + (into {}))] + + (-> state + (assoc-in [:viewer :file] file) + (assoc-in [:viewer :pages] pages)))))) + +(defn- generate-update-position-data-changes + [shapes page-id] + (reduce + (fn [result shape] + (conj result + {:type :mod-obj + :id (:id shape) + :page-id page-id + :operations + [{:type :set + :attr :position-data + :val (wasm.api/calculate-position-data shape) + :ignore-touched true + :ignore-geometry true}]})) + [] + shapes)) + +(defn update-page-position-data + [file-id page-id] + (ptk/reify ::update-page-position-data + ptk/WatchEvent + (watch [_ state _] + (if (features/active-feature? state "render-wasm/v1") + (let [objects (dsh/lookup-page-objects state file-id page-id) + + shapes + (reduce-kv + (fn [result _ shape] + (cond-> result + (and (cfh/text-shape? shape) (nil? (:position-data shape))) + (conj shape))) + [] + objects) + + ;; Creates a stream from the async callback. This stream will only + ;; emit one single value after the objects have finished loading + ;; in the wasm memory. + set-objects-stream + (rx/create + (fn [subs] + (wasm.api/init-canvas-context (js/OffscreenCanvas. 0 0)) + (wasm.api/set-objects-callback shapes #(rx/push! subs :done)) + nil))] + + (if (d/not-empty? shapes) + (->> (rx/from @wasm.api/module) + (rx/mapcat (constantly set-objects-stream)) + (rx/mapcat + (fn [] + (let [changes (generate-update-position-data-changes shapes page-id) + _ (wasm.api/clear-canvas)] + (if (d/not-empty? changes) + (rx/of (apply-changes-viewer changes)) + (rx/empty)))))) + (rx/empty))) + + ;; Render wasm disabled, we do nothing + (rx/empty))))) + (defn bundle-fetched [{:keys [project file team share-links libraries users permissions thumbnails] :as bundle}] (let [pages (->> (dm/get-in file [:data :pages]) @@ -205,13 +290,17 @@ (let [route (:route state) qparams (:query-params route) index (some-> (rt/get-query-param qparams :index) parse-long) - frame-id (some-> (:frame-id qparams) uuid/parse)] + frame-id (some-> (:frame-id qparams) uuid/parse) + page-id (some-> (rt/get-query-param qparams :page-id) uuid/parse) + file-id (some-> (rt/get-query-param qparams :file-id) uuid/parse)] + (rx/merge (rx/of (case (:zoom qparams) "fit" zoom-to-fit "fill" zoom-to-fill nil)) (rx/of + (update-page-position-data file-id page-id) (cond (some? frame-id) (go-to-frame frame-id) (some? index) (go-to-frame-by-index index) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index f878234e1d..5d2feabeac 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -204,36 +204,40 @@ (rx/take-until stopper-s)))))) -(defn check-file-position-data - [file-id] - (ptk/reify ::fix-position-data - ptk/WatchEvent - (watch [it state _] - (let [file (dsh/lookup-file state file-id) - changes - (->> file :data :pages - (mapcat - (fn [page-id] - (->> (dsh/lookup-page-objects state file-id page-id) - (vals) - (filter cfh/text-shape?) - (filter #(nil? (:position-data %))) - (map (fn [shape] - {:type :mod-obj - :id (:id shape) - :page-id page-id - :operations - [{:type :set - :attr :position-data - :val (wasm.api/calculate-position-data shape) - :ignore-touched true - :ignore-geometry true}]}))))) - (into []))] - (rx/of (dch/commit-changes - {:redo-changes changes :undo-changes [] - :save-undo? false - :origin it - :tags #{:position-data}})))))) +(defn update-page-position-data + ([] + (update-page-position-data nil nil)) + ([file-id page-id] + (ptk/reify ::update-page-position-data + ptk/WatchEvent + (watch [it state _] + (let [file-id (or file-id (:current-file-id state)) + page-id (or page-id (:current-page-id state)) + changes + (reduce-kv + (fn [result _ shape] + (if (and (cfh/text-shape? shape) + (nil? (:position-data shape))) + (conj result + {:type :mod-obj + :id (:id shape) + :page-id page-id + :operations + [{:type :set + :attr :position-data + :val (wasm.api/calculate-position-data shape) + :ignore-touched true + :ignore-geometry true}]}) + result)) + [] + (dsh/lookup-page-objects state file-id page-id))] + (if (d/not-empty? changes) + (rx/of (dch/commit-changes + {:redo-changes changes :undo-changes [] + :save-undo? false + :origin it + :tags #{:position-data}})) + (rx/empty))))))) (defn- workspace-initialized [file-id] diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 6c2a741533..4b78301152 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -17,7 +17,7 @@ [app.common.types.shape :as cts] [app.common.types.shape.layout :as ctl] [app.main.data.modal :as modal] - ;; [app.main.data.workspace :as dw] + [app.main.data.workspace :as dw] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.variants :as dwv] [app.main.features :as features] @@ -459,9 +459,9 @@ ;; is set. (wasm.api/initialize-viewport base-objects zoom vbox :background background - ;; :on-shapes-ready - ;; (st/emit! (dw/check-file-position-data file-id)) - ) + :on-shapes-ready + (fn [] + (st/emit! (dw/update-page-position-data)))) (reset! initialized? true)) (when (and (some? vern) (not= vern (mf/ref-val last-vern-ref))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 2d3b106451..25b2e6fbb4 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -438,6 +438,19 @@ (aget buffer 2) (aget buffer 3))))) +(defn has-shape + [id] + (when wasm/context-initialized? + (let [buffer (uuid/get-u32 id) + + result + (h/call wasm/internal-module "_has_shape" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))] + (= result 1)))) + (defn set-shape-text-content "This function sets shape text content and returns a stream that loads the needed fonts asynchronously" [shape-id content] @@ -1404,6 +1417,53 @@ noop-fn)))))))] (process-next-chunk 0 [] [])))))) + +;; This is a version of process-pending that doesn't have sideffects +;; with like request render or update layout. +(defn- process-pending-no-sideffects + [thumbnails full on-complete] + (let [pending-thumbnails + (d/index-by :key :callback thumbnails) + + pending-full + (d/index-by :key :callback full)] + + (if (or (seq pending-thumbnails) (seq pending-full)) + (->> (rx/concat + (->> (rx/from (vals pending-thumbnails)) + (rx/merge-map (fn [callback] (if (fn? callback) (callback) (rx/empty)))) + (rx/reduce conj []) + (rx/catch #(rx/empty))) + (->> (rx/from (vals pending-full)) + (rx/mapcat (fn [callback] (if (fn? callback) (callback) (rx/empty)))) + (rx/reduce conj []) + (rx/catch #(rx/empty)))) + (rx/subs! + noop-fn + noop-fn + (fn [] + (when (fn? on-complete) (on-complete))))) + ;; No pending images — complete immediately. + (when on-complete (on-complete))))) + +(defn set-objects-callback + "Sets the shapes and when the async operations are done calls the callback. Won't + interact with the rendering pipeline, this call is only to set the model (used currently + in the viewer)." + [shapes set-objects-cb] + (let [total-shapes (count shapes) + {:keys [thumbnails full]} + (loop [index 0 thumbnails-acc (transient []) full-acc (transient [])] + (if (< index total-shapes) + (let [shape (nth shapes index) + {:keys [thumbnails full]} (set-object shape)] + (recur (inc index) + (reduce conj! thumbnails-acc thumbnails) + (reduce conj! full-acc full))) + {:thumbnails (persistent! thumbnails-acc) :full (persistent! full-acc)}))] + + (process-pending-no-sideffects thumbnails full set-objects-cb))) + (defn- set-objects-sync "Synchronously process all shapes (for small shape counts)." [shapes render-callback on-shapes-ready] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index c1ebc94839..1008fae3e1 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -407,6 +407,15 @@ pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> { Ok(()) } +#[no_mangle] +#[wasm_error] +pub extern "C" fn has_shape(a: u32, b: u32, c: u32, d: u32) -> Result { + with_state!(state, { + let id = uuid_from_u32_quartet(a, b, c, d); + return Ok(state.has_shape(id)); + }); +} + #[no_mangle] #[wasm_error] pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> { diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 116e7a67c4..c622372a46 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -119,6 +119,10 @@ impl State { self.current_id = Some(id); } + pub fn has_shape(&mut self, id: Uuid) -> bool { + self.shapes.has(&id) + } + pub fn delete_shape_children(&mut self, parent_id: Uuid, id: Uuid) { let render_state = get_render_state();