diff --git a/common/app/common/data.cljc b/common/app/common/data.cljc index 12b58d619..da478337a 100644 --- a/common/app/common/data.cljc +++ b/common/app/common/data.cljc @@ -461,3 +461,9 @@ kw (if (keyword? kw) (name kw) kw)] (keyword (str prefix kw)))) + +(defn tap + "Simpilar to the tap in rxjs but for plain collections" + [f coll] + (f coll) + coll) diff --git a/common/app/common/pages.cljc b/common/app/common/pages.cljc index 2776e8168..8282cf12c 100644 --- a/common/app/common/pages.cljc +++ b/common/app/common/pages.cljc @@ -11,6 +11,7 @@ [app.common.pages.changes :as changes] [app.common.pages.common :as common] [app.common.pages.helpers :as helpers] + [app.common.pages.indices :as indices] [app.common.pages.init :as init] [app.common.pages.spec :as spec] [clojure.spec.alpha :as s])) @@ -42,7 +43,6 @@ (d/export helpers/is-shape-grouped) (d/export helpers/get-parent) (d/export helpers/get-parents) -(d/export helpers/generate-child-parent-index) (d/export helpers/clean-loops) (d/export helpers/calculate-invalid-targets) (d/export helpers/valid-frame-target) @@ -60,13 +60,17 @@ (d/export helpers/get-base-shape) (d/export helpers/is-parent?) (d/export helpers/get-index-in-parent) -(d/export helpers/calculate-z-index) -(d/export helpers/generate-child-all-parents-index) (d/export helpers/parse-path-name) (d/export helpers/merge-path-item) (d/export helpers/compact-path) (d/export helpers/compact-name) +;; Indices +(d/export indices/calculate-z-index) +(d/export indices/generate-child-all-parents-index) +(d/export indices/generate-child-parent-index) +(d/export indices/create-mask-index) + ;; Process changes (d/export changes/process-changes) diff --git a/common/app/common/pages/helpers.cljc b/common/app/common/pages/helpers.cljc index 39597788d..f3a52cbc9 100644 --- a/common/app/common/pages/helpers.cljc +++ b/common/app/common/pages/helpers.cljc @@ -160,27 +160,6 @@ (when parent-id (lazy-seq (cons parent-id (get-parents parent-id objects)))))) -(defn generate-child-parent-index - [objects] - (reduce-kv - (fn [index id obj] - (assoc index id (:parent-id obj))) - {} objects)) - -(defn generate-child-all-parents-index - "Creates an index where the key is the shape id and the value is a set - with all the parents" - ([objects] - (generate-child-all-parents-index objects (vals objects))) - - ([objects shapes] - (let [shape->parents - (fn [shape] - (->> (get-parents (:id shape) objects) - (into [])))] - (->> shapes - (map #(vector (:id %) (shape->parents %))) - (into {}))))) (defn clean-loops "Clean a list of ids from circular references." @@ -347,40 +326,7 @@ (reduce red-fn cur-idx (reverse (:shapes object)))))] (into {} (rec-index '() uuid/zero)))) -(defn calculate-z-index - "Given a collection of shapes calculates their z-index. Greater index - means is displayed over other shapes with less index." - [objects] - (let [is-frame? (fn [id] (= :frame (get-in objects [id :type]))) - root-children (get-in objects [uuid/zero :shapes]) - num-frames (->> root-children (filter is-frame?) count)] - (when (seq root-children) - (loop [current (peek root-children) - pending (pop root-children) - current-idx (+ (count objects) num-frames -1) - z-index {}] - - (let [children (->> (get-in objects [current :shapes])) - children (cond - (and (is-frame? current) (contains? z-index current)) - [] - - (and (is-frame? current) - (not (contains? z-index current))) - (into [current] children) - - :else - children) - pending (into (vec pending) children)] - (if (empty? pending) - (assoc z-index current current-idx) - - (let [] - (recur (peek pending) - (pop pending) - (dec current-idx) - (assoc z-index current current-idx))))))))) (defn expand-region-selection "Given a selection selects all the shapes between the first and last in diff --git a/common/app/common/pages/indices.cljc b/common/app/common/pages/indices.cljc new file mode 100644 index 000000000..d58accd00 --- /dev/null +++ b/common/app/common/pages/indices.cljc @@ -0,0 +1,83 @@ +;; 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.indices + (:require + [app.common.data :as d] + [app.common.geom.shapes :as gsh] + [app.common.pages.helpers :as helpers] + [app.common.uuid :as uuid])) + +(defn calculate-z-index + "Given a collection of shapes calculates their z-index. Greater index + means is displayed over other shapes with less index." + [objects] + (let [is-frame? (fn [id] (= :frame (get-in objects [id :type]))) + root-children (or (get-in objects [uuid/zero :shapes]) []) + num-frames (->> root-children (filterv is-frame?) count)] + + (when-not (empty? root-children) + (loop [current (peek root-children) + pending (pop root-children) + current-idx (+ (count objects) num-frames -1) + z-index (transient {})] + + (let [children (get-in objects [current :shapes]) + assigned? (contains? z-index current) + is-frame? (is-frame? current) + + pending (cond + (not is-frame?) + (d/concat pending children) + + (not assigned?) + (d/concat pending [current] children) + + :else + pending)] + + (if (empty? pending) + (-> (assoc! z-index current current-idx) + (persistent!)) + (recur (peek pending) + (pop pending) + (dec current-idx) + (assoc! z-index current current-idx)))))))) + +(defn generate-child-parent-index + [objects] + (reduce-kv + (fn [index id obj] + (assoc index id (:parent-id obj))) + {} objects)) + +(defn generate-child-all-parents-index + "Creates an index where the key is the shape id and the value is a set + with all the parents" + ([objects] + (generate-child-all-parents-index objects (vals objects))) + + ([objects shapes] + (let [shape->parents + (fn [shape] + (->> (helpers/get-parents (:id shape) objects) + (into [])))] + (->> shapes + (map #(vector (:id %) (shape->parents %))) + (into {}))))) + +(defn create-mask-index + "Retrieves the mask information for an object" + [objects parents-index] + (let [retrieve-masks + (fn [id parents] + (->> parents + (map #(get objects %)) + (filter #(:masked-group? %)) + ;; Retrieve the masking element + (mapv #(get objects (->> % :shapes first)))))] + (->> parents-index + (d/mapm retrieve-masks)))) diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 279682c2f..cd86cb48f 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -264,7 +264,7 @@ :index (cp/get-index-in-parent objects (:id shape)) :shapes [(:id shape)]})))] - (when-not (empty? rch) + (when-not (empty? uch) (rx/of dwu/pop-undo-into-transaction (dch/commit-changes rch uch {:commit-local? true}) (dwu/commit-undo-transaction) @@ -294,12 +294,15 @@ (rx/take-until stopper) (rx/map #(gpt/to-vec from-position %))) - snap-delta (->> position - (rx/throttle 20) - (rx/switch-map - (fn [pos] - (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) - (rx/map #(vector pos %))))))] + snap-delta (rx/concat + ;; We send the nil first so the stream is not waiting for the first value + (rx/of nil) + (->> position + (rx/throttle 20) + (rx/switch-map + (fn [pos] + (->> (snap/closest-snap-move page-id shapes objects layout zoom pos) + (rx/map #(vector pos %)))))))] (if (empty? shapes) (rx/empty) (rx/concat diff --git a/frontend/src/app/main/snap.cljs b/frontend/src/app/main/snap.cljs index 021d2528a..b8e700b6b 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])) -(defonce ^:private snap-accuracy 10) +(defonce ^:private snap-accuracy 5) (defonce ^:private snap-path-accuracy 10) (defonce ^:private snap-distance-accuracy 10) @@ -337,13 +337,15 @@ "Snaps a position given an old snap to a different position. We use this to provide a temporal snap while the new is being processed." [[position [snap-pos snap-delta]]] - (let [dx (if (not= 0 (:x snap-delta)) - (- (+ (:x snap-pos) (:x snap-delta)) (:x position)) - 0) - dy (if (not= 0 (:y snap-delta)) - (- (+ (:y snap-pos) (:y snap-delta)) (:y position)) - 0)] + (if (some? snap-delta) + (let [dx (if (not= 0 (:x snap-delta)) + (- (+ (:x snap-pos) (:x snap-delta)) (:x position)) + 0) + dy (if (not= 0 (:y snap-delta)) + (- (+ (:y snap-pos) (:y snap-delta)) (:y position)) + 0)] - (cond-> position - (<= (mth/abs dx) snap-accuracy) (update :x + dx) - (<= (mth/abs dy) snap-accuracy) (update :y + dy)))) + (cond-> position + (<= (mth/abs dx) snap-accuracy) (update :x + dx) + (<= (mth/abs dy) snap-accuracy) (update :y + dy))) + position)) diff --git a/frontend/src/app/util/quadtree.js b/frontend/src/app/util/quadtree.js index a6f904c10..000ae7c42 100644 --- a/frontend/src/app/util/quadtree.js +++ b/frontend/src/app/util/quadtree.js @@ -32,12 +32,16 @@ "use strict"; goog.provide("app.util.quadtree"); +goog.require("cljs.core"); goog.scope(function() { const self = app.util.quadtree; + const eq = cljs.core._EQ_; + const contains = cljs.core.contains_QMARK_; class Node { - constructor(bounds, data) { + constructor(id, bounds, data) { + this.id = id; this.bounds = bounds; this.data = data; } @@ -51,8 +55,8 @@ goog.scope(function() { this.level = level || 0; this.bounds = bounds; - this.objects = []; - this.indexes = []; + this.objects = []; + this.indexes = []; } split() { @@ -183,14 +187,18 @@ goog.scope(function() { this.objects = []; this.indexes = []; } + + getObjects() { + return this.objects; + } } self.create = function(rect) { return new Quadtree(rect, 10, 4, 0); }; - self.insert = function(index, bounds, data) { - const node = new Node(bounds, data); + self.insert = function(index, id, bounds, data) { + const node = new Node(id, bounds, data); index.insert(node); return index; }; @@ -210,4 +218,29 @@ goog.scope(function() { } }; + self.remove = function(index, id) { + const result = self.create(index.bounds); + + for (let node of index.objects) { + if (!eq(id, node.id)) { + self.insert(result, node.id, node.bounds, node.data); + } + } + + return result; + } + + // FIXME: Inefficient to recreate the index. Needs to be improved + self.remove_all = function(index, ids) { + const result = self.create(index.bounds); + + for (let node of self.search(index, index.bounds)) { + if (!contains(ids, node.id)) { + self.insert(result, node.id, node.bounds, node.data); + } + } + + return result; + } + }); diff --git a/frontend/src/app/util/range_tree.js b/frontend/src/app/util/range_tree.js index ed62a0200..64e3d3fdd 100644 --- a/frontend/src/app/util/range_tree.js +++ b/frontend/src/app/util/range_tree.js @@ -13,7 +13,7 @@ "use strict"; goog.provide("app.util.range_tree"); -goog.require("cljs.core") +goog.require("cljs.core"); goog.scope(function() { const eq = cljs.core._EQ_; @@ -92,7 +92,7 @@ goog.scope(function() { } isEmpty() { - return this.root === null; + return !this.root; } toString() { @@ -116,7 +116,7 @@ goog.scope(function() { // Insert recursively in the tree function recInsert (branch, value, data) { - if (branch === null) { + if (!branch) { const ret = new Node(value, data); ret.color = Color.RED; return ret; @@ -144,7 +144,7 @@ goog.scope(function() { // Search for the min node function searchMin(branch) { - if (branch.left === null) { + if (!branch.left) { return branch; } else { return searchMin(branch.left); @@ -153,7 +153,7 @@ goog.scope(function() { // Remove the lefmost node of the current branch function recRemoveMin(branch) { - if (branch.left === null) { + if (!branch.left) { return null; } @@ -167,7 +167,7 @@ goog.scope(function() { // Remove the data element for the value given // this will not remove the node, we have to remove the empty node afterwards function recRemoveData(branch, value, data) { - if (branch === null) { + if (!branch) { // Not found return branch; } else if (branch.value === value) { @@ -193,7 +193,7 @@ goog.scope(function() { if (isRed(branch.left)) { branch = rotateRight(branch); } - if (value === branch.value && branch.right === null) { + if (value === branch.value && !branch.right) { return null; } if (!isRed(branch.right) && !isRed(branch.right.left)) { @@ -214,7 +214,7 @@ goog.scope(function() { // Retrieve all the data related to value function recGet(branch, value) { - if (branch === null) { + if (!branch) { return null; } else if (branch.value === value) { return branch.data; @@ -226,7 +226,7 @@ goog.scope(function() { } function recUpdate(branch, value, oldData, newData) { - if (branch === null) { + if (!branch) { return branch; } else if (branch.value === value) { branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it); @@ -239,7 +239,7 @@ goog.scope(function() { } function recRangeQuery(branch, fromValue, toValue, result) { - if (branch === null) { + if (!branch) { return result; } if (fromValue < branch.value) { @@ -329,7 +329,7 @@ goog.scope(function() { // This will return the string representation. We don't care about internal structure // only the data function recToString(branch, result) { - if (branch === null) { + if (!branch) { return; } diff --git a/frontend/src/app/worker/impl.cljs b/frontend/src/app/worker/impl.cljs index 107270387..77032133a 100644 --- a/frontend/src/app/worker/impl.cljs +++ b/frontend/src/app/worker/impl.cljs @@ -39,11 +39,15 @@ (defmethod handler :update-page-indices [{:keys [page-id changes] :as message}] - (swap! state ch/process-changes changes false) + (let [old-objects (get-in @state [:pages-index page-id :objects])] + (swap! state ch/process-changes changes false) - (let [objects (get-in @state [:pages-index page-id :objects]) - message (assoc message :objects objects)] - (handler (-> message - (assoc :cmd :selection/update-index))) - (handler (-> message - (assoc :cmd :snaps/update-index))))) + (let [new-objects (get-in @state [:pages-index page-id :objects]) + message (assoc message + :objects new-objects + :new-objects new-objects + :old-objects old-objects)] + (handler (-> message + (assoc :cmd :selection/update-index))) + (handler (-> message + (assoc :cmd :snaps/update-index)))))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index 2d0ac3a5d..b49c2fe93 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -15,12 +15,110 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.util.quadtree :as qdt] - [app.worker.impl :as impl])) + [app.worker.impl :as impl] + [clojure.set :as set])) (defonce state (l/atom {})) -(declare index-object) -(declare create-index) +(defn- index-object + [objects z-index parents-index masks-index index obj] + (let [{:keys [x y width height]} (:selrect obj) + shape-bound #js {:x x :y y :width width :height height} + + parents (get parents-index (:id obj)) + masks (get masks-index (:id obj)) + z (get z-index (:id obj)) + + frame (when (and (not= :frame (:type obj)) + (not= (:frame-id obj) uuid/zero)) + (get objects (:frame-id obj)))] + (qdt/insert index + (:id obj) + shape-bound + (assoc obj :frame frame :masks masks :parents parents :z z)))) + +(defn- create-index + [objects] + (let [shapes (-> objects (dissoc uuid/zero) (vals)) + z-index (cp/calculate-z-index objects) + parents-index (cp/generate-child-all-parents-index objects) + masks-index (cp/create-mask-index objects parents-index) + bounds (gsh/selection-rect shapes) + bounds #js {:x (:x bounds) + :y (:y bounds) + :width (:width bounds) + :height (:height bounds)}] + + (reduce (partial index-object objects z-index parents-index masks-index) + (qdt/create bounds) + shapes))) + +(defn- update-index + [index old-objects new-objects] + + (let [changes? (fn [id] + (not= (get old-objects id) + (get new-objects id))) + + changed-ids (into #{} + (filter changes?) + (set/union (keys old-objects) + (keys new-objects))) + + shapes (->> changed-ids (mapv #(get new-objects %)) (filterv (comp not nil?))) + z-index (cp/calculate-z-index new-objects) + parents-index (cp/generate-child-all-parents-index new-objects shapes) + masks-index (cp/create-mask-index new-objects parents-index) + + new-index (qdt/remove-all index changed-ids)] + + (reduce (partial index-object new-objects z-index parents-index masks-index) + new-index + shapes))) + +(defn- query-index + [index rect frame-id include-frames? include-groups? disabled-masks reverse?] + (let [result (-> (qdt/search index (clj->js rect)) + (es6-iterator-seq)) + + ;; Check if the shape matches the filter criteria + match-criteria? + (fn [shape] + (and (not (:hidden shape)) + (not (:blocked shape)) + (or (not frame-id) (= frame-id (:frame-id shape))) + (case (:type shape) + :frame include-frames? + :group include-groups? + true))) + + overlaps? + (fn [shape] + (gsh/overlaps? shape rect)) + + overlaps-masks? + (fn [masks] + (->> masks + (some (comp not overlaps?)) + not)) + + ;; Shapes after filters of overlapping and criteria + matching-shapes + (into [] + (comp (map #(unchecked-get % "data")) + (filter match-criteria?) + (filter (comp overlaps? :frame)) + (filter (comp overlaps-masks? :masks)) + (filter overlaps?)) + result) + + keyfn (if reverse? (comp - :z) :z)] + + (into (d/ordered-set) + (->> matching-shapes + (sort-by keyfn) + (map :id))))) + (defmethod impl/handler :selection/initialize-index [{:keys [file-id data] :as message}] @@ -35,96 +133,13 @@ nil)) (defmethod impl/handler :selection/update-index - [{:keys [page-id objects] :as message}] - (let [index (create-index objects)] - (swap! state update page-id (constantly index)) - nil)) + [{:keys [page-id old-objects new-objects] :as message}] + (swap! state update page-id update-index old-objects new-objects) + nil) (defmethod impl/handler :selection/query [{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?] :or {include-groups? true disabled-masks #{} reverse? false} :as message}] (when-let [index (get @state page-id)] - (let [result (-> (qdt/search index (clj->js rect)) - (es6-iterator-seq)) - - ;; Check if the shape matches the filter criteria - match-criteria? - (fn [shape] - (and (not (:hidden shape)) - (not (:blocked shape)) - (or (not frame-id) (= frame-id (:frame-id shape))) - (case (:type shape) - :frame include-frames? - :group include-groups? - true))) - - overlaps? - (fn [shape] - (gsh/overlaps? shape rect)) - - overlaps-masks? - (fn [masks] - (->> masks - (some (comp not overlaps?)) - not)) - - ;; Shapes after filters of overlapping and criteria - matching-shapes - (into [] - (comp (map #(unchecked-get % "data")) - (filter match-criteria?) - (filter (comp overlaps? :frame)) - (filter (comp overlaps-masks? :masks)) - (filter overlaps?)) - result) - - keyfn (if reverse? (comp - :z) :z)] - - (into (d/ordered-set) - (->> matching-shapes - (sort-by keyfn) - (map :id)))))) - -(defn create-mask-index - "Retrieves the mask information for an object" - [objects parents-index] - (let [retrieve-masks - (fn [id parents] - (->> parents - (map #(get objects %)) - (filter #(:masked-group? %)) - ;; Retrieve the masking element - (mapv #(get objects (->> % :shapes first)))))] - (->> parents-index - (d/mapm retrieve-masks)))) - -(defn- create-index - [objects] - (let [shapes (-> objects (dissoc uuid/zero) (vals)) - z-index (cp/calculate-z-index objects) - parents-index (cp/generate-child-all-parents-index objects) - masks-index (create-mask-index objects parents-index) - bounds (gsh/selection-rect shapes) - bounds #js {:x (:x bounds) - :y (:y bounds) - :width (:width bounds) - :height (:height bounds)}] - - (reduce (partial index-object objects z-index parents-index masks-index) - (qdt/create bounds) - shapes))) - -(defn- index-object - [objects z-index parents-index masks-index index obj] - (let [{:keys [x y width height]} (:selrect obj) - shape-bound #js {:x x :y y :width width :height height} - parents (get parents-index (:id obj)) - masks (get masks-index (:id obj)) - z (get z-index (:id obj)) - frame (when (and (not= :frame (:type obj)) - (not= (:frame-id obj) uuid/zero)) - (get objects (:frame-id obj)))] - (qdt/insert index - shape-bound - (assoc obj :frame frame :masks masks :parents parents :z z)))) + (query-index index rect frame-id include-frames? include-groups? disabled-masks reverse?))) diff --git a/frontend/src/app/worker/snaps.cljs b/frontend/src/app/worker/snaps.cljs index 9da49438b..9cd78cec0 100644 --- a/frontend/src/app/worker/snaps.cljs +++ b/frontend/src/app/worker/snaps.cljs @@ -6,48 +6,105 @@ (ns app.worker.snaps (:require - [okulary.core :as l] - [app.common.uuid :as uuid] - [app.common.pages :as cp] [app.common.data :as d] - [app.worker.impl :as impl] - [app.util.range-tree :as rt] + [app.common.pages :as cp] + [app.common.uuid :as uuid] + [app.util.geom.grid :as gg] [app.util.geom.snap-points :as snap] - [app.util.geom.grid :as gg])) + [app.util.range-tree :as rt] + [app.worker.impl :as impl] + [clojure.set :as set] + [okulary.core :as l])) (defonce state (l/atom {})) -(defn- create-coord-data - "Initializes the range tree given the shapes" - [frame-id shapes coord] - (let [process-shape (fn [coord] - (fn [shape] - (concat - (let [points (snap/shape-snap-points shape)] - (map #(vector % (:id shape)) points)) +(defn process-shape [frame-id coord] + (fn [shape] + (let [points (snap/shape-snap-points shape) + shape-data (->> points (mapv #(vector % (:id shape))))] + (if (= (:id shape) frame-id) + (d/concat + shape-data - ;; The grid points are only added by the "root" of the coord-dat - (when (= (:id shape) frame-id) - (let [points (gg/grid-snap-points shape coord)] - (map #(vector % :layout) points)))))) - into-tree (fn [tree [point _ :as data]] - (rt/insert tree (coord point) 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 coord)) - (reduce into-tree (rt/make-tree))))) + (mapcat (process-shape frame-id coord)) + (reduce into-tree data)))) + +(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 data)))) + +(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 [frame-shapes (->> (vals objects) - (filter :frame-id) - (group-by :frame-id)) - frame-shapes (->> (cp/select-frames objects) - (reduce #(update %1 (:id %2) conj %2) frame-shapes))] + (let [shapes-data (aggregate-data objects) - (d/mapm (fn [frame-id shapes] {:x (create-coord-data frame-id shapes :x) - :y (create-coord-data frame-id shapes :y)}) - frame-shapes))) + create-index + (fn [frame-id shapes] {:x (add-coord-data (rt/make-tree) frame-id shapes :x) + :y (add-coord-data (rt/make-tree) frame-id shapes :y)})] + + (d/mapm create-index shapes-data))) + +(defn- update-snap-data + [snap-data old-objects new-objects] + + (let [changed? #(not= (get old-objects %) (get new-objects %)) + + changed-ids (into #{} + (filter changed?) + (set/union (keys old-objects) (keys new-objects))) + + to-delete (aggregate-data old-objects changed-ids) + to-add (aggregate-data new-objects changed-ids) + + 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))) + + snap-data (->> to-delete + (reduce delete-data snap-data)) + + snap-data (->> to-add + (reduce add-data snap-data))] + + snap-data)) (defn- log-state "Helper function to print a friendly version of the snap tree. Debugging purposes" @@ -60,6 +117,16 @@ (let [snap-data (initialize-snap-data objects)] (assoc state page-id snap-data))) +(defn- update-page [state page-id old-objects new-objects] + (let [changed? #(not= (get old-objects %) (get new-objects %)) + changed-ids (into #{} + (filter changed?) + (set/union (keys old-objects) (keys new-objects))) + + 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 [file-id data] :as message}] @@ -74,9 +141,9 @@ nil)) (defmethod impl/handler :snaps/update-index - [{:keys [page-id objects] :as message}] + [{:keys [page-id old-objects new-objects] :as message}] ;; TODO: Check the difference and update the index acordingly - (swap! state index-page page-id objects) + (swap! state update-page page-id old-objects new-objects) ;; (log-state) nil)