@vulfram/engine 0.5.8-alpha → 0.17.1-alpha

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.
Files changed (58) hide show
  1. package/README.md +106 -0
  2. package/package.json +55 -4
  3. package/src/core.ts +14 -0
  4. package/src/ecs.ts +1 -0
  5. package/src/engine/api.ts +234 -23
  6. package/src/engine/bridge/dispatch.ts +265 -40
  7. package/src/engine/bridge/guards.ts +4 -1
  8. package/src/engine/bridge/protocol.ts +72 -54
  9. package/src/engine/ecs/index.ts +187 -42
  10. package/src/engine/state.ts +133 -2
  11. package/src/engine/systems/command-intent.ts +153 -3
  12. package/src/engine/systems/constraint-solve.ts +167 -0
  13. package/src/engine/systems/core-command-builder.ts +9 -265
  14. package/src/engine/systems/diagnostics.ts +20 -19
  15. package/src/engine/systems/index.ts +3 -1
  16. package/src/engine/systems/input-mirror.ts +101 -3
  17. package/src/engine/systems/resource-upload.ts +96 -44
  18. package/src/engine/systems/response-decode.ts +69 -15
  19. package/src/engine/systems/scene-sync.ts +306 -0
  20. package/src/engine/systems/ui-bridge.ts +360 -0
  21. package/src/engine/systems/utils.ts +43 -1
  22. package/src/engine/systems/world-lifecycle.ts +72 -103
  23. package/src/engine/window/manager.ts +168 -0
  24. package/src/engine/world/entities.ts +931 -33
  25. package/src/engine/world/mount.ts +174 -0
  26. package/src/engine/world/types.ts +71 -0
  27. package/src/engine/world/world-ui.ts +266 -0
  28. package/src/engine/world/world3d.ts +280 -0
  29. package/src/index.ts +30 -1
  30. package/src/mount.ts +2 -0
  31. package/src/types/cmds/audio.ts +189 -0
  32. package/src/types/cmds/camera.ts +18 -13
  33. package/src/types/cmds/environment.ts +47 -4
  34. package/src/types/cmds/geometry.ts +18 -16
  35. package/src/types/cmds/index.ts +203 -132
  36. package/src/types/cmds/light.ts +17 -13
  37. package/src/types/cmds/material.ts +14 -13
  38. package/src/types/cmds/model.ts +40 -16
  39. package/src/types/cmds/realm.ts +25 -0
  40. package/src/types/cmds/render-graph.ts +49 -0
  41. package/src/types/cmds/resources.ts +4 -0
  42. package/src/types/cmds/shadow.ts +7 -7
  43. package/src/types/cmds/system.ts +29 -0
  44. package/src/types/cmds/target.ts +82 -0
  45. package/src/types/cmds/texture.ts +19 -5
  46. package/src/types/cmds/ui.ts +220 -0
  47. package/src/types/cmds/window.ts +41 -204
  48. package/src/types/events/index.ts +4 -1
  49. package/src/types/events/pointer.ts +42 -13
  50. package/src/types/events/system.ts +150 -7
  51. package/src/types/events/ui.ts +21 -0
  52. package/src/types/index.ts +1 -0
  53. package/src/types/json.ts +15 -0
  54. package/src/types/kinds.ts +3 -0
  55. package/src/window.ts +8 -0
  56. package/src/world-ui.ts +2 -0
  57. package/src/world3d.ts +10 -0
  58. package/tsconfig.json +0 -29
