@vulfram/engine 0.14.8-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 (63) hide show
  1. package/README.md +106 -0
  2. package/package.json +60 -4
  3. package/src/core.ts +14 -0
  4. package/src/ecs.ts +1 -0
  5. package/src/engine/api.ts +222 -24
  6. package/src/engine/bridge/dispatch.ts +260 -40
  7. package/src/engine/bridge/guards.ts +4 -1
  8. package/src/engine/bridge/protocol.ts +69 -52
  9. package/src/engine/ecs/components.ts +340 -0
  10. package/src/engine/ecs/index.ts +3 -518
  11. package/src/engine/ecs/intents.ts +184 -0
  12. package/src/engine/ecs/systems.ts +26 -0
  13. package/src/engine/intents/store.ts +72 -0
  14. package/src/engine/state.ts +136 -5
  15. package/src/engine/systems/command-intent.ts +159 -14
  16. package/src/engine/systems/constraint-solve.ts +167 -0
  17. package/src/engine/systems/core-command-builder.ts +9 -268
  18. package/src/engine/systems/diagnostics.ts +20 -29
  19. package/src/engine/systems/index.ts +3 -1
  20. package/src/engine/systems/input-mirror.ts +257 -21
  21. package/src/engine/systems/resource-upload.ts +108 -58
  22. package/src/engine/systems/response-decode.ts +86 -15
  23. package/src/engine/systems/scene-sync.ts +305 -0
  24. package/src/engine/systems/ui-bridge.ts +381 -0
  25. package/src/engine/systems/utils.ts +86 -1
  26. package/src/engine/systems/world-lifecycle.ts +43 -114
  27. package/src/engine/window/manager.ts +168 -0
  28. package/src/engine/world/entities.ts +998 -91
  29. package/src/engine/world/mount.ts +195 -0
  30. package/src/engine/world/types.ts +71 -0
  31. package/src/engine/world/world-ui.ts +313 -0
  32. package/src/engine/world/world3d.ts +529 -0
  33. package/src/helpers/collision.ts +487 -0
  34. package/src/helpers/index.ts +2 -0
  35. package/src/helpers/raycast.ts +442 -0
  36. package/src/index.ts +30 -1
  37. package/src/mount.ts +2 -0
  38. package/src/types/cmds/audio.ts +73 -48
  39. package/src/types/cmds/camera.ts +12 -8
  40. package/src/types/cmds/environment.ts +9 -3
  41. package/src/types/cmds/geometry.ts +15 -16
  42. package/src/types/cmds/index.ts +234 -162
  43. package/src/types/cmds/input.ts +39 -0
  44. package/src/types/cmds/light.ts +12 -11
  45. package/src/types/cmds/material.ts +19 -21
  46. package/src/types/cmds/model.ts +17 -15
  47. package/src/types/cmds/realm.ts +23 -0
  48. package/src/types/cmds/system.ts +29 -0
  49. package/src/types/cmds/target.ts +96 -0
  50. package/src/types/cmds/texture.ts +13 -3
  51. package/src/types/cmds/ui.ts +220 -0
  52. package/src/types/cmds/window.ts +41 -204
  53. package/src/types/events/index.ts +4 -1
  54. package/src/types/events/keyboard.ts +2 -2
  55. package/src/types/events/pointer.ts +85 -13
  56. package/src/types/events/system.ts +188 -30
  57. package/src/types/events/ui.ts +21 -0
  58. package/src/types/index.ts +1 -0
  59. package/src/types/json.ts +15 -0
  60. package/src/window.ts +8 -0
  61. package/src/world-ui.ts +2 -0
  62. package/src/world3d.ts +10 -0
  63. package/tsconfig.json +0 -29
@@ -1,19 +1,110 @@
1
1
  import type { KeyboardEvent } from '../../types/events/keyboard';
2
2
  import type { PointerEvent } from '../../types/events/pointer';
3
+ import type { GamepadEvent } from '../../types/events/gamepad';
4
+ import type { SystemEvent } from '../../types/events/system';
5
+ import type { UiEvent } from '../../types/events/ui';
3
6
  import type { WindowEvent } from '../../types/events/window';
