ecspresso 0.16.1 → 0.16.2
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/dist/index.js +2 -2
- package/dist/index.js.map +4 -4
- package/dist/plugins/ai/detection.js +2 -2
- package/dist/plugins/ai/detection.js.map +3 -3
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +7 -6
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +6 -5
- package/dist/plugins/physics/collision3D.js +2 -2
- package/dist/plugins/physics/collision3D.js.map +8 -7
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +6 -5
- package/dist/plugins/physics/physics3D.js +2 -2
- package/dist/plugins/physics/physics3D.js.map +6 -5
- package/dist/plugins/spatial/spatial-index.js +2 -2
- package/dist/plugins/spatial/spatial-index.js.map +4 -4
- package/dist/plugins/spatial/spatial-index3D.js +2 -2
- package/dist/plugins/spatial/spatial-index3D.js.map +4 -4
- package/dist/query-cache.d.ts +7 -7
- package/dist/utils/layer-bit-registry.d.ts +17 -0
- package/dist/utils/narrowphase.d.ts +31 -6
- package/dist/utils/narrowphase3D.d.ts +10 -0
- package/dist/utils/spatial-hash.d.ts +49 -14
- package/dist/utils/spatial-hash3D.d.ts +49 -14
- package/package.json +1 -1
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/spatial-index.ts", "../src/utils/spatial-hash.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from '../physics/collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n} from '../../utils/spatial-hash';\n\n//
|
|
6
|
-
"/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number,
|
|
5
|
+
"/**\n * Spatial Index Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries. Replaces O(n²) brute-force with O(n·d) where\n * d = local density.\n *\n * Standalone usage: queryRect / queryRadius for proximity queries.\n * Automatic acceleration: collision and physics2D plugins detect the\n * spatialIndex resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { TransformComponentTypes } from './transform';\nimport type { CollisionComponentTypes } from '../physics/collision';\nimport {\n\ttype SpatialEntry,\n\ttype SpatialHashGrid,\n\ttype SpatialIndex,\n\tcreateGrid,\n\tclearGrid,\n\tinsertEntity,\n\tgridQueryRect,\n\tgridQueryRadius,\n\tgetLiveEntry,\n} from '../../utils/spatial-hash';\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndexResourceTypes {\n\tspatialIndex: SpatialIndex;\n}\n\nfunction createSpatialIndexResource(grid: SpatialHashGrid): SpatialIndex {\n\treturn {\n\t\tgrid,\n\t\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[] {\n\t\t\tconst out: number[] = [];\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, out);\n\t\t\treturn out;\n\t\t},\n\t\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: number[], minId?: number): void {\n\t\t\tgridQueryRect(grid, minX, minY, maxX, maxY, result, minId);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, radius: number): number[] {\n\t\t\tconst out: number[] = [];\n\t\t\tgridQueryRadius(grid, cx, cy, radius, out);\n\t\t\treturn out;\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, radius: number, result: number[]): void {\n\t\t\tgridQueryRadius(grid, cx, cy, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry | undefined {\n\t\t\treturn getLiveEntry(grid, entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndexComponentTypes =\n\tTransformComponentTypes & Pick<CollisionComponentTypes<string>, 'aabbCollider' | 'circleCollider'>;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndexPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndexLabel = `spatial-index-rebuild-${SpatialIndexPhase}`;\n\nexport interface SpatialIndexPluginOptions<G extends string = 'spatialIndex'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndexPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates collision detection.\n * When installed alongside the collision or physics2D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransformPlugin())\n * .withPlugin(createCollisionPlugin({ layers }))\n * .withPlugin(createSpatialIndexPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex');\n * const nearby = si.queryRadius(playerX, playerY, 200);\n * ```\n */\nexport function createSpatialIndexPlugin<G extends string = 'spatialIndex'>(\n\toptions?: SpatialIndexPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid(cellSize);\n\tconst resource = createSpatialIndexResource(grid);\n\n\treturn definePlugin('spatialIndex')\n\t\t.withComponentTypes<SpatialIndexComponentTypes>()\n\t\t.withResourceTypes<SpatialIndexResourceTypes>()\n\t\t.withLabels<SpatialIndexLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform' : 'worldTransform';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabbCollider');\n\t\t\t\t\t\t\tconst circle = ecs.getComponent(entity.id, 'circleCollider');\n\n\t\t\t\t\t\t\t// Only insert entities that have a collider\n\t\t\t\t\t\t\tif (!aabb && !circle) continue;\n\n\t\t\t\t\t\t\tlet x = transform.x;\n\t\t\t\t\t\t\tlet y = transform.y;\n\t\t\t\t\t\t\tlet halfW = 0;\n\t\t\t\t\t\t\tlet halfH = 0;\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tx += aabb.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += aabb.offsetY ?? 0;\n\t\t\t\t\t\t\t\thalfW = aabb.width / 2;\n\t\t\t\t\t\t\t\thalfH = aabb.height / 2;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (circle) {\n\t\t\t\t\t\t\t\tx += circle.offsetX ?? 0;\n\t\t\t\t\t\t\t\ty += circle.offsetY ?? 0;\n\t\t\t\t\t\t\t\t// Circle: use radius as half-extent in both dimensions\n\t\t\t\t\t\t\t\thalfW = Math.max(halfW, circle.radius);\n\t\t\t\t\t\t\t\thalfH = Math.max(halfH, circle.radius);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
|
|
6
|
+
"/**\n * Spatial Hash Grid\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structure ====================\n\nexport interface SpatialEntry {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\thalfW: number;\n\thalfH: number;\n\t/** Generation stamp used by query functions to dedup multi-cell hits without a Set. Internal. */\n\t_lastSeenGen: number;\n\t/** Rebuild generation when this entry was last inserted. Internal. */\n\t_aliveGen: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, SpatialEntry[]>;\n\t/**\n\t * Dense, indexed by entityId. Holes are `undefined`. Entries from previous\n\t * rebuilds remain in place for in-place reuse (zero allocation in steady\n\t * state); liveness is determined by `entry._aliveGen === grid._aliveGen`.\n\t * Internal — read live entries via `getEntry` / `liveEntryCount` helpers.\n\t *\n\t * High-water-mark grows with max entityId ever inserted; despawned ids\n\t * leave their slot occupied by a stale entry. Acceptable when the entity\n\t * manager recycles ids or peak count is bounded.\n\t */\n\tentries: (SpatialEntry | undefined)[];\n\t/** Monotonic counter bumped by each `clearGrid` call. Internal. */\n\t_aliveGen: number;\n\t/** Monotonic counter bumped on each query; entries record their last-seen gen for O(1) dedup. Internal. */\n\t_queryGen: number;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate pair to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell(cx: number, cy: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663);\n}\n\n/**\n * Create a new empty spatial hash grid.\n */\nexport function createGrid(cellSize: number): SpatialHashGrid {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: [],\n\t\t_aliveGen: 0,\n\t\t_queryGen: 0,\n\t};\n}\n\n/**\n * Prepare the grid for a rebuild.\n *\n * Bumps the alive-generation counter so entries inserted prior to this call\n * are implicitly stale (any access via `getEntry`/`liveEntryCount` filters by\n * the current gen). Existing `SpatialEntry` objects remain in the `entries`\n * array for in-place reuse by the next `insertEntity`, so steady-state\n * rebuilds allocate zero entries.\n *\n * Cell buckets are cleared in place — keys are retained so subsequent\n * inserts hit the existing array rather than allocating a fresh one.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid._aliveGen++;\n\n\tfor (const bucket of grid.cells.values()) {\n\t\tbucket.length = 0;\n\t}\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity(\n\tgrid: SpatialHashGrid,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\thalfW: number,\n\thalfH: number,\n): void {\n\tconst gen = grid._aliveGen;\n\tconst existing = grid.entries[entityId];\n\tlet entry: SpatialEntry;\n\tif (existing) {\n\t\texisting.x = x;\n\t\texisting.y = y;\n\t\texisting.halfW = halfW;\n\t\texisting.halfH = halfH;\n\t\texisting._aliveGen = gen;\n\t\tentry = existing;\n\t} else {\n\t\tentry = { entityId, x, y, halfW, halfH, _lastSeenGen: 0, _aliveGen: gen };\n\t\tgrid.entries[entityId] = entry;\n\t}\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst key = hashCell(cx, cy);\n\t\t\tconst bucket = grid.cells.get(key);\n\t\t\tif (bucket) {\n\t\t\t\tbucket.push(entry);\n\t\t\t} else {\n\t\t\t\tgrid.cells.set(key, [entry]);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given rectangle.\n *\n * Appends to `result` (caller clears/truncates first if reusing). Multi-cell\n * entries are deduplicated via a per-grid generation stamp on each\n * `SpatialEntry`.\n *\n * When `minId` is provided, only entries with `entityId > minId` are added —\n * used for symmetric broadphase pair generation.\n */\nexport function gridQueryRect(\n\tgrid: SpatialHashGrid,\n\tminX: number,\n\tminY: number,\n\tmaxX: number,\n\tmaxY: number,\n\tresult: number[],\n\tminId: number = -1,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\n\tconst gen = ++grid._queryGen;\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(cx, cy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (entry.entityId <= minId || entry._lastSeenGen === gen) continue;\n\t\t\t\tentry._lastSeenGen = gen;\n\t\t\t\tresult.push(entry.entityId);\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs within a circle. AABB-to-point distance filter against\n * the cells overlapping the circle's bounding rect. Appends to `result`.\n */\nexport function gridQueryRadius(\n\tgrid: SpatialHashGrid,\n\tcx: number,\n\tcy: number,\n\tradius: number,\n\tresult: number[],\n): void {\n\tconst rSq = radius * radius;\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((cx - radius) * inv);\n\tconst maxCX = Math.floor((cx + radius) * inv);\n\tconst minCY = Math.floor((cy - radius) * inv);\n\tconst maxCY = Math.floor((cy + radius) * inv);\n\n\tconst gen = ++grid._queryGen;\n\n\tfor (let icx = minCX; icx <= maxCX; icx++) {\n\t\tfor (let icy = minCY; icy <= maxCY; icy++) {\n\t\t\tconst bucket = grid.cells.get(hashCell(icx, icy));\n\t\t\tif (!bucket) continue;\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (entry._lastSeenGen === gen) continue;\n\t\t\t\tentry._lastSeenGen = gen;\n\n\t\t\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\t\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\t\t\tconst dx = cx - closestX;\n\t\t\t\tconst dy = cy - closestY;\n\n\t\t\t\tif (dx * dx + dy * dy <= rSq) {\n\t\t\t\t\tresult.push(entry.entityId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Get the current-generation entry for an entityId, or `undefined` if the\n * entity isn't in the index for this rebuild. Stale entries from previous\n * rebuilds remain in `entries` for in-place reuse but are filtered here.\n */\nexport function getLiveEntry(grid: SpatialHashGrid, entityId: number): SpatialEntry | undefined {\n\tconst entry = grid.entries[entityId];\n\tif (!entry || entry._aliveGen !== grid._aliveGen) return undefined;\n\treturn entry;\n}\n\n/**\n * Count entries inserted in the current rebuild generation. Linear scan —\n * intended for tests and diagnostics, not hot paths.\n */\nexport function liveEntryCount(grid: SpatialHashGrid): number {\n\tconst gen = grid._aliveGen;\n\tlet n = 0;\n\tfor (const entry of grid.entries) {\n\t\tif (entry && entry._aliveGen === gen) n++;\n\t}\n\treturn n;\n}\n\n// ==================== Resource API ====================\n\n// TODO: Move SpatialIndex interface to src/plugins/spatial/spatial-index.ts.\n// It's a resource API concern, not a data structure concern. This file should\n// only contain the grid primitives (SpatialEntry, SpatialHashGrid, and the\n// pure functions that operate on them).\nexport interface SpatialIndex {\n\treadonly grid: SpatialHashGrid;\n\tqueryRect(minX: number, minY: number, maxX: number, maxY: number): number[];\n\tqueryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: number[], minId?: number): void;\n\tqueryRadius(cx: number, cy: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, radius: number, result: number[]): void;\n\tgetEntry(entityId: number): SpatialEntry | undefined;\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "2PAYA,uBAAS,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "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",
|
|
9
|
+
"debugId": "0408432B17B2791864756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var f=((F)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(F,{get:(J,K)=>(typeof require<"u"?require:J)[K]}):F)(function(F){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+F+'" is not supported')});import{definePlugin as D}from"ecspresso";function W(F,J,K){return F*73856093^J*19349663^K*83492791}function v(F){return{cellSize:F,invCellSize:1/F,cells:new Map,entries:[],_aliveGen:0,_queryGen:0}}function C(F){F._aliveGen++;for(let J of F.cells.values())J.length=0}function k(F,J,K,U,M,O,j,V){let P=F._aliveGen,_=F.entries[J],R;if(_)_.x=K,_.y=U,_.z=M,_.halfW=O,_.halfH=j,_.halfD=V,_._aliveGen=P,R=_;else R={entityId:J,x:K,y:U,z:M,halfW:O,halfH:j,halfD:V,_lastSeenGen:0,_aliveGen:P},F.entries[J]=R;let L=F.invCellSize,q=Math.floor((K-O)*L),A=Math.floor((K+O)*L),$=Math.floor((U-j)*L),T=Math.floor((U+j)*L),G=Math.floor((M-V)*L),E=Math.floor((M+V)*L);for(let B=q;B<=A;B++)for(let N=$;N<=T;N++)for(let w=G;w<=E;w++){let Q=W(B,N,w),S=F.cells.get(Q);if(S)S.push(R);else F.cells.set(Q,[R])}}function H(F,J,K,U,M,O,j,V,P=-1){let _=F.invCellSize,R=Math.floor(J*_),L=Math.floor(M*_),q=Math.floor(K*_),A=Math.floor(O*_),$=Math.floor(U*_),T=Math.floor(j*_),G=++F._queryGen;for(let E=R;E<=L;E++)for(let B=q;B<=A;B++)for(let N=$;N<=T;N++){let w=F.cells.get(W(E,B,N));if(!w)continue;for(let Q of w){if(Q.entityId<=P||Q._lastSeenGen===G)continue;Q._lastSeenGen=G,V.push(Q.entityId)}}}function I(F,J,K,U,M,O){let j=M*M,V=F.invCellSize,P=Math.floor((J-M)*V),_=Math.floor((J+M)*V),R=Math.floor((K-M)*V),L=Math.floor((K+M)*V),q=Math.floor((U-M)*V),A=Math.floor((U+M)*V),$=++F._queryGen;for(let T=P;T<=_;T++)for(let G=R;G<=L;G++)for(let E=q;E<=A;E++){let B=F.cells.get(W(T,G,E));if(!B)continue;for(let N of B){if(N._lastSeenGen===$)continue;N._lastSeenGen=$;let w=Math.max(N.x-N.halfW,Math.min(J,N.x+N.halfW)),Q=Math.max(N.y-N.halfH,Math.min(K,N.y+N.halfH)),S=Math.max(N.z-N.halfD,Math.min(U,N.z+N.halfD)),X=J-w,Y=K-Q,Z=U-S;if(X*X+Y*Y+Z*Z<=j)O.push(N.entityId)}}}function b(F,J){let K=F.entries[J];if(!K||K._aliveGen!==F._aliveGen)return;return K}function p(F){return{grid:F,queryBox(J,K,U,M,O,j){let V=[];return H(F,J,K,U,M,O,j,V),V},queryBoxInto(J,K,U,M,O,j,V,P){H(F,J,K,U,M,O,j,V,P)},queryRadius(J,K,U,M){let O=[];return I(F,J,K,U,M,O),O},queryRadiusInto(J,K,U,M,O){I(F,J,K,U,M,O)},getEntry(J){return b(F,J)}}}function l(F){let{cellSize:J=64,systemGroup:K="spatialIndex3D",priority:U=2000,phases:M=["fixedUpdate","postUpdate"]}=F??{},O=v(J),j=p(O);return D("spatialIndex3D").withComponentTypes().withResourceTypes().withLabels().withGroups().install((V)=>{V.addResource("spatialIndex3D",j);for(let P of M){let _=P==="fixedUpdate"?"localTransform3D":"worldTransform3D";V.addSystem(`spatial-index3D-rebuild-${P}`).setPriority(U).inPhase(P).inGroup(K).addQuery("transforms",{with:[_]}).runWhenEmpty().setProcess(({queries:R,ecs:L})=>{C(O);for(let q of R.transforms){let A=q.components[_],$=L.getComponent(q.id,"aabb3DCollider"),T=$?void 0:L.getComponent(q.id,"sphereCollider");if($){k(O,q.id,A.x+($.offsetX??0),A.y+($.offsetY??0),A.z+($.offsetZ??0),$.width/2,$.height/2,$.depth/2);continue}if(T){let G=T.radius;k(O,q.id,A.x+(T.offsetX??0),A.y+(T.offsetY??0),A.z+(T.offsetZ??0),G,G,G)}}})}})}export{l as createSpatialIndex3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=10C4CA741CCEA05664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index3D.js.map
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/spatial/spatial-index3D.ts", "../src/utils/spatial-hash3D.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Spatial Index 3D Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries in 3D. Rebuilds the grid each frame from entity\n * transforms. Replaces O(n²) brute-force with O(n·d) where d = local density.\n *\n * Standalone usage: queryBox / queryRadius for proximity queries.\n * Automatic acceleration: collision3D and physics3D plugins detect the\n * spatialIndex3D resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport {\n\ttype SpatialEntry3D,\n\ttype SpatialHashGrid3D,\n\ttype SpatialIndex3D,\n\tcreateGrid3D,\n\tclearGrid3D,\n\tinsertEntity3D,\n\tgridQueryBox3D,\n\tgridQueryRadius3D,\n} from '../../utils/spatial-hash3D';\n\n//
|
|
6
|
-
"/**\n * Spatial Hash Grid 3D\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries in 3D. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structures ====================\n\nexport interface SpatialEntry3D {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tz: number;\n\thalfW: number;\n\thalfH: number;\n\thalfD: number;\n}\n\nexport interface SpatialHashGrid3D {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number,
|
|
5
|
+
"/**\n * Spatial Index 3D Plugin for ECSpresso\n *\n * Provides a uniform-grid spatial hash for broadphase collision detection\n * and proximity queries in 3D. Rebuilds the grid each frame from entity\n * transforms. Replaces O(n²) brute-force with O(n·d) where d = local density.\n *\n * Standalone usage: queryBox / queryRadius for proximity queries.\n * Automatic acceleration: collision3D and physics3D plugins detect the\n * spatialIndex3D resource at runtime and use it for broadphase when present.\n */\n\nimport { definePlugin } from 'ecspresso';\nimport type { SystemPhase } from 'ecspresso';\nimport type { Transform3DComponentTypes } from './transform3D';\nimport {\n\ttype SpatialEntry3D,\n\ttype SpatialHashGrid3D,\n\ttype SpatialIndex3D,\n\tcreateGrid3D,\n\tclearGrid3D,\n\tinsertEntity3D,\n\tgridQueryBox3D,\n\tgridQueryRadius3D,\n\tgetLiveEntry3D,\n} from '../../utils/spatial-hash3D';\n\n// ==================== Collider Component Types ====================\n\n/**\n * 3D axis-aligned bounding box collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface AABB3DCollider {\n\twidth: number;\n\theight: number;\n\tdepth: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\n/**\n * Sphere collider component.\n * Defined here (spatial layer) so collision3D can import rather than redefine.\n */\nexport interface SphereCollider {\n\tradius: number;\n\toffsetX?: number;\n\toffsetY?: number;\n\toffsetZ?: number;\n}\n\nexport interface Spatial3DColliderComponentTypes {\n\taabb3DCollider: AABB3DCollider;\n\tsphereCollider: SphereCollider;\n}\n\n// ==================== Resource API ====================\n\nexport interface SpatialIndex3DResourceTypes {\n\tspatialIndex3D: SpatialIndex3D;\n}\n\nfunction createSpatialIndex3DResource(grid: SpatialHashGrid3D): SpatialIndex3D {\n\treturn {\n\t\tgrid,\n\t\tqueryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[] {\n\t\t\tconst out: number[] = [];\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, out);\n\t\t\treturn out;\n\t\t},\n\t\tqueryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: number[], minId?: number): void {\n\t\t\tgridQueryBox3D(grid, minX, minY, minZ, maxX, maxY, maxZ, result, minId);\n\t\t},\n\t\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[] {\n\t\t\tconst out: number[] = [];\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, out);\n\t\t\treturn out;\n\t\t},\n\t\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: number[]): void {\n\t\t\tgridQueryRadius3D(grid, cx, cy, cz, radius, result);\n\t\t},\n\t\tgetEntry(entityId: number): SpatialEntry3D | undefined {\n\t\t\treturn getLiveEntry3D(grid, entityId);\n\t\t},\n\t};\n}\n\n// ==================== Component Types ====================\n\ntype SpatialIndex3DComponentTypes = Transform3DComponentTypes & Spatial3DColliderComponentTypes;\n\n// ==================== Plugin Options ====================\n\nexport type SpatialIndex3DPhase = 'fixedUpdate' | 'postUpdate';\ntype SpatialIndex3DLabel = `spatial-index3D-rebuild-${SpatialIndex3DPhase}`;\n\nexport interface SpatialIndex3DPluginOptions<G extends string = 'spatialIndex3D'> {\n\t/** Cell size for the spatial hash grid (default: 64) */\n\tcellSize?: number;\n\t/** System group name (default: 'spatialIndex3D') */\n\tsystemGroup?: G;\n\t/** Priority for rebuild systems (default: 2000, before collision) */\n\tpriority?: number;\n\t/** Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']) */\n\tphases?: ReadonlyArray<SpatialIndex3DPhase>;\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a 3D spatial index plugin for ECSpresso.\n *\n * Provides a uniform-grid spatial hash that accelerates 3D collision detection.\n * When installed alongside the collision3D or physics3D plugins, they\n * automatically use the spatial index for broadphase instead of O(n²)\n * brute-force.\n *\n * Also provides proximity query methods for game logic (e.g. \"find all\n * enemies within 200 units\").\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTransform3DPlugin())\n * .withPlugin(createCollision3DPlugin({ layers }))\n * .withPlugin(createSpatialIndex3DPlugin({ cellSize: 128 }))\n * .build();\n *\n * // Proximity query in a system:\n * const si = ecs.getResource('spatialIndex3D');\n * const nearby = si.queryRadius(playerX, playerY, playerZ, 200);\n * ```\n */\nexport function createSpatialIndex3DPlugin<G extends string = 'spatialIndex3D'>(\n\toptions?: SpatialIndex3DPluginOptions<G>,\n) {\n\tconst {\n\t\tcellSize = 64,\n\t\tsystemGroup = 'spatialIndex3D',\n\t\tpriority = 2000,\n\t\tphases = ['fixedUpdate', 'postUpdate'] as const,\n\t} = options ?? {};\n\n\tconst grid = createGrid3D(cellSize);\n\tconst resource = createSpatialIndex3DResource(grid);\n\n\treturn definePlugin('spatialIndex3D')\n\t\t.withComponentTypes<SpatialIndex3DComponentTypes>()\n\t\t.withResourceTypes<SpatialIndex3DResourceTypes>()\n\t\t.withLabels<SpatialIndex3DLabel>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld.addResource('spatialIndex3D', resource);\n\n\t\t\t// Register a rebuild system for each requested phase\n\t\t\tfor (const phase of phases) {\n\t\t\t\tconst transformComponent = phase === 'fixedUpdate' ? 'localTransform3D' : 'worldTransform3D';\n\n\t\t\t\tworld\n\t\t\t\t\t.addSystem(`spatial-index3D-rebuild-${phase}`)\n\t\t\t\t\t.setPriority(priority)\n\t\t\t\t\t.inPhase(phase as SystemPhase)\n\t\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t\t.addQuery('transforms', {\n\t\t\t\t\t\twith: [transformComponent],\n\t\t\t\t\t})\n\t\t\t\t\t.runWhenEmpty()\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tclearGrid3D(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.transforms) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst aabb = ecs.getComponent(entity.id, 'aabb3DCollider');\n\t\t\t\t\t\t\t// AABB wins when both are present, matching collision3D / physics3D.\n\t\t\t\t\t\t\tconst sphere = aabb ? undefined : ecs.getComponent(entity.id, 'sphereCollider');\n\n\t\t\t\t\t\t\tif (aabb) {\n\t\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\t\ttransform.x + (aabb.offsetX ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.y + (aabb.offsetY ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.z + (aabb.offsetZ ?? 0),\n\t\t\t\t\t\t\t\t\taabb.width / 2, aabb.height / 2, aabb.depth / 2,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (sphere) {\n\t\t\t\t\t\t\t\tconst r = sphere.radius;\n\t\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\t\ttransform.x + (sphere.offsetX ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.y + (sphere.offsetY ?? 0),\n\t\t\t\t\t\t\t\t\ttransform.z + (sphere.offsetZ ?? 0),\n\t\t\t\t\t\t\t\t\tr, r, r,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t});\n\t\t\t}\n\t\t});\n}\n",
|
|
6
|
+
"/**\n * Spatial Hash Grid 3D\n *\n * Uniform-grid spatial hash for broadphase collision detection and\n * proximity queries in 3D. Pure data structure, no ECS dependencies.\n */\n\n// ==================== Data Structures ====================\n\nexport interface SpatialEntry3D {\n\tentityId: number;\n\tx: number;\n\ty: number;\n\tz: number;\n\thalfW: number;\n\thalfH: number;\n\thalfD: number;\n\t/** Generation stamp used by query functions to dedup multi-cell hits without a Set. Internal. */\n\t_lastSeenGen: number;\n\t/** Rebuild generation when this entry was last inserted. Internal. */\n\t_aliveGen: number;\n}\n\nexport interface SpatialHashGrid3D {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, SpatialEntry3D[]>;\n\t/**\n\t * Dense, indexed by entityId. Holes are `undefined`. Entries from previous\n\t * rebuilds remain in place for in-place reuse (zero allocation in steady\n\t * state); liveness is determined by `entry._aliveGen === grid._aliveGen`.\n\t * Internal — read live entries via `getLiveEntry3D` / `liveEntryCount3D` helpers.\n\t *\n\t * High-water-mark grows with max entityId ever inserted; despawned ids\n\t * leave their slot occupied by a stale entry. Acceptable when the entity\n\t * manager recycles ids or peak count is bounded.\n\t */\n\tentries: (SpatialEntry3D | undefined)[];\n\t/** Monotonic counter bumped by each `clearGrid3D` call. Internal. */\n\t_aliveGen: number;\n\t/** Monotonic counter bumped on each query; entries record their last-seen gen for O(1) dedup. Internal. */\n\t_queryGen: number;\n}\n\n// ==================== Pure Functions ====================\n\n/**\n * Hash a cell coordinate triple to a single integer key.\n * Uses large-prime XOR to distribute values.\n */\nexport function hashCell3D(cx: number, cy: number, cz: number): number {\n\t// Large primes for spatial hashing distribution\n\treturn (cx * 73856093) ^ (cy * 19349663) ^ (cz * 83492791);\n}\n\n/**\n * Create a new empty 3D spatial hash grid.\n */\nexport function createGrid3D(cellSize: number): SpatialHashGrid3D {\n\treturn {\n\t\tcellSize,\n\t\tinvCellSize: 1 / cellSize,\n\t\tcells: new Map(),\n\t\tentries: [],\n\t\t_aliveGen: 0,\n\t\t_queryGen: 0,\n\t};\n}\n\n/**\n * Prepare the grid for a rebuild.\n *\n * Bumps the alive-generation counter so entries inserted prior to this call\n * are implicitly stale (any access via `getLiveEntry3D` / `liveEntryCount3D`\n * filters by the current gen). Existing `SpatialEntry3D` objects remain in\n * the `entries` array for in-place reuse by the next `insertEntity3D`, so\n * steady-state rebuilds allocate zero entries.\n *\n * Cell buckets are cleared in place — keys are retained so subsequent\n * inserts hit the existing array rather than allocating a fresh one.\n */\nexport function clearGrid3D(grid: SpatialHashGrid3D): void {\n\tgrid._aliveGen++;\n\n\tfor (const bucket of grid.cells.values()) {\n\t\tbucket.length = 0;\n\t}\n}\n\n/**\n * Insert an entity into all overlapping cells of the grid.\n */\nexport function insertEntity3D(\n\tgrid: SpatialHashGrid3D,\n\tentityId: number,\n\tx: number,\n\ty: number,\n\tz: number,\n\thalfW: number,\n\thalfH: number,\n\thalfD: number,\n): void {\n\tconst gen = grid._aliveGen;\n\tconst existing = grid.entries[entityId];\n\tlet entry: SpatialEntry3D;\n\tif (existing) {\n\t\texisting.x = x;\n\t\texisting.y = y;\n\t\texisting.z = z;\n\t\texisting.halfW = halfW;\n\t\texisting.halfH = halfH;\n\t\texisting.halfD = halfD;\n\t\texisting._aliveGen = gen;\n\t\tentry = existing;\n\t} else {\n\t\tentry = { entityId, x, y, z, halfW, halfH, halfD, _lastSeenGen: 0, _aliveGen: gen };\n\t\tgrid.entries[entityId] = entry;\n\t}\n\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((x - halfW) * inv);\n\tconst maxCX = Math.floor((x + halfW) * inv);\n\tconst minCY = Math.floor((y - halfH) * inv);\n\tconst maxCY = Math.floor((y + halfH) * inv);\n\tconst minCZ = Math.floor((z - halfD) * inv);\n\tconst maxCZ = Math.floor((z + halfD) * inv);\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tfor (let cz = minCZ; cz <= maxCZ; cz++) {\n\t\t\t\tconst key = hashCell3D(cx, cy, cz);\n\t\t\t\tconst bucket = grid.cells.get(key);\n\t\t\t\tif (bucket) {\n\t\t\t\t\tbucket.push(entry);\n\t\t\t\t} else {\n\t\t\t\t\tgrid.cells.set(key, [entry]);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs from all cells overlapping the given 3D box.\n *\n * Appends to `result` (caller clears/truncates first if reusing). Multi-cell\n * entries are deduplicated via a per-grid generation stamp on each\n * `SpatialEntry3D`.\n *\n * When `minId` is provided, only entries with `entityId > minId` are added —\n * used for symmetric broadphase pair generation.\n */\nexport function gridQueryBox3D(\n\tgrid: SpatialHashGrid3D,\n\tminX: number,\n\tminY: number,\n\tminZ: number,\n\tmaxX: number,\n\tmaxY: number,\n\tmaxZ: number,\n\tresult: number[],\n\tminId: number = -1,\n): void {\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor(minX * inv);\n\tconst maxCX = Math.floor(maxX * inv);\n\tconst minCY = Math.floor(minY * inv);\n\tconst maxCY = Math.floor(maxY * inv);\n\tconst minCZ = Math.floor(minZ * inv);\n\tconst maxCZ = Math.floor(maxZ * inv);\n\n\tconst gen = ++grid._queryGen;\n\n\tfor (let cx = minCX; cx <= maxCX; cx++) {\n\t\tfor (let cy = minCY; cy <= maxCY; cy++) {\n\t\t\tfor (let cz = minCZ; cz <= maxCZ; cz++) {\n\t\t\t\tconst bucket = grid.cells.get(hashCell3D(cx, cy, cz));\n\t\t\t\tif (!bucket) continue;\n\t\t\t\tfor (const entry of bucket) {\n\t\t\t\t\tif (entry.entityId <= minId || entry._lastSeenGen === gen) continue;\n\t\t\t\t\tentry._lastSeenGen = gen;\n\t\t\t\t\tresult.push(entry.entityId);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Collect entity IDs within a sphere. AABB-to-point distance filter against\n * the cells overlapping the sphere's bounding box. Appends to `result`.\n */\nexport function gridQueryRadius3D(\n\tgrid: SpatialHashGrid3D,\n\tcx: number,\n\tcy: number,\n\tcz: number,\n\tradius: number,\n\tresult: number[],\n): void {\n\tconst rSq = radius * radius;\n\tconst inv = grid.invCellSize;\n\tconst minCX = Math.floor((cx - radius) * inv);\n\tconst maxCX = Math.floor((cx + radius) * inv);\n\tconst minCY = Math.floor((cy - radius) * inv);\n\tconst maxCY = Math.floor((cy + radius) * inv);\n\tconst minCZ = Math.floor((cz - radius) * inv);\n\tconst maxCZ = Math.floor((cz + radius) * inv);\n\n\tconst gen = ++grid._queryGen;\n\n\tfor (let icx = minCX; icx <= maxCX; icx++) {\n\t\tfor (let icy = minCY; icy <= maxCY; icy++) {\n\t\t\tfor (let icz = minCZ; icz <= maxCZ; icz++) {\n\t\t\t\tconst bucket = grid.cells.get(hashCell3D(icx, icy, icz));\n\t\t\t\tif (!bucket) continue;\n\t\t\t\tfor (const entry of bucket) {\n\t\t\t\t\tif (entry._lastSeenGen === gen) continue;\n\t\t\t\t\tentry._lastSeenGen = gen;\n\n\t\t\t\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\t\t\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\t\t\t\tconst closestZ = Math.max(entry.z - entry.halfD, Math.min(cz, entry.z + entry.halfD));\n\t\t\t\t\tconst dx = cx - closestX;\n\t\t\t\t\tconst dy = cy - closestY;\n\t\t\t\t\tconst dz = cz - closestZ;\n\n\t\t\t\t\tif (dx * dx + dy * dy + dz * dz <= rSq) {\n\t\t\t\t\t\tresult.push(entry.entityId);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Get the current-generation entry for an entityId, or `undefined` if the\n * entity isn't in the index for this rebuild. Stale entries from previous\n * rebuilds remain in `entries` for in-place reuse but are filtered here.\n */\nexport function getLiveEntry3D(grid: SpatialHashGrid3D, entityId: number): SpatialEntry3D | undefined {\n\tconst entry = grid.entries[entityId];\n\tif (!entry || entry._aliveGen !== grid._aliveGen) return undefined;\n\treturn entry;\n}\n\n/**\n * Count entries inserted in the current rebuild generation. Linear scan —\n * intended for tests and diagnostics, not hot paths.\n */\nexport function liveEntryCount3D(grid: SpatialHashGrid3D): number {\n\tconst gen = grid._aliveGen;\n\tlet n = 0;\n\tfor (const entry of grid.entries) {\n\t\tif (entry && entry._aliveGen === gen) n++;\n\t}\n\treturn n;\n}\n\n// ==================== SpatialIndex3D Interface ====================\n\n/**\n * High-level spatial index API for 3D broadphase queries.\n *\n * Defined here (the utility layer) so that narrowphase3D can accept it\n * without importing the ECS plugin. The spatial-index3D plugin creates\n * an object that implements this interface and registers it as a resource.\n */\nexport interface SpatialIndex3D {\n\treadonly grid: SpatialHashGrid3D;\n\tqueryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[];\n\tqueryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: number[], minId?: number): void;\n\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: number[]): void;\n\tgetEntry(entityId: number): SpatialEntry3D | undefined;\n}\n"
|
|
7
7
|
],
|
|
8
|
-
"mappings": "2PAYA,uBAAS,
|
|
9
|
-
"debugId": "
|
|
8
|
+
"mappings": "2PAYA,uBAAS,kBCsCF,SAAS,CAAU,CAAC,EAAY,EAAY,EAAoB,CAEtE,OAAQ,EAAK,SAAa,EAAK,SAAa,EAAK,SAM3C,SAAS,CAAY,CAAC,EAAqC,CACjE,MAAO,CACN,WACA,YAAa,EAAI,EACjB,MAAO,IAAI,IACX,QAAS,CAAC,EACV,UAAW,EACX,UAAW,CACZ,EAeM,SAAS,CAAW,CAAC,EAA+B,CAC1D,EAAK,YAEL,QAAW,KAAU,EAAK,MAAM,OAAO,EACtC,EAAO,OAAS,EAOX,SAAS,CAAc,CAC7B,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACO,CACP,IAAM,EAAM,EAAK,UACX,EAAW,EAAK,QAAQ,GAC1B,EACJ,GAAI,EACH,EAAS,EAAI,EACb,EAAS,EAAI,EACb,EAAS,EAAI,EACb,EAAS,MAAQ,EACjB,EAAS,MAAQ,EACjB,EAAS,MAAQ,EACjB,EAAS,UAAY,EACrB,EAAQ,EAER,OAAQ,CAAE,WAAU,IAAG,IAAG,IAAG,QAAO,QAAO,QAAO,aAAc,EAAG,UAAW,CAAI,EAClF,EAAK,QAAQ,GAAY,EAG1B,IAAM,EAAM,EAAK,YACX,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EACpC,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EACpC,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EACpC,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EACpC,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EACpC,EAAQ,KAAK,OAAO,EAAI,GAAS,CAAG,EAE1C,QAAS,EAAK,EAAO,GAAM,EAAO,IACjC,QAAS,EAAK,EAAO,GAAM,EAAO,IACjC,QAAS,EAAK,EAAO,GAAM,EAAO,IAAM,CACvC,IAAM,EAAM,EAAW,EAAI,EAAI,CAAE,EAC3B,EAAS,EAAK,MAAM,IAAI,CAAG,EACjC,GAAI,EACH,EAAO,KAAK,CAAK,EAEjB,OAAK,MAAM,IAAI,EAAK,CAAC,CAAK,CAAC,GAiBzB,SAAS,CAAc,CAC7B,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EACA,EAAgB,GACT,CACP,IAAM,EAAM,EAAK,YACX,EAAQ,KAAK,MAAM,EAAO,CAAG,EAC7B,EAAQ,KAAK,MAAM,EAAO,CAAG,EAC7B,EAAQ,KAAK,MAAM,EAAO,CAAG,EAC7B,EAAQ,KAAK,MAAM,EAAO,CAAG,EAC7B,EAAQ,KAAK,MAAM,EAAO,CAAG,EAC7B,EAAQ,KAAK,MAAM,EAAO,CAAG,EAE7B,EAAM,EAAE,EAAK,UAEnB,QAAS,EAAK,EAAO,GAAM,EAAO,IACjC,QAAS,EAAK,EAAO,GAAM,EAAO,IACjC,QAAS,EAAK,EAAO,GAAM,EAAO,IAAM,CACvC,IAAM,EAAS,EAAK,MAAM,IAAI,EAAW,EAAI,EAAI,CAAE,CAAC,EACpD,GAAI,CAAC,EAAQ,SACb,QAAW,KAAS,EAAQ,CAC3B,GAAI,EAAM,UAAY,GAAS,EAAM,eAAiB,EAAK,SAC3D,EAAM,aAAe,EACrB,EAAO,KAAK,EAAM,QAAQ,IAWxB,SAAS,CAAiB,CAChC,EACA,EACA,EACA,EACA,EACA,EACO,CACP,IAAM,EAAM,EAAS,EACf,EAAM,EAAK,YACX,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EACtC,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EACtC,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EACtC,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EACtC,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EACtC,EAAQ,KAAK,OAAO,EAAK,GAAU,CAAG,EAEtC,EAAM,EAAE,EAAK,UAEnB,QAAS,EAAM,EAAO,GAAO,EAAO,IACnC,QAAS,EAAM,EAAO,GAAO,EAAO,IACnC,QAAS,EAAM,EAAO,GAAO,EAAO,IAAO,CAC1C,IAAM,EAAS,EAAK,MAAM,IAAI,EAAW,EAAK,EAAK,CAAG,CAAC,EACvD,GAAI,CAAC,EAAQ,SACb,QAAW,KAAS,EAAQ,CAC3B,GAAI,EAAM,eAAiB,EAAK,SAChC,EAAM,aAAe,EAErB,IAAM,EAAW,KAAK,IAAI,EAAM,EAAI,EAAM,MAAO,KAAK,IAAI,EAAI,EAAM,EAAI,EAAM,KAAK,CAAC,EAC9E,EAAW,KAAK,IAAI,EAAM,EAAI,EAAM,MAAO,KAAK,IAAI,EAAI,EAAM,EAAI,EAAM,KAAK,CAAC,EAC9E,EAAW,KAAK,IAAI,EAAM,EAAI,EAAM,MAAO,KAAK,IAAI,EAAI,EAAM,EAAI,EAAM,KAAK,CAAC,EAC9E,EAAK,EAAK,EACV,EAAK,EAAK,EACV,EAAK,EAAK,EAEhB,GAAI,EAAK,EAAK,EAAK,EAAK,EAAK,GAAM,EAClC,EAAO,KAAK,EAAM,QAAQ,IAazB,SAAS,CAAc,CAAC,EAAyB,EAA8C,CACrG,IAAM,EAAQ,EAAK,QAAQ,GAC3B,GAAI,CAAC,GAAS,EAAM,YAAc,EAAK,UAAW,OAClD,OAAO,EDpLR,SAAS,CAA4B,CAAC,EAAyC,CAC9E,MAAO,CACN,OACA,QAAQ,CAAC,EAAc,EAAc,EAAc,EAAc,EAAc,EAAwB,CACtG,IAAM,EAAgB,CAAC,EAEvB,OADA,EAAe,EAAM,EAAM,EAAM,EAAM,EAAM,EAAM,EAAM,CAAG,EACrD,GAER,YAAY,CAAC,EAAc,EAAc,EAAc,EAAc,EAAc,EAAc,EAAkB,EAAsB,CACxI,EAAe,EAAM,EAAM,EAAM,EAAM,EAAM,EAAM,EAAM,EAAQ,CAAK,GAEvE,WAAW,CAAC,EAAY,EAAY,EAAY,EAA0B,CACzE,IAAM,EAAgB,CAAC,EAEvB,OADA,EAAkB,EAAM,EAAI,EAAI,EAAI,EAAQ,CAAG,EACxC,GAER,eAAe,CAAC,EAAY,EAAY,EAAY,EAAgB,EAAwB,CAC3F,EAAkB,EAAM,EAAI,EAAI,EAAI,EAAQ,CAAM,GAEnD,QAAQ,CAAC,EAA8C,CACtD,OAAO,EAAe,EAAM,CAAQ,EAEtC,EAiDM,SAAS,CAA+D,CAC9E,EACC,CACD,IACC,WAAW,GACX,cAAc,iBACd,WAAW,KACX,SAAS,CAAC,cAAe,YAAY,GAClC,GAAW,CAAC,EAEV,EAAO,EAAa,CAAQ,EAC5B,EAAW,EAA6B,CAAI,EAElD,OAAO,EAAa,gBAAgB,EAClC,mBAAiD,EACjD,kBAA+C,EAC/C,WAAgC,EAChC,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EAAM,YAAY,iBAAkB,CAAQ,EAG5C,QAAW,KAAS,EAAQ,CAC3B,IAAM,EAAqB,IAAU,cAAgB,mBAAqB,mBAE1E,EACE,UAAU,2BAA2B,GAAO,EAC5C,YAAY,CAAQ,EACpB,QAAQ,CAAoB,EAC5B,QAAQ,CAAW,EACnB,SAAS,aAAc,CACvB,KAAM,CAAC,CAAkB,CAC1B,CAAC,EACA,aAAa,EACb,WAAW,EAAG,UAAS,SAAU,CACjC,EAAY,CAAI,EAEhB,QAAW,KAAU,EAAQ,WAAY,CACxC,IAAM,EAAY,EAAO,WAAW,GAC9B,EAAO,EAAI,aAAa,EAAO,GAAI,gBAAgB,EAEnD,EAAS,EAAO,OAAY,EAAI,aAAa,EAAO,GAAI,gBAAgB,EAE9E,GAAI,EAAM,CACT,EACC,EAAM,EAAO,GACb,EAAU,GAAK,EAAK,SAAW,GAC/B,EAAU,GAAK,EAAK,SAAW,GAC/B,EAAU,GAAK,EAAK,SAAW,GAC/B,EAAK,MAAQ,EAAG,EAAK,OAAS,EAAG,EAAK,MAAQ,CAC/C,EACA,SAGD,GAAI,EAAQ,CACX,IAAM,EAAI,EAAO,OACjB,EACC,EAAM,EAAO,GACb,EAAU,GAAK,EAAO,SAAW,GACjC,EAAU,GAAK,EAAO,SAAW,GACjC,EAAU,GAAK,EAAO,SAAW,GACjC,EAAG,EAAG,CACP,IAGF,GAEH",
|
|
9
|
+
"debugId": "10C4CA741CCEA05664756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/query-cache.d.ts
CHANGED
|
@@ -10,8 +10,8 @@ export interface QueryCacheHost<ComponentTypes> extends MatchHost<ComponentTypes
|
|
|
10
10
|
componentIndex(component: keyof ComponentTypes): Set<number> | undefined;
|
|
11
11
|
}
|
|
12
12
|
/**
|
|
13
|
-
* Maintains incrementally-updated
|
|
14
|
-
* portion of registered query shapes (with / without / parentHas).
|
|
13
|
+
* Maintains incrementally-updated Maps of entity id → Entity matching the
|
|
14
|
+
* static portion of registered query shapes (with / without / parentHas).
|
|
15
15
|
* EntityManager calls the on* hooks on component add/remove, entity
|
|
16
16
|
* removal, and parent change. The `changed` filter is applied as a
|
|
17
17
|
* post-pass by the caller, since its threshold advances each tick.
|
|
@@ -23,12 +23,12 @@ export default class QueryCache<ComponentTypes> {
|
|
|
23
23
|
private readonly byParentComp;
|
|
24
24
|
constructor(host: QueryCacheHost<ComponentTypes>);
|
|
25
25
|
/**
|
|
26
|
-
* Returns the
|
|
27
|
-
* shape. Caches are interned by canonical shape — identical
|
|
28
|
-
* a single
|
|
29
|
-
* smallest matching component index.
|
|
26
|
+
* Returns the Map of entity id → Entity matching the (with, without,
|
|
27
|
+
* parentHas) shape. Caches are interned by canonical shape — identical
|
|
28
|
+
* shapes share a single Map across systems. Cold-start populates by
|
|
29
|
+
* iterating the smallest matching component index.
|
|
30
30
|
*/
|
|
31
|
-
getOrCreate(withC: ReadonlyArray<keyof ComponentTypes>, withoutC: ReadonlyArray<keyof ComponentTypes>, parentHas: ReadonlyArray<keyof ComponentTypes>):
|
|
31
|
+
getOrCreate(withC: ReadonlyArray<keyof ComponentTypes>, withoutC: ReadonlyArray<keyof ComponentTypes>, parentHas: ReadonlyArray<keyof ComponentTypes>): Map<number, Entity<ComponentTypes>>;
|
|
32
32
|
/** Test-only: returns the number of distinct interned shapes. */
|
|
33
33
|
get cacheCount(): number;
|
|
34
34
|
private populate;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy monotonic registry mapping layer name → unique bit. Lets pair
|
|
3
|
+
* filtering use a single `(a.collidesWithMask & b.layerBit)` check
|
|
4
|
+
* instead of `Array.includes` on every collision pair.
|
|
5
|
+
*
|
|
6
|
+
* One registry per dimension (2D and 3D) — user-defined layer namespaces
|
|
7
|
+
* are independent, so bits should not be shared across systems.
|
|
8
|
+
*
|
|
9
|
+
* Maximum 32 layers per registry (one per bit in a 32-bit signed int).
|
|
10
|
+
* Crossing the limit throws on the next `getLayerBit` call.
|
|
11
|
+
*/
|
|
12
|
+
export interface LayerBitRegistry {
|
|
13
|
+
getLayerBit(layer: string): number;
|
|
14
|
+
/** OR of `getLayerBit` for every entry. Cached by array reference. */
|
|
15
|
+
getCollidesWithMask(collidesWith: readonly string[]): number;
|
|
16
|
+
}
|
|
17
|
+
export declare function createLayerBitRegistry(label: string): LayerBitRegistry;
|
|
@@ -41,11 +41,21 @@ export interface BaseColliderInfo<L extends string = string> {
|
|
|
41
41
|
y: number;
|
|
42
42
|
layer: L;
|
|
43
43
|
collidesWith: readonly L[];
|
|
44
|
+
/**
|
|
45
|
+
* Bit assigned to `layer` from the lazy layer registry. Populated by
|
|
46
|
+
* `fillBaseColliderInfo`. Used together with `collidesWithMask` to
|
|
47
|
+
* replace per-pair `Array.includes` layer checks with a single AND.
|
|
48
|
+
*/
|
|
49
|
+
layerBit: number;
|
|
50
|
+
/** OR of `getLayerBit` for every entry in `collidesWith`. */
|
|
51
|
+
collidesWithMask: number;
|
|
44
52
|
shape: ColliderShape;
|
|
45
53
|
halfWidth: number;
|
|
46
54
|
halfHeight: number;
|
|
47
55
|
radius: number;
|
|
48
56
|
}
|
|
57
|
+
export declare const getLayerBit: (layer: string) => number;
|
|
58
|
+
export declare const getCollidesWithMask: (collidesWith: readonly string[]) => number;
|
|
49
59
|
/**
|
|
50
60
|
* Populate a `BaseColliderInfo` slot in place from raw component data.
|
|
51
61
|
* Returns `true` if the slot was filled, `false` if the entity has no
|
|
@@ -84,6 +94,23 @@ export declare function computeAABBvsCircle(aabbX: number, aabbY: number, ahw: n
|
|
|
84
94
|
* write the contact into `out`. Returns `true` if the pair overlaps.
|
|
85
95
|
*/
|
|
86
96
|
export declare function computeContact(a: BaseColliderInfo, b: BaseColliderInfo, out: Contact): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Per-caller scratch for the broadphase entityId → collider lookup.
|
|
99
|
+
*
|
|
100
|
+
* Dense `arr` indexed by entityId, paired with a `gen` stamp array that marks
|
|
101
|
+
* which slots are live this call. Bumping `current` invalidates all prior
|
|
102
|
+
* entries without clearing — replaces the per-frame `Map.clear()` + N
|
|
103
|
+
* `Map.set()` allocation churn that a `Map<number, I>` would incur.
|
|
104
|
+
*
|
|
105
|
+
* Owned per plugin instance (alongside its `colliderPool`), so concurrent
|
|
106
|
+
* worlds don't share state and `I` stays fully typed without erasure.
|
|
107
|
+
*/
|
|
108
|
+
export interface BroadphaseScratch<I extends BaseColliderInfo> {
|
|
109
|
+
arr: (I | undefined)[];
|
|
110
|
+
gen: number[];
|
|
111
|
+
current: number;
|
|
112
|
+
}
|
|
113
|
+
export declare function createBroadphaseScratch<I extends BaseColliderInfo>(): BroadphaseScratch<I>;
|
|
87
114
|
/**
|
|
88
115
|
* Generic collision detection pipeline: brute-force or broadphase,
|
|
89
116
|
* with layer filtering and contact computation.
|
|
@@ -92,13 +119,11 @@ export declare function computeContact(a: BaseColliderInfo, b: BaseColliderInfo,
|
|
|
92
119
|
* The array itself may be a grow-only pool — only indices `[0, count)`
|
|
93
120
|
* are iterated, so trailing pool slots are ignored.
|
|
94
121
|
*
|
|
95
|
-
* `
|
|
96
|
-
* path as an entityId → collider lookup.
|
|
97
|
-
*
|
|
98
|
-
* every frame. Unused by the brute-force path but still required so that
|
|
99
|
-
* typed reuse is the default, not an opt-in.
|
|
122
|
+
* `scratch` is a caller-owned `BroadphaseScratch<I>` used by the broadphase
|
|
123
|
+
* path as an entityId → collider lookup. Allocate it once per plugin instance
|
|
124
|
+
* and pass the same reference every call.
|
|
100
125
|
*
|
|
101
126
|
* Uses a context parameter forwarded to the callback to avoid
|
|
102
127
|
* per-frame closure allocation.
|
|
103
128
|
*/
|
|
104
|
-
export declare function detectCollisions<I extends BaseColliderInfo, C>(colliders: I[], count: number,
|
|
129
|
+
export declare function detectCollisions<I extends BaseColliderInfo, C>(colliders: I[], count: number, scratch: BroadphaseScratch<I>, spatialIndex: SpatialIndex | undefined, onContact: (a: I, b: I, contact: Contact, context: C) => void, context: C): void;
|
|
@@ -43,12 +43,22 @@ export interface BaseColliderInfo3D<L extends string = string> {
|
|
|
43
43
|
z: number;
|
|
44
44
|
layer: L;
|
|
45
45
|
collidesWith: readonly L[];
|
|
46
|
+
/**
|
|
47
|
+
* Bit assigned to `layer` from the lazy layer registry. Populated by
|
|
48
|
+
* `fillBaseColliderInfo3D`. Used together with `collidesWithMask` to
|
|
49
|
+
* replace per-pair `Array.includes` layer checks with a single AND.
|
|
50
|
+
*/
|
|
51
|
+
layerBit: number;
|
|
52
|
+
/** OR of `getLayerBit3D` for every entry in `collidesWith`. */
|
|
53
|
+
collidesWithMask: number;
|
|
46
54
|
shape: ColliderShape3D;
|
|
47
55
|
halfWidth: number;
|
|
48
56
|
halfHeight: number;
|
|
49
57
|
halfDepth: number;
|
|
50
58
|
radius: number;
|
|
51
59
|
}
|
|
60
|
+
export declare const getLayerBit3D: (layer: string) => number;
|
|
61
|
+
export declare const getCollidesWithMask3D: (collidesWith: readonly string[]) => number;
|
|
52
62
|
/**
|
|
53
63
|
* Populate a `BaseColliderInfo3D` slot in place from raw component data.
|
|
54
64
|
* Returns `true` if the slot was filled, `false` if the entity has no
|
|
@@ -10,14 +10,30 @@ export interface SpatialEntry {
|
|
|
10
10
|
y: number;
|
|
11
11
|
halfW: number;
|
|
12
12
|
halfH: number;
|
|
13
|
+
/** Generation stamp used by query functions to dedup multi-cell hits without a Set. Internal. */
|
|
14
|
+
_lastSeenGen: number;
|
|
15
|
+
/** Rebuild generation when this entry was last inserted. Internal. */
|
|
16
|
+
_aliveGen: number;
|
|
13
17
|
}
|
|
14
18
|
export interface SpatialHashGrid {
|
|
15
19
|
cellSize: number;
|
|
16
20
|
invCellSize: number;
|
|
17
|
-
cells: Map<number,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
cells: Map<number, SpatialEntry[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Dense, indexed by entityId. Holes are `undefined`. Entries from previous
|
|
24
|
+
* rebuilds remain in place for in-place reuse (zero allocation in steady
|
|
25
|
+
* state); liveness is determined by `entry._aliveGen === grid._aliveGen`.
|
|
26
|
+
* Internal — read live entries via `getEntry` / `liveEntryCount` helpers.
|
|
27
|
+
*
|
|
28
|
+
* High-water-mark grows with max entityId ever inserted; despawned ids
|
|
29
|
+
* leave their slot occupied by a stale entry. Acceptable when the entity
|
|
30
|
+
* manager recycles ids or peak count is bounded.
|
|
31
|
+
*/
|
|
32
|
+
entries: (SpatialEntry | undefined)[];
|
|
33
|
+
/** Monotonic counter bumped by each `clearGrid` call. Internal. */
|
|
34
|
+
_aliveGen: number;
|
|
35
|
+
/** Monotonic counter bumped on each query; entries record their last-seen gen for O(1) dedup. Internal. */
|
|
36
|
+
_queryGen: number;
|
|
21
37
|
}
|
|
22
38
|
/**
|
|
23
39
|
* Hash a cell coordinate pair to a single integer key.
|
|
@@ -31,10 +47,11 @@ export declare function createGrid(cellSize: number): SpatialHashGrid;
|
|
|
31
47
|
/**
|
|
32
48
|
* Prepare the grid for a rebuild.
|
|
33
49
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
50
|
+
* Bumps the alive-generation counter so entries inserted prior to this call
|
|
51
|
+
* are implicitly stale (any access via `getEntry`/`liveEntryCount` filters by
|
|
52
|
+
* the current gen). Existing `SpatialEntry` objects remain in the `entries`
|
|
53
|
+
* array for in-place reuse by the next `insertEntity`, so steady-state
|
|
54
|
+
* rebuilds allocate zero entries.
|
|
38
55
|
*
|
|
39
56
|
* Cell buckets are cleared in place — keys are retained so subsequent
|
|
40
57
|
* inserts hit the existing array rather than allocating a fresh one.
|
|
@@ -46,18 +63,36 @@ export declare function clearGrid(grid: SpatialHashGrid): void;
|
|
|
46
63
|
export declare function insertEntity(grid: SpatialHashGrid, entityId: number, x: number, y: number, halfW: number, halfH: number): void;
|
|
47
64
|
/**
|
|
48
65
|
* Collect entity IDs from all cells overlapping the given rectangle.
|
|
66
|
+
*
|
|
67
|
+
* Appends to `result` (caller clears/truncates first if reusing). Multi-cell
|
|
68
|
+
* entries are deduplicated via a per-grid generation stamp on each
|
|
69
|
+
* `SpatialEntry`.
|
|
70
|
+
*
|
|
71
|
+
* When `minId` is provided, only entries with `entityId > minId` are added —
|
|
72
|
+
* used for symmetric broadphase pair generation.
|
|
73
|
+
*/
|
|
74
|
+
export declare function gridQueryRect(grid: SpatialHashGrid, minX: number, minY: number, maxX: number, maxY: number, result: number[], minId?: number): void;
|
|
75
|
+
/**
|
|
76
|
+
* Collect entity IDs within a circle. AABB-to-point distance filter against
|
|
77
|
+
* the cells overlapping the circle's bounding rect. Appends to `result`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function gridQueryRadius(grid: SpatialHashGrid, cx: number, cy: number, radius: number, result: number[]): void;
|
|
80
|
+
/**
|
|
81
|
+
* Get the current-generation entry for an entityId, or `undefined` if the
|
|
82
|
+
* entity isn't in the index for this rebuild. Stale entries from previous
|
|
83
|
+
* rebuilds remain in `entries` for in-place reuse but are filtered here.
|
|
49
84
|
*/
|
|
50
|
-
export declare function
|
|
85
|
+
export declare function getLiveEntry(grid: SpatialHashGrid, entityId: number): SpatialEntry | undefined;
|
|
51
86
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
87
|
+
* Count entries inserted in the current rebuild generation. Linear scan —
|
|
88
|
+
* intended for tests and diagnostics, not hot paths.
|
|
54
89
|
*/
|
|
55
|
-
export declare function
|
|
90
|
+
export declare function liveEntryCount(grid: SpatialHashGrid): number;
|
|
56
91
|
export interface SpatialIndex {
|
|
57
92
|
readonly grid: SpatialHashGrid;
|
|
58
93
|
queryRect(minX: number, minY: number, maxX: number, maxY: number): number[];
|
|
59
|
-
queryRectInto(minX: number, minY: number, maxX: number, maxY: number, result:
|
|
94
|
+
queryRectInto(minX: number, minY: number, maxX: number, maxY: number, result: number[], minId?: number): void;
|
|
60
95
|
queryRadius(cx: number, cy: number, radius: number): number[];
|
|
61
|
-
queryRadiusInto(cx: number, cy: number, radius: number, result:
|
|
96
|
+
queryRadiusInto(cx: number, cy: number, radius: number, result: number[]): void;
|
|
62
97
|
getEntry(entityId: number): SpatialEntry | undefined;
|
|
63
98
|
}
|
|
@@ -12,14 +12,30 @@ export interface SpatialEntry3D {
|
|
|
12
12
|
halfW: number;
|
|
13
13
|
halfH: number;
|
|
14
14
|
halfD: number;
|
|
15
|
+
/** Generation stamp used by query functions to dedup multi-cell hits without a Set. Internal. */
|
|
16
|
+
_lastSeenGen: number;
|
|
17
|
+
/** Rebuild generation when this entry was last inserted. Internal. */
|
|
18
|
+
_aliveGen: number;
|
|
15
19
|
}
|
|
16
20
|
export interface SpatialHashGrid3D {
|
|
17
21
|
cellSize: number;
|
|
18
22
|
invCellSize: number;
|
|
19
|
-
cells: Map<number,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
cells: Map<number, SpatialEntry3D[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Dense, indexed by entityId. Holes are `undefined`. Entries from previous
|
|
26
|
+
* rebuilds remain in place for in-place reuse (zero allocation in steady
|
|
27
|
+
* state); liveness is determined by `entry._aliveGen === grid._aliveGen`.
|
|
28
|
+
* Internal — read live entries via `getLiveEntry3D` / `liveEntryCount3D` helpers.
|
|
29
|
+
*
|
|
30
|
+
* High-water-mark grows with max entityId ever inserted; despawned ids
|
|
31
|
+
* leave their slot occupied by a stale entry. Acceptable when the entity
|
|
32
|
+
* manager recycles ids or peak count is bounded.
|
|
33
|
+
*/
|
|
34
|
+
entries: (SpatialEntry3D | undefined)[];
|
|
35
|
+
/** Monotonic counter bumped by each `clearGrid3D` call. Internal. */
|
|
36
|
+
_aliveGen: number;
|
|
37
|
+
/** Monotonic counter bumped on each query; entries record their last-seen gen for O(1) dedup. Internal. */
|
|
38
|
+
_queryGen: number;
|
|
23
39
|
}
|
|
24
40
|
/**
|
|
25
41
|
* Hash a cell coordinate triple to a single integer key.
|
|
@@ -33,10 +49,11 @@ export declare function createGrid3D(cellSize: number): SpatialHashGrid3D;
|
|
|
33
49
|
/**
|
|
34
50
|
* Prepare the grid for a rebuild.
|
|
35
51
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
52
|
+
* Bumps the alive-generation counter so entries inserted prior to this call
|
|
53
|
+
* are implicitly stale (any access via `getLiveEntry3D` / `liveEntryCount3D`
|
|
54
|
+
* filters by the current gen). Existing `SpatialEntry3D` objects remain in
|
|
55
|
+
* the `entries` array for in-place reuse by the next `insertEntity3D`, so
|
|
56
|
+
* steady-state rebuilds allocate zero entries.
|
|
40
57
|
*
|
|
41
58
|
* Cell buckets are cleared in place — keys are retained so subsequent
|
|
42
59
|
* inserts hit the existing array rather than allocating a fresh one.
|
|
@@ -48,13 +65,31 @@ export declare function clearGrid3D(grid: SpatialHashGrid3D): void;
|
|
|
48
65
|
export declare function insertEntity3D(grid: SpatialHashGrid3D, entityId: number, x: number, y: number, z: number, halfW: number, halfH: number, halfD: number): void;
|
|
49
66
|
/**
|
|
50
67
|
* Collect entity IDs from all cells overlapping the given 3D box.
|
|
68
|
+
*
|
|
69
|
+
* Appends to `result` (caller clears/truncates first if reusing). Multi-cell
|
|
70
|
+
* entries are deduplicated via a per-grid generation stamp on each
|
|
71
|
+
* `SpatialEntry3D`.
|
|
72
|
+
*
|
|
73
|
+
* When `minId` is provided, only entries with `entityId > minId` are added —
|
|
74
|
+
* used for symmetric broadphase pair generation.
|
|
75
|
+
*/
|
|
76
|
+
export declare function gridQueryBox3D(grid: SpatialHashGrid3D, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: number[], minId?: number): void;
|
|
77
|
+
/**
|
|
78
|
+
* Collect entity IDs within a sphere. AABB-to-point distance filter against
|
|
79
|
+
* the cells overlapping the sphere's bounding box. Appends to `result`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function gridQueryRadius3D(grid: SpatialHashGrid3D, cx: number, cy: number, cz: number, radius: number, result: number[]): void;
|
|
82
|
+
/**
|
|
83
|
+
* Get the current-generation entry for an entityId, or `undefined` if the
|
|
84
|
+
* entity isn't in the index for this rebuild. Stale entries from previous
|
|
85
|
+
* rebuilds remain in `entries` for in-place reuse but are filtered here.
|
|
51
86
|
*/
|
|
52
|
-
export declare function
|
|
87
|
+
export declare function getLiveEntry3D(grid: SpatialHashGrid3D, entityId: number): SpatialEntry3D | undefined;
|
|
53
88
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
89
|
+
* Count entries inserted in the current rebuild generation. Linear scan —
|
|
90
|
+
* intended for tests and diagnostics, not hot paths.
|
|
56
91
|
*/
|
|
57
|
-
export declare function
|
|
92
|
+
export declare function liveEntryCount3D(grid: SpatialHashGrid3D): number;
|
|
58
93
|
/**
|
|
59
94
|
* High-level spatial index API for 3D broadphase queries.
|
|
60
95
|
*
|
|
@@ -65,8 +100,8 @@ export declare function gridQueryRadius3D(grid: SpatialHashGrid3D, cx: number, c
|
|
|
65
100
|
export interface SpatialIndex3D {
|
|
66
101
|
readonly grid: SpatialHashGrid3D;
|
|
67
102
|
queryBox(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number[];
|
|
68
|
-
queryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result:
|
|
103
|
+
queryBoxInto(minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, result: number[], minId?: number): void;
|
|
69
104
|
queryRadius(cx: number, cy: number, cz: number, radius: number): number[];
|
|
70
|
-
queryRadiusInto(cx: number, cy: number, cz: number, radius: number, result:
|
|
105
|
+
queryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: number[]): void;
|
|
71
106
|
getEntry(entityId: number): SpatialEntry3D | undefined;
|
|
72
107
|
}
|