aiquadtreejs 0.5.9 → 0.6.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Tiny 2D quadtree for per-frame rebuild collision broadphase. Insert AABBs, retrieve candidates, then run precise collision checks yourself.
4
4
 
5
- > **Status: 0.5.9 - stable 1.0-track surface.** The root entry is the public API.
5
+ > **Status: 0.6.0 - stable 1.0-track surface.** The root entry is the public API.
6
6
 
7
7
  ## Install
8
8
 
@@ -43,25 +43,28 @@ const candidates = tree.retrieve({ x: 80, y: 80, width: 120, height: 120 });
43
43
  - `createQuadtree<T extends AABB>({ bounds, maxObjects?, maxLevels? })` creates a tree.
44
44
  - `insert(obj)` stores an object reference in overlapping nodes.
45
45
  - `retrieve(region)` returns a deduplicated broadphase candidate array.
46
- - `retrieveInto(region, target)` reuses a caller-owned result array.
46
+ - `retrieveInto(region, target)` reuses a caller-owned result array; `target` must be an array.
47
47
  - `clear()` empties the tree for the next frame and clears scratch buffers.
48
48
  - `dispose()` is idempotent permanent teardown.
49
- - Errors: `QuadtreeError`, `QuadtreeDisposedError`.
49
+ - Errors: `QuadtreeError`, `QuadtreeDisposedError`. Every `QuadtreeError` message starts with `aiquadtreejs: `; match on the class, not the exact text.
50
50
 
51
51
  ## Model
52
52
 
53
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.
54
+ - This is a broadphase only. Returned candidates may not actually overlap the query region, but every inserted object that overlaps the region inside `bounds` is returned.
55
55
  - Expected usage is per-frame rebuild: `clear()`, insert active bodies, query.
56
56
  - Objects spanning quadrant boundaries can be stored in multiple child nodes; results are deduplicated.
57
+ - Nodes store their edges. Right/bottom children share the parent's exact `x + width` / `y + height`, so fractional bounds such as `x: -0.3, width: 2.4` lose nothing at the edge.
57
58
  - `maxLevels` has no hard cap. Very high values plus spanning objects can create huge node counts.
59
+ - Subdivision also stops once a node's midpoint is no longer representable in floating point (typically around depth 45-52), so a very high `maxLevels` cannot make a dense point cluster silently vanish from `retrieve()`.
58
60
 
59
61
  ## Sharp Edges
60
62
 
61
63
  - Zero-size points (width = 0, height = 0) follow right-open `[x, x+width)` semantics: a point on the minimum `x/y` boundary is **inclusive** and is inserted/retrieved correctly; a point at the exclusive maximum edge is outside the root and is ignored. (Was a bug before 0.5.8; fixed.)
62
64
  - Fully outside objects are ignored by retrieval.
63
- - Negative width/height and non-finite coordinates throw.
65
+ - `QuadtreeError` is thrown for a missing options object or `bounds`, negative width/height, non-finite coordinates, and a `retrieveInto()` target that is not an array.
64
66
  - `retrieveInto()` clears the target array before writing results.
67
+ - Inserted objects are stored by reference and re-read when their node subdivides. Do not move an inserted object until the next `clear()`; rebuild the tree instead.
65
68
  - After `dispose()`, all methods except `dispose()` throw `QuadtreeDisposedError`.
66
69
 
67
70
  ## AI Context
package/README_ZHTW.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  小型 2D quadtree,用於每 frame 重建的 collision broadphase。插入 AABB、取回候選物件,精確碰撞檢查由呼叫端負責。
4
4
 
5
- > **狀態:0.5.9 - 穩定 1.0 軌道 API。** root entry 是公開 API。
5
+ > **狀態:0.6.0 - 穩定 1.0 軌道 API。** root entry 是公開 API。
6
6
 
7
7
  ## 安裝
8
8
 
@@ -43,25 +43,28 @@ const candidates = tree.retrieve({ x: 80, y: 80, width: 120, height: 120 });
43
43
  - `createQuadtree<T extends AABB>({ bounds, maxObjects?, maxLevels? })` 建立 tree。
44
44
  - `insert(obj)` 將物件參照存進重疊 nodes。
45
45
  - `retrieve(region)` 回傳 dedup 後的 broadphase candidates。
46
- - `retrieveInto(region, target)` 重用呼叫端提供的 result array。
46
+ - `retrieveInto(region, target)` 重用呼叫端提供的 result array;`target` 必須是 array。
47
47
  - `clear()` 清空 tree 與 scratch buffers,準備下一 frame。
48
48
  - `dispose()` 是可重複呼叫的永久 teardown。
49
- - Errors:`QuadtreeError`、`QuadtreeDisposedError`。
49
+ - Errors:`QuadtreeError`、`QuadtreeDisposedError`。每個 `QuadtreeError` 訊息都以 `aiquadtreejs: ` 開頭;請比對 class,不要比對完整文字。
50
50
 
51
51
  ## Model
52
52
 
53
53
  - 座標採 right-open:`{ x, y, width, height }` 覆蓋 `[x, x + width)` 與 `[y, y + height)`。
54
- - 這只是 broadphase。回傳候選物件不保證真的與 query region 相交。
54
+ - 這只是 broadphase。回傳候選物件不保證真的與 query region 相交,但在 `bounds` 內與 region 重疊的每個已插入物件都一定會回傳。
55
55
  - 預期用法是每 frame 重建:`clear()`、插入 active bodies、query。
56
56
  - 跨 quadrant 的物件可能存在多個 child nodes;結果會 dedup。
57
+ - Node 直接儲存邊界。右側與下側 child 共用 parent 精確的 `x + width` 與 `y + height`,所以像 `x: -0.3, width: 2.4` 這類帶小數的 bounds 在邊緣也不會遺漏物件。
57
58
  - `maxLevels` 沒有硬上限。很高的值加上 spanning objects 可能建立巨大 node 數。
59
+ - 一旦 node 的中點在浮點數中無法被表示(通常在深度約 45-52 左右),subdivision 也會停止,所以再高的 `maxLevels` 也不會讓密集的 point cluster 從 `retrieve()` 中悄悄消失。
58
60
 
59
61
  ## 注意事項
60
62
 
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。
63
+ - 零尺寸 point(width = 0, height = 0)遵循 right-open 的 `[x, x+width)` 語意:剛好落在 minimum `x/y` 邊界上的 point 屬於**包含**範圍,會被正確插入與取回;落在 exclusive 的 maximum 邊界上的 point 則在 root 之外,會被忽略。(0.5.8 之前是已知 bug,現已修正。)
62
64
  - 完全在 bounds 外的物件不會被 retrieve 到。
63
- - 負 width/height 與非有限座標會 throw。
65
+ - 缺少 options 物件或 `bounds`、負 width/height、非有限座標,以及不是 array 的 `retrieveInto()` target,都會丟 `QuadtreeError`。
64
66
  - `retrieveInto()` 會先清空 target array 再寫入結果。
67
+ - 已插入的物件以參照儲存,所在 node 細分時會重新讀取座標。在下一次 `clear()` 之前不要移動已插入的物件;請改為重建 tree。
65
68
  - `dispose()` 後除了 `dispose()` 本身外,所有方法都會丟 `QuadtreeDisposedError`。
66
69
 
