@vulfram/engine 0.14.8-alpha → 0.17.1-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 (54) hide show
  1. package/README.md +106 -0
  2. package/package.json +55 -4
  3. package/src/core.ts +14 -0
  4. package/src/ecs.ts +1 -0
  5. package/src/engine/api.ts +234 -23
  6. package/src/engine/bridge/dispatch.ts +265 -40
  7. package/src/engine/bridge/guards.ts +4 -1
  8. package/src/engine/bridge/protocol.ts +69 -52
  9. package/src/engine/ecs/index.ts +185 -42
  10. package/src/engine/state.ts +133 -2
  11. package/src/engine/systems/command-intent.ts +153 -3
  12. package/src/engine/systems/constraint-solve.ts +167 -0
  13. package/src/engine/systems/core-command-builder.ts +9 -268
  14. package/src/engine/systems/diagnostics.ts +20 -19
  15. package/src/engine/systems/index.ts +3 -1
  16. package/src/engine/systems/input-mirror.ts +101 -3
  17. package/src/engine/systems/resource-upload.ts +96 -44
  18. package/src/engine/systems/response-decode.ts +69 -15
  19. package/src/engine/systems/scene-sync.ts +306 -0
  20. package/src/engine/systems/ui-bridge.ts +360 -0
  21. package/src/engine/systems/utils.ts +43 -1
  22. package/src/engine/systems/world-lifecycle.ts +37 -102
  23. package/src/engine/window/manager.ts +168 -0
  24. package/src/engine/world/entities.ts +821 -78
  25. package/src/engine/world/mount.ts +174 -0
  26. package/src/engine/world/types.ts +71 -0
  27. package/src/engine/world/world-ui.ts +266 -0
  28. package/src/engine/world/world3d.ts +280 -0
  29. package/src/index.ts +30 -1
  30. package/src/mount.ts +2 -0
  31. package/src/types/cmds/audio.ts +73 -48
  32. package/src/types/cmds/camera.ts +12 -8
  33. package/src/types/cmds/environment.ts +9 -3
  34. package/src/types/cmds/geometry.ts +13 -14
  35. package/src/types/cmds/index.ts +198 -168
  36. package/src/types/cmds/light.ts +12 -11
  37. package/src/types/cmds/material.ts +9 -11
  38. package/src/types/cmds/model.ts +17 -15
  39. package/src/types/cmds/realm.ts +25 -0
  40. package/src/types/cmds/system.ts +19 -0
  41. package/src/types/cmds/target.ts +82 -0
  42. package/src/types/cmds/texture.ts +13 -3
  43. package/src/types/cmds/ui.ts +220 -0
  44. package/src/types/cmds/window.ts +41 -204
  45. package/src/types/events/index.ts +4 -1
  46. package/src/types/events/pointer.ts +42 -13
  47. package/src/types/events/system.ts +144 -30
  48. package/src/types/events/ui.ts +21 -0
  49. package/src/types/index.ts +1 -0
  50. package/src/types/json.ts +15 -0
  51. package/src/window.ts +8 -0
  52. package/src/world-ui.ts +2 -0
  53. package/src/world3d.ts +10 -0
  54. package/tsconfig.json +0 -29
@@ -4,17 +4,35 @@ import type { MaterialOptions } from '../../types/cmds/material';
4
4
  import type { ShadowConfig } from '../../types/cmds/shadow';
5
5
  import type { ForwardAtlasOptions } from '../../types/cmds/texture';
6
6
  import type { EnvironmentConfig } from '../../types/cmds/environment';
