@zeitfall/quadtree 1.0.0 → 1.0.1
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/.github/workflows/publish.yml +8 -4
- package/README.md +89 -0
- package/index.test.ts +155 -0
- package/package.json +21 -11
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +1 -2
|
@@ -23,10 +23,14 @@ jobs:
|
|
|
23
23
|
node-version-file: package.json
|
|
24
24
|
registry-url: 'https://registry.npmjs.org/'
|
|
25
25
|
|
|
26
|
-
- name:
|
|
27
|
-
run:
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
- name: Install Dependencies
|
|
27
|
+
run: npm ci
|
|
28
|
+
|
|
29
|
+
- name: Test
|
|
30
|
+
run: npm run test
|
|
31
|
+
|
|
32
|
+
- name: Build
|
|
33
|
+
run: npm run build
|
|
30
34
|
|
|
31
35
|
- name: Publish
|
|
32
36
|
run: npm publish --provenance --access public
|
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
## Overview
|
|
2
|
+
|
|
3
|
+
A TypeScript implementation of a region-based QuadTree for spatial partitioning of 2D point data, supporting generic payloads, capacity-based subdivision, and depth-limiting.
|
|
4
|
+
|
|
5
|
+
## Mathematical & Algorithmic Properties
|
|
6
|
+
|
|
7
|
+
| Operation / Metric | Average Case | Worst Case |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Insertion | O(log n) | O(n) |
|
|
10
|
+
| Spatial Query | O(log n + k) | O(n) |
|
|
11
|
+
| Space Complexity | O(n) | O(n) |
|
|
12
|
+
|
|
13
|
+
*Note: `n` represents the total number of inserted points; `k` represents the number of points intersecting the query region.*
|
|
14
|
+
|
|
15
|
+
## API Reference
|
|
16
|
+
|
|
17
|
+
### `QuadTree<I unknown>`
|
|
18
|
+
|
|
19
|
+
The primary structural class representing a node within the spatial tree.
|
|
20
|
+
|
|
21
|
+
* **Constructor**
|
|
22
|
+
`constructor(boundary: QuadTreeBoundary, threshold: number, depth = 0, maxDepth = 8)`
|
|
23
|
+
Initializes a node with a defined spatial boundary, capacity threshold, current depth layer, and structural depth limit.
|
|
24
|
+
* **`insert(x: number, y: number, value: I): boolean`**
|
|
25
|
+
Inserts a point payload into the tree. Returns `true` if insertion succeeds, or `false` if the coordinates fall outside the node boundary.
|
|
26
|
+
* *Mechanism*: Governed by internal fields `#threshold`, `#depth`, and `#maxDepth`. If the node capacity exceeds `#threshold` and `#depth` is less than `#maxDepth`, `#subdivide()` is executed, transforming the leaf into an internal node and redistributing existing payloads to four child quadrants (`#nodeNW`, `#nodeNE`, `#nodeSW`, `#nodeSE`).
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
* **`query(region: QuadTreeRegion, result: QuadTreeInsertable<I>[]): void`**
|
|
30
|
+
Populates the `result` array with all payloads contained within the specified `QuadTreeRegion`. Evaluates intersection via `region.intersects` before performing point-in-boundary checks.
|
|
31
|
+
* **`clear(): void`**
|
|
32
|
+
Clears all internal point references and recursively clears child node instances.
|
|
33
|
+
|
|
34
|
+
### `QuadTreeBoundary`
|
|
35
|
+
|
|
36
|
+
Implements `QuadTreeRegion`. Defines the spatial bounding box for a node.
|
|
37
|
+
|
|
38
|
+
* **Constructor**
|
|
39
|
+
`constructor(x: number, y: number, width: number, height: number)`
|
|
40
|
+
Sets the origin coordinates (`x`, `y`) and spatial dimensions (`width`, `height`).
|
|
41
|
+
* **Properties**
|
|
42
|
+
`x: number`, `y: number`, `width: number`, `height: number`, `left: number`, `right: number`, `top: number`, `bottom: number`
|
|
43
|
+
* **`contains(x: number, y: number): boolean`**
|
|
44
|
+
Evaluates whether a point falls within the spatial boundaries.
|
|
45
|
+
* **`intersects(other: QuadTreeBoundary): boolean`**
|
|
46
|
+
Evaluates Axis-Aligned Bounding Box (AABB) intersection between two boundaries.
|
|
47
|
+
|
|
48
|
+
### `QuadTreeRegion`
|
|
49
|
+
|
|
50
|
+
An interface defining spatial bounds evaluation.
|
|
51
|
+
|
|
52
|
+
* **`contains(x: number, y: number): boolean`**
|
|
53
|
+
* **`intersects(boundary: QuadTreeBoundary): boolean`**
|
|
54
|
+
|
|
55
|
+
### `QuadTreeInsertable<I unknown>`
|
|
56
|
+
|
|
57
|
+
An interface representing the structured storage layout of data elements.
|
|
58
|
+
|
|
59
|
+
* **Properties**
|
|
60
|
+
`x: number`, `y: number`, `value: I`
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Operational Context (Behavioral Notes)
|
|
65
|
+
|
|
66
|
+
* **Boundary Conditions:** Coordinate boundaries evaluate containment using inclusive lower bounds and exclusive upper bounds: `[x, x + width)` and `[y, y + height)`.
|
|
67
|
+
* **Subdivision Behavior:** Structural subdivision is lazy. Node splitting occurs only when point allocations exceed the target node `#threshold`, provided the maximum structural depth limitation (`#maxDepth`) has not been reached.
|
|
68
|
+
* **Clearing Mechanics:** The `.clear()` routine executes a post-order traversal to empty local point arrays, unset structural split states, and nullify all internal child node pointers, freeing references for immediate garbage collection.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Usage Example
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { QuadTree, QuadTreeBoundary, QuadTreeInsertable } from './quadtree';
|
|
76
|
+
|
|
77
|
+
const boundary = new QuadTreeBoundary(0, 0, 200, 200);
|
|
78
|
+
const quadTree = new QuadTree<string>(boundary, 2, 0, 4);
|
|
79
|
+
|
|
80
|
+
quadTree.insert(10, 15, "Payload_A");
|
|
81
|
+
quadTree.insert(45, 80, "Payload_B");
|
|
82
|
+
quadTree.insert(12, 18, "Payload_C");
|
|
83
|
+
|
|
84
|
+
const queryRegion = new QuadTreeBoundary(0, 0, 50, 50);
|
|
85
|
+
const matchedPoints: QuadTreeInsertable<string>[] = [];
|
|
86
|
+
|
|
87
|
+
quadTree.query(queryRegion, matchedPoints);
|
|
88
|
+
|
|
89
|
+
```
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { it, describe, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
QuadTree,
|
|
6
|
+
QuadTreeBoundary,
|
|
7
|
+
type QuadTreeInsertable
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
} from './index.ts';
|
|
10
|
+
|
|
11
|
+
class TestRegion {
|
|
12
|
+
readonly x: number;
|
|
13
|
+
readonly y: number;
|
|
14
|
+
readonly width: number;
|
|
15
|
+
readonly height: number;
|
|
16
|
+
|
|
17
|
+
constructor(x: number, y: number, width: number, height: number) {
|
|
18
|
+
this.x = x;
|
|
19
|
+
this.y = y;
|
|
20
|
+
this.width = width;
|
|
21
|
+
this.height = height;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
contains(x: number, y: number): boolean {
|
|
25
|
+
return x >= this.x && x < this.x + this.width && y >= this.y && y < this.y + this.height;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
intersects(boundary: QuadTreeBoundary): boolean {
|
|
29
|
+
return this.x < boundary.right &&
|
|
30
|
+
this.x + this.width > boundary.left &&
|
|
31
|
+
this.y < boundary.bottom &&
|
|
32
|
+
this.y + this.height > boundary.top;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('QuadTree', () => {
|
|
37
|
+
let quadTreeBoundary: QuadTreeBoundary;
|
|
38
|
+
let quadTree: QuadTree<string>;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
quadTreeBoundary = new QuadTreeBoundary(0, 0, 100, 100);
|
|
42
|
+
quadTree = new QuadTree<string>(quadTreeBoundary, 2);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should successfully insert points within boundaries', () => {
|
|
46
|
+
const inserted1 = quadTree.insert(10, 10, 'Point A');
|
|
47
|
+
const inserted2 = quadTree.insert(50, 50, 'Point B');
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(inserted1, true);
|
|
50
|
+
assert.strictEqual(inserted2, true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should fail to insert points outside boundaries', () => {
|
|
54
|
+
const insertedOutside = quadTree.insert(150, 50, 'Outside');
|
|
55
|
+
const insertedNegative = quadTree.insert(-10, 20, 'Negative');
|
|
56
|
+
|
|
57
|
+
assert.strictEqual(insertedOutside, false);
|
|
58
|
+
assert.strictEqual(insertedNegative, false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should subdivide when threshold is exceeded', () => {
|
|
62
|
+
quadTree.insert(10, 10, 'NW Point');
|
|
63
|
+
quadTree.insert(60, 10, 'NE Point');
|
|
64
|
+
|
|
65
|
+
const insertedThird = quadTree.insert(10, 60, 'SW Point');
|
|
66
|
+
assert.strictEqual(insertedThird, true);
|
|
67
|
+
|
|
68
|
+
const nwResult: QuadTreeInsertable<string>[] = [];
|
|
69
|
+
const nwRegion = new TestRegion(0, 0, 50, 50);
|
|
70
|
+
|
|
71
|
+
quadTree.query(nwRegion, nwResult);
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(nwResult.length, 1);
|
|
74
|
+
assert.strictEqual(nwResult[0]?.value, 'NW Point');
|
|
75
|
+
|
|
76
|
+
const swResult: QuadTreeInsertable<string>[] = [];
|
|
77
|
+
const swRegion = new TestRegion(0, 50, 50, 50);
|
|
78
|
+
|
|
79
|
+
quadTree.query(swRegion, swResult);
|
|
80
|
+
|
|
81
|
+
assert.strictEqual(swResult.length, 1);
|
|
82
|
+
assert.strictEqual(swResult[0]?.value, 'SW Point');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should respect maxDepth and not subdivide further', () => {
|
|
86
|
+
const flatTree = new QuadTree<string>(quadTreeBoundary, 1, 0, 0);
|
|
87
|
+
|
|
88
|
+
const inserted1 = flatTree.insert(10, 10, 'A');
|
|
89
|
+
const inserted2 = flatTree.insert(20, 20, 'B');
|
|
90
|
+
const inserted3 = flatTree.insert(30, 30, 'C');
|
|
91
|
+
|
|
92
|
+
assert.strictEqual(inserted1, true);
|
|
93
|
+
assert.strictEqual(inserted2, true);
|
|
94
|
+
assert.strictEqual(inserted3, true);
|
|
95
|
+
|
|
96
|
+
const result: QuadTreeInsertable<string>[] = [];
|
|
97
|
+
|
|
98
|
+
flatTree.query(new TestRegion(0, 0, 100, 100), result);
|
|
99
|
+
|
|
100
|
+
assert.strictEqual(result.length, 3);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should return only points within the queried region', () => {
|
|
104
|
+
quadTree.insert(10, 10, 'Target 1');
|
|
105
|
+
quadTree.insert(20, 20, 'Target 2');
|
|
106
|
+
quadTree.insert(80, 80, 'Far point');
|
|
107
|
+
|
|
108
|
+
const result: QuadTreeInsertable<string>[] = [];
|
|
109
|
+
const queryRegion = new TestRegion(0, 0, 30, 30);
|
|
110
|
+
|
|
111
|
+
quadTree.query(queryRegion, result);
|
|
112
|
+
|
|
113
|
+
assert.strictEqual(result.length, 2);
|
|
114
|
+
|
|
115
|
+
const values = result.map(r => r.value);
|
|
116
|
+
|
|
117
|
+
assert.ok(values.includes('Target 1'));
|
|
118
|
+
assert.ok(values.includes('Target 2'));
|
|
119
|
+
assert.ok(!values.includes('Far point'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should clear all nodes and elements', () => {
|
|
123
|
+
quadTree.insert(10, 10, 'A');
|
|
124
|
+
quadTree.insert(60, 60, 'B');
|
|
125
|
+
quadTree.insert(20, 80, 'C');
|
|
126
|
+
|
|
127
|
+
quadTree.clear();
|
|
128
|
+
|
|
129
|
+
const result: QuadTreeInsertable<string>[] = [];
|
|
130
|
+
|
|
131
|
+
quadTree.query(new TestRegion(0, 0, 100, 100), result);
|
|
132
|
+
|
|
133
|
+
assert.strictEqual(result.length, 0);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('QuadTreeBoundary', () => {
|
|
138
|
+
it('contains method checks bounds correctly', () => {
|
|
139
|
+
const b = new QuadTreeBoundary(10, 10, 20, 20);
|
|
140
|
+
|
|
141
|
+
assert.strictEqual(b.contains(15, 15), true);
|
|
142
|
+
assert.strictEqual(b.contains(10, 10), true);
|
|
143
|
+
assert.strictEqual(b.contains(30, 15), false);
|
|
144
|
+
assert.strictEqual(b.contains(15, 30), false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('intersects method detects overlapping boundaries', () => {
|
|
148
|
+
const b1 = new QuadTreeBoundary(0, 0, 50, 50);
|
|
149
|
+
const b2 = new QuadTreeBoundary(25, 25, 50, 50);
|
|
150
|
+
const b3 = new QuadTreeBoundary(60, 60, 10, 10);
|
|
151
|
+
|
|
152
|
+
assert.strictEqual(b1.intersects(b2), true);
|
|
153
|
+
assert.strictEqual(b1.intersects(b3), false);
|
|
154
|
+
});
|
|
155
|
+
});
|
package/package.json
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeitfall/quadtree",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.1",
|
|
5
5
|
"description": "Implementation of point-region quadtree algorithm",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"
|
|
17
|
-
"build": "tsc"
|
|
16
|
+
"test": "node --experimental-strip-types --test",
|
|
17
|
+
"build": "tsc -p tsconfig.build.json"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
20
|
"node": ">=22.19.0"
|
|
@@ -27,7 +27,17 @@
|
|
|
27
27
|
"type": "git",
|
|
28
28
|
"url": "git+https://github.com/zeitfall/quadtree.git"
|
|
29
29
|
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"typescript",
|
|
32
|
+
"quadtree",
|
|
33
|
+
"quad-tree",
|
|
34
|
+
"spatial-partitioning",
|
|
35
|
+
"spatial-indexing",
|
|
36
|
+
"spatial-index",
|
|
37
|
+
"collision-detection"
|
|
38
|
+
],
|
|
30
39
|
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.9.1",
|
|
31
41
|
"typescript": "^6.0.3"
|
|
32
42
|
}
|
|
33
|
-
}
|
|
43
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -9,10 +9,9 @@
|
|
|
9
9
|
// See also https://aka.ms/tsconfig/module
|
|
10
10
|
"module": "nodenext",
|
|
11
11
|
"target": "esnext",
|
|
12
|
-
"types": [],
|
|
13
12
|
// For nodejs:
|
|
14
13
|
// "lib": ["esnext"],
|
|
15
|
-
|
|
14
|
+
"types": ["node"],
|
|
16
15
|
// and npm install -D @types/node
|
|
17
16
|
|
|
18
17
|
// Other Outputs
|