@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.
- package/README.md +3 -3
- package/package.json +9 -4
- package/src/engine/api.ts +12 -25
- package/src/engine/bridge/dispatch.ts +3 -8
- package/src/engine/ecs/components.ts +340 -0
- package/src/engine/ecs/index.ts +3 -661
- package/src/engine/ecs/intents.ts +184 -0
- package/src/engine/ecs/systems.ts +26 -0
- package/src/engine/intents/store.ts +72 -0
- package/src/engine/state.ts +3 -3
- package/src/engine/systems/command-intent.ts +11 -16
- package/src/engine/systems/diagnostics.ts +3 -13
- package/src/engine/systems/input-mirror.ts +156 -18
- package/src/engine/systems/resource-upload.ts +12 -14
- package/src/engine/systems/response-decode.ts +17 -0
- package/src/engine/systems/scene-sync.ts +12 -13
- package/src/engine/systems/ui-bridge.ts +31 -10
- package/src/engine/systems/utils.ts +46 -3
- package/src/engine/systems/world-lifecycle.ts +9 -15
- package/src/engine/world/entities.ts +201 -37
- package/src/engine/world/mount.ts +27 -6
- package/src/engine/world/world-ui.ts +77 -30
- package/src/engine/world/world3d.ts +282 -33
- package/src/helpers/collision.ts +487 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/raycast.ts +442 -0
- package/src/types/cmds/geometry.ts +2 -2
- package/src/types/cmds/index.ts +42 -0
- package/src/types/cmds/input.ts +39 -0
- package/src/types/cmds/material.ts +10 -10
- package/src/types/cmds/realm.ts +0 -2
- package/src/types/cmds/system.ts +10 -0
- package/src/types/cmds/target.ts +14 -0
- package/src/types/events/keyboard.ts +2 -2
- package/src/types/events/pointer.ts +43 -0
- 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
|
+
}
|
package/src/engine/state.ts
CHANGED
|
@@ -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
|
-
*
|
|
112
|
+
* Indexed intent store to query/consume by type with stable insertion order.
|
|
113
113
|
*/
|
|
114
|
-
|
|
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 <
|
|
46
|
-
const intent =
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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.
|
|
108
|
-
inputState.
|
|
109
|
-
inputState.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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.
|
|
158
|
-
inputState.
|
|
264
|
+
if (!inputState.pointerButtons.has(button)) {
|
|
265
|
+
inputState.pointerJustPressed.add(button);
|
|
159
266
|
}
|
|
160
|
-
inputState.
|
|
267
|
+
inputState.pointerButtons.add(button);
|
|
161
268
|
} else {
|
|
162
|
-
inputState.
|
|
163
|
-
inputState.
|
|
269
|
+
inputState.pointerButtons.delete(button);
|
|
270
|
+
inputState.pointerJustReleased.add(button);
|
|
164
271
|
}
|
|
165
|
-
inputState.
|
|
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
|
|