@vulfram/engine 0.17.1-alpha → 0.19.2-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 (36) hide show
  1. package/README.md +3 -3
  2. package/package.json +9 -4
  3. package/src/engine/api.ts +12 -25
  4. package/src/engine/bridge/dispatch.ts +3 -8
  5. package/src/engine/ecs/components.ts +340 -0
  6. package/src/engine/ecs/index.ts +3 -661
  7. package/src/engine/ecs/intents.ts +184 -0
  8. package/src/engine/ecs/systems.ts +26 -0
  9. package/src/engine/intents/store.ts +72 -0
  10. package/src/engine/state.ts +3 -3
  11. package/src/engine/systems/command-intent.ts +11 -16
  12. package/src/engine/systems/diagnostics.ts +3 -13
  13. package/src/engine/systems/input-mirror.ts +156 -18
  14. package/src/engine/systems/resource-upload.ts +12 -14
  15. package/src/engine/systems/response-decode.ts +17 -0
  16. package/src/engine/systems/scene-sync.ts +12 -13
  17. package/src/engine/systems/ui-bridge.ts +31 -10
  18. package/src/engine/systems/utils.ts +46 -3
  19. package/src/engine/systems/world-lifecycle.ts +9 -15
  20. package/src/engine/world/entities.ts +201 -37
  21. package/src/engine/world/mount.ts +27 -6
  22. package/src/engine/world/world-ui.ts +77 -30
  23. package/src/engine/world/world3d.ts +282 -33
  24. package/src/helpers/collision.ts +487 -0
  25. package/src/helpers/index.ts +2 -0
  26. package/src/helpers/raycast.ts +442 -0
  27. package/src/types/cmds/geometry.ts +2 -2
  28. package/src/types/cmds/index.ts +42 -0
  29. package/src/types/cmds/input.ts +39 -0
  30. package/src/types/cmds/material.ts +10 -10
  31. package/src/types/cmds/realm.ts +0 -2
  32. package/src/types/cmds/system.ts +10 -0
  33. package/src/types/cmds/target.ts +14 -0
  34. package/src/types/events/keyboard.ts +2 -2
  35. package/src/types/events/pointer.ts +43 -0
  36. package/src/types/events/system.ts +44 -0