67
70
  ## AI Context
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
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
1
+ 'use strict';var o=class extends Error{name="QuadtreeError"},T=class extends Error{name="QuadtreeDisposedError"};function A(e,t,i,r,u){return {x0:e,y0:t,x1:i,y1:r,level:u,objects:[],children:[]}}function b(e,t){return e+(t-e)/2}function j(e,t){let i=t.width===0?t.x>=e.x0&&t.x<e.x1:t.x<e.x1&&t.x+t.width>e.x0,r=t.height===0?t.y>=e.y0&&t.y<e.y1:t.y<e.y1&&t.y+t.height>e.y0;return i&&r}function q(e,t){let i=b(e.x0,e.x1),r=b(e.y0,e.y1),u=t.x<i,c=t.width===0?t.x>=i:t.x+t.width>i,d=t.y<r,h=t.height===0?t.y>=r:t.y+t.height>r,n=[];return d&&u&&n.push(0),d&&c&&n.push(1),h&&u&&n.push(2),h&&c&&n.push(3),n}function X(e){let{x0:t,y0:i,x1:r,y1:u}=e,c=b(t,r),d=b(i,u),h=e.level+1;e.children.push(A(t,i,c,d,h),A(c,i,r,d,h),A(t,d,c,u,h),A(c,d,r,u,h));for(let n of e.objects)for(let f of q(e,n)){let B=e.children[f];B!==void 0&&B.objects.push(n);}e.objects.length=0;}function Y(e){let t=b(e.x0,e.x1),i=b(e.y0,e.y1);return e.x0<t&&t<e.x1&&e.y0<i&&i<e.y1}function F(e,t,i,r){if(!(e.level===0&&!j(e,t))){if(e.children.length===4){for(let u of q(e,t)){let c=e.children[u];c!==void 0&&F(c,t,i,r);}return}e.objects.push(t),e.objects.length>i&&e.level<r&&Y(e)&&X(e);}}function O(e){e.objects.length=0;for(let t of e.children)O(t);e.children.length=0;}function k(e){if(e===null||typeof e!="object")throw new o("aiquadtreejs: options must be an object with bounds");let t=e.bounds;if(t===null||typeof t!="object")throw new o("aiquadtreejs: bounds must be an object with finite numeric x, y, width and height");let i=e.maxObjects??10,r=e.maxLevels??4,u=t.x,c=t.y,d=t.width,h=t.height;if(!Number.isFinite(u)||!Number.isFinite(c)||!Number.isFinite(d)||!Number.isFinite(h))throw new o("aiquadtreejs: bounds must contain finite numbers");if(d<=0)throw new o("aiquadtreejs: bounds.width must be > 0");if(h<=0)throw new o("aiquadtreejs: bounds.height must be > 0");if(!Number.isInteger(i)||i<=0)throw new o("aiquadtreejs: maxObjects must be a positive integer");if(!Number.isInteger(r)||r<=0)throw new o("aiquadtreejs: maxLevels must be a positive integer");let n={root:A(u,c,u+d,c+h,0),maxObjects:i,maxLevels:r,disposed:false};function f(){if(n.disposed)throw new T("aiquadtreejs: quadtree has been disposed")}function B(s){if(f(),!s||!Number.isFinite(s.x)||!Number.isFinite(s.y)||!Number.isFinite(s.width)||!Number.isFinite(s.height))throw new o("aiquadtreejs: inserted object must be defined with finite numeric x, y, width and height");if(s.width<0)throw new o("aiquadtreejs: inserted object width must be >= 0");if(s.height<0)throw new o("aiquadtreejs: inserted object height must be >= 0");F(n.root,s,n.maxObjects,n.maxLevels);}let l=new Set,a=[],w={x:0,y:0,width:0,height:0};function N(s){let m=s?.x,y=s?.y,x=s?.width,v=s?.height;if(!Number.isFinite(m)||!Number.isFinite(y)||!Number.isFinite(x)||!Number.isFinite(v))throw new o("aiquadtreejs: retrieve region must have finite numeric x, y, width and height");if(x<0)throw new o("aiquadtreejs: retrieve region width must be >= 0");if(v<0)throw new o("aiquadtreejs: retrieve region height must be >= 0");for(w.x=m,w.y=y,w.width=x,w.height=v,l.clear(),a.length=0,a.push(n.root);a.length>0;){let g=a.pop();if(g!==void 0&&j(g,w)){for(let p of g.objects)l.add(p);for(let p of g.children)a.push(p);}}return l}function S(s){return f(),Array.from(N(s))}function I(s,m){if(f(),!Array.isArray(m))throw new o("aiquadtreejs: retrieveInto target must be an array");let y=N(s);m.length=0;for(let x of y)m.push(x);return m}function L(){f(),O(n.root),l.clear(),a.length=0;}function Q(){n.disposed||(n.disposed=true,n.root.objects.length=0,n.root.children.length=0,l.clear(),a.length=0);}return {insert:B,retrieve:S,retrieveInto:I,clear:L,dispose:Q,get disposed(){return n.disposed}}}exports.QuadtreeDisposedError=T;exports.QuadtreeError=o;exports.createQuadtree=k;//# sourceMappingURL=index.cjs.map
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
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// Plain-object nodes, iterative-DFS retrieve, Set-based dedup, idempotent\n// dispose, destructurable methods (no `this`). Version: see package.json.\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 * Non-finite coordinates or negative dimensions throw QuadtreeError with\n * 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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["QuadtreeError","QuadtreeDisposedError","makeNode","x0","y0","x1","y1","level","mid","a","b","nodeOverlaps","node","box","inX","inY","quadrantIndices","obj","midX","midY","inLeft","inRight","inTop","inBottom","result","subdivide","lvl","i","child","isSplitRepresentable","insertNode","maxObjects","maxLevels","clearNode","createQuadtree","opts","bounds","bx","by","bw","bh","state","ck","insert","scratchSet","scratchStack","scratchRegion","retrieveSet","region","rx","ry","rw","rh","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// Plain-object nodes, iterative-DFS retrieve, Set-based dedup, idempotent\n// dispose, destructurable methods (no `this`). Version: see package.json.\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 * Each field is read once, so accessor-backed bounds (e.g. PixiJS v8\n * `Bounds`) work. The root's exclusive edges are computed once as\n * `x + width` and `y + height`, and every right/bottom child shares its\n * parent's exact edge.\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 * **Precision-bound depth:** subdivision also stops once a node's\n * midpoint is no longer representable in floating point (its width or\n * height has fallen below the ulp of its coordinate) — typically around\n * depth 45-52 for typical scene-sized bounds, well below the 4^L node-count\n * concern above. Nodes past this depth become terminal leaves regardless\n * of `maxLevels`, so a very high `maxLevels` cannot make a dense point\n * cluster vanish from `retrieve()`.\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 `obj` is `null` / `undefined`, if any of\n * `x`, `y`, `width`, or `height` is non-finite (`NaN`, `Infinity`,\n * `-Infinity`), or if `width` or `height` is negative. Zero-extent\n * 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 * Each `region` field is read once and the walk uses the validated values,\n * so an accessor-backed region behaves like a plain object.\n *\n * @throws {@link QuadtreeError} if `region` is `null` / `undefined`, if any\n * of `region.x`, `region.y`, `region.width`, or `region.height` is\n * non-finite (`NaN`, `Infinity`, `-Infinity`), or if `region.width` or\n * `region.height` is negative. Zero-extent regions are valid (they still\n * query any overlapping node).\n */\n retrieve(region: AABB): T[];\n\n /**\n * Reduced-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: this avoids the fresh result array that {@link retrieve}\n * allocates on every call — the internal DFS stack is reused across calls,\n * and results are written into the caller's `target` instead of a new\n * array. It does **not** allocate zero heap per call in practice: on V8,\n * clearing the internal dedup `Set` replaces its backing table and\n * `target.length = 0` drops the target array's backing store, so both are\n * rebuilt on the next call, at a cost proportional to the result size.\n * These are small, short-lived young-generation allocations, not the\n * unbounded fresh-array allocation `retrieve()` makes, but they do not\n * amortise away to literally zero.\n *\n * @throws {@link QuadtreeError} if `target` is not an array (checked\n * before `target` is touched), or for the same `region` violations as\n * {@link retrieve}. 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 a missing or\n * non-object options argument, a missing or non-object `bounds`, or invalid\n * construction options; by `insert()` for precondition violations (e.g. an\n * inserted object with non-finite coordinates or negative `width` /\n * `height`); by `retrieve()` / `retrieveInto()` for regions with non-finite\n * fields or negative `width` / `height`; and by `retrieveInto()` for a\n * `target` that is not an array.\n *\n * Every message starts with `aiquadtreejs: ` (e.g.\n * `aiquadtreejs: maxObjects must be a positive integer`) and `name` is\n * `\"QuadtreeError\"`. Match on the class, not the exact text.\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\n// A node covers the right-open rectangle [x0, x1) x [y0, y1). Edges are\n// stored, never re-derived from a width: the root takes `x1 = x + width`\n// once from the validated bounds, and each child takes its parent's exact\n// edges and midpoint. Recomputing a right child's edge as `(x + w) + w` can\n// land an ulp short of the parent's `x + width`, and an object routed into\n// that sliver would then be missed by `retrieve()`.\ninterface Node<T extends AABB> {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\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 makeNode<T extends AABB>(\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n level: number,\n): Node<T> {\n return { x0, y0, x1, y1, level, objects: [], children: [] };\n}\n\n// Split line of [a, b). subdivide, quadrantIndices and isSplitRepresentable\n// all use this one formula, so the routing midpoint is bit-for-bit the edge\n// the children were built with.\nfunction mid(a: number, b: number): number {\n return a + (b - a) / 2;\n}\n\n// Node/box overlap test used at the insert root gate and by retrieve's\n// node walk (with the query region as `box`).\n//\n// Right-open semantics for positive-extent dimensions:\n// overlaps iff box.x < node.x1 AND box.x + box.width > node.x0\n//\n// Zero-extent exception for the minimum edge: a zero-size point sitting exactly\n// on node.x0 or node.y0 satisfies neither side of the strict-inequality test,\n// so it would be silently dropped (or, as a query, match no node). Instead, per axis:\n// - zero-extent: overlaps iff coordinate is within [x0, x1) —\n// inclusive minimum, exclusive maximum (right-open, matching the box contract)\n// - positive-extent: keep the strict right-open overlap\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 nodeOverlaps<T extends AABB>(node: Node<T>, box: AABB): boolean {\n const inX =\n box.width === 0\n ? box.x >= node.x0 && box.x < node.x1\n : box.x < node.x1 && box.x + box.width > node.x0;\n const inY =\n box.height === 0\n ? box.y >= node.y0 && box.y < node.y1\n : box.y < node.y1 && box.y + box.height > node.y0;\n return inX && inY;\n}\n\nfunction quadrantIndices<T extends AABB>(node: Node<T>, obj: AABB): number[] {\n const midX = mid(node.x0, node.x1);\n const midY = mid(node.y0, node.y1);\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 { x0, y0, x1, y1 } = node;\n const midX = mid(x0, x1);\n const midY = mid(y0, y1);\n const lvl = node.level + 1;\n // TL, TR, BL, BR. Right/bottom children take the parent's exact x1/y1.\n node.children.push(\n makeNode(x0, y0, midX, midY, lvl),\n makeNode(midX, y0, x1, midY, lvl),\n makeNode(x0, midY, midX, y1, lvl),\n makeNode(midX, midY, x1, y1, lvl),\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\n// True while the node still has a representable midpoint on both axes,\n// i.e. `mid(x0, x1)` actually lands strictly between `x0` and `x1` in\n// floating point (and likewise for y). Once a node's extent falls below the\n// ulp of its coordinate, the computed midpoint rounds onto `x0` or `x1`, so\n// `subdivide()` would create children that are not smaller than their\n// parent — an infinite-seeming split that silently stops matching queries\n// instead of erroring. Below this point further subdivision is skipped and\n// the node stays a terminal leaf even if `node.level < maxLevels`.\nfunction isSplitRepresentable<T extends AABB>(node: Node<T>): boolean {\n const midX = mid(node.x0, node.x1);\n const midY = mid(node.y0, node.y1);\n return node.x0 < midX && midX < node.x1 && node.y0 < midY && midY < node.y1;\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.\n // nodeOverlaps accepts zero-size points/lines sitting exactly on the\n // minimum (left/top) edge with inclusive semantics, whilst positive-size\n // objects retain right-open exclusion on the maximum edge.\n if (node.level === 0 && !nodeOverlaps(node, 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 && isSplitRepresentable(node)) {\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 * @throws {@link QuadtreeError} if `opts` is not an object, if `opts.bounds`\n * is not an object, if any bounds field is non-finite, if `bounds.width` or\n * `bounds.height` is not positive, or if `maxObjects` / `maxLevels` is not\n * a positive integer. Checked in that order, before the tree exists.\n *\n * @public\n */\nexport function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T> {\n if (opts === null || typeof opts !== \"object\") {\n throw new QuadtreeError(\"aiquadtreejs: options must be an object with bounds\");\n }\n const bounds = opts.bounds;\n if (bounds === null || typeof bounds !== \"object\") {\n throw new QuadtreeError(\n \"aiquadtreejs: bounds must be an object with finite numeric x, y, width and height\",\n );\n }\n const maxObjects = opts.maxObjects ?? 10;\n const maxLevels = opts.maxLevels ?? 4;\n // Read each field once: accessor-backed bounds (e.g. PixiJS v8 `Bounds`)\n // would be lost by an object spread, and a second read could disagree\n // with the validated value.\n const bx = bounds.x;\n const by = bounds.y;\n const bw = bounds.width;\n const bh = bounds.height;\n\n if (\n !Number.isFinite(bx) ||\n !Number.isFinite(by) ||\n !Number.isFinite(bw) ||\n !Number.isFinite(bh)\n ) {\n throw new QuadtreeError(\"aiquadtreejs: bounds must contain finite numbers\");\n }\n if (bw <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: bounds.width must be > 0\");\n }\n if (bh <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: bounds.height must be > 0\");\n }\n if (!Number.isInteger(maxObjects) || maxObjects <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: maxObjects must be a positive integer\");\n }\n if (!Number.isInteger(maxLevels) || maxLevels <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: maxLevels must be a positive integer\");\n }\n\n const state: State<T> = {\n root: makeNode(bx, by, bx + bw, by + bh, 0),\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 \"aiquadtreejs: inserted object must be defined with finite numeric x, y, width and height\",\n );\n }\n if (obj.width < 0) {\n throw new QuadtreeError(\"aiquadtreejs: inserted object width must be >= 0\");\n }\n if (obj.height < 0) {\n throw new QuadtreeError(\"aiquadtreejs: inserted object height must be >= 0\");\n }\n insertNode(state.root, obj, state.maxObjects, state.maxLevels);\n }\n\n // Reusable scratch for retrieveSet, hoisted to avoid a fresh DFS stack per\n // call. Safe because the returned Set never escapes the module: retrieve\n // copies it out via Array.from and retrieveInto via a push loop, both\n // synchronously and fully before any subsequent call. Note that\n // scratchSet.clear() still rebuilds the Set's backing table on V8, so this\n // does not make retrieveInto literally allocation-free — see its JSDoc.\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 // Validate `region` and collect the deduplicated candidates into scratchSet.\n //\n // Each region field is read exactly once, validated, and only then copied\n // into scratchRegion, so an accessor-backed region (e.g. PixiJS v8 `Bounds`)\n // walks with the values that passed validation, and a getter that calls\n // back into this tree completes during the four reads, before this call\n // touches the shared scratch. Optional chaining turns a nullish region into\n // the same QuadtreeError as a non-finite field.\n function retrieveSet(region: AABB): Set<T> {\n const rx = region?.x;\n const ry = region?.y;\n const rw = region?.width;\n const rh = region?.height;\n if (\n !Number.isFinite(rx) ||\n !Number.isFinite(ry) ||\n !Number.isFinite(rw) ||\n !Number.isFinite(rh)\n ) {\n throw new QuadtreeError(\n \"aiquadtreejs: retrieve region must have finite numeric x, y, width and height\",\n );\n }\n if (rw < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region width must be >= 0\");\n }\n if (rh < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region height must be >= 0\");\n }\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 (!nodeOverlaps(node, 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 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 if (!Array.isArray(target)) {\n throw new QuadtreeError(\"aiquadtreejs: retrieveInto target must be an array\");\n }\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
@@ -24,6 +24,11 @@ interface QuadtreeOptions {
24
24
  * Outer bounds. Objects partially outside `bounds` still insert into
25
25
  * whichever child nodes they overlap; objects fully outside are ignored
26
26
  * by `retrieve()` because no node overlaps them.
27
+ *
28
+ * Each field is read once, so accessor-backed bounds (e.g. PixiJS v8
29
+ * `Bounds`) work. The root's exclusive edges are computed once as
30
+ * `x + width` and `y + height`, and every right/bottom child shares its
31
+ * parent's exact edge.
27
32
  */
28
33
  bounds: AABB;
29
34
  /**
@@ -46,6 +51,14 @@ interface QuadtreeOptions {
46
51
  * large vs small objects in your scene. No upper-bound cap is applied
47
52
  * (the caller knows their workload); the default `4` is safe for typical
48
53
  * game scenes with 500–10,000 entities.
54
+ *
55
+ * **Precision-bound depth:** subdivision also stops once a node's
56
+ * midpoint is no longer representable in floating point (its width or
57
+ * height has fallen below the ulp of its coordinate) — typically around
58
+ * depth 45-52 for typical scene-sized bounds, well below the 4^L node-count
59
+ * concern above. Nodes past this depth become terminal leaves regardless
60
+ * of `maxLevels`, so a very high `maxLevels` cannot make a dense point
61
+ * cluster vanish from `retrieve()`.
49
62
  */
50
63
  maxLevels?: number;
51
64
  }
