From d5ab0eea1a0c528446df180d39fb6cda4b3a7269 Mon Sep 17 00:00:00 2001 From: "alonso.torres" <alonso.torres@kaleidos.net> Date: Tue, 3 Jan 2023 16:57:01 +0100 Subject: [PATCH 1/5] :zap: Removed reflow in viewport --- .../src/app/main/ui/workspace/viewport.cljs | 16 ++--- .../main/ui/workspace/viewport/actions.cljs | 43 +++++------- .../app/main/ui/workspace/viewport/hooks.cljs | 20 ++++-- .../ui/workspace/viewport/scroll_bars.cljs | 14 ++-- .../app/main/ui/workspace/viewport/utils.cljs | 15 +---- .../ui/workspace/viewport/viewport_ref.cljs | 65 +++++++++++++++++++ 6 files changed, 111 insertions(+), 62 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index a44943058..81b2ecec9 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -40,6 +40,7 @@ [app.main.ui.workspace.viewport.snap-distances :as snap-distances] [app.main.ui.workspace.viewport.snap-points :as snap-points] [app.main.ui.workspace.viewport.utils :as utils] + [app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]] [app.main.ui.workspace.viewport.widgets :as widgets] [beicon.core :as rx] [debug :refer [debug?]] @@ -98,7 +99,8 @@ active-frames (mf/use-state #{}) ;; REFS - viewport-ref (mf/use-ref nil) + [viewport-ref + on-viewport-ref] (create-viewport-ref) ;; VARS disable-paste (mf/use-var false) @@ -140,15 +142,14 @@ on-double-click (actions/on-double-click hover hover-ids drawing-path? base-objects edition workspace-read-only?) on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) - on-drop (actions/on-drop file viewport-ref zoom) + on-drop (actions/on-drop file) on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? node-editing? - drawing-path? create-comment? space? viewport-ref zoom panning - workspace-read-only?) + drawing-path? create-comment? space? panning workspace-read-only?) on-mouse-up (actions/on-mouse-up disable-paste) on-pointer-down (actions/on-pointer-down) on-pointer-enter (actions/on-pointer-enter in-viewport?) on-pointer-leave (actions/on-pointer-leave in-viewport?) - on-pointer-move (actions/on-pointer-move viewport-ref zoom move-stream) + on-pointer-move (actions/on-pointer-move move-stream) on-pointer-up (actions/on-pointer-up) on-move-selected (actions/on-move-selected hover hover-ids selected space? workspace-read-only?) on-menu-selected (actions/on-menu-selected hover hover-ids selected workspace-read-only?) @@ -269,7 +270,7 @@ :preserveAspectRatio "xMidYMid meet" :key (str "viewport" page-id) :view-box (utils/format-viewbox vbox) - :ref viewport-ref + :ref on-viewport-ref :class (when drawing-tool "drawing") :style {:cursor @cursor} :fill "none" @@ -423,8 +424,7 @@ [:& scroll-bars/viewport-scrollbars {:objects base-objects :zoom zoom - :vbox vbox - :viewport-ref viewport-ref}] + :vbox vbox}] (when show-rules? [:& rules/rules diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 0bf2505fd..1a85587f2 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -19,7 +19,7 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.streams :as ms] - [app.main.ui.workspace.viewport.utils :as utils] + [app.main.ui.workspace.viewport.viewport-ref :as uwvv] [app.util.dom :as dom] [app.util.dom.dnd :as dnd] [app.util.dom.normalize-wheel :as nw] @@ -34,11 +34,11 @@ (defn on-mouse-down [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? - node-editing? drawing-path? create-comment? space? viewport-ref zoom panning + node-editing? drawing-path? create-comment? space? panning workspace-read-only?] (mf/use-callback (mf/deps id blocked hidden type selected edition drawing-tool text-editing? - node-editing? drawing-path? create-comment? @space? viewport-ref zoom + node-editing? drawing-path? create-comment? @space? panning workspace-read-only?) (fn [bevent] (when (or (dom/class? (dom/get-target bevent) "viewport-controls") @@ -61,8 +61,7 @@ (dom/prevent-default bevent) (if mod? (let [raw-pt (dom/get-client-position event) - viewport (mf/ref-val viewport-ref) - pt (utils/translate-point-to-viewport viewport zoom raw-pt)] + pt (uwvv/point->viewport raw-pt)] (st/emit! (dw/start-zooming pt))) (st/emit! (dw/start-panning)))) @@ -322,15 +321,12 @@ (= "TEXTAREA" (obj/get target "tagName")))] (st/emit! (ms/->KeyboardEvent :up key shift? ctrl? alt? meta? editing?)))))) -(defn on-mouse-move [viewport-ref zoom] +(defn on-mouse-move [] (let [last-position (mf/use-var nil)] (mf/use-callback - (mf/deps zoom) (fn [event] - (let [event (.getBrowserEvent ^js event) - raw-pt (dom/get-client-position event) - viewport (mf/ref-val viewport-ref) - pt (utils/translate-point-to-viewport viewport zoom raw-pt) + (let [raw-pt (dom/get-client-position event) + pt (uwvv/point->viewport raw-pt) ;; We calculate the delta because Safari's MouseEvent.movementX/Y drop ;; events @@ -350,30 +346,27 @@ (kbd/alt? event) (kbd/meta? event)))))))) -(defn on-pointer-move [viewport-ref zoom move-stream] +(defn on-pointer-move [move-stream] (mf/use-callback - (mf/deps zoom move-stream) + (mf/deps move-stream) (fn [event] (let [raw-pt (dom/get-client-position event) - viewport (mf/ref-val viewport-ref) - pt (utils/translate-point-to-viewport viewport zoom raw-pt)] + pt (uwvv/point->viewport raw-pt)] (rx/push! move-stream pt))))) -(defn on-mouse-wheel [viewport-ref zoom] +(defn on-mouse-wheel [zoom] (mf/use-callback (mf/deps zoom) (fn [event] - (let [viewport (mf/ref-val viewport-ref) - event (.getBrowserEvent ^js event) + (let [event (.getBrowserEvent ^js event) target (dom/get-target event) mod? (kbd/mod? event)] - (when (dom/is-child? viewport target) + (when (uwvv/inside-viewport? target) (dom/prevent-default event) (dom/stop-propagation event) - (let [pt (->> (dom/get-client-position event) - (utils/translate-point-to-viewport viewport zoom)) - + (let [raw-pt (dom/get-client-position event) + pt (uwvv/point->viewport raw-pt) norm-event ^js (nw/normalize-wheel event) ctrl? (kbd/ctrl? event) delta-y (.-pixelY norm-event) @@ -413,14 +406,12 @@ (dom/prevent-default e))))) (defn on-drop - [file viewport-ref zoom] + [file] (mf/use-fn - (mf/deps zoom) (fn [event] (dom/prevent-default event) (let [point (gpt/point (.-clientX event) (.-clientY event)) - viewport (mf/ref-val viewport-ref) - viewport-coord (utils/translate-point-to-viewport viewport zoom point) + viewport-coord (uwvv/point->viewport point) asset-id (-> (dnd/get-data event "text/asset-id") uuid/uuid) asset-name (dnd/get-data event "text/asset-name") asset-type (dnd/get-data event "text/asset-type")] diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index fb20a66fb..5076cb86f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -35,21 +35,29 @@ (defn setup-dom-events [viewport-ref zoom disable-paste in-viewport? workspace-read-only?] (let [on-key-down (actions/on-key-down) on-key-up (actions/on-key-up) - on-mouse-move (actions/on-mouse-move viewport-ref zoom) - on-mouse-wheel (actions/on-mouse-wheel viewport-ref zoom) + on-mouse-move (actions/on-mouse-move) + on-mouse-wheel (actions/on-mouse-wheel zoom) on-paste (actions/on-paste disable-paste in-viewport? workspace-read-only?)] + + ;; We use the DOM listener because the goog.closure one forces reflow to generate its internal + ;; structure. As we don't need currently nothing from BrowserEvent we optimize by using the basic event + (mf/use-layout-effect + (mf/deps on-mouse-move) + (fn [] + (let [node (mf/ref-val viewport-ref)] + (.addEventListener node "mousemove" on-mouse-move) + (fn [] + (.removeEventListener node "mousemove" on-mouse-move))))) + (mf/use-layout-effect (mf/deps on-key-down on-key-up on-mouse-move on-mouse-wheel on-paste workspace-read-only?) (fn [] - (let [node (mf/ref-val viewport-ref) - keys [(events/listen js/document EventType.KEYDOWN on-key-down) + (let [keys [(events/listen js/document EventType.KEYDOWN on-key-down) (events/listen js/document EventType.KEYUP on-key-up) - (events/listen node EventType.MOUSEMOVE on-mouse-move) ;; bind with passive=false to allow the event to be cancelled ;; https://stackoverflow.com/a/57582286/3219895 (events/listen js/window EventType.WHEEL on-mouse-wheel #js {:passive false}) (events/listen js/window EventType.PASTE on-paste)]] - (fn [] (doseq [key keys] (events/unlistenByKey key)))))))) diff --git a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs index 9e6a823b7..75166092a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/scroll_bars.cljs @@ -11,7 +11,7 @@ [app.common.pages.helpers :as cph] [app.main.data.workspace :as dw] [app.main.store :as st] - [app.main.ui.workspace.viewport.utils :as utils] + [app.main.ui.workspace.viewport.viewport-ref :refer [point->viewport]] [app.util.dom :as dom] [rumext.v2 :as mf])) @@ -27,7 +27,7 @@ (mf/defc viewport-scrollbars {::mf/wrap [mf/memo]} - [{:keys [objects viewport-ref zoom vbox]}] + [{:keys [objects zoom vbox]}] (let [v-scrolling? (mf/use-state false) h-scrolling? (mf/use-state false) @@ -126,10 +126,9 @@ on-mouse-move (fn [event axis] (when-let [_ (or @v-scrolling? @h-scrolling?)] - (let [viewport (mf/ref-val viewport-ref) - start-pt (mf/ref-val start-ref) + (let [start-pt (mf/ref-val start-ref) current-pt (dom/get-client-position event) - current-pt-viewport (utils/translate-point-to-viewport viewport zoom current-pt) + current-pt-viewport (point->viewport current-pt) y-delta (/ (* (mf/ref-val height-factor-ref) (- (:y current-pt) (:y start-pt))) zoom) x-delta (/ (* (mf/ref-val width-factor-ref) (- (:x current-pt) (:x start-pt))) zoom) new-v-scrollbar-y (-> current-pt-viewport @@ -150,9 +149,8 @@ on-mouse-down (fn [event axis] - (let [viewport (mf/ref-val viewport-ref) - start-pt (dom/get-client-position event) - viewport-point (utils/translate-point-to-viewport viewport zoom start-pt) + (let [start-pt (dom/get-client-position event) + viewport-point (point->viewport start-pt) new-h-scrollbar-x (:x viewport-point) new-v-scrollbar-y (:y viewport-point) v-scrollbar-y-padding (- v-scrollbar-y new-v-scrollbar-y) diff --git a/frontend/src/app/main/ui/workspace/viewport/utils.cljs b/frontend/src/app/main/ui/workspace/viewport/utils.cljs index 40b54f43c..59c411b4a 100644 --- a/frontend/src/app/main/ui/workspace/viewport/utils.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/utils.cljs @@ -6,13 +6,11 @@ (ns app.main.ui.workspace.viewport.utils (:require - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.geom.point :as gpt] [app.common.geom.shapes :as gsh] [app.main.ui.cursors :as cur] - [app.main.ui.formats :refer [format-number]] - [app.util.dom :as dom])) + [app.main.ui.formats :refer [format-number]])) (defn format-viewbox [vbox] (dm/str (format-number(:x vbox 0)) " " @@ -20,17 +18,6 @@ (format-number (:width vbox 0)) " " (format-number (:height vbox 0)))) -(defn translate-point-to-viewport [viewport zoom pt] - (let [vbox (.. ^js viewport -viewBox -baseVal) - brect (dom/get-bounding-rect viewport) - brect (gpt/point (d/parse-integer (:left brect)) - (d/parse-integer (:top brect))) - box (gpt/point (.-x vbox) (.-y vbox)) - zoom (gpt/point zoom)] - (-> (gpt/subtract pt brect) - (gpt/divide zoom) - (gpt/add box)))) - (defn get-cursor [cursor] (case cursor :hand cur/hand diff --git a/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs new file mode 100644 index 000000000..aa293f191 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/viewport/viewport_ref.cljs @@ -0,0 +1,65 @@ +;; 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.viewport.viewport-ref + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.common.geom.point :as gpt] + [app.main.store :as st] + [app.util.dom :as dom] + [rumext.v2 :as mf])) + +(defonce viewport-ref (atom nil)) +(defonce current-observer (atom nil)) +(defonce viewport-brect (atom nil)) + +(defn init-observer + [node on-change-bounds] + + (let [observer (js/ResizeObserver. on-change-bounds)] + (when (some? @current-observer) + (.disconnect @current-observer)) + + (reset! current-observer observer) + + (when (some? node) + (.observe observer node)))) + +(defn on-change-bounds + [_] + (when @viewport-ref + (let [brect (dom/get-bounding-rect @viewport-ref) + brect (gpt/point (d/parse-integer (:left brect)) + (d/parse-integer (:top brect)))] + (reset! viewport-brect brect)))) + +(defn create-viewport-ref + [] + + (let [ref (mf/use-ref nil)] + [ref + (mf/use-memo + #(fn [node] + (mf/set-ref-val! ref node) + (reset! viewport-ref node) + (init-observer node on-change-bounds)))])) + +(defn point->viewport + [pt] + (let [zoom (dm/get-in @st/state [:workspace-local :zoom])] + (when (some? @viewport-ref) + (let [vbox (.. ^js @viewport-ref -viewBox -baseVal) + brect @viewport-brect + box (gpt/point (.-x vbox) (.-y vbox)) + zoom (gpt/point zoom)] + (-> (gpt/subtract pt brect) + (gpt/divide zoom) + (gpt/add box)))))) + +(defn inside-viewport? + [target] + (dom/is-child? @viewport-ref target)) From 98698cf2db43f56302323b3e5bb43a9a3e960abd Mon Sep 17 00:00:00 2001 From: "alonso.torres" <alonso.torres@kaleidos.net> Date: Tue, 3 Jan 2023 16:58:10 +0100 Subject: [PATCH 2/5] :zap: Improved modifiers lens --- common/src/app/common/types/shape/layout.cljc | 12 +++--- .../app/main/data/workspace/drawing/box.cljs | 22 ++++++----- .../app/main/data/workspace/transforms.cljs | 1 + frontend/src/app/main/refs.cljs | 39 +++++++++++-------- .../src/app/main/ui/workspace/shapes.cljs | 4 +- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/common/src/app/common/types/shape/layout.cljc b/common/src/app/common/types/shape/layout.cljc index 5a9ffcf97..534174444 100644 --- a/common/src/app/common/types/shape/layout.cljc +++ b/common/src/app/common/types/shape/layout.cljc @@ -100,15 +100,13 @@ (and (= :frame (:type shape)) (= :flex (:layout shape))))) (defn layout-child? [objects shape] - (let [parent-id (:parent-id shape) - parent (get objects parent-id)] - (layout? parent))) + (let [frame-id (:frame-id shape) + frame (get objects frame-id)] + (layout? frame))) (defn layout-child-id? [objects id] - (let [shape (get objects id) - parent-id (:parent-id shape) - parent (get objects parent-id)] - (layout? parent))) + (let [shape (get objects id)] + (layout-child? objects shape))) (defn inside-layout? "Check if the shape is inside a layout" diff --git a/frontend/src/app/main/data/workspace/drawing/box.cljs b/frontend/src/app/main/data/workspace/drawing/box.cljs index 4c4c3346e..39971af7c 100644 --- a/frontend/src/app/main/data/workspace/drawing/box.cljs +++ b/frontend/src/app/main/data/workspace/drawing/box.cljs @@ -37,19 +37,21 @@ (assoc :x (- (:x point) (* sx (- dy dx))))))) (defn resize-shape [{:keys [x y width height] :as shape} initial point lock?] - (let [draw-rect (gsh/make-rect initial (cond-> point lock? (adjust-ratio initial))) - shape-rect (gsh/make-rect x y width height) + (if (and (some? x) (some? y) (some? width) (some? height)) + (let [draw-rect (gsh/make-rect initial (cond-> point lock? (adjust-ratio initial))) + shape-rect (gsh/make-rect x y width height) - scalev (gpt/point (/ (:width draw-rect) (:width shape-rect)) - (/ (:height draw-rect) (:height shape-rect))) + scalev (gpt/point (/ (:width draw-rect) (:width shape-rect)) + (/ (:height draw-rect) (:height shape-rect))) - movev (gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))] + movev (gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))] - (-> shape - (assoc :click-draw? false) - (gsh/transform-shape (-> (ctm/empty) - (ctm/resize scalev (gpt/point x y)) - (ctm/move movev)))))) + (-> shape + (assoc :click-draw? false) + (gsh/transform-shape (-> (ctm/empty) + (ctm/resize scalev (gpt/point x y)) + (ctm/move movev))))) + shape)) (defn update-drawing [state initial point lock?] (update-in state [:workspace-drawing :object] resize-shape initial point lock?)) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 848379901..2512826dc 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -219,6 +219,7 @@ (rx/concat (->> ms/mouse-position + (rx/filter some?) (rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt) (rx/map normalize-proportion-lock) (rx/switch-map (fn [[point _ _ :as current]] diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index 858f8fb57..51a0becaa 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -324,11 +324,7 @@ (l/derived :workspace-editor-state st/state)) (def workspace-modifiers - (l/derived :workspace-modifiers st/state)) - -(defn workspace-modifiers-by-id - [ids] - (l/derived #(select-keys % ids) workspace-modifiers)) + (l/derived :workspace-modifiers st/state =)) (def workspace-modifiers-with-objects (l/derived @@ -340,20 +336,29 @@ (and (= (:modifiers a) (:modifiers b)) (identical? (:objects a) (:objects b)))))) -(defn workspace-modifiers-by-frame-id - [frame-id] +(def workspace-frame-modifiers (l/derived (fn [{:keys [modifiers objects]}] - (let [keys (->> modifiers - (keys) - (filter (fn [id] - (let [shape (get objects id)] - (or (= frame-id id) - (and (= frame-id (:frame-id shape)) - (not (= :frame (:type shape)))))))))] - (select-keys modifiers keys))) - workspace-modifiers-with-objects - =)) + (->> modifiers + (reduce + (fn [result [id modifiers]] + (let [shape (get objects id) + frame-id (:frame-id shape)] + (cond + (cph/frame-shape? shape) + (assoc-in result [id id] modifiers) + + (some? frame-id) + (assoc-in result [frame-id id] modifiers) + + :else + result))) + {}))) + workspace-modifiers-with-objects)) + +(defn workspace-modifiers-by-frame-id + [frame-id] + (l/derived #(get % frame-id) workspace-frame-modifiers =)) (defn select-bool-children [id] (l/derived (partial wsh/select-bool-children id) st/state =)) diff --git a/frontend/src/app/main/ui/workspace/shapes.cljs b/frontend/src/app/main/ui/workspace/shapes.cljs index 0b92d475e..35148dc16 100644 --- a/frontend/src/app/main/ui/workspace/shapes.cljs +++ b/frontend/src/app/main/ui/workspace/shapes.cljs @@ -110,9 +110,7 @@ :circle [:> circle-wrapper opts] :svg-raw [:> svg-raw-wrapper opts] :bool [:> bool-wrapper opts] - - ;; Only used when drawing a new frame. - :frame [:> nested-frame-wrapper opts] + :frame [:> nested-frame-wrapper opts] nil)]))) From 837b52aea114c91ca399bf132e2197e12ff51076 Mon Sep 17 00:00:00 2001 From: "alonso.torres" <alonso.torres@kaleidos.net> Date: Tue, 3 Jan 2023 16:59:18 +0100 Subject: [PATCH 3/5] :zap: Improved performand for hug content in layout --- .../src/app/common/geom/shapes/modifiers.cljc | 110 ++++++++++++++---- 1 file changed, 86 insertions(+), 24 deletions(-) diff --git a/common/src/app/common/geom/shapes/modifiers.cljc b/common/src/app/common/geom/shapes/modifiers.cljc index b1761aac5..4b11007a3 100644 --- a/common/src/app/common/geom/shapes/modifiers.cljc +++ b/common/src/app/common/geom/shapes/modifiers.cljc @@ -28,6 +28,16 @@ ;; [(get-in objects [k :name]) v])) ;; modif-tree)))) +(defn children-sequence + "Given an id returns a sequence of its children" + [id objects] + + (->> (tree-seq + #(d/not-empty? (dm/get-in objects [% :shapes])) + #(dm/get-in objects [% :shapes]) + id) + (map #(get objects %)))) + (defn resolve-tree-sequence "Given the ids that have changed search for layout roots to recalculate" [ids objects] @@ -75,20 +85,12 @@ (cond-> result (not contains-parent?) - (conj root))))) - - (generate-tree ;; Generate a tree sequence from a given root id - [id] - (->> (tree-seq - #(d/not-empty? (dm/get-in objects [% :shapes])) - #(dm/get-in objects [% :shapes]) - id) - (map #(get objects %))))] + (conj root)))))] (let [roots (->> ids (reduce calculate-common-roots #{}))] (concat (when (contains? ids uuid/zero) [(get objects uuid/zero)]) - (mapcat generate-tree roots))))) + (mapcat #(children-sequence % objects) roots))))) (defn- set-children-modifiers "Propagates the modifiers from a parent too its children applying constraints if necesary" @@ -296,28 +298,88 @@ result (assoc result (:id shape) new-bounds)] (recur result (rest shapes))))))) +(defn reflow-layout + [objects old-modif-tree bounds ignore-constraints id] + + (let [tree-seq (children-sequence id objects) + + [modif-tree _] + (reduce + #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] + tree-seq) + + bounds (transform-bounds bounds objects modif-tree tree-seq) + + modif-tree (merge-modif-tree old-modif-tree modif-tree)] + [modif-tree bounds])) + (defn sizing-auto-modifiers "Recalculates the layouts to adjust the sizing: auto new sizes" [modif-tree sizing-auto-layouts objects bounds ignore-constraints] - (loop [modif-tree modif-tree - bounds bounds - sizing-auto-layouts (reverse sizing-auto-layouts)] - (if-let [current (first sizing-auto-layouts)] - (let [parent-base (get objects current) + (let [;; Step-1 resize the auto-width/height. Reflow the parents if they are also auto-width/height + [modif-tree bounds to-reflow] + (loop [modif-tree modif-tree + bounds bounds + sizing-auto-layouts (reverse sizing-auto-layouts) + to-reflow #{}] + (if-let [current (first sizing-auto-layouts)] + (let [parent-base (get objects current) - resize-modif-tree - {current {:modifiers (calc-auto-modifiers objects bounds parent-base)}} + [modif-tree bounds] + (if (contains? to-reflow current) + (reflow-layout objects modif-tree bounds ignore-constraints current) + [modif-tree bounds]) - tree-seq (resolve-tree-sequence #{current} objects) + auto-resize-modifiers + (calc-auto-modifiers objects bounds parent-base) - [resize-modif-tree _] - (reduce #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}] tree-seq) + to-reflow + (cond-> to-reflow + (contains? to-reflow current) + (disj current))] - bounds (transform-bounds bounds objects resize-modif-tree tree-seq) + (if (ctm/empty? auto-resize-modifiers) + (recur modif-tree + bounds + (rest sizing-auto-layouts) + to-reflow) - modif-tree (merge-modif-tree modif-tree resize-modif-tree)] - (recur modif-tree bounds (rest sizing-auto-layouts))) - modif-tree))) + (let [resize-modif-tree {current {:modifiers auto-resize-modifiers}} + + tree-seq (children-sequence current objects) + + [resize-modif-tree _] + (reduce + #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [resize-modif-tree #{}] + tree-seq) + + bounds (transform-bounds bounds objects resize-modif-tree tree-seq) + + modif-tree (merge-modif-tree modif-tree resize-modif-tree) + + to-reflow + (cond-> to-reflow + (and (ctl/layout-child-id? objects current) + (not= uuid/zero (:frame-id parent-base))) + (conj (:frame-id parent-base)))] + (recur modif-tree + bounds + (rest sizing-auto-layouts) + to-reflow)))) + [modif-tree bounds to-reflow])) + + ;; Step-2: After resizing we still need to reflow the layout parents that are not auto-width/height + + tree-seq (resolve-tree-sequence to-reflow objects) + + [reflow-modif-tree _] + (reduce + #(propagate-modifiers-layout objects bounds ignore-constraints %1 %2) [{} #{}] + tree-seq) + + result (merge-modif-tree modif-tree reflow-modif-tree)] + + result)) (defn set-objects-modifiers ([modif-tree objects ignore-constraints snap-pixel?] From 84e9f6921351323c8a15533493b9c47f633ecd45 Mon Sep 17 00:00:00 2001 From: "alonso.torres" <alonso.torres@kaleidos.net> Date: Wed, 4 Jan 2023 16:12:00 +0100 Subject: [PATCH 4/5] :zap: Improved text rendering performance --- .../src/app/main/data/workspace/texts.cljs | 12 +- .../app/main/data/workspace/transforms.cljs | 14 +- frontend/src/app/main/streams.cljs | 10 ++ .../shapes/text/viewport_texts_html.cljs | 127 ++++++++++-------- .../workspace/sidebar/options/menus/blur.cljs | 38 ++++-- .../src/app/main/ui/workspace/viewport.cljs | 29 +++- frontend/src/app/util/text_svg_position.cljs | 3 +- 7 files changed, 148 insertions(+), 85 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 1d4c0098f..3355bfc3f 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -415,10 +415,14 @@ [id] (ptk/reify ::clean-text-modifier ptk/WatchEvent - (watch [_ _ _] - (->> (rx/of #(update % :workspace-text-modifier dissoc id)) - ;; We delay a bit the change so there is no weird transition to the user - (rx/delay 50))))) + (watch [_ state _] + (let [current-value (dm/get-in state [:workspace-text-modifier id])] + ;; We only dissocc the value when hasn't change after a time + (->> (rx/of (fn [state] + (cond-> state + (identical? (dm/get-in state [:workspace-text-modifier id]) current-value) + (update :workspace-text-modifier dissoc id)))) + (rx/delay 100)))))) (defn remove-text-modifier [id] diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 2512826dc..b61f63749 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -601,9 +601,17 @@ move-events (->> stream (rx/filter (ptk/type? ::nudge-selected-shapes)) (rx/filter #(= direction (deref %)))) - stopper (->> move-events - (rx/debounce 100) - (rx/take 1)) + + stopper + (->> move-events + ;; We stop when there's been 1s without movement or after 250ms after a key-up + (rx/switch-map #(rx/merge + (rx/timer 1000) + (->> stream + (rx/filter ms/key-up?) + (rx/delay 250)))) + (rx/take 1)) + scale (if shift? (gpt/point (or (:big nudge) 10)) (gpt/point (or (:small nudge) 1))) mov-vec (gpt/multiply (get-displacement direction) scale)] diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index 1a4cb1069..a586077a9 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -21,6 +21,16 @@ [v] (instance? KeyboardEvent v)) +(defn key-up? + [v] + (and (keyboard-event? v) + (= :up (:type v)))) + +(defn key-down? + [v] + (and (keyboard-event? v) + (= :down (:type v)))) + (defrecord MouseEvent [type ctrl shift alt meta]) (defn mouse-event? diff --git a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs index 7658de673..bfd704d7d 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/viewport_texts_html.cljs @@ -30,29 +30,16 @@ [promesa.core :as p] [rumext.v2 :as mf])) -(defn strip-position-data [shape] - (-> shape - (cond-> (some? (meta (:position-data shape))) - (with-meta (meta (:position-data shape)))) - (dissoc :position-data))) - -(defn fix-position [shape modifier] - (let [shape' (gsh/transform-shape shape modifier) +(defn fix-position [shape] + (let [modifiers (:modifiers shape) + shape' (gsh/transform-shape shape modifiers) ;; We need to remove the movement because the dynamic modifiers will have move it deltav (gpt/to-vec (gpt/point (:selrect shape')) (gpt/point (:selrect shape)))] (-> shape - (gsh/transform-shape (ctm/move modifier deltav)) - (mdwm/update-grow-type shape)))) - -(defn process-shape [modifiers {:keys [id] :as shape}] - (let [modifier (dm/get-in modifiers [id :modifiers])] - (-> shape - (cond-> (and (some? modifier) (not (ctm/only-move? modifier))) - (fix-position modifier)) - (cond-> (nil? (:position-data shape)) - (assoc :migrate true)) - strip-position-data))) + (gsh/transform-shape (ctm/move modifiers deltav)) + (mdwm/update-grow-type shape) + (dissoc :modifiers)))) (defn- update-with-editor-state "Updates the shape with the current state in the editor" @@ -87,6 +74,7 @@ (not (mth/almost-zero? height)) (not migrate)) (st/emit! (dwt/resize-text id width height))))) + (st/emit! (dwt/clean-text-modifier id)))) (defn- update-text-modifier @@ -136,8 +124,8 @@ (or (identical? shape other) (and ;; Check if both shapes are equivalent removing their geometry data - (= (dissoc shape :migrate :points :selrect :height :width :x :y) - (dissoc other :migrate :points :selrect :height :width :x :y)) + (= (dissoc shape :migrate :points :selrect :height :width :x :y :position-data :modifiers) + (dissoc other :migrate :points :selrect :height :width :x :y :position-data :modifiers)) ;; Check if the position and size is close. If any of these changes the shape has changed ;; and if not there is no geometry relevant change @@ -146,55 +134,69 @@ (mth/close? (:width shape) (:width other)) (mth/close? (:height shape) (:height other))))) -(mf/defc viewport-texts-wrapper - {::mf/wrap-props false - ::mf/wrap [mf/memo #(mf/deferred % ts/idle-then-raf)]} +(mf/defc text-changes-renderer + {::mf/wrap-props false} [props] (let [text-shapes (obj/get props "text-shapes") - modifiers (obj/get props "modifiers") - prev-modifiers (hooks/use-previous modifiers) prev-text-shapes (hooks/use-previous text-shapes) - ;; A change in position-data won't be a "real" change text-change? (fn [id] (let [new-shape (get text-shapes id) old-shape (get prev-text-shapes id) - old-modifiers (ctm/select-geometry (get prev-modifiers id)) - new-modifiers (ctm/select-geometry (get modifiers id)) - remote? (some? (-> new-shape meta :session-id))] - (or (and (not remote?) + + (or (and (not remote?) ;; changes caused by a remote peer are not re-calculated (not (text-properties-equal? old-shape new-shape))) - - (and (not= new-modifiers old-modifiers) - (or (ctm/empty? new-modifiers) - (ctm/empty? old-modifiers))) - - (and (not= new-modifiers old-modifiers) - (or (not (ctm/only-move? new-modifiers)) - (not (ctm/only-move? old-modifiers)))) - ;; When the position data is nil we force to recalculate - (:migrate new-shape)))) + (nil? (:position-data new-shape))))) changed-texts (mf/use-memo - (mf/deps text-shapes modifiers) + (mf/deps text-shapes) #(->> (keys text-shapes) (filter text-change?) (map (d/getf text-shapes)))) - handle-update-modifier (mf/use-callback update-text-modifier) handle-update-shape (mf/use-callback update-text-shape)] - [:* + [:.text-changes-renderer (for [{:keys [id] :as shape} changed-texts] - [:& text-container {:shape shape - :on-update (if (some? (get modifiers (:id shape))) - handle-update-modifier - handle-update-shape) - :key (str (dm/str "text-container-" id))}])])) + [:& text-container {:key (str (dm/str "text-container-" id)) + :shape shape + :on-update handle-update-shape}])])) + +(mf/defc text-modifiers-renderer + {::mf/wrap-props false} + [props] + (let [text-shapes (-> (obj/get props "text-shapes") + (update-vals fix-position)) + + prev-text-shapes (hooks/use-previous text-shapes) + + text-change? + (fn [id] + (let [new-shape (get text-shapes id) + old-shape (get prev-text-shapes id)] + (and + (some? new-shape) + (some? old-shape) + (not (text-properties-equal? old-shape new-shape))))) + + changed-texts + (mf/use-memo + (mf/deps text-shapes) + #(->> (keys text-shapes) + (filter text-change?) + (map (d/getf text-shapes)))) + + handle-update-shape (mf/use-callback update-text-modifier)] + + [:.text-changes-renderer + (for [{:keys [id] :as shape} changed-texts] + [:& text-container {:key (str (dm/str "text-container-" id)) + :shape shape + :on-update handle-update-shape}])])) (mf/defc viewport-text-editing {::mf/wrap-props false} @@ -256,21 +258,30 @@ text-shapes (mf/use-memo (mf/deps objects) - #(into {} (filter (comp cph/text-shape? second)) objects)) + (fn [] + (into {} (filter (comp cph/text-shape? second)) objects))) text-shapes - (mf/use-memo - (mf/deps text-shapes modifiers) - #(update-vals text-shapes (partial process-shape modifiers))) + (hooks/use-equal-memo text-shapes) editing-shape (get text-shapes edition) - ;; This memo is necessary so the viewport-text-wrapper memoize its props correctly - text-shapes-wrapper + text-shapes-changes (mf/use-memo (mf/deps text-shapes edition) (fn [] - (dissoc text-shapes edition)))] + (-> text-shapes + (dissoc edition)))) + + text-shapes-modifiers + (mf/use-memo + (mf/deps modifiers text-shapes) + (fn [] + (into {} + (keep (fn [[id modifiers]] + (when-let [shape (get text-shapes id)] + (vector id (merge shape modifiers))))) + modifiers)))] ;; We only need the effect to run on "mount" because the next fonts will be changed when the texts are ;; edited @@ -284,5 +295,5 @@ (when editing-shape [:& viewport-text-editing {:shape editing-shape}]) - [:& viewport-texts-wrapper {:text-shapes text-shapes-wrapper - :modifiers modifiers}]])) + [:& text-modifiers-renderer {:text-shapes text-shapes-modifiers}] + [:& text-changes-renderer {:text-shapes text-shapes-changes}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index 64a25b603..8b6eb7895 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -28,28 +28,40 @@ has-value? (not (nil? blur)) multiple? (= blur :multiple) - change! (fn [update-fn] (st/emit! (dch/update-shapes ids update-fn))) + change! + (mf/use-callback + (mf/deps ids) + (fn [update-fn] + (st/emit! (dch/update-shapes ids update-fn)))) handle-add - (fn [] - (change! #(assoc % :blur (create-blur)))) + (mf/use-callback + (mf/deps change!) + (fn [] + (change! #(assoc % :blur (create-blur))))) handle-delete - (fn [] - (change! #(dissoc % :blur))) + (mf/use-callback + (mf/deps change!) + (fn [] + (change! #(dissoc % :blur)))) handle-change - (fn [value] - (change! #(cond-> % - (not (contains? % :blur)) - (assoc :blur (create-blur)) + (mf/use-callback + (mf/deps change!) + (fn [value] + (change! #(cond-> % + (not (contains? % :blur)) + (assoc :blur (create-blur)) - :always - (assoc-in [:blur :value] value)))) + :always + (assoc-in [:blur :value] value))))) handle-toggle-visibility - (fn [] - (change! #(update-in % [:blur :hidden] not)))] + (mf/use-callback + (mf/deps change!) + (fn [] + (change! #(update-in % [:blur :hidden] not))))] [:div.element-set [:div.element-set-title diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 81b2ecec9..da5d13c89 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -12,6 +12,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages.helpers :as cph] [app.common.types.shape.layout :as ctl] + [app.main.data.workspace.modifiers :as dwm] [app.main.refs :as refs] [app.main.ui.context :as ctx] [app.main.ui.hooks :as ui-hooks] @@ -48,6 +49,20 @@ ;; --- Viewport +(defn apply-modifiers-to-selected + [selected objects text-modifiers modifiers] + (into [] + (comp + (keep (d/getf objects)) + (map (fn [{:keys [id] :as shape}] + (cond-> shape + (and (cph/text-shape? shape) (contains? text-modifiers id)) + (dwm/apply-text-modifier (get text-modifiers id)) + + (contains? modifiers id) + (gsh/transform-shape (dm/get-in modifiers [id :modifiers])))))) + selected)) + (mf/defc viewport [{:keys [wlocal wglobal selected layout file] :as props}] (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check @@ -80,6 +95,7 @@ base-objects (-> objects (ui-hooks/with-focus-objects focus)) modifiers (mf/deref refs/workspace-modifiers) + text-modifiers (mf/deref refs/workspace-text-modifier) objects-modified (mf/with-memo [base-objects modifiers] (gsh/apply-objects-modifiers base-objects modifiers selected)) @@ -120,7 +136,8 @@ drawing-tool (:tool drawing) drawing-obj (:object drawing) - selected-shapes (into [] (keep (d/getf objects-modified)) selected) + selected-shapes (apply-modifiers-to-selected selected base-objects text-modifiers modifiers) + selected-frames (into #{} (map :frame-id) selected-shapes) ;; Only when we have all the selected shapes in one frame @@ -303,7 +320,7 @@ outlined-frame (get objects outlined-frame-id)] [:* [:& outline/shape-outlines - {:objects objects-modified + {:objects base-objects :hover #{outlined-frame-id} :zoom zoom :modifiers modifiers}] @@ -443,25 +460,25 @@ ;; DEBUG LAYOUT DROP-ZONES (when (debug? :layout-drop-zones) [:& wvd/debug-drop-zones {:selected-shapes selected-shapes - :objects objects-modified + :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) (when (debug? :layout-content-bounds) [:& wvd/debug-content-bounds {:selected-shapes selected-shapes - :objects objects-modified + :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) (when (debug? :layout-lines) [:& wvd/debug-layout-lines {:selected-shapes selected-shapes - :objects objects-modified + :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) (when (debug? :parent-bounds) [:& wvd/debug-parent-bounds {:selected-shapes selected-shapes - :objects objects-modified + :objects base-objects :hover-top-frame-id @hover-top-frame-id :zoom zoom}]) diff --git a/frontend/src/app/util/text_svg_position.cljs b/frontend/src/app/util/text_svg_position.cljs index ec0f9e003..a99a57c6c 100644 --- a/frontend/src/app/util/text_svg_position.cljs +++ b/frontend/src/app/util/text_svg_position.cljs @@ -63,7 +63,8 @@ [shape-id] (when (some? shape-id) - (let [text-nodes (dom/query-all (dm/str "#html-text-node-" shape-id " .text-node")) + (let [text-nodes (-> (dom/query (dm/fmt "#html-text-node-%" shape-id)) + (dom/query-all ".text-node")) load-fonts (->> text-nodes (map resolve-font)) process-text-node From 10439934d496cae64f727b2cadf2580645e4d9a6 Mon Sep 17 00:00:00 2001 From: "alonso.torres" <alonso.torres@kaleidos.net> Date: Wed, 4 Jan 2023 16:18:12 +0100 Subject: [PATCH 5/5] :zap: Use the function `hypot` for distances --- common/src/app/common/geom/point.cljc | 6 ++---- common/src/app/common/geom/shapes/path.cljc | 2 +- common/src/app/common/math.cljc | 9 ++++++++- frontend/src/app/util/svg.cljs | 3 +-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/common/src/app/common/geom/point.cljc b/common/src/app/common/geom/point.cljc index bfc55dc6d..5dd41501e 100644 --- a/common/src/app/common/geom/point.cljc +++ b/common/src/app/common/geom/point.cljc @@ -170,8 +170,7 @@ (dm/get-prop p2 :x)) dy (- (dm/get-prop p1 :y) (dm/get-prop p2 :y))] - (mth/sqrt (+ (mth/pow dx 2) - (mth/pow dy 2))))) + (mth/hypot dx dy))) (defn distance-vector "Calculate the distance, separated x and y." @@ -191,8 +190,7 @@ (assert (point? pt) "point instance expected") (let [x (dm/get-prop pt :x) y (dm/get-prop pt :y)] - (mth/sqrt (+ (mth/pow x 2) - (mth/pow y 2))))) + (mth/hypot x y))) (defn angle "Returns the smaller angle between two vectors. diff --git a/common/src/app/common/geom/shapes/path.cljc b/common/src/app/common/geom/shapes/path.cljc index 9178f2774..830e98e76 100644 --- a/common/src/app/common/geom/shapes/path.cljc +++ b/common/src/app/common/geom/shapes/path.cljc @@ -117,7 +117,7 @@ [x y] (->> coords (mapv solve-derivative)) ;; normalize value - d (mth/sqrt (+ (* x x) (* y y)))] + d (mth/hypot x y)] (if (mth/almost-zero? d) (gpt/point 0 0) diff --git a/common/src/app/common/math.cljc b/common/src/app/common/math.cljc index eee928bd9..03525d4d3 100644 --- a/common/src/app/common/math.cljc +++ b/common/src/app/common/math.cljc @@ -139,12 +139,18 @@ #?(:cljs (math/toDegrees radians) :clj (Math/toDegrees radians))) +(defn hypot + "Square root of the squares addition" + [a b] + #?(:cljs (js/Math.hypot a b) + :clj (Math/hypot a b))) + (defn distance "Calculate the distance between two points." [[x1 y1] [x2 y2]] (let [dx (- x1 x2) dy (- y1 y2)] - (-> (sqrt (+ (pow dx 2) (pow dy 2))) + (-> (hypot dx dy) (precision 2)))) (defn log10 @@ -182,3 +188,4 @@ "Get the sign (+1 / -1) for the number" [n] (if (neg? n) -1 1)) + diff --git a/frontend/src/app/util/svg.cljs b/frontend/src/app/util/svg.cljs index 8056ddd33..04b23ebf0 100644 --- a/frontend/src/app/util/svg.cljs +++ b/frontend/src/app/util/svg.cljs @@ -890,8 +890,7 @@ (defn calculate-ratio ;; sqrt((actual-width)**2 + (actual-height)**2)/sqrt(2). [width height] - (/ (mth/sqrt (+ (mth/pow width 2) - (mth/pow height 2))) + (/ (mth/hypot width height) (mth/sqrt 2))) (defn fix-percents