4
7
  import type {
8
+ GamepadStateComponent,
5
9
  InputStateComponent,
10
+ SystemEventStateComponent,
6
11
  System,
12
+ UiEventStateComponent,
7
13
  WindowStateComponent,
8
14
  } from '../ecs';
9
15
 
10
16
  /**
11
- * InputMirrorSystem: Processes input events and updates InputState component
17
+ * Mirrors inbound core events into world-level ECS state components.
18
+ *
19
+ * This system owns runtime input snapshots for keyboard, pointer, window,
20
+ * gamepad, system, and UI event streams.
12
21
  */
13
22
  export const InputMirrorSystem: System = (world) => {
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
+
14
102
  // Ensure InputState component exists for the world
15
103
  let inputState: InputStateComponent | undefined;
16
104
  let windowState: WindowStateComponent | undefined;
105
+ let gamepadState: GamepadStateComponent | undefined;
106
+ let systemEventState: SystemEventStateComponent | undefined;
107
+ let uiEventState: UiEventStateComponent | undefined;
17
108
 
18
109
  // Find or create InputState (attached to a special entity ID 0)
19
110
  const WORLD_ENTITY_ID = 0;
@@ -31,12 +122,15 @@ export const InputMirrorSystem: System = (world) => {
31
122
  keysPressed: new Set(),
32
123
  keysJustPressed: new Set(),
33
124
  keysJustReleased: new Set(),
34
- mouseButtons: new Set(),
35
- mousePosition: [0, 0],
36
- mouseJustPressed: new Set(),
37
- mouseJustReleased: new Set(),
38
- 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,
39
132
  scrollDelta: [0, 0],
133
+ imeEnabled: false,
40
134
  };
41
135
  worldStore.set('InputState', inputState);
42
136
  }
@@ -57,21 +151,60 @@ export const InputMirrorSystem: System = (world) => {
57
151
  worldStore.set('WindowState', windowState);
58
152
  }
59
153
 
154
+ gamepadState = worldStore.get('GamepadState') as GamepadStateComponent;
155
+ if (!gamepadState) {
156
+ gamepadState = {
157
+ type: 'GamepadState',
158
+ connected: new Map(),
159
+ buttons: new Map(),
160
+ axes: new Map(),
161
+ eventsThisFrame: [],
162
+ };
163
+ worldStore.set('GamepadState', gamepadState);
164
+ }
165
+
166
+ systemEventState = worldStore.get(
167
+ 'SystemEventState',
168
+ ) as SystemEventStateComponent;
169
+ if (!systemEventState) {
170
+ systemEventState = {
171
+ type: 'SystemEventState',
172
+ eventsThisFrame: [],
173
+ };
174
+ worldStore.set('SystemEventState', systemEventState);
175
+ }
176
+
177
+ uiEventState = worldStore.get('UiEventState') as UiEventStateComponent;
178
+ if (!uiEventState) {
179
+ uiEventState = {
180
+ type: 'UiEventState',
181
+ eventsThisFrame: [],
182
+ };
183
+ worldStore.set('UiEventState', uiEventState);
184
+ }
185
+
60
186
  // Clear "just pressed/released" from previous frame
61
187
  inputState.keysJustPressed.clear();
62
188
  inputState.keysJustReleased.clear();
63
- inputState.mouseJustPressed.clear();
64
- inputState.mouseJustReleased.clear();
65
- 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;
66
195
  inputState.scrollDelta = [0, 0];
196
+ inputState.imeCommitText = undefined;
67
197
  windowState.resizedThisFrame = false;
68
198
  windowState.movedThisFrame = false;
69
199
  windowState.focusChangedThisFrame = false;
70
200
  windowState.closeRequested = false;
201
+ gamepadState.eventsThisFrame.length = 0;
202
+ systemEventState.eventsThisFrame.length = 0;
203
+ uiEventState.eventsThisFrame.length = 0;
71
204
 
72
205
  // Process inbound events
