@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.
Files changed (40) hide show
  1. package/package.json +17 -0
  2. package/src/engine/api.ts +282 -0
  3. package/src/engine/bridge/dispatch.ts +115 -0
  4. package/src/engine/bridge/guards.ts +28 -0
  5. package/src/engine/bridge/protocol.ts +130 -0
  6. package/src/engine/ecs/index.ts +516 -0
  7. package/src/engine/errors.ts +10 -0
  8. package/src/engine/state.ts +135 -0
  9. package/src/engine/systems/command-intent.ts +74 -0
  10. package/src/engine/systems/core-command-builder.ts +265 -0
  11. package/src/engine/systems/diagnostics.ts +42 -0
  12. package/src/engine/systems/index.ts +7 -0
  13. package/src/engine/systems/input-mirror.ts +152 -0
  14. package/src/engine/systems/resource-upload.ts +143 -0
  15. package/src/engine/systems/response-decode.ts +28 -0
  16. package/src/engine/systems/utils.ts +147 -0
  17. package/src/engine/systems/world-lifecycle.ts +164 -0
  18. package/src/engine/world/entities.ts +516 -0
  19. package/src/index.ts +9 -0
  20. package/src/types/cmds/camera.ts +76 -0
  21. package/src/types/cmds/environment.ts +45 -0
  22. package/src/types/cmds/geometry.ts +144 -0
  23. package/src/types/cmds/gizmo.ts +18 -0
  24. package/src/types/cmds/index.ts +231 -0
  25. package/src/types/cmds/light.ts +69 -0
  26. package/src/types/cmds/material.ts +98 -0
  27. package/src/types/cmds/model.ts +59 -0
  28. package/src/types/cmds/shadow.ts +22 -0
  29. package/src/types/cmds/system.ts +15 -0
  30. package/src/types/cmds/texture.ts +63 -0
  31. package/src/types/cmds/window.ts +263 -0
  32. package/src/types/events/gamepad.ts +34 -0
  33. package/src/types/events/index.ts +19 -0
  34. package/src/types/events/keyboard.ts +225 -0
  35. package/src/types/events/pointer.ts +105 -0
  36. package/src/types/events/system.ts +13 -0
  37. package/src/types/events/window.ts +96 -0
  38. package/src/types/index.ts +3 -0
  39. package/src/types/kinds.ts +111 -0
  40. 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
+ };