0
Fork 0
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:
alonso.torres 2020-05-06 07:29:26 +02:00
parent 02e438eb28
commit a7bdb02dbf
3 changed files with 337 additions and 5 deletions

View file

@ -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)))

View 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));
});

View 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) [])))))