73
- while (world.inboundEvents.length > 0) {
74
- const event = world.inboundEvents.shift();
206
+ for (let i = 0; i < world.inboundEvents.length; i++) {
207
+ const event = world.inboundEvents[i];
75
208
  if (!event) continue;
76
209
 
77
210
  // Keyboard events
@@ -90,6 +223,21 @@ export const InputMirrorSystem: System = (world) => {
90
223
  inputState.keysPressed.delete(keyCode);
91
224
  inputState.keysJustReleased.add(keyCode);
92
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;
93
241
  }
94
242
  }
95
243
 
@@ -98,25 +246,43 @@ export const InputMirrorSystem: System = (world) => {
98
246
  const ptrEvent = event.content as PointerEvent;
99
247
 
100
248
  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;
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
+ );
105
259
  } else if (ptrEvent.event === 'on-button') {
106
260
  const button = ptrEvent.data.button;
107
261
  const pressed = ptrEvent.data.state === 'pressed';
108
262
 
109
263
  if (pressed) {
110
- if (!inputState.mouseButtons.has(button)) {
111
- inputState.mouseJustPressed.add(button);
264
+ if (!inputState.pointerButtons.has(button)) {
265
+ inputState.pointerJustPressed.add(button);
112
266
  }
113
- inputState.mouseButtons.add(button);
267
+ inputState.pointerButtons.add(button);
114
268
  } else {
115
- inputState.mouseButtons.delete(button);
116
- inputState.mouseJustReleased.add(button);
269
+ inputState.pointerButtons.delete(button);
270
+ inputState.pointerJustReleased.add(button);
117
271
  }
118
- 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
+ );
119
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);
120
286
  const delta = ptrEvent.data.delta;
121
287
  if (delta.type === 'line') {
122
288
  inputState.scrollDelta = delta.value;
@@ -124,6 +290,25 @@ export const InputMirrorSystem: System = (world) => {
124
290
  // Convert pixel to approximate line delta (rough estimate)
125
291
  inputState.scrollDelta = [delta.value[0] / 20, delta.value[1] / 20];
126
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);
127
312
  }
128
313
  }
129
314
 
@@ -148,5 +333,56 @@ export const InputMirrorSystem: System = (world) => {
148
333
  windowState.scaleFactor = winEvent.data.scaleFactor;
149
334
  }
150
335
  }
336
+ // Gamepad events
337
+ else if (event.type === 'gamepad') {
338
+ const gpEvent = event.content as GamepadEvent;
339
+ gamepadState.eventsThisFrame.push(gpEvent);
340
+ if (gpEvent.event === 'on-connect') {
341
+ gamepadState.connected.set(gpEvent.data.gamepadId, {
342
+ name: gpEvent.data.name,
343
+ });
344
+ } else if (gpEvent.event === 'on-disconnect') {
345
+ gamepadState.connected.delete(gpEvent.data.gamepadId);
346
+ gamepadState.buttons.delete(gpEvent.data.gamepadId);
347
+ gamepadState.axes.delete(gpEvent.data.gamepadId);
348
+ } else if (gpEvent.event === 'on-button') {
349
+ const id = gpEvent.data.gamepadId;
350
+ let buttons = gamepadState.buttons.get(id);
351
+ if (!buttons) {
352
+ buttons = new Map();
353
+ gamepadState.buttons.set(id, buttons);
354
+ }
355
+ buttons.set(gpEvent.data.button, {
356
+ pressed: gpEvent.data.state === 'pressed',
357
+ value: gpEvent.data.value,
358
+ });
359
+ } else if (gpEvent.event === 'on-axis') {
360
+ const id = gpEvent.data.gamepadId;
361
+ let axes = gamepadState.axes.get(id);
362
+ if (!axes) {
363
+ axes = new Map();
364
+ gamepadState.axes.set(id, axes);
365
+ }
366
+ axes.set(gpEvent.data.axis, gpEvent.data.value);
367
+ }
368
+ }
369
+ // System events
370
+ else if (event.type === 'system') {
371
+ const sysEvent = event.content as SystemEvent;
372
+ systemEventState.eventsThisFrame.push(sysEvent);
373
+ if (sysEvent.event === 'error') {
374
+ systemEventState.lastError = {
375
+ scope: sysEvent.data.scope,
376
+ message: sysEvent.data.message,
377
+ commandId: sysEvent.data.commandId,
378
+ commandType: sysEvent.data.commandType,
379
+ };
380
+ }
381
+ }
382
+ // UI events
383
+ else if (event.type === 'ui') {
384
+ uiEventState.eventsThisFrame.push(event.content as UiEvent);
385
+ }
151
386
  }
