Add existing to tracked

This commit is contained in:
Jay
2026-08-11 09:53:42 -04:00
parent afe07f3055
commit ffd6e3d73c
8531 changed files with 4396230 additions and 0 deletions
+15
View File
@@ -0,0 +1,15 @@
ISC License
Copyright (c) 2026, Vladimir Agafonkin
Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
+104
View File
@@ -0,0 +1,104 @@
## KDBush
A very fast static spatial index for 2D points based on a flat KD-tree.
Compared to [RBush](https://github.com/mourner/rbush):
- **Points only** — no rectangles.
- **Static** — you can't add/remove items after initial indexing.
- **Faster** indexing and search, with lower **memory** footprint.
- Index is stored as a single **array buffer** (so you can [transfer](https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects) it between threads or store it as a compact file).
If you need a static index for rectangles, not only points, see [Flatbush](https://github.com/mourner/flatbush). When indexing points, KDBush has the advantage of taking ~2x less memory than Flatbush.
[![Build Status](https://github.com/mourner/kdbush/actions/workflows/node.yml/badge.svg)](https://github.com/mourner/kdbush/actions)
[![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects)
## Usage
```js
// initialize KDBush for 1000 items
const index = new KDBush(1000);
// fill it with 1000 points
for (const {x, y} of items) {
index.add(x, y);
}
// perform the indexing
index.finish();
// make a bounding box query
const foundIds = index.range(minX, minY, maxX, maxY);
// map ids to original items
const foundItems = foundIds.map(i => items[i]);
// make a radius query
const neighborIds = index.within(x, y, 5);
// instantly transfer the index from a worker to the main thread
postMessage(index.data, [index.data]);
// reconstruct the index from a raw array buffer
const index = KDBush.from(e.data);
```
## Install
Install with NPM: `npm install kdbush`, then import as a module:
```js
import KDBush from 'kdbush';
```
Or use as a module directly in the browser with [jsDelivr](https://www.jsdelivr.com/esm):
```html
<script type="module">
import KDBush from 'https://cdn.jsdelivr.net/npm/kdbush/+esm';
</script>
```
Alternatively, there's a browser bundle with a `KDBush` global variable:
```html
<script src="https://cdn.jsdelivr.net/npm/kdbush"></script>
```
## API
#### new KDBush(numItems[, nodeSize, ArrayType, ArrayBufferType])
Creates an index that will hold a given number of points (`numItems`). Additionally accepts:
- `nodeSize`: Size of the KD-tree node, `64` by default. Higher means faster indexing but slower search, and vise versa.
- `ArrayType`: Array type to use for storing coordinate values. `Float64Array` by default, but if your coordinates are integer values, `Int32Array` makes the index faster and smaller.
- `ArrayBufferType`: the array buffer type used to store data (`ArrayBuffer` by default);
you may prefer `SharedArrayBuffer` if you want to share the index between threads (multiple `Worker`, `SharedWorker` or `ServiceWorker`).
#### index.add(x, y)
Adds a given point to the index. Returns a zero-based, incremental number that represents the newly added point.
#### index.range(minX, minY, maxX, maxY)
Finds all items within the given bounding box and returns an array of indices that refer to the order the items were added (the values returned by `index.add(x, y)`).
#### index.within(x, y, radius)
Finds all items within a given radius from the query point and returns an array of indices.
#### `KDBush.from(data)`
Recreates a KDBush index from raw `ArrayBuffer` or `SharedArrayBuffer` data
(that's exposed as `index.data` on a previously indexed KDBush instance).
Very useful for transferring or sharing indices between threads or storing them in a file.
### Properties
- `data`: array buffer that holds the index.
- `numItems`: number of stored items.
- `nodeSize`: number of items in a KD-tree node.
- `ArrayType`: array type used for internal coordinates storage.
- `IndexArrayType`: array type used for internal item indices storage.
+67
View File
@@ -0,0 +1,67 @@
export default class KDBush {
/**
* Creates an index from raw `ArrayBuffer` data.
* @param {ArrayBufferLike} data
*/
static from(data: ArrayBufferLike): KDBush;
/**
* Creates an index that will hold a given number of items.
* @param {number} numItems
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
* @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used for storage (`ArrayBuffer` by default).
* @param {ArrayBufferLike} [data] (For internal use only)
*/
constructor(numItems: number, nodeSize?: number, ArrayType?: TypedArrayConstructor, ArrayBufferType?: ArrayBufferConstructor | SharedArrayBufferConstructor, data?: ArrayBufferLike);
numItems: number;
nodeSize: number;
ArrayType: TypedArrayConstructor;
IndexArrayType: Uint16ArrayConstructor | Uint32ArrayConstructor;
data: ArrayBufferLike;
ids: Uint16Array<ArrayBuffer> | Uint32Array<ArrayBuffer>;
coords: Int8Array<ArrayBuffer> | Uint8Array<ArrayBuffer> | Uint8ClampedArray<ArrayBuffer> | Int16Array<ArrayBuffer> | Uint16Array<ArrayBuffer> | Int32Array<ArrayBuffer> | Uint32Array<ArrayBuffer> | Float32Array<ArrayBuffer> | Float64Array<ArrayBuffer>;
_pos: number;
_finished: boolean;
/**
* Add a point to the index.
* @param {number} x
* @param {number} y
* @returns {number} An incremental index associated with the added item (starting from `0`).
*/
add(x: number, y: number): number;
/**
* Perform indexing of the added points.
*/
finish(): this;
/**
* Search the index for items within a given bounding box.
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {number[]} An array of indices correponding to the found items.
*/
range(minX: number, minY: number, maxX: number, maxY: number): number[];
/**
* Search the index for items within a given radius.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @returns {number[]} An array of indices correponding to the found items.
*/
within(qx: number, qy: number, r: number): number[];
/**
* Search the index for items within a given radius, writing matching ids into `out`
* via indexed assignment (`out[i] = id`). Accepts any indexed-writable container —
* a typed array sized to the expected upper bound (allocation-free, fast) or a plain
* `Array` (which will grow as needed). Returns the number of matches written.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @param {number[] | TypedArray} out Container to write matching ids into.
* @returns {number} The number of matches written to `out`.
*/
withinInto(qx: number, qy: number, r: number, out: number[] | TypedArray): number;
}
export type TypedArrayConstructor = Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor;
export type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array;
+362
View File
@@ -0,0 +1,362 @@
const ARRAY_TYPES = [
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float32Array, Float64Array
];
/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
/** @typedef {Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array} TypedArray */
const VERSION = 1; // serialized format version
const HEADER_SIZE = 8;
// Shared scratch stack for iterative DFS in range/within. Sized for the worst case:
// 3 ints per frame * (treeHeight + 1), with treeHeight ≤ ceil(log2(2^32 / 3)) ≈ 31.
const STACK = new Uint32Array(96);
export default class KDBush {
/**
* Creates an index from raw `ArrayBuffer` data.
* @param {ArrayBufferLike} data
*/
static from(data) {
// @ts-expect-error duck typing array buffers
if (!data || data.byteLength === undefined || data.buffer) {
throw new Error('Data must be an instance of ArrayBuffer or SharedArrayBuffer.');
}
const [magic, versionAndType] = new Uint8Array(data, 0, 2);
if (magic !== 0xdb) {
throw new Error('Data does not appear to be in a KDBush format.');
}
const version = versionAndType >> 4;
if (version !== VERSION) {
throw new Error(`Got v${version} data when expected v${VERSION}.`);
}
const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
if (!ArrayType) {
throw new Error('Unrecognized array type.');
}
const [nodeSize] = new Uint16Array(data, 2, 1);
const [numItems] = new Uint32Array(data, 4, 1);
return new KDBush(numItems, nodeSize, ArrayType, undefined, data);
}
/**
* Creates an index that will hold a given number of items.
* @param {number} numItems
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
* @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used for storage (`ArrayBuffer` by default).
* @param {ArrayBufferLike} [data] (For internal use only)
*/
constructor(numItems, nodeSize = 64, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data) {
if (isNaN(numItems) || numItems < 0) throw new Error(`Unexpected numItems value: ${numItems}.`);
this.numItems = +numItems;
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
this.ArrayType = ArrayType;
this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
const padCoords = (8 - idsByteSize % 8) % 8;
if (arrayTypeIndex < 0) {
throw new Error(`Unexpected typed array class: ${ArrayType}.`);
}
if (data) { // reconstruct an index from a buffer
this.data = data;
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
this._pos = numItems * 2;
this._finished = true;
} else { // initialize a new index
const data = this.data = new ArrayBufferType(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
this._pos = 0;
this._finished = false;
// set header
new Uint8Array(data, 0, 2).set([0xdb, (VERSION << 4) + arrayTypeIndex]);
new Uint16Array(data, 2, 1)[0] = nodeSize;
new Uint32Array(data, 4, 1)[0] = numItems;
}
}
/**
* Add a point to the index.
* @param {number} x
* @param {number} y
* @returns {number} An incremental index associated with the added item (starting from `0`).
*/
add(x, y) {
const index = this._pos >> 1;
this.ids[index] = index;
this.coords[this._pos++] = x;
this.coords[this._pos++] = y;
return index;
}
/**
* Perform indexing of the added points.
*/
finish() {
const numAdded = this._pos >> 1;
if (numAdded !== this.numItems) {
throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
}
// kd-sort both arrays for efficient search
sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);
this._finished = true;
return this;
}
/**
* Search the index for items within a given bounding box.
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {number[]} An array of indices correponding to the found items.
*/
range(minX, minY, maxX, maxY) {
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
const {ids, coords, nodeSize} = this;
STACK[0] = 0;
STACK[1] = ids.length - 1;
STACK[2] = 0;
let sp = 3;
const result = [];
// recursively search for items in range in the kd-sorted arrays
while (sp > 0) {
const axis = STACK[--sp];
const right = STACK[--sp];
const left = STACK[--sp];
// if we reached "tree node", search linearly
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
const x = coords[2 * i];
const y = coords[2 * i + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]);
}
continue;
}
// otherwise find the middle index
const m = (left + right) >> 1;
// include the middle item if it's in range
const x = coords[2 * m];
const y = coords[2 * m + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);
// queue search in halves that intersect the query
if (axis === 0 ? minX <= x : minY <= y) {
STACK[sp++] = left;
STACK[sp++] = m - 1;
STACK[sp++] = 1 - axis;
}
if (axis === 0 ? maxX >= x : maxY >= y) {
STACK[sp++] = m + 1;
STACK[sp++] = right;
STACK[sp++] = 1 - axis;
}
}
return result;
}
/**
* Search the index for items within a given radius.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @returns {number[]} An array of indices correponding to the found items.
*/
within(qx, qy, r) {
const result = /** @type {number[]} */ ([]);
this.withinInto(qx, qy, r, result);
return result;
}
/**
* Search the index for items within a given radius, writing matching ids into `out`
* via indexed assignment (`out[i] = id`). Accepts any indexed-writable container —
* a typed array sized to the expected upper bound (allocation-free, fast) or a plain
* `Array` (which will grow as needed). Returns the number of matches written.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @param {number[] | TypedArray} out Container to write matching ids into.
* @returns {number} The number of matches written to `out`.
*/
withinInto(qx, qy, r, out) {
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
const {ids, coords, nodeSize} = this;
STACK[0] = 0;
STACK[1] = ids.length - 1;
STACK[2] = 0;
let sp = 3;
let count = 0;
const r2 = r * r;
// recursively search for items within radius in the kd-sorted arrays
while (sp > 0) {
const axis = STACK[--sp];
const right = STACK[--sp];
const left = STACK[--sp];
// if we reached "tree node", search linearly
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) out[count++] = ids[i];
}
continue;
}
// otherwise find the middle index
const m = (left + right) >> 1;
// include the middle item if it's in range
const x = coords[2 * m];
const y = coords[2 * m + 1];
if (sqDist(x, y, qx, qy) <= r2) out[count++] = ids[m];
// queue search in halves that intersect the query
if (axis === 0 ? qx - r <= x : qy - r <= y) {
STACK[sp++] = left;
STACK[sp++] = m - 1;
STACK[sp++] = 1 - axis;
}
if (axis === 0 ? qx + r >= x : qy + r >= y) {
STACK[sp++] = m + 1;
STACK[sp++] = right;
STACK[sp++] = 1 - axis;
}
}
return count;
}
}
/**
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} nodeSize
* @param {number} left
* @param {number} right
* @param {number} axis
*/
function sort(ids, coords, nodeSize, left, right, axis) {
if (right - left <= nodeSize) return;
const m = (left + right) >> 1; // middle index
// sort ids and coords around the middle index so that the halves lie
// either left/right or top/bottom correspondingly (taking turns)
select(ids, coords, m, left, right, axis);
// recursively kd-sort first half and second half on the opposite axis
sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
}
/**
* Custom Floyd-Rivest selection algorithm: sort ids and coords so that
* [left..k-1] items are smaller than k-th item (on either x or y axis)
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} k
* @param {number} left
* @param {number} right
* @param {number} axis
*/
function select(ids, coords, k, left, right, axis) {
while (right > left) {
if (right - left > 600) {
const n = right - left + 1;
const m = k - left + 1;
const z = Math.log(n);
const s = 0.5 * Math.exp(2 * z / 3);
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
select(ids, coords, k, newLeft, newRight, axis);
}
const t = coords[2 * k + axis];
let i = left;
let j = right;
swapItem(ids, coords, left, k);
if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);
while (i < j) {
swapItem(ids, coords, i, j);
i++;
j--;
while (coords[2 * i + axis] < t) i++;
while (coords[2 * j + axis] > t) j--;
}
if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
else {
j++;
swapItem(ids, coords, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
/**
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} i
* @param {number} j
*/
function swapItem(ids, coords, i, j) {
swap(ids, i, j);
swap(coords, 2 * i, 2 * j);
swap(coords, 2 * i + 1, 2 * j + 1);
}
/**
* @param {TypedArray} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* @param {number} ax
* @param {number} ay
* @param {number} bx
* @param {number} by
*/
function sqDist(ax, ay, bx, by) {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
+371
View File
@@ -0,0 +1,371 @@
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.KDBush = factory());
})(this, (function () { 'use strict';
const ARRAY_TYPES = [
Int8Array, Uint8Array, Uint8ClampedArray, Int16Array, Uint16Array,
Int32Array, Uint32Array, Float32Array, Float64Array
];
/** @typedef {Int8ArrayConstructor | Uint8ArrayConstructor | Uint8ClampedArrayConstructor | Int16ArrayConstructor | Uint16ArrayConstructor | Int32ArrayConstructor | Uint32ArrayConstructor | Float32ArrayConstructor | Float64ArrayConstructor} TypedArrayConstructor */
/** @typedef {Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array} TypedArray */
const VERSION = 1; // serialized format version
const HEADER_SIZE = 8;
// Shared scratch stack for iterative DFS in range/within. Sized for the worst case:
// 3 ints per frame * (treeHeight + 1), with treeHeight ≤ ceil(log2(2^32 / 3)) ≈ 31.
const STACK = new Uint32Array(96);
class KDBush {
/**
* Creates an index from raw `ArrayBuffer` data.
* @param {ArrayBufferLike} data
*/
static from(data) {
// @ts-expect-error duck typing array buffers
if (!data || data.byteLength === undefined || data.buffer) {
throw new Error('Data must be an instance of ArrayBuffer or SharedArrayBuffer.');
}
const [magic, versionAndType] = new Uint8Array(data, 0, 2);
if (magic !== 0xdb) {
throw new Error('Data does not appear to be in a KDBush format.');
}
const version = versionAndType >> 4;
if (version !== VERSION) {
throw new Error(`Got v${version} data when expected v${VERSION}.`);
}
const ArrayType = ARRAY_TYPES[versionAndType & 0x0f];
if (!ArrayType) {
throw new Error('Unrecognized array type.');
}
const [nodeSize] = new Uint16Array(data, 2, 1);
const [numItems] = new Uint32Array(data, 4, 1);
return new KDBush(numItems, nodeSize, ArrayType, undefined, data);
}
/**
* Creates an index that will hold a given number of items.
* @param {number} numItems
* @param {number} [nodeSize=64] Size of the KD-tree node (64 by default).
* @param {TypedArrayConstructor} [ArrayType=Float64Array] The array type used for coordinates storage (`Float64Array` by default).
* @param {ArrayBufferConstructor | SharedArrayBufferConstructor} [ArrayBufferType=ArrayBuffer] The array buffer type used for storage (`ArrayBuffer` by default).
* @param {ArrayBufferLike} [data] (For internal use only)
*/
constructor(numItems, nodeSize = 64, ArrayType = Float64Array, ArrayBufferType = ArrayBuffer, data) {
if (isNaN(numItems) || numItems < 0) throw new Error(`Unexpected numItems value: ${numItems}.`);
this.numItems = +numItems;
this.nodeSize = Math.min(Math.max(+nodeSize, 2), 65535);
this.ArrayType = ArrayType;
this.IndexArrayType = numItems < 65536 ? Uint16Array : Uint32Array;
const arrayTypeIndex = ARRAY_TYPES.indexOf(this.ArrayType);
const coordsByteSize = numItems * 2 * this.ArrayType.BYTES_PER_ELEMENT;
const idsByteSize = numItems * this.IndexArrayType.BYTES_PER_ELEMENT;
const padCoords = (8 - idsByteSize % 8) % 8;
if (arrayTypeIndex < 0) {
throw new Error(`Unexpected typed array class: ${ArrayType}.`);
}
if (data) { // reconstruct an index from a buffer
this.data = data;
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
this._pos = numItems * 2;
this._finished = true;
} else { // initialize a new index
const data = this.data = new ArrayBufferType(HEADER_SIZE + coordsByteSize + idsByteSize + padCoords);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.ids = new this.IndexArrayType(data, HEADER_SIZE, numItems);
// @ts-expect-error TS can't handle SharedArrayBuffer overloads
this.coords = new ArrayType(data, HEADER_SIZE + idsByteSize + padCoords, numItems * 2);
this._pos = 0;
this._finished = false;
// set header
new Uint8Array(data, 0, 2).set([0xdb, (VERSION << 4) + arrayTypeIndex]);
new Uint16Array(data, 2, 1)[0] = nodeSize;
new Uint32Array(data, 4, 1)[0] = numItems;
}
}
/**
* Add a point to the index.
* @param {number} x
* @param {number} y
* @returns {number} An incremental index associated with the added item (starting from `0`).
*/
add(x, y) {
const index = this._pos >> 1;
this.ids[index] = index;
this.coords[this._pos++] = x;
this.coords[this._pos++] = y;
return index;
}
/**
* Perform indexing of the added points.
*/
finish() {
const numAdded = this._pos >> 1;
if (numAdded !== this.numItems) {
throw new Error(`Added ${numAdded} items when expected ${this.numItems}.`);
}
// kd-sort both arrays for efficient search
sort(this.ids, this.coords, this.nodeSize, 0, this.numItems - 1, 0);
this._finished = true;
return this;
}
/**
* Search the index for items within a given bounding box.
* @param {number} minX
* @param {number} minY
* @param {number} maxX
* @param {number} maxY
* @returns {number[]} An array of indices correponding to the found items.
*/
range(minX, minY, maxX, maxY) {
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
const {ids, coords, nodeSize} = this;
STACK[0] = 0;
STACK[1] = ids.length - 1;
STACK[2] = 0;
let sp = 3;
const result = [];
// recursively search for items in range in the kd-sorted arrays
while (sp > 0) {
const axis = STACK[--sp];
const right = STACK[--sp];
const left = STACK[--sp];
// if we reached "tree node", search linearly
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
const x = coords[2 * i];
const y = coords[2 * i + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[i]);
}
continue;
}
// otherwise find the middle index
const m = (left + right) >> 1;
// include the middle item if it's in range
const x = coords[2 * m];
const y = coords[2 * m + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) result.push(ids[m]);
// queue search in halves that intersect the query
if (axis === 0 ? minX <= x : minY <= y) {
STACK[sp++] = left;
STACK[sp++] = m - 1;
STACK[sp++] = 1 - axis;
}
if (axis === 0 ? maxX >= x : maxY >= y) {
STACK[sp++] = m + 1;
STACK[sp++] = right;
STACK[sp++] = 1 - axis;
}
}
return result;
}
/**
* Search the index for items within a given radius.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @returns {number[]} An array of indices correponding to the found items.
*/
within(qx, qy, r) {
const result = /** @type {number[]} */ ([]);
this.withinInto(qx, qy, r, result);
return result;
}
/**
* Search the index for items within a given radius, writing matching ids into `out`
* via indexed assignment (`out[i] = id`). Accepts any indexed-writable container —
* a typed array sized to the expected upper bound (allocation-free, fast) or a plain
* `Array` (which will grow as needed). Returns the number of matches written.
* @param {number} qx
* @param {number} qy
* @param {number} r Query radius.
* @param {number[] | TypedArray} out Container to write matching ids into.
* @returns {number} The number of matches written to `out`.
*/
withinInto(qx, qy, r, out) {
if (!this._finished) throw new Error('Data not yet indexed - call index.finish().');
const {ids, coords, nodeSize} = this;
STACK[0] = 0;
STACK[1] = ids.length - 1;
STACK[2] = 0;
let sp = 3;
let count = 0;
const r2 = r * r;
// recursively search for items within radius in the kd-sorted arrays
while (sp > 0) {
const axis = STACK[--sp];
const right = STACK[--sp];
const left = STACK[--sp];
// if we reached "tree node", search linearly
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) out[count++] = ids[i];
}
continue;
}
// otherwise find the middle index
const m = (left + right) >> 1;
// include the middle item if it's in range
const x = coords[2 * m];
const y = coords[2 * m + 1];
if (sqDist(x, y, qx, qy) <= r2) out[count++] = ids[m];
// queue search in halves that intersect the query
if (axis === 0 ? qx - r <= x : qy - r <= y) {
STACK[sp++] = left;
STACK[sp++] = m - 1;
STACK[sp++] = 1 - axis;
}
if (axis === 0 ? qx + r >= x : qy + r >= y) {
STACK[sp++] = m + 1;
STACK[sp++] = right;
STACK[sp++] = 1 - axis;
}
}
return count;
}
}
/**
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} nodeSize
* @param {number} left
* @param {number} right
* @param {number} axis
*/
function sort(ids, coords, nodeSize, left, right, axis) {
if (right - left <= nodeSize) return;
const m = (left + right) >> 1; // middle index
// sort ids and coords around the middle index so that the halves lie
// either left/right or top/bottom correspondingly (taking turns)
select(ids, coords, m, left, right, axis);
// recursively kd-sort first half and second half on the opposite axis
sort(ids, coords, nodeSize, left, m - 1, 1 - axis);
sort(ids, coords, nodeSize, m + 1, right, 1 - axis);
}
/**
* Custom Floyd-Rivest selection algorithm: sort ids and coords so that
* [left..k-1] items are smaller than k-th item (on either x or y axis)
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} k
* @param {number} left
* @param {number} right
* @param {number} axis
*/
function select(ids, coords, k, left, right, axis) {
while (right > left) {
if (right - left > 600) {
const n = right - left + 1;
const m = k - left + 1;
const z = Math.log(n);
const s = 0.5 * Math.exp(2 * z / 3);
const sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
const newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
const newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
select(ids, coords, k, newLeft, newRight, axis);
}
const t = coords[2 * k + axis];
let i = left;
let j = right;
swapItem(ids, coords, left, k);
if (coords[2 * right + axis] > t) swapItem(ids, coords, left, right);
while (i < j) {
swapItem(ids, coords, i, j);
i++;
j--;
while (coords[2 * i + axis] < t) i++;
while (coords[2 * j + axis] > t) j--;
}
if (coords[2 * left + axis] === t) swapItem(ids, coords, left, j);
else {
j++;
swapItem(ids, coords, j, right);
}
if (j <= k) left = j + 1;
if (k <= j) right = j - 1;
}
}
/**
* @param {Uint16Array | Uint32Array} ids
* @param {TypedArray} coords
* @param {number} i
* @param {number} j
*/
function swapItem(ids, coords, i, j) {
swap(ids, i, j);
swap(coords, 2 * i, 2 * j);
swap(coords, 2 * i + 1, 2 * j + 1);
}
/**
* @param {TypedArray} arr
* @param {number} i
* @param {number} j
*/
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
/**
* @param {number} ax
* @param {number} ay
* @param {number} bx
* @param {number} by
*/
function sqDist(ax, ay, bx, by) {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
return KDBush;
}));
+1
View File
@@ -0,0 +1 @@
!function(t,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define(r):(t="undefined"!=typeof globalThis?globalThis:t||self).KDBush=r()}(this,function(){"use strict";const t=[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array],r=new Uint32Array(96);class n{static from(r){if(!r||void 0===r.byteLength||r.buffer)throw new Error("Data must be an instance of ArrayBuffer or SharedArrayBuffer.");const[e,i]=new Uint8Array(r,0,2);if(219!==e)throw new Error("Data does not appear to be in a KDBush format.");const s=i>>4;if(1!==s)throw new Error(`Got v${s} data when expected v1.`);const o=t[15&i];if(!o)throw new Error("Unrecognized array type.");const[a]=new Uint16Array(r,2,1),[h]=new Uint32Array(r,4,1);return new n(h,a,o,void 0,r)}constructor(r,n=64,e=Float64Array,i=ArrayBuffer,s){if(isNaN(r)||r<0)throw new Error(`Unexpected numItems value: ${r}.`);this.numItems=+r,this.nodeSize=Math.min(Math.max(+n,2),65535),this.ArrayType=e,this.IndexArrayType=r<65536?Uint16Array:Uint32Array;const o=t.indexOf(this.ArrayType),a=2*r*this.ArrayType.BYTES_PER_ELEMENT,h=r*this.IndexArrayType.BYTES_PER_ELEMENT,d=(8-h%8)%8;if(o<0)throw new Error(`Unexpected typed array class: ${e}.`);if(s)this.data=s,this.ids=new this.IndexArrayType(s,8,r),this.coords=new e(s,8+h+d,2*r),this._pos=2*r,this._finished=!0;else{const t=this.data=new i(8+a+h+d);this.ids=new this.IndexArrayType(t,8,r),this.coords=new e(t,8+h+d,2*r),this._pos=0,this._finished=!1,new Uint8Array(t,0,2).set([219,16+o]),new Uint16Array(t,2,1)[0]=n,new Uint32Array(t,4,1)[0]=r}}add(t,r){const n=this._pos>>1;return this.ids[n]=n,this.coords[this._pos++]=t,this.coords[this._pos++]=r,n}finish(){const t=this._pos>>1;if(t!==this.numItems)throw new Error(`Added ${t} items when expected ${this.numItems}.`);return e(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(t,n,e,i){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:s,coords:o,nodeSize:a}=this;r[0]=0,r[1]=s.length-1,r[2]=0;let h=3;const d=[];for(;h>0;){const f=r[--h],c=r[--h],y=r[--h];if(c-y<=a){for(let r=y;r<=c;r++){const a=o[2*r],h=o[2*r+1];a>=t&&a<=e&&h>=n&&h<=i&&d.push(s[r])}continue}const u=y+c>>1,w=o[2*u],p=o[2*u+1];w>=t&&w<=e&&p>=n&&p<=i&&d.push(s[u]),(0===f?t<=w:n<=p)&&(r[h++]=y,r[h++]=u-1,r[h++]=1-f),(0===f?e>=w:i>=p)&&(r[h++]=u+1,r[h++]=c,r[h++]=1-f)}return d}within(t,r,n){const e=[];return this.withinInto(t,r,n,e),e}withinInto(t,n,e,i){if(!this._finished)throw new Error("Data not yet indexed - call index.finish().");const{ids:s,coords:o,nodeSize:h}=this;r[0]=0,r[1]=s.length-1,r[2]=0;let d=3,f=0;const c=e*e;for(;d>0;){const y=r[--d],u=r[--d],w=r[--d];if(u-w<=h){for(let r=w;r<=u;r++)a(o[2*r],o[2*r+1],t,n)<=c&&(i[f++]=s[r]);continue}const p=w+u>>1,A=o[2*p],l=o[2*p+1];a(A,l,t,n)<=c&&(i[f++]=s[p]),(0===y?t-e<=A:n-e<=l)&&(r[d++]=w,r[d++]=p-1,r[d++]=1-y),(0===y?t+e>=A:n+e>=l)&&(r[d++]=p+1,r[d++]=u,r[d++]=1-y)}return f}}function e(t,r,n,s,o,a){if(o-s<=n)return;const h=s+o>>1;i(t,r,h,s,o,a),e(t,r,n,s,h-1,1-a),e(t,r,n,h+1,o,1-a)}function i(t,r,n,e,o,a){for(;o>e;){if(o-e>600){const s=o-e+1,h=n-e+1,d=Math.log(s),f=.5*Math.exp(2*d/3),c=.5*Math.sqrt(d*f*(s-f)/s)*(h-s/2<0?-1:1);i(t,r,n,Math.max(e,Math.floor(n-h*f/s+c)),Math.min(o,Math.floor(n+(s-h)*f/s+c)),a)}const h=r[2*n+a];let d=e,f=o;for(s(t,r,e,n),r[2*o+a]>h&&s(t,r,e,o);d<f;){for(s(t,r,d,f),d++,f--;r[2*d+a]<h;)d++;for(;r[2*f+a]>h;)f--}r[2*e+a]===h?s(t,r,e,f):(f++,s(t,r,f,o)),f<=n&&(e=f+1),n<=f&&(o=f-1)}}function s(t,r,n,e){o(t,n,e),o(r,2*n,2*e),o(r,2*n+1,2*e+1)}function o(t,r,n){const e=t[r];t[r]=t[n],t[n]=e}function a(t,r,n,e){const i=t-n,s=r-e;return i*i+s*s}return n});
+46
View File
@@ -0,0 +1,46 @@
{
"name": "kdbush",
"version": "4.1.0",
"description": "A very fast static 2D index for points based on kd-tree.",
"type": "module",
"main": "kdbush.js",
"module": "index.js",
"exports": "./index.js",
"types": "index.d.ts",
"sideEffects": false,
"repository": {
"type": "git",
"url": "git://github.com/mourner/kdbush.git"
},
"devDependencies": {
"@rollup/plugin-terser": "^1.0.0",
"eslint": "^10.4.0",
"eslint-config-mourner": "^4.1.0",
"rollup": "^4.60.4",
"typescript": "^6.0.3"
},
"scripts": {
"pretest": "eslint",
"test": "tsc && node test.js",
"bench": "node bench.js",
"build": "rollup -c",
"prepublishOnly": "npm run test && npm run build"
},
"keywords": [
"index",
"points",
"kd-tree",
"data structures",
"algorithms",
"spatial",
"geometry"
],
"files": [
"kdbush.js",
"kdbush.min.js",
"index.js",
"index.d.ts"
],
"author": "Vladimir Agafonkin",
"license": "ISC"
}