UNPKG

rbush-full

Version:

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

590 lines (459 loc) 16 kB
boxIntersect = require 'box-intersect' MAX_REMOVALS_BEFORE_SWAP = 50 class TmpBbox CACHE_SIZE = 20 bboxes = [] nextBbox = 0 for i in [0...CACHE_SIZE] bboxes.push [Infinity, Infinity, -Infinity, -Infinity] @get: -> bboxes[nextBbox++ & (CACHE_SIZE - 1)]; class GrowingArray constructor: (@size = 8, { @double = false } = {}) -> @current = new Array(@size) @other = new Array(@size) if @double @currentLen = 0 @[Symbol.iterator] = -> i = 0; max = @currentLen; cur = @current next: -> if i < max value: cur[i++], done: false else done: true push: (item, items2) -> @enlarge() if @currentLen is @size @other[@currentLen] = item2 if item2? @current[@currentLen++] = item pop: -> @current[--@currentLen] if @currentLen > 0 enlarge: -> @size = Math.floor(@size * 1.5) @current.length = @size @other?.length = @size clear: -> @currentLen = 0 class GrowingArrayPool extends GrowingArray constructor: (size, @innerSize) -> super(size) get: -> @pop() or new GrowingArray(@innerSize) release: (item) -> @push item class SortableStack extends GrowingArray constructor: (size) -> super(size, double: true) push: (item, value) -> @enlarge() if @currentLen is @size pos = 0 while pos < @currentLen if value > @other[pos] break else pos++ while pos <= @currentLen [newItem, newValue] = [@current[pos], @other[pos]] @current[pos] = item @other[pos] = value [item, value] = [newItem, newValue] pos++ @currentLen++ class ObjectStorage extends GrowingArray constructor: (size) -> super(size, double: true) @removalsCount = 0 @[Symbol.iterator] = -> i = 0; max = @currentLen; cur = @current next: -> while i < max value = cur[i++] if not value._removed return { value, done: false } done: true remove: (item) -> item._removed = true @removalsCount++ maybeCondense: (threshold) -> if @removalsCount > (threshold ? Math.max(@currentLen, 50) * 0.1) @condense() condense: -> newIndex = 0 for index in [0...@currentLen] item = @current[index] if item._removed item._removed = null continue @other[newIndex++] = item @swap newIndex swap: (@currentLen) -> [@current, @other, @removalsCount] = [@other, @current, 0] null class LeafNodes extends ObjectStorage constructor: -> super(arguments...) @overlappingPool = new GrowingArrayPool(@size, Math.ceil(@size / 4)) push: (item) -> super(arguments...) item.overlapping ?= @overlappingPool.get() remove: (item) -> super(arguments...) @overlappingPool.release item.overlapping item.overlapping = null class RBush constructor: (maxEntries = 9) -> # max entries in a node is 9 by default; min node fill is 40% for best performance @_maxEntries = Math.max(4, maxEntries) @_minEntries = Math.max(2, Math.ceil(@_maxEntries * 0.4)) @_collisionRunId = 0 @_stacks = [0..8].map -> new SortableStack(maxEntries) @raycastResponse = dist: Infinity, item: null @nonStatic = new ObjectStorage(64) @result = new GrowingArray(32) @searchPath = new GrowingArray(32) @leafNodes = new LeafNodes(16) @clear() all: (predicate, result = @result) -> result.currentLen = 0 @searchPath.currentLen = 0 @_all(@data, predicate, result) search: (bbox, predicate, result = @result) -> node = @data result.currentLen = 0 return result if not intersects(bbox, node.bbox) @searchPath.currentLen = 0 while node for child in node.children when not child._ignore if intersects(bbox, child.bbox) if node.leaf if not predicate? or predicate(child) result.push(child) else if contains(bbox, child.bbox) @_all(child, predicate, result) else @searchPath.push(child) node = @searchPath.pop() result collides: (bbox) -> node = @data return false if not intersects(bbox, node) @searchPath.currentLen = 0 while node for child in node.children if intersects(bbox, child.bbox) return true if node.leaf or contains(bbox, child.bbox) @searchPath.push(child) node = @searchPath.pop() return false update: (item) -> if contains(item.parent.bbox, item.bbox) return reinsert = true @remove(item, reinsert) @insert(item, reinsert) insert: (item, reinsert) -> unless item?.bbox log "[RBush::insert] can't add without bbox", item return if not item.isStatic and item._removed item._removed = null @nonStatic.removalsCount-- reinsert = true @_insert(item) unless item.isStatic or reinsert @nonStatic.push(item) this clear: -> @data = createNode([]) @leafNodes.currentLen = 0 @leafNodes.push @data this remove: (item, reinsert) -> return unless item? parent = item.parent index = parent.children.indexOf(item) if index is -1 throw "[RBush remove] ERROR: parent doesn't have that item" parent.children.splice index, 1 unless item.isStatic or reinsert @nonStatic.remove(item) @_condense(parent) this checkCollisions: -> @result.currentLen = 0 @_collisionRunId = 0 if ++@_collisionRunId > 99999 { other, current, currentLen, removalsCount } = @nonStatic if removalsCount > MAX_REMOVALS_BEFORE_SWAP swapping = true newIndex = 0 leafs = @leafNodes.current for i in [0...@leafNodes.currentLen] leaf = leafs[i] if not leaf._removed leaf.overlapping.currentLen = 0 boxIntersect leafs, @leafNodes.currentLen, (leaf1, leaf2) -> leaf1.overlapping.push leaf2 leaf2.overlapping.push leaf1 undefined for index in [0...currentLen] item = current[index] continue if item._removed other[newIndex++] = item if swapping continue if item._ignore item._colRunId = @_collisionRunId leaf = item.parent for c, i in leaf.children when not c._ignore and c._colRunId isnt @_collisionRunId if intersects(item.bbox, c.bbox) @result.push item @result.push c # using iterators here is much slower overlapping = leaf.overlapping if overlapping.currentLen for i in [0...overlapping.currentLen] otherLeaf = overlapping.current[i] if intersects(item.bbox, otherLeaf.bbox) for c in otherLeaf.children when not c._ignore and c._colRunId isnt @_collisionRunId if intersects(item.bbox, c.bbox) @result.push item @result.push c null if swapping @nonStatic.swap(newIndex) @result # _rayObjectDistance: (distToBbox, origin, dir, dstX, dstY, range, item) -> raycast: (origin, dir, range = Infinity, predicate) -> node = @data invDirx = 1 / dir.x invDiry = 1 / dir.y dstX = origin.x + dir.x * range dstY = origin.y + dir.y * range tmin = Infinity item = null for stack in @_stacks stack.currentLen = 0 while node stack = @_stacks[node.height] if node.leaf for child in node.children when not child._ignore if not predicate? or predicate(child) t = rayBboxDistance(origin.x, origin.y, invDirx, invDiry, child.bbox) if t < range and t < tmin if @_rayObjectDistance? t = @_rayObjectDistance(t, origin, dir, dstX, dstY, range, child) if t < range and t < tmin tmin = t item = child else for child in node.children when not child._ignore t = rayBboxDistance(origin.x, origin.y, invDirx, invDiry, child.bbox) if t < range and t < tmin stack.push child, t while node popped = @_stacks[node.height].pop() if popped node = popped break node = node.parent @raycastResponse.dist = tmin @raycastResponse.item = item @raycastResponse _all: (node, predicate, result) -> i = @searchPath.currentLen while node if node.leaf for child in node.children when not child._ignore if not predicate? or predicate(child) result.push(child) else for child in node.children @searchPath.push(child) break if @searchPath.currentLen is i node = @searchPath.pop() result _chooseSubtree: (bbox, node) -> while true break if node.leaf minArea = Infinity minEnlargement = Infinity targetNode = null for child in node.children area = bboxArea(child.bbox) enlargement = enlargedArea(bbox, child.bbox) - area # choose entry with the least area enlargement if enlargement < minEnlargement minEnlargement = enlargement minArea = if area < minArea then area else minArea targetNode = child else if enlargement is minEnlargement # otherwise choose one with the smallest area if area < minArea minArea = area targetNode = child node = targetNode or node.children[0] node _insert: (item) -> bbox = item.bbox # find the best node for accommodating the item, saving all nodes along the path too node = @_chooseSubtree(bbox, @data) # put the item into the node node.children.push(item) item.parent = node extend(node.bbox, bbox) # split on node overflow; propagate upwards if necessary parent = node while parent? if parent.children.length > @_maxEntries @_split parent parent = parent.parent # adjust bboxes along the insertion path @_adjustParentBBoxes(bbox, node.parent) # split overflowed node into two _split: (node) -> M = node.children.length m = @_minEntries @_chooseSplitAxis(node, m, M) splitIndex = @_chooseSplitIndex(node, m, M) newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex)) newNode.height = node.height newNode.leaf = node.leaf if newNode.leaf @leafNodes.push newNode calcBBox(node) calcBBox(newNode) if node.parent? node.parent.children.push(newNode) newNode.parent = node.parent else @_splitRoot(node, newNode) _splitRoot: (node, newNode) -> # split root node @data = createNode([node, newNode]) @data.height = node.height + 1 if @data.height is @_stacks.length @_stacks.push new SortableStack(@_maxEntries) @data.leaf = false calcBBox(@data) _chooseSplitIndex: (node, m, M) -> index = null minOverlap = Infinity minArea = Infinity i = m - 1 for i in [m..M - m] bbox1 = distBBox(node, 0, i) bbox2 = distBBox(node, i, M) overlap = intersectionArea(bbox1, bbox2) area = bboxArea(bbox1) + bboxArea(bbox2) # choose distribution with minimum overlap if (overlap < minOverlap) minOverlap = overlap index = i minArea = if area < minArea then area else minArea else if overlap is minOverlap # otherwise choose distribution with minimum area if (area < minArea) minArea = area index = i return index || M - m # sorts node children by the best axis for split _chooseSplitAxis: (node, m, M) -> # all of these sorts could be done with Timsort # two options: # 1. Typed Arrays implementation: https://github.com/LXSMNSYC/TimSort # - this requires children to be indexed separetely (separete order array) # - splitting as well as chooseSplitIndex would have to traverse the order array instead # - this could be difficult # # 2. Drop-in replacement implementation: https://github.com/mziccard/node-timsort xMargin = @_allDistMargin(node, m, M, (a, b) -> a.bbox[0] - b.bbox[0]) yMargin = @_allDistMargin(node, m, M, (a, b) -> a.bbox[1] - b.bbox[1]) # if total distributions margin value is minimal for x, sort by minX, # otherwise it's already sorted by minY if (xMargin < yMargin) node.children.sort((a, b) -> a.bbox[0] - b.bbox[0]) # total margin of all possible split distributions where each node is at least m full _allDistMargin: (node, m, M, compare) -> node.children.sort(compare) leftBbox = distBBox(node, 0, m) rightBbox = distBBox(node, M - m, M) margin = bboxMargin(leftBbox) + bboxMargin(rightBbox) for i in [m...M - m] child = node.children[i] extend(leftBbox, child.bbox) margin += bboxMargin(leftBbox) for i in [M - m - 1..m] child = node.children[i] extend(rightBbox, child.bbox) margin += bboxMargin(rightBbox) margin _adjustParentBBoxes: (bbox, node) -> while node extend node.bbox, bbox node = node.parent null _condense: (node) -> # go upward, removing empty while node if node.children.length is 0 if node.parent? siblings = node.parent.children siblings.splice siblings.indexOf(node), 1 if node.leaf @leafNodes.remove node @leafNodes.maybeCondense() else return @clear() else calcBBox(node) node = node.parent null # calculate node's bbox from bboxes of its children calcBBox = (node) -> distBBox(node, 0, node.children.length, node) # min bounding rectangle of node children from k to p-1 distBBox = (node, k, p, destNode) -> bbox = destNode?.bbox ? TmpBbox.get() bbox[0] = Infinity; bbox[1] = Infinity; bbox[2] = -Infinity; bbox[3] = -Infinity; for i in [k...p] extend(bbox, node.children[i].bbox) bbox extend = (a, b) -> a[0] = Math.min(a[0], b[0]) # minX a[1] = Math.min(a[1], b[1]) # minY a[2] = Math.max(a[2], b[2]) # maxX a[3] = Math.max(a[3], b[3]) # maxY a bboxArea = (a) -> (a[2] - a[0]) * (a[3] - a[1]) bboxMargin = (a) -> (a[2] - a[0]) + (a[3] - a[1]) enlargedArea = (a, b) -> (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) * (Math.max(b[3], a[3]) - Math.min(b[1], a[1])) intersectionArea = (a, b) -> minX = Math.max(a[0], b[0]) minY = Math.max(a[1], b[1]) maxX = Math.min(a[2], b[2]) maxY = Math.min(a[3], b[3]) Math.max(0, maxX - minX) * Math.max(0, maxY - minY) contains = (a, b) -> a[0] <= b[0] && a[1] <= b[1] && b[2] <= a[2] && b[3] <= a[3] intersects = (a, b) -> b[0] <= a[2] && b[1] <= a[3] && b[2] >= a[0] && b[3] >= a[1] rayBboxDistance = (x, y, invdx, invdy, bbox) -> tx1 = (bbox[0] - x) * invdx tx2 = (bbox[2] - x) * invdx tmin = Math.min(tx1, tx2) tmax = Math.max(tx1, tx2) ty1 = (bbox[1] - y) * invdy ty2 = (bbox[3] - y) * invdy tmin = Math.max(tmin, Math.min(ty1, ty2)) tmax = Math.min(tmax, Math.max(ty1, ty2)) if tmax > Math.max(tmin, 0) then tmin else Infinity createNode = (children) -> node = children: children height: 1 leaf: true bbox: [Infinity, Infinity, -Infinity, -Infinity] for c in children c.parent = node node module.exports = { RBush, boxIntersect, rayBboxDistance, GrowingArray, GrowingArrayPool, ObjectStorage }