ecspresso 0.16.1 → 0.16.3
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/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.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 +63 -16
- package/dist/utils/spatial-hash3D.d.ts +63 -16
- package/package.json +1 -1
|
@@ -22,11 +22,11 @@ import { type BasePluginOptions } from 'ecspresso';
|
|
|
22
22
|
* }
|
|
23
23
|
* ```
|
|
24
24
|
*/
|
|
25
|
-
export interface TimerEventData {
|
|
25
|
+
export interface TimerEventData<Slots extends string = string> {
|
|
26
26
|
/** The entity ID that owns the timer slot */
|
|
27
27
|
entityId: number;
|
|
28
28
|
/** The slot name within the entity's `timers` map */
|
|
29
|
-
slot:
|
|
29
|
+
slot: Slots;
|
|
30
30
|
/** The slot's configured duration in seconds */
|
|
31
31
|
duration: number;
|
|
32
32
|
/** The actual elapsed time (may exceed duration slightly) */
|
|
@@ -36,7 +36,7 @@ export interface TimerEventData {
|
|
|
36
36
|
* A single timer's data. Multiple of these can live on one entity, keyed by slot name.
|
|
37
37
|
* Use `justFinished` to detect completion in your systems.
|
|
38
38
|
*/
|
|
39
|
-
export interface Timer {
|
|
39
|
+
export interface Timer<Slots extends string = string> {
|
|
40
40
|
/** Time accumulated so far (seconds) */
|
|
41
41
|
elapsed: number;
|
|
42
42
|
/** Target duration (seconds) */
|
|
@@ -48,7 +48,7 @@ export interface Timer {
|
|
|
48
48
|
/** True for one frame after the timer completes */
|
|
49
49
|
justFinished: boolean;
|
|
50
50
|
/** Optional callback invoked when the timer completes */
|
|
51
|
-
onComplete?: (data: TimerEventData) => void;
|
|
51
|
+
onComplete?: (data: TimerEventData<Slots>) => void;
|
|
52
52
|
}
|
|
53
53
|
/**
|
|
54
54
|
* Component types provided by the timer plugin.
|
|
@@ -71,14 +71,14 @@ export interface Timer {
|
|
|
71
71
|
* });
|
|
72
72
|
* ```
|
|
73
73
|
*/
|
|
74
|
-
export interface TimerComponentTypes {
|
|
75
|
-
timers: Record<
|
|
74
|
+
export interface TimerComponentTypes<Slots extends string = string> {
|
|
75
|
+
timers: Partial<Record<Slots, Timer<Slots>>>;
|
|
76
76
|
}
|
|
77
77
|
export interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {
|
|
78
78
|
}
|
|
79
|
-
export interface TimerOptions {
|
|
79
|
+
export interface TimerOptions<Slots extends string = string> {
|
|
80
80
|
/** Callback invoked when the timer completes */
|
|
81
|
-
onComplete?: (data: TimerEventData) => void;
|
|
81
|
+
onComplete?: (data: TimerEventData<Slots>) => void;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
84
|
* Create a one-shot `Timer` to drop into a `timers` slot.
|
|
@@ -105,7 +105,7 @@ export interface TimerOptions {
|
|
|
105
105
|
* });
|
|
106
106
|
* ```
|
|
107
107
|
*/
|
|
108
|
-
export declare function createTimer(duration: number, options?: TimerOptions): Timer
|
|
108
|
+
export declare function createTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots>;
|
|
109
109
|
/**
|
|
110
110
|
* Create a repeating `Timer` to drop into a `timers` slot. Fires
|
|
111
111
|
* `justFinished` once per cycle and continues running.
|
|
@@ -118,7 +118,7 @@ export declare function createTimer(duration: number, options?: TimerOptions): T
|
|
|
118
118
|
* });
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
|
-
export declare function createRepeatingTimer(duration: number, options?: TimerOptions): Timer
|
|
121
|
+
export declare function createRepeatingTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots>;
|
|
122
122
|
/**
|
|
123
123
|
* Create a timer plugin for ECSpresso.
|
|
124
124
|
*
|
|
@@ -148,5 +148,30 @@ export declare function createRepeatingTimer(duration: number, options?: TimerOp
|
|
|
148
148
|
* }
|
|
149
149
|
* });
|
|
150
150
|
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* Typed slot names — pass a string-union generic to lock the set of legal
|
|
154
|
+
* slot names. Spawn sites reject typos, autocomplete works on slot access,
|
|
155
|
+
* and `slot` is narrowed in `onComplete` callbacks. Defaults to `string`
|
|
156
|
+
* (any slot name) when omitted.
|
|
157
|
+
*
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const ecs = ECSpresso.create()
|
|
160
|
+
* .withPlugin(createTimerPlugin<'launch' | 'hangarCycle'>())
|
|
161
|
+
* .build();
|
|
162
|
+
*
|
|
163
|
+
* ecs.spawn({ timers: { launch: createTimer(2.0) } }); // ok
|
|
164
|
+
* ecs.spawn({ timers: { typo: createTimer(2.0) } }); // type error
|
|
165
|
+
*
|
|
166
|
+
* createTimer<'launch' | 'hangarCycle'>(1.0, {
|
|
167
|
+
* onComplete: ({ slot }) => {
|
|
168
|
+
* // slot is 'launch' | 'hangarCycle', not string
|
|
169
|
+
* },
|
|
170
|
+
* });
|
|
171
|
+
* ```
|
|
172
|
+
*
|
|
173
|
+
* Only one timer plugin can be installed per world. Feature plugins should
|
|
174
|
+
* re-export their slot union as a type so the app can assemble them:
|
|
175
|
+
* `createTimerPlugin<FighterSlots | CarrierSlots>()`.
|
|
151
176
|
*/
|
|
152
|
-
export declare function createTimerPlugin<G extends string = 'timers'>(options?: TimerPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TimerComponentTypes
|
|
177
|
+
export declare function createTimerPlugin<Slots extends string = string, G extends string = 'timers'>(options?: TimerPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TimerComponentTypes<Slots>>, import("ecspresso").EmptyConfig, "timer-update", G, never, never>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var X=((A)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(A,{get:(B,H)=>(typeof require<"u"?require:B)[H]}):A)(function(A){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+A+'" is not supported')});import{definePlugin as W}from"ecspresso";function _(A,B){return{elapsed:0,duration:A,repeat:!1,active:!0,justFinished:!1,onComplete:B?.onComplete}}function $(A,B){return{elapsed:0,duration:A,repeat:!0,active:!0,justFinished:!1,onComplete:B?.onComplete}}function D(A){let{systemGroup:B="timers",priority:H=0,phase:M="preUpdate"}=A??{};return W("timers").withComponentTypes().withLabels().withGroups().install((N)=>{N.addSystem("timer-update").setPriority(H).inPhase(M).inGroup(B).addQuery("timers",{with:["timers"]}).setProcess(({queries:U,dt:V})=>{for(let J of U.timers){let L=J.components.timers;for(let K in L){let z=L[K];if(!z)continue;if(z.justFinished=!1,!z.active)continue;if(z.elapsed+=V,z.elapsed<z.duration)continue;if(z.repeat)while(z.elapsed>=z.duration)z.justFinished=!0,z.onComplete?.({entityId:J.id,slot:K,duration:z.duration,elapsed:z.elapsed}),z.elapsed-=z.duration;else z.justFinished=!0,z.onComplete?.({entityId:J.id,slot:K,duration:z.duration,elapsed:z.elapsed}),z.active=!1}}})})}export{D as createTimerPlugin,_ as createTimer,$ as createRepeatingTimer};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=5793392D19F4504664756E2164756E21
|
|
4
4
|
//# sourceMappingURL=timers.js.map
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/plugins/scripting/timers.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * Timer Plugin for ECSpresso\n *\n * ECS-native timers as pure data. An entity may carry multiple named timer\n * slots; the plugin's update system ticks every slot each frame and exposes\n * `justFinished` for the frame a slot crosses its duration. The plugin never\n * touches entity lifecycle — callers despawn (or do anything else) themselves\n * by reacting to `justFinished` or in the slot's `onComplete` callback.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data passed to a slot's `onComplete` callback when its timer completes.\n *\n * @example\n * ```typescript\n * timers: {\n * launch: createTimer(1.5, {\n * onComplete: ({ entityId, slot, elapsed }) => {\n * console.log(`Slot ${slot} on entity ${entityId} finished after ${elapsed}s`);\n * },\n * }),\n * }\n * ```\n */\nexport interface TimerEventData {\n\t/** The entity ID that owns the timer slot */\n\tentityId: number;\n\t/** The slot name within the entity's `timers` map */\n\tslot:
|
|
5
|
+
"/**\n * Timer Plugin for ECSpresso\n *\n * ECS-native timers as pure data. An entity may carry multiple named timer\n * slots; the plugin's update system ticks every slot each frame and exposes\n * `justFinished` for the frame a slot crosses its duration. The plugin never\n * touches entity lifecycle — callers despawn (or do anything else) themselves\n * by reacting to `justFinished` or in the slot's `onComplete` callback.\n */\n\nimport { definePlugin, type BasePluginOptions } from 'ecspresso';\n\n// ==================== Event Types ====================\n\n/**\n * Data passed to a slot's `onComplete` callback when its timer completes.\n *\n * @example\n * ```typescript\n * timers: {\n * launch: createTimer(1.5, {\n * onComplete: ({ entityId, slot, elapsed }) => {\n * console.log(`Slot ${slot} on entity ${entityId} finished after ${elapsed}s`);\n * },\n * }),\n * }\n * ```\n */\nexport interface TimerEventData<Slots extends string = string> {\n\t/** The entity ID that owns the timer slot */\n\tentityId: number;\n\t/** The slot name within the entity's `timers` map */\n\tslot: Slots;\n\t/** The slot's configured duration in seconds */\n\tduration: number;\n\t/** The actual elapsed time (may exceed duration slightly) */\n\telapsed: number;\n}\n\n// ==================== Component Types ====================\n\n/**\n * A single timer's data. Multiple of these can live on one entity, keyed by slot name.\n * Use `justFinished` to detect completion in your systems.\n */\nexport interface Timer<Slots extends string = string> {\n\t/** Time accumulated so far (seconds) */\n\telapsed: number;\n\t/** Target duration (seconds) */\n\tduration: number;\n\t/** Whether the timer repeats after completion */\n\trepeat: boolean;\n\t/** Whether the timer is currently running */\n\tactive: boolean;\n\t/** True for one frame after the timer completes */\n\tjustFinished: boolean;\n\t/** Optional callback invoked when the timer completes */\n\tonComplete?: (data: TimerEventData<Slots>) => void;\n}\n\n/**\n * Component types provided by the timer plugin.\n *\n * Each entity carries a single `timers` component whose value is a map of\n * named slots. This lets one entity host independent phase clocks\n * (e.g. `{ launch: ..., shieldDepletion: ..., hangarCycle: ... }`) without\n * one timer's lifecycle constraining another.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ fighter: true }>()\n * .build();\n *\n * ecs.spawn({\n * fighter: true,\n * timers: { launch: createTimer(2.0) },\n * });\n * ```\n */\nexport interface TimerComponentTypes<Slots extends string = string> {\n\ttimers: Partial<Record<Slots, Timer<Slots>>>;\n}\n\n// ==================== Plugin Options ====================\n\nexport interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {}\n\n// ==================== Helper Functions ====================\n\nexport interface TimerOptions<Slots extends string = string> {\n\t/** Callback invoked when the timer completes */\n\tonComplete?: (data: TimerEventData<Slots>) => void;\n}\n\n/**\n * Create a one-shot `Timer` to drop into a `timers` slot.\n *\n * The timer fires `justFinished` for one frame on completion and then idles\n * (`active = false`). The entity is left alone — if the slot's lifetime\n * coincides with the entity's lifetime (vfx, blasts, summon-anim), despawn\n * the host yourself in `onComplete` or in a system that watches `justFinished`.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * fighter: true,\n * timers: { launch: createTimer(2.0) },\n * });\n *\n * // Self-destructing vfx — caller owns the despawn:\n * ecs.spawn({\n * timers: {\n * fade: createTimer(1.0, {\n * onComplete: ({ entityId }) => ecs.commands.removeEntity(entityId),\n * }),\n * },\n * });\n * ```\n */\nexport function createTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots> {\n\treturn {\n\t\telapsed: 0,\n\t\tduration,\n\t\trepeat: false,\n\t\tactive: true,\n\t\tjustFinished: false,\n\t\tonComplete: options?.onComplete,\n\t};\n}\n\n/**\n * Create a repeating `Timer` to drop into a `timers` slot. Fires\n * `justFinished` once per cycle and continues running.\n *\n * @example\n * ```typescript\n * ecs.spawn({\n * carrier: true,\n * timers: { hangarCycle: createRepeatingTimer(5.0) },\n * });\n * ```\n */\nexport function createRepeatingTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots> {\n\treturn {\n\t\telapsed: 0,\n\t\tduration,\n\t\trepeat: true,\n\t\tactive: true,\n\t\tjustFinished: false,\n\t\tonComplete: options?.onComplete,\n\t};\n}\n\n// ==================== Plugin Factory ====================\n\n/**\n * Create a timer plugin for ECSpresso.\n *\n * The plugin installs one update system that ticks every slot of every\n * `timers` component each frame. It does not touch entity lifecycle —\n * react to `justFinished` (or use `onComplete`) and despawn yourself if needed.\n *\n * @example\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin())\n * .withComponentTypes<{ spawner: true }>()\n * .build();\n *\n * ecs.spawn({\n * spawner: true,\n * timers: { wave: createRepeatingTimer(5.0) },\n * });\n *\n * ecs.addSystem('spawn-on-timer')\n * .addQuery('spawners', { with: ['timers', 'spawner'] })\n * .setProcess(({ queries, ecs }) => {\n * for (const { components } of queries.spawners) {\n * if (components.timers.wave?.justFinished) {\n * ecs.spawn({ enemy: true });\n * }\n * }\n * });\n * ```\n *\n * @example\n * Typed slot names — pass a string-union generic to lock the set of legal\n * slot names. Spawn sites reject typos, autocomplete works on slot access,\n * and `slot` is narrowed in `onComplete` callbacks. Defaults to `string`\n * (any slot name) when omitted.\n *\n * ```typescript\n * const ecs = ECSpresso.create()\n * .withPlugin(createTimerPlugin<'launch' | 'hangarCycle'>())\n * .build();\n *\n * ecs.spawn({ timers: { launch: createTimer(2.0) } }); // ok\n * ecs.spawn({ timers: { typo: createTimer(2.0) } }); // type error\n *\n * createTimer<'launch' | 'hangarCycle'>(1.0, {\n * onComplete: ({ slot }) => {\n * // slot is 'launch' | 'hangarCycle', not string\n * },\n * });\n * ```\n *\n * Only one timer plugin can be installed per world. Feature plugins should\n * re-export their slot union as a type so the app can assemble them:\n * `createTimerPlugin<FighterSlots | CarrierSlots>()`.\n */\nexport function createTimerPlugin<\n\tSlots extends string = string,\n\tG extends string = 'timers',\n>(\n\toptions?: TimerPluginOptions<G>\n) {\n\tconst {\n\t\tsystemGroup = 'timers',\n\t\tpriority = 0,\n\t\tphase = 'preUpdate',\n\t} = options ?? {};\n\n\treturn definePlugin('timers')\n\t\t.withComponentTypes<TimerComponentTypes<Slots>>()\n\t\t.withLabels<'timer-update'>()\n\t\t.withGroups<G>()\n\t\t.install((world) => {\n\t\t\tworld\n\t\t\t\t.addSystem('timer-update')\n\t\t\t\t.setPriority(priority)\n\t\t\t\t.inPhase(phase)\n\t\t\t\t.inGroup(systemGroup)\n\t\t\t\t.addQuery('timers', { with: ['timers'] })\n\t\t\t\t.setProcess(({ queries, dt }) => {\n\t\t\t\t\tfor (const entity of queries.timers) {\n\t\t\t\t\t\tconst slots = entity.components.timers;\n\t\t\t\t\t\tfor (const slot in slots) {\n\t\t\t\t\t\t\tconst timer = slots[slot];\n\t\t\t\t\t\t\tif (!timer) continue;\n\n\t\t\t\t\t\t\ttimer.justFinished = false;\n\t\t\t\t\t\t\tif (!timer.active) continue;\n\n\t\t\t\t\t\t\ttimer.elapsed += dt;\n\t\t\t\t\t\t\tif (timer.elapsed < timer.duration) continue;\n\n\t\t\t\t\t\t\tif (timer.repeat) {\n\t\t\t\t\t\t\t\twhile (timer.elapsed >= timer.duration) {\n\t\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\t\ttimer.onComplete?.({\n\t\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\t\t\t\tduration: timer.duration,\n\t\t\t\t\t\t\t\t\t\telapsed: timer.elapsed,\n\t\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\t\ttimer.elapsed -= timer.duration;\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\ttimer.justFinished = true;\n\t\t\t\t\t\t\t\ttimer.onComplete?.({\n\t\t\t\t\t\t\t\t\tentityId: entity.id,\n\t\t\t\t\t\t\t\t\tslot,\n\t\t\t\t\t\t\t\t\tduration: timer.duration,\n\t\t\t\t\t\t\t\t\telapsed: timer.elapsed,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t\ttimer.active = false;\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\t});\n\t\t});\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "2PAUA,uBAAS,kBA+GF,SAAS,
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": "2PAUA,uBAAS,kBA+GF,SAAS,CAA0C,CAAC,EAAkB,EAA6C,CACzH,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,EAeM,SAAS,CAAmD,CAAC,EAAkB,EAA6C,CAClI,MAAO,CACN,QAAS,EACT,WACA,OAAQ,GACR,OAAQ,GACR,aAAc,GACd,WAAY,GAAS,UACtB,EA4DM,SAAS,CAGf,CACA,EACC,CACD,IACC,cAAc,SACd,WAAW,EACX,QAAQ,aACL,GAAW,CAAC,EAEhB,OAAO,EAAa,QAAQ,EAC1B,mBAA+C,EAC/C,WAA2B,EAC3B,WAAc,EACd,QAAQ,CAAC,IAAU,CACnB,EACE,UAAU,cAAc,EACxB,YAAY,CAAQ,EACpB,QAAQ,CAAK,EACb,QAAQ,CAAW,EACnB,SAAS,SAAU,CAAE,KAAM,CAAC,QAAQ,CAAE,CAAC,EACvC,WAAW,EAAG,UAAS,QAAS,CAChC,QAAW,KAAU,EAAQ,OAAQ,CACpC,IAAM,EAAQ,EAAO,WAAW,OAChC,QAAW,KAAQ,EAAO,CACzB,IAAM,EAAQ,EAAM,GACpB,GAAI,CAAC,EAAO,SAGZ,GADA,EAAM,aAAe,GACjB,CAAC,EAAM,OAAQ,SAGnB,GADA,EAAM,SAAW,EACb,EAAM,QAAU,EAAM,SAAU,SAEpC,GAAI,EAAM,OACT,MAAO,EAAM,SAAW,EAAM,SAC7B,EAAM,aAAe,GACrB,EAAM,aAAa,CAClB,SAAU,EAAO,GACjB,OACA,SAAU,EAAM,SAChB,QAAS,EAAM,OAChB,CAAC,EACD,EAAM,SAAW,EAAM,SAGxB,OAAM,aAAe,GACrB,EAAM,aAAa,CAClB,SAAU,EAAO,GACjB,OACA,SAAU,EAAM,SAChB,QAAS,EAAM,OAChB,CAAC,EACD,EAAM,OAAS,KAIlB,EACF",
|
|
8
|
+
"debugId": "5793392D19F4504664756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var v=((j)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(j,{get:(A,B)=>(typeof require<"u"?require:A)[B]}):j)(function(j){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+j+'" is not supported')});import{definePlugin as k}from"ecspresso";function w(j,A){return j*73856093^A*19349663}function R(j){return{cellSize:j,invCellSize:1/j,cells:new Map,entries:[],_aliveGen:0,_queryGen:0}}function S(j){j._aliveGen++}function H(j,A,B,D,J,O){let U=j._aliveGen,T=j.entries[A],V;if(T)T.x=B,T.y=D,T.halfW=J,T.halfH=O,T._aliveGen=U,V=T;else V={entityId:A,x:B,y:D,halfW:J,halfH:O,_lastSeenGen:0,_aliveGen:U},j.entries[A]=V;let Z=j.invCellSize,E=Math.floor((B-J)*Z),L=Math.floor((B+J)*Z),_=Math.floor((D-O)*Z),$=Math.floor((D+O)*Z);for(let K=E;K<=L;K++)for(let M=_;M<=$;M++){let F=w(K,M),N=j.cells.get(F);if(N&&N._gen===U)N.push(V);else if(N)N.length=0,N._gen=U,N.push(V);else{let P=[V];P._gen=U,j.cells.set(F,P)}}}function Q(j,A,B,D,J,O,U=-1){let T=j.invCellSize,V=Math.floor(A*T),Z=Math.floor(D*T),E=Math.floor(B*T),L=Math.floor(J*T),_=++j._queryGen,$=j._aliveGen;for(let K=V;K<=Z;K++)for(let M=E;M<=L;M++){let F=j.cells.get(w(K,M));if(!F||F._gen!==$)continue;for(let N of F){if(N.entityId<=U||N._lastSeenGen===_)continue;N._lastSeenGen=_,O.push(N.entityId)}}}function z(j,A,B,D,J){let O=D*D,U=j.invCellSize,T=Math.floor((A-D)*U),V=Math.floor((A+D)*U),Z=Math.floor((B-D)*U),E=Math.floor((B+D)*U),L=++j._queryGen,_=j._aliveGen;for(let $=T;$<=V;$++)for(let K=Z;K<=E;K++){let M=j.cells.get(w($,K));if(!M||M._gen!==_)continue;for(let F of M){if(F._lastSeenGen===L)continue;F._lastSeenGen=L;let N=Math.max(F.x-F.halfW,Math.min(A,F.x+F.halfW)),P=Math.max(F.y-F.halfH,Math.min(B,F.y+F.halfH)),q=A-N,G=B-P;if(q*q+G*G<=O)J.push(F.entityId)}}}function W(j,A){let B=j.entries[A];if(!B||B._aliveGen!==j._aliveGen)return;return B}function p(j){return{grid:j,queryRect(A,B,D,J){let O=[];return Q(j,A,B,D,J,O),O},queryRectInto(A,B,D,J,O,U){Q(j,A,B,D,J,O,U)},queryRadius(A,B,D){let J=[];return z(j,A,B,D,J),J},queryRadiusInto(A,B,D,J){z(j,A,B,D,J)},getEntry(A){return W(j,A)}}}function b(j){let{cellSize:A=64,systemGroup:B="spatialIndex",priority:D=2000,phases:J=["fixedUpdate","postUpdate"]}=j??{},O=R(A),U=p(O);return k("spatialIndex").withComponentTypes().withResourceTypes().withLabels().withGroups().install((T)=>{T.addResource("spatialIndex",U);for(let V of J){let Z=V==="fixedUpdate"?"localTransform":"worldTransform";T.addSystem(`spatial-index-rebuild-${V}`).setPriority(D).inPhase(V).inGroup(B).addQuery("transforms",{with:[Z]}).setProcess(({queries:E,ecs:L})=>{S(O);for(let _ of E.transforms){let $=_.components[Z],K=L.getComponent(_.id,"aabbCollider"),M=L.getComponent(_.id,"circleCollider");if(!K&&!M)continue;let{x:F,y:N}=$,P=0,q=0;if(K)F+=K.offsetX??0,N+=K.offsetY??0,P=K.width/2,q=K.height/2;if(M)F+=M.offsetX??0,N+=M.offsetY??0,P=Math.max(P,M.radius),q=Math.max(q,M.radius);H(O,_.id,F,N,P,q)}})}})}export{b as createSpatialIndexPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=9F6AD37F3DB1C43164756E2164756E21
|
|
4
4
|
//# sourceMappingURL=spatial-index.js.map
|
|
@@ -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\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": "9F6AD37F3DB1C43164756E2164756E21",
|
|
10
10
|
"names": []
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var
|
|
1
|
+
var z=((F)=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(F,{get:(K,J)=>(typeof require<"u"?require:K)[J]}):F)(function(F){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+F+'" is not supported')});import{definePlugin as b}from"ecspresso";function H(F,K,J){return F*73856093^K*19349663^J*83492791}function C(F){return{cellSize:F,invCellSize:1/F,cells:new Map,entries:[],_aliveGen:0,_queryGen:0}}function D(F){F._aliveGen++}function k(F,K,J,U,M,O,L,V){let $=F._aliveGen,_=F.entries[K],A;if(_)_.x=J,_.y=U,_.z=M,_.halfW=O,_.halfH=L,_.halfD=V,_._aliveGen=$,A=_;else A={entityId:K,x:J,y:U,z:M,halfW:O,halfH:L,halfD:V,_lastSeenGen:0,_aliveGen:$},F.entries[K]=A;let P=F.invCellSize,B=Math.floor((J-O)*P),R=Math.floor((J+O)*P),j=Math.floor((U-L)*P),E=Math.floor((U+L)*P),q=Math.floor((M-V)*P),S=Math.floor((M+V)*P);for(let Q=B;Q<=R;Q++)for(let G=j;G<=E;G++)for(let N=q;N<=S;N++){let w=H(Q,G,N),T=F.cells.get(w);if(T&&T._gen===$)T.push(A);else if(T)T.length=0,T._gen=$,T.push(A);else{let W=[A];W._gen=$,F.cells.set(w,W)}}}function I(F,K,J,U,M,O,L,V,$=-1){let _=F.invCellSize,A=Math.floor(K*_),P=Math.floor(M*_),B=Math.floor(J*_),R=Math.floor(O*_),j=Math.floor(U*_),E=Math.floor(L*_),q=++F._queryGen,S=F._aliveGen;for(let Q=A;Q<=P;Q++)for(let G=B;G<=R;G++)for(let N=j;N<=E;N++){let w=F.cells.get(H(Q,G,N));if(!w||w._gen!==S)continue;for(let T of w){if(T.entityId<=$||T._lastSeenGen===q)continue;T._lastSeenGen=q,V.push(T.entityId)}}}function X(F,K,J,U,M,O){let L=M*M,V=F.invCellSize,$=Math.floor((K-M)*V),_=Math.floor((K+M)*V),A=Math.floor((J-M)*V),P=Math.floor((J+M)*V),B=Math.floor((U-M)*V),R=Math.floor((U+M)*V),j=++F._queryGen,E=F._aliveGen;for(let q=$;q<=_;q++)for(let S=A;S<=P;S++)for(let Q=B;Q<=R;Q++){let G=F.cells.get(H(q,S,Q));if(!G||G._gen!==E)continue;for(let N of G){if(N._lastSeenGen===j)continue;N._lastSeenGen=j;let w=Math.max(N.x-N.halfW,Math.min(K,N.x+N.halfW)),T=Math.max(N.y-N.halfH,Math.min(J,N.y+N.halfH)),W=Math.max(N.z-N.halfD,Math.min(U,N.z+N.halfD)),Y=K-w,Z=J-T,v=U-W;if(Y*Y+Z*Z+v*v<=L)O.push(N.entityId)}}}function p(F,K){let J=F.entries[K];if(!J||J._aliveGen!==F._aliveGen)return;return J}function f(F){return{grid:F,queryBox(K,J,U,M,O,L){let V=[];return I(F,K,J,U,M,O,L,V),V},queryBoxInto(K,J,U,M,O,L,V,$){I(F,K,J,U,M,O,L,V,$)},queryRadius(K,J,U,M){let O=[];return X(F,K,J,U,M,O),O},queryRadiusInto(K,J,U,M,O){X(F,K,J,U,M,O)},getEntry(K){return p(F,K)}}}function m(F){let{cellSize:K=64,systemGroup:J="spatialIndex3D",priority:U=2000,phases:M=["fixedUpdate","postUpdate"]}=F??{},O=C(K),L=f(O);return b("spatialIndex3D").withComponentTypes().withResourceTypes().withLabels().withGroups().install((V)=>{V.addResource("spatialIndex3D",L);for(let $ of M){let _=$==="fixedUpdate"?"localTransform3D":"worldTransform3D";V.addSystem(`spatial-index3D-rebuild-${$}`).setPriority(U).inPhase($).inGroup(J).addQuery("transforms",{with:[_]}).runWhenEmpty().setProcess(({queries:A,ecs:P})=>{D(O);for(let B of A.transforms){let R=B.components[_],j=P.getComponent(B.id,"aabb3DCollider"),E=j?void 0:P.getComponent(B.id,"sphereCollider");if(j){k(O,B.id,R.x+(j.offsetX??0),R.y+(j.offsetY??0),R.z+(j.offsetZ??0),j.width/2,j.height/2,j.depth/2);continue}if(E){let q=E.radius;k(O,B.id,R.x+(E.offsetX??0),R.y+(E.offsetY??0),R.z+(E.offsetZ??0),q,q,q)}}})}})}export{m as createSpatialIndex3DPlugin};
|
|
2
2
|
|
|
3
|
-
//# debugId=
|
|
3
|
+
//# debugId=FC8F8AAE90676C3164756E2164756E21
|
|
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, number[]>;\n\tentries: Map<number, SpatialEntry3D>;\n\t/** Previous-frame entries held for in-place reuse during rebuild. Internal. */\n\t_entriesPrev: Map<number, SpatialEntry3D>;\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: new Map(),\n\t\t_entriesPrev: new Map(),\n\t};\n}\n\n/**\n * Prepare the grid for a rebuild.\n *\n * Swaps `entries` with `_entriesPrev` so `insertEntity3D` can reuse existing\n * `SpatialEntry3D` objects in place (steady-state rebuilds allocate zero\n * entries). Any stale entries left in `_entriesPrev` from the previous\n * rebuild are dropped here.\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._entriesPrev.clear();\n\tconst tmp = grid.entries;\n\tgrid.entries = grid._entriesPrev;\n\tgrid._entriesPrev = tmp;\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 recycled = grid._entriesPrev.get(entityId);\n\tif (recycled) {\n\t\tgrid._entriesPrev.delete(entityId);\n\t\trecycled.x = x;\n\t\trecycled.y = y;\n\t\trecycled.z = z;\n\t\trecycled.halfW = halfW;\n\t\trecycled.halfH = halfH;\n\t\trecycled.halfD = halfD;\n\t\tgrid.entries.set(entityId, recycled);\n\t} else {\n\t\tgrid.entries.set(entityId, { entityId, x, y, z, halfW, halfH, halfD });\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(entityId);\n\t\t\t\t} else {\n\t\t\t\t\tgrid.cells.set(key, [entityId]);\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 */\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: Set<number>,\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\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 id of bucket) result.add(id);\n\t\t\t}\n\t\t}\n\t}\n}\n\n// Module-scoped reusable set to reduce GC pressure\nconst _radiusCandidates3D = new Set<number>();\n\n/**\n * Collect entity IDs within a sphere. Uses box broadphase then\n * 3D AABB-to-point distance filter.\n */\nexport function gridQueryRadius3D(\n\tgrid: SpatialHashGrid3D,\n\tcx: number,\n\tcy: number,\n\tcz: number,\n\tradius: number,\n\tresult: Set<number>,\n): void {\n\tconst candidates = _radiusCandidates3D;\n\tcandidates.clear();\n\tgridQueryBox3D(\n\t\tgrid,\n\t\tcx - radius, cy - radius, cz - radius,\n\t\tcx + radius, cy + radius, cz + radius,\n\t\tcandidates,\n\t);\n\n\tconst rSq = radius * radius;\n\n\tfor (const entityId of candidates) {\n\t\tconst entry = grid.entries.get(entityId);\n\t\tif (!entry) continue;\n\n\t\t// Closest point on entity AABB to query center\n\t\tconst closestX = Math.max(entry.x - entry.halfW, Math.min(cx, entry.x + entry.halfW));\n\t\tconst closestY = Math.max(entry.y - entry.halfH, Math.min(cy, entry.y + entry.halfH));\n\t\tconst closestZ = Math.max(entry.z - entry.halfD, Math.min(cz, entry.z + entry.halfD));\n\t\tconst dx = cx - closestX;\n\t\tconst dy = cy - closestY;\n\t\tconst dz = cz - closestZ;\n\n\t\tif (dx * dx + dy * dy + dz * dz <= rSq) {\n\t\t\tresult.add(entityId);\n\t\t}\n\t}\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: Set<number>): void;\n\tqueryRadius(cx: number, cy: number, cz: number, radius: number): number[];\n\tqueryRadiusInto(cx: number, cy: number, cz: number, radius: number, result: Set<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/** 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\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": "2PAYA,uBAAS,kBCiDF,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,EAgBM,SAAS,CAAW,CAAC,EAA+B,CAC1D,EAAK,YAMC,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,GAAU,EAAO,OAAS,EAE7B,EAAO,KAAK,CAAK,EACX,QAAI,EAEV,EAAO,OAAS,EAChB,EAAO,KAAO,EACd,EAAO,KAAK,CAAK,EACX,KACN,IAAM,EAAQ,CAAC,CAAK,EACpB,EAAM,KAAO,EACb,EAAK,MAAM,IAAI,EAAK,CAAK,IAiBvB,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,UACb,EAAW,EAAK,UAEtB,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,GAAU,EAAO,OAAS,EAAU,SACzC,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,UACb,EAAW,EAAK,UAEtB,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,GAAU,EAAO,OAAS,EAAU,SACzC,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,EDtMR,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": "FC8F8AAE90676C3164756E2164756E21",
|
|
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
|