@@ -66,9 +79,10 @@ interface Quadtree<T extends AABB> {
66
79
  * in multiple leaf nodes when it spans quadrant boundaries; `retrieve()`
67
80
  * deduplicates with a `Set` so the caller sees it exactly once.
68
81
  *
69
- * @throws {@link QuadtreeError} if any of `x`, `y`, `width`, or `height`
70
- * is non-finite (`NaN`, `Infinity`, `-Infinity`), or if `width` or
71
- * `height` is negative. Zero-extent objects (points / lines) are valid.
82
+ * @throws {@link QuadtreeError} if `obj` is `null` / `undefined`, if any of
83
+ * `x`, `y`, `width`, or `height` is non-finite (`NaN`, `Infinity`,
84
+ * `-Infinity`), or if `width` or `height` is negative. Zero-extent
85
+ * objects (points / lines) are valid.
72
86
  */
73
87
  insert(obj: T): void;
74
88
  /**
@@ -76,14 +90,18 @@ interface Quadtree<T extends AABB> {
76
90
  * deduplicated. The result is a **broadphase**: callers must still run
77
91
  * a precise AABB or pixel-level hit test on each candidate.
78
92
  *
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).
93
+ * Each `region` field is read once and the walk uses the validated values,
94
+ * so an accessor-backed region behaves like a plain object.
95
+ *
96
+ * @throws {@link QuadtreeError} if `region` is `null` / `undefined`, if any
97
+ * of `region.x`, `region.y`, `region.width`, or `region.height` is
98
+ * non-finite (`NaN`, `Infinity`, `-Infinity`), or if `region.width` or
99
+ * `region.height` is negative. Zero-extent regions are valid (they still
100
+ * query any overlapping node).
83
101
  */
84
102
  retrieve(region: AABB): T[];
85
103
  /**
86
- * Zero-allocation variant of {@link retrieve}.
104
+ * Reduced-allocation variant of {@link retrieve}.
87
105
  *
88
106
  * Clears `target` (sets `target.length = 0`), walks the tree using the same
89
107
  * iterative DFS + Set-based dedup as {@link retrieve}, then writes every
@@ -101,17 +119,20 @@ interface Quadtree<T extends AABB> {
101
119
  * @invariant Dedup semantics identical to {@link retrieve}: objects
102
120
  * spanning multiple quadrants appear exactly once.
103
121
  *
104
- * Allocation: in steady state this performs no per-call heap allocation.
105
- * The dedup `Set` and DFS stack are reused across calls (cleared, not
106
- * re-created), and results are written into the caller's `target` instead
107
- * of a fresh array. The first calls may grow the internal scratch; once
108
- * result sizes stabilise, allocation amortises to zero — the design goal
109
- * for per-frame broadphase loops issuing thousands of queries.
122
+ * Allocation: this avoids the fresh result array that {@link retrieve}
123
+ * allocates on every call — the internal DFS stack is reused across calls,
124
+ * and results are written into the caller's `target` instead of a new
125
+ * array. It does **not** allocate zero heap per call in practice: on V8,
126
+ * clearing the internal dedup `Set` replaces its backing table and
127
+ * `target.length = 0` drops the target array's backing store, so both are
128
+ * rebuilt on the next call, at a cost proportional to the result size.
129
+ * These are small, short-lived young-generation allocations, not the
130
+ * unbounded fresh-array allocation `retrieve()` makes, but they do not
131
+ * amortise away to literally zero.
110
132
  *
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.
133
+ * @throws {@link QuadtreeError} if `target` is not an array (checked
134
+ * before `target` is touched), or for the same `region` violations as
135
+ * {@link retrieve}. Zero-extent regions are valid.
115
136
  */
116
137
  retrieveInto(region: AABB, target: T[]): T[];
117
138
  /**
@@ -137,10 +158,17 @@ interface Quadtree<T extends AABB> {
137
158
  readonly disposed: boolean;
138
159
  }
139
160
  /**
140
- * Recoverable quadtree error — thrown by `createQuadtree` for invalid
141
- * construction options and by `insert()` for precondition violations
142
- * (e.g. an inserted object with non-finite coordinates or negative
143
- * `width` / `height`).
161
+ * Recoverable quadtree error — thrown by `createQuadtree` for a missing or
162
+ * non-object options argument, a missing or non-object `bounds`, or invalid
163
+ * construction options; by `insert()` for precondition violations (e.g. an
164
+ * inserted object with non-finite coordinates or negative `width` /
165
+ * `height`); by `retrieve()` / `retrieveInto()` for regions with non-finite
166
+ * fields or negative `width` / `height`; and by `retrieveInto()` for a
167
+ * `target` that is not an array.
168
+ *
169
+ * Every message starts with `aiquadtreejs: ` (e.g.
170
+ * `aiquadtreejs: maxObjects must be a positive integer`) and `name` is
171
+ * `"QuadtreeError"`. Match on the class, not the exact text.
144
172
  *
145
173
  * @public
146
174
  */
@@ -188,6 +216,11 @@ declare class QuadtreeDisposedError extends Error {
188
216
  * // Caller runs a precise hit test on `candidates`.
189
217
  * ```
190
218
  *
219
+ * @throws {@link QuadtreeError} if `opts` is not an object, if `opts.bounds`
220
+ * is not an object, if any bounds field is non-finite, if `bounds.width` or
221
+ * `bounds.height` is not positive, or if `maxObjects` / `maxLevels` is not
222
+ * a positive integer. Checked in that order, before the tree exists.
223
+ *
191
224
  * @public
192
225
  */
193
226
  declare function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T>;
package/dist/index.d.ts CHANGED
@@ -24,6 +24,11 @@ interface QuadtreeOptions {
24
24
  * Outer bounds. Objects partially outside `bounds` still insert into
25
25
  * whichever child nodes they overlap; objects fully outside are ignored
26
26
  * by `retrieve()` because no node overlaps them.
27
+ *
28
+ * Each field is read once, so accessor-backed bounds (e.g. PixiJS v8
29
+ * `Bounds`) work. The root's exclusive edges are computed once as
30
+ * `x + width` and `y + height`, and every right/bottom child shares its
31
+ * parent's exact edge.
27
32
  */
28
33
  bounds: AABB;
29
34
  /**
@@ -46,6 +51,14 @@ interface QuadtreeOptions {
46
51
  * large vs small objects in your scene. No upper-bound cap is applied
47
52
  * (the caller knows their workload); the default `4` is safe for typical
48
53
  * game scenes with 500–10,000 entities.
54
+ *
55
+ * **Precision-bound depth:** subdivision also stops once a node's
56
+ * midpoint is no longer representable in floating point (its width or
57
+ * height has fallen below the ulp of its coordinate) — typically around
58
+ * depth 45-52 for typical scene-sized bounds, well below the 4^L node-count
59
+ * concern above. Nodes past this depth become terminal leaves regardless
60
+ * of `maxLevels`, so a very high `maxLevels` cannot make a dense point
61
+ * cluster vanish from `retrieve()`.
49
62
  */
50
63
  maxLevels?: number;
51
64
  }
@@ -66,9 +79,10 @@ interface Quadtree<T extends AABB> {
66
79
  * in multiple leaf nodes when it spans quadrant boundaries; `retrieve()`
67
80
  * deduplicates with a `Set` so the caller sees it exactly once.
68
81
  *
69
- * @throws {@link QuadtreeError} if any of `x`, `y`, `width`, or `height`
70
- * is non-finite (`NaN`, `Infinity`, `-Infinity`), or if `width` or
71
- * `height` is negative. Zero-extent objects (points / lines) are valid.
82
+ * @throws {@link QuadtreeError} if `obj` is `null` / `undefined`, if any of
83
+ * `x`, `y`, `width`, or `height` is non-finite (`NaN`, `Infinity`,
84
+ * `-Infinity`), or if `width` or `height` is negative. Zero-extent
85
+ * objects (points / lines) are valid.
72
86
  */
73
87
  insert(obj: T): void;
74
88
  /**
@@ -76,14 +90,18 @@ interface Quadtree<T extends AABB> {
76
90
  * deduplicated. The result is a **broadphase**: callers must still run
77
91
  * a precise AABB or pixel-level hit test on each candidate.
78
92
  *
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).
93
+ * Each `region` field is read once and the walk uses the validated values,
94
+ * so an accessor-backed region behaves like a plain object.
95
+ *
96
+ * @throws {@link QuadtreeError} if `region` is `null` / `undefined`, if any
97
+ * of `region.x`, `region.y`, `region.width`, or `region.height` is
98
+ * non-finite (`NaN`, `Infinity`, `-Infinity`), or if `region.width` or
99
+ * `region.height` is negative. Zero-extent regions are valid (they still
100
+ * query any overlapping node).
83
101
  */
84
102
  retrieve(region: AABB): T[];
85
103
  /**
86
- * Zero-allocation variant of {@link retrieve}.
104
+ * Reduced-allocation variant of {@link retrieve}.
87
105
  *
88
106
  * Clears `target` (sets `target.length = 0`), walks the tree using the same
89
107
  * iterative DFS + Set-based dedup as {@link retrieve}, then writes every
@@ -101,17 +119,20 @@ interface Quadtree<T extends AABB> {
101
119
  * @invariant Dedup semantics identical to {@link retrieve}: objects
102
120
  * spanning multiple quadrants appear exactly once.
103
121
  *
104
- * Allocation: in steady state this performs no per-call heap allocation.
105
- * The dedup `Set` and DFS stack are reused across calls (cleared, not
106
- * re-created), and results are written into the caller's `target` instead
107
- * of a fresh array. The first calls may grow the internal scratch; once
108
- * result sizes stabilise, allocation amortises to zero — the design goal
109
- * for per-frame broadphase loops issuing thousands of queries.
122
+ * Allocation: this avoids the fresh result array that {@link retrieve}
123
+ * allocates on every call — the internal DFS stack is reused across calls,
124
+ * and results are written into the caller's `target` instead of a new
125
+ * array. It does **not** allocate zero heap per call in practice: on V8,
126
+ * clearing the internal dedup `Set` replaces its backing table and
127
+ * `target.length = 0` drops the target array's backing store, so both are
128
+ * rebuilt on the next call, at a cost proportional to the result size.
129
+ * These are small, short-lived young-generation allocations, not the
130
+ * unbounded fresh-array allocation `retrieve()` makes, but they do not
131
+ * amortise away to literally zero.
110
132
  *
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.
133
+ * @throws {@link QuadtreeError} if `target` is not an array (checked
134
+ * before `target` is touched), or for the same `region` violations as
135
+ * {@link retrieve}. Zero-extent regions are valid.
115
136
  */
116
137
  retrieveInto(region: AABB, target: T[]): T[];
117
138
  /**
@@ -137,10 +158,17 @@ interface Quadtree<T extends AABB> {
137
158
  readonly disposed: boolean;
138
159
  }
139
160
  /**
140
- * Recoverable quadtree error — thrown by `createQuadtree` for invalid
141
- * construction options and by `insert()` for precondition violations
142
- * (e.g. an inserted object with non-finite coordinates or negative
143
- * `width` / `height`).
161
+ * Recoverable quadtree error — thrown by `createQuadtree` for a missing or
162
+ * non-object options argument, a missing or non-object `bounds`, or invalid
163
+ * construction options; by `insert()` for precondition violations (e.g. an
164
+ * inserted object with non-finite coordinates or negative `width` /
165
+ * `height`); by `retrieve()` / `retrieveInto()` for regions with non-finite
166
+ * fields or negative `width` / `height`; and by `retrieveInto()` for a
167
+ * `target` that is not an array.
168
+ *
169
+ * Every message starts with `aiquadtreejs: ` (e.g.
170
+ * `aiquadtreejs: maxObjects must be a positive integer`) and `name` is
171
+ * `"QuadtreeError"`. Match on the class, not the exact text.
144
172
  *
145
173
  * @public
146
174
  */
@@ -188,6 +216,11 @@ declare class QuadtreeDisposedError extends Error {
188
216
  * // Caller runs a precise hit test on `candidates`.
189
217
  * ```
190
218
  *
219
+ * @throws {@link QuadtreeError} if `opts` is not an object, if `opts.bounds`
220
+ * is not an object, if any bounds field is non-finite, if `bounds.width` or
221
+ * `bounds.height` is not positive, or if `maxObjects` / `maxLevels` is not
222
+ * a positive integer. Checked in that order, before the tree exists.
223
+ *
191
224
  * @public
192
225
  */
193
226
  declare function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T>;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- 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}}}export{A as QuadtreeDisposedError,d as QuadtreeError,I as createQuadtree};//# sourceMappingURL=index.js.map
