From 0494dc843f01f0f745ca6294cc714e42a14750a5 Mon Sep 17 00:00:00 2001 From: Aitor Date: Mon, 4 Sep 2023 11:20:11 +0200 Subject: [PATCH] :recycle: Refactor thumbnails --- frontend/resources/styles/common/base.scss | 5 + frontend/src/app/main.cljs | 2 + .../app/main/data/workspace/persistence.cljs | 2 +- .../app/main/data/workspace/thumbnails.cljs | 87 +++--- frontend/src/app/main/fonts.cljs | 22 ++ frontend/src/app/main/imposters.cljs | 77 +++++ frontend/src/app/main/rasterizer.cljs | 10 +- frontend/src/app/main/refs.cljs | 4 +- frontend/src/app/main/render.cljs | 21 +- .../src/app/main/ui/workspace/shapes.cljs | 4 +- .../app/main/ui/workspace/shapes/frame.cljs | 121 ++++---- .../ui/workspace/shapes/frame/node_store.cljs | 55 ---- .../shapes/frame/thumbnail_render.cljs | 265 ------------------ .../ui/workspace/viewport/interactions.cljs | 4 +- .../ui/workspace/viewport/pixel_overlay.cljs | 2 +- frontend/src/app/util/dom.cljs | 5 + frontend/src/app/util/imposters.cljs | 15 + frontend/src/app/worker/thumbnails.cljs | 18 -- 18 files changed, 259 insertions(+), 460 deletions(-) create mode 100644 frontend/src/app/main/imposters.cljs delete mode 100644 frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs delete mode 100644 frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs create mode 100644 frontend/src/app/util/imposters.cljs diff --git a/frontend/resources/styles/common/base.scss b/frontend/resources/styles/common/base.scss index 145658f68..fd1e4ecb0 100644 --- a/frontend/resources/styles/common/base.scss +++ b/frontend/resources/styles/common/base.scss @@ -262,3 +262,8 @@ input[type="number"] { -webkit-user-select: text; user-select: text; } + +[data-hidden="true"] { + display: none; + pointer-events: none; +} diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 93aea3d18..0bb7cc3bd 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -15,6 +15,7 @@ [app.main.data.websocket :as ws] [app.main.errors] [app.main.features :as feat] + [app.main.imposters :as imp] [app.main.rasterizer :as thr] [app.main.store :as st] [app.main.ui :as ui] @@ -113,6 +114,7 @@ (theme/init! cf/themes) (cur/init-styles) (thr/init!) + (imp/init!) (init-ui) (st/emit! (initialize))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 1ccdc27d1..158e564a2 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -169,7 +169,7 @@ (->> (rx/from frame-updates) (rx/mapcat (fn [[page-id frames]] (->> frames (map #(vector page-id %))))) - (rx/map (fn [[page-id frame-id]] (dwt/update-thumbnail file-id page-id frame-id)))) + (rx/map (fn [[_ frame-id]] (dwt/update-thumbnail frame-id)))) (->> (rx/from (concat lagged commits)) (rx/merge-map diff --git a/frontend/src/app/main/data/workspace/thumbnails.cljs b/frontend/src/app/main/data/workspace/thumbnails.cljs index 9684ad780..06218f02a 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -7,65 +7,61 @@ (ns app.main.data.workspace.thumbnails (:require [app.common.data.macros :as dm] + [app.common.logging :as log] [app.common.pages.helpers :as cph] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.state-helpers :as wsh] + [app.main.rasterizer :as thr] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] - [app.main.worker :as uw] - [app.util.dom :as dom] [app.util.http :as http] + [app.util.imposters :as imps] + [app.util.time :as tp] [app.util.timers :as tm] [app.util.webapi :as wapi] [beicon.core :as rx] [potok.core :as ptk])) -(defn force-render-stream - "Stream that will inform the frame-wrapper to mount into memory" - [id] - (->> st/stream - (rx/filter (ptk/type? ::force-render)) - (rx/map deref) - (rx/filter #(= % id)) - (rx/take 1))) +(log/set-level! :debug) + +(declare set-workspace-thumbnail) (defn get-thumbnail - [object-id] - ;; Look for the thumbnail canvas to send the data to the backend - (let [node (dom/query (dm/fmt "image.thumbnail-bitmap[data-object-id='%']" object-id)) - stopper (->> st/stream - (rx/filter (ptk/type? :app.main.data.workspace/finalize-page)) - (rx/take 1))] - ;; renders #svg image - (if (some? node) - (->> (rx/from (wapi/create-image-bitmap-with-workaround node)) - (rx/switch-map #(uw/ask! {:cmd :thumbnails/render-offscreen-canvas} %)) - (rx/map :result)) - - ;; Not found, we retry after delay - (->> (rx/timer 250) - (rx/merge-map (partial get-thumbnail object-id)) - (rx/take-until stopper))))) + [id] + (let [object-id (dm/str id) + tp (tp/tpoint-ms)] + (->> (rx/of id) + (rx/mapcat @imps/render-fn) + (rx/filter #(= object-id (unchecked-get % "id"))) + (rx/take 1) + (rx/map (fn [imposter] + {:data (unchecked-get imposter "data") + :styles (unchecked-get imposter "styles") + :width (unchecked-get imposter "width")})) + (rx/mapcat thr/render) + (rx/map (fn [blob] (wapi/create-uri blob))) + (rx/tap #(log/debug :hint "generated thumbnail" :elapsed (dm/str (tp) "ms")))))) (defn clear-thumbnail - [page-id frame-id] + [frame-id] (ptk/reify ::clear-thumbnail ptk/UpdateEvent (update [_ state] - (let [object-id (dm/str page-id frame-id)] - (when-let [uri (dm/get-in state [:workspace-thumbnails object-id])] - (tm/schedule-on-idle (partial wapi/revoke-uri uri))) - (update state :workspace-thumbnails dissoc object-id))))) + (let [object-id (dm/str frame-id)] + (when-let [uri (dm/get-in state [:workspace-thumbnails object-id])] + (tm/schedule-on-idle (partial wapi/revoke-uri uri))) + (update state :workspace-thumbnails dissoc object-id))))) (defn set-workspace-thumbnail - [object-id uri] + [id uri] (let [prev-uri* (volatile! nil)] (ptk/reify ::set-workspace-thumbnail ptk/UpdateEvent (update [_ state] - (let [prev-uri (dm/get-in state [:workspace-thumbnails object-id])] + (let [object-id (dm/str id) + prev-uri (dm/get-in state [:workspace-thumbnails object-id])] (some->> prev-uri (vreset! prev-uri*)) (update state :workspace-thumbnails assoc object-id uri))) @@ -78,29 +74,26 @@ (ptk/reify ::duplicate-thumbnail ptk/UpdateEvent (update [_ state] - (let [page-id (:current-page-id state) - thumbnail (dm/get-in state [:workspace-thumbnails (dm/str page-id old-id)])] - (update state :workspace-thumbnails assoc (dm/str page-id new-id) thumbnail))))) + (let [old-id (dm/str old-id) + new-id (dm/str new-id) + thumbnail (dm/get-in state [:workspace-thumbnails old-id])] + (update state :workspace-thumbnails assoc new-id thumbnail))))) (defn update-thumbnail "Updates the thumbnail information for the given frame `id`" - ([page-id frame-id] - (update-thumbnail nil page-id frame-id)) - - ([file-id page-id frame-id] + ([id] (ptk/reify ::update-thumbnail ptk/WatchEvent (watch [_ state _] - (let [object-id (dm/str page-id frame-id) - file-id (or file-id (:current-file-id state))] - + (let [object-id (dm/str id) + file-id (:current-file-id state)] (rx/concat ;; Delete the thumbnail first so if we interrupt we can regenerate after (->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id}) (rx/catch rx/empty)) ;; Send the update to the back-end - (->> (get-thumbnail object-id) + (->> (get-thumbnail id) (rx/filter (fn [data] (and (some? data) (some? file-id)))) (rx/merge-map (fn [uri] @@ -110,7 +103,7 @@ (->> (http/send! {:uri uri :response-type :blob :method :get}) (rx/map :body) (rx/mapcat (fn [blob] - ;; Send the data to backend + ;; Send the data to backend (let [params {:file-id file-id :object-id object-id :media blob}] @@ -193,9 +186,9 @@ (->> (rx/merge (->> frame-changes-s (rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state)))) - (rx/map (fn [[page-id frame-id]] (clear-thumbnail page-id frame-id)))) + (rx/map (fn [[_ frame-id]] (clear-thumbnail frame-id)))) (->> frame-changes-s (rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state)))) - (rx/map (fn [[_ frame-id]] (ptk/data-event ::force-render frame-id))))) + (rx/map (fn [[_ frame-id]] (update-thumbnail frame-id))))) (rx/take-until stopper)))))) diff --git a/frontend/src/app/main/fonts.cljs b/frontend/src/app/main/fonts.cljs index 57fd19c15..dbae6943a 100644 --- a/frontend/src/app/main/fonts.cljs +++ b/frontend/src/app/main/fonts.cljs @@ -297,3 +297,25 @@ (->> (rx/from font-refs) (rx/mapcat fetch-font-css) (rx/reduce (fn [acc css] (dm/str acc "\n" css)) ""))) + +(defonce font-styles (js/Map.)) + +(defn get-font-style-id + [{:keys [font-id font-variant-id] + :or {font-variant-id "regular"}}] + (dm/fmt "%:%" font-id font-variant-id)) + +(defn get-font-styles-by-font-ref + [font-ref] + (let [id (get-font-style-id font-ref)] + (if (.has font-styles id) + (rx/of (.get font-styles id)) + (->> (rx/of font-ref) + (rx/mapcat fetch-font-css) + (rx/tap (fn [css] (.set font-styles id css))))))) + +(defn render-font-styles-cached + [font-refs] + (->> (rx/from font-refs) + (rx/merge-map get-font-styles-by-font-ref) + (rx/reduce (fn [acc css] (dm/str acc "\n" css)) ""))) diff --git a/frontend/src/app/main/imposters.cljs b/frontend/src/app/main/imposters.cljs new file mode 100644 index 000000000..fd7958cf4 --- /dev/null +++ b/frontend/src/app/main/imposters.cljs @@ -0,0 +1,77 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.main.imposters + (:require ["react-dom/server" :as rds] + [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] + [app.main.data.workspace.state-helpers :as wsh] + [app.main.fonts :as fonts] + [app.main.refs :as refs] + [app.main.render :as render] + [app.main.store :as st] + [app.main.ui.shapes.text.fontfaces :as ff] + [app.util.imposters :as imps] + [app.util.thumbnails :as th] + [beicon.core :as rx] + [rumext.v2 :as mf])) + +(defn render + "Render the frame and store it in the imposter map" + ([id shape objects] + (render id shape objects nil)) + ([id shape objects fonts] + (let [object-id (dm/str id) + shape (if (nil? shape) (get objects id) shape) + fonts (if (nil? fonts) (ff/shape->fonts shape objects) fonts) + + all-children (deref (refs/all-children-objects id)) + + bounds + (if (:show-content shape) + (gsh/shapes->rect (cons shape all-children)) + (-> shape :points grc/points->rect)) + + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) + + viewbox (dm/fmt "% % % %" x y width height) + + [fixed-width fixed-height] (th/get-proportional-size width height) + + data (rds/renderToStaticMarkup + (mf/element render/frame-imposter-svg + {:objects objects + :frame shape + :vbox viewbox + :width width + :height height + :show-thumbnails? false}))] + (->> (fonts/render-font-styles-cached fonts) + (rx/catch rx/empty) + (rx/map (fn [styles] #js {:id object-id + :data data + :viewbox viewbox + :width fixed-width + :height fixed-height + :styles styles})))))) + +(defn render-by-id + "Render the shape by its id (IMPORTANT! id as uuid, not string)" + [id] + (dm/assert! "expected uuid" (uuid? id)) + (let [objects (wsh/lookup-page-objects @st/state) + shape (get objects id) + fonts (ff/shape->fonts shape objects)] + (render id shape objects fonts))) + +(defn init! + "Initializes the render function" + [] + (imps/init! render-by-id)) diff --git a/frontend/src/app/main/rasterizer.cljs b/frontend/src/app/main/rasterizer.cljs index ebf58e3e9..7c3cedeb4 100644 --- a/frontend/src/app/main/rasterizer.cljs +++ b/frontend/src/app/main/rasterizer.cljs @@ -7,8 +7,8 @@ (ns app.main.rasterizer "A main entry point for the rasterizer API interface. - This ns is responsible to provide an API for create thumbnail - renderer iframes and interact with them using asyncrhonous + This ns is responsible to provide an API for create rasterizer + iframes and interact with them using asyncrhonous messages." (:require [app.common.data :as d] @@ -36,7 +36,7 @@ (recur (.shift ^js queue))))) (defn- on-message - "Handles a message from the thumbnail renderer." + "Handles a message from the rasterizer." [event] (let [evorigin (unchecked-get event "origin") evdata (unchecked-get event "data")] @@ -51,7 +51,7 @@ (rx/push! msgbus evdata)))))) (defn- send-message! - "Sends a message to the thumbnail renderer." + "Sends a message to the rasterizer." [message] (let [window (.-contentWindow ^js instance)] (.postMessage ^js window message origin))) @@ -94,7 +94,7 @@ (render {:data data :styles styles :width width :result result}))) (defn init! - "Initializes the thumbnail renderer." + "Initializes the rasterizer." [] (let [iframe (dom/create-element "iframe")] (dom/set-attribute! iframe "src" origin) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 867a225c0..fab0cbcc9 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -479,10 +479,10 @@ (l/derived #(get % :workspace-thumbnails {}) st/state)) (defn thumbnail-frame-data - [page-id frame-id] + [frame-id] (l/derived (fn [thumbnails] - (get thumbnails (dm/str page-id frame-id))) + (get thumbnails (dm/str frame-id))) thumbnail-data)) (def workspace-text-modifier diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index 1d93a1ace..f6facae45 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -229,6 +229,25 @@ [:& shape-wrapper {:shape item :key (:id item)}])]]]])) +;; Component that serves for render frame thumbnails, mainly used in +;; the viewer and inspector +(mf/defc frame-imposter-svg + {::mf/wrap [mf/memo]} + [{:keys [objects frame vbox width height show-thumbnails?] :as props}] + (let [shape-wrapper + (mf/use-memo + (mf/deps objects) + #(shape-wrapper-factory objects))] + + [:& (mf/provider muc/render-thumbnails) {:value show-thumbnails?} + [:svg {:view-box vbox + :width (ust/format-precision width viewbox-decimal-precision) + :height (ust/format-precision height viewbox-decimal-precision) + :version "1.1" + :xmlns "http://www.w3.org/2000/svg" + :xmlnsXlink "http://www.w3.org/1999/xlink" + :fill "none"} + [:& shape-wrapper {:shape frame}]]])) ;; Component that serves for render frame thumbnails, mainly used in ;; the viewer and inspector @@ -443,7 +462,7 @@ (for [[id component] (source data)] (let [component (ctf/load-component-objects data component)] [:& component-symbol {:key (dm/str id) :component component}]))] - + children]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 3d4d42f78..20a14b31d 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -35,8 +35,8 @@ (declare group-wrapper) (declare svg-raw-wrapper) (declare bool-wrapper) -(declare root-frame-wrapper) (declare nested-frame-wrapper) +(declare root-frame-wrapper) (def circle-wrapper (common/generic-wrapper-factory circle/circle-shape)) (def image-wrapper (common/generic-wrapper-factory image/image-shape)) @@ -121,5 +121,5 @@ (def group-wrapper (group/group-wrapper-factory shape-wrapper)) (def svg-raw-wrapper (svg-raw/svg-raw-wrapper-factory shape-wrapper)) (def bool-wrapper (bool/bool-wrapper-factory shape-wrapper)) -(def root-frame-wrapper (frame/root-frame-wrapper-factory shape-wrapper)) (def nested-frame-wrapper (frame/nested-frame-wrapper-factory shape-wrapper)) +(def root-frame-wrapper (frame/root-frame-wrapper-factory shape-wrapper)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 0fc57d3e6..e02088953 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,24 +6,20 @@ (ns app.main.ui.workspace.shapes.frame (:require - [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.geom.rect :as grc] + [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.main.data.workspace.state-helpers :as wsh] [app.main.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] [app.main.store :as st] - [app.main.ui.context :as ctx] - [app.main.ui.hooks :as hooks] [app.main.ui.shapes.embed :as embed] [app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.shape :refer [shape-container]] - [app.main.ui.shapes.text.fontfaces :as ff] [app.main.ui.workspace.shapes.common :refer [check-shape-props]] [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] - [app.main.ui.workspace.shapes.frame.node-store :as fns] - [app.main.ui.workspace.shapes.frame.thumbnail-render :as ftr] - [beicon.core :as rx] + [debug :refer [debug?]] [rumext.v2 :as mf])) (defn frame-shape-factory @@ -44,7 +40,7 @@ [:& (mf/provider embed/context) {:value true} [:& shape-container {:shape shape :ref ref :disable-shadows? (cph/is-direct-child-of-root? shape)} - [:& frame-shape {:shape shape :childs childs} ]]])))) + [:& frame-shape {:shape shape :childs childs}]]])))) (defn check-props [new-props old-props] @@ -86,74 +82,75 @@ (let [shape (unchecked-get props "shape") thumbnail? (unchecked-get props "thumbnail?") - page-id (mf/use-ctx ctx/current-page-id) - frame-id (dm/get-prop shape :id) + ;; page-id (mf/use-ctx ctx/current-page-id) + frame-id (:id shape) - objects (wsh/lookup-page-objects @st/state page-id) + objects (wsh/lookup-page-objects @st/state) - node-ref (mf/use-ref nil) - root-ref (mf/use-ref nil) + container-ref (mf/use-ref nil) + content-ref (mf/use-ref nil) - force-render* (mf/use-state false) - force-render? (deref force-render*) + all-children-ref (mf/with-memo [frame-id] + (refs/all-children-objects frame-id)) + all-children (mf/deref all-children-ref) - ;; when `true` we've called the mount for the frame - rendered-ref (mf/use-ref false) + bounds + (if (:show-content shape) + (gsh/shapes->rect (cons shape all-children)) + (-> shape :points grc/points->rect)) + + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) + + thumbnail-uri* (mf/with-memo [frame-id] + (refs/thumbnail-frame-data frame-id)) + thumbnail-uri (mf/deref thumbnail-uri*) modifiers-ref (mf/with-memo [frame-id] (refs/workspace-modifiers-by-frame-id frame-id)) modifiers (mf/deref modifiers-ref) - fonts (mf/with-memo [shape objects] - (ff/shape->fonts shape objects)) - fonts (hooks/use-equal-memo fonts) + debug? (debug? :thumbnails)] - disable-thumbnail? (d/not-empty? (dm/get-in modifiers [frame-id :modifiers])) + (when-not (some? thumbnail-uri) + (st/emit! (dwt/update-thumbnail frame-id))) - [on-load-frame-dom render-frame? children] - (ftr/use-render-thumbnail page-id shape root-ref node-ref rendered-ref disable-thumbnail? force-render?) - - on-frame-load - (fns/use-node-store node-ref rendered-ref thumbnail? render-frame?) - - ] - - (fdm/use-dynamic-modifiers objects (mf/ref-val node-ref) modifiers) - - (mf/with-effect [] - ;; When a change in the data is received a "force-render" event is emitted - ;; that will force the component to be mounted in memory - (let [sub (->> (dwt/force-render-stream frame-id) - (rx/take-while #(not (mf/ref-val rendered-ref))) - (rx/subs #(reset! force-render* true)))] - #(some-> sub rx/dispose!))) - - (mf/with-effect [shape fonts thumbnail? on-load-frame-dom force-render? render-frame?] - (when (and (some? (mf/ref-val node-ref)) - (or (mf/ref-val rendered-ref) - (false? thumbnail?) - (true? force-render?) - (true? render-frame?))) - - (when (false? (mf/ref-val rendered-ref)) - (when-let [node (mf/ref-val node-ref)] - (mf/set-ref-val! root-ref (mf/create-root node)) - (mf/set-ref-val! rendered-ref true))) - - (when-let [root (mf/ref-val root-ref)] - (mf/render! root (mf/element frame-shape #js {:ref on-load-frame-dom :shape shape :fonts fonts}))) - - (constantly nil))) + (fdm/use-dynamic-modifiers objects (mf/ref-val content-ref) modifiers) [:& shape-container {:shape shape} [:g.frame-container {:id (dm/str "frame-container-" frame-id) :key "frame-container" - :ref on-frame-load + ;; :ref on-container-ref :opacity (when (:hidden shape) 0)} - [:& ff/fontfaces-style {:fonts fonts}] - [:g.frame-thumbnail-wrapper - {:id (dm/str "thumbnail-container-" frame-id) - ;; Hide the thumbnail when not displaying - :opacity (when-not thumbnail? 0)} - children]]])))) + + ;; When thumbnail is enabled. + [:g.frame-imposter + ;; Render thumbnail image. + [:image.thumbnail-bitmap + {;; :ref on-imposter-ref + :x x + :y y + :width width + :height height + :href thumbnail-uri + :style {:display (when-not thumbnail? "none")}}] + + ;; Render border around image when we are debugging + ;; thumbnails. + (when ^boolean debug? + [:rect {:x (+ x 2) + :y (+ y 2) + :width (- width 4) + :height (- height 4) + :stroke "#f0f" + :stroke-width 2}])] + + ;; When thumbnail is disabled. + (when-not thumbnail? + [:g.frame-content + {:id (dm/str "frame-content-" frame-id) + :ref container-ref} + [:& frame-shape {:shape shape :ref content-ref}]])]])))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs deleted file mode 100644 index c94d6bd93..000000000 --- a/frontend/src/app/main/ui/workspace/shapes/frame/node_store.cljs +++ /dev/null @@ -1,55 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.shapes.frame.node-store - (:require - [app.util.dom :as dom] - [rumext.v2 :as mf])) - -(defn use-node-store - "Hook responsible of storing the rendered DOM node in memory while not being used" - [node-ref rendered-ref thumbnail? render-frame?] - - (let [re-render* (mf/use-state 0) - parent-ref (mf/use-ref nil) - present-ref (mf/use-ref false) - - on-frame-load - (mf/use-fn - (fn [node] - (when (and (some? node) - (nil? (mf/ref-val node-ref))) - (let [content (-> (dom/create-element "http://www.w3.org/2000/svg" "g") - (dom/add-class! "frame-content"))] - (mf/set-ref-val! node-ref content) - (mf/set-ref-val! parent-ref node) - (swap! re-render* inc)))))] - - (mf/with-effect [thumbnail? render-frame?] - (let [rendered? (mf/ref-val rendered-ref) - present? (mf/ref-val present-ref)] - - (when (and (true? rendered?) - (true? thumbnail?) - (false? render-frame?) - (true? present?)) - (when-let [parent (mf/ref-val parent-ref)] - (when-let [node (mf/ref-val node-ref)] - (dom/remove-child! parent node) - (mf/set-ref-val! present-ref false) - (swap! re-render* inc)))) - - (when (and (false? present?) - (or (false? thumbnail?) - (true? render-frame?))) - (when-let [parent (mf/ref-val parent-ref)] - (when-let [node (mf/ref-val node-ref)] - (when-not (dom/child? parent node) - (dom/append-child! parent node) - (mf/set-ref-val! present-ref true) - (swap! re-render* inc))))))) - - on-frame-load)) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs deleted file mode 100644 index 513f6eab4..000000000 --- a/frontend/src/app/main/ui/workspace/shapes/frame/thumbnail_render.cljs +++ /dev/null @@ -1,265 +0,0 @@ -;; This Source Code Form is subject to the terms of the Mozilla Public -;; License, v. 2.0. If a copy of the MPL was not distributed with this -;; file, You can obtain one at http://mozilla.org/MPL/2.0/. -;; -;; Copyright (c) KALEIDOS INC - -(ns app.main.ui.workspace.shapes.frame.thumbnail-render - (:require - [app.common.data :as d] - [app.common.data.macros :as dm] - [app.common.geom.rect :as grc] - [app.common.geom.shapes :as gsh] - [app.config :as cf] - [app.main.data.workspace.thumbnails :as dwt] - [app.main.rasterizer :as thr] - [app.main.refs :as refs] - [app.main.store :as st] - [app.main.ui.hooks :as hooks] - [app.main.ui.shapes.frame :as frame] - [app.util.dom :as dom] - [app.util.thumbnails :as th] - [app.util.timers :as ts] - [app.util.webapi :as wapi] - [beicon.core :as rx] - [cuerdas.core :as str] - [debug :refer [debug?]] - [rumext.v2 :as mf])) - -(defn- remove-image-loading - "Remove the changes related to change a url for its embed value. This is necessary - so we don't have to recalculate the thumbnail when the image loads." - [value] - (if (.isArray js/Array value) - (->> value - (remove (fn [change] - (or (= "data-loading" (.-attributeName change)) - (and (= "attributes" (.-type change)) - (= "href" (.-attributeName change)) - (str/starts-with? (.-oldValue change) "http")))))) - [value])) - -(defn- create-svg-blob-uri-from - [rect node style-node] - (let [{:keys [x y width height]} rect - viewbox (dm/str x " " y " " width " " height) - - ;; Calculate the fixed width and height - ;; We don't want to generate thumbnails - ;; bigger than 2000px - [fixed-width fixed-height] (th/get-proportional-size width height) - - ;; This is way faster than creating a node - ;; through the DOM API - svg-data - (dm/fmt "% %" - viewbox - fixed-width - fixed-height - (if (some? style-node) (dom/node->xml style-node) "") - (dom/node->xml node))] - - (->> (rx/of {:data svg-data :width fixed-width}) - (rx/mapcat thr/render) - (rx/map wapi/create-uri)))) - -(defn use-render-thumbnail - "Hook that will create the thumbnail data" - [page-id {:keys [id] :as shape} root-ref node-ref rendered-ref disable? force-render] - - (let [;; frame-image-ref (mf/use-ref nil) - - disable-ref (mf/use-ref disable?) - regenerate-ref (mf/use-ref false) - - all-children-ref (mf/with-memo [id] - (refs/all-children-objects id)) - all-children (mf/deref all-children-ref) - - bounds - (if (:show-content shape) - (gsh/shapes->rect (cons shape all-children)) - (-> shape :points grc/points->rect)) - - x (dm/get-prop bounds :x) - y (dm/get-prop bounds :y) - width (dm/get-prop bounds :width) - height (dm/get-prop bounds :height) - - ;; TODO: Si usamos rasterizer ya no necesitamos esto. - ;; svg-uri* (mf/use-state nil) - ;; svg-uri (deref svg-uri*) - - bitmap-uri* (mf/use-state nil) - bitmap-uri (deref bitmap-uri*) - - observer-ref (mf/use-ref nil) - - bounds-ref (hooks/use-update-ref bounds) - updates-s (mf/use-memo rx/subject) - - thumbnail-uri* (mf/with-memo [page-id id] - (refs/thumbnail-frame-data page-id id)) - thumbnail-uri (mf/deref thumbnail-uri*) - - ;; State to indicate to the parent that should render the frame - render-frame* (mf/use-state (not thumbnail-uri)) - render-frame? (deref render-frame*) - - debug? (debug? :thumbnails) - - ;; FIXME: Esto habría que sacarlo de aquí y llamarlo inmediatamente cuando se crea el frame - ;; además ya no tiene sentido utilizar el elemento del DOM porque no lo vamos a usar - ;; para nada. - on-bitmap-load - (mf/use-fn - (mf/deps thumbnail-uri) - (fn [] - ;; If we don't have the thumbnail data saved (normally the first load) we update the data - ;; when available - (when-not (some? thumbnail-uri) - (st/emit! (dwt/update-thumbnail page-id id))) - - (reset! render-frame* false))) - - generate-thumbnail - (mf/use-fn - (mf/deps id) - (fn generate-thumbnail [] - (try - ;; When starting generating the canvas we mark it as not ready so its not send to back until - ;; we have time to update it. - (when-let [node (mf/ref-val node-ref)] - (if (dom/has-children? node) - ;; The frame-content need to have children in order to generate the thumbnail. - (let [style-node (dom/query (dm/str "#frame-container-" id " style")) - bounds (mf/ref-val bounds-ref) - stream (create-svg-blob-uri-from bounds node style-node)] - - (->> stream - (rx/subs (fn [uri] (reset! bitmap-uri* uri))))) - - ;; Node not yet ready, we schedule a new generation. - (ts/raf generate-thumbnail))) - (catch :default e - (.error js/console e))))) - - on-change-frame - (mf/use-fn - (mf/deps id generate-thumbnail) - (fn [] - (when (and (some? (mf/ref-val node-ref)) - (some? (mf/ref-val rendered-ref)) - (some? (mf/ref-val regenerate-ref))) - (let [node (mf/ref-val node-ref) - loading-images? (some? (dom/query node "[data-loading='true']")) - loading-fonts? (some? (dom/query (dm/str "#frame-container-" id " > style[data-loading='true']")))] - (when (and (not loading-images?) - (not loading-fonts?)) - ;; (reset! svg-uri* nil) - (reset! bitmap-uri* nil) - (generate-thumbnail) - (mf/set-ref-val! regenerate-ref false)))))) - - ;; When the frame is updated, it is marked as not ready - ;; so that it is not sent to the background until - ;; it is regenerated. - on-update-frame - (mf/use-fn - (fn [] - (when-not ^boolean (mf/ref-val disable-ref) - ;; (reset! svg-uri* nil) - (reset! bitmap-uri* nil) - (reset! render-frame* true) - (mf/set-ref-val! regenerate-ref true)))) - - on-load-frame-dom - (mf/use-fn - (fn [node] - (when (and (nil? (mf/ref-val observer-ref)) (some? node)) - (when-not (some? @thumbnail-uri*) - (rx/push! updates-s :update)) - - (let [observer (js/MutationObserver. (partial rx/push! updates-s))] - (.observe observer node #js {:childList true - :attributes true - :attributeOldValue true - :characterData true - :subtree true}) - (mf/set-ref-val! observer-ref observer)))))] - - (mf/with-effect [thumbnail-uri] - (when (some? thumbnail-uri) - (reset! bitmap-uri* thumbnail-uri))) - - (mf/with-effect [force-render] - (when ^boolean force-render - (rx/push! updates-s :update))) - - (mf/with-effect [] - (let [subid (->> updates-s - (rx/map remove-image-loading) - (rx/filter d/not-empty?) - (rx/catch (fn [err] (.error js/console err))) - (rx/subs on-update-frame))] - (partial rx/dispose! subid))) - - ;; on-change-frame will get every change in the frame - (mf/with-effect [] - (let [subid (->> updates-s - (rx/debounce 400) - (rx/observe-on :af) - (rx/catch (fn [err] (.error js/console err))) - (rx/subs on-change-frame))] - (partial rx/dispose! subid))) - - (mf/with-effect [disable?] - (when (and ^boolean disable? (not (mf/ref-val disable-ref))) - (rx/push! updates-s :update)) - - (mf/set-ref-val! disable-ref disable?) - nil) - - (mf/with-effect [] - (fn [] - (when (and (some? (mf/ref-val node-ref)) - (true? (mf/ref-val rendered-ref))) - (when-let [root (mf/ref-val root-ref)] - ;; NOTE: the unmount should be always scheduled to be - ;; executed asynchronously of the current flow (react - ;; rules). - (ts/schedule #(mf/unmount! ^js root))) - - (mf/set-ref-val! node-ref nil) - (mf/set-ref-val! rendered-ref false) - - (when-let [observer (mf/ref-val observer-ref)] - (.disconnect ^js observer) - (mf/set-ref-val! observer-ref nil))))) - - [on-load-frame-dom render-frame? - (mf/html - [:& frame/frame-container {:bounds bounds :shape shape} - - ;; Safari don't support filters so instead we add a rectangle around the thumbnail - (when (and (cf/check-browser? :safari) - ^boolean debug?) - [:rect {:x (+ x 2) - :y (+ y 2) - :width (- width 4) - :height (- height 4) - :stroke "blue" - :stroke-width 2}]) - - ;; The frame content is the only thing that we want to render - (when (some? bitmap-uri) - [:image.thumbnail-bitmap - {:x x - :y y - :key (dm/str "thumbnail-canvas-" id) - :data-object-id (dm/str page-id id) - :width width - :height height - :href bitmap-uri - :style {:filter (when ^boolean debug? "sepia(1)")} - :on-load on-bitmap-load}])])])) diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 6b4ba2ad4..499d13a82 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -240,7 +240,9 @@ dest-shape-id (:id dest-shape) - thumbnail-data-ref (mf/use-memo (mf/deps page-id dest-shape-id) #(refs/thumbnail-frame-data page-id dest-shape-id)) + thumbnail-data-ref (mf/use-memo + (mf/deps page-id dest-shape-id) + #(refs/thumbnail-frame-data dest-shape-id)) thumbnail-data (mf/deref thumbnail-data-ref) dest-shape (cond-> dest-shape diff --git a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs index e84412dda..e911ae42e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/pixel_overlay.cljs @@ -49,7 +49,7 @@ (mf/deps viewport-node) (fn [event] (when-let [image-data (mf/ref-val canvas-image-data)] - (when-let [zoom-view-node (.getElementById js/document "picker-detail")] + (when-let [zoom-view-node (dom/get-element "picker-detail")] (when-not (mf/ref-val zoom-view-context) (mf/set-ref-val! zoom-view-context (.getContext zoom-view-node "2d"))) (let [{brx :left bry :top} (dom/get-bounding-rect viewport-node) diff --git a/frontend/src/app/util/dom.cljs b/frontend/src/app/util/dom.cljs index 94552f818..8ce4347ae 100644 --- a/frontend/src/app/util/dom.cljs +++ b/frontend/src/app/util/dom.cljs @@ -326,6 +326,11 @@ (.removeChild ^js el child)) el) +(defn remove! + [^js el] + (when (some? el) + (.remove ^js el))) + (defn get-first-child [^js el] (when (some? el) diff --git a/frontend/src/app/util/imposters.cljs b/frontend/src/app/util/imposters.cljs new file mode 100644 index 000000000..e4ebd22c1 --- /dev/null +++ b/frontend/src/app/util/imposters.cljs @@ -0,0 +1,15 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC + +(ns app.util.imposters) + +;; This is needed to avoid a circular dependency between +;; app.main.ui.workspace.shapes.frame and app.util.imposters +(defonce render-fn (atom nil)) + +(defn init! + [fn] + (reset! render-fn fn)) diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs index 7bf6203bb..56437a3d8 100644 --- a/frontend/src/app/worker/thumbnails.cljs +++ b/frontend/src/app/worker/thumbnails.cljs @@ -7,19 +7,15 @@ (ns app.worker.thumbnails (:require ["react-dom/server" :as rds] - [app.common.data.macros :as dm] [app.common.logging :as log] [app.common.uri :as u] [app.config :as cf] [app.main.fonts :as fonts] [app.main.render :as render] [app.util.http :as http] - [app.util.time :as ts] - [app.util.webapi :as wapi] [app.worker.impl :as impl] [beicon.core :as rx] [okulary.core :as l] - [promesa.core :as p] [rumext.v2 :as mf])) (log/set-level! :trace) @@ -82,17 +78,3 @@ [{:keys [file-id revn features] :as message} _] (->> (request-data-for-thumbnail file-id revn features) (rx/map render-thumbnail))) - -(defmethod impl/handler :thumbnails/render-offscreen-canvas - [_ ibpm] - (let [canvas (js/OffscreenCanvas. (.-width ^js ibpm) (.-height ^js ibpm)) - ctx (.getContext ^js canvas "bitmaprenderer") - tp (ts/tpoint-ms)] - (.transferFromImageBitmap ^js ctx ibpm) - (->> (.convertToBlob ^js canvas #js {:type "image/png"}) - (p/fmap (fn [blob] - {:result (wapi/create-uri blob)})) - (p/fnly (fn [_] - (log/debug :hint "generated thumbnail" :elapsed (dm/str (tp) "ms")) - (.close ^js ibpm)))))) -