From fb9b023faeaf50cadc7079e9ed66964c44bf9d75 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 30 Nov 2021 16:23:40 +0100 Subject: [PATCH] :zap: Improve selection performance --- common/src/app/common/geom/shapes.cljc | 1 + common/src/app/common/geom/shapes/rect.cljc | 8 ++ .../app/main/ui/workspace/viewport/hooks.cljs | 2 +- .../main/ui/workspace/viewport/widgets.cljs | 1 + frontend/src/app/util/worker.cljs | 10 +-- frontend/src/app/worker/selection.cljs | 90 ++++++++++++------- 6 files changed, 76 insertions(+), 36 deletions(-) diff --git a/common/src/app/common/geom/shapes.cljc b/common/src/app/common/geom/shapes.cljc index aa6dccf81..82d5dd6c7 100644 --- a/common/src/app/common/geom/shapes.cljc +++ b/common/src/app/common/geom/shapes.cljc @@ -149,6 +149,7 @@ (d/export gpr/points->rect) (d/export gpr/center->rect) (d/export gpr/join-rects) +(d/export gpr/contains-selrect?) (d/export gtr/move) (d/export gtr/absolute-move) diff --git a/common/src/app/common/geom/shapes/rect.cljc b/common/src/app/common/geom/shapes/rect.cljc index 047781a70..e80dae7a9 100644 --- a/common/src/app/common/geom/shapes/rect.cljc +++ b/common/src/app/common/geom/shapes/rect.cljc @@ -121,3 +121,11 @@ (or (< px x2) (s= px x2)) (or (> py y1) (s= py y1)) (or (< py y2) (s= py y2))))) + +(defn contains-selrect? + "Check if a selrect sr2 is contained inside sr1" + [sr1 sr2] + (and (>= (:x1 sr2) (:x1 sr1)) + (<= (:x2 sr2) (:x2 sr1)) + (>= (:y1 sr2) (:y1 sr1)) + (<= (:y2 sr2) (:y2 sr1)))) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index b2e596e11..c1869680b 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -120,7 +120,7 @@ (->> move-stream ;; When transforming shapes we stop querying the worker (rx/filter #(not (some? (mf/ref-val transform-ref)))) - (rx/switch-map query-point)) + (rx/merge-map query-point)) (->> move-stream ;; When transforming shapes we stop querying the worker diff --git a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs index 9b8d30d58..e910e095f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/widgets.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/widgets.cljs @@ -90,6 +90,7 @@ "translate(" (* zoom x) ", " (* zoom y) ")"))) (mf/defc frame-title + {::mf/wrap [mf/memo]} [{:keys [frame modifiers selected? zoom on-frame-enter on-frame-leave on-frame-select]}] (let [{:keys [width x y]} (gsh/transform-shape frame) label-pos (gpt/point x (- y (/ 10 zoom))) diff --git a/frontend/src/app/util/worker.cljs b/frontend/src/app/util/worker.cljs index 497a050d5..0ffa78d54 100644 --- a/frontend/src/app/util/worker.cljs +++ b/frontend/src/app/util/worker.cljs @@ -35,6 +35,7 @@ (->> (:stream worker) (rx/filter #(= (:reply-to %) sender-id)) (take-messages) + (rx/filter (complement :dropped)) (rx/map handle-response))) (rx/empty))))) @@ -91,9 +92,8 @@ worker)) (defn- handle-response - [{:keys [payload error dropped]}] - (when-not dropped - (if-let [{:keys [data message]} error] - (throw (ex-info message data)) - payload))) + [{:keys [payload error]}] + (if-let [{:keys [data message]} error] + (throw (ex-info message data)) + payload)) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index dc05e1532..10f57f856 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -15,6 +15,8 @@ [clojure.set :as set] [okulary.core :as l])) +(def ^:const padding-percent 0.10) + (defonce state (l/atom {})) (defn index-shape @@ -37,55 +39,71 @@ :clip-parents clip-parents :parents parents))))) +(defn objects-bounds + "Calculates the bounds of the quadtree given a objects map." + [objects] + (-> objects + (dissoc uuid/zero) + vals + gsh/selection-rect)) + +(defn add-padding-bounds + "Adds a padding to the bounds defined as a percent in the constant `padding-percent`. + For a value of 0.1 will add a 20% width increase (2 x padding)" + [bounds] + (let [width-pad (* (:width bounds) padding-percent) + height-pad (* (:height bounds) padding-percent)] + (-> bounds + (update :x - width-pad) + (update :x1 - width-pad) + (update :x2 + width-pad) + (update :y1 - height-pad) + (update :y2 + height-pad) + (update :width + width-pad width-pad) + (update :height + height-pad height-pad)))) + (defn- create-index [objects] - (let [shapes (-> objects (dissoc uuid/zero) (vals)) + (let [shapes (-> objects (dissoc uuid/zero) vals) parents-index (cp/generate-child-all-parents-index objects) clip-parents-index (cp/create-clip-index objects parents-index) - bounds #js {:x (int -0.5e7) - :y (int -0.5e7) - :width (int 1e7) - :height (int 1e7)} + bounds (-> objects objects-bounds add-padding-bounds) index (reduce (index-shape objects parents-index clip-parents-index) - (qdt/create bounds) + (qdt/create (clj->js bounds)) shapes) z-index (cp/calculate-z-index objects)] - {:index index :z-index z-index})) + {:index index :z-index z-index :bounds bounds})) (defn- update-index [{index :index z-index :z-index :as data} old-objects new-objects] - (if (some? data) - (let [changes? (fn [id] - (not= (get old-objects id) - (get new-objects id))) + (let [changes? (fn [id] + (not= (get old-objects id) + (get new-objects id))) - changed-ids (into #{} - (comp (filter #(not= % uuid/zero)) - (filter changes?) - (mapcat #(into [%] (cp/get-children % new-objects)))) - (set/union (set (keys old-objects)) - (set (keys new-objects)))) + changed-ids (into #{} + (comp (filter #(not= % uuid/zero)) + (filter changes?) + (mapcat #(into [%] (cp/get-children % new-objects)))) + (set/union (set (keys old-objects)) + (set (keys new-objects)))) - shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) - parents-index (cp/generate-child-all-parents-index new-objects shapes) - clip-parents-index (cp/create-clip-index new-objects parents-index) + shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) + parents-index (cp/generate-child-all-parents-index new-objects shapes) + clip-parents-index (cp/create-clip-index new-objects parents-index) - new-index (qdt/remove-all index changed-ids) + new-index (qdt/remove-all index changed-ids) - index (reduce (index-shape new-objects parents-index clip-parents-index) - new-index - shapes) + index (reduce (index-shape new-objects parents-index clip-parents-index) + new-index + shapes) - z-index (cp/update-z-index z-index changed-ids old-objects new-objects)] + z-index (cp/update-z-index z-index changed-ids old-objects new-objects)] - {:index index :z-index z-index}) - - ;; If not previous data. We need to create from scratch - (create-index new-objects))) + (assoc data :index index :z-index z-index))) (defn- query-index [{index :index z-index :z-index} rect frame-id full-frame? include-frames? clip-children? reverse?] @@ -154,7 +172,19 @@ (defmethod impl/handler :selection/update-index [{:keys [page-id old-objects new-objects] :as message}] - (swap! state update page-id update-index old-objects new-objects) + (let [update-page-index + (fn [index] + (let [old-bounds (:bounds index) + new-bounds (objects-bounds new-objects)] + + ;; If the new bounds are contained within the old bounds we can + ;; update the index. + ;; Otherwise we need to re-create it + (if (and (some? index) + (gsh/contains-selrect? old-bounds new-bounds)) + (update-index index old-objects new-objects) + (create-index new-objects))))] + (swap! state update page-id update-page-index)) nil) (defmethod impl/handler :selection/query