0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-16 00:41:25 -05:00

♻️ Refactor thumbnails

This commit is contained in:
Aitor 2023-09-04 11:20:11 +02:00 committed by Andrey Antukh
parent 0721fc9d80
commit 0494dc843f
18 changed files with 259 additions and 460 deletions

View file

@ -262,3 +262,8 @@ input[type="number"] {
-webkit-user-select: text;
user-select: text;
}
[data-hidden="true"] {
display: none;
pointer-events: none;
}

View file

@ -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)))

View file

@ -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

View file

@ -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))))))

View file

@ -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)) "")))

View file

@ -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))

View file

@ -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)

View file

@ -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

View file

@ -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]]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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))

View file

@ -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}]])]]))))

View file

@ -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))

View file

@ -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 "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"%\" width=\"%\" height=\"%\" fill=\"none\">% %</svg>"
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}])])]))

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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))))))