1
+ var o=class extends Error{name="QuadtreeError"},T=class extends Error{name="QuadtreeDisposedError"};function A(e,t,i,r,u){return {x0:e,y0:t,x1:i,y1:r,level:u,objects:[],children:[]}}function b(e,t){return e+(t-e)/2}function j(e,t){let i=t.width===0?t.x>=e.x0&&t.x<e.x1:t.x<e.x1&&t.x+t.width>e.x0,r=t.height===0?t.y>=e.y0&&t.y<e.y1:t.y<e.y1&&t.y+t.height>e.y0;return i&&r}function q(e,t){let i=b(e.x0,e.x1),r=b(e.y0,e.y1),u=t.x<i,c=t.width===0?t.x>=i:t.x+t.width>i,d=t.y<r,h=t.height===0?t.y>=r:t.y+t.height>r,n=[];return d&&u&&n.push(0),d&&c&&n.push(1),h&&u&&n.push(2),h&&c&&n.push(3),n}function X(e){let{x0:t,y0:i,x1:r,y1:u}=e,c=b(t,r),d=b(i,u),h=e.level+1;e.children.push(A(t,i,c,d,h),A(c,i,r,d,h),A(t,d,c,u,h),A(c,d,r,u,h));for(let n of e.objects)for(let f of q(e,n)){let B=e.children[f];B!==void 0&&B.objects.push(n);}e.objects.length=0;}function Y(e){let t=b(e.x0,e.x1),i=b(e.y0,e.y1);return e.x0<t&&t<e.x1&&e.y0<i&&i<e.y1}function F(e,t,i,r){if(!(e.level===0&&!j(e,t))){if(e.children.length===4){for(let u of q(e,t)){let c=e.children[u];c!==void 0&&F(c,t,i,r);}return}e.objects.push(t),e.objects.length>i&&e.level<r&&Y(e)&&X(e);}}function O(e){e.objects.length=0;for(let t of e.children)O(t);e.children.length=0;}function k(e){if(e===null||typeof e!="object")throw new o("aiquadtreejs: options must be an object with bounds");let t=e.bounds;if(t===null||typeof t!="object")throw new o("aiquadtreejs: bounds must be an object with finite numeric x, y, width and height");let i=e.maxObjects??10,r=e.maxLevels??4,u=t.x,c=t.y,d=t.width,h=t.height;if(!Number.isFinite(u)||!Number.isFinite(c)||!Number.isFinite(d)||!Number.isFinite(h))throw new o("aiquadtreejs: bounds must contain finite numbers");if(d<=0)throw new o("aiquadtreejs: bounds.width must be > 0");if(h<=0)throw new o("aiquadtreejs: bounds.height must be > 0");if(!Number.isInteger(i)||i<=0)throw new o("aiquadtreejs: maxObjects must be a positive integer");if(!Number.isInteger(r)||r<=0)throw new o("aiquadtreejs: maxLevels must be a positive integer");let n={root:A(u,c,u+d,c+h,0),maxObjects:i,maxLevels:r,disposed:false};function f(){if(n.disposed)throw new T("aiquadtreejs: quadtree has been disposed")}function B(s){if(f(),!s||!Number.isFinite(s.x)||!Number.isFinite(s.y)||!Number.isFinite(s.width)||!Number.isFinite(s.height))throw new o("aiquadtreejs: inserted object must be defined with finite numeric x, y, width and height");if(s.width<0)throw new o("aiquadtreejs: inserted object width must be >= 0");if(s.height<0)throw new o("aiquadtreejs: inserted object height must be >= 0");F(n.root,s,n.maxObjects,n.maxLevels);}let l=new Set,a=[],w={x:0,y:0,width:0,height:0};function N(s){let m=s?.x,y=s?.y,x=s?.width,v=s?.height;if(!Number.isFinite(m)||!Number.isFinite(y)||!Number.isFinite(x)||!Number.isFinite(v))throw new o("aiquadtreejs: retrieve region must have finite numeric x, y, width and height");if(x<0)throw new o("aiquadtreejs: retrieve region width must be >= 0");if(v<0)throw new o("aiquadtreejs: retrieve region height must be >= 0");for(w.x=m,w.y=y,w.width=x,w.height=v,l.clear(),a.length=0,a.push(n.root);a.length>0;){let g=a.pop();if(g!==void 0&&j(g,w)){for(let p of g.objects)l.add(p);for(let p of g.children)a.push(p);}}return l}function S(s){return f(),Array.from(N(s))}function I(s,m){if(f(),!Array.isArray(m))throw new o("aiquadtreejs: retrieveInto target must be an array");let y=N(s);m.length=0;for(let x of y)m.push(x);return m}function L(){f(),O(n.root),l.clear(),a.length=0;}function Q(){n.disposed||(n.disposed=true,n.root.objects.length=0,n.root.children.length=0,l.clear(),a.length=0);}return {insert:B,retrieve:S,retrieveInto:I,clear:L,dispose:Q,get disposed(){return n.disposed}}}export{T as QuadtreeDisposedError,o as QuadtreeError,k as createQuadtree};//# sourceMappingURL=index.js.map
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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.js","sourcesContent":["// aiquadtreejs — 2D quadtree for per-frame rebuild collision broadphase.\n//\n// Plain-object nodes, iterative-DFS retrieve, Set-based dedup, idempotent\n// dispose, destructurable methods (no `this`). Version: see package.json.\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 * Non-finite coordinates or negative dimensions throw QuadtreeError with\n * 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"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["QuadtreeError","QuadtreeDisposedError","makeNode","x0","y0","x1","y1","level","mid","a","b","nodeOverlaps","node","box","inX","inY","quadrantIndices","obj","midX","midY","inLeft","inRight","inTop","inBottom","result","subdivide","lvl","i","child","isSplitRepresentable","insertNode","maxObjects","maxLevels","clearNode","createQuadtree","opts","bounds","bx","by","bw","bh","state","ck","insert","scratchSet","scratchStack","scratchRegion","retrieveSet","region","rx","ry","rw","rh","retrieve","retrieveInto","target","set","v","clear","dispose"],"mappings":"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","file":"index.js","sourcesContent":["// aiquadtreejs — 2D quadtree for per-frame rebuild collision broadphase.\n//\n// Plain-object nodes, iterative-DFS retrieve, Set-based dedup, idempotent\n// dispose, destructurable methods (no `this`). Version: see package.json.\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 * Each field is read once, so accessor-backed bounds (e.g. PixiJS v8\n * `Bounds`) work. The root's exclusive edges are computed once as\n * `x + width` and `y + height`, and every right/bottom child shares its\n * parent's exact edge.\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 * **Precision-bound depth:** subdivision also stops once a node's\n * midpoint is no longer representable in floating point (its width or\n * height has fallen below the ulp of its coordinate) — typically around\n * depth 45-52 for typical scene-sized bounds, well below the 4^L node-count\n * concern above. Nodes past this depth become terminal leaves regardless\n * of `maxLevels`, so a very high `maxLevels` cannot make a dense point\n * cluster vanish from `retrieve()`.\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 `obj` is `null` / `undefined`, if any of\n * `x`, `y`, `width`, or `height` is non-finite (`NaN`, `Infinity`,\n * `-Infinity`), or if `width` or `height` is negative. Zero-extent\n * 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 * Each `region` field is read once and the walk uses the validated values,\n * so an accessor-backed region behaves like a plain object.\n *\n * @throws {@link QuadtreeError} if `region` is `null` / `undefined`, if any\n * of `region.x`, `region.y`, `region.width`, or `region.height` is\n * non-finite (`NaN`, `Infinity`, `-Infinity`), or if `region.width` or\n * `region.height` is negative. Zero-extent regions are valid (they still\n * query any overlapping node).\n */\n retrieve(region: AABB): T[];\n\n /**\n * Reduced-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: this avoids the fresh result array that {@link retrieve}\n * allocates on every call — the internal DFS stack is reused across calls,\n * and results are written into the caller's `target` instead of a new\n * array. It does **not** allocate zero heap per call in practice: on V8,\n * clearing the internal dedup `Set` replaces its backing table and\n * `target.length = 0` drops the target array's backing store, so both are\n * rebuilt on the next call, at a cost proportional to the result size.\n * These are small, short-lived young-generation allocations, not the\n * unbounded fresh-array allocation `retrieve()` makes, but they do not\n * amortise away to literally zero.\n *\n * @throws {@link QuadtreeError} if `target` is not an array (checked\n * before `target` is touched), or for the same `region` violations as\n * {@link retrieve}. 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 a missing or\n * non-object options argument, a missing or non-object `bounds`, or invalid\n * construction options; by `insert()` for precondition violations (e.g. an\n * inserted object with non-finite coordinates or negative `width` /\n * `height`); by `retrieve()` / `retrieveInto()` for regions with non-finite\n * fields or negative `width` / `height`; and by `retrieveInto()` for a\n * `target` that is not an array.\n *\n * Every message starts with `aiquadtreejs: ` (e.g.\n * `aiquadtreejs: maxObjects must be a positive integer`) and `name` is\n * `\"QuadtreeError\"`. Match on the class, not the exact text.\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\n// A node covers the right-open rectangle [x0, x1) x [y0, y1). Edges are\n// stored, never re-derived from a width: the root takes `x1 = x + width`\n// once from the validated bounds, and each child takes its parent's exact\n// edges and midpoint. Recomputing a right child's edge as `(x + w) + w` can\n// land an ulp short of the parent's `x + width`, and an object routed into\n// that sliver would then be missed by `retrieve()`.\ninterface Node<T extends AABB> {\n x0: number;\n y0: number;\n x1: number;\n y1: number;\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 makeNode<T extends AABB>(\n x0: number,\n y0: number,\n x1: number,\n y1: number,\n level: number,\n): Node<T> {\n return { x0, y0, x1, y1, level, objects: [], children: [] };\n}\n\n// Split line of [a, b). subdivide, quadrantIndices and isSplitRepresentable\n// all use this one formula, so the routing midpoint is bit-for-bit the edge\n// the children were built with.\nfunction mid(a: number, b: number): number {\n return a + (b - a) / 2;\n}\n\n// Node/box overlap test used at the insert root gate and by retrieve's\n// node walk (with the query region as `box`).\n//\n// Right-open semantics for positive-extent dimensions:\n// overlaps iff box.x < node.x1 AND box.x + box.width > node.x0\n//\n// Zero-extent exception for the minimum edge: a zero-size point sitting exactly\n// on node.x0 or node.y0 satisfies neither side of the strict-inequality test,\n// so it would be silently dropped (or, as a query, match no node). Instead, per axis:\n// - zero-extent: overlaps iff coordinate is within [x0, x1) —\n// inclusive minimum, exclusive maximum (right-open, matching the box contract)\n// - positive-extent: keep the strict right-open overlap\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 nodeOverlaps<T extends AABB>(node: Node<T>, box: AABB): boolean {\n const inX =\n box.width === 0\n ? box.x >= node.x0 && box.x < node.x1\n : box.x < node.x1 && box.x + box.width > node.x0;\n const inY =\n box.height === 0\n ? box.y >= node.y0 && box.y < node.y1\n : box.y < node.y1 && box.y + box.height > node.y0;\n return inX && inY;\n}\n\nfunction quadrantIndices<T extends AABB>(node: Node<T>, obj: AABB): number[] {\n const midX = mid(node.x0, node.x1);\n const midY = mid(node.y0, node.y1);\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 { x0, y0, x1, y1 } = node;\n const midX = mid(x0, x1);\n const midY = mid(y0, y1);\n const lvl = node.level + 1;\n // TL, TR, BL, BR. Right/bottom children take the parent's exact x1/y1.\n node.children.push(\n makeNode(x0, y0, midX, midY, lvl),\n makeNode(midX, y0, x1, midY, lvl),\n makeNode(x0, midY, midX, y1, lvl),\n makeNode(midX, midY, x1, y1, lvl),\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\n// True while the node still has a representable midpoint on both axes,\n// i.e. `mid(x0, x1)` actually lands strictly between `x0` and `x1` in\n// floating point (and likewise for y). Once a node's extent falls below the\n// ulp of its coordinate, the computed midpoint rounds onto `x0` or `x1`, so\n// `subdivide()` would create children that are not smaller than their\n// parent — an infinite-seeming split that silently stops matching queries\n// instead of erroring. Below this point further subdivision is skipped and\n// the node stays a terminal leaf even if `node.level < maxLevels`.\nfunction isSplitRepresentable<T extends AABB>(node: Node<T>): boolean {\n const midX = mid(node.x0, node.x1);\n const midY = mid(node.y0, node.y1);\n return node.x0 < midX && midX < node.x1 && node.y0 < midY && midY < node.y1;\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.\n // nodeOverlaps accepts zero-size points/lines sitting exactly on the\n // minimum (left/top) edge with inclusive semantics, whilst positive-size\n // objects retain right-open exclusion on the maximum edge.\n if (node.level === 0 && !nodeOverlaps(node, 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 && isSplitRepresentable(node)) {\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 * @throws {@link QuadtreeError} if `opts` is not an object, if `opts.bounds`\n * is not an object, if any bounds field is non-finite, if `bounds.width` or\n * `bounds.height` is not positive, or if `maxObjects` / `maxLevels` is not\n * a positive integer. Checked in that order, before the tree exists.\n *\n * @public\n */\nexport function createQuadtree<T extends AABB>(opts: QuadtreeOptions): Quadtree<T> {\n if (opts === null || typeof opts !== \"object\") {\n throw new QuadtreeError(\"aiquadtreejs: options must be an object with bounds\");\n }\n const bounds = opts.bounds;\n if (bounds === null || typeof bounds !== \"object\") {\n throw new QuadtreeError(\n \"aiquadtreejs: bounds must be an object with finite numeric x, y, width and height\",\n );\n }\n const maxObjects = opts.maxObjects ?? 10;\n const maxLevels = opts.maxLevels ?? 4;\n // Read each field once: accessor-backed bounds (e.g. PixiJS v8 `Bounds`)\n // would be lost by an object spread, and a second read could disagree\n // with the validated value.\n const bx = bounds.x;\n const by = bounds.y;\n const bw = bounds.width;\n const bh = bounds.height;\n\n if (\n !Number.isFinite(bx) ||\n !Number.isFinite(by) ||\n !Number.isFinite(bw) ||\n !Number.isFinite(bh)\n ) {\n throw new QuadtreeError(\"aiquadtreejs: bounds must contain finite numbers\");\n }\n if (bw <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: bounds.width must be > 0\");\n }\n if (bh <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: bounds.height must be > 0\");\n }\n if (!Number.isInteger(maxObjects) || maxObjects <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: maxObjects must be a positive integer\");\n }\n if (!Number.isInteger(maxLevels) || maxLevels <= 0) {\n throw new QuadtreeError(\"aiquadtreejs: maxLevels must be a positive integer\");\n }\n\n const state: State<T> = {\n root: makeNode(bx, by, bx + bw, by + bh, 0),\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 \"aiquadtreejs: inserted object must be defined with finite numeric x, y, width and height\",\n );\n }\n if (obj.width < 0) {\n throw new QuadtreeError(\"aiquadtreejs: inserted object width must be >= 0\");\n }\n if (obj.height < 0) {\n throw new QuadtreeError(\"aiquadtreejs: inserted object height must be >= 0\");\n }\n insertNode(state.root, obj, state.maxObjects, state.maxLevels);\n }\n\n // Reusable scratch for retrieveSet, hoisted to avoid a fresh DFS stack per\n // call. Safe because the returned Set never escapes the module: retrieve\n // copies it out via Array.from and retrieveInto via a push loop, both\n // synchronously and fully before any subsequent call. Note that\n // scratchSet.clear() still rebuilds the Set's backing table on V8, so this\n // does not make retrieveInto literally allocation-free — see its JSDoc.\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 // Validate `region` and collect the deduplicated candidates into scratchSet.\n //\n // Each region field is read exactly once, validated, and only then copied\n // into scratchRegion, so an accessor-backed region (e.g. PixiJS v8 `Bounds`)\n // walks with the values that passed validation, and a getter that calls\n // back into this tree completes during the four reads, before this call\n // touches the shared scratch. Optional chaining turns a nullish region into\n // the same QuadtreeError as a non-finite field.\n function retrieveSet(region: AABB): Set<T> {\n const rx = region?.x;\n const ry = region?.y;\n const rw = region?.width;\n const rh = region?.height;\n if (\n !Number.isFinite(rx) ||\n !Number.isFinite(ry) ||\n !Number.isFinite(rw) ||\n !Number.isFinite(rh)\n ) {\n throw new QuadtreeError(\n \"aiquadtreejs: retrieve region must have finite numeric x, y, width and height\",\n );\n }\n if (rw < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region width must be >= 0\");\n }\n if (rh < 0) {\n throw new QuadtreeError(\"aiquadtreejs: retrieve region height must be >= 0\");\n }\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 (!nodeOverlaps(node, 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 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 if (!Array.isArray(target)) {\n throw new QuadtreeError(\"aiquadtreejs: retrieveInto target must be an array\");\n }\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/llms-full.txt CHANGED
@@ -15,7 +15,7 @@ The short index lives at `llms.txt` (see https://llmstxt.org/).
15
15
 
16
16
  Tiny 2D quadtree for per-frame rebuild collision broadphase. Insert AABBs, retrieve candidates, then run precise collision checks yourself.
17
17
 
18
- > **Status: 0.5.9 - stable 1.0-track surface.** The root entry is the public API.
18
+ > **Status: 0.6.0 - stable 1.0-track surface.** The root entry is the public API.
19
19
 
20
20
  ## Install
21
21
 
@@ -56,25 +56,28 @@ const candidates = tree.retrieve({ x: 80, y: 80, width: 120, height: 120 });
56
56
  - `createQuadtree<T extends AABB>({ bounds, maxObjects?, maxLevels? })` creates a tree.
57
57
  - `insert(obj)` stores an object reference in overlapping nodes.
58
58
  - `retrieve(region)` returns a deduplicated broadphase candidate array.
59
- - `retrieveInto(region, target)` reuses a caller-owned result array.
59
+ - `retrieveInto(region, target)` reuses a caller-owned result array; `target` must be an array.
60
60
  - `clear()` empties the tree for the next frame and clears scratch buffers.
61
61
  - `dispose()` is idempotent permanent teardown.
62
- - Errors: `QuadtreeError`, `QuadtreeDisposedError`.
62
+ - Errors: `QuadtreeError`, `QuadtreeDisposedError`. Every `QuadtreeError` message starts with `aiquadtreejs: `; match on the class, not the exact text.
63
63
 
64
64
  ## Model
65
65
 
66
66
  - Coordinates are right-open: `{ x, y, width, height }` covers `[x, x + width)` and `[y, y + height)`.
67
- - This is a broadphase only. Returned candidates may not actually overlap the query region.
67
+ - This is a broadphase only. Returned candidates may not actually overlap the query region, but every inserted object that overlaps the region inside `bounds` is returned.
68
68
  - Expected usage is per-frame rebuild: `clear()`, insert active bodies, query.
69
69
  - Objects spanning quadrant boundaries can be stored in multiple child nodes; results are deduplicated.
70
+ - Nodes store their edges. Right/bottom children share the parent's exact `x + width` / `y + height`, so fractional bounds such as `x: -0.3, width: 2.4` lose nothing at the edge.
70
71
  - `maxLevels` has no hard cap. Very high values plus spanning objects can create huge node counts.
72
+ - Subdivision also stops once a node's midpoint is no longer representable in floating point (typically around depth 45-52), so a very high `maxLevels` cannot make a dense point cluster silently vanish from `retrieve()`.
71
73
 
72
74
  ## Sharp Edges
73
75
 
74
76
  - Zero-size points (width = 0, height = 0) follow right-open `[x, x+width)` semantics: a point on the minimum `x/y` boundary is **inclusive** and is inserted/retrieved correctly; a point at the exclusive maximum edge is outside the root and is ignored. (Was a bug before 0.5.8; fixed.)
75
77
  - Fully outside objects are ignored by retrieval.
76
- - Negative width/height and non-finite coordinates throw.
78
+ - `QuadtreeError` is thrown for a missing options object or `bounds`, negative width/height, non-finite coordinates, and a `retrieveInto()` target that is not an array.
77
79
  - `retrieveInto()` clears the target array before writing results.
80
+ - Inserted objects are stored by reference and re-read when their node subdivides. Do not move an inserted object until the next `clear()`; rebuild the tree instead.
78
81
  - After `dispose()`, all methods except `dispose()` throw `QuadtreeDisposedError`.
79
82
 
80
83
  ## AI Context
@@ -97,7 +100,25 @@ MIT
97
100
 
98
101
  All notable changes to aiquadtreejs are summarized here.
99
102
 
100
- ## [Unreleased]
103
+ ## [0.6.0] - 2026-09-29
104
+
105
+ ### Breaking
106
+
107
+ - `retrieveInto()`: a `target` that is not an array now throws `QuadtreeError` (`aiquadtreejs: retrieveInto target must be an array`) before the tree is walked, instead of a bare `TypeError` from `target.length = 0` / `target.push` or, for a non-array object when nothing matched, silently returning that object. Migration: pass an array (an `Array` subclass is fine) as `target`, and catch `QuadtreeError` where you caught `TypeError`.
108
+
109
+ ### Changes
110
+
111
+ - Changed: `createQuadtree()` with a missing or non-object options argument throws `QuadtreeError` (`aiquadtreejs: options must be an object with bounds`), and with a missing or non-object `bounds` throws `QuadtreeError` (`aiquadtreejs: bounds must be an object with finite numeric x, y, width and height`), instead of a bare `TypeError` from destructuring.
112
+ - Changed: every `QuadtreeError` message now starts with `aiquadtreejs: `; the `createQuadtree()` and `insert()` messages gained the prefix the `retrieve()` messages already had (e.g. `aiquadtreejs: maxObjects must be a positive integer`). Match on the class plus a regex, not the exact text.
113
+ - Fixed: `createQuadtree` read `bounds.x/y/width/height` into locals instead of storing `{ ...bounds }`, so accessor-backed bounds (e.g. PixiJS v8 `Bounds`, returned by `getBounds()`) are no longer dropped — previously every insert was silently lost and `retrieve()` always returned `[]`.
114
+ - Fixed: `retrieve()`/`retrieveInto()` now match zero-extent regions that land exactly on a node's minimum edge (the root's `x`/`y` or any subdivision midline), instead of missing them depending on whether unrelated inserts had already subdivided the tree.
115
+ - Fixed: subdivision now stops once a node's midpoint is no longer representable in floating point, instead of continuing until `maxLevels`; previously a dense point cluster near that depth would silently stop matching `retrieve()` region queries.
116
+ - Fixed: `subdivide()` built right/bottom children as `x + w` with extent `w`, so their outer edge was `(x + w) + w`; with fractional bounds such as `x: -0.3, width: 2.4` that is one ulp short of the parent's `x + width`, and an object in that sliver was missed by `retrieve()`. Nodes now store min/max edges, and each child takes its parent's exact edges and midpoint.
117
+ - Fixed: `retrieve()`/`retrieveInto()` read each region field once and walk with the validated values; a region getter used to be read twice, so a value that changed between the reads passed validation and then silently matched nothing.
118
+ - Fixed: `package.json#exports` nests `types` under each of `import`/`require` (with `./dist/index.d.cts` for `require`), fixing TS1479 for CommonJS consumers under `module: node16`/`nodenext`; `verify-exports.mjs` walks nested condition objects.
119
+ - Fixed: `pnpm typecheck` now type-checks `test/` (the test tsconfig inherited `exclude: ["test"]`), so the suite's compile-time assertions are enforced.
120
+ - Docs: STABILITY.md's Behavioral Contract now states the construction validation order, shared node edges, retrieval completeness (pinned by new brute-force oracle properties over float bounds with negative origins), read-once regions, the `retrieveInto()` target check, the `aiquadtreejs: ` message shape and the re-entrancy clause (no events, callbacks or mailbox; getters only).
121
+ - Docs: README/README_ZHTW list the misuse errors and the completeness guarantee, and warn that inserted objects are re-read when their node subdivides; JSDoc for `QuadtreeError`, `createQuadtree()`, `QuadtreeOptions.bounds`, `insert()`, `retrieve()` and `retrieveInto()` matches the 0.6.0 contract.
101
122
 
