@vulfram/engine 0.17.1-alpha → 0.19.3-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +3 -3
  2. package/package.json +10 -5
  3. package/src/engine/api.ts +12 -25
  4. package/src/engine/bridge/dispatch.ts +3 -8
  5. package/src/engine/ecs/components.ts +340 -0
  6. package/src/engine/ecs/index.ts +3 -661
  7. package/src/engine/ecs/intents.ts +184 -0
  8. package/src/engine/ecs/systems.ts +26 -0
  9. package/src/engine/intents/store.ts +72 -0
  10. package/src/engine/state.ts +3 -3
  11. package/src/engine/systems/command-intent.ts +11 -16
  12. package/src/engine/systems/diagnostics.ts +3 -13
  13. package/src/engine/systems/input-mirror.ts +156 -18
  14. package/src/engine/systems/resource-upload.ts +12 -14
  15. package/src/engine/systems/response-decode.ts +17 -0
  16. package/src/engine/systems/scene-sync.ts +12 -13
  17. package/src/engine/systems/ui-bridge.ts +31 -10
  18. package/src/engine/systems/utils.ts +46 -3
  19. package/src/engine/systems/world-lifecycle.ts +9 -15
  20. package/src/engine/world/entities.ts +201 -37
  21. package/src/engine/world/mount.ts +27 -6
  22. package/src/engine/world/world-ui.ts +77 -30
  23. package/src/engine/world/world3d.ts +282 -33
  24. package/src/helpers/collision.ts +487 -0
  25. package/src/helpers/index.ts +2 -0
  26. package/src/helpers/raycast.ts +442 -0
  27. package/src/types/cmds/geometry.ts +2 -2
  28. package/src/types/cmds/index.ts +42 -0
  29. package/src/types/cmds/input.ts +39 -0
  30. package/src/types/cmds/material.ts +10 -10
  31. package/src/types/cmds/realm.ts +0 -2
  32. package/src/types/cmds/system.ts +10 -0
  33. package/src/types/cmds/target.ts +14 -0
  34. package/src/types/events/keyboard.ts +2 -2
  35. package/src/types/events/pointer.ts +43 -0
  36. package/src/types/events/system.ts +44 -0
@@ -6,6 +6,15 @@ import { enqueueCommand } from '../bridge/dispatch';
6
6
  import type { GeometryProps, System } from '../ecs';
7
7
  import { normalizeMaterialOptions, normalizePrimitiveOptions } from './utils';
