UNPKG

rbush-3d

Version:

High-performance 3D spatial index for cuboids (based on R*-tree with bulk loading and bulk insertion algorithms)

575 lines 18.5 kB
import quickselect from 'quickselect'; const nodePool = []; const freeNode = (node) => nodePool.push(node); const freeAllNode = (node) => { if (node) { freeNode(node); if (!isLeaf(node)) { node.children.forEach(freeAllNode); } } }; const allowNode = (children) => { let node = nodePool.pop(); if (node) { node.children = children; node.height = 1; node.leaf = true; node.minX = Infinity; node.minY = Infinity; node.minZ = Infinity; node.maxX = -Infinity; node.maxY = -Infinity; node.maxZ = -Infinity; } else { node = { children: children, height: 1, leaf: true, minX: Infinity, minY: Infinity, minZ: Infinity, maxX: -Infinity, maxY: -Infinity, maxZ: -Infinity, }; } return node; }; const distNodePool = []; const freeDistNode = (node) => distNodePool.push(node); const allowDistNode = (dist, node) => { let heapNode = distNodePool.pop(); if (heapNode) { heapNode.dist = dist; heapNode.node = node; } else { heapNode = { dist, node }; } return heapNode; }; const isLeaf = (node) => { return node.leaf; }; const isLeafChild = (node, child) => { return node.leaf; }; const findItem = (item, items, equalsFn) => { if (!equalsFn) return items.indexOf(item); for (let i = 0; i < items.length; i++) { if (equalsFn(item, items[i])) return i; } return -1; }; const calcBBox = (node) => { distBBox(node, 0, node.children.length, node); }; const distBBox = (node, k, p, destNode) => { let dNode = destNode; if (dNode) { dNode.minX = Infinity; dNode.minY = Infinity; dNode.minZ = Infinity; dNode.maxX = -Infinity; dNode.maxY = -Infinity; dNode.maxZ = -Infinity; } else { dNode = allowNode([]); } for (let i = k, child; i < p; i++) { child = node.children[i]; extend(dNode, child); } return dNode; }; const extend = (a, b) => { a.minX = Math.min(a.minX, b.minX); a.minY = Math.min(a.minY, b.minY); a.minZ = Math.min(a.minZ, b.minZ); a.maxX = Math.max(a.maxX, b.maxX); a.maxY = Math.max(a.maxY, b.maxY); a.maxZ = Math.max(a.maxZ, b.maxZ); return a; }; const bboxVolume = (a) => (a.maxX - a.minX) * (a.maxY - a.minY) * (a.maxZ - a.minZ); const bboxMargin = (a) => (a.maxX - a.minX) + (a.maxY - a.minY) + (a.maxZ - a.minZ); const enlargedVolume = (a, b) => { const minX = Math.min(a.minX, b.minX), minY = Math.min(a.minY, b.minY), minZ = Math.min(a.minZ, b.minZ), maxX = Math.max(a.maxX, b.maxX), maxY = Math.max(a.maxY, b.maxY), maxZ = Math.max(a.maxZ, b.maxZ); return (maxX - minX) * (maxY - minY) * (maxZ - minZ); }; const intersectionVolume = (a, b) => { const minX = Math.max(a.minX, b.minX), minY = Math.max(a.minY, b.minY), minZ = Math.max(a.minZ, b.minZ), maxX = Math.min(a.maxX, b.maxX), maxY = Math.min(a.maxY, b.maxY), maxZ = Math.min(a.maxZ, b.maxZ); return Math.max(0, maxX - minX) * Math.max(0, maxY - minY) * Math.max(0, maxZ - minZ); }; const contains = (a, b) => a.minX <= b.minX && a.minY <= b.minY && a.minZ <= b.minZ && b.maxX <= a.maxX && b.maxY <= a.maxY && b.maxZ <= a.maxZ; export const intersects = (a, b) => b.minX <= a.maxX && b.minY <= a.maxY && b.minZ <= a.maxZ && b.maxX >= a.minX && b.maxY >= a.minY && b.maxZ >= a.minZ; export const boxRayIntersects = (box, ox, oy, oz, idx, idy, idz) => { const tx0 = (box.minX - ox) * idx; const tx1 = (box.maxX - ox) * idx; const ty0 = (box.minY - oy) * idy; const ty1 = (box.maxY - oy) * idy; const tz0 = (box.minZ - oz) * idz; const tz1 = (box.maxZ - oz) * idz; const z0 = Math.min(tz0, tz1); const z1 = Math.max(tz0, tz1); const y0 = Math.min(ty0, ty1); const y1 = Math.max(ty0, ty1); const x0 = Math.min(tx0, tx1); const x1 = Math.max(tx0, tx1); const tmin = Math.max(0, x0, y0, z0); const tmax = Math.min(x1, y1, z1); return tmax >= tmin ? tmin : Infinity; }; const multiSelect = (arr, left, right, n, compare) => { const stack = [left, right]; let mid; while (stack.length) { right = stack.pop(); left = stack.pop(); if (right - left <= n) continue; mid = left + Math.ceil((right - left) / n / 2) * n; quickselect(arr, mid, left, right, compare); stack.push(left, mid, mid, right); } }; const compareMinX = (a, b) => a.minX - b.minX; const compareMinY = (a, b) => a.minY - b.minY; const compareMinZ = (a, b) => a.minZ - b.minZ; export class RBush3D { data = { children: [], height: 0, leaf: true, minX: 0, minY: 0, minZ: 0, maxX: 0, maxY: 0, maxZ: 0, }; maxEntries; minEntries; static pool = []; static alloc() { return this.pool.pop() || new this(); } static free(rbush) { rbush.clear(); this.pool.push(rbush); } constructor(maxEntries = 16) { this.maxEntries = Math.max(maxEntries, 8); this.minEntries = Math.max(4, Math.ceil(this.maxEntries * 0.4)); this.clear(); } search(bbox) { let node = this.data; const result = []; if (!intersects(bbox, node)) return result; const nodesToSearch = []; while (node) { for (let i = 0, len = node.children.length; i < len; i++) { const child = node.children[i]; if (intersects(bbox, child)) { if (isLeafChild(node, child)) result.push(child); else if (contains(bbox, child)) this._all(child, result); else nodesToSearch.push(child); } } node = nodesToSearch.pop(); } return result; } collides(bbox) { let node = this.data; if (!intersects(bbox, node)) return false; const nodesToSearch = []; while (node) { for (let i = 0, len = node.children.length; i < len; i++) { const child = node.children[i]; if (intersects(bbox, child)) { if (isLeafChild(node, child) || contains(bbox, child)) return true; nodesToSearch.push(child); } } node = nodesToSearch.pop(); } return false; } raycastInv(ox, oy, oz, idx, idy, idz, maxLen = Infinity) { let node = this.data; if (idx === Infinity && idy === Infinity && idz === Infinity) return allowDistNode(Infinity, undefined); if (boxRayIntersects(node, ox, oy, oz, idx, idy, idz) === Infinity) return allowDistNode(Infinity, undefined); const heap = [allowDistNode(0, node)]; const swap = (a, b) => { const t = heap[a]; heap[a] = heap[b]; heap[b] = t; }; const pop = () => { const top = heap[0]; const newLen = heap.length - 1; heap[0] = heap[newLen]; heap.length = newLen; let idx = 0; while (true) { let left = (idx << 1) | 1; if (left >= newLen) break; const right = left + 1; if (right < newLen && heap[right].dist < heap[left].dist) { left = right; } if (heap[idx].dist < heap[left].dist) break; swap(idx, left); idx = left; } freeDistNode(top); return top.node; }; const push = (dist, node) => { let idx = heap.length; heap.push(allowDistNode(dist, node)); while (idx > 0) { const p = (idx - 1) >> 1; if (heap[p].dist <= heap[idx].dist) break; swap(idx, p); idx = p; } }; let dist = maxLen; let result; while (heap.length && heap[0].dist < dist) { node = pop(); for (let i = 0, len = node.children.length; i < len; i++) { const child = node.children[i]; const d = boxRayIntersects(child, ox, oy, oz, idx, idy, idz); if (!isLeafChild(node, child)) { push(d, child); } else if (d < dist) { if (d === 0) { return allowDistNode(d, child); } dist = d; result = child; } } } return allowDistNode(dist < maxLen ? dist : Infinity, result); } raycast(ox, oy, oz, dx, dy, dz, maxLen = Infinity) { return this.raycastInv(ox, oy, oz, 1 / dx, 1 / dy, 1 / dz, maxLen); } all() { return this._all(this.data, []); } load(data) { if (!(data && data.length)) return this; if (data.length < this.minEntries) { for (let i = 0, len = data.length; i < len; i++) { this.insert(data[i]); } return this; } let node = this.build(data.slice(), 0, data.length - 1, 0); if (!this.data.children.length) { this.data = node; } else if (this.data.height === node.height) { this.splitRoot(this.data, node); } else { if (this.data.height < node.height) { const tmpNode = this.data; this.data = node; node = tmpNode; } this._insert(node, this.data.height - node.height - 1, true); } return this; } insert(item) { if (item) this._insert(item, this.data.height - 1); return this; } clear() { if (this.data) { freeAllNode(this.data); } this.data = allowNode([]); return this; } remove(item, equalsFn) { if (!item) return this; let node = this.data; let i = 0; let goingUp = false; let index; let parent; const path = []; const indexes = []; while (node || path.length) { if (!node) { node = path.pop(); i = indexes.pop(); parent = path[path.length - 1]; goingUp = true; } if (isLeaf(node)) { index = findItem(item, node.children, equalsFn); if (index !== -1) { node.children.splice(index, 1); path.push(node); this.condense(path); return this; } } if (!goingUp && !isLeaf(node) && contains(node, item)) { path.push(node); indexes.push(i); i = 0; parent = node; node = node.children[0]; } else if (parent) { i++; node = parent.children[i]; goingUp = false; } else { node = undefined; } } return this; } toJSON() { return this.data; } fromJSON(data) { freeAllNode(this.data); this.data = data; return this; } build(items, left, right, height) { const N = right - left + 1; let M = this.maxEntries; let node; if (N <= M) { node = allowNode(items.slice(left, right + 1)); calcBBox(node); return node; } if (!height) { height = Math.ceil(Math.log(N) / Math.log(M)); M = Math.ceil(N / Math.pow(M, height - 1)); } node = allowNode([]); node.leaf = false; node.height = height; const N3 = Math.ceil(N / M), N2 = N3 * Math.ceil(Math.pow(M, 2 / 3)), N1 = N3 * Math.ceil(Math.pow(M, 1 / 3)); multiSelect(items, left, right, N1, compareMinX); for (let i = left; i <= right; i += N1) { const right2 = Math.min(i + N1 - 1, right); multiSelect(items, i, right2, N2, compareMinY); for (let j = i; j <= right2; j += N2) { const right3 = Math.min(j + N2 - 1, right2); multiSelect(items, j, right3, N3, compareMinZ); for (let k = j; k <= right3; k += N3) { const right4 = Math.min(k + N3 - 1, right3); node.children.push(this.build(items, k, right4, height - 1)); } } } calcBBox(node); return node; } _all(node, result) { const nodesToSearch = []; while (node) { if (isLeaf(node)) result.push(...node.children); else nodesToSearch.push(...node.children); node = nodesToSearch.pop(); } return result; } chooseSubtree(bbox, node, level, path) { let minVolume; let minEnlargement; let targetNode; while (true) { path.push(node); if (isLeaf(node) || path.length - 1 === level) break; minVolume = minEnlargement = Infinity; for (let i = 0, len = node.children.length; i < len; i++) { const child = node.children[i]; const volume = bboxVolume(child); const enlargement = enlargedVolume(bbox, child) - volume; if (enlargement < minEnlargement) { minEnlargement = enlargement; minVolume = volume < minVolume ? volume : minVolume; targetNode = child; } else if (enlargement === minEnlargement) { if (volume < minVolume) { minVolume = volume; targetNode = child; } } } node = targetNode || node.children[0]; } return node; } split(insertPath, level) { const node = insertPath[level]; const M = node.children.length; const m = this.minEntries; this.chooseSplitAxis(node, m, M); const splitIndex = this.chooseSplitIndex(node, m, M); const newNode = allowNode(node.children.splice(splitIndex, node.children.length - splitIndex)); newNode.height = node.height; newNode.leaf = node.leaf; calcBBox(node); calcBBox(newNode); if (level) insertPath[level - 1].children.push(newNode); else this.splitRoot(node, newNode); } splitRoot(node, newNode) { this.data = allowNode([node, newNode]); this.data.height = node.height + 1; this.data.leaf = false; calcBBox(this.data); } chooseSplitIndex(node, m, M) { let minOverlap = Infinity; let minVolume = Infinity; let index; for (let i = m; i <= M - m; i++) { const bbox1 = distBBox(node, 0, i); const bbox2 = distBBox(node, i, M); const overlap = intersectionVolume(bbox1, bbox2); const volume = bboxVolume(bbox1) + bboxVolume(bbox2); if (overlap < minOverlap) { minOverlap = overlap; index = i; minVolume = volume < minVolume ? volume : minVolume; } else if (overlap === minOverlap) { if (volume < minVolume) { minVolume = volume; index = i; } } } return index; } chooseSplitAxis(node, m, M) { const xMargin = this.allDistMargin(node, m, M, compareMinX); const yMargin = this.allDistMargin(node, m, M, compareMinY); const zMargin = this.allDistMargin(node, m, M, compareMinZ); if (xMargin < yMargin && xMargin < zMargin) { node.children.sort(compareMinX); } else if (yMargin < xMargin && yMargin < zMargin) { node.children.sort(compareMinY); } } allDistMargin(node, m, M, compare) { node.children.sort(compare); const leftBBox = distBBox(node, 0, m); const rightBBox = distBBox(node, M - m, M); let margin = bboxMargin(leftBBox) + bboxMargin(rightBBox); for (let i = m; i < M - m; i++) { const child = node.children[i]; extend(leftBBox, child); margin += bboxMargin(leftBBox); } for (let i = M - m - 1; i >= m; i--) { const child = node.children[i]; extend(rightBBox, child); margin += bboxMargin(rightBBox); } return margin; } adjustParentBBoxes(bbox, path, level) { for (let i = level; i >= 0; i--) { extend(path[i], bbox); } } condense(path) { for (let i = path.length - 1, siblings; i >= 0; i--) { if (path[i].children.length === 0) { if (i > 0) { siblings = path[i - 1].children; siblings.splice(siblings.indexOf(path[i]), 1); freeNode(path[i]); } else { this.clear(); } } else { calcBBox(path[i]); } } } _insert(item, level, isNode) { const insertPath = []; const node = this.chooseSubtree(item, this.data, level, insertPath); node.children.push(item); extend(node, item); while (level >= 0) { if (insertPath[level].children.length > this.maxEntries) { this.split(insertPath, level); level--; } else break; } this.adjustParentBBoxes(item, insertPath, level); } } //# sourceMappingURL=index.js.map