aiquadtreejs 0.5.5 → 0.5.8
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/README.md +47 -103
- package/README_ZHTW.md +47 -103
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/llms-full.txt +99 -346
- package/llms.txt +6 -2
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,133 +1,77 @@
|
|
|
1
1
|
# aiquadtreejs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/yshengliao/aiquadtreejs/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
[](https://www.anthropic.com/claude-code)
|
|
7
|
-
[](README_ZHTW.md)
|
|
3
|
+
Tiny 2D quadtree for per-frame rebuild collision broadphase. Insert AABBs, retrieve candidates, then run precise collision checks yourself.
|
|
8
4
|
|
|
9
|
-
>
|
|
5
|
+
> **Status: 0.5.8 - stable 1.0-track surface.** The root entry is the public API.
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
> **Status: 0.5.1 published.** `insert()` now validates object geometry (non-finite coords, negative dimensions throw `QuadtreeError`); 22 new tests (J/K groups). `retrieveInto(region, target)` is a steady-state zero-allocation broadphase (reused internal scratch + caller buffer); property-based dedup invariants. ≥95% coverage, ≤2 KB gzip.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## Why aiquadtreejs
|
|
18
|
-
|
|
19
|
-
Naïve pairwise collision detection on `N` entities is `O(N²)`. At 1,000 entities that's a million comparisons per frame, which at 60 Hz is already a budget killer; at 10,000 entities it's a non-starter. A quadtree replaces the dense outer loop with a spatial filter: each entity asks "who could I possibly collide with?" and the tree returns a small candidate set — sub-linear on average for well-distributed inputs (worst case is `O(N)` when every object overlaps the query region). The precise hit test then runs only on candidates, dropping the actual comparisons by one or two orders of magnitude in typical game distributions.
|
|
20
|
-
|
|
21
|
-
`aiquadtreejs` makes four deliberate trade-offs:
|
|
22
|
-
|
|
23
|
-
- **Per-frame rebuild, not move-tracking.** Tracking which leaf an entity migrated into between frames is doable but error-prone; `clear()` + re-`insert()` is faster in practice and easier to reason about. This mirrors the Kontra.js philosophy. (`clear()` resets the root node and drops the child array; subdividing fresh next frame is cheap because the inner subdivision logic touches at most `maxLevels = 4` levels.)
|
|
24
|
-
- **Set-based dedup on `retrieve`.** An AABB that straddles a quadrant boundary lands in multiple leaves. Without dedup the caller sees the same candidate two or four times and pays double for the precise hit test. The Set guarantees each candidate appears once.
|
|
25
|
-
- **2D AABB only — no 3D, no R-tree, no KD-tree, no Circle / Line primitives.** Those are real techniques for real problems but they sit in a different size class. Keep this library at ≤ 2 KB gzipped and let user-land bring in heavier broadphase when needed.
|
|
26
|
-
- **No precise hit-test.** Broadphase libraries that also do collision response always grow. The contract here ends at "here are the candidates"; pixel-perfect or shape-specific tests belong to whatever physics layer you already have.
|
|
27
|
-
|
|
28
|
-
Why not just import `@timohausmann/quadtree-ts`? That library is solid and you should use it for stand-alone work. `aiquadtreejs` exists so that an ai*js stack can talk to entity IDs from `aiecsjs` without per-frame object adaptation — `insert({ id: eid, x, y, width, height })` lines up with the SoA columns you already maintain.
|
|
29
|
-
|
|
30
|
-
> `aiquadtreejs` is one of the four 0.3-cycle siblings joining the family — alongside [aipooljs](https://github.com/yshengliao/aipooljs) (object pool), `aieventjs` (typed events; self-built, not a `mitt` fork), and `aiaudiojs` (Web Audio shell over a Howler.js `peerDependency`).
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Quick Start
|
|
7
|
+
## Install
|
|
35
8
|
|
|
36
9
|
```bash
|
|
37
10
|
pnpm add aiquadtreejs
|
|
38
11
|
```
|
|
39
12
|
|
|
40
|
-
```
|
|
13
|
+
```ts
|
|
41
14
|
import { createQuadtree, type AABB } from "aiquadtreejs";
|
|
15
|
+
```
|
|
42
16
|
|
|
43
|
-
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
interface Body extends AABB {
|
|
21
|
+
id: number;
|
|
22
|
+
}
|
|
44
23
|
|
|
45
|
-
|
|
46
|
-
const qt = createQuadtree<Body>({
|
|
24
|
+
const tree = createQuadtree<Body>({
|
|
47
25
|
bounds: { x: 0, y: 0, width: 800, height: 600 },
|
|
48
26
|
maxObjects: 10,
|
|
49
27
|
maxLevels: 4,
|
|
50
28
|
});
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 3. Per query: broadphase, then precise hit-test.
|
|
59
|
-
function nearbyEnemies(player: Body, enemies: Body[]): Body[] {
|
|
60
|
-
const region: AABB = { x: player.x - 50, y: player.y - 50, width: 100, height: 100 };
|
|
61
|
-
const candidates = qt.retrieve(region);
|
|
62
|
-
// Caller filters with precise AABB / pixel test:
|
|
63
|
-
return candidates.filter((c) => aabbOverlap(c, region));
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Right-open coordinate semantics: `x + width` and `y + height` are exclusive (renderer-neutral; matches common conventions such as PixiJS `getBounds()`).
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## Capabilities / Limitations
|
|
72
|
-
|
|
73
|
-
| Will do (v1) | Won't do |
|
|
74
|
-
| --------------------------------------------------------- | ----------------------------------------------------- |
|
|
75
|
-
| 2D rectangle quadtree | 3D octree / R-tree / KD-tree |
|
|
76
|
-
| `insert()` / `retrieve()` / `clear()` / `dispose()` | Move-tracking (use `clear()` + re-`insert()`) |
|
|
77
|
-
| Set-based dedup on `retrieve` (each candidate once) | Precise hit-test (broadphase only) |
|
|
78
|
-
| `maxObjects` + `maxLevels` knobs | Auto-rebalance / dynamic depth growth |
|
|
79
|
-
| Reuse the root node across frames; resubdivide cheaply | Circle / Line / polygon primitives |
|
|
80
|
-
| `dispose()` idempotent; post-dispose calls throw | Persistence / snapshot / serialise (out of scope) |
|
|
30
|
+
const bodies: Body[] = [
|
|
31
|
+
{ id: 1, x: 100, y: 100, width: 32, height: 32 },
|
|
32
|
+
{ id: 2, x: 400, y: 250, width: 32, height: 32 },
|
|
33
|
+
];
|
|
81
34
|
|
|
82
|
-
|
|
35
|
+
tree.clear();
|
|
36
|
+
for (const body of bodies) tree.insert(body);
|
|
83
37
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
interface AABB {
|
|
88
|
-
x: number;
|
|
89
|
-
y: number;
|
|
90
|
-
width: number; // x + width is exclusive
|
|
91
|
-
height: number; // y + height is exclusive
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface QuadtreeOptions {
|
|
95
|
-
bounds: AABB;
|
|
96
|
-
maxObjects?: number; // default 10
|
|
97
|
-
maxLevels?: number; // default 4
|
|
98
|
-
}
|
|
38
|
+
const candidates = tree.retrieve({ x: 80, y: 80, width: 120, height: 120 });
|
|
39
|
+
```
|
|
99
40
|
|
|
100
|
-
|
|
101
|
-
insert(obj: T): void;
|
|
102
|
-
retrieve(region: AABB): T[];
|
|
103
|
-
retrieveInto(region: AABB, target: T[]): T[]; // ← new in 0.3.0
|
|
104
|
-
clear(): void;
|
|
105
|
-
dispose(): void;
|
|
106
|
-
readonly disposed: boolean;
|
|
107
|
-
}
|
|
41
|
+
## Core API
|
|
108
42
|
|
|
109
|
-
|
|
110
|
-
|
|
43
|
+
- `createQuadtree<T extends AABB>({ bounds, maxObjects?, maxLevels? })` creates a tree.
|
|
44
|
+
- `insert(obj)` stores an object reference in overlapping nodes.
|
|
45
|
+
- `retrieve(region)` returns a deduplicated broadphase candidate array.
|
|
46
|
+
- `retrieveInto(region, target)` reuses a caller-owned result array.
|
|
47
|
+
- `clear()` empties the tree for the next frame and clears scratch buffers.
|
|
48
|
+
- `dispose()` is idempotent permanent teardown.
|
|
49
|
+
- Errors: `QuadtreeError`, `QuadtreeDisposedError`.
|
|
111
50
|
|
|
112
|
-
|
|
113
|
-
```
|
|
51
|
+
## Model
|
|
114
52
|
|
|
115
|
-
|
|
53
|
+
- Coordinates are right-open: `{ x, y, width, height }` covers `[x, x + width)` and `[y, y + height)`.
|
|
54
|
+
- This is a broadphase only. Returned candidates may not actually overlap the query region.
|
|
55
|
+
- Expected usage is per-frame rebuild: `clear()`, insert active bodies, query.
|
|
56
|
+
- Objects spanning quadrant boundaries can be stored in multiple child nodes; results are deduplicated.
|
|
57
|
+
- `maxLevels` has no hard cap. Very high values plus spanning objects can create huge node counts.
|
|
116
58
|
|
|
117
|
-
|
|
59
|
+
## Sharp Edges
|
|
118
60
|
|
|
119
|
-
|
|
61
|
+
- Known bug: a zero-size point exactly on the root `left/top` boundary, such as `{ x: bounds.x, y: bounds.y, width: 0, height: 0 }`, is currently ignored by the root overlap check. Zero-size objects away from that root minimum edge are covered by tests. Next code pass should fix the root containment helper and add boundary tests.
|
|
62
|
+
- Fully outside objects are ignored by retrieval.
|
|
63
|
+
- Negative width/height and non-finite coordinates throw.
|
|
64
|
+
- `retrieveInto()` clears the target array before writing results.
|
|
65
|
+
- After `dispose()`, all methods except `dispose()` throw `QuadtreeDisposedError`.
|
|
120
66
|
|
|
121
|
-
|
|
122
|
-
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
-
| **0.1.0** | `createQuadtree`, `insert` / `retrieve` / `clear` / `dispose`, Set-based dedup, ≥95% coverage, ≤2 KB gzip. |
|
|
124
|
-
| **0.3.0** | `retrieveInto(region, target)` zero-alloc API; property-based tests (`fast-check`); STABILITY.md tracking. |
|
|
125
|
-
| **0.4.0** | Dependency hygiene (removed unused `tsx`, aligned `fast-check`); 0.3.x public surface frozen for the 1.x track. No runtime API change. |
|
|
126
|
-
| **0.5.1** | `insert()` input validation: throws `QuadtreeError` for non-finite coords or negative dimensions; 22 new tests (J1–J15, K1–K7). |
|
|
127
|
-
| **0.6+** | Evaluate 3D octree variant (`createOctree<T extends AABB3>`); see `STABILITY.md` for current draft. |
|
|
67
|
+
## AI Context
|
|
128
68
|
|
|
129
|
-
|
|
69
|
+
- Short index: [`llms.txt`](llms.txt)
|
|
70
|
+
- Full generated context: [`llms-full.txt`](llms-full.txt)
|
|
71
|
+
- Stability contract: [`STABILITY.md`](STABILITY.md)
|
|
72
|
+
- Current review backlog: [`REVIEW.md`](REVIEW.md)
|
|
73
|
+
- Release history: [`CHANGELOG.md`](CHANGELOG.md)
|
|
130
74
|
|
|
131
75
|
## License
|
|
132
76
|
|
|
133
|
-
|
|
77
|
+
MIT
|
package/README_ZHTW.md
CHANGED
|
@@ -1,133 +1,77 @@
|
|
|
1
1
|
# aiquadtreejs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://github.com/yshengliao/aiquadtreejs/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
6
|
-
[](https://www.anthropic.com/claude-code)
|
|
7
|
-
[](README.md)
|
|
3
|
+
小型 2D quadtree,用於每 frame 重建的 collision broadphase。插入 AABB、取回候選物件,精確碰撞檢查由呼叫端負責。
|
|
8
4
|
|
|
9
|
-
>
|
|
5
|
+
> **狀態:0.5.8 - 穩定 1.0 軌道 API。** root entry 是公開 API。
|
|
10
6
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
> **狀態:0.5.1 已發佈。** `insert()` 現在驗證物件幾何(非 finite 座標或負維度拋出 `QuadtreeError`);22 項新測試(J/K 組)。`retrieveInto(region, target)` 為 steady-state 零分配 broadphase(內部 scratch 重用 + caller buffer)與 property-based 去重不變式。≥95% coverage,≤2 KB gzip。
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 為什麼有 aiquadtreejs
|
|
18
|
-
|
|
19
|
-
對 `N` 個 entity 做 N² 兩兩碰撞比對,1,000 個 entity 就是百萬次比對,在 60 Hz 已經吃光預算;10,000 個 entity 直接破表。Quadtree 把那個密集外層 loop 換成空間過濾器:每個 entity 問「我可能會撞到誰?」,樹回傳一個小的候選集,分布均勻時平均次線性(最壞 `O(N)`,當所有物件都與查詢區域重疊時)。精確碰撞測試只跑在候選上,實際比較數量降一到兩個量級。
|
|
20
|
-
|
|
21
|
-
`aiquadtreejs` 刻意做這四個取捨:
|
|
22
|
-
|
|
23
|
-
- **Per-frame rebuild,不追蹤移動。** 追蹤 entity 跨幀在哪個 leaf 之間遷移,能做但容易出錯;對重用 node object 的樹做 `clear()` + re-`insert()`,實測更快、心智模型更乾淨。這跟 Kontra.js 哲學一致。
|
|
24
|
-
- **`retrieve` 用 Set 去重。** 跨象限的 AABB 會落進多個 leaf;不去重的話呼叫方會看到同一個候選 2 或 4 次,精確碰撞測試成本翻倍。Set 保證每個候選只出現一次。
|
|
25
|
-
- **只做 2D AABB ── 不做 3D、不做 R-tree / KD-tree、不做 Circle / Line。** 那些是針對特定問題的好技術,但屬於不同 size class。這個套件壓在 ≤ 2 KB gzip,更重的 broadphase 由 user-land 帶。
|
|
26
|
-
- **不做精確碰撞測試。** Broadphase + 碰撞回應綁在一起的套件永遠在膨脹。本套件契約止於「這是候選名單」;pixel-perfect 或特殊形狀的精確測試交給你已經有的 physics layer。
|
|
27
|
-
|
|
28
|
-
那為什麼不直接用 `@timohausmann/quadtree-ts`?它做得很好,獨立場景直接用沒問題。`aiquadtreejs` 存在的理由是讓 ai*js stack 能直接接 `aiecsjs` 的 entity ID,不必每 frame 再轉一次物件 ── `insert({ id: eid, x, y, width, height })` 直接對齊你已經維護的 SoA 欄位。
|
|
29
|
-
|
|
30
|
-
> `aiquadtreejs` 是 v0.3 cycle 四個新加入兄弟套件之一 ── 另外三個是 [aipooljs](https://github.com/yshengliao/aipooljs)(物件池)、`aieventjs`(typed event;**自寫不 fork mitt**)、`aiaudiojs`(Web Audio 薄殼,底層用 Howler.js 作 `peerDependency`)。
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## Quick Start
|
|
7
|
+
## 安裝
|
|
35
8
|
|
|
36
9
|
```bash
|
|
37
10
|
pnpm add aiquadtreejs
|
|
38
11
|
```
|
|
39
12
|
|
|
40
|
-
```
|
|
13
|
+
```ts
|
|
41
14
|
import { createQuadtree, type AABB } from "aiquadtreejs";
|
|
15
|
+
```
|
|
42
16
|
|
|
43
|
-
|
|
17
|
+
## 快速開始
|
|
44
18
|
|
|
45
|
-
|
|
46
|
-
|
|
19
|
+
```ts
|
|
20
|
+
interface Body extends AABB {
|
|
21
|
+
id: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const tree = createQuadtree<Body>({
|
|
47
25
|
bounds: { x: 0, y: 0, width: 800, height: 600 },
|
|
48
26
|
maxObjects: 10,
|
|
49
27
|
maxLevels: 4,
|
|
50
28
|
});
|
|
51
29
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// 3. 每次查詢:broadphase 再精確碰撞。
|
|
59
|
-
function nearbyEnemies(player: Body, enemies: Body[]): Body[] {
|
|
60
|
-
const region: AABB = { x: player.x - 50, y: player.y - 50, width: 100, height: 100 };
|
|
61
|
-
const candidates = qt.retrieve(region);
|
|
62
|
-
// 呼叫方做精確 AABB / pixel 測試:
|
|
63
|
-
return candidates.filter((c) => aabbOverlap(c, region));
|
|
64
|
-
}
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
右開座標語意:`x + width` 與 `y + height` 皆為右開(不含)。此為 renderer-neutral 慣例,PixiJS `getBounds()` 等渲染器亦同。
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
## 能做 / 不做
|
|
72
|
-
|
|
73
|
-
| 會做(v1) | 不會做 |
|
|
74
|
-
| --------------------------------------------------------- | ----------------------------------------------------- |
|
|
75
|
-
| 2D rectangle quadtree | 3D octree / R-tree / KD-tree |
|
|
76
|
-
| `insert()` / `retrieve()` / `clear()` / `dispose()` | 追蹤移動(用 `clear()` + re-`insert()`) |
|
|
77
|
-
| `retrieve` Set 去重(每候選只出現一次) | 精確碰撞測試(只做 broadphase) |
|
|
78
|
-
| `maxObjects` + `maxLevels` 旋鈕 | Auto-rebalance / 動態深度成長 |
|
|
79
|
-
| 跨 frame 重用 node slot(零 GC churn) | Circle / Line / Polygon 原型 |
|
|
80
|
-
| `dispose()` 冪等;dispose 後呼叫拋錯 | Persistence / snapshot / serialise(不在範圍內) |
|
|
30
|
+
const bodies: Body[] = [
|
|
31
|
+
{ id: 1, x: 100, y: 100, width: 32, height: 32 },
|
|
32
|
+
{ id: 2, x: 400, y: 250, width: 32, height: 32 },
|
|
33
|
+
];
|
|
81
34
|
|
|
82
|
-
|
|
35
|
+
tree.clear();
|
|
36
|
+
for (const body of bodies) tree.insert(body);
|
|
83
37
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
```typescript
|
|
87
|
-
interface AABB {
|
|
88
|
-
x: number;
|
|
89
|
-
y: number;
|
|
90
|
-
width: number; // x + width 為右開(不含)
|
|
91
|
-
height: number; // y + height 為右開(不含)
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
interface QuadtreeOptions {
|
|
95
|
-
bounds: AABB;
|
|
96
|
-
maxObjects?: number; // 預設 10
|
|
97
|
-
maxLevels?: number; // 預設 4
|
|
98
|
-
}
|
|
38
|
+
const candidates = tree.retrieve({ x: 80, y: 80, width: 120, height: 120 });
|
|
39
|
+
```
|
|
99
40
|
|
|
100
|
-
|
|
101
|
-
insert(obj: T): void;
|
|
102
|
-
retrieve(region: AABB): T[];
|
|
103
|
-
retrieveInto(region: AABB, target: T[]): T[]; // ← 0.3.0 新增
|
|
104
|
-
clear(): void;
|
|
105
|
-
dispose(): void;
|
|
106
|
-
readonly disposed: boolean;
|
|
107
|
-
}
|
|
41
|
+
## 核心 API
|
|
108
42
|
|
|
109
|
-
|
|
110
|
-
|
|
43
|
+
- `createQuadtree<T extends AABB>({ bounds, maxObjects?, maxLevels? })` 建立 tree。
|
|
44
|
+
- `insert(obj)` 將物件參照存進重疊 nodes。
|
|
45
|
+
- `retrieve(region)` 回傳 dedup 後的 broadphase candidates。
|
|
46
|
+
- `retrieveInto(region, target)` 重用呼叫端提供的 result array。
|
|
47
|
+
- `clear()` 清空 tree 與 scratch buffers,準備下一 frame。
|
|
48
|
+
- `dispose()` 是可重複呼叫的永久 teardown。
|
|
49
|
+
- Errors:`QuadtreeError`、`QuadtreeDisposedError`。
|
|
111
50
|
|
|
112
|
-
|
|
113
|
-
```
|
|
51
|
+
## Model
|
|
114
52
|
|
|
115
|
-
|
|
53
|
+
- 座標採 right-open:`{ x, y, width, height }` 覆蓋 `[x, x + width)` 與 `[y, y + height)`。
|
|
54
|
+
- 這只是 broadphase。回傳候選物件不保證真的與 query region 相交。
|
|
55
|
+
- 預期用法是每 frame 重建:`clear()`、插入 active bodies、query。
|
|
56
|
+
- 跨 quadrant 的物件可能存在多個 child nodes;結果會 dedup。
|
|
57
|
+
- `maxLevels` 沒有硬上限。很高的值加上 spanning objects 可能建立巨大 node 數。
|
|
116
58
|
|
|
117
|
-
|
|
59
|
+
## 注意事項
|
|
118
60
|
|
|
119
|
-
|
|
61
|
+
- 已知 bug:零尺寸 point 若剛好在 root `left/top` 邊界,例如 `{ x: bounds.x, y: bounds.y, width: 0, height: 0 }`,目前會被 root overlap check 忽略。離開 root minimum edge 的零尺寸物件已有測試覆蓋。下一輪 code pass 應修正 root containment helper 並補 boundary tests。
|
|
62
|
+
- 完全在 bounds 外的物件不會被 retrieve 到。
|
|
63
|
+
- 負 width/height 與非有限座標會 throw。
|
|
64
|
+
- `retrieveInto()` 會先清空 target array 再寫入結果。
|
|
65
|
+
- `dispose()` 後除了 `dispose()` 本身外,所有方法都會丟 `QuadtreeDisposedError`。
|
|
120
66
|
|
|
121
|
-
|
|
122
|
-
| ---------- | --------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
-
| **0.1.0** | `createQuadtree`、`insert` / `retrieve` / `clear` / `dispose`、Set 去重、≥95% coverage、≤2 KB gzip。 |
|
|
124
|
-
| **0.3.0** | `retrieveInto(region, target)` 零分配 API;property-based 測試(`fast-check`);`STABILITY.md` API 穩定度追蹤。 |
|
|
125
|
-
| **0.4.0** | 依賴 hygiene(移除未用 `tsx`、對齊 `fast-check`);0.3.x public surface 凍結為 1.x track。無 runtime API 變動。 |
|
|
126
|
-
| **0.5.1** | `insert()` 輸入驗證:非 finite 座標或負維度拋出 `QuadtreeError`;22 項新測試(J1–J15、K1–K7)。 |
|
|
127
|
-
| **0.6+** | 評估 3D octree 變體(`createOctree<T extends AABB3>`);現有草稿見 `STABILITY.md`。 |
|
|
67
|
+
## AI Context
|
|
128
68
|
|
|
129
|
-
|
|
69
|
+
- 短索引:[`llms.txt`](llms.txt)
|
|
70
|
+
- 完整生成內容:[`llms-full.txt`](llms-full.txt)
|
|
71
|
+
- 穩定度契約:[`STABILITY.md`](STABILITY.md)
|
|
72
|
+
- 目前 review backlog:[`REVIEW.md`](REVIEW.md)
|
|
73
|
+
- 版本紀錄:[`CHANGELOG.md`](CHANGELOG.md)
|
|
130
74
|
|
|
131
75
|
## License
|
|
132
76
|
|
|
133
|
-
|
|
77
|
+
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
'use strict';var
|
|
1
|
+
'use strict';var d=class extends Error{name="QuadtreeError"},A=class extends Error{name="QuadtreeDisposedError"};function S(e,t){return e.x<t.x+t.width&&e.x+e.width>t.x&&e.y<t.y+t.height&&e.y+e.height>t.y}function q(e,t){let r=t.width===0?t.x>=e.x&&t.x<e.x+e.width:t.x<e.x+e.width&&t.x+t.width>e.x,s=t.height===0?t.y>=e.y&&t.y<e.y+e.height:t.y<e.y+e.height&&t.y+t.height>e.y;return r&&s}function v(e,t){let r=e.bounds.x+e.bounds.width/2,s=e.bounds.y+e.bounds.height/2,n=t.x<r,o=t.width===0?t.x>=r:t.x+t.width>r,u=t.y<s,c=t.height===0?t.y>=s:t.y+t.height>s,h=[];return u&&n&&h.push(0),u&&o&&h.push(1),c&&n&&h.push(2),c&&o&&h.push(3),h}function L(e){let t=e.bounds.width/2,r=e.bounds.height/2,s=e.bounds.x,n=e.bounds.y,o=e.level+1;e.children.push({bounds:{x:s,y:n,width:t,height:r},level:o,objects:[],children:[]},{bounds:{x:s+t,y:n,width:t,height:r},level:o,objects:[],children:[]},{bounds:{x:s,y:n+r,width:t,height:r},level:o,objects:[],children:[]},{bounds:{x:s+t,y:n+r,width:t,height:r},level:o,objects:[],children:[]});for(let u of e.objects)for(let c of v(e,u)){let h=e.children[c];h!==void 0&&h.objects.push(u);}e.objects.length=0;}function g(e,t,r,s){if(!(e.level===0&&!q(e.bounds,t))){if(e.children.length===4){for(let n of v(e,t)){let o=e.children[n];o!==void 0&&g(o,t,r,s);}return}e.objects.push(t),e.objects.length>r&&e.level<s&&L(e);}}function y(e){e.objects.length=0;for(let t of e.children)y(t);e.children.length=0;}function I(e){let{bounds:t}=e,r=e.maxObjects??10,s=e.maxLevels??4;if(!Number.isFinite(t.x)||!Number.isFinite(t.y)||!Number.isFinite(t.width)||!Number.isFinite(t.height))throw new d("bounds must contain finite numbers");if(t.width<=0)throw new d("bounds.width must be > 0");if(t.height<=0)throw new d("bounds.height must be > 0");if(!Number.isInteger(r)||r<=0)throw new d("maxObjects must be a positive integer");if(!Number.isInteger(s)||s<=0)throw new d("maxLevels must be a positive integer");let n={root:{bounds:{...t},level:0,objects:[],children:[]},maxObjects:r,maxLevels:s,disposed:false};function o(){if(n.disposed)throw new A("aiquadtreejs: quadtree has been disposed")}function u(i){if(o(),!i||!Number.isFinite(i.x)||!Number.isFinite(i.y)||!Number.isFinite(i.width)||!Number.isFinite(i.height))throw new d("inserted object must be defined with finite numeric x, y, width and height");if(i.width<0)throw new d("inserted object width must be >= 0");if(i.height<0)throw new d("inserted object height must be >= 0");g(n.root,i,n.maxObjects,n.maxLevels);}let c=new Set,h=[],l={x:0,y:0,width:0,height:0};function B(i){let f=i.x,w=i.y,m=i.width,O=i.height;for(l.x=f,l.y=w,l.width=m,l.height=O,c.clear(),h.length=0,h.push(n.root);h.length>0;){let a=h.pop();if(a!==void 0&&S(a.bounds,l)){for(let x of a.objects)c.add(x);for(let x of a.children)h.push(x);}}return c}function b(i){if(!i||!Number.isFinite(i.x)||!Number.isFinite(i.y)||!Number.isFinite(i.width)||!Number.isFinite(i.height))throw new d("aiquadtreejs: retrieve region must have finite numeric x, y, width and height");if(i.width<0)throw new d("aiquadtreejs: retrieve region width must be >= 0");if(i.height<0)throw new d("aiquadtreejs: retrieve region height must be >= 0")}function p(i){return o(),b(i),Array.from(B(i))}function T(i,f){o(),b(i);let w=B(i);f.length=0;for(let m of w)f.push(m);return f}function N(){o(),y(n.root),c.clear(),h.length=0;}function F(){n.disposed||(n.disposed=true,n.root.objects.length=0,n.root.children.length=0,c.clear(),h.length=0);}return {insert:u,retrieve:p,retrieveInto:T,clear:N,dispose:F,get disposed(){return n.disposed}}}exports.QuadtreeDisposedError=A;exports.QuadtreeError=d;exports.createQuadtree=I;//# sourceMappingURL=index.cjs.map
|
|
2
2
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["QuadtreeError","QuadtreeDisposedError","rectsOverlap","a","b","quadrantIndices","node","obj","midX","midY","inLeft","inRight","inTop","inBottom","result","subdivide","w","h","x","y","lvl","i","child","insertNode","maxObjects","maxLevels","clearNode","createQuadtree","opts","bounds","state","ck","insert","scratchSet","scratchStack","retrieveSet","region","retrieve","retrieveInto","target","set","v","clear","dispose"],"mappings":"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","file":"index.cjs","sourcesContent":["// aiquadtreejs — 2D quadtree for per-frame rebuild collision broadphase.\n//\n// v0.5.1: full implementation. Plain-object nodes, iterative-DFS retrieve,\n// Set-based dedup, idempotent dispose, destructurable methods (no `this`).\n\n/**\n * Axis-aligned bounding box.\n *\n * Right-open coordinate semantics: `x` / `y` are the top-left corner and\n * `x + width` / `y + height` are **exclusive**. A 32×32 box at `(0, 0)`\n * covers `[0, 32)` on both axes. (This matches the convention used by\n * renderers such as PixiJS `getBounds()`, but the type is renderer-agnostic.)\n *\n * @public\n */\nexport interface AABB {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Configuration for {@link createQuadtree}.\n *\n * @public\n */\nexport interface QuadtreeOptions {\n /**\n * Outer bounds. Objects partially outside `bounds` still insert into\n * whichever child nodes they overlap; objects fully outside are ignored\n * by `retrieve()` because no node overlaps them.\n */\n bounds: AABB;\n\n /**\n * Threshold above which a node subdivides. Default `10`. Lower values\n * mean deeper trees and fewer candidates per `retrieve()`; higher values\n * mean shallower trees and cheaper `insert()`.\n */\n maxObjects?: number;\n\n /**\n * Maximum subdivision depth. Default `4`. Caps recursion so a very dense\n * cluster doesn't blow up into an unbounded tree.\n */\n maxLevels?: number;\n}\n\n/**\n * Quadtree storing objects that extend {@link AABB}. `T` may carry any\n * payload (entity ID, sprite reference, user data) alongside the geometry.\n *\n * The expected usage pattern is **per-frame rebuild**: at the start of each\n * frame, call `clear()` and re-`insert()` every active object. This is\n * cheaper than tracking movements through the tree and gives correct results\n * regardless of how objects moved.\n *\n * @public\n */\nexport interface Quadtree<T extends AABB> {\n /**\n * Insert an object. The same object reference may legitimately appear\n * in multiple leaf nodes when it spans quadrant boundaries; `retrieve()`\n * deduplicates with a `Set` so the caller sees it exactly once.\n *\n * @throws {@link QuadtreeError} if any of `x`, `y`, `width`, or `height`\n * is non-finite (`NaN`, `Infinity`, `-Infinity`), or if `width` or\n * `height` is negative. Zero-extent objects (points / lines) are valid.\n */\n insert(obj: T): void;\n\n /**\n * Return every inserted object whose containing node overlaps `region`,\n * deduplicated. The result is a **broadphase**: callers must still run\n * a precise AABB or pixel-level hit test on each candidate.\n */\n retrieve(region: AABB): T[];\n\n /**\n * Zero-allocation variant of {@link retrieve}.\n *\n * Clears `target` (sets `target.length = 0`), walks the tree using the same\n * iterative DFS + Set-based dedup as {@link retrieve}, then writes every\n * deduplicated candidate into `target` and returns it.\n *\n * Designed for hot-path callers (per-frame broadphase queries in a game\n * loop) that hold a permanent `T[]` buffer and want to avoid allocating a\n * fresh result array on every call.\n *\n * @invariant `target` identity is preserved — only its contents are\n * replaced. `retrieveInto(r, buf) === buf` always holds.\n * @invariant After return, `target.length` equals the deduplicated\n * candidate count. No `undefined` / `null` holes.\n * @invariant Empty result set → `target.length === 0`.\n * @invariant Dedup semantics identical to {@link retrieve}: objects\n * spanning multiple quadrants appear exactly once.\n *\n * Allocation: in steady state this performs no per-call heap allocation.\n * The dedup `Set` and DFS stack are reused across calls (cleared, not\n * re-created), and results are written into the caller's `target` instead\n * of a fresh array. The first calls may grow the internal scratch; once\n * result sizes stabilise, allocation amortises to zero — the design goal\n * for per-frame broadphase loops issuing thousands of queries.\n */\n retrieveInto(region: AABB, target: T[]): T[];\n\n /**\n * Reset the tree to empty. The root node object is reused across\n * frames; child nodes are released on clear() and re-created next\n * time subdivision triggers. The per-frame churn is bounded by\n * `4 * (subdivided-internal-node-count)` and stays well inside V8's\n * young-generation budget for typical game-loop usage.\n */\n clear(): void;\n\n /**\n * Idempotent teardown. Drops references so the GC can reclaim everything.\n * After disposal, every method except `dispose` itself — `insert`,\n * `retrieve`, `retrieveInto`, `clear` — throws {@link QuadtreeDisposedError}.\n */\n dispose(): void;\n\n /** `true` once {@link dispose} has been called. */\n readonly disposed: boolean;\n}\n\n/**\n * Recoverable quadtree error — thrown by `createQuadtree` for invalid\n * construction options and by `insert()` for precondition violations\n * (e.g. an inserted object with non-finite coordinates or negative\n * `width` / `height`).\n *\n * @public\n */\nexport class QuadtreeError extends Error {\n override readonly name = \"QuadtreeError\";\n}\n\n/**\n * Thrown by any quadtree method called after {@link Quadtree.dispose}.\n *\n * @public\n */\nexport class QuadtreeDisposedError extends Error {\n override readonly name = \"QuadtreeDisposedError\";\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ninterface Node<T extends AABB> {\n bounds: AABB;\n level: number;\n objects: T[];\n children: Node<T>[];\n}\n\ninterface State<T extends AABB> {\n root: Node<T>;\n maxObjects: number;\n maxLevels: number;\n disposed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction rectsOverlap(a: AABB, b: AABB): boolean {\n return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;\n}\n\nfunction quadrantIndices<T extends AABB>(node: Node<T>, obj: AABB): number[] {\n const midX = node.bounds.x + node.bounds.width / 2;\n const midY = node.bounds.y + node.bounds.height / 2;\n // Zero-extent objects (points) sitting exactly on midX / midY would fall\n // through both `<` and `>` checks; treat the point as belonging to the\n // right/bottom side so it doesn't silently disappear.\n const inLeft = obj.x < midX;\n const inRight = obj.width === 0 ? obj.x >= midX : obj.x + obj.width > midX;\n const inTop = obj.y < midY;\n const inBottom = obj.height === 0 ? obj.y >= midY : obj.y + obj.height > midY;\n const result: number[] = [];\n if (inTop && inLeft) result.push(0);\n if (inTop && inRight) result.push(1);\n if (inBottom && inLeft) result.push(2);\n if (inBottom && inRight) result.push(3);\n return result;\n}\n\nfunction subdivide<T extends AABB>(node: Node<T>): void {\n const w = node.bounds.width / 2;\n const h = node.bounds.height / 2;\n const x = node.bounds.x;\n const y = node.bounds.y;\n const lvl = node.level + 1;\n node.children.push(\n { bounds: { x, y, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x: x + w, y, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x, y: y + h, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x: x + w, y: y + h, width: w, height: h }, level: lvl, objects: [], children: [] },\n );\n for (const obj of node.objects) {\n for (const i of quadrantIndices(node, obj)) {\n const child = node.children[i];\n if (child !== undefined) child.objects.push(obj);\n }\n }\n node.objects.length = 0;\n}\n\nfunction insertNode<T extends AABB>(\n node: Node<T>,\n obj: T,\n maxObjects: number,\n maxLevels: number,\n): void {\n // Reject objects entirely outside the root bounds; for inner nodes we\n // trust `quadrantIndices` to route correctly (it has zero-extent fallback\n // logic that `rectsOverlap` does not, so the strict check is too tight\n // at child level for points sitting on a child boundary).\n if (node.level === 0 && !rectsOverlap(node.bounds, obj)) return;\n if (node.children.length === 4) {\n for (const i of quadrantIndices(node, obj)) {\n const child = node.children[i];\n if (child !== undefined) insertNode(child, obj, maxObjects, maxLevels);\n }\n return;\n }\n node.objects.push(obj);\n if (node.objects.length > maxObjects && node.level < maxLevels) {\n subdivide(node);\n }\n}\n\nfunction clearNode<T extends AABB>(node: Node<T>): void {\n node.objects.length = 0;\n for (const child of node.children) {\n clearNode(child);\n }\n node.children.length = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a 2D quadtree.\n *\n * @example\n * ```ts\n * import { createQuadtree, type AABB } from \"aiquadtreejs\";\n *\n * interface Body extends AABB {\n * id: number;\n * }\n *\n * const entities: Body[] = [\n * { id: 1, x: 100, y: 100, width: 32, height: 32 },\n * { id: 2, x: 400, y: 250, width: 32, height: 32 },\n * ];\n * const player: Body = { id: 0, x: 200, y: 200, width: 32, height: 32 };\n *\n * const qt = createQuadtree<Body>({\n * bounds: { x: 0, y: 0, width: 800, height: 600 },\n * maxObjects: 10,\n * maxLevels: 4,\n * });\n *\n * // Per-frame:\n * qt.clear();\n * for (const e of entities) qt.insert(e);\n *\n * // Broadphase lookup near the player:\n * const region: AABB = { x: player.x - 50, y: player.y - 50, width: 100, height: 100 };\n * const candidates = qt.retrieve(region);\n * // Caller runs a precise hit test on `candidates`.\n * ```\n *\n * @public\n */\nexport function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T> {\n const { bounds } = opts;\n const maxObjects = opts.maxObjects ?? 10;\n const maxLevels = opts.maxLevels ?? 4;\n\n if (\n !Number.isFinite(bounds.x) ||\n !Number.isFinite(bounds.y) ||\n !Number.isFinite(bounds.width) ||\n !Number.isFinite(bounds.height)\n ) {\n throw new QuadtreeError(\"bounds must contain finite numbers\");\n }\n if (bounds.width <= 0) {\n throw new QuadtreeError(\"bounds.width must be > 0\");\n }\n if (bounds.height <= 0) {\n throw new QuadtreeError(\"bounds.height must be > 0\");\n }\n if (!Number.isInteger(maxObjects) || maxObjects <= 0) {\n throw new QuadtreeError(\"maxObjects must be a positive integer\");\n }\n if (!Number.isInteger(maxLevels) || maxLevels <= 0) {\n throw new QuadtreeError(\"maxLevels must be a positive integer\");\n }\n\n const state: State<T> = {\n root: {\n bounds: { ...bounds },\n level: 0,\n objects: [],\n children: [],\n },\n maxObjects,\n maxLevels,\n disposed: false,\n };\n\n function ck(): void {\n if (state.disposed) throw new QuadtreeDisposedError(\"aiquadtreejs: quadtree has been disposed\");\n }\n\n function insert(obj: T): void {\n ck();\n if (\n !obj ||\n !Number.isFinite(obj.x) ||\n !Number.isFinite(obj.y) ||\n !Number.isFinite(obj.width) ||\n !Number.isFinite(obj.height)\n ) {\n throw new QuadtreeError(\n \"inserted object must be defined with finite numeric x, y, width and height\",\n );\n }\n if (obj.width < 0) {\n throw new QuadtreeError(\"inserted object width must be >= 0\");\n }\n if (obj.height < 0) {\n throw new QuadtreeError(\"inserted object height must be >= 0\");\n }\n insertNode(state.root, obj, state.maxObjects, state.maxLevels);\n }\n\n // Reusable scratch for retrieveSet, hoisted so steady-state queries\n // allocate nothing. Safe because the returned Set never escapes the\n // module: retrieve copies it out via Array.from and retrieveInto via a\n // push loop, both synchronously and fully before any subsequent call.\n const scratchSet = new Set<T>();\n const scratchStack: Node<T>[] = [];\n\n function retrieveSet(region: AABB): Set<T> {\n scratchSet.clear();\n scratchStack.length = 0;\n scratchStack.push(state.root);\n while (scratchStack.length > 0) {\n const node = scratchStack.pop();\n if (node === undefined) continue;\n if (!rectsOverlap(node.bounds, region)) continue;\n for (const obj of node.objects) scratchSet.add(obj);\n for (const child of node.children) scratchStack.push(child);\n }\n return scratchSet;\n }\n\n function retrieve(region: AABB): T[] {\n ck();\n return Array.from(retrieveSet(region));\n }\n\n function retrieveInto(region: AABB, target: T[]): T[] {\n ck();\n const set = retrieveSet(region);\n target.length = 0;\n for (const v of set) target.push(v);\n return target;\n }\n\n function clear(): void {\n ck();\n clearNode(state.root);\n }\n\n function dispose(): void {\n if (state.disposed) return;\n state.disposed = true;\n state.root.objects.length = 0;\n state.root.children.length = 0;\n scratchSet.clear();\n scratchStack.length = 0;\n }\n\n return {\n insert,\n retrieve,\n retrieveInto,\n clear,\n dispose,\n get disposed() {\n return state.disposed;\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["QuadtreeError","QuadtreeDisposedError","rectsOverlap","a","b","rootContains","bounds","obj","inX","inY","quadrantIndices","node","midX","midY","inLeft","inRight","inTop","inBottom","result","subdivide","w","h","x","y","lvl","i","child","insertNode","maxObjects","maxLevels","clearNode","createQuadtree","opts","state","ck","insert","scratchSet","scratchStack","scratchRegion","retrieveSet","region","rx","ry","rw","rh","validateRegion","retrieve","retrieveInto","target","set","v","clear","dispose"],"mappings":"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","file":"index.cjs","sourcesContent":["// aiquadtreejs — 2D quadtree for per-frame rebuild collision broadphase.\n//\n// v0.5.1: full implementation. Plain-object nodes, iterative-DFS retrieve,\n// Set-based dedup, idempotent dispose, destructurable methods (no `this`).\n\n/**\n * Axis-aligned bounding box.\n *\n * Right-open coordinate semantics: `x` / `y` are the top-left corner and\n * `x + width` / `y + height` are **exclusive**. A 32×32 box at `(0, 0)`\n * covers `[0, 32)` on both axes. (This matches the convention used by\n * renderers such as PixiJS `getBounds()`, but the type is renderer-agnostic.)\n *\n * @public\n */\nexport interface AABB {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\n/**\n * Configuration for {@link createQuadtree}.\n *\n * @public\n */\nexport interface QuadtreeOptions {\n /**\n * Outer bounds. Objects partially outside `bounds` still insert into\n * whichever child nodes they overlap; objects fully outside are ignored\n * by `retrieve()` because no node overlaps them.\n */\n bounds: AABB;\n\n /**\n * Threshold above which a node subdivides. Default `10`. Lower values\n * mean deeper trees and fewer candidates per `retrieve()`; higher values\n * mean shallower trees and cheaper `insert()`.\n */\n maxObjects?: number;\n\n /**\n * Maximum subdivision depth. Default `4`. Caps recursion so a very dense\n * cluster doesn't blow up into an unbounded tree.\n *\n * **Spanning-object cost warning:** an object that spans multiple quadrant\n * boundaries is copied into every child node it overlaps. In the worst case\n * (an object covering the entire tree bounds) at depth `L`, up to `4^L`\n * nodes each hold a reference to that object. The default of `4` means at\n * most 256 leaf nodes; raising `maxLevels` to `10` allows ~1 M nodes, and\n * `20` allows ~10^12 — **OOM territory for dense inputs with spanning\n * objects**. Raise this value only when you understand the distribution of\n * large vs small objects in your scene. No upper-bound cap is applied\n * (the caller knows their workload); the default `4` is safe for typical\n * game scenes with 500–10,000 entities.\n */\n maxLevels?: number;\n}\n\n/**\n * Quadtree storing objects that extend {@link AABB}. `T` may carry any\n * payload (entity ID, sprite reference, user data) alongside the geometry.\n *\n * The expected usage pattern is **per-frame rebuild**: at the start of each\n * frame, call `clear()` and re-`insert()` every active object. This is\n * cheaper than tracking movements through the tree and gives correct results\n * regardless of how objects moved.\n *\n * @public\n */\nexport interface Quadtree<T extends AABB> {\n /**\n * Insert an object. The same object reference may legitimately appear\n * in multiple leaf nodes when it spans quadrant boundaries; `retrieve()`\n * deduplicates with a `Set` so the caller sees it exactly once.\n *\n * @throws {@link QuadtreeError} if any of `x`, `y`, `width`, or `height`\n * is non-finite (`NaN`, `Infinity`, `-Infinity`), or if `width` or\n * `height` is negative. Zero-extent objects (points / lines) are valid.\n */\n insert(obj: T): void;\n\n /**\n * Return every inserted object whose containing node overlaps `region`,\n * deduplicated. The result is a **broadphase**: callers must still run\n * a precise AABB or pixel-level hit test on each candidate.\n *\n * @throws {@link QuadtreeError} if any of `region.x`, `region.y`,\n * `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,\n * `-Infinity`), or if `region.width` or `region.height` is negative.\n * Zero-extent regions are valid (they still query any overlapping node).\n */\n retrieve(region: AABB): T[];\n\n /**\n * Zero-allocation variant of {@link retrieve}.\n *\n * Clears `target` (sets `target.length = 0`), walks the tree using the same\n * iterative DFS + Set-based dedup as {@link retrieve}, then writes every\n * deduplicated candidate into `target` and returns it.\n *\n * Designed for hot-path callers (per-frame broadphase queries in a game\n * loop) that hold a permanent `T[]` buffer and want to avoid allocating a\n * fresh result array on every call.\n *\n * @invariant `target` identity is preserved — only its contents are\n * replaced. `retrieveInto(r, buf) === buf` always holds.\n * @invariant After return, `target.length` equals the deduplicated\n * candidate count. No `undefined` / `null` holes.\n * @invariant Empty result set → `target.length === 0`.\n * @invariant Dedup semantics identical to {@link retrieve}: objects\n * spanning multiple quadrants appear exactly once.\n *\n * Allocation: in steady state this performs no per-call heap allocation.\n * The dedup `Set` and DFS stack are reused across calls (cleared, not\n * re-created), and results are written into the caller's `target` instead\n * of a fresh array. The first calls may grow the internal scratch; once\n * result sizes stabilise, allocation amortises to zero — the design goal\n * for per-frame broadphase loops issuing thousands of queries.\n *\n * @throws {@link QuadtreeError} if any of `region.x`, `region.y`,\n * `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,\n * `-Infinity`), or if `region.width` or `region.height` is negative.\n * Zero-extent regions are valid.\n */\n retrieveInto(region: AABB, target: T[]): T[];\n\n /**\n * Reset the tree to empty. The root node object is reused across\n * frames; child nodes are released on clear() and re-created next\n * time subdivision triggers. The per-frame churn is bounded by\n * `4 * (subdivided-internal-node-count)` and stays well inside V8's\n * young-generation budget for typical game-loop usage.\n *\n * Internal scratch buffers (dedup `Set` + DFS stack) are also drained on\n * clear(), matching the GC guarantee already provided by {@link dispose}.\n * This ensures a tree held alive but not queried after clear() does not\n * retain the previous query's object references.\n */\n clear(): void;\n\n /**\n * Idempotent teardown. Drops references so the GC can reclaim everything.\n * After disposal, every method except `dispose` itself — `insert`,\n * `retrieve`, `retrieveInto`, `clear` — throws {@link QuadtreeDisposedError}.\n */\n dispose(): void;\n\n /** `true` once {@link dispose} has been called. */\n readonly disposed: boolean;\n}\n\n/**\n * Recoverable quadtree error — thrown by `createQuadtree` for invalid\n * construction options and by `insert()` for precondition violations\n * (e.g. an inserted object with non-finite coordinates or negative\n * `width` / `height`).\n *\n * @public\n */\nexport class QuadtreeError extends Error {\n override readonly name = \"QuadtreeError\";\n}\n\n/**\n * Thrown by any quadtree method called after {@link Quadtree.dispose}.\n *\n * @public\n */\nexport class QuadtreeDisposedError extends Error {\n override readonly name = \"QuadtreeDisposedError\";\n}\n\n// ---------------------------------------------------------------------------\n// Internal types\n// ---------------------------------------------------------------------------\n\ninterface Node<T extends AABB> {\n bounds: AABB;\n level: number;\n objects: T[];\n children: Node<T>[];\n}\n\ninterface State<T extends AABB> {\n root: Node<T>;\n maxObjects: number;\n maxLevels: number;\n disposed: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction rectsOverlap(a: AABB, b: AABB): boolean {\n return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;\n}\n\n// Root containment check used only at the insert root gate.\n//\n// Right-open semantics for positive-extent dimensions (matching rectsOverlap):\n// contained iff obj.x < bounds.x + bounds.width AND obj.x + obj.width > bounds.x\n//\n// Zero-extent exception for the minimum edge: a zero-size point sitting exactly\n// on bounds.x or bounds.y satisfies neither side of the strict-inequality test,\n// so it would be silently dropped. Instead, per axis:\n// - zero-extent: contained iff coordinate is within [bounds.min, bounds.max) —\n// inclusive minimum, exclusive maximum (right-open, matching the box contract)\n// - positive-extent: keep the existing strict right-open overlap (unchanged)\n//\n// This matches quadrantIndices' own zero-extent fallback (obj.x >= midX etc.)\n// and preserves the invariant that a positive-size object flush on the right/bottom\n// exclusive boundary stays rejected.\nfunction rootContains(bounds: AABB, obj: AABB): boolean {\n const inX =\n obj.width === 0\n ? obj.x >= bounds.x && obj.x < bounds.x + bounds.width\n : obj.x < bounds.x + bounds.width && obj.x + obj.width > bounds.x;\n const inY =\n obj.height === 0\n ? obj.y >= bounds.y && obj.y < bounds.y + bounds.height\n : obj.y < bounds.y + bounds.height && obj.y + obj.height > bounds.y;\n return inX && inY;\n}\n\nfunction quadrantIndices<T extends AABB>(node: Node<T>, obj: AABB): number[] {\n const midX = node.bounds.x + node.bounds.width / 2;\n const midY = node.bounds.y + node.bounds.height / 2;\n // Zero-extent objects (points) sitting exactly on midX / midY would fall\n // through both `<` and `>` checks; treat the point as belonging to the\n // right/bottom side so it doesn't silently disappear.\n const inLeft = obj.x < midX;\n const inRight = obj.width === 0 ? obj.x >= midX : obj.x + obj.width > midX;\n const inTop = obj.y < midY;\n const inBottom = obj.height === 0 ? obj.y >= midY : obj.y + obj.height > midY;\n const result: number[] = [];\n if (inTop && inLeft) result.push(0);\n if (inTop && inRight) result.push(1);\n if (inBottom && inLeft) result.push(2);\n if (inBottom && inRight) result.push(3);\n return result;\n}\n\nfunction subdivide<T extends AABB>(node: Node<T>): void {\n const w = node.bounds.width / 2;\n const h = node.bounds.height / 2;\n const x = node.bounds.x;\n const y = node.bounds.y;\n const lvl = node.level + 1;\n node.children.push(\n { bounds: { x, y, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x: x + w, y, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x, y: y + h, width: w, height: h }, level: lvl, objects: [], children: [] },\n { bounds: { x: x + w, y: y + h, width: w, height: h }, level: lvl, objects: [], children: [] },\n );\n for (const obj of node.objects) {\n for (const i of quadrantIndices(node, obj)) {\n const child = node.children[i];\n if (child !== undefined) child.objects.push(obj);\n }\n }\n node.objects.length = 0;\n}\n\nfunction insertNode<T extends AABB>(\n node: Node<T>,\n obj: T,\n maxObjects: number,\n maxLevels: number,\n): void {\n // Reject objects entirely outside the root bounds; for inner nodes we\n // trust `quadrantIndices` to route correctly (it has zero-extent fallback\n // logic that `rectsOverlap` does not, so the strict check is too tight\n // at child level for points sitting on a child boundary).\n // rootContains is used instead of rectsOverlap here so that zero-size\n // points/lines sitting exactly on the minimum (left/top) edge are accepted\n // with inclusive semantics, whilst positive-size objects retain right-open\n // exclusion on the maximum edge.\n if (node.level === 0 && !rootContains(node.bounds, obj)) return;\n if (node.children.length === 4) {\n for (const i of quadrantIndices(node, obj)) {\n const child = node.children[i];\n if (child !== undefined) insertNode(child, obj, maxObjects, maxLevels);\n }\n return;\n }\n node.objects.push(obj);\n if (node.objects.length > maxObjects && node.level < maxLevels) {\n subdivide(node);\n }\n}\n\nfunction clearNode<T extends AABB>(node: Node<T>): void {\n node.objects.length = 0;\n for (const child of node.children) {\n clearNode(child);\n }\n node.children.length = 0;\n}\n\n// ---------------------------------------------------------------------------\n// Factory\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a 2D quadtree.\n *\n * @example\n * ```ts\n * import { createQuadtree, type AABB } from \"aiquadtreejs\";\n *\n * interface Body extends AABB {\n * id: number;\n * }\n *\n * const entities: Body[] = [\n * { id: 1, x: 100, y: 100, width: 32, height: 32 },\n * { id: 2, x: 400, y: 250, width: 32, height: 32 },\n * ];\n * const player: Body = { id: 0, x: 200, y: 200, width: 32, height: 32 };\n *\n * const qt = createQuadtree<Body>({\n * bounds: { x: 0, y: 0, width: 800, height: 600 },\n * maxObjects: 10,\n * maxLevels: 4,\n * });\n *\n * // Per-frame:\n * qt.clear();\n * for (const e of entities) qt.insert(e);\n *\n * // Broadphase lookup near the player:\n * const region: AABB = { x: player.x - 50, y: player.y - 50, width: 100, height: 100 };\n * const candidates = qt.retrieve(region);\n * // Caller runs a precise hit test on `candidates`.\n * ```\n *\n * @public\n */\nexport function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T> {\n const { bounds } = opts;\n const maxObjects = opts.maxObjects ?? 10;\n const maxLevels = opts.maxLevels ?? 4;\n\n if (\n !Number.isFinite(bounds.x) ||\n !Number.isFinite(bounds.y) ||\n !Number.isFinite(bounds.width) ||\n !Number.isFinite(bounds.height)\n ) {\n throw new QuadtreeError(\"bounds must contain finite numbers\");\n }\n if (bounds.width <= 0) {\n throw new QuadtreeError(\"bounds.width must be > 0\");\n }\n if (bounds.height <= 0) {\n throw new QuadtreeError(\"bounds.height must be > 0\");\n }\n if (!Number.isInteger(maxObjects) || maxObjects <= 0) {\n throw new QuadtreeError(\"maxObjects must be a positive integer\");\n }\n if (!Number.isInteger(maxLevels) || maxLevels <= 0) {\n throw new QuadtreeError(\"maxLevels must be a positive integer\");\n }\n\n const state: State<T> = {\n root: {\n bounds: { ...bounds },\n level: 0,\n objects: [],\n children: [],\n },\n maxObjects,\n maxLevels,\n disposed: false,\n };\n\n function ck(): void {\n if (state.disposed) throw new QuadtreeDisposedError(\"aiquadtreejs: quadtree has been disposed\");\n }\n\n function insert(obj: T): void {\n ck();\n if (\n !obj ||\n !Number.isFinite(obj.x) ||\n !Number.isFinite(obj.y) ||\n !Number.isFinite(obj.width) ||\n !Number.isFinite(obj.height)\n ) {\n throw new QuadtreeError(\n \"inserted object must be defined with finite numeric x, y, width and height\",\n );\n }\n if (obj.width < 0) {\n throw new QuadtreeError(\"inserted object width must be >= 0\");\n }\n if (obj.height < 0) {\n throw new QuadtreeError(\"inserted object height must be >= 0\");\n }\n insertNode(state.root, obj, state.maxObjects, state.maxLevels);\n }\n\n // Reusable scratch for retrieveSet, hoisted so steady-state queries\n // allocate nothing. Safe because the returned Set never escapes the\n // module: retrieve copies it out via Array.from and retrieveInto via a\n // push loop, both synchronously and fully before any subsequent call.\n //\n // Plain-data assumption (tightened, QDT-B-02): region.x/y/width/height\n // are read once into locals at the top of retrieveSet, then written into\n // the reusable scratchRegion (no per-query allocation — the zero-alloc\n // contract of retrieveInto holds). This prevents a structurally-typed\n // region whose getter calls back into retrieve* from corrupting the shared\n // scratch mid-walk: any re-entrant call triggered by a getter completes\n // synchronously during the four reads, before this call touches scratch.\n // Adversarial-only: plain-object callers (all documented examples) are\n // unaffected.\n const scratchSet = new Set<T>();\n const scratchStack: Node<T>[] = [];\n const scratchRegion: AABB = { x: 0, y: 0, width: 0, height: 0 };\n\n function retrieveSet(region: AABB): Set<T> {\n // Snapshot region fields into locals once so that a getter-bearing\n // region cannot mutate the walk by re-entering retrieve* mid-DFS.\n const rx = region.x;\n const ry = region.y;\n const rw = region.width;\n const rh = region.height;\n scratchRegion.x = rx;\n scratchRegion.y = ry;\n scratchRegion.width = rw;\n scratchRegion.height = rh;\n scratchSet.clear();\n scratchStack.length = 0;\n scratchStack.push(state.root);\n while (scratchStack.length > 0) {\n const node = scratchStack.pop();\n if (node === undefined) continue;\n if (!rectsOverlap(node.bounds, scratchRegion)) continue;\n for (const obj of node.objects) scratchSet.add(obj);\n for (const child of node.children) scratchStack.push(child);\n }\n return scratchSet;\n }\n\n /**\n * Validate a region AABB for use in retrieve / retrieveInto.\n * Mirrors insert()'s 0.5.1 validation: non-finite coordinates or negative\n * dimensions throw QuadtreeError with an `aiquadtreejs: ` prefix message.\n */\n function validateRegion(region: AABB): void {\n if (\n !region ||\n !Number.isFinite(region.x) ||\n !Number.isFinite(region.y) ||\n !Number.isFinite(region.width) ||\n !Number.isFinite(region.height)\n ) {\n throw new QuadtreeError(\n \"aiquadtreejs: retrieve region must have finite numeric x, y, width and height\",\n );\n }\n if (region.width < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region width must be >= 0\");\n }\n if (region.height < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region height must be >= 0\");\n }\n }\n\n function retrieve(region: AABB): T[] {\n ck();\n validateRegion(region);\n return Array.from(retrieveSet(region));\n }\n\n function retrieveInto(region: AABB, target: T[]): T[] {\n ck();\n validateRegion(region);\n const set = retrieveSet(region);\n target.length = 0;\n for (const v of set) target.push(v);\n return target;\n }\n\n function clear(): void {\n ck();\n clearNode(state.root);\n // Drain internal scratch so that a tree held alive but not queried after\n // clear() does not pin the previous query's object references against GC.\n // (dispose() drains scratch for the same reason; clear() now provides the\n // same guarantee for the per-frame rebuild pattern.)\n scratchSet.clear();\n scratchStack.length = 0;\n }\n\n function dispose(): void {\n if (state.disposed) return;\n state.disposed = true;\n state.root.objects.length = 0;\n state.root.children.length = 0;\n scratchSet.clear();\n scratchStack.length = 0;\n }\n\n return {\n insert,\n retrieve,\n retrieveInto,\n clear,\n dispose,\n get disposed() {\n return state.disposed;\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -35,6 +35,17 @@ interface QuadtreeOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* Maximum subdivision depth. Default `4`. Caps recursion so a very dense
|
|
37
37
|
* cluster doesn't blow up into an unbounded tree.
|
|
38
|
+
*
|
|
39
|
+
* **Spanning-object cost warning:** an object that spans multiple quadrant
|
|
40
|
+
* boundaries is copied into every child node it overlaps. In the worst case
|
|
41
|
+
* (an object covering the entire tree bounds) at depth `L`, up to `4^L`
|
|
42
|
+
* nodes each hold a reference to that object. The default of `4` means at
|
|
43
|
+
* most 256 leaf nodes; raising `maxLevels` to `10` allows ~1 M nodes, and
|
|
44
|
+
* `20` allows ~10^12 — **OOM territory for dense inputs with spanning
|
|
45
|
+
* objects**. Raise this value only when you understand the distribution of
|
|
46
|
+
* large vs small objects in your scene. No upper-bound cap is applied
|
|
47
|
+
* (the caller knows their workload); the default `4` is safe for typical
|
|
48
|
+
* game scenes with 500–10,000 entities.
|
|
38
49
|
*/
|
|
39
50
|
maxLevels?: number;
|
|
40
51
|
}
|
|
@@ -64,6 +75,11 @@ interface Quadtree<T extends AABB> {
|
|
|
64
75
|
* Return every inserted object whose containing node overlaps `region`,
|
|
65
76
|
* deduplicated. The result is a **broadphase**: callers must still run
|
|
66
77
|
* a precise AABB or pixel-level hit test on each candidate.
|
|
78
|
+
*
|
|
79
|
+
* @throws {@link QuadtreeError} if any of `region.x`, `region.y`,
|
|
80
|
+
* `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,
|
|
81
|
+
* `-Infinity`), or if `region.width` or `region.height` is negative.
|
|
82
|
+
* Zero-extent regions are valid (they still query any overlapping node).
|
|
67
83
|
*/
|
|
68
84
|
retrieve(region: AABB): T[];
|
|
69
85
|
/**
|
|
@@ -91,6 +107,11 @@ interface Quadtree<T extends AABB> {
|
|
|
91
107
|
* of a fresh array. The first calls may grow the internal scratch; once
|
|
92
108
|
* result sizes stabilise, allocation amortises to zero — the design goal
|
|
93
109
|
* for per-frame broadphase loops issuing thousands of queries.
|
|
110
|
+
*
|
|
111
|
+
* @throws {@link QuadtreeError} if any of `region.x`, `region.y`,
|
|
112
|
+
* `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,
|
|
113
|
+
* `-Infinity`), or if `region.width` or `region.height` is negative.
|
|
114
|
+
* Zero-extent regions are valid.
|
|
94
115
|
*/
|
|
95
116
|
retrieveInto(region: AABB, target: T[]): T[];
|
|
96
117
|
/**
|
|
@@ -99,6 +120,11 @@ interface Quadtree<T extends AABB> {
|
|
|
99
120
|
* time subdivision triggers. The per-frame churn is bounded by
|
|
100
121
|
* `4 * (subdivided-internal-node-count)` and stays well inside V8's
|
|
101
122
|
* young-generation budget for typical game-loop usage.
|
|
123
|
+
*
|
|
124
|
+
* Internal scratch buffers (dedup `Set` + DFS stack) are also drained on
|
|
125
|
+
* clear(), matching the GC guarantee already provided by {@link dispose}.
|
|
126
|
+
* This ensures a tree held alive but not queried after clear() does not
|
|
127
|
+
* retain the previous query's object references.
|
|
102
128
|
*/
|
|
103
129
|
clear(): void;
|
|
104
130
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,17 @@ interface QuadtreeOptions {
|
|
|
35
35
|
/**
|
|
36
36
|
* Maximum subdivision depth. Default `4`. Caps recursion so a very dense
|
|
37
37
|
* cluster doesn't blow up into an unbounded tree.
|
|
38
|
+
*
|
|
39
|
+
* **Spanning-object cost warning:** an object that spans multiple quadrant
|
|
40
|
+
* boundaries is copied into every child node it overlaps. In the worst case
|
|
41
|
+
* (an object covering the entire tree bounds) at depth `L`, up to `4^L`
|
|
42
|
+
* nodes each hold a reference to that object. The default of `4` means at
|
|
43
|
+
* most 256 leaf nodes; raising `maxLevels` to `10` allows ~1 M nodes, and
|
|
44
|
+
* `20` allows ~10^12 — **OOM territory for dense inputs with spanning
|
|
45
|
+
* objects**. Raise this value only when you understand the distribution of
|
|
46
|
+
* large vs small objects in your scene. No upper-bound cap is applied
|
|
47
|
+
* (the caller knows their workload); the default `4` is safe for typical
|
|
48
|
+
* game scenes with 500–10,000 entities.
|
|
38
49
|
*/
|
|
39
50
|
maxLevels?: number;
|
|
40
51
|
}
|
|
@@ -64,6 +75,11 @@ interface Quadtree<T extends AABB> {
|
|
|
64
75
|
* Return every inserted object whose containing node overlaps `region`,
|
|
65
76
|
* deduplicated. The result is a **broadphase**: callers must still run
|
|
66
77
|
* a precise AABB or pixel-level hit test on each candidate.
|
|
78
|
+
*
|
|
79
|
+
* @throws {@link QuadtreeError} if any of `region.x`, `region.y`,
|
|
80
|
+
* `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,
|
|
81
|
+
* `-Infinity`), or if `region.width` or `region.height` is negative.
|
|
82
|
+
* Zero-extent regions are valid (they still query any overlapping node).
|
|
67
83
|
*/
|
|
68
84
|
retrieve(region: AABB): T[];
|
|
69
85
|
/**
|
|
@@ -91,6 +107,11 @@ interface Quadtree<T extends AABB> {
|
|
|
91
107
|
* of a fresh array. The first calls may grow the internal scratch; once
|
|
92
108
|
* result sizes stabilise, allocation amortises to zero — the design goal
|
|
93
109
|
* for per-frame broadphase loops issuing thousands of queries.
|
|
110
|
+
*
|
|
111
|
+
* @throws {@link QuadtreeError} if any of `region.x`, `region.y`,
|
|
112
|
+
* `region.width`, or `region.height` is non-finite (`NaN`, `Infinity`,
|
|
113
|
+
* `-Infinity`), or if `region.width` or `region.height` is negative.
|
|
114
|
+
* Zero-extent regions are valid.
|
|
94
115
|
*/
|
|
95
116
|
retrieveInto(region: AABB, target: T[]): T[];
|
|
96
117
|
/**
|
|
@@ -99,6 +120,11 @@ interface Quadtree<T extends AABB> {
|
|
|
99
120
|
* time subdivision triggers. The per-frame churn is bounded by
|
|
100
121
|
* `4 * (subdivided-internal-node-count)` and stays well inside V8's
|
|
101
122
|
* young-generation budget for typical game-loop usage.
|
|
123
|
+
*
|
|
124
|
+
* Internal scratch buffers (dedup `Set` + DFS stack) are also drained on
|
|
125
|
+
* clear(), matching the GC guarantee already provided by {@link dispose}.
|
|
126
|
+
* This ensures a tree held alive but not queried after clear() does not
|
|
127
|
+
* retain the previous query's object references.
|
|
102
128
|
*/
|
|
103
129
|
clear(): void;
|
|
104
130
|
/**
|