sigma
Version:
A JavaScript library dedicated to graph drawing.
327 lines (326 loc) • 13.9 kB
JavaScript
"use strict";
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.rectangleCollidesWithQuad = exports.squareCollidesWithQuad = exports.getCircumscribedAlignedRectangle = exports.isRectangleAligned = void 0;
/**
* Sigma.js Quad Tree Class
* =========================
*
* Class implementing the quad tree data structure used to solve hovers and
* determine which elements are currently in the scope of the camera so that
* we don't waste time rendering things the user cannot see anyway.
* @module
*/
/* eslint no-nested-ternary: 0 */
/* eslint no-constant-condition: 0 */
var extend_1 = __importDefault(require("@yomguithereal/helpers/extend"));
// TODO: should not ask the quadtree when the camera has the whole graph in
// sight.
// TODO: a square can be represented as topleft + width, saying for the quad blocks (reduce mem)
// TODO: jsdoc
// TODO: be sure we can handle cases overcoming boundaries (because of size) or use a maxed size
// TODO: filtering unwanted labels beforehand through the filter function
// NOTE: this is basically a MX-CIF Quadtree at this point
// NOTE: need to explore R-Trees for edges
// NOTE: need to explore 2d segment tree for edges
// NOTE: probably can do faster using spatial hashing
/**
* Constants.
*
* Note that since we are representing a static 4-ary tree, the indices of the
* quadrants are the following:
* - TOP_LEFT: 4i + b
* - TOP_RIGHT: 4i + 2b
* - BOTTOM_LEFT: 4i + 3b
* - BOTTOM_RIGHT: 4i + 4b
*/
var BLOCKS = 4, MAX_LEVEL = 5;
var X_OFFSET = 0, Y_OFFSET = 1, WIDTH_OFFSET = 2, HEIGHT_OFFSET = 3;
var TOP_LEFT = 1, TOP_RIGHT = 2, BOTTOM_LEFT = 3, BOTTOM_RIGHT = 4;
/**
* Geometry helpers.
*/
/**
* Function returning whether the given rectangle is axis-aligned.
*
* @param {Rectangle} rect
* @return {boolean}
*/
function isRectangleAligned(rect) {
return rect.x1 === rect.x2 || rect.y1 === rect.y2;
}
exports.isRectangleAligned = isRectangleAligned;
/**
* Function returning the smallest rectangle that contains the given rectangle, and that is aligned with the axis.
*
* @param {Rectangle} rect
* @return {Rectangle}
*/
function getCircumscribedAlignedRectangle(rect) {
var width = Math.sqrt(Math.pow(rect.x2 - rect.x1, 2) + Math.pow(rect.y2 - rect.y1, 2));
var heightVector = {
x: ((rect.y1 - rect.y2) * rect.height) / width,
y: ((rect.x2 - rect.x1) * rect.height) / width,
};
// Compute all corners:
var tl = { x: rect.x1, y: rect.y1 };
var tr = { x: rect.x2, y: rect.y2 };
var bl = {
x: rect.x1 + heightVector.x,
y: rect.y1 + heightVector.y,
};
var br = {
x: rect.x2 + heightVector.x,
y: rect.y2 + heightVector.y,
};
var xL = Math.min(tl.x, tr.x, bl.x, br.x);
var xR = Math.max(tl.x, tr.x, bl.x, br.x);
var yT = Math.min(tl.y, tr.y, bl.y, br.y);
var yB = Math.max(tl.y, tr.y, bl.y, br.y);
return {
x1: xL,
y1: yT,
x2: xR,
y2: yT,
height: yB - yT,
};
}
exports.getCircumscribedAlignedRectangle = getCircumscribedAlignedRectangle;
/**
*
* @param x1
* @param y1
* @param w
* @param qx
* @param qy
* @param qw
* @param qh
*/
function squareCollidesWithQuad(x1, y1, w, qx, qy, qw, qh) {
return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + w > qy;
}
exports.squareCollidesWithQuad = squareCollidesWithQuad;
function rectangleCollidesWithQuad(x1, y1, w, h, qx, qy, qw, qh) {
return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + h > qy;
}
exports.rectangleCollidesWithQuad = rectangleCollidesWithQuad;
function pointIsInQuad(x, y, qx, qy, qw, qh) {
var xmp = qx + qw / 2, ymp = qy + qh / 2, top = y < ymp, left = x < xmp;
return top ? (left ? TOP_LEFT : TOP_RIGHT) : left ? BOTTOM_LEFT : BOTTOM_RIGHT;
}
/**
* Helper functions that are not bound to the class so an external user
* cannot mess with them.
*/
function buildQuadrants(maxLevel, data) {
// [block, level]
var stack = [0, 0];
while (stack.length) {
var level = stack.pop(), block = stack.pop();
var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
var x = data[block + X_OFFSET], y = data[block + Y_OFFSET], width = data[block + WIDTH_OFFSET], height = data[block + HEIGHT_OFFSET], hw = width / 2, hh = height / 2;
data[topLeftBlock + X_OFFSET] = x;
data[topLeftBlock + Y_OFFSET] = y;
data[topLeftBlock + WIDTH_OFFSET] = hw;
data[topLeftBlock + HEIGHT_OFFSET] = hh;
data[topRightBlock + X_OFFSET] = x + hw;
data[topRightBlock + Y_OFFSET] = y;
data[topRightBlock + WIDTH_OFFSET] = hw;
data[topRightBlock + HEIGHT_OFFSET] = hh;
data[bottomLeftBlock + X_OFFSET] = x;
data[bottomLeftBlock + Y_OFFSET] = y + hh;
data[bottomLeftBlock + WIDTH_OFFSET] = hw;
data[bottomLeftBlock + HEIGHT_OFFSET] = hh;
data[bottomRightBlock + X_OFFSET] = x + hw;
data[bottomRightBlock + Y_OFFSET] = y + hh;
data[bottomRightBlock + WIDTH_OFFSET] = hw;
data[bottomRightBlock + HEIGHT_OFFSET] = hh;
if (level < maxLevel - 1) {
stack.push(bottomRightBlock, level + 1);
stack.push(bottomLeftBlock, level + 1);
stack.push(topRightBlock, level + 1);
stack.push(topLeftBlock, level + 1);
}
}
}
function insertNode(maxLevel, data, containers, key, x, y, size) {
var x1 = x - size, y1 = y - size, w = size * 2;
var level = 0, block = 0;
while (true) {
// If we reached max level
if (level >= maxLevel) {
containers[block] = containers[block] || [];
containers[block].push(key);
return;
}
var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
var collidingWithTopLeft = squareCollidesWithQuad(x1, y1, w, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]);
var collidingWithTopRight = squareCollidesWithQuad(x1, y1, w, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]);
var collidingWithBottomLeft = squareCollidesWithQuad(x1, y1, w, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]);
var collidingWithBottomRight = squareCollidesWithQuad(x1, y1, w, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]);
var collisions = [
collidingWithTopLeft,
collidingWithTopRight,
collidingWithBottomLeft,
collidingWithBottomRight,
].reduce(function (acc, current) {
if (current)
return acc + 1;
else
return acc;
}, 0);
// If we don't have at least a collision, there is an issue
if (collisions === 0)
throw new Error("sigma/quadtree.insertNode: no collision (level: " + level + ", key: " + key + ", x: " + x + ", y: " + y + ", size: " + size + ").");
// If we have 3 collisions, we have a geometry problem obviously
if (collisions === 3)
throw new Error("sigma/quadtree.insertNode: 3 impossible collisions (level: " + level + ", key: " + key + ", x: " + x + ", y: " + y + ", size: " + size + ").");
// If we have more that one collision, we stop here and store the node
// in the relevant containers
if (collisions > 1) {
containers[block] = containers[block] || [];
containers[block].push(key);
return;
}
else {
level++;
}
// Else we recurse into the correct quads
if (collidingWithTopLeft)
block = topLeftBlock;
if (collidingWithTopRight)
block = topRightBlock;
if (collidingWithBottomLeft)
block = bottomLeftBlock;
if (collidingWithBottomRight)
block = bottomRightBlock;
}
}
function getNodesInAxisAlignedRectangleArea(maxLevel, data, containers, x1, y1, w, h) {
// [block, level]
var stack = [0, 0];
var collectedNodes = [];
var container;
while (stack.length) {
var level = stack.pop(), block = stack.pop();
// Collecting nodes
container = containers[block];
if (container)
extend_1.default(collectedNodes, container);
// If we reached max level
if (level >= maxLevel)
continue;
var topLeftBlock = 4 * block + BLOCKS, topRightBlock = 4 * block + 2 * BLOCKS, bottomLeftBlock = 4 * block + 3 * BLOCKS, bottomRightBlock = 4 * block + 4 * BLOCKS;
var collidingWithTopLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[topLeftBlock + X_OFFSET], data[topLeftBlock + Y_OFFSET], data[topLeftBlock + WIDTH_OFFSET], data[topLeftBlock + HEIGHT_OFFSET]);
var collidingWithTopRight = rectangleCollidesWithQuad(x1, y1, w, h, data[topRightBlock + X_OFFSET], data[topRightBlock + Y_OFFSET], data[topRightBlock + WIDTH_OFFSET], data[topRightBlock + HEIGHT_OFFSET]);
var collidingWithBottomLeft = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomLeftBlock + X_OFFSET], data[bottomLeftBlock + Y_OFFSET], data[bottomLeftBlock + WIDTH_OFFSET], data[bottomLeftBlock + HEIGHT_OFFSET]);
var collidingWithBottomRight = rectangleCollidesWithQuad(x1, y1, w, h, data[bottomRightBlock + X_OFFSET], data[bottomRightBlock + Y_OFFSET], data[bottomRightBlock + WIDTH_OFFSET], data[bottomRightBlock + HEIGHT_OFFSET]);
if (collidingWithTopLeft)
stack.push(topLeftBlock, level + 1);
if (collidingWithTopRight)
stack.push(topRightBlock, level + 1);
if (collidingWithBottomLeft)
stack.push(bottomLeftBlock, level + 1);
if (collidingWithBottomRight)
stack.push(bottomRightBlock, level + 1);
}
return collectedNodes;
}
/**
* QuadTree class.
*
* @constructor
* @param {object} boundaries - The graph boundaries.
*/
var QuadTree = /** @class */ (function () {
function QuadTree(params) {
if (params === void 0) { params = {}; }
this.containers = {};
this.cache = null;
this.lastRectangle = null;
// Allocating the underlying byte array
var L = Math.pow(4, MAX_LEVEL);
this.data = new Float32Array(BLOCKS * ((4 * L - 1) / 3));
if (params.boundaries)
this.resize(params.boundaries);
else
this.resize({
x: 0,
y: 0,
width: 1,
height: 1,
});
}
QuadTree.prototype.add = function (key, x, y, size) {
insertNode(MAX_LEVEL, this.data, this.containers, key, x, y, size);
return this;
};
QuadTree.prototype.resize = function (boundaries) {
this.clear();
// Building the quadrants
this.data[X_OFFSET] = boundaries.x;
this.data[Y_OFFSET] = boundaries.y;
this.data[WIDTH_OFFSET] = boundaries.width;
this.data[HEIGHT_OFFSET] = boundaries.height;
buildQuadrants(MAX_LEVEL, this.data);
};
QuadTree.prototype.clear = function () {
this.containers = {};
return this;
};
QuadTree.prototype.point = function (x, y) {
var nodes = [];
var block = 0, level = 0;
do {
if (this.containers[block])
nodes.push.apply(nodes, __spreadArray([], __read(this.containers[block])));
var quad = pointIsInQuad(x, y, this.data[block + X_OFFSET], this.data[block + Y_OFFSET], this.data[block + WIDTH_OFFSET], this.data[block + HEIGHT_OFFSET]);
block = 4 * block + quad * BLOCKS;
level++;
} while (level <= MAX_LEVEL);
return nodes;
};
QuadTree.prototype.rectangle = function (x1, y1, x2, y2, height) {
var lr = this.lastRectangle;
if (lr && x1 === lr.x1 && x2 === lr.x2 && y1 === lr.y1 && y2 === lr.y2 && height === lr.height) {
return this.cache;
}
this.lastRectangle = {
x1: x1,
y1: y1,
x2: x2,
y2: y2,
height: height,
};
// If the rectangle is shifted, we use the smallest aligned rectangle that contains the shifted one:
if (!isRectangleAligned(this.lastRectangle))
this.lastRectangle = getCircumscribedAlignedRectangle(this.lastRectangle);
this.cache = getNodesInAxisAlignedRectangleArea(MAX_LEVEL, this.data, this.containers, x1, y1, Math.abs(x1 - x2) || Math.abs(y1 - y2), height);
return this.cache;
};
return QuadTree;
}());
exports.default = QuadTree;