7
+ import type { GamepadEvent, SystemEvent, UiEvent } from '../../types/events';
8
+ import type {
9
+ CmdUiAccessKitActionRequestArgs,
10
+ CmdUiApplyOpsArgs,
11
+ CmdUiClipboardPasteArgs,
12
+ CmdUiDebugSetArgs,
13
+ CmdUiDocumentCreateArgs,
14
+ CmdUiDocumentDisposeArgs,
15
+ CmdUiDocumentGetLayoutRectsArgs,
16
+ CmdUiDocumentGetTreeArgs,
17
+ CmdUiDocumentSetRectArgs,
18
+ CmdUiDocumentSetThemeArgs,
19
+ CmdUiEventTraceSetArgs,
20
+ CmdUiFocusGetArgs,
21
+ CmdUiFocusSetArgs,
22
+ CmdUiImageCreateFromBufferArgs,
23
+ CmdUiImageDisposeArgs,
24
+ CmdUiScreenshotReplyArgs,
25
+ CmdUiThemeDefineArgs,
26
+ CmdUiThemeDisposeArgs,
27
+ } from '../../types/cmds/ui';
7
28
  import type {
8
29
  CameraKind,
9
30
  LightKind,
10
31
  MaterialKind,
11
32
  TextureCreateMode,
12
33
  NotificationLevel,
13
- WindowState,
14
- CursorGrabMode,
15
- CursorIcon,
16
- UserAttentionType,
17
34
  } from '../../types/kinds';
35
+ import type { JsonObject, JsonValue } from '../../types/json';
18
36
  import type { WorldState } from '../state';
19
37
 
20
38
  /**
@@ -179,6 +197,91 @@ export interface WindowStateComponent {
179
197
  focusChangedThisFrame: boolean;
180
198
  }
181
199
 
200
+ /**
201
+ * Gamepad state mirrored from core gamepad events.
202
+ */
203
+ export interface GamepadStateComponent {
204
+ type: 'GamepadState';
205
+ connected: Map<number, { name: string }>;
206
+ buttons: Map<number, Map<number, { pressed: boolean; value: number }>>;
207
+ axes: Map<number, Map<number, number>>;
208
+ eventsThisFrame: GamepadEvent[];
209
+ }
210
+
211
+ /**
212
+ * System events mirrored for host-level integrations.
213
+ */
214
+ export interface SystemEventStateComponent {
215
+ type: 'SystemEventState';
216
+ eventsThisFrame: SystemEvent[];
217
+ lastError?: {
218
+ scope: string;
219
+ message: string;
220
+ commandId?: number;
221
+ commandType?: string;
222
+ };
223
+ }
224
+
225
+ /**
226
+ * UI events mirrored from core UI event channel.
227
+ */
228
+ export interface UiEventStateComponent {
229
+ type: 'UiEventState';
230
+ eventsThisFrame: UiEvent[];
231
+ }
232
+
233
+ /**
234
+ * Focus traversal behavior when reaching first/last input.
235
+ */
236
+ export type UiFocusCycleMode = 'wrap' | 'clamp';
237
+
238
+ /**
239
+ * Form scope state used to control focus traversal.
240
+ */
241
+ export interface UiFormScope {
242
+ formId: string;
243
+ windowId: number;
244
+ realmId: number;
245
+ documentId: number;
246
+ disabled: boolean;
247
+ cycleMode: UiFocusCycleMode;
248
+ activeFieldsetId?: string;
249
+ activeNodeId?: number;
250
+ }
251
+
252
+ /**
253
+ * Fieldset scope metadata.
254
+ */
255
+ export interface UiFieldsetScope {
256
+ formId: string;
257
+ fieldsetId: string;
258
+ disabled: boolean;
259
+ legendNodeId?: number;
260
+ }
261
+
262
+ /**
263
+ * Focusable node metadata for tab ordering.
264
+ */
265
+ export interface UiFocusableNode {
266
+ formId: string;
267
+ nodeId: number;
268
+ tabIndex: number;
269
+ fieldsetId?: string;
270
+ disabled: boolean;
271
+ orderHint: number;
272
+ }
273
+
274
+ /**
275
+ * UI runtime state mirrored in the ECS world.
276
+ */
277
+ export interface UiStateComponent {
278
+ type: 'UiState';
279
+ forms: Map<string, UiFormScope>;
280
+ fieldsets: Map<string, UiFieldsetScope>;
281
+ nodes: Map<number, UiFocusableNode>;
282
+ focusByWindow: Map<number, { formId: string; nodeId: number }>;
283
+ }
284
+
182
285
  /**
183
286
  * Resource Properties
184
287
  */