@@ -0,0 +1,167 @@
1
+ import { mat4 } from 'gl-matrix';
2
+ import type { ParentComponent, System, TransformComponent } from '../ecs';
3
+ import { mat4EqualsApprox, getEntityLocalTransformMatrix } from './utils';
4
+
5
+ type ConstraintStrategyContext = {
6
+ worldId: number;
7
+ entityId: number;
8
+ };
9
+
10
+ interface ConstraintStrategy {
11
+ name: string;
12
+ apply(a: mat4, b: mat4, context: ConstraintStrategyContext): mat4;
13
+ }
14
+
15
+ const ParentConstraintStrategy: ConstraintStrategy = {
16
+ name: 'parent',
17
+ apply(a, b) {
18
+ const out = mat4.create();
19
+ mat4.multiply(out, a, b);
20
+ return out;
21
+ },
22
+ };
23
+
24
+ type ResolveState = {
25
+ worldId: number;
26
+ world: Parameters<System>[0];
27
+ affected: Set<number>;
28
+ resolved: Map<number, mat4>;
29
+ visiting: Set<number>;
30
+ cycleLogged: boolean;
31
+ };
32
+
33
+ function resolveEntityMatrix(state: ResolveState, entityId: number): mat4 {
34
+ if (!state.affected.has(entityId)) {
35
+ const existing = state.world.resolvedEntityTransforms.get(entityId);
36
+ if (existing) {
37
+ return existing as unknown as mat4;
38
+ }
39
+ }
40
+
41
+ const cached = state.resolved.get(entityId);
42
+ if (cached) {
43
+ return cached;
44
+ }
45
+
46
+ if (state.visiting.has(entityId)) {
47
+ if (!state.cycleLogged) {
48
+ console.error(
49
+ `[World ${state.worldId}] Constraint cycle detected in parent hierarchy. Falling back to local transforms for cyclic nodes.`,
50
+ );
51
+ state.cycleLogged = true;
52
+ }
53
+ return getEntityLocalTransformMatrix(state.world, entityId);
54
+ }
55
+
56
+ state.visiting.add(entityId);
57
+ const local = getEntityLocalTransformMatrix(state.world, entityId);
58
+ const store = state.world.components.get(entityId);
59
+ const parent = store?.get('Parent') as ParentComponent | undefined;
60
+
61
+ let resolved = local;
62
+ if (parent) {
63
+ const parentStore = state.world.components.get(parent.parentId);
64
+ const parentTransform = parentStore?.get('Transform') as
65
+ | TransformComponent
66
+ | undefined;
67
+ if (parentTransform) {
68
+ const parentMatrix = resolveEntityMatrix(state, parent.parentId);
69
+ resolved = ParentConstraintStrategy.apply(parentMatrix, local, {
70
+ worldId: state.worldId,
71
+ entityId,
72
+ });
73
+ }
74
+ }
75
+
76
+ state.visiting.delete(entityId);
77
+ state.resolved.set(entityId, resolved);
78
+ return resolved;
79
+ }
80
+
81
+ function collectAffectedEntities(world: Parameters<System>[0]): Set<number> {
82
+ const affected = new Set<number>();
83
+ const queue: number[] = [];
84
+
85
+ for (const entityId of world.constraintDirtyEntities) {
86
+ affected.add(entityId);
87
+ queue.push(entityId);
88
+ }
89
+
90
+ for (let i = 0; i < queue.length; i++) {
91
+ const parentId = queue[i]!;
92
+ const children = world.constraintChildrenByParent.get(parentId);
93
+ if (!children) continue;
94
+ for (const childId of children) {
95
+ if (affected.has(childId)) continue;
96
+ affected.add(childId);
97
+ queue.push(childId);
98
+ }
99
+ }
100
+
101
+ return affected;
102
+ }
103
+
104
+ /**
105
+ * Resolves transform constraints into world matrices.
106
+ *
107
+ * Current built-in strategy:
108
+ * - `parent`: composes parent world matrix with child local matrix (`A * B`).
109
+ *
110
+ * The solver walks dependencies from higher nodes to leaves and caches each
111
+ * resolved entity matrix so render sync can emit final transforms to core.
112
+ */
113
+ export const ConstraintSolveSystem: System = (world, context) => {
114
+ if (world.constraintDirtyEntities.size === 0) {
115
+ world.constraintChangedEntities.clear();
116
+ return;
117
+ }
118
+
119
+ const matrices = world.resolvedEntityTransforms;
120
+ const changed = world.constraintChangedEntities;
121
+ changed.clear();
122
+ const affected = collectAffectedEntities(world);
123
+
124
+ const resolveState: ResolveState = {
125
+ worldId: context.worldId,
126
+ world,
127
+ affected,
128
+ resolved: world.constraintScratchResolved,
129
+ visiting: world.constraintScratchVisiting,
130
+ cycleLogged: false,
131
+ };
132
+ resolveState.resolved.clear();
133
+ resolveState.visiting.clear();
134
+
135
+ for (const entityId of affected) {
136
+ if (!world.entities.has(entityId)) {
137
+ if (matrices.delete(entityId)) {
138
+ changed.add(entityId);
139
+ }
140
+ continue;
141
+ }
142
+
143
+ const store = world.components.get(entityId);
144
+ const transform = store?.get('Transform') as TransformComponent | undefined;
145
+ if (!transform) {
146
+ if (matrices.delete(entityId)) {
147
+ changed.add(entityId);
148
+ }
149
+ continue;
150
+ }
151
+
152
+ const resolved = resolveEntityMatrix(resolveState, entityId);
153
+ const previousSnapshot = matrices.get(entityId);
154
+ if (!previousSnapshot) {
155
+ matrices.set(entityId, new Float32Array(resolved));
156
+ changed.add(entityId);
157
+ continue;
158
+ }
159
+
160
+ if (!mat4EqualsApprox(previousSnapshot, resolved)) {
161
+ previousSnapshot.set(resolved);
162
+ changed.add(entityId);
163
+ }
164
+ }
165
+
166
+ world.constraintDirtyEntities.clear();
167
+ };
@@ -1,265 +1,9 @@
1
- import { mat4, vec3 } from 'gl-matrix';
2
- import type { CameraKind, LightKind } from '../../types/kinds';
3
- import { enqueueCommand } from '../bridge/dispatch';
4
- import type {
5
- CameraComponent,
6
- Component,
7
- LightComponent,
8
- ModelComponent,
9
- System,
10
- TransformComponent,
11
- } from '../ecs';
12
- import { getEntityTransformMatrix, toQuat, toVec2, toVec3, toVec4 } from './utils';
13
-
14
- export const CoreCommandBuilderSystem: System = (world, context) => {
15
- const intentsToRemove: number[] = [];
16
-
17
- for (let i = 0; i < world.pendingIntents.length; i++) {
18
- const intent = world.pendingIntents[i];
19
- if (!intent) continue;
20
-
21
- if (intent.type === 'attach-model') {
22
- const modelId = world.nextCoreId++;
23
- const transform = getEntityTransformMatrix(world, intent.entityId);
24
- const castShadow = intent.props.castShadow ?? true;
25
- const receiveShadow = intent.props.receiveShadow ?? true;
26
-
27
- enqueueCommand(context.worldId, 'cmd-model-create', {
28
- windowId: context.worldId,
29
- modelId,
30
- geometryId: intent.props.geometryId,
31
- materialId: intent.props.materialId,
32
- transform: Array.from(transform),
33
- layerMask: 0x7fffffff,
34
- castShadow,
35
- receiveShadow,
36
- });
37
-
38
- let store = world.components.get(intent.entityId);
39
- if (!store) {
40
- store = new Map();
41
- world.components.set(intent.entityId, store);
42
- }
43
- store.set('Model', {
44
- type: 'Model',
45
- id: modelId,
46
- geometryId: intent.props.geometryId,
47
- materialId: intent.props.materialId,
48
- castShadow: intent.props.castShadow ?? true,
49
- receiveShadow: intent.props.receiveShadow ?? true,
50
- skipUpdate: true,
51
- });
52
-
53
- intentsToRemove.push(i);
54
- } else if (intent.type === 'attach-camera') {
55
- const cameraId = world.nextCoreId++;
56
- const transform = getEntityTransformMatrix(world, intent.entityId);
57
-
58
- enqueueCommand(context.worldId, 'cmd-camera-create', {
59
- windowId: context.worldId,
60
- cameraId,
61
- label: `Cam ${cameraId}`,
62
- kind: intent.props.kind ?? ('perspective' as CameraKind),
63
- flags: 0,
64
- nearFar: [intent.props.near ?? 0.1, intent.props.far ?? 1000],
65
- order: intent.props.order ?? 0,
66
- transform: Array.from(transform),
67
- layerMask: 0x7fffffff,
68
- orthoScale: 1.0,
69
- });
70
-
71
- let store = world.components.get(intent.entityId);
72
- if (!store) {
73
- store = new Map();
74
- world.components.set(intent.entityId, store);
75
- }
76
- store.set('Camera', {
77
- type: 'Camera',
78
- id: cameraId,
79
- kind: intent.props.kind ?? ('perspective' as CameraKind),
80
- near: intent.props.near ?? 0.1,
81
- far: intent.props.far ?? 1000,
82
- order: intent.props.order ?? 0,
83
- orthoScale: 1.0,
84
- skipUpdate: true,
85
- });
86
-
87
- intentsToRemove.push(i);
88
- } else if (intent.type === 'attach-light') {
89
- const lightId = world.nextCoreId++;
90
- const transform = getEntityTransformMatrix(world, intent.entityId);
91
-
92
- const pos = vec3.create();
93
- mat4.getTranslation(pos, transform);
94
- const direction = intent.props.direction
95
- ? toVec3(intent.props.direction)
96
- : ([0, 0, -1] as [number, number, number]);
97
- const color = intent.props.color
98
- ? toVec3(intent.props.color)
99
- : ([1, 1, 1] as [number, number, number]);
100
-
101
- const lightCmd: {
102
- windowId: number;
103
- lightId: number;
104
- kind: LightKind;
105
- color: [number, number, number, number];
106
- intensity: number;
107
- range: number;
108
- castShadow: boolean;
109
- position: [number, number, number, number];
110
- layerMask: number;
111
- direction?: [number, number, number, number];
112
- spotInnerOuter?: [number, number];
113
- } = {
114
- windowId: context.worldId,
115
- lightId,
116
- kind: intent.props.kind ?? ('directional' as LightKind),
117
- color: [...color, 1] as [number, number, number, number],
118
- intensity: intent.props.intensity ?? 1.0,
119
- range: intent.props.range ?? 10.0,
120
- castShadow: intent.props.castShadow ?? true,
121
- position: [pos[0], pos[1], pos[2], 1],
122
- layerMask: 0x7fffffff,
123
- };
124
- if (direction) {
125
- const [dirX, dirY, dirZ] = direction;
126
- lightCmd.direction = [dirX, dirY, dirZ, 0];
127
- }
128
- if (intent.props.spotInnerOuter) {
129
- lightCmd.spotInnerOuter = toVec2(intent.props.spotInnerOuter);
130
- }
131
- enqueueCommand(context.worldId, 'cmd-light-create', lightCmd);
132
-
133
- let store = world.components.get(intent.entityId);
134
- if (!store) {
135
- store = new Map();
136
- world.components.set(intent.entityId, store);
137
- }
138
- store.set('Light', {
139
- type: 'Light',
140
- id: lightId,
141
- kind: intent.props.kind ?? ('directional' as LightKind),
142
- color,
143
- intensity: intent.props.intensity ?? 1.0,
144
- range: intent.props.range ?? 10.0,
145
- castShadow: intent.props.castShadow ?? true,
146
- direction,
147
- spotInnerOuter: intent.props.spotInnerOuter
148
- ? toVec2(intent.props.spotInnerOuter)
149
- : [0.2, 0.6],
150
- skipUpdate: true,
151
- });
152
-
153
- intentsToRemove.push(i);
154
- } else if (intent.type === 'update-transform') {
155
- const store = world.components.get(intent.entityId);
156
- if (store) {
157
- const transform = store.get('Transform') as
158
- | TransformComponent
159
- | undefined;
160
- if (transform) {
161
- const nextProps = { ...intent.props };
162
- if (nextProps.position) {
163
- nextProps.position = toVec3(nextProps.position);
164
- }
165
- if (nextProps.rotation) {
166
- nextProps.rotation = toQuat(nextProps.rotation);
167
- }
168
- if (nextProps.scale) {
169
- nextProps.scale = toVec3(nextProps.scale);
170
- }
171
- Object.assign(transform, nextProps);
172
- const matrix = getEntityTransformMatrix(world, intent.entityId);
173
- const matrixArray = Array.from(matrix);
174
-
175
- const model = store.get('Model') as ModelComponent | undefined;
176
- if (model) {
177
- if (model.skipUpdate) {
178
- model.skipUpdate = false;
179
- } else {
180
- enqueueCommand(context.worldId, 'cmd-model-update', {
181
- windowId: context.worldId,
182
- modelId: model.id,
183
- transform: matrixArray,
184
- });
185
- }
186
- }
187
- const camera = store.get('Camera') as CameraComponent | undefined;
188
- if (camera) {
189
- if (camera.skipUpdate) {
190
- camera.skipUpdate = false;
191
- } else {
192
- enqueueCommand(context.worldId, 'cmd-camera-update', {
193
- windowId: context.worldId,
194
- cameraId: camera.id,
195
- transform: matrixArray,
196
- });
197
- }
198
- }
199
- const light = store.get('Light') as LightComponent | undefined;
200
- if (light) {
201
- if (light.skipUpdate) {
202
- light.skipUpdate = false;
203
- } else {
204
- const pos = vec3.create();
205
- mat4.getTranslation(pos, matrix);
206
- enqueueCommand(context.worldId, 'cmd-light-update', {
207
- windowId: context.worldId,
208
- lightId: light.id,
209
- position: [pos[0], pos[1], pos[2], 1],
210
- });
211
- }
212
- }
213
- }
214
- }
215
- intentsToRemove.push(i);
216
- } else if (intent.type === 'detach-component') {
217
- const store = world.components.get(intent.entityId);
218
- if (store) {
219
- const comp = store.get(intent.componentType) as Component | undefined;
220
- if (comp && 'id' in comp) {
221
- if (intent.componentType === 'Model') {
222
- const modelComp = comp as ModelComponent;
223
- enqueueCommand(context.worldId, 'cmd-model-dispose', {
224
- windowId: context.worldId,
225
- modelId: modelComp.id,
226
- });
227
- } else if (intent.componentType === 'Camera') {
228
- const cameraComp = comp as CameraComponent;
229
- enqueueCommand(context.worldId, 'cmd-camera-dispose', {
230
- windowId: context.worldId,
231
- cameraId: cameraComp.id,
232
- });
233
- } else if (intent.componentType === 'Light') {
234
- const lightComp = comp as LightComponent;
235
- enqueueCommand(context.worldId, 'cmd-light-dispose', {
236
- windowId: context.worldId,
237
- lightId: lightComp.id,
238
- });
239
- }
240
- }
241
- store.delete(intent.componentType);
242
- }
243
- intentsToRemove.push(i);
244
- } else if (intent.type === 'gizmo-draw-line') {
245
- enqueueCommand(context.worldId, 'cmd-gizmo-draw-line', {
246
- start: toVec3(intent.start),
247
- end: toVec3(intent.end),
248
- color: toVec4(intent.color),
249
- });
250
- intentsToRemove.push(i);
251
- } else if (intent.type === 'gizmo-draw-aabb') {
252
- enqueueCommand(context.worldId, 'cmd-gizmo-draw-aabb', {
253
- min: toVec3(intent.min),
254
- max: toVec3(intent.max),
255
- color: toVec4(intent.color),
256
- });
257
- intentsToRemove.push(i);
258
- }
259
- }
260
-
261
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
262
- const idx = intentsToRemove[i];
263
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
264
- }
265
- };
1
+ /**
2
+ * Backward-compatible module shim.
3
+ *
4
+ * The canonical implementation now lives in `scene-sync.ts`.
5
+ */
6
+ export {
7
+ CoreCommandBuilderSystem,
8
+ SceneSyncSystem,
9
+ } from './scene-sync';
@@ -2,16 +2,35 @@ import type { EngineCmd } from '../../types/cmds';
2
2
  import { enqueueCommand } from '../bridge/dispatch';