8
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
+
9
18
  function buildPrimitiveOptions(
10
19
  primitiveProps: Extract<GeometryProps, { type: 'primitive' }>,
11
20
  ): PrimitiveOptions {
@@ -77,10 +86,10 @@ function buildPrimitiveOptions(
77
86
  * - texture (from buffer or solid color)
78
87
  */
79
88
  export const ResourceUploadSystem: System = (world, context) => {
80
- const intentsToRemove: number[] = [];
89
+ const intents = world.intentStore.takeMany(RESOURCE_INTENT_TYPES);
81
90
 
82
- for (let i = 0; i < world.pendingIntents.length; i++) {
83
- const intent = world.pendingIntents[i];
91
+ for (let i = 0; i < intents.length; i++) {
92
+ const intent = intents[i];
84
93
  if (!intent) continue;
85
94
 
86
95
  if (intent.type === 'create-material') {
@@ -99,12 +108,10 @@ export const ResourceUploadSystem: System = (world, context) => {
99
108
  kind: intent.props.kind ?? 'standard',
100
109
  options: options,
101
110
  });
102
- intentsToRemove.push(i);
103
111
  } else if (intent.type === 'dispose-material') {
104
112
  enqueueCommand(context.worldId, 'cmd-material-dispose', {
105
113
  materialId: intent.resourceId,
106
114
  });
107
- intentsToRemove.push(i);
108
115
  } else if (intent.type === 'create-geometry') {
109
116
  if (intent.props.type === 'primitive') {
110
117
  const options = normalizePrimitiveOptions(
@@ -124,12 +131,10 @@ export const ResourceUploadSystem: System = (world, context) => {
124
131
  entries: intent.props.entries,
125
132
  });
126
133
  }
127
- intentsToRemove.push(i);
128
134
  } else if (intent.type === 'dispose-geometry') {
129
135
  enqueueCommand(context.worldId, 'cmd-geometry-dispose', {
130
136
  geometryId: intent.resourceId,
131
137
  });
132
- intentsToRemove.push(i);
133
138
  } else if (intent.type === 'create-texture') {
134
139
  if (intent.props.source.type === 'color') {
135
140
  const cmd: {
@@ -179,17 +184,10 @@ export const ResourceUploadSystem: System = (world, context) => {
179
184
  }
180
185
  enqueueCommand(context.worldId, 'cmd-texture-create-from-buffer', cmd);
181
186
  }
182
- intentsToRemove.push(i);
183
187
  } else if (intent.type === 'dispose-texture') {
184
188
  enqueueCommand(context.worldId, 'cmd-texture-dispose', {
185
189
  textureId: intent.resourceId,
186
190
  });
187
- intentsToRemove.push(i);
188
191
  }
189
192
  }
190
-
191
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
192
- const idx = intentsToRemove[i];
193
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
194
- }
195
193
  };
@@ -5,6 +5,21 @@ import { engineState } from '../state';
5
5
  const MAX_REALM_CREATE_RETRIES = 8;
6
6
  const BASE_REALM_RETRY_DELAY_MS = 32;
7
7
 
8
+ function flushPendingTargetLayerBinds(world: Parameters<System>[0], worldId: number): void {
9
+ const realmId = world.coreRealmId;
10
+ if (realmId === undefined) return;
11
+
12
+ for (const binding of world.targetLayerBindings.values()) {
13
+ enqueueCommand(worldId, 'cmd-target-layer-upsert', {
14
+ realmId,
15
+ targetId: binding.targetId,
16
+ layout: binding.layout,
17
+ cameraId: binding.cameraId,
18
+ environmentId: binding.environmentId,
19
+ });
20
+ }
21
+ }
22
+
8
23
  /**
9
24
  * Decodes and applies command responses routed to a world.
10
25
  *
@@ -57,6 +72,7 @@ export const ResponseDecodeSystem: System = (world, context) => {
57
72
  world.realmCreateRetryCount = 0;
58
73
  world.nextRealmCreateRetryAtMs = 0;
59
74
  markRoutingIndexDirty();
75
+ flushPendingTargetLayerBinds(world, context.worldId);
60
76
  }
61
77
  } else if (res.type === 'window-create') {
62
78
  const created = res.content as {
@@ -69,6 +85,7 @@ export const ResponseDecodeSystem: System = (world, context) => {
69
85
  world.realmCreateRetryCount = 0;
70
86
  world.nextRealmCreateRetryAtMs = 0;
71
87
  markRoutingIndexDirty();
88
+ flushPendingTargetLayerBinds(world, context.worldId);
72
89
  }
73
90
  if (typeof created.surfaceId === 'number') {
74
91
  world.coreSurfaceId = created.surfaceId;
@@ -15,6 +15,15 @@ import {
15
15
  toVec4,
16
16
  } from './utils';
17
17
 
18
+ const SCENE_SYNC_INTENT_TYPES = [
19
+ 'attach-model',
20
+ 'attach-camera',
21
+ 'attach-light',
22
+ 'detach-component',
23
+ 'gizmo-draw-line',
24
+ 'gizmo-draw-aabb',
25
+ ] as const;
26
+
18
27
  function copyMatrixToScratch(
19
28
  world: Parameters<System>[0],
20
29
  entityId: number,
@@ -39,12 +48,12 @@ function copyMatrixToScratch(
39
48
  * constraint matrix changed in the current tick.
40
49
  */
41
50
  export const SceneSyncSystem: System = (world, context) => {
42
- const intentsToRemove: number[] = [];
43
51
  const realmId = world.coreRealmId;
44
52
  if (realmId === undefined) return;
53
+ const intents = world.intentStore.takeMany(SCENE_SYNC_INTENT_TYPES);
45
54
 
46
- for (let i = 0; i < world.pendingIntents.length; i++) {
47
- const intent = world.pendingIntents[i];
55
+ for (let i = 0; i < intents.length; i++) {
56
+ const intent = intents[i];
48
57
  if (!intent) continue;
49
58
 
50
59
  if (intent.type === 'attach-model') {
@@ -85,7 +94,6 @@ export const SceneSyncSystem: System = (world, context) => {
85
94
  skipUpdate: true,
86
95
  });
87
96
 
88
- intentsToRemove.push(i);
89
97
  } else if (intent.type === 'attach-camera') {
90
98
  const cameraId = world.nextCoreId++;
91
99
  const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
@@ -133,7 +141,6 @@ export const SceneSyncSystem: System = (world, context) => {
133
141
  binding.cameraId = cameraId;
134
142
  }
135
143
 
136
- intentsToRemove.push(i);
137
144
  } else if (intent.type === 'attach-light') {
138
145
  const lightId = world.nextCoreId++;
139
146
  const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
@@ -199,7 +206,6 @@ export const SceneSyncSystem: System = (world, context) => {
199
206
  skipUpdate: true,
200
207
  });
201
208
 
202
- intentsToRemove.push(i);
203
209
  } else if (intent.type === 'detach-component') {
204
210
  const store = world.components.get(intent.entityId);
205
211
  if (store) {
@@ -227,21 +233,18 @@ export const SceneSyncSystem: System = (world, context) => {
227
233
  }
228
234
  store.delete(intent.componentType);
229
235
  }
230
- intentsToRemove.push(i);
231
236
  } else if (intent.type === 'gizmo-draw-line') {
232
237
  enqueueCommand(context.worldId, 'cmd-gizmo-draw-line', {
233
238
  start: toVec3(intent.start),
234
239
  end: toVec3(intent.end),
235
240
  color: toVec4(intent.color),
236
241
  });
237
- intentsToRemove.push(i);
238
242
  } else if (intent.type === 'gizmo-draw-aabb') {
239
243
  enqueueCommand(context.worldId, 'cmd-gizmo-draw-aabb', {
240
244
  min: toVec3(intent.min),
241
245
  max: toVec3(intent.max),
242
246
  color: toVec4(intent.color),
243
247
  });
244
- intentsToRemove.push(i);
245
248
  }
246
249
  }
247
250
 
@@ -296,10 +299,6 @@ export const SceneSyncSystem: System = (world, context) => {
296
299
  }
297
300
  }
298
301
 
299
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
300
- const idx = intentsToRemove[i];
301
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
302
- }
303
302
  };
