mirror of
https://github.com/penpot/penpot.git
synced 2025-01-09 08:20:45 -05:00
✨ Adds range tree data structure
This commit is contained in:
parent
02e438eb28
commit
a7bdb02dbf
3 changed files with 337 additions and 5 deletions
|
@ -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)))
|
||||
|
||||
|
|
192
frontend/src/uxbox/util/range_tree.js
Normal file
192
frontend/src/uxbox/util/range_tree.js
Normal file
|
@ -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));
|
||||
});
|
140
frontend/tests/uxbox/test_util_range_tree.cljs
Normal file
140
frontend/tests/uxbox/test_util_range_tree.cljs
Normal file
|
@ -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) [])))))
|
||||
|
Loading…
Reference in a new issue