102
123
  ## [0.5.9] - 2026-06-29
103
124
 
@@ -133,16 +154,22 @@ All notable changes to aiquadtreejs are summarized here.
133
154
  | `createQuadtree()` | Stable | Root factory. |
134
155
  | `AABB`, `QuadtreeOptions`, `Quadtree<T>` | Stable | Public types. |
135
156
  | `insert`, `retrieve`, `retrieveInto`, `clear`, `dispose` | Stable | Main methods. |
157
+ | `disposed` (read-only getter) | Stable | `true` once `dispose()` has been called. |
136
158
  | Error classes | Stable | `QuadtreeError`, `QuadtreeDisposedError`. |
137
159
 
138
160
  ## Behavioral Contract
139
161
 
140
162
  - Bounds use right-open coordinates.
141
- - Inserted object references are not cloned.
142
- - Retrieval is broadphase and deduplicated.
143
- - `retrieveInto()` preserves target array identity and clears it first.
163
+ - `createQuadtree()` validates before the tree exists, in this order: the options argument is an object, `bounds` is an object, the four `bounds` fields are finite (each read once), `width > 0`, `height > 0`, `maxObjects` is a positive integer, `maxLevels` is a positive integer.
164
+ - Nodes store their min/max edges. The root's exclusive edges are `x + width` and `y + height`, computed once; each child takes its parent's exact edges and midpoint, so no edge is ever re-derived from a width.
165
+ - Inserted object references are not cloned. They are re-read whenever their node subdivides, so an object moved before the next `clear()` is routed by its new coordinates.
166
+ - Retrieval is broadphase and deduplicated, and it is complete: every inserted object that shares a point with the query region inside `bounds` is returned. On each axis a zero extent is the point `{x}` and a positive extent is the right-open span `[x, x + width)`. Brute-force property tests pin this.
167
+ - `retrieve()` and `retrieveInto()` read each region field once and walk with the validated values.
168
+ - `retrieveInto()` checks that `target` is an array before touching it, preserves its identity, and clears it first.
144
169
  - `clear()` drains node contents and scratch references.
