mirror of
https://github.com/penpot/penpot.git
synced 2025-06-06 20:41:37 +02: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));
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue