@gridworkjs/query 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 gridworkjs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,132 @@
1
+ <p align="center">
2
+ <img src="logo.svg" width="256" height="256" alt="@gridworkjs/query">
3
+ </p>
4
+
5
+ <h1 align="center">@gridworkjs/query</h1>
6
+
7
+ <p align="center">Higher-level spatial queries against any gridwork index</p>
8
+
9
+ ## Install
10
+
11
+ ```
12
+ npm install @gridworkjs/query
13
+ ```
14
+
15
+ ## Why
16
+
17
+ Spatial indexes give you `search()` (rectangular intersection) and `nearest()` (k closest items). But real applications need more: circular radius searches, raycasting, containment checks, distance-annotated results, filtered nearest-neighbor. This package provides those queries as standalone functions that work with any gridwork index.
18
+
19
+ ## Usage
20
+
21
+ Every query function takes a spatial index as its first argument. The index carries its own accessor, so you never pass it twice.
22
+
23
+ ```js
24
+ import { radius, knn, ray, within } from '@gridworkjs/query'
25
+ import { createQuadtree } from '@gridworkjs/quadtree'
26
+ import { point, rect, bounds } from '@gridworkjs/core'
27
+
28
+ const tree = createQuadtree(entity => bounds(entity.position))
29
+ tree.insert({ id: 'player', position: point(100, 200), hp: 80 })
30
+ tree.insert({ id: 'enemy-1', position: point(120, 210), hp: 50 })
31
+ tree.insert({ id: 'enemy-2', position: point(400, 100), hp: 90 })
32
+ tree.insert({ id: 'chest', position: point(105, 195), hp: null })
33
+ ```
34
+
35
+ ### Radius - "What's near me?"
36
+
37
+ Find all entities within 50 units of the player. A tower defense game checking which enemies are in range:
38
+
39
+ ```js
40
+ const inRange = radius(tree, { x: 100, y: 200 }, 50)
41
+ // => [{ item: { id: 'chest', ... }, distance: 7.07 },
42
+ // { item: { id: 'enemy-1', ... }, distance: 22.36 }]
43
+
44
+ for (const { item, distance } of inRange) {
45
+ if (item.hp != null) dealDamage(item, falloff(distance))
46
+ }
47
+ ```
48
+
49
+ ### KNN - "What's closest?"
50
+
51
+ Find the 3 nearest items to a point, but only enemies, and only within 200 units. An AI deciding which target to engage:
52
+
53
+ ```js
54
+ const targets = knn(tree, { x: 100, y: 200 }, 3, {
55
+ maxDistance: 200,
56
+ filter: item => item.id.startsWith('enemy')
57
+ })
58
+ // => [{ item: { id: 'enemy-1', ... }, distance: 22.36 }]
59
+
60
+ const primary = targets[0]?.item
61
+ ```
62
+
63
+ ### Ray - "What does this line hit?"
64
+
65
+ Cast a ray from the player eastward. A bullet, a line of sight check, or a laser:
66
+
67
+ ```js
68
+ const hits = ray(tree, { x: 100, y: 200 }, { x: 1, y: 0 })
69
+ // => [{ item: { id: 'chest', ... }, distance: 5 },
70
+ // { item: { id: 'enemy-2', ... }, distance: 300 }]
71
+
72
+ const firstHit = hits[0]?.item
73
+ ```
74
+
75
+ ### Within - "What's fully inside this area?"
76
+
77
+ Find all entities completely contained in a selection rectangle. A strategy game box-selecting units:
78
+
79
+ ```js
80
+ const selected = within(tree, rect(90, 190, 130, 220))
81
+ // => [{ id: 'player', ... }, { id: 'chest', ... }, { id: 'enemy-1', ... }]
82
+ ```
83
+
84
+ ## API
85
+
86
+ ### `radius(index, point, r, options?)`
87
+
88
+ Find all items within distance `r` of a point. Returns `{ item, distance }[]` sorted by distance ascending.
89
+
90
+ - `index` - any spatial index implementing the gridwork protocol
91
+ - `point` - `{ x, y }` center point
92
+ - `r` - search radius (non-negative)
93
+ - `options.filter` - optional predicate to filter results
94
+
95
+ ### `knn(index, point, k, options?)`
96
+
97
+ Find the `k` nearest items to a point with distance annotations. Returns `{ item, distance }[]` sorted by distance ascending.
98
+
99
+ - `index` - any spatial index implementing the gridwork protocol
100
+ - `point` - `{ x, y }` query point
101
+ - `k` - number of nearest neighbors (positive integer)
102
+ - `options.maxDistance` - exclude items farther than this distance
103
+ - `options.filter` - predicate to filter candidates
104
+
105
+ ### `ray(index, origin, direction, options?)`
106
+
107
+ Cast a ray and find all items it intersects. Returns `{ item, distance }[]` sorted by distance along the ray.
108
+
109
+ - `index` - any spatial index implementing the gridwork protocol
110
+ - `origin` - `{ x, y }` ray starting point
111
+ - `direction` - `{ x, y }` direction vector (automatically normalized)
112
+ - `options.maxDistance` - maximum ray length
113
+
114
+ ### `within(index, region)`
115
+
116
+ Find all items fully contained within a region. Unlike `search()` which returns items that intersect, `within()` requires complete containment. Returns plain items (no distance annotation).
117
+
118
+ - `index` - any spatial index implementing the gridwork protocol
119
+ - `region` - bounds object, or any gridwork geometry (point, rect, circle)
120
+
121
+ ## Works with Any Gridwork Index
122
+
123
+ These functions accept any object implementing the gridwork spatial index protocol. Use whichever index fits your data:
124
+
125
+ - `@gridworkjs/quadtree` - dynamic, sparse data
126
+ - `@gridworkjs/rtree` - rectangles, bulk loading
127
+ - `@gridworkjs/hashgrid` - uniform distributions
128
+ - `@gridworkjs/kd` - static point sets
129
+
130
+ ## License
131
+
132
+ MIT
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@gridworkjs/query",
3
+ "version": "1.0.0",
4
+ "description": "Higher-level spatial queries against any gridwork index",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "./types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./types/index.d.ts",
11
+ "default": "./src/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "test": "node --test",
16
+ "prepublishOnly": "npx tsc --declaration --allowJs --emitDeclarationOnly --skipLibCheck --target es2020 --module nodenext --moduleResolution nodenext --strict false --esModuleInterop true --outDir ./types src/index.js"
17
+ },
18
+ "keywords": [
19
+ "spatial",
20
+ "query",
21
+ "knn",
22
+ "radius",
23
+ "raycast",
24
+ "containment",
25
+ "spatial-index",
26
+ "gridwork"
27
+ ],
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/gridworkjs/query.git"
32
+ },
33
+ "files": [
34
+ "src/",
35
+ "types/"
36
+ ],
37
+ "dependencies": {
38
+ "@gridworkjs/core": "^1.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@gridworkjs/quadtree": "file:../quadtree",
42
+ "typescript": "^5.9.3"
43
+ }
44
+ }
package/src/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { radius } from './radius.js'
2
+ export { knn } from './knn.js'
3
+ export { ray } from './ray.js'
4
+ export { within } from './within.js'
package/src/knn.js ADDED
@@ -0,0 +1,66 @@
1
+ import { bounds, distanceToPoint } from '@gridworkjs/core/bounds'
2
+ import { validateIndex, validateFiniteNumber } from './validate.js'
3
+
4
+ /**
5
+ * Enhanced k-nearest-neighbor query with maxDistance and filter support.
6
+ * Returns { item, distance }[] sorted by distance ascending.
7
+ *
8
+ * @param {object} index - A spatial index implementing the gridwork protocol
9
+ * @param {{ x: number, y: number }} point - Query point
10
+ * @param {number} k - Number of nearest neighbors to find
11
+ * @param {{ maxDistance?: number, filter?: function }} [options]
12
+ * @returns {{ item: any, distance: number }[]}
13
+ */
14
+ export function knn(index, point, k, options) {
15
+ validateIndex(index)
16
+ validateFiniteNumber(point.x, 'point.x')
17
+ validateFiniteNumber(point.y, 'point.y')
18
+
19
+ if (typeof k !== 'number' || !Number.isInteger(k)) {
20
+ throw new Error('k must be an integer')
21
+ }
22
+ if (k <= 0) return []
23
+
24
+ const accessor = index.accessor
25
+ const maxDistance = options && options.maxDistance
26
+ const filter = options && options.filter
27
+
28
+ if (maxDistance !== undefined && maxDistance !== null) {
29
+ validateFiniteNumber(maxDistance, 'maxDistance')
30
+ if (maxDistance < 0) throw new Error('maxDistance must be non-negative')
31
+ }
32
+
33
+ const candidates = index.nearest(point, filter ? k * 4 : k)
34
+ const results = []
35
+
36
+ for (let i = 0; i < candidates.length && results.length < k; i++) {
37
+ const item = candidates[i]
38
+ const b = bounds(accessor(item))
39
+ const dist = distanceToPoint(b, point.x, point.y)
40
+
41
+ if (maxDistance != null && dist > maxDistance) break
42
+
43
+ if (!filter || filter(item)) {
44
+ results.push({ item, distance: dist })
45
+ }
46
+ }
47
+
48
+ if (filter && results.length < k && candidates.length > 0) {
49
+ const allItems = index.nearest(point, index.size)
50
+ results.length = 0
51
+
52
+ for (let i = 0; i < allItems.length && results.length < k; i++) {
53
+ const item = allItems[i]
54
+ const b = bounds(accessor(item))
55
+ const dist = distanceToPoint(b, point.x, point.y)
56
+
57
+ if (maxDistance != null && dist > maxDistance) break
58
+
59
+ if (filter(item)) {
60
+ results.push({ item, distance: dist })
61
+ }
62
+ }
63
+ }
64
+
65
+ return results
66
+ }
package/src/radius.js ADDED
@@ -0,0 +1,48 @@
1
+ import { bounds, distanceToPoint } from '@gridworkjs/core/bounds'
2
+ import { validateIndex, validateFiniteNumber } from './validate.js'
3
+
4
+ /**
5
+ * Find all items within a given radius of a point.
6
+ * Returns { item, distance }[] sorted by distance ascending.
7
+ *
8
+ * @param {object} index - A spatial index implementing the gridwork protocol
9
+ * @param {{ x: number, y: number }} point - Center point
10
+ * @param {number} r - Search radius
11
+ * @param {{ filter?: function }} [options]
12
+ * @returns {{ item: any, distance: number }[]}
13
+ */
14
+ export function radius(index, point, r, options) {
15
+ validateIndex(index)
16
+ validateFiniteNumber(point.x, 'point.x')
17
+ validateFiniteNumber(point.y, 'point.y')
18
+ validateFiniteNumber(r, 'radius')
19
+
20
+ if (r < 0) throw new Error('radius must be non-negative')
21
+ if (r === 0) return []
22
+
23
+ const accessor = index.accessor
24
+ const searchBounds = {
25
+ minX: point.x - r,
26
+ minY: point.y - r,
27
+ maxX: point.x + r,
28
+ maxY: point.y + r
29
+ }
30
+
31
+ const candidates = index.search(searchBounds)
32
+ const filter = options && options.filter
33
+ const results = []
34
+
35
+ for (let i = 0; i < candidates.length; i++) {
36
+ const item = candidates[i]
37
+ const b = bounds(accessor(item))
38
+ const dist = distanceToPoint(b, point.x, point.y)
39
+ if (dist <= r) {
40
+ if (!filter || filter(item)) {
41
+ results.push({ item, distance: dist })
42
+ }
43
+ }
44
+ }
45
+
46
+ results.sort((a, b) => a.distance - b.distance)
47
+ return results
48
+ }
package/src/ray.js ADDED
@@ -0,0 +1,102 @@
1
+ import { bounds } from '@gridworkjs/core/bounds'
2
+ import { validateIndex, validateFiniteNumber } from './validate.js'
3
+
4
+ /**
5
+ * Cast a ray through a spatial index and find all items it intersects.
6
+ * Returns { item, distance }[] sorted by distance along the ray.
7
+ *
8
+ * @param {object} index - A spatial index implementing the gridwork protocol
9
+ * @param {{ x: number, y: number }} origin - Ray origin point
10
+ * @param {{ x: number, y: number }} direction - Ray direction vector (will be normalized)
11
+ * @param {{ maxDistance?: number }} [options]
12
+ * @returns {{ item: any, distance: number }[]}
13
+ */
14
+ export function ray(index, origin, direction, options) {
15
+ validateIndex(index)
16
+ validateFiniteNumber(origin.x, 'origin.x')
17
+ validateFiniteNumber(origin.y, 'origin.y')
18
+ validateFiniteNumber(direction.x, 'direction.x')
19
+ validateFiniteNumber(direction.y, 'direction.y')
20
+
21
+ const len = Math.sqrt(direction.x * direction.x + direction.y * direction.y)
22
+ if (len === 0) throw new Error('direction vector must be non-zero')
23
+
24
+ const dx = direction.x / len
25
+ const dy = direction.y / len
26
+
27
+ const maxDist = (options && options.maxDistance) || Infinity
28
+ if (options && options.maxDistance !== undefined) {
29
+ validateFiniteNumber(options.maxDistance, 'maxDistance')
30
+ if (options.maxDistance < 0) throw new Error('maxDistance must be non-negative')
31
+ }
32
+
33
+ const indexBounds = index.bounds
34
+ if (!indexBounds) return []
35
+
36
+ const accessor = index.accessor
37
+ const searchBounds = raySearchBounds(origin, dx, dy, maxDist, indexBounds)
38
+ const candidates = index.search(searchBounds)
39
+ const results = []
40
+
41
+ for (let i = 0; i < candidates.length; i++) {
42
+ const item = candidates[i]
43
+ const b = bounds(accessor(item))
44
+ const t = rayIntersectsAABB(origin, dx, dy, b)
45
+
46
+ if (t !== null && t <= maxDist) {
47
+ results.push({ item, distance: t })
48
+ }
49
+ }
50
+
51
+ results.sort((a, b) => a.distance - b.distance)
52
+ return results
53
+ }
54
+
55
+ function raySearchBounds(origin, dx, dy, maxDist, indexBounds) {
56
+ let endDist = maxDist
57
+ if (!Number.isFinite(endDist)) {
58
+ const w = indexBounds.maxX - indexBounds.minX
59
+ const h = indexBounds.maxY - indexBounds.minY
60
+ endDist = Math.sqrt(w * w + h * h) * 2
61
+ }
62
+
63
+ const endX = origin.x + dx * endDist
64
+ const endY = origin.y + dy * endDist
65
+
66
+ return {
67
+ minX: Math.min(origin.x, endX),
68
+ minY: Math.min(origin.y, endY),
69
+ maxX: Math.max(origin.x, endX),
70
+ maxY: Math.max(origin.y, endY)
71
+ }
72
+ }
73
+
74
+ function rayIntersectsAABB(origin, dx, dy, box) {
75
+ let tmin = -Infinity
76
+ let tmax = Infinity
77
+
78
+ if (dx !== 0) {
79
+ let t1 = (box.minX - origin.x) / dx
80
+ let t2 = (box.maxX - origin.x) / dx
81
+ if (t1 > t2) { const tmp = t1; t1 = t2; t2 = tmp }
82
+ tmin = Math.max(tmin, t1)
83
+ tmax = Math.min(tmax, t2)
84
+ } else {
85
+ if (origin.x < box.minX || origin.x > box.maxX) return null
86
+ }
87
+
88
+ if (dy !== 0) {
89
+ let t1 = (box.minY - origin.y) / dy
90
+ let t2 = (box.maxY - origin.y) / dy
91
+ if (t1 > t2) { const tmp = t1; t1 = t2; t2 = tmp }
92
+ tmin = Math.max(tmin, t1)
93
+ tmax = Math.min(tmax, t2)
94
+ } else {
95
+ if (origin.y < box.minY || origin.y > box.maxY) return null
96
+ }
97
+
98
+ if (tmin > tmax) return null
99
+ if (tmax < 0) return null
100
+
101
+ return tmin >= 0 ? tmin : 0
102
+ }
@@ -0,0 +1,13 @@
1
+ import { isSpatialIndex } from '@gridworkjs/core/protocol'
2
+
3
+ export function validateIndex(index) {
4
+ if (!isSpatialIndex(index)) {
5
+ throw new Error('first argument must be a spatial index')
6
+ }
7
+ }
8
+
9
+ export function validateFiniteNumber(value, name) {
10
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
11
+ throw new Error(`${name} must be a finite number`)
12
+ }
13
+ }
package/src/within.js ADDED
@@ -0,0 +1,29 @@
1
+ import { bounds, contains } from '@gridworkjs/core/bounds'
2
+ import { validateIndex } from './validate.js'
3
+
4
+ /**
5
+ * Find all items fully contained within a region.
6
+ * Unlike search() which returns items that intersect, within() requires full containment.
7
+ *
8
+ * @param {object} index - A spatial index implementing the gridwork protocol
9
+ * @param {object} region - Bounding region (bounds object or geometry)
10
+ * @returns {any[]}
11
+ */
12
+ export function within(index, region) {
13
+ validateIndex(index)
14
+
15
+ const accessor = index.accessor
16
+ const regionBounds = bounds(region)
17
+ const candidates = index.search(regionBounds)
18
+ const results = []
19
+
20
+ for (let i = 0; i < candidates.length; i++) {
21
+ const item = candidates[i]
22
+ const b = bounds(accessor(item))
23
+ if (contains(regionBounds, b)) {
24
+ results.push(item)
25
+ }
26
+ }
27
+
28
+ return results
29
+ }
@@ -0,0 +1,4 @@
1
+ export { radius } from "./radius.js";
2
+ export { knn } from "./knn.js";
3
+ export { ray } from "./ray.js";
4
+ export { within } from "./within.js";
package/types/knn.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Enhanced k-nearest-neighbor query with maxDistance and filter support.
3
+ * Returns { item, distance }[] sorted by distance ascending.
4
+ *
5
+ * @param {object} index - A spatial index implementing the gridwork protocol
6
+ * @param {{ x: number, y: number }} point - Query point
7
+ * @param {number} k - Number of nearest neighbors to find
8
+ * @param {{ maxDistance?: number, filter?: function }} [options]
9
+ * @returns {{ item: any, distance: number }[]}
10
+ */
11
+ export function knn(index: object, point: {
12
+ x: number;
13
+ y: number;
14
+ }, k: number, options?: {
15
+ maxDistance?: number;
16
+ filter?: Function;
17
+ }): {
18
+ item: any;
19
+ distance: number;
20
+ }[];
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Find all items within a given radius of a point.
3
+ * Returns { item, distance }[] sorted by distance ascending.
4
+ *
5
+ * @param {object} index - A spatial index implementing the gridwork protocol
6
+ * @param {{ x: number, y: number }} point - Center point
7
+ * @param {number} r - Search radius
8
+ * @param {{ filter?: function }} [options]
9
+ * @returns {{ item: any, distance: number }[]}
10
+ */
11
+ export function radius(index: object, point: {
12
+ x: number;
13
+ y: number;
14
+ }, r: number, options?: {
15
+ filter?: Function;
16
+ }): {
17
+ item: any;
18
+ distance: number;
19
+ }[];
package/types/ray.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cast a ray through a spatial index and find all items it intersects.
3
+ * Returns { item, distance }[] sorted by distance along the ray.
4
+ *
5
+ * @param {object} index - A spatial index implementing the gridwork protocol
6
+ * @param {{ x: number, y: number }} origin - Ray origin point
7
+ * @param {{ x: number, y: number }} direction - Ray direction vector (will be normalized)
8
+ * @param {{ maxDistance?: number }} [options]
9
+ * @returns {{ item: any, distance: number }[]}
10
+ */
11
+ export function ray(index: object, origin: {
12
+ x: number;
13
+ y: number;
14
+ }, direction: {
15
+ x: number;
16
+ y: number;
17
+ }, options?: {
18
+ maxDistance?: number;
19
+ }): {
20
+ item: any;
21
+ distance: number;
22
+ }[];
@@ -0,0 +1,2 @@
1
+ export function validateIndex(index: any): void;
2
+ export function validateFiniteNumber(value: any, name: any): void;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Find all items fully contained within a region.
3
+ * Unlike search() which returns items that intersect, within() requires full containment.
4
+ *
5
+ * @param {object} index - A spatial index implementing the gridwork protocol
6
+ * @param {object} region - Bounding region (bounds object or geometry)
7
+ * @returns {any[]}
8
+ */
9
+ export function within(index: object, region: object): any[];