145
- - `dispose()` is idempotent and permanent.
170
+ - `dispose()` is idempotent and permanent. After it, every other method throws `QuadtreeDisposedError` before validating its arguments.
171
+ - Misuse errors are `QuadtreeError`; every message starts with `aiquadtreejs: `, and `error.name` equals the class name.
172
+ - Re-entrancy: the tree dispatches no events, runs no callbacks and has no mailbox. The only user code it runs is property getters on `bounds`, query regions and inserted objects. `bounds` and region fields are read once before any state changes, so a region getter that calls back into the tree completes before the walk starts. Getters on inserted objects are re-read during `insert()` and later subdivisions and must not call back into the same tree. Separate trees are independent.
146
173
 
147
174
  ## Zero-Size Point Boundary Semantics
148
175
 
@@ -180,6 +207,8 @@ Run `pnpm lint` before PRs. If docs change, regenerate `llms-full.txt`.
180
207
  - Add tests for root edges, quadrant boundaries, zero-size objects, `retrieveInto()`, `clear()`, and dispose.
181
208
  - Do not turn broadphase results into precise collision promises.
182
209
  - Keep allocation behavior visible in docs and tests.
210
+ - Store node edges and hand them to children; never re-derive an edge from a width. The brute-force completeness properties (`prop5`, `prop6`) must keep passing.
211
+ - Every `QuadtreeError` message starts with `aiquadtreejs: `; tests match the class plus an anchored regex.
183
212
 
184
213
  ## License
185
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiquadtreejs",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Tiny 2D quadtree for per-frame rebuild collision broadphase. Insert AABBs, retrieve candidates, clear. Caller does precise hit-testing. Designed for PixiJS games with 500–10,000 active entities.",
5
5
  "keywords": [
6
6
  "quadtree",
@@ -31,9 +31,14 @@
31
31
  "types": "./dist/index.d.ts",
32
32
  "exports": {
33
33
  ".": {
34
- "types": "./dist/index.d.ts",
35
- "import": "./dist/index.js",
36
- "require": "./dist/index.cjs"
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
37
42
  }
38
43
  },
39
44
  "files": [