304
303
 
305
304
  /** Backward-compatible alias while migrating existing integrations. */
@@ -11,6 +11,33 @@ import type {
11
11
  } from '../ecs';
12
12
 
13
13
  const WORLD_ENTITY_ID = 0;
14
+ const UI_INTENT_TYPES = [
15
+ 'ui-theme-define',
16
+ 'ui-theme-dispose',
17
+ 'ui-document-create',
18
+ 'ui-document-dispose',
19
+ 'ui-document-set-rect',
20
+ 'ui-document-set-theme',
21
+ 'ui-document-get-tree',
22
+ 'ui-document-get-layout-rects',
23
+ 'ui-apply-ops',
24
+ 'ui-debug-set',
25
+ 'ui-focus-set',
26
+ 'ui-focus-get',
27
+ 'ui-event-trace-set',
28
+ 'ui-image-create-from-buffer',
29
+ 'ui-image-dispose',
30
+ 'ui-clipboard-paste',
31
+ 'ui-screenshot-reply',
32
+ 'ui-access-kit-action-request',
33
+ 'ui-form-upsert',
34
+ 'ui-form-dispose',
35
+ 'ui-fieldset-upsert',
36
+ 'ui-fieldset-dispose',
37
+ 'ui-focusable-upsert',
38
+ 'ui-focusable-dispose',
39
+ 'ui-focus-next',
40
+ ] as const;
14
41
 