@@ -228,6 +331,8 @@ export interface SphereOptions {
228
331
  export interface CylinderOptions {
229
332
  radius?: number;
230
333
  height?: number;
334
+ sectors?: number;
335
+ /** @deprecated use `sectors` */
231
336
  segments?: number;
232
337
  }
233
338
 
@@ -237,7 +342,11 @@ export interface CylinderOptions {
237
342
  export interface TorusOptions {
238
343
  majorRadius?: number;
239
344
  minorRadius?: number;
345
+ majorSegments?: number;
346
+ minorSegments?: number;
347
+ /** @deprecated use `majorSegments` */
240
348
  radialSegments?: number;
349
+ /** @deprecated use `minorSegments` */
241
350
  tubularSegments?: number;
242
351
  }
243
352
 
@@ -292,6 +401,10 @@ export type Component =
292
401
  | TagComponent
293
402
  | InputStateComponent
294
403
  | WindowStateComponent
404
+ | GamepadStateComponent
405
+ | SystemEventStateComponent
406
+ | UiEventStateComponent
407
+ | UiStateComponent
295
408
  | CustomComponent;
296
409
 
297
410
  /**
@@ -302,7 +415,7 @@ export type Component =
302
415
  */
303
416
  export interface CustomComponent {
304
417
  type: string;
305
- data: Record<string, unknown>;
418
+ data: JsonObject;
306
419
  }
307
420
 
308
421
  /**
@@ -335,7 +448,7 @@ export type PropertyType =
335
448
  */
336
449
  export interface SchemaProperty {
337
450
  type: PropertyType;
338
- default?: unknown;
451
+ default?: JsonValue;
339
452
  optional?: boolean;
340
453
  }
341
454
 
@@ -346,36 +459,6 @@ export interface ComponentSchema {
346
459
  [key: string]: SchemaProperty;
347
460
  }
348
461
 
349
- /**
350
- * Window update properties.
351
- */
352
- export interface WindowProps {
353
- title?: string;
354
- position?: [number, number];
355
- size?: [number, number];
356
- state?: WindowState;
357
- resizable?: boolean;
358
- decorations?: boolean;
359
- cursorVisible?: boolean;
360
- cursorGrab?: CursorGrabMode;
361
- icon?: number;
362
- cursorIcon?: CursorIcon;
363
- }
364
-
365
- /**
366
- * Window creation properties.
367
- */
368
- export interface CreateWindowProps {
369
- title: string;
370
- size: [number, number];
371
- position: [number, number];
372
- canvasId?: string;
373
- borderless?: boolean;
374
- resizable?: boolean;
375
- transparent?: boolean;
376
- initialState?: WindowState;
377
- }
378
-
379
462
  /**
380
463
  * Intents:
381
464
  * Requests to change the state of the World.
@@ -383,11 +466,6 @@ export interface CreateWindowProps {
383
466
  */
384
467
 
385
468
  export type Intent =
386
- | { type: 'create-window'; props: CreateWindowProps }
387
- | { type: 'close-window' }
388
- | { type: 'update-window'; props: WindowProps }
389
- | { type: 'request-attention'; attentionType?: UserAttentionType }
390
- | { type: 'focus-window' }
391
469
  | { type: 'configure-environment'; config: EnvironmentConfig }
392
470
  | {
393
471
  type: 'request-resource-list';
@@ -453,10 +531,75 @@ export type Intent =
453
531
  max: [number, number, number];
454
532
  color: [number, number, number, number];
455
533
  }
534
+ | { type: 'ui-theme-define'; args: CmdUiThemeDefineArgs }
535
+ | { type: 'ui-theme-dispose'; args: CmdUiThemeDisposeArgs }
536
+ | { type: 'ui-document-create'; args: CmdUiDocumentCreateArgs }
537
+ | { type: 'ui-document-dispose'; args: CmdUiDocumentDisposeArgs }
538
+ | { type: 'ui-document-set-rect'; args: CmdUiDocumentSetRectArgs }
539
+ | { type: 'ui-document-set-theme'; args: CmdUiDocumentSetThemeArgs }
540
+ | { type: 'ui-document-get-tree'; args: CmdUiDocumentGetTreeArgs }
541
+ | {
542
+ type: 'ui-document-get-layout-rects';
543
+ args: CmdUiDocumentGetLayoutRectsArgs;
544
+ }
545
+ | { type: 'ui-apply-ops'; args: CmdUiApplyOpsArgs }
546
+ | { type: 'ui-debug-set'; args: CmdUiDebugSetArgs }
547
+ | { type: 'ui-focus-set'; args: CmdUiFocusSetArgs }
548
+ | { type: 'ui-focus-get'; args: CmdUiFocusGetArgs }
549
+ | { type: 'ui-event-trace-set'; args: CmdUiEventTraceSetArgs }
550
+ | { type: 'ui-image-create-from-buffer'; args: CmdUiImageCreateFromBufferArgs }
551
+ | { type: 'ui-image-dispose'; args: CmdUiImageDisposeArgs }
552
+ | { type: 'ui-clipboard-paste'; args: CmdUiClipboardPasteArgs }
553
+ | { type: 'ui-screenshot-reply'; args: CmdUiScreenshotReplyArgs }
554
+ | {
555
+ type: 'ui-access-kit-action-request';
556
+ args: CmdUiAccessKitActionRequestArgs;
557
+ }
558
+ | {
559
+ type: 'ui-form-upsert';
560
+ form: {
561
+ formId: string;
562
+ windowId: number;
563
+ realmId: number;
564
+ documentId: number;
565
+ disabled?: boolean;
566
+ cycleMode?: UiFocusCycleMode;
567
+ activeFieldsetId?: string;
568
+ };
569
+ }
570
+ | { type: 'ui-form-dispose'; formId: string }
571
+ | {
572
+ type: 'ui-fieldset-upsert';
573
+ fieldset: {
574
+ formId: string;
575
+ fieldsetId: string;
576
+ disabled?: boolean;
577
+ legendNodeId?: number;
578
+ };
579
+ }
580
+ | { type: 'ui-fieldset-dispose'; formId: string; fieldsetId: string }
581
+ | {
582
+ type: 'ui-focusable-upsert';
583
+ focusable: {
584
+ formId: string;
585
+ nodeId: number;
586
+ tabIndex?: number;
587
+ fieldsetId?: string;
588
+ disabled?: boolean;
589
+ orderHint?: number;
590
+ };
591
+ }
592
+ | { type: 'ui-focusable-dispose'; nodeId: number }
593
+ | {
594
+ type: 'ui-focus-next';
595
+ windowId: number;
596
+ backwards?: boolean;
597
+ formId?: string;
598
+ }
456
599
  | {
457
600
  type: 'custom';
458
601
  name: string;
459
- data: Record<string, unknown>;
602
+ data: JsonObject;
460
603
  };
461
604
 
462
605
  /**
@@ -1,6 +1,7 @@
1
1
  import type { EngineTransport } from '@vulfram/transport-types';
2
2
  import type { CommandResponseEnvelope, EngineCmdEnvelope } from '../types/cmds';
3
3
  import type { EngineEvent } from '../types/events';
4
+ import type { CmdRealmCreateArgs, RealmKind } from '../types/cmds/realm';
4
5
  import type {
5
6
  Component,
6
7
  ComponentType,
@@ -28,8 +29,78 @@ type ResourceIdCounters = {
28
29
  texture: number;
29
30
  };
30
31
 
32
+ export type RoutingIndex = {
33
+ dirty: boolean;
34
+ byWindowId: Map<number, number[]>;
35
+ byRealmId: Map<number, number[]>;
36
+ byTargetId: Map<number, number[]>;
37
+ };
38
+
31
39
  export type WorldState = {
32
- windowId: number;
40
+ worldId: number;
41
+ /**
42
+ * Realm kind for this world.
43
+ * `three-d` realms are used by World3D APIs and `two-d` realms by WorldUI APIs.
44
+ */
45
+ realmKind: RealmKind;
46
+ primaryWindowId?: number;
47
+ boundWindowIds: Set<number>;
48
+ /**
49
+ * Latest target-layer binding args keyed by targetId.
50
+ * Used to keep realm->target binds coherent when cameras are created later.
51
+ */
52
+ targetLayerBindings: Map<
53
+ number,
54
+ {
55
+ targetId: number;
56
+ layout: import('../types/cmds/target').TargetLayerLayout;
57
+ cameraId?: number;
58
+ environmentId?: number;
59
+ }
60
+ >;
61
+ /**
62
+ * Window target ownership keyed by targetId.
63
+ * Tracks which window a target is currently presenting into for routing purposes.
64
+ */
65
+ targetWindowBindings: Map<number, number>;
66
+ coreRealmId?: number;
67
+ realmCreateRetryCount: number;
68
+ nextRealmCreateRetryAtMs: number;
69
+ realmCreateArgs: CmdRealmCreateArgs;
70
+ coreSurfaceId?: number;
71
+ corePresentId?: number;
72
+ /**
73
+ * Constraint-solved world matrices keyed by entity id.
74
+ */
75
+ resolvedEntityTransforms: Map<number, Float32Array>;
76
+ /**
77
+ * Entities explicitly marked as dirty for constraint recomputation.
78
+ */
79
+ constraintDirtyEntities: Set<number>;
80
+ /**
81
+ * Scratch cache for per-tick constraint solve graph resolution.
82
+ */
83
+ constraintScratchResolved: Map<number, Float32Array>;
84
+ /**
85
+ * Scratch set used while resolving hierarchy recursion (cycle detection).
86
+ */
87
+ constraintScratchVisiting: Set<number>;
88
+ /**
89
+ * Entities with resolved matrix changes in the current tick.
90
+ */
91
+ constraintChangedEntities: Set<number>;
92
+ /**
93
+ * Parent -> children relationship index for hierarchy propagation.
94
+ */
95
+ constraintChildrenByParent: Map<number, Set<number>>;
96
+ /**
97
+ * Child -> parent relationship index for fast reparent operations.
98
+ */
99
+ constraintParentByChild: Map<number, number>;
100
+ /**
101
+ * Reusable scratch arrays used when syncing matrices to core.
102
+ */
103
+ sceneSyncMatrixScratch: Map<number, number[]>;
33
104
  entities: Set<number>;
34
105
  /**
35
106
  * Components stored by entityId -> componentType -> ComponentData
@@ -50,6 +121,7 @@ export type WorldState = {
50
121
  * Will be moved to the engine's global batch during the tick.
51
122
  */
52
123
  pendingCommands: EngineCmdEnvelope[];
124
+ pendingCommandsHead: number;
53
125
  /**
54
126
  * Events routed to this world in the current frame.
55
127
  */
@@ -64,17 +136,57 @@ export type EngineState = {
64
136
  status: EngineStatus;
65
137
  transport: EngineTransport | null;
66
138
  worlds: Map<number, WorldState>;
139
+ nextWorldId: number;
140
+ nextWindowId: number;
67
141
  nextEntityId: number;
68
142
  nextCommandId: number;
143
+ /**
144
+ * Global ID allocator for core-global resources/objects.
145
+ * Use for IDs that are not realm-scoped (material/geometry/texture/target/...).
146
+ */
147
+ nextGlobalId: number;
69
148
  /**
70
149
  * Global command batch for the current frame.
71
150
  */
72
151
  commandBatch: EngineCmdEnvelope[];
152
+ /**
153
+ * Tracks reserved window IDs to avoid logical ID collisions for host windows.
154
+ */
155
+ usedWindowIds: Set<number>;
156
+ /**
157
+ * Pending global commands not scoped to any specific world.
158
+ */
159
+ globalPendingCommands: EngineCmdEnvelope[];
160
+ globalPendingCommandsHead: number;
161
+ /**
162
+ * Window IDs confirmed by core `window-create` responses.
163
+ */
164
+ confirmedWindowIds: Set<number>;
165
+ /**
166
+ * Maps pending window-create command ids to requested window ids.
167
+ */
168
+ pendingWindowCreateByCommandId: Map<number, number>;
169
+ /**
170
+ * Maps pending window-close command ids to target window ids.
171
+ */
172
+ pendingWindowCloseByCommandId: Map<number, number>;
73
173
  /**
74
174
  * Maps command IDs to the world ID that created them.
75
175
  * This is used to route responses back to the correct world.
76
176
  */
77
177
  commandTracker: Map<number, number>;
178
+ /**
179
+ * Tracks global commands (not world-scoped) so their responses are not lost.
180
+ */
181
+ globalCommandTracker: Set<number>;
182
+ /**
183
+ * Buffered responses for global commands.
184
+ */
185
+ globalInboundResponses: CommandResponseEnvelope[];
186
+ /**
187
+ * Cached world routing index for event fanout.
188
+ */
189
+ routingIndex: RoutingIndex;
78
190
  /**
79
191
  * Registry for custom components and systems.
80
192
  */
@@ -99,8 +211,10 @@ export type EngineState = {
99
211
  export const REQUIRED_SYSTEMS = [
100
212
  'InputMirrorSystem',
101
213
  'CommandIntentSystem',
214
+ 'UiBridgeSystem',
102
215
  'ResourceUploadSystem',
103
- 'CoreCommandBuilderSystem',
216
+ 'ConstraintSolveSystem',
217
+ 'SceneSyncSystem',
104
218
  'ResponseDecodeSystem',
105
219
  'WorldLifecycleSystem',
106
220
  'DiagnosticsSystem',
@@ -110,10 +224,27 @@ export const engineState: EngineState = {
110
224
  status: 'uninitialized',
111
225
  transport: null,
112
226
  worlds: new Map(),
227
+ nextWorldId: 1,
228
+ nextWindowId: 1,
113
229
  nextEntityId: 1,
114
230
  nextCommandId: 1,
231
+ nextGlobalId: 100,
115
232
  commandBatch: [],
233
+ usedWindowIds: new Set(),
234
+ globalPendingCommands: [],
235
+ globalPendingCommandsHead: 0,
236
+ confirmedWindowIds: new Set(),
237
+ pendingWindowCreateByCommandId: new Map(),
238
+ pendingWindowCloseByCommandId: new Map(),
116
239
  commandTracker: new Map(),
240
+ globalCommandTracker: new Set(),
241
+ globalInboundResponses: [],
242
+ routingIndex: {
243
+ dirty: true,
244
+ byWindowId: new Map(),
245
+ byRealmId: new Map(),
246
+ byTargetId: new Map(),
247
+ },
117
248
  registry: {
118
249
  components: new Map(),
119
250
  systems: {
@@ -5,10 +5,42 @@ import type {
5
5
  LightComponent,
6
6
  ModelComponent,
7
7
  System,
8
+ TransformComponent,
8
9
  } from '../ecs';
10
+ import { toQuat, toVec3 } from './utils';
9
11
 
12
+ function wouldCreateParentCycle(
13
+ world: Parameters<System>[0],
14
+ childEntityId: number,
15
+ parentEntityId: number,
16
+ ): boolean {
17
+ let cursor: number | null = parentEntityId;
18
+ const visited = new Set<number>();
19
+
20
+ while (cursor !== null) {
21
+ if (cursor === childEntityId) {
22
+ return true;
23
+ }
24
+ if (visited.has(cursor)) {
25
+ return true;
26
+ }
27
+ visited.add(cursor);
28
+
29
+ const store = world.components.get(cursor);
30
+ const parent = store?.get('Parent') as { parentId: number } | undefined;
31
+ cursor = parent?.parentId ?? null;
32
+ }
33
+
34
+ return false;
35
+ }
36
+
37
+ /**
38
+ * Applies structural ECS intents that must mutate local world state before
39
+ * render-time command synthesis (entity lifecycle, tags, parent links, transforms).
40
+ */
10
41
  export const CommandIntentSystem: System = (world, context) => {
11
42
  const intentsToRemove: number[] = [];
43
+ const realmId = world.coreRealmId;
12
44
 
13
45
  for (let i = 0; i < world.pendingIntents.length; i++) {
14
46
  const intent = world.pendingIntents[i];
@@ -32,8 +64,123 @@ export const CommandIntentSystem: System = (world, context) => {
32
64
  visible: true,
33
65
  });
34
66
  }
67
+ world.constraintDirtyEntities.add(intent.entityId);
68
+ intentsToRemove.push(i);
69
+ } else if (intent.type === 'attach-tag') {
70
+ let store = world.components.get(intent.entityId);
71
+ if (!store) {
72
+ store = new Map();
73
+ world.components.set(intent.entityId, store);
74
+ }
75
+ store.set('Tag', {
76
+ type: 'Tag',
77
+ name: intent.props.name ?? '',
78
+ labels: new Set(intent.props.labels ?? []),
79
+ });
80
+ intentsToRemove.push(i);
81
+ } else if (intent.type === 'set-parent') {
82
+ let store = world.components.get(intent.entityId);
83
+ if (!store) {
84
+ store = new Map();
85
+ world.components.set(intent.entityId, store);
86
+ }
87
+ if (intent.parentId === null) {
88
+ const oldParentId = world.constraintParentByChild.get(intent.entityId);
89
+ if (oldParentId !== undefined) {
90
+ const siblings = world.constraintChildrenByParent.get(oldParentId);
91
+ siblings?.delete(intent.entityId);
92
+ if (siblings && siblings.size === 0) {
93
+ world.constraintChildrenByParent.delete(oldParentId);
94
+ }
95
+ world.constraintParentByChild.delete(intent.entityId);
96
+ }
97
+ store.delete('Parent');
98
+ } else {
99
+ if (intent.parentId === intent.entityId) {
100
+ console.error(
101
+ `[World ${context.worldId}] Invalid parent constraint: entity ${intent.entityId} cannot parent itself.`,
102
+ );
103
+ intentsToRemove.push(i);
104
+ continue;
105
+ }
106
+ if (wouldCreateParentCycle(world, intent.entityId, intent.parentId)) {
107
+ console.error(
108
+ `[World ${context.worldId}] Invalid parent constraint: cycle detected for child ${intent.entityId} and parent ${intent.parentId}.`,
109
+ );
110
+ intentsToRemove.push(i);
111
+ continue;
112
+ }
113
+ store.set('Parent', {
114
+ type: 'Parent',
115
+ parentId: intent.parentId,
116
+ });
117
+
118
+ const oldParentId = world.constraintParentByChild.get(intent.entityId);
119
+ if (oldParentId !== undefined && oldParentId !== intent.parentId) {
120
+ const siblings = world.constraintChildrenByParent.get(oldParentId);
121
+ siblings?.delete(intent.entityId);
122
+ if (siblings && siblings.size === 0) {
123
+ world.constraintChildrenByParent.delete(oldParentId);
124
+ }
125
+ }
126
+ world.constraintParentByChild.set(intent.entityId, intent.parentId);
127
+ let children = world.constraintChildrenByParent.get(intent.parentId);
128
+ if (!children) {
129
+ children = new Set();
130
+ world.constraintChildrenByParent.set(intent.parentId, children);
131
+ }
132
+ children.add(intent.entityId);
133
+ }
134
+ world.constraintDirtyEntities.add(intent.entityId);
135
+ intentsToRemove.push(i);
136
+ } else if (intent.type === 'update-transform') {
137
+ const store = world.components.get(intent.entityId);
138
+ if (!store) {
139
+ continue;
140
+ }
141
+ const transform = store.get('Transform') as TransformComponent | undefined;
142
+ if (!transform) {
143
+ continue;
144
+ }
145
+
146
+ const nextProps = { ...intent.props };
147
+ if (nextProps.position) {
148
+ nextProps.position = toVec3(nextProps.position);
149
+ }
150
+ if (nextProps.rotation) {
151
+ nextProps.rotation = toQuat(nextProps.rotation);
152
+ }
153
+ if (nextProps.scale) {
154
+ nextProps.scale = toVec3(nextProps.scale);
155
+ }
156
+ Object.assign(transform, nextProps);
157
+ world.constraintDirtyEntities.add(intent.entityId);
35
158
  intentsToRemove.push(i);
36
159
  } else if (intent.type === 'remove-entity') {
160
+ if (realmId === undefined) continue;
161
+
162
+ // Detach all children linked to this entity to keep hierarchy state coherent.
163
+ const children = world.constraintChildrenByParent.get(intent.entityId);
164
+ if (children) {
165
+ for (const childId of children) {
166
+ const childStore = world.components.get(childId);
167
+ childStore?.delete('Parent');
168
+ world.constraintParentByChild.delete(childId);
169
+ world.constraintDirtyEntities.add(childId);
170
+ }
171
+ world.constraintChildrenByParent.delete(intent.entityId);
172
+ }
173
+
174
+ const parentId = world.constraintParentByChild.get(intent.entityId);
175
+ if (parentId !== undefined) {
176
+ const siblings = world.constraintChildrenByParent.get(parentId);
177
+ siblings?.delete(intent.entityId);
178
+ if (siblings && siblings.size === 0) {
179
+ world.constraintChildrenByParent.delete(parentId);
180
+ }
181
+ world.constraintParentByChild.delete(intent.entityId);
182
+ }
183
+
37
184
  const store = world.components.get(intent.entityId);
38
185
  if (store) {
39
186
  // Emit disposal commands for all components with IDs
@@ -42,19 +189,19 @@ export const CommandIntentSystem: System = (world, context) => {
42
189
  if (type === 'Model') {
43
190
  const modelComp = comp as ModelComponent;
44
191
  enqueueCommand(context.worldId, 'cmd-model-dispose', {
45
- windowId: context.worldId,
192
+ realmId,
46
193
  modelId: modelComp.id,
47
194
  });
48
195
  } else if (type === 'Camera') {
49
196
  const cameraComp = comp as CameraComponent;
50
197
  enqueueCommand(context.worldId, 'cmd-camera-dispose', {
51
- windowId: context.worldId,
198
+ realmId,
52
199
  cameraId: cameraComp.id,
53
200
  });
54
201
  } else if (type === 'Light') {
55
202
  const lightComp = comp as LightComponent;
56
203
  enqueueCommand(context.worldId, 'cmd-light-dispose', {
57
- windowId: context.worldId,
204
+ realmId,
58
205
  lightId: lightComp.id,
59
206
  });
60
207
  }
@@ -63,6 +210,9 @@ export const CommandIntentSystem: System = (world, context) => {
63
210
  world.components.delete(intent.entityId);
64
211
  }
65
212
  world.entities.delete(intent.entityId);
213
+ world.resolvedEntityTransforms.delete(intent.entityId);
214
+ world.sceneSyncMatrixScratch.delete(intent.entityId);
215
+ world.constraintDirtyEntities.add(intent.entityId);
66
216
  intentsToRemove.push(i);
67
217
  }
68
218
  }