@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 +21 -0
- package/README.md +132 -0
- package/package.json +44 -0
- package/src/index.js +4 -0
- package/src/knn.js +66 -0
- package/src/radius.js +48 -0
- package/src/ray.js +102 -0
- package/src/validate.js +13 -0
- package/src/within.js +29 -0
- package/types/index.d.ts +4 -0
- package/types/knn.d.ts +20 -0
- package/types/radius.d.ts +19 -0
- package/types/ray.d.ts +22 -0
- package/types/validate.d.ts +2 -0
- package/types/within.d.ts +9 -0
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
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
|
+
}
|
package/src/validate.js
ADDED
|
@@ -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
|
+
}
|
package/types/index.d.ts
ADDED
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,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[];
|