15
42
  function ensureUiState(world: Parameters<System>[0]): UiStateComponent {
16
43
  let worldStore = world.components.get(WORLD_ENTITY_ID);
@@ -315,20 +342,18 @@ function processUiIntent(
315
342
  */
316
343
  export const UiBridgeSystem: System = (world, context) => {
317
344
  const uiState = ensureUiState(world);
318
- const intentsToRemove: number[] = [];
319
345
  let explicitFocusNavigation = false;
346
+ const intents = world.intentStore.takeMany(UI_INTENT_TYPES);
320
347
 
321
- for (let i = 0; i < world.pendingIntents.length; i++) {
322
- const intent = world.pendingIntents[i];
348
+ for (let i = 0; i < intents.length; i++) {
349
+ const intent = intents[i];
323
350
  if (!intent) continue;
324
351
 
325
352
  if (intent.type === 'ui-focus-next') {
326
353
  explicitFocusNavigation = true;
327
354
  }
328
355
 
329
- if (processUiIntent(context.worldId, uiState, intent)) {
330
- intentsToRemove.push(i);
331
- }
356
+ processUiIntent(context.worldId, uiState, intent);
332
357
  }
333
358
 
334
359
  if (!explicitFocusNavigation) {
@@ -353,8 +378,4 @@ export const UiBridgeSystem: System = (world, context) => {
353
378
  }
354
379
  }
355
380
 
356
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
357
- const idx = intentsToRemove[i];
358
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
359
- }
360
381
  };
@@ -168,10 +168,53 @@ export function getResolvedEntityTransformMatrix(
168
168
  entityId: number,
169
169
  ): mat4 {
170
170
  const resolved = world.resolvedEntityTransforms.get(entityId);
171
- if (!resolved) {
172
- return getEntityLocalTransformMatrix(world, entityId);
171
+ if (resolved && !hasDirtyConstraintPath(world, entityId)) {
172
+ return mat4.clone(resolved);
173
173
  }
174
- return mat4.clone(resolved);
174
+
175
+ // Fallback for same-frame reads before ConstraintSolve runs:
176
+ // recompute parent-chain world transform directly from local components.
177
+ return resolveEntityWorldTransformImmediate(world, entityId);
178
+ }
179
+
180
+ function hasDirtyConstraintPath(world: WorldState, entityId: number): boolean {
181
+ let current: number | undefined = entityId;
182
+ while (current !== undefined) {
183
+ if (world.constraintDirtyEntities.has(current)) {
184
+ return true;
185
+ }
186
+ current = world.constraintParentByChild.get(current);
187
+ }
188
+ return false;
189
+ }
190
+
191
+ function resolveEntityWorldTransformImmediate(
192
+ world: WorldState,
193
+ entityId: number,
194
+ ): mat4 {
195
+ const visiting = new Set<number>();
196
+
197
+ const resolveRecursive = (currentId: number): mat4 => {
198
+ if (visiting.has(currentId)) {
199
+ return getEntityLocalTransformMatrix(world, currentId);
200
+ }
201
+ visiting.add(currentId);
202
+
203
+ const local = getEntityLocalTransformMatrix(world, currentId);
204
+ const parentId = world.constraintParentByChild.get(currentId);
205
+ if (parentId === undefined) {
206
+ visiting.delete(currentId);
207
+ return local;
208
+ }
209
+
210
+ const parentWorld = resolveRecursive(parentId);
211
+ const out = mat4.create();
212
+ mat4.multiply(out, parentWorld, local);
213
+ visiting.delete(currentId);
214
+ return out;
215
+ };
216
+
217
+ return resolveRecursive(entityId);
175
218
  }
176
219
 
177
220
  /** Compares 4x4 matrices using an absolute epsilon per component. */
@@ -4,6 +4,12 @@ import { enqueueCommand } from '../bridge/dispatch';
4
4
  import type { System } from '../ecs';
5
5
  import { toVec3, toVec4 } from './utils';
6
6
 
7
+ const WORLD_LIFECYCLE_INTENT_TYPES = [
8
+ 'configure-environment',
9
+ 'configure-shadows',
10
+ 'send-notification',
11
+ ] as const;
12
+
7
13
  /**
8
14
  * Applies world-scoped lifecycle/configuration intents.
9
15
  *
@@ -13,10 +19,10 @@ import { toVec3, toVec4 } from './utils';
13
19
  * - notification dispatch
14
20
  */
15
21
  export const WorldLifecycleSystem: System = (world, context) => {
16
- const intentsToRemove: number[] = [];
22
+ const intents = world.intentStore.takeMany(WORLD_LIFECYCLE_INTENT_TYPES);
17
23
 
18
- for (let i = 0; i < world.pendingIntents.length; i++) {
19
- const intent = world.pendingIntents[i];
24
+ for (let i = 0; i < intents.length; i++) {
25
+ const intent = intents[i];
20
26
  if (!intent) continue;
21
27
 
22
28
  if (intent.type === 'configure-environment') {
@@ -85,7 +91,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
85
91
  environmentId: context.worldId,
86
92
  });
87
93
  }
88
- intentsToRemove.push(i);
89
94
  } else if (intent.type === 'configure-shadows') {
90
95
  let windowId = world.primaryWindowId;
91
96
  if (windowId === undefined) {
@@ -94,9 +99,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
94
99
  break;
95
100
  }
96
101
  }
97
- if (windowId === undefined) {
98
- windowId = world.realmCreateArgs.hostWindowId;
99
- }
100
102
  if (windowId === undefined) continue;
101
103
  const c = intent.config || {};
102
104
  const config: ShadowConfig = {
@@ -112,7 +114,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
112
114
  windowId,
113
115
  config,
114
116
  });
115
- intentsToRemove.push(i);
116
117
  } else if (intent.type === 'send-notification') {
117
118
  enqueueCommand(context.worldId, 'cmd-notification-send', {
118
119
  id: `notif-${Date.now()}`,
@@ -121,13 +122,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
121
122
  body: intent.message,
122
123
  timeout: 5000,
123
124
  });
124
- intentsToRemove.push(i);
125
125
  }
126
126
  }
127
-
128
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
129
- const idx = intentsToRemove[i];
130
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
131
- }
132
-
133
127
  };