From 64e7cad2927b4bd0ec498029799f4b4dac6f2764 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 20 Jan 2022 22:20:32 +0100 Subject: [PATCH] :recycle: Redone the snap calculation and added guides --- common/src/app/common/file_builder.cljc | 28 ++ common/src/app/common/pages/diff.cljc | 170 ++++++++++++ .../src/app/main/data/workspace/guides.cljs | 15 +- frontend/src/app/main/snap.cljs | 10 +- .../main/ui/workspace/viewport/guides.cljs | 3 +- frontend/src/app/util/geom/snap_points.cljs | 9 + frontend/src/app/util/snap_data.cljs | 244 ++++++++++++++++++ frontend/src/app/worker/impl.cljs | 9 +- frontend/src/app/worker/selection.cljs | 6 +- frontend/src/app/worker/snaps.cljs | 174 +------------ frontend/test/app/util/snap_data_test.cljs | 243 +++++++++++++++++ 11 files changed, 726 insertions(+), 185 deletions(-) create mode 100644 common/src/app/common/pages/diff.cljc create mode 100644 frontend/src/app/util/snap_data.cljs create mode 100644 frontend/test/app/util/snap_data_test.cljs diff --git a/common/src/app/common/file_builder.cljc b/common/src/app/common/file_builder.cljc index 0d6c96ff4..b323f51d9 100644 --- a/common/src/app/common/file_builder.cljc +++ b/common/src/app/common/file_builder.cljc @@ -568,4 +568,32 @@ (dissoc :current-component-id) (update :parent-stack pop)))) +(defn add-guide + [file guide] + (let [guide (cond-> guide + (nil? (:id guide)) + (assoc :id (uuid/next))) + page-id (:current-page-id file) + old-guides (or (get-in file [:data :pages-index page-id :options :guides]) {}) + new-guides (assoc old-guides (:id guide) guide)] + (commit-change + file + {:type :set-option + :page-id page-id + :option :guides + :value new-guides}))) + +(defn delete-object + [file id] + (let [page-id (:current-page-id file)] + (commit-change + file + {:type :del-obj + :page-id page-id + :id id}))) + +(defn get-current-page + [file] + (let [page-id (:current-page-id file)] + (-> file (get-in [:data :pages-index page-id])))) diff --git a/common/src/app/common/pages/diff.cljc b/common/src/app/common/pages/diff.cljc new file mode 100644 index 000000000..46ba2f46b --- /dev/null +++ b/common/src/app/common/pages/diff.cljc @@ -0,0 +1,170 @@ +;; 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) UXBOX Labs SL + +(ns app.common.pages.diff + "Given a page in its old version and the new will retrieve a map with + the differences that will have an impact in the snap data" + (:require + [app.common.data :as d] + [clojure.set :as set])) + +(defn calculate-page-diff + [old-page page check-attrs] + + (let [old-objects (get old-page :objects) + old-guides (or (get-in old-page [:options :guides]) []) + + new-objects (get page :objects) + new-guides (or (get-in page [:options :guides]) []) + + changed-object? + (fn [id] + (let [oldv (get old-objects id) + newv (get new-objects id)] + ;; Check first without select-keys because is faster if they are + ;; the same reference + (and (not= oldv newv) + (not= (select-keys oldv check-attrs) + (select-keys newv check-attrs))))) + + frame? + (fn [id] + (or (= :frame (get-in new-objects [id :type])) + (= :frame (get-in old-objects [id :type])))) + + changed-guide? + (fn [id] + (not= (get old-guides id) + (get new-guides id))) + + deleted-object? + #(and (contains? old-objects %) + (not (contains? new-objects %))) + + deleted-guide? + #(and (contains? old-guides %) + (not (contains? new-guides %))) + + new-object? + #(and (not (contains? old-objects %)) + (contains? new-objects %)) + + new-guide? + #(and (not (contains? old-guides %)) + (contains? new-guides %)) + + changed-frame-object? + #(and (contains? new-objects %) + (contains? old-objects %) + (not= (get-in old-objects [% :frame-id]) + (get-in new-objects [% :frame-id]))) + + changed-frame-guide? + #(and (contains? new-guides %) + (contains? old-guides %) + (not= (get-in old-objects [% :frame-id]) + (get-in new-objects [% :frame-id]))) + + changed-attrs-object? + #(and (contains? new-objects %) + (contains? old-objects %) + (= (get-in old-objects [% :frame-id]) + (get-in new-objects [% :frame-id]))) + + changed-attrs-guide? + #(and (contains? new-guides %) + (contains? old-guides %) + (= (get-in old-objects [% :frame-id]) + (get-in new-objects [% :frame-id]))) + + changed-object-ids + (into #{} + (filter changed-object?) + (set/union (set (keys old-objects)) + (set (keys new-objects)))) + + changed-guides-ids + (into #{} + (filter changed-guide?) + (set/union (set (keys old-guides)) + (set (keys new-guides)))) + + get-diff-object (fn [id] [(get old-objects id) (get new-objects id)]) + get-diff-guide (fn [id] [(get old-guides id) (get new-guides id)]) + + ;; Shapes with different frame owner + change-frame-shapes + (->> changed-object-ids + (into [] (comp (filter changed-frame-object?) + (map get-diff-object)))) + + ;; Guides that changed frames + change-frame-guides + (->> changed-guides-ids + (into [] (comp (filter changed-frame-guide?) + (map get-diff-guide)))) + + removed-frames + (->> changed-object-ids + (into [] (comp (filter frame?) + (filter deleted-object?) + (map (d/getf old-objects))))) + + removed-shapes + (->> changed-object-ids + (into [] (comp (remove frame?) + (filter deleted-object?) + (map (d/getf old-objects))))) + + removed-guides + (->> changed-guides-ids + (into [] (comp (filter deleted-guide?) + (map (d/getf old-guides))))) + + updated-frames + (->> changed-object-ids + (into [] (comp (filter frame?) + (filter changed-attrs-object?) + (map get-diff-object)))) + + updated-shapes + (->> changed-object-ids + (into [] (comp (remove frame?) + (filter changed-attrs-object?) + (map get-diff-object)))) + + updated-guides + (->> changed-guides-ids + (into [] (comp (filter changed-attrs-guide?) + (map get-diff-guide)))) + + new-frames + (->> changed-object-ids + (into [] (comp (filter frame?) + (filter new-object?) + (map (d/getf new-objects))))) + + new-shapes + (->> changed-object-ids + (into [] (comp (remove frame?) + (filter new-object?) + (map (d/getf new-objects))))) + + new-guides + (->> changed-guides-ids + (into [] (comp (filter new-guide?) + (map (d/getf new-guides)))))] + {:change-frame-shapes change-frame-shapes + :change-frame-guides change-frame-guides + :removed-frames removed-frames + :removed-shapes removed-shapes + :removed-guides removed-guides + :updated-frames updated-frames + :updated-shapes updated-shapes + :updated-guides updated-guides + :new-frames new-frames + :new-shapes new-shapes + :new-guides new-guides})) diff --git a/frontend/src/app/main/data/workspace/guides.cljs b/frontend/src/app/main/data/workspace/guides.cljs index 612b4adf7..45502688a 100644 --- a/frontend/src/app/main/data/workspace/guides.cljs +++ b/frontend/src/app/main/data/workspace/guides.cljs @@ -22,16 +22,9 @@ ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides [])) - guides-ids? (into #{} (map :id) guides) + guides (-> state wsh/lookup-page-options (:guides {})) - new-guides - (if (guides-ids? (:id guide)) - ;; Update existing guide - (mapv (make-update-guide guide) guides) - - ;; Add new guide - (conj guides guide)) + new-guides (assoc guides (:id guide) guide) rch [{:type :set-option :page-id page-id @@ -52,8 +45,8 @@ ptk/WatchEvent (watch [it state _] (let [page-id (:current-page-id state) - guides (-> state wsh/lookup-page-options (:guides [])) - new-guides (filterv #(not= (:id %) (:id guide)) guides) + guides (-> state wsh/lookup-page-options (:guides {})) + new-guides (dissoc guides (:id guide)) rch [{:type :set-option :page-id page-id diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 41bd7ae79..8fa0dc6ec 100644 --- a/frontend/src/app/main/snap.cljs +++ b/frontend/src/app/main/snap.cljs @@ -19,7 +19,7 @@ [beicon.core :as rx] [clojure.set :as set])) -(def ^:const snap-accuracy 5) +(def ^:const snap-accuracy 10) (def ^:const snap-path-accuracy 10) (def ^:const snap-distance-accuracy 10) @@ -27,12 +27,12 @@ [remove-id?] (fn [query-result] (->> query-result - (map (fn [[value data]] [value (remove (comp remove-id? second) data)])) + (map (fn [[value data]] [value (remove (comp remove-id? :id) data)])) (filter (fn [[_ data]] (seq data)))))) (defn- flatten-to-points [query-result] - (mapcat (fn [[_ data]] (map (fn [[point _]] point) data)) query-result)) + (mapcat (fn [[_ data]] (map :pt data)) query-result)) (defn- calculate-distance [query-result point coord] (->> query-result @@ -62,7 +62,7 @@ (->> (uw/ask! {:cmd :snaps/range-query :page-id page-id :frame-id frame-id - :coord coord + :axis coord :ranges [[(- value 0.5) (+ value 0.5)]]}) (rx/first) (rx/map (remove-from-snap-points filter-shapes)) @@ -78,7 +78,7 @@ (->> (uw/ask! {:cmd :snaps/range-query :page-id page-id :frame-id frame-id - :coord coord + :axis coord :ranges ranges}) (rx/first) (rx/map (remove-from-snap-points filter-shapes)) diff --git a/frontend/src/app/main/ui/workspace/viewport/guides.cljs b/frontend/src/app/main/ui/workspace/viewport/guides.cljs index 64e846e33..1251a71e1 100644 --- a/frontend/src/app/main/ui/workspace/viewport/guides.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/guides.cljs @@ -396,7 +396,8 @@ [{:keys [zoom vbox hover-frame]}] (let [page (mf/deref refs/workspace-page) - guides (->> (get-in page [:options :guides] []) + guides (->> (get-in page [:options :guides] {}) + (vals) (filter (guide-inside-vbox? vbox))) hover-frame-ref (mf/use-ref nil) diff --git a/frontend/src/app/util/geom/snap_points.cljs b/frontend/src/app/util/geom/snap_points.cljs index 669a29113..04ce1ebbf 100644 --- a/frontend/src/app/util/geom/snap_points.cljs +++ b/frontend/src/app/util/geom/snap_points.cljs @@ -28,3 +28,12 @@ (case (:type shape) :frame (-> shape :selrect frame-snap-points) (into #{(gsh/center-shape shape)} (:points shape))))) + +(defn guide-snap-points + [guide] + + ;; TODO: The line will be displayed from the position to the axis. Maybe + ;; revisit this + (if (= :x (:axis guide)) + #{(gpt/point (:position guide) 0)} + #{(gpt/point 0 (:position guide))})) diff --git a/frontend/src/app/util/snap_data.cljs b/frontend/src/app/util/snap_data.cljs new file mode 100644 index 000000000..ed5427234 --- /dev/null +++ b/frontend/src/app/util/snap_data.cljs @@ -0,0 +1,244 @@ +;; 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) UXBOX Labs SL + +(ns app.util.snap-data + "Data structure that holds and retrieves the data to make the snaps. Internaly + is implemented with a balanced binary tree that queries by range. + https://en.wikipedia.org/wiki/Range_tree" + (:require + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.pages.diff :as diff] + [app.common.uuid :as uuid] + [app.util.geom.grid :as gg] + [app.util.geom.snap-points :as snap] + [app.util.range-tree :as rt])) + +(def snap-attrs [:frame-id :x :y :width :height :hidden :selrect :grids]) + +;; PRIVATE FUNCTIONS + +(defn make-insert-tree-data + [shape-data axis] + (fn [tree] + (let [tree (or tree (rt/make-tree))] + (as-> tree $ + (reduce (fn [tree data] + (rt/insert tree (get-in data [:pt axis]) data)) + $ shape-data))))) + +(defn make-delete-tree-data + [shape-data axis] + (fn [tree] + (let [tree (or tree (rt/make-tree))] + (as-> tree $ + (reduce (fn [tree data] + (rt/remove tree (get-in data [:pt axis]) data)) + $ shape-data))))) + +(defn add-root-frame + [page-data] + (let [frame-id uuid/zero] + + (-> page-data + (assoc-in [frame-id :x] (rt/make-tree)) + (assoc-in [frame-id :y] (rt/make-tree))))) + +(defn add-frame + [page-data frame] + (let [frame-id (:id frame) + parent-id (:parent-id frame) + frame-data (->> (snap/shape-snap-points frame) + (map #(hash-map :type :shape + :id frame-id + :pt %))) + + grid-x-data (->> (gg/grid-snap-points frame :x) + (map #(hash-map :type :grid-x + :id frame-id + :pt %))) + + grid-y-data (->> (gg/grid-snap-points frame :y) + (map #(hash-map :type :grid-y + :id frame-id + :pt %)))] + + (-> page-data + ;; Update root frame information + (assoc-in [uuid/zero :objects-data frame-id] frame-data) + (update-in [parent-id :x] (make-insert-tree-data frame-data :x)) + (update-in [parent-id :y] (make-insert-tree-data frame-data :y)) + + ;; Update frame information + (assoc-in [frame-id :objects-data frame-id] (d/concat-vec frame-data grid-x-data grid-y-data)) + (update-in [frame-id :x] #(or % (rt/make-tree))) + (update-in [frame-id :y] #(or % (rt/make-tree))) + (update-in [frame-id :x] (make-insert-tree-data (d/concat-vec frame-data grid-x-data) :x)) + (update-in [frame-id :y] (make-insert-tree-data (d/concat-vec frame-data grid-y-data) :y))))) + +(defn add-shape + [page-data shape] + (let [frame-id (:frame-id shape) + snap-points (snap/shape-snap-points shape) + shape-data (->> snap-points + (mapv #(hash-map + :type :shape + :id (:id shape) + :pt %)))] + (-> page-data + (assoc-in [frame-id :objects-data (:id shape)] shape-data) + (update-in [frame-id :x] (make-insert-tree-data shape-data :x)) + (update-in [frame-id :y] (make-insert-tree-data shape-data :y))))) + + +(defn add-guide + [page-data guide] + + (let [guide-data (->> (snap/guide-snap-points guide) + (mapv #(hash-map + :type :guide + :id (:id guide) + :pt %)))] + + (if-let [frame-id (:frame-id guide)] + ;; Guide inside frame, we add the information only on that frame + (-> page-data + (assoc-in [frame-id :objects-data (:id guide)] guide-data) + (update-in [frame-id (:axis guide)] (make-insert-tree-data guide-data (:axis guide)))) + + ;; Guide outside the frame. We add the information in the global guides data + (-> page-data + (assoc-in [:guides :objects-data (:id guide)] [guide-data]) + (update-in [:guides (:axis guide)] (make-insert-tree-data guide-data (:axis guide))))))) + +(defn remove-frame + [page-data frame] + (let [frame-id (:id frame) + root-data (get-in page-data [uuid/zero :objects-data frame-id])] + (-> page-data + (d/dissoc-in [uuid/zero :objects-data frame-id]) + (update-in [uuid/zero :x] (make-delete-tree-data root-data :x)) + (update-in [uuid/zero :y] (make-delete-tree-data root-data :y)) + (dissoc frame-id)))) + +(defn remove-shape + [page-data shape] + + (let [frame-id (:frame-id shape) + shape-data (get-in page-data [frame-id :objects-data (:id shape)])] + (-> page-data + (d/dissoc-in [frame-id :objects-data (:id shape)]) + (update-in [frame-id :x] (make-delete-tree-data shape-data :x)) + (update-in [frame-id :y] (make-delete-tree-data shape-data :y))))) + +(defn remove-guide + [page-data guide] + (if-let [frame-id (:frame-id guide)] + (let [guide-data (get-in page-data [frame-id :objects-data (:id guide)])] + (-> page-data + (d/dissoc-in [frame-id :objects-data (:id guide)]) + (update-in [frame-id (:axis guide)] (make-delete-tree-data guide-data (:axis guide))))) + + ;; Guide outside the frame. We add the information in the global guides data + (let [guide-data (get-in page-data [:guides :objects-data (:id guide)])] + (-> page-data + (d/dissoc-in [:guides :objects-data (:id guide)]) + (update-in [:guides (:axis guide)] (make-delete-tree-data guide-data (:axis guide))))))) + +(defn update-frame + [page-data [_ new-frame]] + (let [frame-id (:id new-frame) + root-data (get-in page-data [uuid/zero :objects-data frame-id]) + frame-data (get-in page-data [frame-id :objects-data frame-id])] + (-> page-data + (update-in [uuid/zero :x] (make-delete-tree-data root-data :x)) + (update-in [uuid/zero :y] (make-delete-tree-data root-data :y)) + (update-in [frame-id :x] (make-delete-tree-data frame-data :x)) + (update-in [frame-id :y] (make-delete-tree-data frame-data :y)) + (add-frame new-frame)))) + +(defn update-shape + [page-data [old-shape new-shape]] + (-> page-data + (remove-shape old-shape) + (add-shape new-shape))) + +(defn update-guide + [page-data [old-guide new-guide]] + (-> page-data + (remove-guide old-guide) + (add-guide new-guide))) + +;; PUBLIC API +(defn make-snap-data + "Creates an empty snap index" + [] + {}) + +(defn add-page + "Adds page information" + [snap-data {:keys [objects options] :as page}] + + (let [frames (cp/select-frames objects) + shapes (cp/select-objects #(not= :frame (:type %)) page) + guides (vals (:guides options)) + + page-data + (as-> {} $ + (add-root-frame $) + (reduce add-frame $ frames) + (reduce add-shape $ shapes) + (reduce add-guide $ guides))] + (assoc snap-data (:id page) page-data))) + +(defn update-page + "Updates a previously inserted page with new data" + [snap-data old-page page] + + (if (contains? snap-data (:id page)) + ;; Update page + (update snap-data (:id page) + (fn [page-data] + (let [{:keys [change-frame-shapes + change-frame-guides + removed-frames + removed-shapes + removed-guides + updated-frames + updated-shapes + updated-guides + new-frames + new-shapes + new-guides]} + (diff/calculate-page-diff old-page page snap-attrs)] + + (as-> page-data $ + (reduce update-shape $ change-frame-shapes) + (reduce remove-frame $ removed-frames) + (reduce remove-shape $ removed-shapes) + (reduce update-frame $ updated-frames) + (reduce update-shape $ updated-shapes) + (reduce add-frame $ new-frames) + (reduce add-shape $ new-shapes) + (reduce update-guide $ change-frame-guides) + (reduce remove-guide $ removed-guides) + (reduce update-guide $ updated-guides) + (reduce add-guide $ new-guides))))) + + ;; Page doesn't exist, we create a new entry + (add-page snap-data page))) + +(defn query + [snap-data page-id frame-id axis [from to]] + + (d/concat-vec + (-> snap-data + (get-in [page-id frame-id axis]) + (rt/range-query from to)) + + (-> snap-data + (get-in [page-id :guides axis]) + (rt/range-query from to)))) diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 13b7d167f..977c8d85e 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -40,14 +40,13 @@ (defmethod handler :update-page-indices [{:keys [page-id changes] :as message}] - (let [old-objects (get-in @state [:pages-index page-id :objects])] + (let [old-page (get-in @state [:pages-index page-id])] (swap! state ch/process-changes changes false) - (let [new-objects (get-in @state [:pages-index page-id :objects]) + (let [new-page (get-in @state [:pages-index page-id]) message (assoc message - :objects new-objects - :new-objects new-objects - :old-objects old-objects)] + :old-page old-page + :new-page new-page)] (handler (-> message (assoc :cmd :selection/update-index))) (handler (-> message diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 3b9d92291..d0b229034 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -170,8 +170,10 @@ nil)) (defmethod impl/handler :selection/update-index - [{:keys [page-id old-objects new-objects] :as message}] - (let [update-page-index + [{:keys [page-id old-page new-page] :as message}] + (let [old-objects (:objects old-page) + new-objects (:objects new-page) + update-page-index (fn [index] (let [old-bounds (:bounds index) new-bounds (objects-bounds new-objects)] diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 66a4dff83..b992b51e7 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,179 +6,31 @@ (ns app.worker.snaps (:require - [app.common.data :as d] - [app.common.uuid :as uuid] - [app.util.geom.grid :as gg] - [app.util.geom.snap-points :as snap] - [app.util.range-tree :as rt] + [app.util.snap-data :as sd] [app.worker.impl :as impl] - [clojure.set :as set] [okulary.core :as l])) (defonce state (l/atom {})) -(defn process-shape [frame-id coord] - (fn [shape] - (let [points (when-not (:hidden shape) (snap/shape-snap-points shape)) - shape-data (->> points (mapv #(vector % (:id shape))))] - (if (= (:id shape) frame-id) - (into shape-data - - ;; The grid points are only added by the "root" of the coord-dat - (->> (gg/grid-snap-points shape coord) - (map #(vector % :layout)))) - shape-data)))) - -(defn- add-coord-data - "Initializes the range tree given the shapes" - [data frame-id shapes coord] - (letfn [(into-tree [tree [point _ :as data]] - (rt/insert tree (coord point) data))] - (->> shapes - (mapcat (process-shape frame-id coord)) - (reduce into-tree (or data (rt/make-tree)))))) - -(defn remove-coord-data - [data frame-id shapes coord] - (letfn [(remove-tree [tree [point _ :as data]] - (rt/remove tree (coord point) data))] - (->> shapes - (mapcat (process-shape frame-id coord)) - (reduce remove-tree (or data (rt/make-tree)))))) - -(defn aggregate-data - ([objects] - (aggregate-data objects (keys objects))) - - ([objects ids] - (->> ids - (filter #(contains? objects %)) - (map #(get objects %)) - (filter :frame-id) - (group-by :frame-id) - ;; Adds the frame - (d/mapm #(conj %2 (get objects %1)))))) - -(defn- initialize-snap-data - "Initialize the snap information with the current workspace information" - [objects] - (let [shapes-data (aggregate-data objects) - - create-index - (fn [frame-id shapes] - {:x (-> (rt/make-tree) (add-coord-data frame-id shapes :x)) - :y (-> (rt/make-tree) (add-coord-data frame-id shapes :y))})] - (d/mapm create-index shapes-data))) - -;; Attributes that will change the values of their snap -(def snap-attrs [:x :y :width :height :hidden :selrect :grids]) - -(defn- update-snap-data - [snap-data old-objects new-objects] - - (let [changed? (fn [id] - (let [oldv (get old-objects id) - newv (get new-objects id)] - ;; Check first without select-keys because is faster if they are - ;; the same reference - (and (not= oldv newv) - (not= (select-keys oldv snap-attrs) - (select-keys newv snap-attrs))))) - - is-deleted-frame? #(and (not= uuid/zero %) - (contains? old-objects %) - (not (contains? new-objects %)) - (= :frame (get-in old-objects [% :type]))) - is-new-frame? #(and (not= uuid/zero %) - (contains? new-objects %) - (not (contains? old-objects %)) - (= :frame (get-in new-objects [% :type]))) - - changed-ids (into #{} - (filter changed?) - (set/union (set (keys old-objects)) - (set (keys new-objects)))) - - to-delete (aggregate-data old-objects changed-ids) - to-add (aggregate-data new-objects changed-ids) - - frames-to-delete (->> changed-ids (filter is-deleted-frame?)) - frames-to-add (->> changed-ids (filter is-new-frame?)) - - delete-data - (fn [snap-data [frame-id shapes]] - (-> snap-data - (update-in [frame-id :x] remove-coord-data frame-id shapes :x) - (update-in [frame-id :y] remove-coord-data frame-id shapes :y))) - - add-data - (fn [snap-data [frame-id shapes]] - (-> snap-data - (update-in [frame-id :x] add-coord-data frame-id shapes :x) - (update-in [frame-id :y] add-coord-data frame-id shapes :y))) - - delete-frames - (fn [snap-data frame-id] - (dissoc snap-data frame-id)) - - add-frames - (fn [snap-data frame-id] - (assoc snap-data frame-id {:x (rt/make-tree) - :y (rt/make-tree)}))] - - (as-> snap-data $ - (reduce delete-data $ to-delete) - (reduce add-frames $ frames-to-add) - (reduce add-data $ to-add) - (reduce delete-frames $ frames-to-delete)))) - -;; (defn- log-state -;; "Helper function to print a friendly version of the snap tree. Debugging purposes" -;; [] -;; (let [process-frame-data #(d/mapm rt/as-map %) -;; process-page-data #(d/mapm process-frame-data %)] -;; (js/console.log "STATE" (clj->js (d/mapm process-page-data @state))))) - -(defn- index-page [state page-id objects] - (let [snap-data (initialize-snap-data objects)] - (assoc state page-id snap-data))) - -(defn- update-page [state page-id old-objects new-objects] - (let [snap-data (get state page-id) - snap-data (update-snap-data snap-data old-objects new-objects)] - (assoc state page-id snap-data))) - ;; Public API (defmethod impl/handler :snaps/initialize-index [{:keys [data] :as message}] - ;; Create the index - (letfn [(process-page [state page] - (let [id (:id page) - objects (:objects page)] - (index-page state id objects)))] - (swap! state #(reduce process-page % (vals (:pages-index data)))) - ;; (log-state) - ;; Return nil so the worker will not answer anything back - nil)) + + (let [pages (vals (:pages-index data))] + (reset! state (reduce sd/add-page (sd/make-snap-data) pages))) + + nil) (defmethod impl/handler :snaps/update-index - [{:keys [page-id old-objects new-objects] :as message}] - (swap! state update-page page-id old-objects new-objects) - - ;; Uncomment this to regenerate the index everytime - #_(swap! state index-page page-id new-objects) - ;; (log-state) + [{:keys [old-page new-page] :as message}] + (swap! state sd/update-page old-page new-page) nil) (defmethod impl/handler :snaps/range-query - [{:keys [page-id frame-id coord ranges] :as message}] - (letfn [(calculate-range [[from to]] - (-> @state - (get-in [page-id frame-id coord]) - (rt/range-query from to)))] - (->> ranges - (mapcat calculate-range) - set ;; unique - (into [])))) + [{:keys [page-id frame-id axis ranges] :as message}] + (->> ranges + (mapcat #(sd/query @state page-id frame-id axis %)) + (set) ;; unique + (into []))) diff --git a/frontend/test/app/util/snap_data_test.cljs b/frontend/test/app/util/snap_data_test.cljs new file mode 100644 index 000000000..ff09693a4 --- /dev/null +++ b/frontend/test/app/util/snap_data_test.cljs @@ -0,0 +1,243 @@ +;; 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) UXBOX Labs SL + +(ns app.util.snap-data-test + (:require + [app.common.uuid :as uuid] + [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [app.common.pages.init :as init] + [app.common.file-builder :as fb] + [app.util.snap-data :as sd])) + +(t/deftest test-create-index + (t/testing "Create empty data" + (let [data (sd/make-snap-data)] + (t/is (some? data)))) + + (t/testing "Add empty page (only root-frame)" + (let [page (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/get-current-page)) + + data (-> (sd/make-snap-data) + (sd/add-page page))] + (t/is (some? data)))) + + (t/testing "Create simple shape on root" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/create-rect + {:x 0 + :y 0 + :width 100 + :height 100})) + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + result-x (sd/query data (:id page) uuid/zero :x [0 100])] + + (t/is (some? data)) + + ;; 3 = left side, center and right side + (t/is (= (count result-x) 3)) + + ;; Left side: two points + (t/is (= (first (nth result-x 0)) 0)) + + ;; Center one point + (t/is (= (first (nth result-x 1)) 50)) + + ;; Right side two points + (t/is (= (first (nth result-x 2)) 100)))) + + (t/testing "Add page with single empty frame" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/add-artboard + {:x 0 + :y 0 + :width 100 + :height 100}) + (fb/close-artboard)) + + frame-id (:last-id file) + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-frame-x (sd/query data (:id page) frame-id :x [0 100])] + + (t/is (some? data)) + (t/is (= (count result-zero-x) 3)) + (t/is (= (count result-frame-x) 3)))) + + (t/testing "Add page with some shapes inside frames" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/add-artboard + {:x 0 + :y 0 + :width 100 + :height 100})) + frame-id (:last-id file) + + file (-> file + (fb/create-rect + {:x 25 + :y 25 + :width 50 + :height 50}) + (fb/close-artboard)) + + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-frame-x (sd/query data (:id page) frame-id :x [0 100])] + + (t/is (some? data)) + (t/is (= (count result-zero-x) 3)) + (t/is (= (count result-frame-x) 5)))) + + (t/testing "Add a global guide" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/add-guide {:position 50 :axis :x}) + (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) + (fb/close-artboard)) + + frame-id (:last-id file) + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) + result-frame-x (sd/query data (:id page) frame-id :x [0 100]) + result-frame-y (sd/query data (:id page) frame-id :y [0 100])] + + (t/is (some? data)) + ;; We can snap in the root + (t/is (= (count result-zero-x) 1)) + (t/is (= (count result-zero-y) 0)) + + ;; We can snap in the frame + (t/is (= (count result-frame-x) 1)) + (t/is (= (count result-frame-y) 0)))) + + (t/testing "Add a frame guide" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/add-artboard {:x 200 :y 200 :width 100 :height 100}) + (fb/close-artboard)) + + frame-id (:last-id file) + + file (-> file + (fb/add-guide {:position 50 :axis :x :frame-id frame-id})) + + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + result-zero-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-zero-y (sd/query data (:id page) uuid/zero :y [0 100]) + result-frame-x (sd/query data (:id page) frame-id :x [0 100]) + result-frame-y (sd/query data (:id page) frame-id :y [0 100])] + (t/is (some? data)) + ;; We can snap in the root + (t/is (= (count result-zero-x) 0)) + (t/is (= (count result-zero-y) 0)) + + ;; We can snap in the frame + (t/is (= (count result-frame-x) 1)) + (t/is (= (count result-frame-y) 0))))) + +(t/deftest test-update-index + (t/testing "Create frame on root and then remove it." + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/add-artboard + {:x 0 + :y 0 + :width 100 + :height 100}) + (fb/close-artboard)) + + shape-id (:last-id file) + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + file (-> file + (fb/delete-object shape-id)) + + new-page (fb/get-current-page file) + data (sd/update-page data page new-page) + + result-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-y (sd/query data (:id page) uuid/zero :y [0 100])] + + (t/is (some? data)) + (t/is (= (count result-x) 0)) + (t/is (= (count result-y) 0)))) + + (t/testing "Create simple shape on root. Then remove it" + (let [file (-> (fb/create-file "Test") + (fb/add-page {:name "Page-1"}) + (fb/create-rect + {:x 0 + :y 0 + :width 100 + :height 100})) + + shape-id (:last-id file) + page (fb/get-current-page file) + + ;; frame-id (:last-id file) + data (-> (sd/make-snap-data) + (sd/add-page page)) + + file (-> file + (fb/delete-object shape-id)) + + new-page (fb/get-current-page file) + data (sd/update-page data page new-page) + + result-x (sd/query data (:id page) uuid/zero :x [0 100]) + result-y (sd/query data (:id page) uuid/zero :y [0 100])] + + (t/is (some? data)) + (t/is (= (count result-x) 0)) + (t/is (= (count result-y) 0)))) + + (t/testing "Create shape inside frame, then remove it") + (t/testing "Create guide then remove it") + + (t/testing "Update frame coordinates") + (t/testing "Update shape coordinates") + (t/testing "Update shape inside frame coordinates") + (t/testing "Update global guide") + (t/testing "Update frame guide") + + (t/testing "Change shape frame") + (t/testing "Change guide frame"))