🐛 Fix canvas not being re-rendered after restoring a file version

This commit is contained in:
Belén Albeza 2026-02-16 15:59:41 +01:00
parent d1d50138ed
commit b5f4ce0a71
5 changed files with 205 additions and 178 deletions

View File

@ -222,9 +222,16 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (let [pending-version-id (:workspace-pending-file-version-id state)
(assoc :thumbnails thumbnails) state (-> state
(update :files assoc file-id file))))) (assoc :thumbnails thumbnails)
(update :files assoc file-id file)
(dissoc :workspace-pending-file-version-id))]
(cond-> state
(some? pending-version-id)
(assoc :workspace-file-version-id pending-version-id)
(nil? pending-version-id)
(dissoc :workspace-file-version-id))))))
(defn zoom-to-frame (defn zoom-to-frame
[] []
@ -280,192 +287,197 @@
(wasm.api/process-object shape)))))) (wasm.api/process-object shape))))))
(defn initialize-workspace (defn initialize-workspace
[team-id file-id] ([team-id file-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`") (initialize-workspace team-id file-id nil))
(assert (uuid? file-id) "expected valud uuid for `file-id`") ([team-id file-id version-id]
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace (ptk/reify ::initialize-workspace
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :recent-colors (:recent-colors storage/user)) (assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user)) (assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
(assoc :workspace-presence {}))) (assoc :workspace-presence {})
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
;; when the new bundle is applied so the viewport re-inits with new data
(assoc :workspace-pending-file-version-id version-id)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state) rparams (rt/get-params state)
features (features/get-enabled-features state team-id) features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")] render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace" (log/debug :hint "initialize-workspace"
:team-id (dm/str team-id) :team-id (dm/str team-id)
:file-id (dm/str file-id)) :file-id (dm/str file-id))
(->> (rx/merge (->> (rx/merge
(rx/concat (rx/concat
;; Fetch all essential data that should be loaded before the file ;; Fetch all essential data that should be loaded before the file
(rx/merge (rx/merge
(if ^boolean render-wasm? (if ^boolean render-wasm?
(->> (rx/from @wasm/module) (->> (rx/from @wasm/module)
(rx/filter true?) (rx/filter true?)
(rx/tap (fn [_] (rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")] (let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event)))) (ug/dispatch! event))))
(rx/ignore)) (rx/ignore))
(rx/empty)) (rx/empty))
(->> stream (->> stream
(rx/filter (ptk/type? ::df/fonts-loaded)) (rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1) (rx/take 1)
(rx/ignore)) (rx/ignore))
(rx/of (ntf/hide) (rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id) (dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles) (dcmt/fetch-profiles)
(df/fetch-fonts team-id))) (df/fetch-fonts team-id)))
;; Once the essential data is fetched, lets proceed to ;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle ;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features))) (rx/of (fetch-bundle file-id features)))
(->> stream (->> stream
(rx/filter (ptk/type? ::bundle-fetched)) (rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1) (rx/take 1)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [file]}] (fn [{:keys [file]}]
(log/debug :hint "bundle fetched" (log/debug :hint "bundle fetched"
:team-id (dm/str team-id) :team-id (dm/str team-id)
:file-id (dm/str file-id)) :file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file)) (rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id) (dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout) (dwsl/initialize-shape-layout)
(fetch-libraries file-id features) (fetch-libraries file-id features)
(-> (workspace-initialized file-id) (-> (workspace-initialized file-id)
(with-meta {:team-id team-id (with-meta {:team-id team-id
:file-id file-id})))))) :file-id file-id}))))))
;; Install dev perf observers once the workspace is ready ;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs) (when (contains? cf/flags :perf-logs)
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1) (rx/take 1)
(rx/tap (fn [_] (perf/setup))))) (rx/tap (fn [_] (perf/setup)))))
(->> stream (->> stream
(rx/filter (ptk/type? ::dps/persistence-notification)) (rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1) (rx/take 1)
(rx/map dwc/set-workspace-visited)) (rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id uuid/parse)] (when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async) (rx/observe-on :async)
(rx/take 1) (rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams))))) (rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when (:board-id rparams) (when (:board-id rparams)
(->> stream (->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport)) (rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1) (rx/take 1)
(rx/map zoom-to-frame))) (rx/map zoom-to-frame)))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)] (when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async) (rx/observe-on :async)
(rx/take 1) (rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id)))) (rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm? (when render-wasm?
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [redo-changes]}] (fn [{:keys [redo-changes]}]
(let [added (->> redo-changes (let [added (->> redo-changes
(filter #(= (:type %) :add-obj)) (filter #(= (:type %) :add-obj))
(map :id))] (map :id))]
(->> (rx/from added) (->> (rx/from added)
(rx/map process-wasm-object))))))) (rx/map process-wasm-object)))))))
(when render-wasm? (when render-wasm?
(let [local-commits-s (let [local-commits-s
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/filter #(and (= :local (:source %)) (rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data)))) (not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?))) (rx/filter (complement empty?)))
notifier-s notifier-s
(rx/merge (rx/merge
(->> local-commits-s (rx/debounce 1000)) (->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?))) (->> stream (rx/filter dps/force-persist?)))
objects-s objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})] (rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s (->> local-commits-s
(rx/buffer-until notifier-s) (rx/buffer-until notifier-s)
(rx/with-latest-from objects-s) (rx/with-latest-from objects-s)
(rx/map (rx/map
(fn [[commits objects]] (fn [[commits objects]]
(->> commits (->> commits
(mapcat :redo-changes) (mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %))) (filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %))) (filter #(cfh/text-shape? objects (:id %)))
(map #(vector (map #(vector
(:id %) (:id %)
(wasm.api/calculate-position-data (get objects (:id %)))))))) (wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s) (rx/with-latest-from current-page-id-s)
(rx/map (rx/map
(fn [[text-position-data page-id]] (fn [[text-position-data page-id]]
(let [changes (let [changes
(->> text-position-data (->> text-position-data
(mapv (fn [[id position-data]] (mapv (fn [[id position-data]]
{:type :mod-obj {:type :mod-obj
:id id :id id
:page-id page-id :page-id page-id
:operations :operations
[{:type :set [{:type :set
:attr :position-data :attr :position-data
:val position-data :val position-data
:ignore-touched true :ignore-touched true
:ignore-geometry true}]})))] :ignore-geometry true}]})))]
(when (d/not-empty? changes) (when (d/not-empty? changes)
(dch/commit-changes (dch/commit-changes
{:redo-changes changes :undo-changes [] {:redo-changes changes :undo-changes []
:save-undo? false :save-undo? false
:tags #{:position-data}}))))) :tags #{:position-data}})))))
(rx/take-until stoper-s)))) (rx/take-until stoper-s))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}] (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes)) (if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes (let [entry {:undo-changes undo-changes
:redo-changes redo-changes :redo-changes redo-changes
:undo-group undo-group :undo-group undo-group
:tags tags}] :tags tags}]
(rx/of (dwu/append-undo entry stack-undo?))) (rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty)))))) (rx/empty))))))
(rx/take-until stoper-s)))) (rx/take-until stoper-s))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ _] (effect [_ _ _]
(let [name (dm/str "workspace-" file-id)] (let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name))))) (unchecked-set ug/global "name" name))))))
(defn finalize-workspace (defn finalize-workspace
[_team-id file-id] [_team-id file-id]

View File

@ -108,7 +108,7 @@
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!)) (rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace team-id file-id))) (rx/map #(dw/initialize-workspace team-id file-id id)))
(case origin (case origin
:version :version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"})) (rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
@ -231,7 +231,7 @@
(rx/filter #(or (nil? %) (= :saved %))) (rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace team-id file-id))) (rx/map #(dw/initialize-workspace team-id file-id id)))
(->> (rx/of 1) (->> (rx/of 1)
(rx/tap resolve) (rx/tap resolve)

View File

@ -256,6 +256,9 @@
(def workspace-layout (def workspace-layout
(l/derived :workspace-layout st/state)) (l/derived :workspace-layout st/state))
(def workspace-file-version-id
(l/derived :workspace-file-version-id st/state))
(def snap-pixel? (def snap-pixel?
(l/derived #(contains? % :snap-pixel-grid) workspace-layout)) (l/derived #(contains? % :snap-pixel-grid) workspace-layout))

View File

@ -50,7 +50,7 @@
(mf/defc workspace-content* (mf/defc workspace-content*
{::mf/private true} {::mf/private true}
[{:keys [file layout page wglobal]}] [{:keys [file layout page wglobal file-version-id]}]
(let [palete-size (mf/use-state nil) (let [palete-size (mf/use-state nil)
selected (mf/deref refs/selected-shapes) selected (mf/deref refs/selected-shapes)
@ -109,6 +109,7 @@
:wglobal wglobal :wglobal wglobal
:selected selected :selected selected
:layout layout :layout layout
:file-version-id file-version-id
:palete-size :palete-size
(when (and (or colorpalette? textpalette?) (not hide-ui?)) (when (and (or colorpalette? textpalette?) (not hide-ui?))
@palete-size)}]]] @palete-size)}]]]
@ -168,7 +169,7 @@
(mf/defc workspace-inner* (mf/defc workspace-inner*
{::mf/private true} {::mf/private true}
[{:keys [page-id file-id file layout wglobal]}] [{:keys [page-id file-id file layout wglobal file-version-id]}]
(let [page-ref (mf/with-memo [file-id page-id] (let [page-ref (mf/with-memo [file-id page-id]
(make-page-ref file-id page-id)) (make-page-ref file-id page-id))
page (mf/deref page-ref)] page (mf/deref page-ref)]
@ -187,7 +188,8 @@
[:> workspace-content* {:file file [:> workspace-content* {:file file
:page page :page page
:wglobal wglobal :wglobal wglobal
:layout layout}] :layout layout
:file-version-id file-version-id}]
[:> workspace-loader*]))) [:> workspace-loader*])))
(mf/defc workspace* (mf/defc workspace*
@ -199,6 +201,7 @@
layout (mf/deref refs/workspace-layout) layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global) wglobal (mf/deref refs/workspace-global)
file-version-id (mf/deref refs/workspace-file-version-id)
team-ref (mf/with-memo [team-id] team-ref (mf/with-memo [team-id]
(make-team-ref team-id)) (make-team-ref team-id))
@ -274,7 +277,8 @@
:file-id file-id :file-id file-id
:file file :file file
:wglobal wglobal :wglobal wglobal
:layout layout}]) :layout layout
:file-version-id file-version-id}])
(when (or (not (and file-loaded? page-id)) (when (or (not (and file-loaded? page-id))
;; in wasm renderer, extend the pixel loader until the first frame is rendered ;; in wasm renderer, extend the pixel loader until the first frame is rendered
;; but do not apply it when switching pages ;; but do not apply it when switching pages

View File

@ -73,7 +73,7 @@
objects))) objects)))
(mf/defc viewport* (mf/defc viewport*
[{:keys [selected wglobal wlocal layout file page palete-size]}] [{:keys [selected wglobal wlocal layout file page palete-size file-version-id]}]
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
;; that the new parameter is sent ;; that the new parameter is sent
{:keys [edit-path {:keys [edit-path
@ -141,6 +141,7 @@
canvas-ref (mf/use-ref nil) canvas-ref (mf/use-ref nil)
text-editor-ref (mf/use-ref nil) text-editor-ref (mf/use-ref nil)
last-file-version-id-ref (mf/use-ref nil)
;; STATE REFS ;; STATE REFS
disable-paste-ref (mf/use-ref false) disable-paste-ref (mf/use-ref false)
@ -344,11 +345,18 @@
(when (and @canvas-init? preview-blend) (when (and @canvas-init? preview-blend)
(wasm.api/request-render "with-effect"))) (wasm.api/request-render "with-effect")))
(mf/with-effect [@canvas-init? zoom vbox background] (mf/with-effect [@canvas-init? file-version-id zoom vbox background]
(when (and @canvas-init? (not @initialized?)) (when @canvas-init?
(wasm.api/clear-canvas-pixels) (if (not @initialized?)
(wasm.api/initialize-viewport base-objects zoom vbox background) (do
(reset! initialized? true))) (wasm.api/clear-canvas-pixels)
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)
(mf/set-ref-val! last-file-version-id-ref file-version-id))
(when (and (some? file-version-id)
(not= file-version-id (mf/ref-val last-file-version-id-ref)))
(wasm.api/initialize-viewport base-objects zoom vbox background)
(mf/set-ref-val! last-file-version-id-ref file-version-id)))))
(mf/with-effect [focus] (mf/with-effect [focus]
(when (and @canvas-init? @initialized?) (when (and @canvas-init? @initialized?)