387
+ world.inboundEvents.length = 0;
152
388
  };
@@ -1,13 +1,95 @@
1
1
  import type { TextureCreateMode } from '../../types/kinds';
2
+ import type {
3
+ PrimitiveOptions,
4
+ } from '../../types/cmds/geometry';
2
5
  import { enqueueCommand } from '../bridge/dispatch';
3
- import type { System } from '../ecs';
6
+ import type { GeometryProps, System } from '../ecs';
4
7
  import { normalizeMaterialOptions, normalizePrimitiveOptions } from './utils';
5
8
 
9
+ const RESOURCE_INTENT_TYPES = [
10
+ 'create-material',
11
+ 'dispose-material',
12
+ 'create-geometry',
13
+ 'dispose-geometry',
14
+ 'create-texture',
15
+ 'dispose-texture',
16
+ ] as const;
17
+
18
+ function buildPrimitiveOptions(
19
+ primitiveProps: Extract<GeometryProps, { type: 'primitive' }>,
20
+ ): PrimitiveOptions {
21
+ const { shape, options } = primitiveProps;
22
+ if (shape === 'cube') {
23
+ return {
24
+ type: 'cube',
25
+ content: {
26
+ size: options?.size ?? [1.0, 1.0, 1.0],
27
+ subdivisions: 1,
28
+ },
29
+ };
30
+ }
31
+ if (shape === 'plane') {
32
+ return {
33
+ type: 'plane',
34
+ content: {
35
+ size: options?.size ?? [1.0, 1.0, 1.0],
36
+ subdivisions: options?.subdivisions ?? 1,
37
+ },
38
+ };
39
+ }
40
+ if (shape === 'sphere') {
41
+ return {
42
+ type: 'sphere',
43
+ content: {
44
+ radius: options?.radius ?? 0.5,
45
+ sectors: options?.sectors ?? 32,
46
+ stacks: options?.stacks ?? 16,
47
+ },
48
+ };
49
+ }
50
+ if (shape === 'cylinder') {
51
+ return {
52
+ type: 'cylinder',
53
+ content: {
54
+ radius: options?.radius ?? 0.5,
55
+ height: options?.height ?? 1.0,
56
+ sectors: options?.sectors ?? options?.segments ?? 32,
57
+ },
58
+ };
59
+ }
60
+ if (shape === 'torus') {
61
+ return {
62
+ type: 'torus',
63
+ content: {
64
+ majorRadius: options?.majorRadius ?? 0.5,
65
+ minorRadius: options?.minorRadius ?? 0.25,
66
+ majorSegments: options?.majorSegments ?? options?.radialSegments ?? 32,
67
+ minorSegments: options?.minorSegments ?? options?.tubularSegments ?? 16,
68
+ },
69
+ };
70
+ }
71
+ return {
72
+ type: 'pyramid',
73
+ content: {
74
+ size: options?.size ?? [1.0, 1.0, 1.0],
75
+ subdivisions: options?.subdivisions ?? 1,
76
+ },
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Processes resource intents and emits corresponding core resource commands.
82
+ *
83
+ * Covered resources:
84
+ * - material
85
+ * - geometry (custom and primitive)
86
+ * - texture (from buffer or solid color)
87
+ */
6
88
  export const ResourceUploadSystem: System = (world, context) => {
7
- const intentsToRemove: number[] = [];
89
+ const intents = world.intentStore.takeMany(RESOURCE_INTENT_TYPES);
8
90
 
9
- for (let i = 0; i < world.pendingIntents.length; i++) {
10
- const intent = world.pendingIntents[i];
91
+ for (let i = 0; i < intents.length; i++) {
92
+ const intent = intents[i];
11
93
  if (!intent) continue;
12
94
 
13
95
  if (intent.type === 'create-material') {
@@ -20,78 +102,49 @@ export const ResourceUploadSystem: System = (world, context) => {
20
102
  },
21
103
  };
22
104
 
23
- enqueueCommand(context.worldId, 'cmd-material-create', {
24
- windowId: context.worldId,
105
+ enqueueCommand(context.worldId, 'cmd-material-upsert', {
25
106
  materialId: intent.resourceId,
26
107
  label: intent.props.label || `Mat ${intent.resourceId}`,
27
108
  kind: intent.props.kind ?? 'standard',
28
109
  options: options,
29
110
  });
30
- intentsToRemove.push(i);
31
111
  } else if (intent.type === 'dispose-material') {
32
112
  enqueueCommand(context.worldId, 'cmd-material-dispose', {
33
- windowId: context.worldId,
34
113
  materialId: intent.resourceId,
35
114
  });
36
- intentsToRemove.push(i);
37
115
  } else if (intent.type === 'create-geometry') {
38
116
  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);
117
+ const options = normalizePrimitiveOptions(
118
+ buildPrimitiveOptions(intent.props),
119
+ );
68
120
 
69
121
  enqueueCommand(context.worldId, 'cmd-primitive-geometry-create', {
70
- windowId: context.worldId,
71
122
  geometryId: intent.resourceId,
72
123
  label: intent.props.label || `Geo ${intent.resourceId}`,
73
- shape: shape,
124
+ shape: intent.props.shape,
74
125
  options: options,
75
126
  });
76
127
  } else {
77
- enqueueCommand(context.worldId, 'cmd-geometry-create', {
78
- windowId: context.worldId,
128
+ enqueueCommand(context.worldId, 'cmd-geometry-upsert', {
79
129
  geometryId: intent.resourceId,
80
130
  label: intent.props.label || `Geo ${intent.resourceId}`,
81
131
  entries: intent.props.entries,
82
132
  });
83
133
  }
84
- intentsToRemove.push(i);
85
134
  } else if (intent.type === 'dispose-geometry') {
86
135
  enqueueCommand(context.worldId, 'cmd-geometry-dispose', {
87
- windowId: context.worldId,
88
136
  geometryId: intent.resourceId,
89
137
  });
90
- intentsToRemove.push(i);
91
138
  } else if (intent.type === 'create-texture') {
92
139
  if (intent.props.source.type === 'color') {
93
- enqueueCommand(context.worldId, 'cmd-texture-create-solid-color', {
94
- windowId: context.worldId,
140
+ const cmd: {
141
+ textureId: number;
142
+ label: string;
143
+ color: [number, number, number, number];
144
+ srgb: boolean;
145
+ mode?: TextureCreateMode;
146
+ atlasOptions?: { tilePx: number; layers: number };
147
+ } = {
95
148
  textureId: intent.resourceId,
96
149
  label: intent.props.label || `Tex ${intent.resourceId}`,
97
150
  color: Array.from(intent.props.source.color) as [
@@ -101,10 +154,16 @@ export const ResourceUploadSystem: System = (world, context) => {
101
154
  number,
102
155
  ],
103
156
  srgb: intent.props.srgb ?? true,
104
- });
157
+ };
158
+ if (intent.props.mode !== undefined) {
159
+ cmd.mode = intent.props.mode;
160
+ }
161
+ if (intent.props.atlasOptions !== undefined) {
162
+ cmd.atlasOptions = intent.props.atlasOptions;
163
+ }
164
+ enqueueCommand(context.worldId, 'cmd-texture-create-solid-color', cmd);
105
165
  } else {
106
166
  const cmd: {
107
- windowId: number;
108
167
  textureId: number;
109
168
  label: string;
110
169
  bufferId: number;
@@ -112,7 +171,6 @@ export const ResourceUploadSystem: System = (world, context) => {
112
171
  mode?: TextureCreateMode;
113
172
  atlasOptions?: { tilePx: number; layers: number };
114
173
  } = {
115
- windowId: context.worldId,
116
174
  textureId: intent.resourceId,
117
175
  label: intent.props.label || `Tex ${intent.resourceId}`,
118
176
  bufferId: intent.props.source.bufferId,
@@ -126,18 +184,10 @@ export const ResourceUploadSystem: System = (world, context) => {
126
184
  }
127
185
  enqueueCommand(context.worldId, 'cmd-texture-create-from-buffer', cmd);
128
186
  }
129
- intentsToRemove.push(i);
130
187
  } else if (intent.type === 'dispose-texture') {
131
188
  enqueueCommand(context.worldId, 'cmd-texture-dispose', {
132
- windowId: context.worldId,
133
189
  textureId: intent.resourceId,
134
190
  });
135
- intentsToRemove.push(i);
136
191
  }
137
192
  }
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
193
  };