@@ -0,0 +1,184 @@
1
+ import type {
2
+ CmdUiAccessKitActionRequestArgs,
3
+ CmdUiApplyOpsArgs,
4
+ CmdUiClipboardPasteArgs,
5
+ CmdUiDebugSetArgs,
6
+ CmdUiDocumentCreateArgs,
7
+ CmdUiDocumentDisposeArgs,
8
+ CmdUiDocumentGetLayoutRectsArgs,
9
+ CmdUiDocumentGetTreeArgs,
10
+ CmdUiDocumentSetRectArgs,
11
+ CmdUiDocumentSetThemeArgs,
12
+ CmdUiEventTraceSetArgs,
13
+ CmdUiFocusGetArgs,
14
+ CmdUiFocusSetArgs,
15
+ CmdUiImageCreateFromBufferArgs,
16
+ CmdUiImageDisposeArgs,
17
+ CmdUiScreenshotReplyArgs,
18
+ CmdUiThemeDefineArgs,
19
+ CmdUiThemeDisposeArgs,
20
+ } from '../../types/cmds/ui';
21
+ import type { EnvironmentConfig } from '../../types/cmds/environment';
22
+ import type { ShadowConfig } from '../../types/cmds/shadow';
23
+ import type { NotificationLevel } from '../../types/kinds';
24
+ import type { JsonObject } from '../../types/json';
25
+ import type {
26
+ CameraProps,
27
+ ComponentType,
28
+ GeometryProps,
29
+ LightProps,
30
+ MaterialProps,
31
+ ModelProps,
32
+ TagProps,
33
+ TextureProps,
34
+ TransformProps,
35
+ UiFocusCycleMode,
36
+ } from './components';
37
+
38
+ export type Intent =
39
+ | { type: 'configure-environment'; config: EnvironmentConfig }
40
+ | {
41
+ type: 'request-resource-list';
42
+ resourceType:
43
+ | 'model'
44
+ | 'material'
45
+ | 'texture'
46
+ | 'geometry'
47
+ | 'light'
48
+ | 'camera';
49
+ }
50
+ | { type: 'create-entity'; worldId: number; entityId: number }
51
+ | { type: 'remove-entity'; entityId: number }
52
+ | {
53
+ type: 'update-transform';
54
+ entityId: number;
55
+ props: TransformProps;
56
+ }
57
+ | {
58
+ type: 'attach-camera';
59
+ entityId: number;
60
+ props: CameraProps;
61
+ }
62
+ | {
63
+ type: 'attach-model';
64
+ entityId: number;
65
+ props: ModelProps;
66
+ }
67
+ | {
68
+ type: 'attach-light';
69
+ entityId: number;
70
+ props: LightProps;
71
+ }
72
+ | {
73
+ type: 'attach-tag';
74
+ entityId: number;
75
+ props: TagProps;
76
+ }
77
+ | { type: 'create-material'; resourceId: number; props: MaterialProps }
78
+ | { type: 'create-geometry'; resourceId: number; props: GeometryProps }
79
+ | { type: 'create-texture'; resourceId: number; props: TextureProps }
80
+ | { type: 'dispose-material'; resourceId: number }
81
+ | { type: 'dispose-texture'; resourceId: number }
82
+ | { type: 'dispose-geometry'; resourceId: number }
83
+ | { type: 'detach-component'; entityId: number; componentType: ComponentType }
84
+ | { type: 'set-parent'; entityId: number; parentId: number | null }
85
+ | {
86
+ type: 'send-notification';
87
+ level: NotificationLevel;
88
+ title: string;
89
+ message: string;
90
+ }
91
+ | { type: 'configure-shadows'; config: ShadowConfig }
92
+ | {
93
+ type: 'gizmo-draw-line';
94
+ start: [number, number, number];
95
+ end: [number, number, number];
96
+ color: [number, number, number, number];
97
+ }
98
+ | {
99
+ type: 'gizmo-draw-aabb';
100
+ min: [number, number, number];
101
+ max: [number, number, number];
102
+ color: [number, number, number, number];
103
+ }
104
+ | { type: 'ui-theme-define'; args: CmdUiThemeDefineArgs }
105
+ | { type: 'ui-theme-dispose'; args: CmdUiThemeDisposeArgs }
106
+ | { type: 'ui-document-create'; args: CmdUiDocumentCreateArgs }
107
+ | { type: 'ui-document-dispose'; args: CmdUiDocumentDisposeArgs }
108
+ | { type: 'ui-document-set-rect'; args: CmdUiDocumentSetRectArgs }
109
+ | { type: 'ui-document-set-theme'; args: CmdUiDocumentSetThemeArgs }
110
+ | { type: 'ui-document-get-tree'; args: CmdUiDocumentGetTreeArgs }
111
+ | {
112
+ type: 'ui-document-get-layout-rects';
113
+ args: CmdUiDocumentGetLayoutRectsArgs;
114
+ }
115
+ | { type: 'ui-apply-ops'; args: CmdUiApplyOpsArgs }
116
+ | { type: 'ui-debug-set'; args: CmdUiDebugSetArgs }
117
+ | { type: 'ui-focus-set'; args: CmdUiFocusSetArgs }
118
+ | { type: 'ui-focus-get'; args: CmdUiFocusGetArgs }
119
+ | { type: 'ui-event-trace-set'; args: CmdUiEventTraceSetArgs }
120
+ | { type: 'ui-image-create-from-buffer'; args: CmdUiImageCreateFromBufferArgs }
121
+ | { type: 'ui-image-dispose'; args: CmdUiImageDisposeArgs }
122
+ | { type: 'ui-clipboard-paste'; args: CmdUiClipboardPasteArgs }
123
+ | { type: 'ui-screenshot-reply'; args: CmdUiScreenshotReplyArgs }
124
+ | {
125
+ type: 'ui-access-kit-action-request';
126
+ args: CmdUiAccessKitActionRequestArgs;
127
+ }
128
+ | {
129
+ type: 'ui-form-upsert';
130
+ form: {
131
+ formId: string;
132
+ windowId: number;
133
+ realmId: number;
134
+ documentId: number;
135
+ disabled?: boolean;
136
+ cycleMode?: UiFocusCycleMode;
137
+ activeFieldsetId?: string;
138
+ };
139
+ }
140
+ | { type: 'ui-form-dispose'; formId: string }
141
+ | {
142
+ type: 'ui-fieldset-upsert';
143
+ fieldset: {
144
+ formId: string;
145
+ fieldsetId: string;
146
+ disabled?: boolean;
147
+ legendNodeId?: number;
148
+ };
149
+ }
150
+ | { type: 'ui-fieldset-dispose'; formId: string; fieldsetId: string }
151
+ | {
152
+ type: 'ui-focusable-upsert';
153
+ focusable: {
154
+ formId: string;
155
+ nodeId: number;
156
+ tabIndex?: number;
157
+ fieldsetId?: string;
158
+ disabled?: boolean;
159
+ orderHint?: number;
160
+ };
161
+ }
162
+ | { type: 'ui-focusable-dispose'; nodeId: number }
163
+ | {
164
+ type: 'ui-focus-next';
165
+ windowId: number;
166
+ backwards?: boolean;
167
+ formId?: string;
168
+ }
169
+ | {
170
+ type: 'custom';
171
+ name: string;
172
+ data: JsonObject;
173
+ };
174
+
175
+ /** Internal World Events. */
176
+ export type WorldEvent =
177
+ | { type: 'entity-created'; entityId: number }
178
+ | { type: 'entity-destroyed'; entityId: number }
179
+ | { type: 'component-added'; entityId: number; componentType: ComponentType }
180
+ | {
181
+ type: 'component-removed';
182
+ entityId: number;
183
+ componentType: ComponentType;
184
+ };
@@ -0,0 +1,26 @@
1
+ import type { WorldState } from '../state';
2
+ import type { ComponentSchema } from './components';
3
+
4
+ /** Pipeline stage at which a system runs. */
5
+ export type SystemStep = 'input' | 'update' | 'preRender' | 'postRender';
6
+
7
+ /** System execution context provided each frame. */
8
+ export interface SystemContext {
9
+ dt: number;
10
+ time: number;
11
+ worldId: number;
12
+ }
13
+
14
+ /** System function signature. */
15
+ export type System = (state: WorldState, context: SystemContext) => void;
16
+
17
+ /** Registry of components and systems. */
18
+ export interface EngineRegistry {
19
+ components: Map<string, ComponentSchema>;
20
+ systems: {
21
+ input: System[];
22
+ update: System[];
23
+ preRender: System[];
24
+ postRender: System[];
25
+ };
26
+ }
@@ -0,0 +1,72 @@
1
+ import type { Intent } from '../ecs';
2
+
3
+ export type IntentType = Intent['type'];
4
+
5
+ type IntentEntry<K extends IntentType> = {
6
+ seq: number;
7
+ intent: Extract<Intent, { type: K }>;
8
+ };
9
+
10
+ export class IntentStore {
11
+ private byType = new Map<IntentType, IntentEntry<IntentType>[]>();
12
+ private sequence = 0;
13
+ private total = 0;
14
+
15
+ enqueue(intent: Intent): void {
16
+ const key = intent.type as IntentType;
17
+ const list = this.byType.get(key);
18
+ const entry: IntentEntry<IntentType> = {
19
+ seq: this.sequence++,
20
+ intent: intent as Extract<Intent, { type: IntentType }>,
21
+ };
22
+ if (list) {
23
+ list.push(entry);
24
+ } else {
25
+ this.byType.set(key, [entry]);
26
+ }
27
+ this.total++;
28
+ }
29
+
30
+ take<K extends IntentType>(type: K): Extract<Intent, { type: K }>[] {
31
+ const list = this.byType.get(type);
32
+ if (!list || list.length === 0) {
33
+ return [];
34
+ }
35
+ this.byType.delete(type);
36
+ this.total -= list.length;
37
+ return list.map((entry) => entry.intent as Extract<Intent, { type: K }>);
38
+ }
39
+
40
+ takeMany<K extends IntentType>(
41
+ types: readonly K[],
42
+ ): Extract<Intent, { type: K }>[] {
43
+ const merged: IntentEntry<K>[] = [];
44
+ for (let i = 0; i < types.length; i++) {
45
+ const type = types[i];
46
+ if (type === undefined) continue;
47
+ const list = this.byType.get(type);
48
+ if (!list || list.length === 0) continue;
49
+ this.byType.delete(type);
50
+ this.total -= list.length;
51
+ for (let j = 0; j < list.length; j++) {
52
+ const entry = list[j];
53
+ if (entry) merged.push(entry as unknown as IntentEntry<K>);
54
+ }
55
+ }
56
+ merged.sort((a, b) => a.seq - b.seq);
57
+ return merged.map((entry) => entry.intent as Extract<Intent, { type: K }>);
58
+ }
59
+
60
+ size(): number {
61
+ return this.total;
62
+ }
63
+
64
+ clear(): void {
65
+ this.byType.clear();
66
+ this.total = 0;
67
+ }
68
+ }
69
+
70
+ export function createIntentStore(): IntentStore {
71
+ return new IntentStore();
72
+ }
@@ -6,9 +6,9 @@ import type {
6
6
  Component,
7
7
  ComponentType,
8
8
  EngineRegistry,
9
- Intent,
10
9
  WorldEvent,
11
10
  } from './ecs';
