@vulfram/engine 0.5.6-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/package.json +17 -0
- package/src/engine/api.ts +282 -0
- package/src/engine/bridge/dispatch.ts +115 -0
- package/src/engine/bridge/guards.ts +28 -0
- package/src/engine/bridge/protocol.ts +130 -0
- package/src/engine/ecs/index.ts +516 -0
- package/src/engine/errors.ts +10 -0
- package/src/engine/state.ts +135 -0
- package/src/engine/systems/command-intent.ts +74 -0
- package/src/engine/systems/core-command-builder.ts +265 -0
- package/src/engine/systems/diagnostics.ts +42 -0
- package/src/engine/systems/index.ts +7 -0
- package/src/engine/systems/input-mirror.ts +152 -0
- package/src/engine/systems/resource-upload.ts +143 -0
- package/src/engine/systems/response-decode.ts +28 -0
- package/src/engine/systems/utils.ts +147 -0
- package/src/engine/systems/world-lifecycle.ts +164 -0
- package/src/engine/world/entities.ts +516 -0
- package/src/index.ts +9 -0
- package/src/types/cmds/camera.ts +76 -0
- package/src/types/cmds/environment.ts +45 -0
- package/src/types/cmds/geometry.ts +144 -0
- package/src/types/cmds/gizmo.ts +18 -0
- package/src/types/cmds/index.ts +231 -0
- package/src/types/cmds/light.ts +69 -0
- package/src/types/cmds/material.ts +98 -0
- package/src/types/cmds/model.ts +59 -0
- package/src/types/cmds/shadow.ts +22 -0
- package/src/types/cmds/system.ts +15 -0
- package/src/types/cmds/texture.ts +63 -0
- package/src/types/cmds/window.ts +263 -0
- package/src/types/events/gamepad.ts +34 -0
- package/src/types/events/index.ts +19 -0
- package/src/types/events/keyboard.ts +225 -0
- package/src/types/events/pointer.ts +105 -0
- package/src/types/events/system.ts +13 -0
- package/src/types/events/window.ts +96 -0
- package/src/types/index.ts +3 -0
- package/src/types/kinds.ts +111 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { enqueueCommand } from '../bridge/dispatch';
|
|
2
|
+
import type {
|
|
3
|
+
CameraComponent,
|
|
4
|
+
Component,
|
|
5
|
+
LightComponent,
|
|
6
|
+
ModelComponent,
|
|
7
|
+
System,
|
|
8
|
+
} from '../ecs';
|
|
9
|
+
|
|
10
|
+
export const CommandIntentSystem: System = (world, context) => {
|
|
11
|
+
const intentsToRemove: number[] = [];
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
14
|
+
const intent = world.pendingIntents[i];
|
|
15
|
+
if (!intent) continue;
|
|
16
|
+
|
|
17
|
+
if (intent.type === 'create-entity') {
|
|
18
|
+
world.entities.add(intent.entityId);
|
|
19
|
+
// Initialize default transform
|
|
20
|
+
let store = world.components.get(intent.entityId);
|
|
21
|
+
if (!store) {
|
|
22
|
+
store = new Map();
|
|
23
|
+
world.components.set(intent.entityId, store);
|
|
24
|
+
}
|
|
25
|
+
if (!store.has('Transform')) {
|
|
26
|
+
store.set('Transform', {
|
|
27
|
+
type: 'Transform',
|
|
28
|
+
position: [0, 0, 0],
|
|
29
|
+
rotation: [0, 0, 0, 1],
|
|
30
|
+
scale: [1, 1, 1],
|
|
31
|
+
layerMask: 0xffffffff,
|
|
32
|
+
visible: true,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
intentsToRemove.push(i);
|
|
36
|
+
} else if (intent.type === 'remove-entity') {
|
|
37
|
+
const store = world.components.get(intent.entityId);
|
|
38
|
+
if (store) {
|
|
39
|
+
// Emit disposal commands for all components with IDs
|
|
40
|
+
for (const [type, comp] of store) {
|
|
41
|
+
if ('id' in comp) {
|
|
42
|
+
if (type === 'Model') {
|
|
43
|
+
const modelComp = comp as ModelComponent;
|
|
44
|
+
enqueueCommand(context.worldId, 'cmd-model-dispose', {
|
|
45
|
+
windowId: context.worldId,
|
|
46
|
+
modelId: modelComp.id,
|
|
47
|
+
});
|
|
48
|
+
} else if (type === 'Camera') {
|
|
49
|
+
const cameraComp = comp as CameraComponent;
|
|
50
|
+
enqueueCommand(context.worldId, 'cmd-camera-dispose', {
|
|
51
|
+
windowId: context.worldId,
|
|
52
|
+
cameraId: cameraComp.id,
|
|
53
|
+
});
|
|
54
|
+
} else if (type === 'Light') {
|
|
55
|
+
const lightComp = comp as LightComponent;
|
|
56
|
+
enqueueCommand(context.worldId, 'cmd-light-dispose', {
|
|
57
|
+
windowId: context.worldId,
|
|
58
|
+
lightId: lightComp.id,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
world.components.delete(intent.entityId);
|
|
64
|
+
}
|
|
65
|
+
world.entities.delete(intent.entityId);
|
|
66
|
+
intentsToRemove.push(i);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
71
|
+
const idx = intentsToRemove[i];
|
|
72
|
+
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
@@ -0,0 +1,265 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { EngineCmd } from '../../types/cmds';
|
|
2
|
+
import { enqueueCommand } from '../bridge/dispatch';
|
|
3
|
+
import type { System } from '../ecs';
|
|
4
|
+
|
|
5
|
+
export const DiagnosticsSystem: System = (world, context) => {
|
|
6
|
+
const intentsToRemove: number[] = [];
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
9
|
+
const intent = world.pendingIntents[i];
|
|
10
|
+
if (intent?.type === 'request-resource-list') {
|
|
11
|
+
const type = intent.resourceType;
|
|
12
|
+
const cmdType = `cmd-${type}-list` as EngineCmd['type'];
|
|
13
|
+
enqueueCommand(context.worldId, cmdType, {
|
|
14
|
+
windowId: context.worldId,
|
|
15
|
+
});
|
|
16
|
+
intentsToRemove.push(i);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
21
|
+
const idx = intentsToRemove[i];
|
|
22
|
+
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
23
|
+
}
|
|
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
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { InputMirrorSystem } from './input-mirror';
|
|
2
|
+
export { CommandIntentSystem } from './command-intent';
|
|
3
|
+
export { CoreCommandBuilderSystem } from './core-command-builder';
|
|
4
|
+
export { ResourceUploadSystem } from './resource-upload';
|
|
5
|
+
export { ResponseDecodeSystem } from './response-decode';
|
|
6
|
+
export { WorldLifecycleSystem } from './world-lifecycle';
|
|
7
|
+
export { DiagnosticsSystem } from './diagnostics';
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type { KeyboardEvent } from '../../types/events/keyboard';
|
|
2
|
+
import type { PointerEvent } from '../../types/events/pointer';
|
|
3
|
+
import type { WindowEvent } from '../../types/events/window';
|
|
4
|
+
import type {
|
|
5
|
+
InputStateComponent,
|
|
6
|
+
System,
|
|
7
|
+
WindowStateComponent,
|
|
8
|
+
} from '../ecs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* InputMirrorSystem: Processes input events and updates InputState component
|
|
12
|
+
*/
|
|
13
|
+
export const InputMirrorSystem: System = (world) => {
|
|
14
|
+
// Ensure InputState component exists for the world
|
|
15
|
+
let inputState: InputStateComponent | undefined;
|
|
16
|
+
let windowState: WindowStateComponent | undefined;
|
|
17
|
+
|
|
18
|
+
// Find or create InputState (attached to a special entity ID 0)
|
|
19
|
+
const WORLD_ENTITY_ID = 0;
|
|
20
|
+
let worldStore = world.components.get(WORLD_ENTITY_ID);
|
|
21
|
+
if (!worldStore) {
|
|
22
|
+
worldStore = new Map();
|
|
23
|
+
world.components.set(WORLD_ENTITY_ID, worldStore);
|
|
24
|
+
world.entities.add(WORLD_ENTITY_ID);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
inputState = worldStore.get('InputState') as InputStateComponent;
|
|
28
|
+
if (!inputState) {
|
|
29
|
+
inputState = {
|
|
30
|
+
type: 'InputState',
|
|
31
|
+
keysPressed: new Set(),
|
|
32
|
+
keysJustPressed: new Set(),
|
|
33
|
+
keysJustReleased: new Set(),
|
|
34
|
+
mouseButtons: new Set(),
|
|
35
|
+
mousePosition: [0, 0],
|
|
36
|
+
mouseJustPressed: new Set(),
|
|
37
|
+
mouseJustReleased: new Set(),
|
|
38
|
+
mouseDelta: [0, 0],
|
|
39
|
+
scrollDelta: [0, 0],
|
|
40
|
+
};
|
|
41
|
+
worldStore.set('InputState', inputState);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
windowState = worldStore.get('WindowState') as WindowStateComponent;
|
|
45
|
+
if (!windowState) {
|
|
46
|
+
windowState = {
|
|
47
|
+
type: 'WindowState',
|
|
48
|
+
focused: true,
|
|
49
|
+
size: [800, 600],
|
|
50
|
+
position: [0, 0],
|
|
51
|
+
scaleFactor: 1.0,
|
|
52
|
+
closeRequested: false,
|
|
53
|
+
resizedThisFrame: false,
|
|
54
|
+
movedThisFrame: false,
|
|
55
|
+
focusChangedThisFrame: false,
|
|
56
|
+
};
|
|
57
|
+
worldStore.set('WindowState', windowState);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Clear "just pressed/released" from previous frame
|
|
61
|
+
inputState.keysJustPressed.clear();
|
|
62
|
+
inputState.keysJustReleased.clear();
|
|
63
|
+
inputState.mouseJustPressed.clear();
|
|
64
|
+
inputState.mouseJustReleased.clear();
|
|
65
|
+
inputState.mouseDelta = [0, 0];
|
|
66
|
+
inputState.scrollDelta = [0, 0];
|
|
67
|
+
windowState.resizedThisFrame = false;
|
|
68
|
+
windowState.movedThisFrame = false;
|
|
69
|
+
windowState.focusChangedThisFrame = false;
|
|
70
|
+
windowState.closeRequested = false;
|
|
71
|
+
|
|
72
|
+
// Process inbound events
|
|
73
|
+
while (world.inboundEvents.length > 0) {
|
|
74
|
+
const event = world.inboundEvents.shift();
|
|
75
|
+
if (!event) continue;
|
|
76
|
+
|
|
77
|
+
// Keyboard events
|
|
78
|
+
if (event.type === 'keyboard') {
|
|
79
|
+
const kbEvent = event.content as KeyboardEvent;
|
|
80
|
+
if (kbEvent.event === 'on-input') {
|
|
81
|
+
const keyCode = kbEvent.data.keyCode;
|
|
82
|
+
const pressed = kbEvent.data.state === 'pressed';
|
|
83
|
+
|
|
84
|
+
if (pressed) {
|
|
85
|
+
if (!inputState.keysPressed.has(keyCode)) {
|
|
86
|
+
inputState.keysJustPressed.add(keyCode);
|
|
87
|
+
}
|
|
88
|
+
inputState.keysPressed.add(keyCode);
|
|
89
|
+
} else {
|
|
90
|
+
inputState.keysPressed.delete(keyCode);
|
|
91
|
+
inputState.keysJustReleased.add(keyCode);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pointer events
|
|
97
|
+
else if (event.type === 'pointer') {
|
|
98
|
+
const ptrEvent = event.content as PointerEvent;
|
|
99
|
+
|
|
100
|
+
if (ptrEvent.event === 'on-move') {
|
|
101
|
+
const oldPos = inputState.mousePosition;
|
|
102
|
+
const newPos = ptrEvent.data.position;
|
|
103
|
+
inputState.mouseDelta = [newPos[0] - oldPos[0], newPos[1] - oldPos[1]];
|
|
104
|
+
inputState.mousePosition = newPos;
|
|
105
|
+
} else if (ptrEvent.event === 'on-button') {
|
|
106
|
+
const button = ptrEvent.data.button;
|
|
107
|
+
const pressed = ptrEvent.data.state === 'pressed';
|
|
108
|
+
|
|
109
|
+
if (pressed) {
|
|
110
|
+
if (!inputState.mouseButtons.has(button)) {
|
|
111
|
+
inputState.mouseJustPressed.add(button);
|
|
112
|
+
}
|
|
113
|
+
inputState.mouseButtons.add(button);
|
|
114
|
+
} else {
|
|
115
|
+
inputState.mouseButtons.delete(button);
|
|
116
|
+
inputState.mouseJustReleased.add(button);
|
|
117
|
+
}
|
|
118
|
+
inputState.mousePosition = ptrEvent.data.position;
|
|
119
|
+
} else if (ptrEvent.event === 'on-scroll') {
|
|
120
|
+
const delta = ptrEvent.data.delta;
|
|
121
|
+
if (delta.type === 'line') {
|
|
122
|
+
inputState.scrollDelta = delta.value;
|
|
123
|
+
} else if (delta.type === 'pixel') {
|
|
124
|
+
// Convert pixel to approximate line delta (rough estimate)
|
|
125
|
+
inputState.scrollDelta = [delta.value[0] / 20, delta.value[1] / 20];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Window events
|
|
131
|
+
else if (event.type === 'window') {
|
|
132
|
+
const winEvent = event.content as WindowEvent;
|
|
133
|
+
|
|
134
|
+
if (winEvent.event === 'on-close-request') {
|
|
135
|
+
windowState.closeRequested = true;
|
|
136
|
+
} else if (winEvent.event === 'on-focus') {
|
|
137
|
+
if (windowState.focused !== winEvent.data.focused) {
|
|
138
|
+
windowState.focusChangedThisFrame = true;
|
|
139
|
+
}
|
|
140
|
+
windowState.focused = winEvent.data.focused;
|
|
141
|
+
} else if (winEvent.event === 'on-resize') {
|
|
142
|
+
windowState.size = [winEvent.data.width, winEvent.data.height];
|
|
143
|
+
windowState.resizedThisFrame = true;
|
|
144
|
+
} else if (winEvent.event === 'on-move') {
|
|
145
|
+
windowState.position = winEvent.data.position;
|
|
146
|
+
windowState.movedThisFrame = true;
|
|
147
|
+
} else if (winEvent.event === 'on-scale-factor-change') {
|
|
148
|
+
windowState.scaleFactor = winEvent.data.scaleFactor;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { TextureCreateMode } from '../../types/kinds';
|
|
2
|
+
import { enqueueCommand } from '../bridge/dispatch';
|
|
3
|
+
import type { System } from '../ecs';
|
|
4
|
+
import { normalizeMaterialOptions, normalizePrimitiveOptions } from './utils';
|
|
5
|
+
|
|
6
|
+
export const ResourceUploadSystem: System = (world, context) => {
|
|
7
|
+
const intentsToRemove: number[] = [];
|
|
8
|
+
|
|
9
|
+
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
10
|
+
const intent = world.pendingIntents[i];
|
|
11
|
+
if (!intent) continue;
|
|
12
|
+
|
|
13
|
+
if (intent.type === 'create-material') {
|
|
14
|
+
const options = normalizeMaterialOptions(intent.props.options) || {
|
|
15
|
+
type: 'standard',
|
|
16
|
+
content: {
|
|
17
|
+
baseColor: [1, 1, 1, 1],
|
|
18
|
+
surfaceType: 'opaque',
|
|
19
|
+
flags: 0,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
enqueueCommand(context.worldId, 'cmd-material-create', {
|
|
24
|
+
windowId: context.worldId,
|
|
25
|
+
materialId: intent.resourceId,
|
|
26
|
+
label: intent.props.label || `Mat ${intent.resourceId}`,
|
|
27
|
+
kind: intent.props.kind ?? 'standard',
|
|
28
|
+
options: options,
|
|
29
|
+
});
|
|
30
|
+
intentsToRemove.push(i);
|
|
31
|
+
} else if (intent.type === 'dispose-material') {
|
|
32
|
+
enqueueCommand(context.worldId, 'cmd-material-dispose', {
|
|
33
|
+
windowId: context.worldId,
|
|
34
|
+
materialId: intent.resourceId,
|
|
35
|
+
});
|
|
36
|
+
intentsToRemove.push(i);
|
|
37
|
+
} else if (intent.type === 'create-geometry') {
|
|
38
|
+
if (intent.props.type === 'primitive') {
|
|
39
|
+
const shape = intent.props.shape ?? 'cube';
|
|
40
|
+
let content: any;
|
|
41
|
+
|
|
42
|
+
const shapeOptions = intent.props.options;
|
|
43
|
+
if (shapeOptions) {
|
|
44
|
+
content = shapeOptions;
|
|
45
|
+
} else {
|
|
46
|
+
if (shape === 'cube' || shape === 'plane' || shape === 'pyramid') {
|
|
47
|
+
content = { size: [1.0, 1.0, 1.0], subdivisions: 1 };
|
|
48
|
+
} else if (shape === 'sphere') {
|
|
49
|
+
content = { radius: 0.5, sectors: 32, stacks: 16 };
|
|
50
|
+
} else if (shape === 'torus') {
|
|
51
|
+
content = {
|
|
52
|
+
majorRadius: 0.5,
|
|
53
|
+
minorRadius: 0.25,
|
|
54
|
+
majorSegments: 32,
|
|
55
|
+
minorSegments: 16,
|
|
56
|
+
};
|
|
57
|
+
} else if (shape === 'cylinder') {
|
|
58
|
+
content = { radius: 0.5, height: 1.0, sectors: 32 };
|
|
59
|
+
} else {
|
|
60
|
+
content = { size: [1.0, 1.0, 1.0], subdivisions: 1 };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const options = normalizePrimitiveOptions({
|
|
65
|
+
type: shape,
|
|
66
|
+
content: content,
|
|
67
|
+
} as any);
|
|
68
|
+
|
|
69
|
+
enqueueCommand(context.worldId, 'cmd-primitive-geometry-create', {
|
|
70
|
+
windowId: context.worldId,
|
|
71
|
+
geometryId: intent.resourceId,
|
|
72
|
+
label: intent.props.label || `Geo ${intent.resourceId}`,
|
|
73
|
+
shape: shape,
|
|
74
|
+
options: options,
|
|
75
|
+
});
|
|
76
|
+
} else {
|
|
77
|
+
enqueueCommand(context.worldId, 'cmd-geometry-create', {
|
|
78
|
+
windowId: context.worldId,
|
|
79
|
+
geometryId: intent.resourceId,
|
|
80
|
+
label: intent.props.label || `Geo ${intent.resourceId}`,
|
|
81
|
+
entries: intent.props.entries,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
intentsToRemove.push(i);
|
|
85
|
+
} else if (intent.type === 'dispose-geometry') {
|
|
86
|
+
enqueueCommand(context.worldId, 'cmd-geometry-dispose', {
|
|
87
|
+
windowId: context.worldId,
|
|
88
|
+
geometryId: intent.resourceId,
|
|
89
|
+
});
|
|
90
|
+
intentsToRemove.push(i);
|
|
91
|
+
} else if (intent.type === 'create-texture') {
|
|
92
|
+
if (intent.props.source.type === 'color') {
|
|
93
|
+
enqueueCommand(context.worldId, 'cmd-texture-create-solid-color', {
|
|
94
|
+
windowId: context.worldId,
|
|
95
|
+
textureId: intent.resourceId,
|
|
96
|
+
label: intent.props.label || `Tex ${intent.resourceId}`,
|
|
97
|
+
color: Array.from(intent.props.source.color) as [
|
|
98
|
+
number,
|
|
99
|
+
number,
|
|
100
|
+
number,
|
|
101
|
+
number,
|
|
102
|
+
],
|
|
103
|
+
srgb: intent.props.srgb ?? true,
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
const cmd: {
|
|
107
|
+
windowId: number;
|
|
108
|
+
textureId: number;
|
|
109
|
+
label: string;
|
|
110
|
+
bufferId: number;
|
|
111
|
+
srgb: boolean;
|
|
112
|
+
mode?: TextureCreateMode;
|
|
113
|
+
atlasOptions?: { tilePx: number; layers: number };
|
|
114
|
+
} = {
|
|
115
|
+
windowId: context.worldId,
|
|
116
|
+
textureId: intent.resourceId,
|
|
117
|
+
label: intent.props.label || `Tex ${intent.resourceId}`,
|
|
118
|
+
bufferId: intent.props.source.bufferId,
|
|
119
|
+
srgb: intent.props.srgb ?? true,
|
|
120
|
+
};
|
|
121
|
+
if (intent.props.mode !== undefined) {
|
|
122
|
+
cmd.mode = intent.props.mode;
|
|
123
|
+
}
|
|
124
|
+
if (intent.props.atlasOptions !== undefined) {
|
|
125
|
+
cmd.atlasOptions = intent.props.atlasOptions;
|
|
126
|
+
}
|
|
127
|
+
enqueueCommand(context.worldId, 'cmd-texture-create-from-buffer', cmd);
|
|
128
|
+
}
|
|
129
|
+
intentsToRemove.push(i);
|
|
130
|
+
} else if (intent.type === 'dispose-texture') {
|
|
131
|
+
enqueueCommand(context.worldId, 'cmd-texture-dispose', {
|
|
132
|
+
windowId: context.worldId,
|
|
133
|
+
textureId: intent.resourceId,
|
|
134
|
+
});
|
|
135
|
+
intentsToRemove.push(i);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
140
|
+
const idx = intentsToRemove[i];
|
|
141
|
+
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
142
|
+
}
|
|
143
|
+
};
|