diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 0bb7cc3bd3..93aea3d183 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -15,7 +15,6 @@ [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] @@ -114,7 +113,6 @@ (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.cljs b/frontend/src/app/main/data/workspace.cljs index 7d3cdf6136..5e89620436 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -385,9 +385,10 @@ ;; we only need to proceed when page-index is properly loaded (when-let [pindex (-> state :workspace-data :pages-index)] (if (contains? pindex page-id) - (rx/of (preload-data-uris page-id) - (dwth/watch-state-changes) - (dwl/watch-component-changes)) + (let [file-id (:current-file-id state)] + (rx/of (preload-data-uris page-id) + (dwth/watch-state-changes file-id page-id) + (dwl/watch-component-changes))) (let [page-id (dm/get-in state [:workspace-data :pages 0])] (rx/of (go-to-page page-id)))))))) diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 1d2412b308..28a7d713a8 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -13,7 +13,7 @@ [app.common.types.shape-tree :as ctst] [app.common.uuid :as uuid] [app.main.data.workspace.changes :as dch] - ;; [app.main.data.workspace.thumbnails :as dwt] + [app.main.data.workspace.thumbnails :as dwt] [app.main.features :as features] [app.main.repo :as rp] [app.main.store :as st] @@ -156,10 +156,9 @@ (->> (rp/cmd! :update-file params) (rx/mapcat (fn [lagged] (log/debug :hint "changes persisted" :lagged (count lagged)) - (let [ - ;; frame-updates - ;; (-> (group-by :page-id changes) - ;; (update-vals #(into #{} (mapcat :frames) %))) + (let [frame-updates + (-> (group-by :page-id changes) + (update-vals #(into #{} (mapcat :frames) %))) commits (->> @pending-commits @@ -167,10 +166,11 @@ (rx/concat (rx/merge - #_(->> (rx/from frame-updates) - (rx/mapcat (fn [[page-id frames]] - (->> frames (map #(vector page-id %))))) - (rx/map (fn [[_ frame-id]] (dwt/update-thumbnail frame-id)))) + (->> (rx/from frame-updates) + (rx/mapcat (fn [[page-id frames]] + (->> frames (map (fn [frame-id] [file-id page-id frame-id]))))) + (rx/map (fn [data] + (ptk/data-event ::dwt/update data)))) (->> (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 e5604c0a2e..d2a7650507 100644 --- a/frontend/src/app/main/data/workspace/thumbnails.cljs +++ b/frontend/src/app/main/data/workspace/thumbnails.cljs @@ -7,67 +7,92 @@ (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.common.logging :as l] + [app.common.pages.helpers :as cph] + [app.common.uuid :as uuid] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.notifications :as-alias wnt] + [app.main.data.workspace.state-helpers :as wsh] [app.main.rasterizer :as thr] - ;; [app.main.refs :as refs] + [app.main.refs :as refs] + [app.main.render :as render] [app.main.repo :as rp] - ;; [app.main.store :as st] [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] + [cuerdas.core :as str] [potok.core :as ptk])) -(log/set-level! :debug) +(l/set-level! :info) -(declare set-workspace-thumbnail) +(defn fmt-object-id + [file-id page-id frame-id] + (str/ffmt "%/%/%" file-id page-id frame-id)) (defn get-thumbnail - [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"))) + [state file-id page-id frame-id & {:keys [object-id]}] + + (let [object-id (or object-id (fmt-object-id file-id page-id frame-id)) + tp (tp/tpoint-ms) + objects (wsh/lookup-page-objects state page-id) + shape (get objects frame-id)] + + (->> (render/render-frame objects shape object-id) (rx/take 1) - (rx/map (fn [imposter] - {:data (unchecked-get imposter "data") - :styles (unchecked-get imposter "styles") - :width (unchecked-get imposter "width")})) + (rx/filter some?) (rx/mapcat thr/render) (rx/map (fn [blob] (wapi/create-uri blob))) - (rx/tap #(log/debug :hint "generated thumbnail" :elapsed (dm/str (tp) "ms")))))) + (rx/tap #(l/dbg :hint "thumbnail rendered" + :elapsed (dm/str (tp) "ms")))))) -(defn clear-thumbnail - [frame-id] - (ptk/reify ::clear-thumbnail - ptk/UpdateEvent - (update [_ state] - (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- clear-thumbnail + ([file-id page-id frame-id] + (clear-thumbnail file-id (fmt-object-id file-id page-id frame-id))) + ([file-id object-id] + (let [emit-rpc? (volatile! false)] + (ptk/reify ::clear-thumbnail + cljs.core/IDeref + (-deref [_] object-id) -(defn set-workspace-thumbnail - [id uri] + ptk/UpdateEvent + (update [_ state] + (let [uri (dm/get-in state [:workspace-thumbnails object-id])] + (if (some? uri) + (do + (l/dbg :hint "clear thumbnail" :object-id object-id) + (vreset! emit-rpc? true) + (tm/schedule-on-idle (partial wapi/revoke-uri uri)) + (update state :workspace-thumbnails dissoc object-id)) + + state))) + + ptk/WatchEvent + (watch [_ _ _] + (when ^boolean @emit-rpc? + (->> (rp/cmd! :delete-file-object-thumbnail {:file-id file-id :object-id object-id}) + (rx/catch rx/empty) + (rx/ignore)))))))) + +(defn- assoc-thumbnail + [object-id uri] (let [prev-uri* (volatile! nil)] - (ptk/reify ::set-workspace-thumbnail + (ptk/reify ::assoc-thumbnail ptk/UpdateEvent (update [_ state] - (let [object-id (dm/str id) - prev-uri (dm/get-in state [:workspace-thumbnails object-id])] + (let [prev-uri (dm/get-in state [:workspace-thumbnails object-id])] (some->> prev-uri (vreset! prev-uri*)) + (l/trc :hint "assoc thumbnail" :object-id object-id :uri uri) + (update state :workspace-thumbnails assoc object-id uri))) ptk/EffectEvent (effect [_ _ _] - (tm/schedule-on-idle #(some-> ^boolean @prev-uri* wapi/revoke-uri)))))) + (tm/schedule-on-idle + (fn [] + (when-let [uri (deref prev-uri*)] + (wapi/revoke-uri uri)))))))) (defn duplicate-thumbnail [old-id new-id] @@ -81,42 +106,45 @@ (defn update-thumbnail "Updates the thumbnail information for the given frame `id`" - ([id] - (ptk/reify ::update-thumbnail - ptk/WatchEvent - (watch [_ 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 id) - (rx/filter (fn [data] (and (some? data) (some? file-id)))) - (rx/merge-map - (fn [uri] - (rx/merge - (rx/of (set-workspace-thumbnail object-id uri)) + [file-id page-id frame-id] + (let [object-id (fmt-object-id file-id page-id frame-id)] + (ptk/reify ::update-thumbnail + cljs.core/IDeref + (-deref [_] object-id) - (->> (http/send! {:uri uri :response-type :blob :method :get}) - (rx/map :body) - (rx/mapcat (fn [blob] - ;; Send the data to backend - (let [params {:file-id file-id - :object-id object-id - :media blob}] - (rp/cmd! :create-file-object-thumbnail params)))) - (rx/catch rx/empty) - (rx/ignore))))) + ptk/WatchEvent + (watch [_ state stream] + (l/dbg :hint "update thumbnail" :object-id object-id) + ;; Send the update to the back-end + (->> (get-thumbnail state file-id page-id frame-id {:object-id object-id}) + (rx/mapcat (fn [uri] + (rx/merge + (rx/of (assoc-thumbnail object-id uri)) + (->> (http/send! {:uri uri :response-type :blob :method :get}) + (rx/map :body) + (rx/mapcat (fn [blob] + ;; Send the data to backend + (let [params {:file-id file-id + :object-id object-id + :media blob}] + (rp/cmd! :create-file-object-thumbnail params)))) + (rx/catch rx/empty) + (rx/ignore))))) + (rx/catch (fn [cause] + (.error js/console cause) + (rx/empty))) - (rx/catch #(do (.error js/console %) - (rx/empty)))))))))) + ;; We cancel all the stream if user starts editing while + ;; thumbnail is generating + (rx/take-until + (->> stream + (rx/filter (ptk/type? ::clear-thumbnail)) + (rx/filter #(= (deref %) object-id))))))))) -#_(defn- extract-frame-changes +(defn- extract-frame-changes "Process a changes set in a commit to extract the frames that are changing" - [[event [old-data new-data]]] + [page-id [event [old-data new-data]]] (let [changes (-> event deref :changes) extract-ids @@ -129,40 +157,42 @@ [])) get-frame-id - (fn [[page-id id]] - (let [old-objects (wsh/lookup-data-objects old-data page-id) - new-objects (wsh/lookup-data-objects new-data page-id) + (fn [[_ id]] + (let [old-objects (wsh/lookup-data-objects old-data page-id) + new-objects (wsh/lookup-data-objects new-data page-id) - new-shape (get new-objects id) - old-shape (get old-objects id) + new-shape (get new-objects id) + old-shape (get old-objects id) old-frame-id (if (cph/frame-shape? old-shape) id (:frame-id old-shape)) new-frame-id (if (cph/frame-shape? new-shape) id (:frame-id new-shape))] (cond-> #{} - (and old-frame-id (not= uuid/zero old-frame-id)) - (conj [page-id old-frame-id]) + (and (some? old-frame-id) (not= uuid/zero old-frame-id)) + (conj old-frame-id) - (and new-frame-id (not= uuid/zero new-frame-id)) - (conj [page-id new-frame-id]))))] + (and (some? new-frame-id) (not= uuid/zero new-frame-id)) + (conj new-frame-id))))] (into #{} (comp (mapcat extract-ids) + (filter (fn [[page-id']] (= page-id page-id'))) (mapcat get-frame-id)) changes))) (defn watch-state-changes "Watch the state for changes inside frames. If a change is detected will force a rendering of the frame data so the thumbnail can be updated." - [] + [file-id page-id] (ptk/reify ::watch-state-changes ptk/WatchEvent - (watch [_ _ _stream] - #_(let [ - stopper - (->> stream - (rx/filter #(or (= :app.main.data.workspace/finalize-page (ptk/type %)) - (= ::watch-state-changes (ptk/type %))))) + (watch [_ _ stream] + (let [stopper-s (rx/filter + (fn [event] + (as-> (ptk/type event) type + (or (= :app.main.data.workspace/finalize-page type) + (= ::watch-state-changes type)))) + stream) workspace-data-s (->> (rx/concat @@ -170,27 +200,60 @@ (rx/from-atom refs/workspace-data {:emit-current-value? true})) ;; We need to keep the old-objects so we can check the frame for the ;; deleted objects - (rx/buffer 2 1)) - - change-s - (->> stream - (rx/filter #(or (dch/commit-changes? %) - (= (ptk/type %) :app.main.data.workspace.notifications/handle-file-change))) - (rx/observe-on :async)) - - frame-changes-s - (->> change-s - (rx/with-latest-from workspace-data-s) - (rx/flat-map extract-frame-changes) + (rx/buffer 2 1) (rx/share)) - ] + + changes-s + (->> (rx/merge + ;; LOCAL CHANGES + (->> stream + (rx/filter dch/commit-changes?) + (rx/observe-on :async) + (rx/with-latest-from workspace-data-s) + (rx/flat-map (partial extract-frame-changes page-id)) + (rx/tap #(l/trc :hint "inconming change" :origin "local" :frame-id (dm/str %)))) + + ;; NOTIFICATIONS CHANGES + (->> stream + (rx/filter (ptk/type? ::wnt/handle-file-change)) + (rx/observe-on :async) + (rx/with-latest-from workspace-data-s) + (rx/flat-map (partial extract-frame-changes page-id)) + (rx/tap #(l/trc :hint "inconming change" :origin "notifications" :frame-id (dm/str %)))) + + ;; PERSISTENCE CHANGES + (->> stream + (rx/filter (ptk/type? ::update)) + (rx/map deref) + (rx/filter (fn [[file-id page-id]] + (and (= file-id file-id) + (= page-id page-id)))) + (rx/map (fn [[_ _ frame-id]] frame-id)) + (rx/tap #(l/trc :hint "inconming change" :origin "persistence" :frame-id (dm/str %))))) + + (rx/share)) + + ;; BUFFER NOTIFIER (window of 5s of inactivity) + notifier-s + (->> changes-s + (rx/debounce 5000) + (rx/tap #(l/trc :hint "buffer initialized")))] (->> (rx/merge - (->> frame-changes-s - (rx/filter (fn [[page-id _]] (not= page-id (:current-page-id @st/state)))) - (rx/map (fn [[_ frame-id]] (clear-thumbnail frame-id)))) + ;; Perform instant thumbnail cleaning of affected frames + ;; and interrupt any ongoing update-thumbnail process + ;; related to current frame-id + (->> changes-s + (rx/map (fn [frame-id] + (clear-thumbnail file-id page-id frame-id)))) + + ;; Generate thumbnails in batchs, once user becomes + ;; inactive for some instant + (->> changes-s + (rx/buffer-until notifier-s) + (rx/mapcat #(into #{} %)) + (rx/map (fn [frame-id] + (update-thumbnail file-id page-id frame-id))))) + + (rx/take-until stopper-s)))))) - (->> frame-changes-s - (rx/filter (fn [[page-id _]] (= page-id (:current-page-id @st/state)))) - (rx/map (fn [[_ frame-id]] (update-thumbnail frame-id))))) - (rx/take-until stopper)))))) diff --git a/frontend/src/app/main/imposters.cljs b/frontend/src/app/main/imposters.cljs deleted file mode 100644 index fd7958cf49..0000000000 --- a/frontend/src/app/main/imposters.cljs +++ /dev/null @@ -1,77 +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.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/refs.cljs b/frontend/src/app/main/refs.cljs index fab0cbcc9f..1d5d03f591 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -475,15 +475,12 @@ (dm/get-in state [:viewer-local :zoom-type])) st/state)) -(def thumbnail-data - (l/derived #(get % :workspace-thumbnails {}) st/state)) - -(defn thumbnail-frame-data - [frame-id] +(defn workspace-thumbnail-by-id + [object-id] (l/derived - (fn [thumbnails] - (get thumbnails (dm/str frame-id))) - thumbnail-data)) + (fn [state] + (dm/get-in state [:workspace-thumbnails object-id])) + st/state)) (def workspace-text-modifier (l/derived :workspace-text-modifier st/state)) diff --git a/frontend/src/app/main/render.cljs b/frontend/src/app/main/render.cljs index f6facae45c..5de3d999f1 100644 --- a/frontend/src/app/main/render.cljs +++ b/frontend/src/app/main/render.cljs @@ -14,11 +14,13 @@ (:require ["react-dom/server" :as rds] [app.common.colors :as clr] + [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] [app.common.geom.shapes :as gsh] [app.common.geom.shapes.bounds :as gsb] + [app.common.logging :as l] [app.common.math :as mth] [app.common.pages.helpers :as cph] [app.common.types.file :as ctf] @@ -43,6 +45,7 @@ [app.util.http :as http] [app.util.object :as obj] [app.util.strings :as ust] + [app.util.thumbnails :as th] [app.util.timers :as ts] [beicon.core :as rx] [clojure.set :as set] @@ -229,17 +232,11 @@ [:& 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?} +(mf/defc frame-imposter + {::mf/wrap-props false} + [{:keys [objects frame vbox width height]}] + (let [shape-wrapper (shape-wrapper-factory objects)] + [:& (mf/provider muc/render-thumbnails) {:value false} [:svg {:view-box vbox :width (ust/format-precision width viewbox-decimal-precision) :height (ust/format-precision height viewbox-decimal-precision) @@ -538,3 +535,45 @@ #js {:data data :render-embed? true :include-metadata? true :source (name source)})] (rds/renderToStaticMarkup elem)))))))) + +(defn render-frame + [objects shape object-id] + (let [shape-id (dm/get-prop shape :id) + fonts (ff/shape->fonts shape objects) + + bounds (if (:show-content shape) + (let [ids (cph/get-children-ids objects shape-id) + children (sequence (keep (d/getf objects)) ids)] + (gsh/shapes->rect (cons shape 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 (str/ffmt "% % % %" x y width height) + + [fixed-width + fixed-height] (th/get-proportional-size width height) + + data (rds/renderToStaticMarkup + (mf/element frame-imposter + #js {:objects objects + :frame shape + :vbox viewbox + :width width + :height height}))] + + (->> (fonts/render-font-styles-cached fonts) + (rx/catch (fn [cause] + (l/err :hint "unexpected error on rendering imposter" + :cause cause) + (rx/empty))) + (rx/map (fn [styles] + {:id object-id + :data data + :viewbox viewbox + :width fixed-width + :height fixed-height + :styles styles}))))) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index f7d6efdeb0..62f667fe5e 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -87,7 +87,7 @@ cap-end (:stroke-cap-end stroke) color (if (some? gradient) - (str/ffmt "url(#stroke-color-gradient_%s_%s)" render-id index) + (str/ffmt "url(#stroke-color-gradient-%s-%s)" render-id index) (:stroke-color stroke)) opacity (when-not (some? gradient) @@ -198,7 +198,7 @@ alignment (:stroke-alignment stroke :center) width (:stroke-width stroke 0) - props #js {:id (dm/str "stroke-color-gradient_" render-id "_" index) + props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index) :gradient gradient :shape shape}] [:* @@ -391,7 +391,7 @@ props (if (or (some? (->> shape-shadow (remove :hidden) seq)) (not ^boolean (:hidden shape-blur))) - (obj/set! props "filter" (dm/fmt "url(#filter_%)" render-id)) + (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id)) props)) svg-attrs (attrs/get-svg-props shape render-id) @@ -494,11 +494,11 @@ (cond (and (some? shape-blur) (not ^boolean (:hidden shape-blur))) - (obj/set! props "filter" (dm/fmt "url(#filter_blur_%)" render-id)) + (obj/set! props "filter" (dm/fmt "url(#filter-blur-%)" render-id)) (and (empty? shape-fills) (some? (->> shape-shadow (remove :hidden) seq))) - (obj/set! props "filter" (dm/fmt "url(#filter_%)" render-id))))] + (obj/set! props "filter" (dm/fmt "url(#filter-%)" render-id))))] (when (d/not-empty? shape-strokes) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 5593a8b4d9..3b542483a6 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -15,14 +15,13 @@ [rumext.v2 :as mf])) (defn get-filter-id [] - (str "filter_" (uuid/next))) + (dm/str "filter-" (uuid/next))) (defn filter-str [filter-id shape] - (when (or (seq (->> (:shadow shape) (remove :hidden))) (and (:blur shape) (-> shape :blur :hidden not))) - (str/fmt "url(#$0)" [filter-id]))) + (str/ffmt "url(#%)" filter-id))) (mf/defc color-matrix [{:keys [color]}] @@ -31,7 +30,7 @@ [r g b] [(/ r 255) (/ g 255) (/ b 255)]] [:feColorMatrix {:type "matrix" - :values (str/fmt "0 0 0 0 $0 0 0 0 0 $1 0 0 0 0 $2 0 0 0 $3 0" [r g b a])}])) + :values (str/ffmt "0 0 0 0 % 0 0 0 0 % 0 0 0 0 % 0 0 0 % 0" r g b a)}])) (mf/defc drop-shadow-filter [{:keys [filter-in filter-id params]}] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 4d017e22ce..488a7dd05e 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -66,7 +66,7 @@ type (dm/get-prop shape :type) render-id (h/use-render-id) - filter-id (dm/str "filter_" render-id) + filter-id (dm/str "filter-" render-id) styles (-> (obj/create) (obj/set! "pointerEvents" pointer-events) (cond-> (and blend-mode (not= blend-mode :normal)) @@ -116,8 +116,8 @@ [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] - [:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter_shadow_%" render-id)}] - [:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter_blur_%" render-id)}] + [:& filters/filters {:shape shape-without-blur :filter-id (dm/fmt "filter-shadow-%" render-id)}] + [:& filters/filters {:shape shape-without-shadows :filter-id (dm/fmt "filter-blur-%" render-id)}] [:& fills/fills {:shape shape :render-id render-id}] [:& frame/frame-clip-def {:shape shape :render-id render-id}]] diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 20a14b31de..de500c98f1 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -74,6 +74,7 @@ (if ^boolean (cph/frame-shape? shape) [:& root-frame-wrapper {:shape shape + :objects objects :thumbnail? (not (contains? active-frames (dm/get-prop shape :id)))}] [:& shape-wrapper {:shape shape}])])]]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index efef1914ba..8498ecb5b6 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -6,20 +6,23 @@ (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.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.data.workspace.thumbnails :as dwt] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.context :as ctx] [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.workspace.shapes.common :refer [check-shape-props]] [app.main.ui.workspace.shapes.frame.dynamic-modifiers :as fdm] - ;; [app.util.debug :as dbg] + [app.util.debug :as dbg] + [app.util.timers :as tm] [rumext.v2 :as mf])) (defn frame-shape-factory @@ -46,6 +49,12 @@ [new-props old-props] (and (= (unchecked-get new-props "thumbnail?") (unchecked-get old-props "thumbnail?")) + + (identical? + (unchecked-get new-props "objects") + (unchecked-get old-props "objects")) + + ^boolean (check-shape-props new-props old-props))) (defn nested-frame-wrapper-factory @@ -69,54 +78,54 @@ (fdm/use-dynamic-modifiers objects (mf/ref-val node-ref) modifiers) [:& frame-shape {:shape shape :ref node-ref}])))) - (defn root-frame-wrapper-factory [shape-wrapper] - (let [frame-shape (frame-shape-factory shape-wrapper)] (mf/fnc frame-wrapper {::mf/wrap [#(mf/memo' % check-props)] ::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - ;; thumbnail? (unchecked-get props "thumbnail?") + (let [shape (unchecked-get props "shape") + thumbnail? (unchecked-get props "thumbnail?") + objects (unchecked-get props "objects") - ;; page-id (mf/use-ctx ctx/current-page-id) - frame-id (:id shape) + file-id (mf/use-ctx ctx/current-file-id) + page-id (mf/use-ctx ctx/current-page-id) + frame-id (dm/get-prop shape :id) - objects (wsh/lookup-page-objects @st/state) + container-ref (mf/use-ref nil) + content-ref (mf/use-ref nil) - container-ref (mf/use-ref nil) - content-ref (mf/use-ref nil) + ;; FIXME: apply specific rendering optimizations separating to a component + bounds (if (:show-content shape) + (let [ids (cph/get-children-ids objects frame-id) + children (sequence (keep (d/getf objects)) ids)] + (gsh/shapes->rect (cons shape children))) + (-> shape :points grc/points->rect)) - ;; all-children-ref (mf/with-memo [frame-id] - ;; (refs/all-children-objects frame-id)) - ;; all-children (mf/deref all-children-ref) + x (dm/get-prop bounds :x) + y (dm/get-prop bounds :y) + width (dm/get-prop bounds :width) + height (dm/get-prop bounds :height) - ;; bounds - ;; (if (:show-content shape) - ;; (gsh/shapes->rect (cons shape all-children)) - ;; (-> shape :points grc/points->rect)) + thumbnail-uri* (mf/with-memo [file-id page-id frame-id] + (let [object-id (dwt/fmt-object-id file-id page-id frame-id)] + (refs/workspace-thumbnail-by-id object-id))) + thumbnail-uri (mf/deref thumbnail-uri*) - ;; x (dm/get-prop bounds :x) - ;; y (dm/get-prop bounds :y) - ;; width (dm/get-prop bounds :width) - ;; height (dm/get-prop bounds :height) + modifiers-ref (mf/with-memo [frame-id] + (refs/workspace-modifiers-by-frame-id frame-id)) + modifiers (mf/deref modifiers-ref) - ;; thumbnail-uri* (mf/with-memo [frame-id] - ;; (refs/thumbnail-frame-data frame-id)) - ;; thumbnail-uri (mf/deref thumbnail-uri*) + hidden? (true? (:hidden shape))] - modifiers-ref (mf/with-memo [frame-id] - (refs/workspace-modifiers-by-frame-id frame-id)) - modifiers (mf/deref modifiers-ref) - - ;; debug? (dbg/enabled? :thumbnails) - ] - - #_(when-not (some? thumbnail-uri) - (st/emit! (dwt/update-thumbnail frame-id))) + ;; NOTE: we don't add deps because we want this to be executed + ;; once on mount with only referenced the initial data + (mf/with-effect [] + (when-not (some? thumbnail-uri) + (tm/schedule-on-idle + #(st/emit! (dwt/update-thumbnail file-id page-id frame-id))))) (fdm/use-dynamic-modifiers objects (mf/ref-val content-ref) modifiers) @@ -124,24 +133,20 @@ [:g.frame-container {:id (dm/str "frame-container-" frame-id) :key "frame-container" - ;; :ref on-container-ref - :opacity (when (:hidden shape) 0)} + :opacity (when ^boolean hidden? 0)} - ;; When thumbnail is enabled. - #_[:g.frame-imposter - ;; Render thumbnail image. + [:g.frame-imposter [:image.thumbnail-bitmap - {;; :ref on-imposter-ref - :x x + {:x x :y y :width width :height height :href thumbnail-uri - :style {:display (when-not thumbnail? "none")}}] + :style {:display (when-not ^boolean thumbnail? "none")}}] ;; Render border around image when we are debugging ;; thumbnails. - (when ^boolean debug? + (when (dbg/enabled? :thumbnails) [:rect {:x (+ x 2) :y (+ y 2) :width (- width 4) @@ -150,8 +155,10 @@ :stroke-width 2}])] ;; When thumbnail is disabled. - (when-not false #_thumbnail? + (when (or (not ^boolean thumbnail?) + (not ^boolean thumbnail-uri)) [: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/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 4b86156559..f90343331d 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -173,13 +173,13 @@ (fn [] (rx/dispose! subs))))) [:g.gradient-handlers [:defs - [:& gradient-line-drop-shadow-filter {:id "gradient_line_drop_shadow" :from-p from-p :to-p to-p :zoom zoom}] - [:& gradient-line-drop-shadow-filter {:id "gradient_width_line_drop_shadow" :from-p from-p :to-p width-p :zoom zoom}] - [:& gradient-square-drop-shadow-filter {:id "gradient_square_from_drop_shadow" :point from-p :zoom zoom}] - [:& gradient-square-drop-shadow-filter {:id "gradient_square_to_drop_shadow" :point to-p :zoom zoom}] - [:& gradient-width-handler-shadow-filter {:id "gradient_width_handler_drop_shadow" :point width-p :zoom zoom}]] + [:& gradient-line-drop-shadow-filter {:id "gradient-line-drop-shadow" :from-p from-p :to-p to-p :zoom zoom}] + [:& gradient-line-drop-shadow-filter {:id "gradient-width-line-drop-shadow" :from-p from-p :to-p width-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient-square-from-drop-shadow" :point from-p :zoom zoom}] + [:& gradient-square-drop-shadow-filter {:id "gradient-square-to-drop-shadow" :point to-p :zoom zoom}] + [:& gradient-width-handler-shadow-filter {:id "gradient-width-handler-drop-shadow" :point width-p :zoom zoom}]] - [:g {:filter "url(#gradient_line_drop_shadow)"} + [:g {:filter "url(#gradient-line-drop-shadow)"} [:line {:x1 (:x from-p) :y1 (:y from-p) :x2 (:x to-p) @@ -188,7 +188,7 @@ :stroke-width (/ gradient-line-stroke-width zoom)}]] (when width-p - [:g {:filter "url(#gradient_width_line_drop_shadow)"} + [:g {:filter "url(#gradient-width-line-drop-shadow)"} [:line {:x1 (:x from-p) :y1 (:y from-p) :x2 (:x width-p) @@ -197,7 +197,7 @@ :stroke-width (/ gradient-line-stroke-width zoom)}]]) (when width-p - [:g {:filter "url(#gradient_width_handler_drop_shadow)"} + [:g {:filter "url(#gradient-width-handler-drop-shadow)"} [:circle {:data-allow-click-modal "colorpicker" :cx (:x width-p) :cy (:y width-p) @@ -208,7 +208,7 @@ [:& gradient-color-handler {:selected (or (not editing) (= editing 0)) - :filter-id "gradient_square_from_drop_shadow" + :filter-id "gradient-square-from-drop-shadow" :zoom zoom :point from-p :color from-color @@ -219,7 +219,7 @@ [:& gradient-color-handler {:selected (= editing 1) - :filter-id "gradient_square_to_drop_shadow" + :filter-id "gradient-square-to-drop-shadow" :zoom zoom :point to-p :color to-color diff --git a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs index 499d13a82f..82f3d1025a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/interactions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/interactions.cljs @@ -240,9 +240,10 @@ dest-shape-id (:id dest-shape) + ;; FIXME: broken thumbnail-data-ref (mf/use-memo (mf/deps page-id dest-shape-id) - #(refs/thumbnail-frame-data dest-shape-id)) + #(refs/workspace-thumbnail-by-id 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/snap_distances.cljs b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs index f94a40e219..d379308393 100644 --- a/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/snap_distances.cljs @@ -132,7 +132,8 @@ (and (>= s1c1 s2c1) (<= s1c1 s2c2)) (and (>= s1c2 s2c1) (<= s1c2 s2c2))))) -(defn calculate-segments [coord selrect lt-shapes gt-shapes] +(defn calculate-segments + [coord selrect lt-shapes gt-shapes] (let [distance-to-selrect (fn [shape] (let [sr (:selrect shape)] @@ -158,10 +159,13 @@ ;; Left/Top shapes and right/bottom shapes (depends on `coord` parameter) ;; Gets the distance to the current selection - distances-xf (comp (map distance-to-selrect) (filter pos?)) + distances-xf (comp (filter some?) + (map distance-to-selrect) + (filter pos?)) + lt-distances (into #{} distances-xf lt-shapes) gt-distances (into #{} distances-xf gt-shapes) - distances (set/union lt-distances gt-distances) + distances (set/union lt-distances gt-distances) ;; We'll show the distances that match a distance from the selrect show-candidate? #(check-in-set % distances) @@ -202,6 +206,26 @@ segments-to-display)) +(defn- query-worker + [page-id coord [selrect selected frame]] + (let [lt-side (if (= coord :x) :left :top) + gt-side (if (= coord :x) :right :bottom) + + vbox (deref refs/vbox) + areas (gsh/get-areas + (or (grc/clip-rect (dm/get-prop frame :selrect) vbox) vbox) + selrect) + + query-side + (fn [side] + (let [rect (get areas side)] + (if (and (> (:width rect) 0) (> (:height rect) 0)) + (ams/select-shapes-area page-id (:id frame) selected @refs/workspace-page-objects rect) + (rx/of nil))))] + + (rx/combine-latest (query-side lt-side) + (query-side gt-side)))) + (mf/defc shape-distance {::mf/wrap-props false} [props] @@ -213,51 +237,46 @@ selected (unchecked-get props "selected") subject (mf/use-memo #(rx/subject)) - to-measure (mf/use-state []) - query-worker - (fn [[selrect selected frame]] - (let [lt-side (if (= coord :x) :left :top) - gt-side (if (= coord :x) :right :bottom) - vbox (deref refs/vbox) - areas (gsh/get-areas - (or (grc/clip-rect (dm/get-prop frame :selrect) vbox) vbox) - selrect) + lt-shapes* (mf/use-state nil) + lt-shapes (deref lt-shapes*) - query-side (fn [side] - (let [rect (get areas side)] - (if (and (> (:width rect) 0) (> (:height rect) 0)) - (ams/select-shapes-area page-id (:id frame) selected @refs/workspace-page-objects rect) - (rx/of nil))))] - (rx/combine-latest (query-side lt-side) - (query-side gt-side)))) + gt-shapes* (mf/use-state nil) + gt-shapes (deref gt-shapes*) - [lt-shapes gt-shapes] @to-measure + segments-to-display + (mf/with-memo [lt-shapes gt-shapes selrect] + (calculate-segments coord selrect lt-shapes gt-shapes))] - segments-to-display (mf/use-memo - (mf/deps @to-measure) - #(calculate-segments coord selrect lt-shapes gt-shapes))] - - (mf/use-effect - (fn [] - (let [sub (->> subject - (rx/throttle 100) - (rx/switch-map query-worker) - (rx/subs #(reset! to-measure %)))] - ;; On unmount dispose - #(rx/dispose! sub)))) + (mf/with-effect [page-id] + (let [sub (->> subject + (rx/throttle 100) + ;; NOTE: we don't put coord on deps because we + ;; know it is a static value and will not go to + ;; change + (rx/switch-map (partial query-worker page-id coord)) + (rx/subs (fn [[lt-shapes gt-shapes]] + (reset! lt-shapes* lt-shapes) + (reset! gt-shapes* gt-shapes))))] + ;; On unmount dispose + #(rx/dispose! sub))) (mf/use-effect (mf/deps selrect) #(rx/push! subject [selrect selected frame])) (for [[sr1 sr2] segments-to-display] - [:& shape-distance-segment {:key (str/join "-" [(:x sr1) (:y sr1) (:x sr2) (:y sr2)]) - :sr1 sr1 - :sr2 sr2 - :coord coord - :zoom zoom}]))) + [:& shape-distance-segment + {:key (str/ffmt "%-%-%-%" + (dm/get-prop sr1 :x) + (dm/get-prop sr1 :y) + (dm/get-prop sr2 :x) + (dm/get-prop sr2 :y)) + :sr1 sr1 + :sr2 sr2 + :coord coord + :zoom zoom}]))) (mf/defc snap-distances {::mf/wrap-props false} diff --git a/frontend/src/app/util/imposters.cljs b/frontend/src/app/util/imposters.cljs deleted file mode 100644 index e4ebd22c13..0000000000 --- a/frontend/src/app/util/imposters.cljs +++ /dev/null @@ -1,15 +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.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))