mirror of
https://github.com/penpot/penpot.git
synced 2025-06-04 03:11:39 +02:00
Merge pull request #199 from uxbox/201/dynamic_alignment
Dynamic Alignment - Range tree and some fixes
This commit is contained in:
commit
b7815f137d
5 changed files with 590 additions and 26 deletions
|
@ -494,9 +494,10 @@
|
||||||
name (generate-unique-name names (:name obj))
|
name (generate-unique-name names (:name obj))
|
||||||
renamed-obj (assoc obj :id id :name name)
|
renamed-obj (assoc obj :id id :name name)
|
||||||
moved-obj (geom/move renamed-obj delta)
|
moved-obj (geom/move renamed-obj delta)
|
||||||
|
frames (cp/select-frames objects)
|
||||||
frame-id (if frame-id
|
frame-id (if frame-id
|
||||||
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)
|
parent-id (or parent-id frame-id)
|
||||||
|
|
||||||
|
|
|
@ -167,7 +167,7 @@
|
||||||
(rx/concat
|
(rx/concat
|
||||||
(->> mouse
|
(->> mouse
|
||||||
(rx/take 1)
|
(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
|
(->> mouse
|
||||||
(rx/with-latest vector ms/mouse-position-ctrl)
|
(rx/with-latest vector ms/mouse-position-ctrl)
|
||||||
(rx/map (fn [[pt ctrl?]] #(update-drawing % initial snap-data pt ctrl?)))
|
(rx/map (fn [[pt ctrl?]] #(update-drawing % initial snap-data pt ctrl?)))
|
||||||
|
|
|
@ -14,9 +14,10 @@
|
||||||
[uxbox.util.math :as mth]
|
[uxbox.util.math :as mth]
|
||||||
[uxbox.common.uuid :refer [zero]]
|
[uxbox.common.uuid :refer [zero]]
|
||||||
[uxbox.util.geom.shapes :as gsh]
|
[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 20)
|
(def ^:private snap-accuracy 10)
|
||||||
|
|
||||||
(defn mapm
|
(defn mapm
|
||||||
"Map over the values of a map"
|
"Map over the values of a map"
|
||||||
|
@ -68,19 +69,20 @@
|
||||||
(let [modified-path (gsh/transform-apply-modifiers shape)
|
(let [modified-path (gsh/transform-apply-modifiers shape)
|
||||||
shape-center (gsh/center modified-path)]
|
shape-center (gsh/center modified-path)]
|
||||||
(case (:type shape)
|
(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))
|
(:path :curve) (into #{shape-center} (-> modified-path gsh/shape->rect-shape :segments))
|
||||||
(into #{shape-center} (-> modified-path :segments)))))
|
(into #{shape-center} (-> modified-path :segments)))))
|
||||||
|
|
||||||
(defn create-coord-data [shapes coord]
|
(defn create-coord-data [shapes coord]
|
||||||
(let [process-shape
|
(let [process-shape (fn [coord]
|
||||||
(fn [coord]
|
(fn [shape]
|
||||||
(fn [shape]
|
(let [points (shape-snap-points shape)]
|
||||||
(let [points (shape-snap-points shape)]
|
(map #(vector % (:id shape)) points))))
|
||||||
(map #(vector % (:id shape)) points))))]
|
into-tree (fn [tree [point _ :as data]]
|
||||||
|
(rt/insert tree (coord point) data))]
|
||||||
(->> shapes
|
(->> shapes
|
||||||
(mapcat (process-shape coord))
|
(mapcat (process-shape coord))
|
||||||
(group-by (comp coord first)))))
|
(reduce into-tree (rt/make-tree)))))
|
||||||
|
|
||||||
(defn initialize-snap-data
|
(defn initialize-snap-data
|
||||||
"Initialize the snap information with the current workspace information"
|
"Initialize the snap information with the current workspace information"
|
||||||
|
@ -98,20 +100,13 @@
|
||||||
:y (create-coord-data shapes :y)})
|
:y (create-coord-data shapes :y)})
|
||||||
frame-shapes)))
|
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]
|
(defn remove-from-snap-points [snap-points ids-to-remove]
|
||||||
(->> snap-points
|
(->> snap-points
|
||||||
(map (fn [[value data]] [value (remove (comp ids-to-remove second) data)]))
|
(map (fn [[value data]] [value (remove (comp ids-to-remove second) data)]))
|
||||||
(filter (fn [[_ data]] (not (empty? data))))))
|
(filter (fn [[_ data]] (not (empty? data))))))
|
||||||
|
|
||||||
(defn search-snap-point
|
(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]
|
[point coord snap-data filter-shapes]
|
||||||
|
|
||||||
(let [coord-value (get point coord)
|
(let [coord-value (get point coord)
|
||||||
|
@ -119,7 +114,7 @@
|
||||||
;; This gives a list of [value [[point1 uuid1] [point2 uuid2] ...] we need to remove
|
;; This gives a list of [value [[point1 uuid1] [point2 uuid2] ...] we need to remove
|
||||||
;; the shapes in filter shapes
|
;; the shapes in filter shapes
|
||||||
candidates (-> snap-data
|
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))
|
(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
|
;; Now return with the distance and the from-to pair that we'll return if this is the chosen
|
||||||
|
@ -134,7 +129,7 @@
|
||||||
|
|
||||||
(let [snap-points (mapcat #(search-snap-point % coord snap-data filter-shapes) points)
|
(let [snap-points (mapcat #(search-snap-point % coord snap-data filter-shapes) points)
|
||||||
result (->> snap-points (apply min-key first) second)]
|
result (->> snap-points (apply min-key first) second)]
|
||||||
(or result [0 0])))
|
result))
|
||||||
|
|
||||||
(defn snap-frame-id [shapes]
|
(defn snap-frame-id [shapes]
|
||||||
(let [frames (into #{} (map :frame-id shapes))]
|
(let [frames (into #{} (map :frame-id shapes))]
|
||||||
|
@ -162,8 +157,8 @@
|
||||||
[snap-from-x snap-to-x] (search-snap shapes-points :x (get-in snap-data [frame-id :x]) remove-shapes)
|
[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)
|
[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)
|
snapv (gpt/to-vec (gpt/point (or snap-from-x 0) (or snap-from-y 0))
|
||||||
(gpt/point snap-to-x snap-to-y))]
|
(gpt/point (or snap-to-x 0) (or snap-to-y 0)))]
|
||||||
|
|
||||||
(gpt/add trans-vec snapv)))
|
(gpt/add trans-vec snapv)))
|
||||||
|
|
||||||
|
@ -187,8 +182,8 @@
|
||||||
|
|
||||||
;; Search for values within 1 pixel
|
;; Search for values within 1 pixel
|
||||||
snap-matches (-> (get-in snap-data [frame-id coord])
|
snap-matches (-> (get-in snap-data [frame-id coord])
|
||||||
(range-query (- value 1) (+ value 1))
|
(rt/range-query (- value 1) (+ value 1))
|
||||||
(remove-from-snap-points filter-shapes))
|
(remove-from-snap-points filter-shapes))
|
||||||
|
|
||||||
snap-points (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) snap-matches)]
|
snap-points (mapcat (fn [[v data]] (map (fn [[point _]] point) data)) snap-matches)]
|
||||||
snap-points))
|
snap-points))
|
||||||
|
@ -196,5 +191,5 @@
|
||||||
(defn is-snapping? [snap-data frame-id shape-id point coord]
|
(defn is-snapping? [snap-data frame-id shape-id point coord]
|
||||||
(let [value (coord point)
|
(let [value (coord point)
|
||||||
;; Search for values within 1 pixel
|
;; 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)))
|
(some (fn [[point other-shape-id]] (not (= shape-id other-shape-id))) snap-points)))
|
||||||
|
|
371
frontend/src/uxbox/util/range_tree.js
Normal file
371
frontend/src/uxbox/util/range_tree.js
Normal file
|
@ -0,0 +1,371 @@
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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");
|
||||||
|
goog.require("cljs.core")
|
||||||
|
|
||||||
|
goog.scope(function() {
|
||||||
|
const eq = cljs.core._EQ_;
|
||||||
|
const vec = cljs.core.vec;
|
||||||
|
const nil = cljs.core.nil;
|
||||||
|
|
||||||
|
const Color = {
|
||||||
|
RED: 1,
|
||||||
|
BLACK: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
class Node {
|
||||||
|
constructor(value, data) {
|
||||||
|
this.value = value;
|
||||||
|
this.data = [ data ];
|
||||||
|
this.left = null;
|
||||||
|
this.right = null;
|
||||||
|
this.color = Color.BLACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
this.root.color = Color.BLACK;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
height() {
|
||||||
|
return recHeight(this.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
isEmpty() {
|
||||||
|
return this.root === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
const result = [];
|
||||||
|
recToString(this.root, result);
|
||||||
|
return result.join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
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);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 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
|
||||||
|
// 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));
|
||||||
|
return branch;
|
||||||
|
} else if (branch.value > value) {
|
||||||
|
branch.left = recRemoveData (branch.left, value, data);
|
||||||
|
return branch;
|
||||||
|
} else if (branch.value < value) {
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
result.push(vec([branch.value, vec(branch.data)]))
|
||||||
|
}
|
||||||
|
if (toValue > branch.value) {
|
||||||
|
recRangeQuery(branch.right, fromValue, toValue, result);
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 "";
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
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) => {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
197
frontend/tests/uxbox/test_util_range_tree.cljs
Normal file
197
frontend/tests/uxbox/test_util_range_tree.cljs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
(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/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
|
||||||
|
(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)
|
||||||
|
[[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)
|
||||||
|
(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) []))))
|
||||||
|
|
||||||
|
(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"
|
||||||
|
(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)))))))
|
||||||
|
|
||||||
|
(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]")))))
|
Loading…
Add table
Add a link
Reference in a new issue