ecspresso 0.16.2 → 0.17.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/dist/command-buffer.d.ts +1 -5
- package/dist/ecspresso-builder.d.ts +15 -7
- package/dist/ecspresso.d.ts +4 -5
- package/dist/entity-manager.d.ts +47 -3
- package/dist/event-bus.d.ts +2 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +9 -9
- package/dist/plugins/ai/flocking.js +2 -2
- package/dist/plugins/ai/flocking.js.map +3 -3
- package/dist/plugins/physics/collision.js +2 -2
- package/dist/plugins/physics/collision.js.map +3 -3
- package/dist/plugins/physics/collision3D.js +2 -2
- package/dist/plugins/physics/collision3D.js.map +4 -4
- package/dist/plugins/physics/physics2D.js +2 -2
- package/dist/plugins/physics/physics2D.js.map +3 -3
- package/dist/plugins/physics/physics3D.js +2 -2
- package/dist/plugins/physics/physics3D.js.map +3 -3
- package/dist/plugins/scripting/timers.d.ts +36 -11
- package/dist/plugins/scripting/timers.js +2 -2
- package/dist/plugins/scripting/timers.js.map +3 -3
- package/dist/plugins/spatial/spatial-index.d.ts +19 -1
- 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.d.ts +19 -1
- package/dist/plugins/spatial/spatial-index3D.js +2 -2
- package/dist/plugins/spatial/spatial-index3D.js.map +4 -4
- package/dist/types.d.ts +9 -1
- package/dist/utils/spatial-hash.d.ts +20 -8
- package/dist/utils/spatial-hash3D.d.ts +20 -8
- package/package.json +4 -2
|
@@ -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\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
|
|
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,
|
|
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/**\n\t * Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']).\n\t *\n\t * When both phases are registered, the `postUpdate` rebuild is\n\t * automatically skipped on cycles where:\n\t * 1. The `fixedUpdate` rebuild already ran this cycle, AND\n\t * 2. No entity hierarchy exists.\n\t *\n\t * In flat-hierarchy scenes `transform-propagation` copies\n\t * `localTransform → worldTransform` unchanged, so the postUpdate\n\t * grid would duplicate the fixedUpdate one. The skip is automatic\n\t * and re-engages once any parent relationship is set, or whenever\n\t * `fixedUpdate` doesn't run that cycle (sub-fixed-DT frames).\n\t *\n\t * Edge case: if you write to `worldTransform` directly between\n\t * phases without using `transform-propagation` or entity hierarchy,\n\t * the auto-skip will leave your writes unindexed. Set\n\t * `phases: ['postUpdate']` explicitly to bypass the auto-skip.\n\t */\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// Flag flipped true by the fixedUpdate rebuild; checked + reset by the\n\t\t\t// postUpdate rebuild to detect whether fixedUpdate ran this cycle.\n\t\t\t// Lets postUpdate auto-skip when its grid would duplicate fixedUpdate's\n\t\t\t// in flat-hierarchy scenes — without breaking sub-fixed-DT frames\n\t\t\t// where only postUpdate runs.\n\t\t\tlet fixedRebuildRanThisCycle = false;\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\t\t\t\tconst isPostUpdate = phase === 'postUpdate';\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('aabbOnly', {\n\t\t\t\t\t\twith: [transformComponent, 'aabbCollider'],\n\t\t\t\t\t\twithout: ['circleCollider'],\n\t\t\t\t\t})\n\t\t\t\t\t.addQuery('circleOnly', {\n\t\t\t\t\t\twith: [transformComponent, 'circleCollider'],\n\t\t\t\t\t\twithout: ['aabbCollider'],\n\t\t\t\t\t})\n\t\t\t\t\t.addQuery('both', {\n\t\t\t\t\t\twith: [transformComponent, 'aabbCollider', 'circleCollider'],\n\t\t\t\t\t})\n\t\t\t\t\t.setProcess(({ queries, ecs }) => {\n\t\t\t\t\t\tif (isPostUpdate) {\n\t\t\t\t\t\t\tconst ranFixed = fixedRebuildRanThisCycle;\n\t\t\t\t\t\t\tfixedRebuildRanThisCycle = false;\n\t\t\t\t\t\t\t// Skip only when fixedUpdate populated the grid this\n\t\t\t\t\t\t\t// cycle AND no hierarchy exists (so worldTransform\n\t\t\t\t\t\t\t// equals localTransform — identical grid data).\n\t\t\t\t\t\t\tif (ranFixed && !ecs.entityManager.hasHierarchy) return;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tclearGrid(grid);\n\n\t\t\t\t\t\tfor (const entity of queries.aabbOnly) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst { aabbCollider } = entity.components;\n\t\t\t\t\t\t\tconst x = transform.x + (aabbCollider.offsetX ?? 0);\n\t\t\t\t\t\t\tconst y = transform.y + (aabbCollider.offsetY ?? 0);\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, aabbCollider.width / 2, aabbCollider.height / 2);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const entity of queries.circleOnly) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst { circleCollider } = entity.components;\n\t\t\t\t\t\t\tconst x = transform.x + (circleCollider.offsetX ?? 0);\n\t\t\t\t\t\t\tconst y = transform.y + (circleCollider.offsetY ?? 0);\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, circleCollider.radius, circleCollider.radius);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Conservative broadphase: stack both offsets and take the\n\t\t\t\t\t\t// larger half-extent in each axis. Preserves the previous\n\t\t\t\t\t\t// \"both colliders\" behavior exactly.\n\t\t\t\t\t\tfor (const entity of queries.both) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst { aabbCollider, circleCollider } = entity.components;\n\t\t\t\t\t\t\tconst x = transform.x + (aabbCollider.offsetX ?? 0) + (circleCollider.offsetX ?? 0);\n\t\t\t\t\t\t\tconst y = transform.y + (aabbCollider.offsetY ?? 0) + (circleCollider.offsetY ?? 0);\n\t\t\t\t\t\t\tconst halfW = Math.max(aabbCollider.width / 2, circleCollider.radius);\n\t\t\t\t\t\t\tconst halfH = Math.max(aabbCollider.height / 2, circleCollider.radius);\n\t\t\t\t\t\t\tinsertEntity(grid, entity.id, x, y, halfW, halfH);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!isPostUpdate) fixedRebuildRanThisCycle = true;\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\n/**\n * A cell bucket — entries plus the alive-gen at which the bucket was last\n * filled. Buckets are reset lazily on the next insert in a new generation\n * (see `insertEntity`); queries skip buckets whose `_gen` is stale.\n *\n * Internal — exposed only through `SpatialHashGrid.cells`.\n */\ninterface CellBucket extends Array<SpatialEntry> {\n\t_gen: number;\n}\n\nexport interface SpatialHashGrid {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, CellBucket>;\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 * O(1): bumps the alive-generation counter so entries inserted prior to this\n * call are implicitly stale. `getLiveEntry` / `liveEntryCount` filter\n * entries by the current gen; queries skip buckets whose own `_gen` lags\n * behind the alive gen; `insertEntity` resets a bucket's `length` lazily\n * the first time it is touched in a new generation.\n *\n * Existing `SpatialEntry` objects and `CellBucket` arrays remain in place\n * for reuse, so steady-state rebuilds allocate zero entries and zero\n * buckets, regardless of how many cells have ever been touched.\n */\nexport function clearGrid(grid: SpatialHashGrid): void {\n\tgrid._aliveGen++;\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 && bucket._gen === gen) {\n\t\t\t\t// Hot path: bucket already populated this generation.\n\t\t\t\tbucket.push(entry);\n\t\t\t} else if (bucket) {\n\t\t\t\t// First touch in this generation — drop stale entries from prior rebuilds.\n\t\t\t\tbucket.length = 0;\n\t\t\t\tbucket._gen = gen;\n\t\t\t\tbucket.push(entry);\n\t\t\t} else {\n\t\t\t\tconst fresh = [entry] as CellBucket;\n\t\t\t\tfresh._gen = gen;\n\t\t\t\tgrid.cells.set(key, fresh);\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\tconst aliveGen = grid._aliveGen;\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 || bucket._gen !== aliveGen) 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\tconst aliveGen = grid._aliveGen;\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 || bucket._gen !== aliveGen) 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": "31D57D221FCA9D0D64756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -49,7 +49,25 @@ export interface SpatialIndex3DPluginOptions<G extends string = 'spatialIndex3D'
|
|
|
49
49
|
systemGroup?: G;
|
|
50
50
|
/** Priority for rebuild systems (default: 2000, before collision) */
|
|
51
51
|
priority?: number;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']).
|
|
54
|
+
*
|
|
55
|
+
* When both phases are registered, the `postUpdate` rebuild is
|
|
56
|
+
* automatically skipped on cycles where:
|
|
57
|
+
* 1. The `fixedUpdate` rebuild already ran this cycle, AND
|
|
58
|
+
* 2. No entity hierarchy exists.
|
|
59
|
+
*
|
|
60
|
+
* In flat-hierarchy scenes `transform3d-propagation` copies
|
|
61
|
+
* `localTransform3D → worldTransform3D` unchanged, so the postUpdate
|
|
62
|
+
* grid would duplicate the fixedUpdate one. The skip is automatic
|
|
63
|
+
* and re-engages once any parent relationship is set, or whenever
|
|
64
|
+
* `fixedUpdate` doesn't run that cycle (sub-fixed-DT frames).
|
|
65
|
+
*
|
|
66
|
+
* Edge case: if you write to `worldTransform3D` directly between
|
|
67
|
+
* phases without using `transform3d-propagation` or entity hierarchy,
|
|
68
|
+
* the auto-skip will leave your writes unindexed. Set
|
|
69
|
+
* `phases: ['postUpdate']` explicitly to bypass the auto-skip.
|
|
70
|
+
*/
|
|
53
71
|
phases?: ReadonlyArray<SpatialIndex3DPhase>;
|
|
54
72
|
}
|
|
55
73
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var f=((
|
|
1
|
+
var f=((J)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(J,{get:(N,K)=>(typeof require<"u"?require:N)[K]}):J)(function(J){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+J+'" is not supported')});import{definePlugin as z}from"ecspresso";function R(J,N,K){return J*73856093^N*19349663^K*83492791}function b(J){return{cellSize:J,invCellSize:1/J,cells:new Map,entries:[],_aliveGen:0,_queryGen:0}}function p(J){J._aliveGen++}function k(J,N,K,j,O,$,U,A){let M=J._aliveGen,V=J.entries[N],B;if(V)V.x=K,V.y=j,V.z=O,V.halfW=$,V.halfH=U,V.halfD=A,V._aliveGen=M,B=V;else B={entityId:N,x:K,y:j,z:O,halfW:$,halfH:U,halfD:A,_lastSeenGen:0,_aliveGen:M},J.entries[N]=B;let P=J.invCellSize,q=Math.floor((K-$)*P),W=Math.floor((K+$)*P),L=Math.floor((j-U)*P),Q=Math.floor((j+U)*P),F=Math.floor((O-A)*P),T=Math.floor((O+A)*P);for(let G=q;G<=W;G++)for(let w=L;w<=Q;w++)for(let _=F;_<=T;_++){let S=R(G,w,_),E=J.cells.get(S);if(E&&E._gen===M)E.push(B);else if(E)E.length=0,E._gen=M,E.push(B);else{let H=[B];H._gen=M,J.cells.set(S,H)}}}function I(J,N,K,j,O,$,U,A,M=-1){let V=J.invCellSize,B=Math.floor(N*V),P=Math.floor(O*V),q=Math.floor(K*V),W=Math.floor($*V),L=Math.floor(j*V),Q=Math.floor(U*V),F=++J._queryGen,T=J._aliveGen;for(let G=B;G<=P;G++)for(let w=q;w<=W;w++)for(let _=L;_<=Q;_++){let S=J.cells.get(R(G,w,_));if(!S||S._gen!==T)continue;for(let E of S){if(E.entityId<=M||E._lastSeenGen===F)continue;E._lastSeenGen=F,A.push(E.entityId)}}}function X(J,N,K,j,O,$){let U=O*O,A=J.invCellSize,M=Math.floor((N-O)*A),V=Math.floor((N+O)*A),B=Math.floor((K-O)*A),P=Math.floor((K+O)*A),q=Math.floor((j-O)*A),W=Math.floor((j+O)*A),L=++J._queryGen,Q=J._aliveGen;for(let F=M;F<=V;F++)for(let T=B;T<=P;T++)for(let G=q;G<=W;G++){let w=J.cells.get(R(F,T,G));if(!w||w._gen!==Q)continue;for(let _ of w){if(_._lastSeenGen===L)continue;_._lastSeenGen=L;let S=Math.max(_.x-_.halfW,Math.min(N,_.x+_.halfW)),E=Math.max(_.y-_.halfH,Math.min(K,_.y+_.halfH)),H=Math.max(_.z-_.halfD,Math.min(j,_.z+_.halfD)),Y=N-S,Z=K-E,v=j-H;if(Y*Y+Z*Z+v*v<=U)$.push(_.entityId)}}}function D(J,N){let K=J.entries[N];if(!K||K._aliveGen!==J._aliveGen)return;return K}function C(J){return{grid:J,queryBox(N,K,j,O,$,U){let A=[];return I(J,N,K,j,O,$,U,A),A},queryBoxInto(N,K,j,O,$,U,A,M){I(J,N,K,j,O,$,U,A,M)},queryRadius(N,K,j,O){let $=[];return X(J,N,K,j,O,$),$},queryRadiusInto(N,K,j,O,$){X(J,N,K,j,O,$)},getEntry(N){return D(J,N)}}}function x(J){let{cellSize:N=64,systemGroup:K="spatialIndex3D",priority:j=2000,phases:O=["fixedUpdate","postUpdate"]}=J??{},$=b(N),U=C($);return z("spatialIndex3D").withComponentTypes().withResourceTypes().withLabels().withGroups().install((A)=>{A.addResource("spatialIndex3D",U);let M=!1;for(let V of O){let B=V==="fixedUpdate"?"localTransform3D":"worldTransform3D",P=V==="postUpdate";A.addSystem(`spatial-index3D-rebuild-${V}`).setPriority(j).inPhase(V).inGroup(K).addQuery("aabbWith",{with:[B,"aabb3DCollider"]}).addQuery("sphereOnly",{with:[B,"sphereCollider"],without:["aabb3DCollider"]}).runWhenEmpty().setProcess(({queries:q,ecs:W})=>{if(P){let L=M;if(M=!1,L&&!W.entityManager.hasHierarchy)return}p($);for(let L of q.aabbWith){let Q=L.components[B],{aabb3DCollider:F}=L.components;k($,L.id,Q.x+(F.offsetX??0),Q.y+(F.offsetY??0),Q.z+(F.offsetZ??0),F.width/2,F.height/2,F.depth/2)}for(let L of q.sphereOnly){let Q=L.components[B],{sphereCollider:F}=L.components,T=F.radius;k($,L.id,Q.x+(F.offsetX??0),Q.y+(F.offsetY??0),Q.z+(F.offsetZ??0),T,T,T)}if(!P)M=!0})}})}export{x as createSpatialIndex3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=C91D70E729A54E7764756E2164756E21
|
|
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\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
|
|
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"
|
|
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/**\n\t * Phases to register rebuild systems in (default: ['fixedUpdate', 'postUpdate']).\n\t *\n\t * When both phases are registered, the `postUpdate` rebuild is\n\t * automatically skipped on cycles where:\n\t * 1. The `fixedUpdate` rebuild already ran this cycle, AND\n\t * 2. No entity hierarchy exists.\n\t *\n\t * In flat-hierarchy scenes `transform3d-propagation` copies\n\t * `localTransform3D → worldTransform3D` unchanged, so the postUpdate\n\t * grid would duplicate the fixedUpdate one. The skip is automatic\n\t * and re-engages once any parent relationship is set, or whenever\n\t * `fixedUpdate` doesn't run that cycle (sub-fixed-DT frames).\n\t *\n\t * Edge case: if you write to `worldTransform3D` directly between\n\t * phases without using `transform3d-propagation` or entity hierarchy,\n\t * the auto-skip will leave your writes unindexed. Set\n\t * `phases: ['postUpdate']` explicitly to bypass the auto-skip.\n\t */\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// Flag flipped true by the fixedUpdate rebuild; checked + reset by the\n\t\t\t// postUpdate rebuild to detect whether fixedUpdate ran this cycle.\n\t\t\t// Lets postUpdate auto-skip when its grid would duplicate fixedUpdate's\n\t\t\t// in flat-hierarchy scenes — without breaking sub-fixed-DT frames\n\t\t\t// where only postUpdate runs.\n\t\t\tlet fixedRebuildRanThisCycle = false;\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\t\t\t\tconst isPostUpdate = phase === 'postUpdate';\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('aabbWith', {\n\t\t\t\t\t\twith: [transformComponent, 'aabb3DCollider'],\n\t\t\t\t\t})\n\t\t\t\t\t.addQuery('sphereOnly', {\n\t\t\t\t\t\twith: [transformComponent, 'sphereCollider'],\n\t\t\t\t\t\twithout: ['aabb3DCollider'],\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\tif (isPostUpdate) {\n\t\t\t\t\t\t\tconst ranFixed = fixedRebuildRanThisCycle;\n\t\t\t\t\t\t\tfixedRebuildRanThisCycle = false;\n\t\t\t\t\t\t\t// Skip only when fixedUpdate populated the grid this\n\t\t\t\t\t\t\t// cycle AND no hierarchy exists (so worldTransform3D\n\t\t\t\t\t\t\t// equals localTransform3D — identical grid data).\n\t\t\t\t\t\t\tif (ranFixed && !ecs.entityManager.hasHierarchy) return;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tclearGrid3D(grid);\n\n\t\t\t\t\t\t// AABB-precedence: aabbWith covers both aabb-only AND\n\t\t\t\t\t\t// entities carrying both colliders. Sphere-only is its own\n\t\t\t\t\t\t// query. Matches collision3D / physics3D semantic.\n\t\t\t\t\t\tfor (const entity of queries.aabbWith) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst { aabb3DCollider } = entity.components;\n\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\ttransform.x + (aabb3DCollider.offsetX ?? 0),\n\t\t\t\t\t\t\t\ttransform.y + (aabb3DCollider.offsetY ?? 0),\n\t\t\t\t\t\t\t\ttransform.z + (aabb3DCollider.offsetZ ?? 0),\n\t\t\t\t\t\t\t\taabb3DCollider.width / 2, aabb3DCollider.height / 2, aabb3DCollider.depth / 2,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor (const entity of queries.sphereOnly) {\n\t\t\t\t\t\t\tconst transform = entity.components[transformComponent];\n\t\t\t\t\t\t\tconst { sphereCollider } = entity.components;\n\t\t\t\t\t\t\tconst r = sphereCollider.radius;\n\t\t\t\t\t\t\tinsertEntity3D(\n\t\t\t\t\t\t\t\tgrid, entity.id,\n\t\t\t\t\t\t\t\ttransform.x + (sphereCollider.offsetX ?? 0),\n\t\t\t\t\t\t\t\ttransform.y + (sphereCollider.offsetY ?? 0),\n\t\t\t\t\t\t\t\ttransform.z + (sphereCollider.offsetZ ?? 0),\n\t\t\t\t\t\t\t\tr, r, r,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (!isPostUpdate) fixedRebuildRanThisCycle = true;\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\n/**\n * A cell bucket — entries plus the alive-gen at which the bucket was last\n * filled. Buckets are reset lazily on the next insert in a new generation\n * (see `insertEntity3D`); queries skip buckets whose `_gen` is stale.\n *\n * Internal — exposed only through `SpatialHashGrid3D.cells`.\n */\ninterface CellBucket3D extends Array<SpatialEntry3D> {\n\t_gen: number;\n}\n\nexport interface SpatialHashGrid3D {\n\tcellSize: number;\n\tinvCellSize: number;\n\tcells: Map<number, CellBucket3D>;\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 * O(1): bumps the alive-generation counter so entries inserted prior to this\n * call are implicitly stale. `getLiveEntry3D` / `liveEntryCount3D` filter\n * entries by the current gen; queries skip buckets whose own `_gen` lags\n * behind the alive gen; `insertEntity3D` resets a bucket's `length` lazily\n * the first time it is touched in a new generation.\n *\n * Existing `SpatialEntry3D` objects and `CellBucket3D` arrays remain in\n * place for reuse, so steady-state rebuilds allocate zero entries and zero\n * buckets, regardless of how many cells have ever been touched.\n */\nexport function clearGrid3D(grid: SpatialHashGrid3D): void {\n\tgrid._aliveGen++;\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 && bucket._gen === gen) {\n\t\t\t\t\t// Hot path: bucket already populated this generation.\n\t\t\t\t\tbucket.push(entry);\n\t\t\t\t} else if (bucket) {\n\t\t\t\t\t// First touch in this generation — drop stale entries from prior rebuilds.\n\t\t\t\t\tbucket.length = 0;\n\t\t\t\t\tbucket._gen = gen;\n\t\t\t\t\tbucket.push(entry);\n\t\t\t\t} else {\n\t\t\t\t\tconst fresh = [entry] as CellBucket3D;\n\t\t\t\t\tfresh._gen = gen;\n\t\t\t\t\tgrid.cells.set(key, fresh);\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\tconst aliveGen = grid._aliveGen;\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 || bucket._gen !== aliveGen) 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\tconst aliveGen = grid._aliveGen;\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 || bucket._gen !== aliveGen) 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": "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",
|
|
9
|
+
"debugId": "C91D70E729A54E7764756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -59,6 +59,10 @@ export interface QueryConfig<ComponentTypes, WithComponents extends keyof Compon
|
|
|
59
59
|
* entity, catching accidental writes at compile time.
|
|
60
60
|
*/
|
|
61
61
|
mutates?: ReadonlyArray<MutatesComponents>;
|
|
62
|
+
/** @internal Pre-resolved component indices for `changed:`, populated at system registration. */
|
|
63
|
+
_changedIdx?: ReadonlyArray<number>;
|
|
64
|
+
/** @internal Pre-resolved component indices for `mutates:`, populated at system registration. */
|
|
65
|
+
_mutatesIdx?: ReadonlyArray<number>;
|
|
62
66
|
}
|
|
63
67
|
/**
|
|
64
68
|
* Utility type to derive the entity type that would result from a query definition.
|
|
@@ -104,6 +108,10 @@ export type QueryDefinition<ComponentTypes extends Record<string, any>, WithComp
|
|
|
104
108
|
* are narrowed to `Readonly<T>` on the iteration entity type.
|
|
105
109
|
*/
|
|
106
110
|
mutates?: ReadonlyArray<MutatesComponents>;
|
|
111
|
+
/** @internal Pre-resolved component indices for `changed:`, populated at system registration. */
|
|
112
|
+
_changedIdx?: ReadonlyArray<number>;
|
|
113
|
+
/** @internal Pre-resolved component indices for `mutates:`, populated at system registration. */
|
|
114
|
+
_mutatesIdx?: ReadonlyArray<number>;
|
|
107
115
|
};
|
|
108
116
|
/**
|
|
109
117
|
* Helper function to create a query definition with proper type inference.
|
|
@@ -240,7 +248,7 @@ export interface System<Cfg extends WorldConfig = EmptyConfig, WithComponents ex
|
|
|
240
248
|
*/
|
|
241
249
|
_autoMarkPairs?: ReadonlyArray<{
|
|
242
250
|
queryName: string;
|
|
243
|
-
|
|
251
|
+
mutatesIdx: ReadonlyArray<number>;
|
|
244
252
|
kind: 'list' | 'singleton';
|
|
245
253
|
}> | null;
|
|
246
254
|
}
|
|
@@ -15,10 +15,20 @@ export interface SpatialEntry {
|
|
|
15
15
|
/** Rebuild generation when this entry was last inserted. Internal. */
|
|
16
16
|
_aliveGen: number;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* A cell bucket — entries plus the alive-gen at which the bucket was last
|
|
20
|
+
* filled. Buckets are reset lazily on the next insert in a new generation
|
|
21
|
+
* (see `insertEntity`); queries skip buckets whose `_gen` is stale.
|
|
22
|
+
*
|
|
23
|
+
* Internal — exposed only through `SpatialHashGrid.cells`.
|
|
24
|
+
*/
|
|
25
|
+
interface CellBucket extends Array<SpatialEntry> {
|
|
26
|
+
_gen: number;
|
|
27
|
+
}
|
|
18
28
|
export interface SpatialHashGrid {
|
|
19
29
|
cellSize: number;
|
|
20
30
|
invCellSize: number;
|
|
21
|
-
cells: Map<number,
|
|
31
|
+
cells: Map<number, CellBucket>;
|
|
22
32
|
/**
|
|
23
33
|
* Dense, indexed by entityId. Holes are `undefined`. Entries from previous
|
|
24
34
|
* rebuilds remain in place for in-place reuse (zero allocation in steady
|
|
@@ -47,14 +57,15 @@ export declare function createGrid(cellSize: number): SpatialHashGrid;
|
|
|
47
57
|
/**
|
|
48
58
|
* Prepare the grid for a rebuild.
|
|
49
59
|
*
|
|
50
|
-
*
|
|
51
|
-
* are implicitly stale
|
|
52
|
-
* the current gen
|
|
53
|
-
*
|
|
54
|
-
*
|
|
60
|
+
* O(1): bumps the alive-generation counter so entries inserted prior to this
|
|
61
|
+
* call are implicitly stale. `getLiveEntry` / `liveEntryCount` filter
|
|
62
|
+
* entries by the current gen; queries skip buckets whose own `_gen` lags
|
|
63
|
+
* behind the alive gen; `insertEntity` resets a bucket's `length` lazily
|
|
64
|
+
* the first time it is touched in a new generation.
|
|
55
65
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
66
|
+
* Existing `SpatialEntry` objects and `CellBucket` arrays remain in place
|
|
67
|
+
* for reuse, so steady-state rebuilds allocate zero entries and zero
|
|
68
|
+
* buckets, regardless of how many cells have ever been touched.
|
|
58
69
|
*/
|
|
59
70
|
export declare function clearGrid(grid: SpatialHashGrid): void;
|
|
60
71
|
/**
|
|
@@ -96,3 +107,4 @@ export interface SpatialIndex {
|
|
|
96
107
|
queryRadiusInto(cx: number, cy: number, radius: number, result: number[]): void;
|
|
97
108
|
getEntry(entityId: number): SpatialEntry | undefined;
|
|
98
109
|
}
|
|
110
|
+
export {};
|
|
@@ -17,10 +17,20 @@ export interface SpatialEntry3D {
|
|
|
17
17
|
/** Rebuild generation when this entry was last inserted. Internal. */
|
|
18
18
|
_aliveGen: number;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A cell bucket — entries plus the alive-gen at which the bucket was last
|
|
22
|
+
* filled. Buckets are reset lazily on the next insert in a new generation
|
|
23
|
+
* (see `insertEntity3D`); queries skip buckets whose `_gen` is stale.
|
|
24
|
+
*
|
|
25
|
+
* Internal — exposed only through `SpatialHashGrid3D.cells`.
|
|
26
|
+
*/
|
|
27
|
+
interface CellBucket3D extends Array<SpatialEntry3D> {
|
|
28
|
+
_gen: number;
|
|
29
|
+
}
|
|
20
30
|
export interface SpatialHashGrid3D {
|
|
21
31
|
cellSize: number;
|
|
22
32
|
invCellSize: number;
|
|
23
|
-
cells: Map<number,
|
|
33
|
+
cells: Map<number, CellBucket3D>;
|
|
24
34
|
/**
|
|
25
35
|
* Dense, indexed by entityId. Holes are `undefined`. Entries from previous
|
|
26
36
|
* rebuilds remain in place for in-place reuse (zero allocation in steady
|
|
@@ -49,14 +59,15 @@ export declare function createGrid3D(cellSize: number): SpatialHashGrid3D;
|
|
|
49
59
|
/**
|
|
50
60
|
* Prepare the grid for a rebuild.
|
|
51
61
|
*
|
|
52
|
-
*
|
|
53
|
-
* are implicitly stale
|
|
54
|
-
*
|
|
55
|
-
* the `
|
|
56
|
-
*
|
|
62
|
+
* O(1): bumps the alive-generation counter so entries inserted prior to this
|
|
63
|
+
* call are implicitly stale. `getLiveEntry3D` / `liveEntryCount3D` filter
|
|
64
|
+
* entries by the current gen; queries skip buckets whose own `_gen` lags
|
|
65
|
+
* behind the alive gen; `insertEntity3D` resets a bucket's `length` lazily
|
|
66
|
+
* the first time it is touched in a new generation.
|
|
57
67
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
68
|
+
* Existing `SpatialEntry3D` objects and `CellBucket3D` arrays remain in
|
|
69
|
+
* place for reuse, so steady-state rebuilds allocate zero entries and zero
|
|
70
|
+
* buckets, regardless of how many cells have ever been touched.
|
|
60
71
|
*/
|
|
61
72
|
export declare function clearGrid3D(grid: SpatialHashGrid3D): void;
|
|
62
73
|
/**
|
|
@@ -105,3 +116,4 @@ export interface SpatialIndex3D {
|
|
|
105
116
|
queryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: number[]): void;
|
|
106
117
|
getEntry(entityId: number): SpatialEntry3D | undefined;
|
|
107
118
|
}
|
|
119
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ecspresso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -177,6 +177,7 @@
|
|
|
177
177
|
"@types/react-dom": "^19.2.3",
|
|
178
178
|
"@types/three": "^0.184.0",
|
|
179
179
|
"howler": "^2.2.4",
|
|
180
|
+
"phaser": "^4.1.0",
|
|
180
181
|
"pixi.js": "^8.18.1",
|
|
181
182
|
"react": "^19.2.5",
|
|
182
183
|
"react-dom": "^19.2.5",
|
|
@@ -219,7 +220,8 @@
|
|
|
219
220
|
"check:types": "bun tsc --noEmit --skipLibCheck",
|
|
220
221
|
"check": "bun run check:types && bun test",
|
|
221
222
|
"examples": "bun ./examples/serve-examples.ts",
|
|
222
|
-
"bench": "bun bench/narrowphase.bench.ts && bun bench/ecs-physics.bench.ts --spatial && bun bench/ecs-physics.bench.ts --no-spatial",
|
|
223
|
+
"bench": "bun bench/narrowphase.bench.ts && bun bench/ecs-physics.bench.ts --spatial && bun bench/ecs-physics.bench.ts --no-spatial && bun bench/ecs-physics3D.bench.ts --spatial && bun bench/ecs-physics3D.bench.ts --no-spatial",
|
|
224
|
+
"bench:ecs3D": "bun bench/ecs-physics3D.bench.ts",
|
|
223
225
|
"bench:narrowphase": "bun bench/narrowphase.bench.ts",
|
|
224
226
|
"bench:ecs": "bun bench/ecs-physics.bench.ts",
|
|
225
227
|
"docs": "typedoc && bun scripts/build-examples.ts && bun scripts/build-docs-index.ts",
|