3
3
  import type { System } from '../ecs';
4
4
 
5
+ /**
6
+ * Handles diagnostic intents that query core-side resource lists.
7
+ *
8
+ * This system is intentionally narrow: it transforms
9
+ * `request-resource-list` intents into typed `cmd-*-list` commands.
10
+ */
5
11
  export const DiagnosticsSystem: System = (world, context) => {
6
12
  const intentsToRemove: number[] = [];
7
13
 
8
14
  for (let i = 0; i < world.pendingIntents.length; i++) {
9
15
  const intent = world.pendingIntents[i];
10
16
  if (intent?.type === 'request-resource-list') {
17
+ let windowId = world.primaryWindowId;
18
+ if (windowId === undefined) {
19
+ for (const boundWindowId of world.targetWindowBindings.values()) {
20
+ windowId = boundWindowId;
21
+ break;
22
+ }
23
+ }
24
+ if (windowId === undefined) {
25
+ windowId = world.realmCreateArgs.hostWindowId;
26
+ }
27
+ if (windowId === undefined) {
28
+ continue;
29
+ }
11
30
  const type = intent.resourceType;
12
31
  const cmdType = `cmd-${type}-list` as EngineCmd['type'];
13
32
  enqueueCommand(context.worldId, cmdType, {
14
- windowId: context.worldId,
33
+ windowId,
15
34
  });
16
35
  intentsToRemove.push(i);
17
36
  }
@@ -21,22 +40,4 @@ export const DiagnosticsSystem: System = (world, context) => {
21
40
  const idx = intentsToRemove[i];
22
41
  if (idx !== undefined) world.pendingIntents.splice(idx, 1);
23
42
  }
24
-
25
- // Simple diagnostic log every 600 frames
26
- if (world.entities.size > 0 && world.nextCoreId > 2) {
27
- // We only log if there is something interesting
28
- const globalCounters = globalThis as unknown as Record<string, number>;
29
- const frame = globalCounters.vulframFrameCount || 0;
30
- globalCounters.vulframFrameCount = frame + 1;
31
-
32
- if (frame % 600 === 0) {
33
- console.debug(
34
- `[Diagnostics] World ${context.worldId} | Entities: ${
35
- world.entities.size
36
- } | Intents: ${world.pendingIntents.length} | Core objects: ${
37
- world.nextCoreId - 2
38
- }`,
39
- );
40
- }
41
- }
42
43
  };
@@ -1,7 +1,9 @@
1
1
  export { InputMirrorSystem } from './input-mirror';
2
2
  export { CommandIntentSystem } from './command-intent';
3
- export { CoreCommandBuilderSystem } from './core-command-builder';
3
+ export { ConstraintSolveSystem } from './constraint-solve';
4
+ export { SceneSyncSystem, CoreCommandBuilderSystem } from './scene-sync';
4
5
  export { ResourceUploadSystem } from './resource-upload';
6
+ export { UiBridgeSystem } from './ui-bridge';
5
7
  export { ResponseDecodeSystem } from './response-decode';
6
8
  export { WorldLifecycleSystem } from './world-lifecycle';
7
9
  export { DiagnosticsSystem } from './diagnostics';
@@ -1,19 +1,31 @@
1
1
  import type { KeyboardEvent } from '../../types/events/keyboard';
2
2
  import type { PointerEvent } from '../../types/events/pointer';
3
+ import type { GamepadEvent } from '../../types/events/gamepad';
4
+ import type { SystemEvent } from '../../types/events/system';
5
+ import type { UiEvent } from '../../types/events/ui';
3
6
  import type { WindowEvent } from '../../types/events/window';
4
7
  import type {
8
+ GamepadStateComponent,
5
9
  InputStateComponent,
10
+ SystemEventStateComponent,
6
11
  System,
12
+ UiEventStateComponent,
7
13
  WindowStateComponent,
8
14
  } from '../ecs';
9
15
 
10
16
  /**
11
- * InputMirrorSystem: Processes input events and updates InputState component
17
+ * Mirrors inbound core events into world-level ECS state components.
18
+ *
19
+ * This system owns runtime input snapshots for keyboard, pointer, window,
20
+ * gamepad, system, and UI event streams.
12
21
  */
13
22
  export const InputMirrorSystem: System = (world) => {
14
23
  // Ensure InputState component exists for the world
15
24
  let inputState: InputStateComponent | undefined;
16
25
  let windowState: WindowStateComponent | undefined;
26
+ let gamepadState: GamepadStateComponent | undefined;
27
+ let systemEventState: SystemEventStateComponent | undefined;
28
+ let uiEventState: UiEventStateComponent | undefined;
17
29
 
18
30
  // Find or create InputState (attached to a special entity ID 0)
19
31
  const WORLD_ENTITY_ID = 0;
@@ -57,6 +69,38 @@ export const InputMirrorSystem: System = (world) => {
57
69
  worldStore.set('WindowState', windowState);
58
70
  }
59
71
 
72
+ gamepadState = worldStore.get('GamepadState') as GamepadStateComponent;
73
+ if (!gamepadState) {
74
+ gamepadState = {
75
+ type: 'GamepadState',
76
+ connected: new Map(),
77
+ buttons: new Map(),
78
+ axes: new Map(),
79
+ eventsThisFrame: [],
80
+ };
81
+ worldStore.set('GamepadState', gamepadState);
82
+ }
83
+
84
+ systemEventState = worldStore.get(
85
+ 'SystemEventState',
86
+ ) as SystemEventStateComponent;
87
+ if (!systemEventState) {
88
+ systemEventState = {
89
+ type: 'SystemEventState',
90
+ eventsThisFrame: [],
91
+ };
92
+ worldStore.set('SystemEventState', systemEventState);
93
+ }
94
+
95
+ uiEventState = worldStore.get('UiEventState') as UiEventStateComponent;
96
+ if (!uiEventState) {
97
+ uiEventState = {
98
+ type: 'UiEventState',
99
+ eventsThisFrame: [],
100
+ };
101
+ worldStore.set('UiEventState', uiEventState);
102
+ }
103
+
60
104
  // Clear "just pressed/released" from previous frame
61
105
  inputState.keysJustPressed.clear();
62
106
  inputState.keysJustReleased.clear();
@@ -68,10 +112,13 @@ export const InputMirrorSystem: System = (world) => {
68
112
  windowState.movedThisFrame = false;
69
113
  windowState.focusChangedThisFrame = false;
70
114
  windowState.closeRequested = false;
115
+ gamepadState.eventsThisFrame.length = 0;
116
+ systemEventState.eventsThisFrame.length = 0;
117
+ uiEventState.eventsThisFrame.length = 0;
71
118
 
72
119
  // Process inbound events
73
- while (world.inboundEvents.length > 0) {
74
- const event = world.inboundEvents.shift();
120
+ for (let i = 0; i < world.inboundEvents.length; i++) {
121
+ const event = world.inboundEvents[i];
75
122
  if (!event) continue;
76
123
 
77
124
  // Keyboard events
@@ -148,5 +195,56 @@ export const InputMirrorSystem: System = (world) => {
148
195
  windowState.scaleFactor = winEvent.data.scaleFactor;
149
196
  }
150
197
  }
198
+ // Gamepad events
199
+ else if (event.type === 'gamepad') {
200
+ const gpEvent = event.content as GamepadEvent;
201
+ gamepadState.eventsThisFrame.push(gpEvent);
202
+ if (gpEvent.event === 'on-connect') {
203
+ gamepadState.connected.set(gpEvent.data.gamepadId, {
204
+ name: gpEvent.data.name,
205
+ });
206
+ } else if (gpEvent.event === 'on-disconnect') {
207
+ gamepadState.connected.delete(gpEvent.data.gamepadId);
208
+ gamepadState.buttons.delete(gpEvent.data.gamepadId);
209
+ gamepadState.axes.delete(gpEvent.data.gamepadId);
210
+ } else if (gpEvent.event === 'on-button') {
211
+ const id = gpEvent.data.gamepadId;
212
+ let buttons = gamepadState.buttons.get(id);
213
+ if (!buttons) {
214
+ buttons = new Map();
215
+ gamepadState.buttons.set(id, buttons);
216
+ }
217
+ buttons.set(gpEvent.data.button, {
218
+ pressed: gpEvent.data.state === 'pressed',
219
+ value: gpEvent.data.value,
220
+ });
221
+ } else if (gpEvent.event === 'on-axis') {
222
+ const id = gpEvent.data.gamepadId;
223
+ let axes = gamepadState.axes.get(id);
224
+ if (!axes) {
225
+ axes = new Map();
226
+ gamepadState.axes.set(id, axes);
227
+ }
228
+ axes.set(gpEvent.data.axis, gpEvent.data.value);
229
+ }
230
+ }
231
+ // System events
232
+ else if (event.type === 'system') {
233
+ const sysEvent = event.content as SystemEvent;
234
+ systemEventState.eventsThisFrame.push(sysEvent);
235
+ if (sysEvent.event === 'error') {
236
+ systemEventState.lastError = {
237
+ scope: sysEvent.data.scope,
238
+ message: sysEvent.data.message,
239
+ commandId: sysEvent.data.commandId,
240
+ commandType: sysEvent.data.commandType,
241
+ };
242
+ }
243
+ }
244
+ // UI events
245
+ else if (event.type === 'ui') {
246
+ uiEventState.eventsThisFrame.push(event.content as UiEvent);
247
+ }
151
248
  }
249
+ world.inboundEvents.length = 0;
152
250
  };