11
+ import type { IntentStore } from './intents/store';
12
12
 
13
13
  type EngineStatus = 'uninitialized' | 'initialized' | 'disposed';
14
14
 
@@ -109,9 +109,9 @@ export type WorldState = {
109
109
  nextCoreId: number;
110
110
  systems: string[];
111
111
  /**
112
- * Pending intents to be processed by systems in the next tick.
112
+ * Indexed intent store to query/consume by type with stable insertion order.
113
113
  */
114
- pendingIntents: Intent[];
114
+ intentStore: IntentStore;
115
115
  /**
116
116
  * Internal world events for system-to-system communication.
117
117
  */
@@ -9,6 +9,14 @@ import type {
9
9
  } from '../ecs';
10
10
  import { toQuat, toVec3 } from './utils';
11
11
 
12
+ const COMMAND_INTENT_TYPES = [
13
+ 'create-entity',
14
+ 'attach-tag',
15
+ 'set-parent',
16
+ 'update-transform',
17
+ 'remove-entity',
18
+ ] as const;
19
+
12
20
  function wouldCreateParentCycle(
13
21
  world: Parameters<System>[0],
14
22
  childEntityId: number,
@@ -39,13 +47,12 @@ function wouldCreateParentCycle(
39
47
  * render-time command synthesis (entity lifecycle, tags, parent links, transforms).
40
48
  */
41
49
  export const CommandIntentSystem: System = (world, context) => {
42
- const intentsToRemove: number[] = [];
43
50
  const realmId = world.coreRealmId;
51
+ const intents = world.intentStore.takeMany(COMMAND_INTENT_TYPES);
44
52
 
45
- for (let i = 0; i < world.pendingIntents.length; i++) {
46
- const intent = world.pendingIntents[i];
53
+ for (let i = 0; i < intents.length; i++) {
54
+ const intent = intents[i];
47
55
  if (!intent) continue;
48
-
49
56
  if (intent.type === 'create-entity') {
50
57
  world.entities.add(intent.entityId);
51
58
  // Initialize default transform
@@ -65,7 +72,6 @@ export const CommandIntentSystem: System = (world, context) => {
65
72
  });
66
73
  }
67
74
  world.constraintDirtyEntities.add(intent.entityId);
68
- intentsToRemove.push(i);
69
75
  } else if (intent.type === 'attach-tag') {
70
76
  let store = world.components.get(intent.entityId);
71
77
  if (!store) {
@@ -77,7 +83,6 @@ export const CommandIntentSystem: System = (world, context) => {
77
83
  name: intent.props.name ?? '',
78
84
  labels: new Set(intent.props.labels ?? []),
79
85
  });
80
- intentsToRemove.push(i);
81
86
  } else if (intent.type === 'set-parent') {
82
87
  let store = world.components.get(intent.entityId);
83
88
  if (!store) {
@@ -100,14 +105,12 @@ export const CommandIntentSystem: System = (world, context) => {
100
105
  console.error(
101
106
  `[World ${context.worldId}] Invalid parent constraint: entity ${intent.entityId} cannot parent itself.`,
102
107
  );
103
- intentsToRemove.push(i);
104
108
  continue;
105
109
  }
106
110
  if (wouldCreateParentCycle(world, intent.entityId, intent.parentId)) {
107
111
  console.error(
108
112
  `[World ${context.worldId}] Invalid parent constraint: cycle detected for child ${intent.entityId} and parent ${intent.parentId}.`,
109
113
  );
110
- intentsToRemove.push(i);
111
114
  continue;
112
115
  }
113
116
  store.set('Parent', {
@@ -132,7 +135,6 @@ export const CommandIntentSystem: System = (world, context) => {
132
135
  children.add(intent.entityId);
133
136
  }
134
137
  world.constraintDirtyEntities.add(intent.entityId);
135
- intentsToRemove.push(i);
136
138
  } else if (intent.type === 'update-transform') {
137
139
  const store = world.components.get(intent.entityId);
138
140
  if (!store) {
@@ -155,7 +157,6 @@ export const CommandIntentSystem: System = (world, context) => {
155
157
  }
156
158
  Object.assign(transform, nextProps);
157
159
  world.constraintDirtyEntities.add(intent.entityId);
158
- intentsToRemove.push(i);
159
160
  } else if (intent.type === 'remove-entity') {
160
161
  if (realmId === undefined) continue;
161
162
 
@@ -213,12 +214,6 @@ export const CommandIntentSystem: System = (world, context) => {
213
214
  world.resolvedEntityTransforms.delete(intent.entityId);
214
215
  world.sceneSyncMatrixScratch.delete(intent.entityId);
215
216
  world.constraintDirtyEntities.add(intent.entityId);
216
- intentsToRemove.push(i);
217
217
  }
218
218
  }
219
-
220
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
221
- const idx = intentsToRemove[i];
222
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
223
- }
224
219
  };
@@ -9,10 +9,9 @@ import type { System } from '../ecs';
9
9
  * `request-resource-list` intents into typed `cmd-*-list` commands.
10
10
  */
11
11
  export const DiagnosticsSystem: System = (world, context) => {
12
- const intentsToRemove: number[] = [];
13
-
14
- for (let i = 0; i < world.pendingIntents.length; i++) {
15
- const intent = world.pendingIntents[i];
12
+ const intents = world.intentStore.take('request-resource-list');
13
+ for (let i = 0; i < intents.length; i++) {
14
+ const intent = intents[i];
16
15
  if (intent?.type === 'request-resource-list') {
17
16
  let windowId = world.primaryWindowId;
18
17
  if (windowId === undefined) {
@@ -21,9 +20,6 @@ export const DiagnosticsSystem: System = (world, context) => {
21
20
  break;
22
21
  }
23
22
  }
24
- if (windowId === undefined) {
25
- windowId = world.realmCreateArgs.hostWindowId;
26
- }
27
23
  if (windowId === undefined) {
28
24
  continue;
29
25
  }
@@ -32,12 +28,6 @@ export const DiagnosticsSystem: System = (world, context) => {
32
28
  enqueueCommand(context.worldId, cmdType, {
33
29
  windowId,
34
30
  });
35
- intentsToRemove.push(i);
36
31
  }
37
32
  }
38
-
39
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
40
- const idx = intentsToRemove[i];
41
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
42
- }
43
33
  };
@@ -20,6 +20,85 @@ import type {
20
20
  * gamepad, system, and UI event streams.
21
21
  */
22
22
  export const InputMirrorSystem: System = (world) => {
23
+ const resolveWindowSize = (data: {
24
+ windowWidth?: number;
25
+ windowHeight?: number;
26
+ }): [number, number] | undefined => {
27
+ if (
28
+ typeof data.windowWidth === 'number' &&
29
+ typeof data.windowHeight === 'number'
30
+ ) {
31
+ return [data.windowWidth, data.windowHeight];
32
+ }
33
+ return undefined;
34
+ };
35
+
36
+ const resolveTargetSize = (data: {
37
+ targetWidth?: number;
38
+ targetHeight?: number;
39
+ }): [number, number] | undefined => {
40
+ if (
41
+ typeof data.targetWidth === 'number' &&
42
+ typeof data.targetHeight === 'number'
43
+ ) {
44
+ return [data.targetWidth, data.targetHeight];
45
+ }
46
+ return undefined;
47
+ };
48
+
49
+ const applyPointerPosition = (
50
+ inputState: InputStateComponent,
51
+ globalPosition: [number, number],
52
+ targetPosition?: [number, number],
53
+ targetId?: number,
54
+ targetUv?: [number, number],
55
+ windowSize?: [number, number],
56
+ targetSize?: [number, number],
57
+ ): void => {
58
+ const oldGlobalPosition = inputState.pointerPosition;
59
+ inputState.pointerDelta = [
60
+ globalPosition[0] - oldGlobalPosition[0],
61
+ globalPosition[1] - oldGlobalPosition[1],
62
+ ];
63
+ inputState.pointerPosition = globalPosition;
64
+ inputState.pointerWindowSize = windowSize;
65
+
66
+ if (targetPosition) {
67
+ const previousTargetId = inputState.pointerTargetId;
68
+ const previousTargetPosition = inputState.pointerPositionTarget;
69
+ if (
70
+ previousTargetId === targetId &&
71
+ previousTargetPosition !== undefined
72
+ ) {
73
+ inputState.pointerTargetDelta = [
74
+ targetPosition[0] - previousTargetPosition[0],
75
+ targetPosition[1] - previousTargetPosition[1],
76
+ ];
77
+ } else {
78
+ inputState.pointerTargetDelta = [0, 0];
79
+ }
80
+ inputState.pointerPositionTarget = targetPosition;
81
+ inputState.pointerTargetId = targetId;
82
+ inputState.pointerTargetSize = targetSize;
83
+ inputState.pointerTargetUv = targetUv;
84
+ return;
85
+ }
86
+
87
+ inputState.pointerPositionTarget = undefined;
88
+ inputState.pointerTargetDelta = undefined;
89
+ inputState.pointerTargetId = undefined;
90
+ inputState.pointerTargetSize = undefined;
91
+ inputState.pointerTargetUv = undefined;
92
+ };
93
+
94
+ const clearPointerTargetState = (inputState: InputStateComponent): void => {
95
+ inputState.pointerPositionTarget = undefined;
96
+ inputState.pointerTargetDelta = undefined;
97
+ inputState.pointerTargetId = undefined;
98
+ inputState.pointerTargetSize = undefined;
99
+ inputState.pointerTargetUv = undefined;
100
+ };
101
+
23
102
  // Ensure InputState component exists for the world
24
103
  let inputState: InputStateComponent | undefined;
25
104
  let windowState: WindowStateComponent | undefined;
@@ -43,12 +122,15 @@ export const InputMirrorSystem: System = (world) => {
43
122
  keysPressed: new Set(),
44
123
  keysJustPressed: new Set(),
45
124
  keysJustReleased: new Set(),
46
- mouseButtons: new Set(),
47
- mousePosition: [0, 0],
48
- mouseJustPressed: new Set(),
49
- mouseJustReleased: new Set(),
50
- mouseDelta: [0, 0],
125
+ pointerButtons: new Set(),
126
+ pointerPosition: [0, 0],
127
+ pointerDelta: [0, 0],
128
+ pointerJustPressed: new Set(),
129
+ pointerJustReleased: new Set(),
130
+ pointerWindowSize: undefined,
131
+ pointerTargetSize: undefined,
51
132
  scrollDelta: [0, 0],
133
+ imeEnabled: false,
52
134
  };
53
135
  worldStore.set('InputState', inputState);
54
136
  }
@@ -104,10 +186,14 @@ export const InputMirrorSystem: System = (world) => {
104
186
  // Clear "just pressed/released" from previous frame
105
187
  inputState.keysJustPressed.clear();
106
188
  inputState.keysJustReleased.clear();
107
- inputState.mouseJustPressed.clear();
108
- inputState.mouseJustReleased.clear();
109
- inputState.mouseDelta = [0, 0];
189
+ inputState.pointerJustPressed.clear();
190
+ inputState.pointerJustReleased.clear();
191
+ inputState.pointerDelta = [0, 0];
192
+ inputState.pointerTargetDelta = inputState.pointerPositionTarget
193
+ ? [0, 0]
194
+ : undefined;
110
195
  inputState.scrollDelta = [0, 0];
196
+ inputState.imeCommitText = undefined;
111
197
  windowState.resizedThisFrame = false;
112
198
  windowState.movedThisFrame = false;
113
199
  windowState.focusChangedThisFrame = false;
@@ -137,6 +223,21 @@ export const InputMirrorSystem: System = (world) => {
137
223
  inputState.keysPressed.delete(keyCode);
138
224
  inputState.keysJustReleased.add(keyCode);
139
225
  }
226
+ } else if (kbEvent.event === 'on-ime-enable') {
227
+ inputState.imeEnabled = true;
228
+ } else if (kbEvent.event === 'on-ime-preedit') {
229
+ inputState.imeEnabled = true;
230
+ inputState.imePreeditText = kbEvent.data.text;
231
+ inputState.imeCursorRange = kbEvent.data.cursorRange;
232
+ } else if (kbEvent.event === 'on-ime-commit') {
233
+ inputState.imeEnabled = true;
234
+ inputState.imeCommitText = kbEvent.data.text;
235
+ inputState.imePreeditText = undefined;
236
+ inputState.imeCursorRange = undefined;
237
+ } else if (kbEvent.event === 'on-ime-disable') {
238
+ inputState.imeEnabled = false;
239
+ inputState.imePreeditText = undefined;
240
+ inputState.imeCursorRange = undefined;
140
241
  }
141
242
  }
142
243
 
@@ -145,25 +246,43 @@ export const InputMirrorSystem: System = (world) => {
145
246
  const ptrEvent = event.content as PointerEvent;
146
247
 
147
248
  if (ptrEvent.event === 'on-move') {
148
- const oldPos = inputState.mousePosition;
149
- const newPos = ptrEvent.data.position;
150
- inputState.mouseDelta = [newPos[0] - oldPos[0], newPos[1] - oldPos[1]];
151
- inputState.mousePosition = newPos;
249
+ inputState.pointerWindowId = ptrEvent.data.windowId;
250
+ applyPointerPosition(
251
+ inputState,
252
+ ptrEvent.data.position,
253
+ ptrEvent.data.positionTarget,
254
+ ptrEvent.data.trace?.targetId,
255
+ ptrEvent.data.trace?.uv,
256
+ resolveWindowSize(ptrEvent.data),
257
+ resolveTargetSize(ptrEvent.data),
258
+ );
152
259
  } else if (ptrEvent.event === 'on-button') {
153
260
  const button = ptrEvent.data.button;
154
261
  const pressed = ptrEvent.data.state === 'pressed';
155
262
 
156
263
  if (pressed) {
157
- if (!inputState.mouseButtons.has(button)) {
158
- inputState.mouseJustPressed.add(button);
264
+ if (!inputState.pointerButtons.has(button)) {
265
+ inputState.pointerJustPressed.add(button);
159
266
  }
160
- inputState.mouseButtons.add(button);
267
+ inputState.pointerButtons.add(button);
161
268
  } else {
162
- inputState.mouseButtons.delete(button);
163
- inputState.mouseJustReleased.add(button);
269
+ inputState.pointerButtons.delete(button);
270
+ inputState.pointerJustReleased.add(button);
164
271
  }
165
- inputState.mousePosition = ptrEvent.data.position;
272
+ inputState.pointerWindowId = ptrEvent.data.windowId;
273
+ applyPointerPosition(
274
+ inputState,
275
+ ptrEvent.data.position,
276
+ ptrEvent.data.positionTarget,
277
+ ptrEvent.data.trace?.targetId,
278
+ ptrEvent.data.trace?.uv,
279
+ resolveWindowSize(ptrEvent.data),
280
+ resolveTargetSize(ptrEvent.data),
281
+ );
166
282
  } else if (ptrEvent.event === 'on-scroll') {
283
+ inputState.pointerWindowId = ptrEvent.data.windowId;
284
+ inputState.pointerWindowSize = resolveWindowSize(ptrEvent.data);
285
+ inputState.pointerTargetSize = resolveTargetSize(ptrEvent.data);
167
286
  const delta = ptrEvent.data.delta;
168
287
  if (delta.type === 'line') {
169
288
  inputState.scrollDelta = delta.value;
@@ -171,6 +290,25 @@ export const InputMirrorSystem: System = (world) => {
171
290
  // Convert pixel to approximate line delta (rough estimate)
172
291
  inputState.scrollDelta = [delta.value[0] / 20, delta.value[1] / 20];
173
292
  }
293
+ } else if (ptrEvent.event === 'on-touch') {
294
+ inputState.pointerWindowId = ptrEvent.data.windowId;
295
+ applyPointerPosition(
296
+ inputState,
297
+ ptrEvent.data.position,
298
+ ptrEvent.data.positionTarget,
299
+ ptrEvent.data.trace?.targetId,
300
+ ptrEvent.data.trace?.uv,
301
+ resolveWindowSize(ptrEvent.data),
302
+ resolveTargetSize(ptrEvent.data),
303
+ );
304
+ } else if (ptrEvent.event === 'on-leave') {
305
+ inputState.pointerWindowId = ptrEvent.data.windowId;
306
+ inputState.pointerWindowSize = resolveWindowSize(ptrEvent.data);
307
+ clearPointerTargetState(inputState);
308
+ } else if (ptrEvent.event === 'on-enter') {
309
+ inputState.pointerWindowId = ptrEvent.data.windowId;
310
+ inputState.pointerWindowSize = resolveWindowSize(ptrEvent.data);
311
+ inputState.pointerTargetSize = resolveTargetSize(ptrEvent.data);
174
312
  }
175
313
  }
176
314