UNPKG

sigma

Version:

A JavaScript library dedicated to graph drawing.

327 lines (326 loc) 13.9 kB
"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;