From a7bdb02dbf99a5a18be15e56962a4f5190b724a7 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 6 May 2020 07:29:26 +0200 Subject: [PATCH 1/4] :sparkles: Adds range tree data structure --- frontend/src/uxbox/util/geom/snap.cljs | 10 +- frontend/src/uxbox/util/range_tree.js | 192 ++++++++++++++++++ .../tests/uxbox/test_util_range_tree.cljs | 140 +++++++++++++ 3 files changed, 337 insertions(+), 5 deletions(-) create mode 100644 frontend/src/uxbox/util/range_tree.js create mode 100644 frontend/tests/uxbox/test_util_range_tree.cljs diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index 2a2a0152c..b61e2b056 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -16,7 +16,7 @@ [uxbox.util.geom.shapes :as gsh] [uxbox.util.geom.point :as gpt])) -(def ^:private snap-accuracy 20) +(def ^:private snap-accuracy 10) (defn mapm "Map over the values of a map" @@ -111,7 +111,7 @@ (filter (fn [[_ data]] (not (empty? data)))))) (defn search-snap-point - "Search snap for a single point" + "Search snap for a single point in the `coord` given" [point coord snap-data filter-shapes] (let [coord-value (get point coord) @@ -134,7 +134,7 @@ (let [snap-points (mapcat #(search-snap-point % coord snap-data filter-shapes) points) result (->> snap-points (apply min-key first) second)] - (or result [0 0]))) + result)) (defn snap-frame-id [shapes] (let [frames (into #{} (map :frame-id shapes))] @@ -162,8 +162,8 @@ [snap-from-x snap-to-x] (search-snap shapes-points :x (get-in snap-data [frame-id :x]) remove-shapes) [snap-from-y snap-to-y] (search-snap shapes-points :y (get-in snap-data [frame-id :y]) remove-shapes) - snapv (gpt/to-vec (gpt/point snap-from-x snap-from-y) - (gpt/point snap-to-x snap-to-y))] + snapv (gpt/to-vec (gpt/point (or snap-from-x 0) (or snap-from-y 0)) + (gpt/point (or snap-to-x 0) (or snap-to-y 0)))] (gpt/add trans-vec snapv))) diff --git a/frontend/src/uxbox/util/range_tree.js b/frontend/src/uxbox/util/range_tree.js new file mode 100644 index 000000000..0b4f6d31b --- /dev/null +++ b/frontend/src/uxbox/util/range_tree.js @@ -0,0 +1,192 @@ +/* + * 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/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + * + * Copyright (c) 2020 UXBOX Labs SL + */ + +"use strict"; + +goog.provide("uxbox.util.range_tree"); +goog.require("cljs.core") + +goog.scope(function() { + const eq = cljs.core._EQ_; + const vec = cljs.core.vec; + const nil = cljs.core.nil; + + class Node { + constructor(value, data) { + this.value = value; + this.data = [ data ]; + this.left = null; + this.right = null; + } + } + + // Will store a map from key to list of data + // value => [ data ] + // The values can be queried in range and the data stored will be retrived whole + // but can be removed/updated individually using clojurescript equality + class RangeTree { + constructor() { + this.root = null; + } + + insert(value, data) { + this.root = recInsert(this.root, value, data); + return this; + } + + remove(value, data) { + this.root = recRemove(this.root, value, data); + return this; + } + + update (value, oldData, newData) { + this.root = recUpdate(this.root, value, oldData, newData); + return this; + } + + get(value) { + return recGet(this.root, value); + } + + rangeQuery (fromValue, toValue) { + return recRangeQuery(this.root, fromValue, toValue, []); + } + } + + // Tree implementation functions + + // Insert recursively in the tree + function recInsert (branch, value, data) { + if (branch === null) { + return new Node(value, data); + } else if (branch.value === value) { + // Find node we'll add to the end of the list + branch.data.push(data); + } else if (branch.value > value) { + // Target value is less than the current value we go left + branch.left = recInsert(branch.left, value, data); + } else if (branch.value < value) { + branch.right = recInsert(branch.right, value, data); + } + return branch; + } + + // Search for the min node + function searchMin(branch) { + if (branch.left === null) { + return branch; + } else { + return searchMin(branch.left); + } + } + + // Remove the lefmost node of the current branch + function recRemoveMin(branch) { + if (branch.left === null) { + return branch.right; + } else { + branch.left = recRemoveMin(branch.left); + return branch; + } + } + + // Remove the data element for the value given + function recRemove(branch, value, data) { + if (branch === null) { + // Not found + return branch; + } else if (branch.value === value) { + // Node found, we remove the data + branch.data = branch.data.filter ((it) => !eq(it, data)); + + if (branch.data.length > 0) { + return branch; + } + + // If the data is empty we need to remove the branch + if (branch.right === null) { + return branch.left; + } else if (branch.left === null) { + return branch.right; + } else { + const oldBranch = branch; + const newBranch = searchMin(branch.right); + newBranch.right = recRemoveMin(oldBranch.right); + newBranch.left = oldBranch.left; + return newBranch; + } + } else if (branch.value > value) { + // Target value is less than the current value we go left + branch.left = recRemove (branch.left, value, data); + return branch; + } else if (branch.value < value) { + branch.right = recRemove (branch.right, value, data); + return branch; + } + } + + // Retrieve all the data related to value + function recGet(branch, value) { + if (branch === null) { + return null; + } else if (branch.value === value) { + return branch.data; + } else if (branch.value > value) { + return recGet(branch.left, value); + } else if (branch.value < value) { + return recGet(branch.right, value); + } + } + + function recUpdate(branch, value, oldData, newData) { + if (branch === null) { + return branch; + } else if (branch.value === value) { + branch.data = branch.data.map((it) => (eq(it, oldData)) ? newData : it); + return branch; + } else if (branch.value > value) { + return recUpdate(branch.left, value, oldData, newData); + } else if (branch.value < value) { + return recUpdate(branch.right, value, oldData, newData); + } + } + + function recRangeQuery(branch, fromValue, toValue, result) { + if (branch === null) { + return result; + } + if (fromValue < branch.value) { + recRangeQuery(branch.left, fromValue, toValue, result); + } + if (fromValue <= branch.value && toValue >= branch.value) { + Array.prototype.push.apply(result, branch.data); + } + if (toValue > branch.value) { + recRangeQuery(branch.right, fromValue, toValue, result); + } + return result; + } + + // External API to CLJS + const self = uxbox.util.range_tree; + self.make_tree = () => new RangeTree(); + self.insert = (tree, value, data) => tree.insert(value, data); + self.remove = (tree, value, data) => tree.remove(value, data); + self.update = (tree, value, oldData, newData) => tree.update(value, oldData, newData); + self.get = (tree, value) => { + const result = tree.get(value); + if (!result) { + return nil; + } + return vec(result); + }; + self.range_query = (tree, from_value, to_value) => vec(tree.rangeQuery(from_value, to_value)); +}); diff --git a/frontend/tests/uxbox/test_util_range_tree.cljs b/frontend/tests/uxbox/test_util_range_tree.cljs new file mode 100644 index 000000000..2031c2af1 --- /dev/null +++ b/frontend/tests/uxbox/test_util_range_tree.cljs @@ -0,0 +1,140 @@ +(ns uxbox.test-util-range-tree + (:require [cljs.test :as t :include-macros true] + [cljs.pprint :refer [pprint]] + [uxbox.util.geom.point :as gpt] + [uxbox.util.range-tree :as rt])) + +(defn check-max-height [tree num-nodes]) +(defn check-sorted [tree]) + +(defn create-random-tree [num-nodes]) + +(t/deftest test-insert-and-retrive-data + (t/testing "Retrieve on empty tree" + (let [tree (rt/make-tree)] + (t/is (= (rt/get tree 100) nil)))) + + (t/testing "First insert/retrieval" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 200) nil)))) + + (t/testing "Insert best case scenario" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 200 :c))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 50) [:b])) + (t/is (= (rt/get tree 200) [:c])))) + + (t/testing "Insert duplicate entry" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 200 :c) + (rt/insert 50 :d) + (rt/insert 200 :e))] + (t/is (= (rt/get tree 100) [:a])) + (t/is (= (rt/get tree 50) [:b :d])) + (t/is (= (rt/get tree 200) [:c :e]))))) + +(t/deftest test-remove-elements + (t/testing "Insert and delete data but not the node" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 100 :b) + (rt/insert 100 :c) + (rt/remove 100 :b))] + (t/is (= (rt/get tree 100) [:a :c])))) + + (t/testing "Try to delete data not in the node is noop" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 100 :b) + (rt/insert 100 :c) + (rt/remove 100 :xx))] + (t/is (= (rt/get tree 100) [:a :b :c])))) + + (t/testing "Delete data and node" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 200 :b) + (rt/insert 300 :c) + (rt/remove 200 :b))] + (t/is (= (rt/get tree 200) nil)))) + + (t/testing "Delete root node the new tree should be correct" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 25 :d) + (rt/insert 75 :e) + (rt/insert 125 :f) + (rt/insert 175 :g) + (rt/remove 100 :a))] + + (t/is (= (rt/get tree 100) nil)) + (t/is (= (rt/get tree 50) [:b])) + (t/is (= (rt/get tree 150) [:c])) + (t/is (= (rt/get tree 25) [:d])) + (t/is (= (rt/get tree 75) [:e])) + (t/is (= (rt/get tree 125) [:f])) + (t/is (= (rt/get tree 175) [:g]))))) + +(t/deftest test-update-elements + (t/testing "Updates an element" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 50 :d) + (rt/insert 50 :e) + (rt/update 50 :d :xx))] + (t/is (= (rt/get tree 50) [:b :xx :e])))) + + (t/testing "Try to update non-existing element" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 50 :d) + (rt/insert 50 :e) + (rt/update 50 :zz :xx))] + (t/is (= (rt/get tree 50) [:b :d :e]))))) + +(t/deftest test-range-query + (t/testing "Creates a tree and test different range queries" + (let [tree (-> (rt/make-tree) + (rt/insert 0 :a) + (rt/insert 25 :b) + (rt/insert 50 :c) + (rt/insert 75 :d) + (rt/insert 100 :e) + (rt/insert 100 :f) + (rt/insert 125 :g) + (rt/insert 150 :h) + (rt/insert 175 :i) + (rt/insert 200 :j) + (rt/insert 200 :k))] + (t/is (= (rt/range-query tree 0 200) [:a :b :c :d :e :f :g :h :i :j :k])) + (t/is (= (rt/range-query tree 0 100) [:a :b :c :d :e :f])) + (t/is (= (rt/range-query tree 100 200) [:e :f :g :h :i :j :k])) + (t/is (= (rt/range-query tree 10 60) [:b :c])) + (t/is (= (rt/range-query tree 199.5 200.5) [:j :k])))) + + (t/testing "Empty range query" + (let [tree (-> (rt/make-tree) + (rt/insert 100 :a) + (rt/insert 50 :b) + (rt/insert 150 :c) + (rt/insert 25 :d) + (rt/insert 75 :e) + (rt/insert 125 :f) + (rt/insert 175 :g))] + (t/is (= (rt/range-query tree -100 0) [])) + (t/is (= (rt/range-query tree 200 300) [])) + (t/is (= (rt/range-query tree 200 0) []))))) + From e596555932ff5f3280f1bf3119e3cbaa13c18260 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 6 May 2020 13:55:32 +0200 Subject: [PATCH 2/4] :sparkles: Range tree implemented as Red-Black tree --- frontend/src/uxbox/util/range_tree.js | 208 +++++++++++++++--- .../tests/uxbox/test_util_range_tree.cljs | 15 +- 2 files changed, 195 insertions(+), 28 deletions(-) diff --git a/frontend/src/uxbox/util/range_tree.js b/frontend/src/uxbox/util/range_tree.js index 0b4f6d31b..81626c5f3 100644 --- a/frontend/src/uxbox/util/range_tree.js +++ b/frontend/src/uxbox/util/range_tree.js @@ -9,6 +9,10 @@ * Copyright (c) 2020 UXBOX Labs SL */ +/* + * Balanced Binary Search Tree based on the red-black BST + * described at "Algorithms" by Robert Sedwick & Kevin Wayne + */ "use strict"; goog.provide("uxbox.util.range_tree"); @@ -18,6 +22,11 @@ goog.scope(function() { const eq = cljs.core._EQ_; const vec = cljs.core.vec; const nil = cljs.core.nil; + + const Color = { + RED: "RED", + BLACK: "BLACK" + } class Node { constructor(value, data) { @@ -25,6 +34,7 @@ goog.scope(function() { this.data = [ data ]; this.left = null; this.right = null; + this.color = Color.BLACK; } } @@ -39,11 +49,31 @@ goog.scope(function() { insert(value, data) { this.root = recInsert(this.root, value, data); + this.root.color = Color.BLACK; return this; } remove(value, data) { - this.root = recRemove(this.root, value, data); + if (!this.root) { + return this; + } + + this.root = recRemoveData(this.root, value, data); + + const newData = recGet(this.root, value); + + if (newData && newData.length === 0) { + if (!isRed(this.root.left) && !isRed(this.root.right)) { + this.root.color = Color.RED; + } + + this.root = recRemoveNode(this.root, value); + + if (this.root) { + this.root.color = Color.BLACK; + } + } + return this; } @@ -59,14 +89,28 @@ goog.scope(function() { rangeQuery (fromValue, toValue) { return recRangeQuery(this.root, fromValue, toValue, []); } + + height() { + return recHeight(this.root); + } + + isEmpty() { + return this.root === null; + } } // Tree implementation functions + function isRed(branch) { + return branch !== null && branch.color === Color.RED; + } + // Insert recursively in the tree function recInsert (branch, value, data) { if (branch === null) { - return new Node(value, data); + const ret = new Node(value, data); + ret.color = Color.RED; + return ret; } else if (branch.value === value) { // Find node we'll add to the end of the list branch.data.push(data); @@ -76,6 +120,16 @@ goog.scope(function() { } else if (branch.value < value) { branch.right = recInsert(branch.right, value, data); } + + if (isRed(branch.right) && !isRed(branch.left)) { + branch = rotateLeft(branch); + } + if (isRed(branch.left) && isRed(branch.left.left)) { + branch = rotateRight(branch); + } + if (isRed(branch.left) && isRed(branch.right)) { + flipColors(branch); + } return branch; } @@ -91,48 +145,64 @@ goog.scope(function() { // Remove the lefmost node of the current branch function recRemoveMin(branch) { if (branch.left === null) { - return branch.right; - } else { - branch.left = recRemoveMin(branch.left); - return branch; + return null; } + + if (!isRed(branch.left) && !isRed(branch.left.left)) { + branch = moveRedLeft(branch); + } + branch.left = recRemoveMin(branch.left); + return balance(branch); } // Remove the data element for the value given - function recRemove(branch, value, data) { + // this will not remove the node, we have to remove the empty node afterwards + function recRemoveData(branch, value, data) { if (branch === null) { // Not found return branch; } else if (branch.value === value) { // Node found, we remove the data branch.data = branch.data.filter ((it) => !eq(it, data)); - - if (branch.data.length > 0) { - return branch; - } - - // If the data is empty we need to remove the branch - if (branch.right === null) { - return branch.left; - } else if (branch.left === null) { - return branch.right; - } else { - const oldBranch = branch; - const newBranch = searchMin(branch.right); - newBranch.right = recRemoveMin(oldBranch.right); - newBranch.left = oldBranch.left; - return newBranch; - } + return branch; } else if (branch.value > value) { - // Target value is less than the current value we go left - branch.left = recRemove (branch.left, value, data); + branch.left = recRemoveData (branch.left, value, data); return branch; } else if (branch.value < value) { - branch.right = recRemove (branch.right, value, data); + branch.right = recRemoveData(branch.right, value, data); return branch; } } + function recRemoveNode(branch, value) { + if (value < branch.value) { + if (!isRed(branch.left) && !isRed(branch.left.left)) { + branch = moveRedLeft(branch); + } + branch.left = recRemoveNode(branch.left, value); + } else { + if (isRed(branch.left)) { + branch = rotateRight(branch); + } + if (value === branch.value && branch.right === null) { + return null; + } + if (!isRed(branch.right) && !isRed(branch.right.left)) { + branch = moveRedRight(branch); + } + + if (value === branch.value) { + const x = searchMin(branch.right); + branch.value = x.value; + branch.data = x.data; + branch.right = recRemoveMin(branch.right); + } else { + branch.right = recRemoveNode(branch.right, value); + } + } + return balance(branch); + } + // Retrieve all the data related to value function recGet(branch, value) { if (branch === null) { @@ -175,6 +245,86 @@ goog.scope(function() { return result; } + function rotateLeft(branch) { + const x = branch.right; + branch.right = x.left; + x.left = branch; + x.color = x.left.color; + x.left.color = Color.RED; + return x; + } + + function rotateRight(branch) { + const x = branch.left; + branch.left = x.right; + x.right = branch; + x.color = x.right.color; + x.right.color = Color.RED; + return x; + } + + function balance(branch) { + if (isRed(branch.right)) { + branch = rotateLeft(branch); + } + if (isRed(branch.left) && isRed(branch.left.left)) { + branch = rotateRight(branch); + } + if (isRed(branch.left) && isRed(branch.right)) { + flipColors(branch); + } + return branch; + } + + function moveRedLeft(branch) { + flipColors(branch); + if (isRed(branch.right.left)) { + branch.right = rotateRight(branch.right); + branch = rotateLeft(branch); + flipColors(branch); + } + return branch; + } + + function moveRedRight(branch) { + flipColors(branch); + if (branch.left && isRed(branch.left.left)) { + branch = rotateRight(branch); + flipColors(branch); + } + return branch; + } + + function flip(color) { + return color === Color.RED ? Color.BLACK : Color.RED; + } + + function flipColors(branch) { + branch.color = flip(branch.color); + if (branch.left) { + branch.left.color = flip(branch.left.color); + } + if (branch.right) { + branch.right.color = flip(branch.right.color); + } + } + + function recHeight(branch) { + let curHeight = 0; + if (branch !== null) { + curHeight = Math.max(recHeight(branch.left), recHeight(branch.right)) + } + return 1 + curHeight; + } + + function printTree(tree) { + if (!tree) { + return ""; + } + const val = tree.color[0] + "(" + tree.value + ")"; + return "[" + printTree(tree.left) + " " + val + " " + printTree(tree.right) + "]"; + } + // External API to CLJS const self = uxbox.util.range_tree; self.make_tree = () => new RangeTree(); @@ -189,4 +339,8 @@ goog.scope(function() { return vec(result); }; self.range_query = (tree, from_value, to_value) => vec(tree.rangeQuery(from_value, to_value)); + self.empty_QMARK_ = (tree) => tree.isEmpty(); + self.height = (tree) => tree.height(); + self.print = (tree) => printTree(tree.root); }); + diff --git a/frontend/tests/uxbox/test_util_range_tree.cljs b/frontend/tests/uxbox/test_util_range_tree.cljs index 2031c2af1..eec8fd187 100644 --- a/frontend/tests/uxbox/test_util_range_tree.cljs +++ b/frontend/tests/uxbox/test_util_range_tree.cljs @@ -82,7 +82,14 @@ (t/is (= (rt/get tree 25) [:d])) (t/is (= (rt/get tree 75) [:e])) (t/is (= (rt/get tree 125) [:f])) - (t/is (= (rt/get tree 175) [:g]))))) + (t/is (= (rt/get tree 175) [:g])))) + + (t/testing "Adds a bunch of nodes and then delete. The tree should be empty" + (let [size 10000 + tree (rt/make-tree) + tree (reduce #(rt/insert %1 %2 :x) tree (range 0 (dec size))) + tree (reduce #(rt/remove %1 %2 :x) tree (range 0 (dec size)))] + (t/is (rt/empty? tree))))) (t/deftest test-update-elements (t/testing "Updates an element" @@ -138,3 +145,9 @@ (t/is (= (rt/range-query tree 200 300) [])) (t/is (= (rt/range-query tree 200 0) []))))) +(t/deftest test-balanced-tree + (t/testing "Creates a worst-case BST and probes for a balanced height" + (let [size 1024 + tree (reduce #(rt/insert %1 %2 :x) (rt/make-tree) (range 0 (dec size))) + height (rt/height tree)] + (t/is (= height (inc (js/Math.log2 size))))))) From dc97056fcfc20eda70973c085b104a4a424e776e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 7 May 2020 11:26:50 +0200 Subject: [PATCH 3/4] :sparkles: Integration of the range-tree into the application --- frontend/src/uxbox/main/data/workspace.cljs | 3 +- frontend/src/uxbox/util/geom/snap.cljs | 31 +++++------ frontend/src/uxbox/util/range_tree.js | 26 ++++++++-- .../tests/uxbox/test_util_range_tree.cljs | 51 +++++++++++++++++-- 4 files changed, 84 insertions(+), 27 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index fa4b87005..80dab0176 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -494,9 +494,10 @@ name (generate-unique-name names (:name obj)) renamed-obj (assoc obj :id id :name name) moved-obj (geom/move renamed-obj delta) + frames (cp/select-frames objects) frame-id (if frame-id frame-id - (dwc/calculate-frame-overlap objects moved-obj)) + (dwc/calculate-frame-overlap frames moved-obj)) parent-id (or parent-id frame-id) diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index b61e2b056..0a882d093 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -14,7 +14,8 @@ [uxbox.util.math :as mth] [uxbox.common.uuid :refer [zero]] [uxbox.util.geom.shapes :as gsh] - [uxbox.util.geom.point :as gpt])) + [uxbox.util.geom.point :as gpt] + [uxbox.util.range-tree :as rt])) (def ^:private snap-accuracy 10) @@ -73,14 +74,15 @@ (into #{shape-center} (-> modified-path :segments))))) (defn create-coord-data [shapes coord] - (let [process-shape - (fn [coord] - (fn [shape] - (let [points (shape-snap-points shape)] - (map #(vector % (:id shape)) points))))] + (let [process-shape (fn [coord] + (fn [shape] + (let [points (shape-snap-points shape)] + (map #(vector % (:id shape)) points)))) + into-tree (fn [tree [point _ :as data]] + (rt/insert tree (coord point) data))] (->> shapes (mapcat (process-shape coord)) - (group-by (comp coord first))))) + (reduce into-tree (rt/make-tree))))) (defn initialize-snap-data "Initialize the snap information with the current workspace information" @@ -98,13 +100,6 @@ :y (create-coord-data shapes :y)}) frame-shapes))) -(defn range-query - "Queries the snap-data within a range of values" - [snap-data from-value to-value] - (filter (fn [[value _]] (and (>= value from-value) - (<= value to-value))) - snap-data)) - (defn remove-from-snap-points [snap-points ids-to-remove] (->> snap-points (map (fn [[value data]] [value (remove (comp ids-to-remove second) data)])) @@ -119,7 +114,7 @@ ;; This gives a list of [value [[point1 uuid1] [point2 uuid2] ...] we need to remove ;; the shapes in filter shapes candidates (-> snap-data - (range-query (- coord-value snap-accuracy) (+ coord-value snap-accuracy)) + (rt/range-query (- coord-value snap-accuracy) (+ coord-value snap-accuracy)) (remove-from-snap-points filter-shapes)) ;; Now return with the distance and the from-to pair that we'll return if this is the chosen @@ -187,8 +182,8 @@ ;; Search for values within 1 pixel snap-matches (-> (get-in snap-data [frame-id coord]) - (range-query (- value 1) (+ value 1)) - (remove-from-snap-points filter-shapes)) + (rt/range-query (- value 1) (+ value 1)) + (remove-from-snap-points filter-shapes)) snap-points (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) snap-matches)] snap-points)) @@ -196,5 +191,5 @@ (defn is-snapping? [snap-data frame-id shape-id point coord] (let [value (coord point) ;; Search for values within 1 pixel - snap-points (range-query (get-in snap-data [frame-id coord]) (- value 1.0) (+ value 1.0))] + snap-points (rt/range-query (get-in snap-data [frame-id coord]) (- value 1.0) (+ value 1.0))] (some (fn [[point other-shape-id]] (not (= shape-id other-shape-id))) snap-points))) diff --git a/frontend/src/uxbox/util/range_tree.js b/frontend/src/uxbox/util/range_tree.js index 81626c5f3..5910e3492 100644 --- a/frontend/src/uxbox/util/range_tree.js +++ b/frontend/src/uxbox/util/range_tree.js @@ -24,8 +24,8 @@ goog.scope(function() { const nil = cljs.core.nil; const Color = { - RED: "RED", - BLACK: "BLACK" + RED: 1, + BLACK: 2 } class Node { @@ -97,6 +97,12 @@ goog.scope(function() { isEmpty() { return this.root === null; } + + toString() { + const result = []; + recToString(this.root, result); + return result.join(", "); + } } // Tree implementation functions @@ -237,7 +243,8 @@ goog.scope(function() { recRangeQuery(branch.left, fromValue, toValue, result); } if (fromValue <= branch.value && toValue >= branch.value) { - Array.prototype.push.apply(result, branch.data); + // Array.prototype.push.apply(result, branch.data); + result.push(vec([branch.value, vec(branch.data)])) } if (toValue > branch.value) { recRangeQuery(branch.right, fromValue, toValue, result); @@ -317,6 +324,19 @@ goog.scope(function() { return 1 + curHeight; } + // This will return the string representation. We don't care about internal structure + // only the data + function recToString(branch, result) { + if (branch === null) { + return; + } + + recToString(branch.left, result); + result.push(`${branch.value}: [${branch.data.join(", ")}]`) + recToString(branch.right, result); + } + + // This function prints the tree structure, not the data function printTree(tree) { if (!tree) { return ""; diff --git a/frontend/tests/uxbox/test_util_range_tree.cljs b/frontend/tests/uxbox/test_util_range_tree.cljs index eec8fd187..56769b677 100644 --- a/frontend/tests/uxbox/test_util_range_tree.cljs +++ b/frontend/tests/uxbox/test_util_range_tree.cljs @@ -85,10 +85,18 @@ (t/is (= (rt/get tree 175) [:g])))) (t/testing "Adds a bunch of nodes and then delete. The tree should be empty" + ;; Try an increase range (let [size 10000 tree (rt/make-tree) tree (reduce #(rt/insert %1 %2 :x) tree (range 0 (dec size))) tree (reduce #(rt/remove %1 %2 :x) tree (range 0 (dec size)))] + (t/is (rt/empty? tree))) + + ;; Try a decreasing range + (let [size 10000 + tree (rt/make-tree) + tree (reduce #(rt/insert %1 %2 :x) tree (range (dec size) -1 -1)) + tree (reduce #(rt/remove %1 %2 :x) tree (range (dec size) -1 -1))] (t/is (rt/empty? tree))))) (t/deftest test-update-elements @@ -126,11 +134,33 @@ (rt/insert 175 :i) (rt/insert 200 :j) (rt/insert 200 :k))] - (t/is (= (rt/range-query tree 0 200) [:a :b :c :d :e :f :g :h :i :j :k])) - (t/is (= (rt/range-query tree 0 100) [:a :b :c :d :e :f])) - (t/is (= (rt/range-query tree 100 200) [:e :f :g :h :i :j :k])) - (t/is (= (rt/range-query tree 10 60) [:b :c])) - (t/is (= (rt/range-query tree 199.5 200.5) [:j :k])))) + (t/is (= (rt/range-query tree 0 200) + [[0 [:a]] + [25 [:b]] + [50 [:c]] + [75 [:d]] + [100 [:e :f]] + [125 [:g]] + [150 [:h]] + [175 [:i]] + [200 [:j :k]]])) + (t/is (= (rt/range-query tree 0 100) + [[0 [:a]] + [25 [:b]] + [50 [:c]] + [75 [:d]] + [100 [:e :f]]])) + (t/is (= (rt/range-query tree 100 200) + [[100 [:e :f]] + [125 [:g]] + [150 [:h]] + [175 [:i]] + [200 [:j :k]]])) + (t/is (= (rt/range-query tree 10 60) + [[25 [:b]] + [50 [:c]]])) + (t/is (= (rt/range-query tree 199.5 200.5) + [[200 [:j :k]]])))) (t/testing "Empty range query" (let [tree (-> (rt/make-tree) @@ -151,3 +181,14 @@ tree (reduce #(rt/insert %1 %2 :x) (rt/make-tree) (range 0 (dec size))) height (rt/height tree)] (t/is (= height (inc (js/Math.log2 size))))))) + +(t/deftest test-to-string + (t/testing "Creates a tree and prints it" + (let [tree (-> (rt/make-tree) + (rt/insert 50 :a) + (rt/insert 25 :b) + (rt/insert 25 :c) + (rt/insert 100 :d) + (rt/insert 75 :e)) + result (str tree)] + (t/is (= result "25: [:b, :c], 50: [:a], 75: [:e], 100: [:d]"))))) From b8d30466bb03e92d6aecadd81185b42ae18383a9 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 7 May 2020 12:22:39 +0200 Subject: [PATCH 4/4] :bug: Fixes problems with snaps --- frontend/src/uxbox/main/ui/workspace/drawarea.cljs | 2 +- frontend/src/uxbox/util/geom/snap.cljs | 2 +- frontend/src/uxbox/util/range_tree.js | 7 ++++++- frontend/tests/uxbox/test_util_range_tree.cljs | 5 ++++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs index ab6402424..d5d088019 100644 --- a/frontend/src/uxbox/main/ui/workspace/drawarea.cljs +++ b/frontend/src/uxbox/main/ui/workspace/drawarea.cljs @@ -167,7 +167,7 @@ (rx/concat (->> mouse (rx/take 1) - (rx/map (fn [pt] #(initialize-drawing % pt frame-id)))) + (rx/map (fn [pt] #(initialize-drawing % pt (or frame-id uuid/zero))))) (->> mouse (rx/with-latest vector ms/mouse-position-ctrl) (rx/map (fn [[pt ctrl?]] #(update-drawing % initial snap-data pt ctrl?))) diff --git a/frontend/src/uxbox/util/geom/snap.cljs b/frontend/src/uxbox/util/geom/snap.cljs index 0a882d093..90ce9afcb 100644 --- a/frontend/src/uxbox/util/geom/snap.cljs +++ b/frontend/src/uxbox/util/geom/snap.cljs @@ -69,7 +69,7 @@ (let [modified-path (gsh/transform-apply-modifiers shape) shape-center (gsh/center modified-path)] (case (:type shape) - :frame (frame-snap-points shape) + :frame (-> modified-path gsh/shape->rect-shape frame-snap-points) (:path :curve) (into #{shape-center} (-> modified-path gsh/shape->rect-shape :segments)) (into #{shape-center} (-> modified-path :segments))))) diff --git a/frontend/src/uxbox/util/range_tree.js b/frontend/src/uxbox/util/range_tree.js index 5910e3492..bc16a6a8f 100644 --- a/frontend/src/uxbox/util/range_tree.js +++ b/frontend/src/uxbox/util/range_tree.js @@ -358,7 +358,12 @@ goog.scope(function() { } return vec(result); }; - self.range_query = (tree, from_value, to_value) => vec(tree.rangeQuery(from_value, to_value)); + self.range_query = (tree, from_value, to_value) => { + if (!tree) { + return vec(); + } + return vec(tree.rangeQuery(from_value, to_value)) + }; self.empty_QMARK_ = (tree) => tree.isEmpty(); self.height = (tree) => tree.height(); self.print = (tree) => printTree(tree.root); diff --git a/frontend/tests/uxbox/test_util_range_tree.cljs b/frontend/tests/uxbox/test_util_range_tree.cljs index 56769b677..ba0c82ac4 100644 --- a/frontend/tests/uxbox/test_util_range_tree.cljs +++ b/frontend/tests/uxbox/test_util_range_tree.cljs @@ -173,7 +173,10 @@ (rt/insert 175 :g))] (t/is (= (rt/range-query tree -100 0) [])) (t/is (= (rt/range-query tree 200 300) [])) - (t/is (= (rt/range-query tree 200 0) []))))) + (t/is (= (rt/range-query tree 200 0) [])))) + + (t/testing "Range query over null should return empty" + (t/is (= (rt/range-query nil 0 100) [])))) (t/deftest test-balanced-tree (t/testing "Creates a worst-case BST and probes for a balanced height"