@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
@@ -1,8 +1,81 @@
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
+ function buildPrimitiveOptions(
10
+ primitiveProps: Extract<GeometryProps, { type: 'primitive' }>,
11
+ ): PrimitiveOptions {
12
+ const { shape, options } = primitiveProps;
13
+ if (shape === 'cube') {
14
+ return {
15
+ type: 'cube',
16
+ content: {
17
+ size: options?.size ?? [1.0, 1.0, 1.0],
18
+ subdivisions: 1,
19
+ },
20
+ };
21
+ }
22
+ if (shape === 'plane') {
23
+ return {
24
+ type: 'plane',
25
+ content: {
26
+ size: options?.size ?? [1.0, 1.0, 1.0],
27
+ subdivisions: options?.subdivisions ?? 1,
28
+ },
29
+ };
30
+ }
31
+ if (shape === 'sphere') {
32
+ return {
33
+ type: 'sphere',
34
+ content: {
35
+ radius: options?.radius ?? 0.5,
36
+ sectors: options?.sectors ?? 32,
37
+ stacks: options?.stacks ?? 16,
38
+ },
39
+ };
40
+ }
41
+ if (shape === 'cylinder') {
42
+ return {
43
+ type: 'cylinder',
44
+ content: {
45
+ radius: options?.radius ?? 0.5,
46
+ height: options?.height ?? 1.0,
47
+ sectors: options?.sectors ?? options?.segments ?? 32,
48
+ },
49
+ };
50
+ }
51
+ if (shape === 'torus') {
52
+ return {
53
+ type: 'torus',
54
+ content: {
55
+ majorRadius: options?.majorRadius ?? 0.5,
56
+ minorRadius: options?.minorRadius ?? 0.25,
57
+ majorSegments: options?.majorSegments ?? options?.radialSegments ?? 32,
58
+ minorSegments: options?.minorSegments ?? options?.tubularSegments ?? 16,
59
+ },
60
+ };
61
+ }
62
+ return {
63
+ type: 'pyramid',
64
+ content: {
65
+ size: options?.size ?? [1.0, 1.0, 1.0],
66
+ subdivisions: options?.subdivisions ?? 1,
67
+ },
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Processes resource intents and emits corresponding core resource commands.
73
+ *
74
+ * Covered resources:
75
+ * - material
76
+ * - geometry (custom and primitive)
77
+ * - texture (from buffer or solid color)
78
+ */
6
79
  export const ResourceUploadSystem: System = (world, context) => {
7
80
  const intentsToRemove: number[] = [];
8
81
 
@@ -20,8 +93,7 @@ export const ResourceUploadSystem: System = (world, context) => {
20
93
  },
21
94
  };
22
95
 
23
- enqueueCommand(context.worldId, 'cmd-material-create', {
24
- windowId: context.worldId,
96
+ enqueueCommand(context.worldId, 'cmd-material-upsert', {
25
97
  materialId: intent.resourceId,
26
98
  label: intent.props.label || `Mat ${intent.resourceId}`,
27
99
  kind: intent.props.kind ?? 'standard',
@@ -30,52 +102,23 @@ export const ResourceUploadSystem: System = (world, context) => {
30
102
  intentsToRemove.push(i);
31
103
  } else if (intent.type === 'dispose-material') {
32
104
  enqueueCommand(context.worldId, 'cmd-material-dispose', {
33
- windowId: context.worldId,
34
105
  materialId: intent.resourceId,
35
106
  });
36
107
  intentsToRemove.push(i);
37
108
  } else if (intent.type === 'create-geometry') {
38
109
  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);
110
+ const options = normalizePrimitiveOptions(
111
+ buildPrimitiveOptions(intent.props),
112
+ );
68
113
 
69
114
  enqueueCommand(context.worldId, 'cmd-primitive-geometry-create', {
70
- windowId: context.worldId,
71
115
  geometryId: intent.resourceId,
72
116
  label: intent.props.label || `Geo ${intent.resourceId}`,
73
- shape: shape,
117
+ shape: intent.props.shape,
74
118
  options: options,
75
119
  });
76
120
  } else {
77
- enqueueCommand(context.worldId, 'cmd-geometry-create', {
78
- windowId: context.worldId,
121
+ enqueueCommand(context.worldId, 'cmd-geometry-upsert', {
79
122
  geometryId: intent.resourceId,
80
123
  label: intent.props.label || `Geo ${intent.resourceId}`,
81
124
  entries: intent.props.entries,
@@ -84,14 +127,19 @@ export const ResourceUploadSystem: System = (world, context) => {
84
127
  intentsToRemove.push(i);
85
128
  } else if (intent.type === 'dispose-geometry') {
86
129
  enqueueCommand(context.worldId, 'cmd-geometry-dispose', {
87
- windowId: context.worldId,
88
130
  geometryId: intent.resourceId,
89
131
  });
90
132
  intentsToRemove.push(i);
91
133
  } else if (intent.type === 'create-texture') {
92
134
  if (intent.props.source.type === 'color') {
93
- enqueueCommand(context.worldId, 'cmd-texture-create-solid-color', {
94
- windowId: context.worldId,
135
+ const cmd: {
136
+ textureId: number;
137
+ label: string;
138
+ color: [number, number, number, number];
139
+ srgb: boolean;
140
+ mode?: TextureCreateMode;
141
+ atlasOptions?: { tilePx: number; layers: number };
142
+ } = {
95
143
  textureId: intent.resourceId,
96
144
  label: intent.props.label || `Tex ${intent.resourceId}`,
97
145
  color: Array.from(intent.props.source.color) as [
@@ -101,10 +149,16 @@ export const ResourceUploadSystem: System = (world, context) => {
101
149
  number,
102
150
  ],
103
151
  srgb: intent.props.srgb ?? true,
104
- });
152
+ };
153
+ if (intent.props.mode !== undefined) {
154
+ cmd.mode = intent.props.mode;
155
+ }
156
+ if (intent.props.atlasOptions !== undefined) {
157
+ cmd.atlasOptions = intent.props.atlasOptions;
158
+ }
159
+ enqueueCommand(context.worldId, 'cmd-texture-create-solid-color', cmd);
105
160
  } else {
106
161
  const cmd: {
107
- windowId: number;
108
162
  textureId: number;
109
163
  label: string;
110
164
  bufferId: number;
@@ -112,7 +166,6 @@ export const ResourceUploadSystem: System = (world, context) => {
112
166
  mode?: TextureCreateMode;
113
167
  atlasOptions?: { tilePx: number; layers: number };
114
168
  } = {
115
- windowId: context.worldId,
116
169
  textureId: intent.resourceId,
117
170
  label: intent.props.label || `Tex ${intent.resourceId}`,
118
171
  bufferId: intent.props.source.bufferId,
@@ -129,7 +182,6 @@ export const ResourceUploadSystem: System = (world, context) => {
129
182
  intentsToRemove.push(i);
130
183
  } else if (intent.type === 'dispose-texture') {
131
184
  enqueueCommand(context.worldId, 'cmd-texture-dispose', {
132
- windowId: context.worldId,
133
185
  textureId: intent.resourceId,
134
186
  });
135
187
  intentsToRemove.push(i);
@@ -1,28 +1,82 @@
1
1
  import type { System } from '../ecs';
2
+ import { enqueueCommand, markRoutingIndexDirty } from '../bridge/dispatch';
2
3
  import { engineState } from '../state';
3
4
 
5
+ const MAX_REALM_CREATE_RETRIES = 8;
6
+ const BASE_REALM_RETRY_DELAY_MS = 32;
7
+
8
+ /**
9
+ * Decodes and applies command responses routed to a world.
10
+ *
11
+ * Responsibilities:
12
+ * - track realm/window ids returned by core
13
+ * - retry realm creation on transient host-window races
14
+ * - complete deferred target binds after target-upsert acknowledgements
15
+ */
4
16
  export const ResponseDecodeSystem: System = (world, context) => {
5
- while (world.inboundResponses.length > 0) {
6
- const res = world.inboundResponses.shift()!;
17
+ for (let i = 0; i < world.inboundResponses.length; i++) {
18
+ const res = world.inboundResponses[i]!;
7
19
  const content = res.content as { success?: boolean; message?: string };
8
20
 
9
- if (engineState.flags.debugEnabled) {
10
- const debugKey = '__vulframDebugRespAllCount';
11
- const count =
12
- (globalThis as unknown as Record<string, number>)[debugKey] || 0;
13
- if (count < 10) {
14
- console.debug('[Debug] Response', res.type, JSON.stringify(content));
15
- (globalThis as unknown as Record<string, number>)[debugKey] = count + 1;
16
- }
17
- }
18
-
19
21
  if (content && typeof content.success === 'boolean' && !content.success) {
22
+ if (
23
+ res.type === 'realm-create' &&
24
+ world.coreRealmId === undefined &&
25
+ typeof content.message === 'string' &&
26
+ content.message.includes('Host window') &&
27
+ content.message.includes('not found')
28
+ ) {
29
+ if (world.realmCreateRetryCount < MAX_REALM_CREATE_RETRIES) {
30
+ const nowMs = engineState.clock.lastTime;
31
+ if (nowMs >= world.nextRealmCreateRetryAtMs) {
32
+ const retryDelay =
33
+ BASE_REALM_RETRY_DELAY_MS * (1 << world.realmCreateRetryCount);
34
+ world.realmCreateRetryCount += 1;
35
+ world.nextRealmCreateRetryAtMs = nowMs + retryDelay;
36
+ enqueueCommand(
37
+ context.worldId,
38
+ 'cmd-realm-create',
39
+ world.realmCreateArgs,
40
+ );
41
+ }
42
+ } else if (world.realmCreateRetryCount === MAX_REALM_CREATE_RETRIES) {
43
+ console.error(
44
+ `[World ${context.worldId}] realm-create retries exhausted (${MAX_REALM_CREATE_RETRIES}). Last error: ${content.message}`,
45
+ );
46
+ world.realmCreateRetryCount += 1;
47
+ }
48
+ continue;
49
+ }
20
50
  console.error(
21
51
  `[World ${context.worldId}] Command ${res.type} (ID: ${res.id}) failed: ${content.message}`,
22
52
  );
23
- } else {
24
- // DEBUG: Print all successes too for now
25
- // console.debug(`[World ${context.worldId}] Command ${res.type} (ID: ${res.id}) succeeded.`);
53
+ } else if (res.type === 'realm-create') {
54
+ const created = res.content as { realmId?: number };
55
+ if (typeof created.realmId === 'number') {
56
+ world.coreRealmId = created.realmId;
57
+ world.realmCreateRetryCount = 0;
58
+ world.nextRealmCreateRetryAtMs = 0;
59
+ markRoutingIndexDirty();
60
+ }
61
+ } else if (res.type === 'window-create') {
62
+ const created = res.content as {
63
+ realmId?: number;
64
+ surfaceId?: number;
65
+ presentId?: number;
66
+ };
67
+ if (typeof created.realmId === 'number') {
68
+ world.coreRealmId = created.realmId;
69
+ world.realmCreateRetryCount = 0;
70
+ world.nextRealmCreateRetryAtMs = 0;
71
+ markRoutingIndexDirty();
72
+ }
73
+ if (typeof created.surfaceId === 'number') {
74
+ world.coreSurfaceId = created.surfaceId;
75
+ }
76
+ if (typeof created.presentId === 'number') {
77
+ world.corePresentId = created.presentId;
78
+ }
26
79
  }
27
80
  }
81
+ world.inboundResponses.length = 0;
28
82
  };
@@ -0,0 +1,306 @@
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
+ } from '../ecs';
11
+ import {
12
+ getResolvedEntityTransformMatrix,
13
+ toVec2,
14
+ toVec3,
15
+ toVec4,
16
+ } from './utils';
17
+
18
+ function copyMatrixToScratch(
19
+ world: Parameters<System>[0],
20
+ entityId: number,
21
+ matrix: ArrayLike<number>,
22
+ ): number[] {
23
+ let scratch = world.sceneSyncMatrixScratch.get(entityId);
24
+ if (!scratch) {
25
+ scratch = new Array<number>(16);
26
+ world.sceneSyncMatrixScratch.set(entityId, scratch);
27
+ }
28
+ for (let i = 0; i < 16; i++) {
29
+ scratch[i] = matrix[i] ?? 0;
30
+ }
31
+ return scratch;
32
+ }
33
+
34
+ /**
35
+ * Synchronizes ECS scene state with core scene objects.
36
+ *
37
+ * This system consumes attach/detach/gizmo intents and emits upsert/dispose
38
+ * commands. It also pushes transform updates for entities whose resolved
39
+ * constraint matrix changed in the current tick.
40
+ */
41
+ export const SceneSyncSystem: System = (world, context) => {
42
+ const intentsToRemove: number[] = [];
43
+ const realmId = world.coreRealmId;
44
+ if (realmId === undefined) return;
45
+
46
+ for (let i = 0; i < world.pendingIntents.length; i++) {
47
+ const intent = world.pendingIntents[i];
48
+ if (!intent) continue;
49
+
50
+ if (intent.type === 'attach-model') {
51
+ const modelId = world.nextCoreId++;
52
+ const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
53
+ const castShadow = intent.props.castShadow ?? true;
54
+ const receiveShadow = intent.props.receiveShadow ?? true;
55
+ const castOutline = intent.props.castOutline ?? false;
56
+ const outlineColor = intent.props.outlineColor ?? ([0, 0, 0, 0] as const);
57
+
58
+ enqueueCommand(context.worldId, 'cmd-model-upsert', {
59
+ realmId,
60
+ modelId,
61
+ geometryId: intent.props.geometryId,
62
+ materialId: intent.props.materialId,
63
+ transform: copyMatrixToScratch(world, intent.entityId, transform),
64
+ layerMask: 0x7fffffff,
65
+ castShadow,
66
+ receiveShadow,
67
+ castOutline,
68
+ outlineColor: [...outlineColor] as [number, number, number, number],
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('Model', {
77
+ type: 'Model',
78
+ id: modelId,
79
+ geometryId: intent.props.geometryId,
80
+ materialId: intent.props.materialId,
81
+ castShadow: intent.props.castShadow ?? true,
82
+ receiveShadow: intent.props.receiveShadow ?? true,
83
+ castOutline,
84
+ outlineColor: [...outlineColor] as [number, number, number, number],
85
+ skipUpdate: true,
86
+ });
87
+
88
+ intentsToRemove.push(i);
89
+ } else if (intent.type === 'attach-camera') {
90
+ const cameraId = world.nextCoreId++;
91
+ const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
92
+
93
+ enqueueCommand(context.worldId, 'cmd-camera-upsert', {
94
+ realmId,
95
+ cameraId,
96
+ label: `Cam ${cameraId}`,
97
+ kind: intent.props.kind ?? ('perspective' as CameraKind),
98
+ flags: 0,
99
+ nearFar: [intent.props.near ?? 0.1, intent.props.far ?? 1000],
100
+ order: intent.props.order ?? 0,
101
+ transform: copyMatrixToScratch(world, intent.entityId, transform),
102
+ layerMask: 0x7fffffff,
103
+ orthoScale: 1.0,
104
+ });
105
+
106
+ let store = world.components.get(intent.entityId);
107
+ if (!store) {
108
+ store = new Map();
109
+ world.components.set(intent.entityId, store);
110
+ }
111
+ store.set('Camera', {
112
+ type: 'Camera',
113
+ id: cameraId,
114
+ kind: intent.props.kind ?? ('perspective' as CameraKind),
115
+ near: intent.props.near ?? 0.1,
116
+ far: intent.props.far ?? 1000,
117
+ order: intent.props.order ?? 0,
118
+ orthoScale: 1.0,
119
+ skipUpdate: true,
120
+ });
121
+
122
+ // Auto-attach the first available camera to realm target layers that were bound
123
+ // before a camera existed (common when presenting world before scene setup).
124
+ for (const binding of world.targetLayerBindings.values()) {
125
+ if (binding.cameraId !== undefined) continue;
126
+ enqueueCommand(context.worldId, 'cmd-target-layer-upsert', {
127
+ realmId,
128
+ targetId: binding.targetId,
129
+ layout: binding.layout,
130
+ cameraId,
131
+ environmentId: binding.environmentId,
132
+ });
133
+ binding.cameraId = cameraId;
134
+ }
135
+
136
+ intentsToRemove.push(i);
137
+ } else if (intent.type === 'attach-light') {
138
+ const lightId = world.nextCoreId++;
139
+ const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
140
+
141
+ const pos = vec3.create();
142
+ mat4.getTranslation(pos, transform);
143
+ const direction = intent.props.direction
144
+ ? toVec3(intent.props.direction)
145
+ : ([0, 0, -1] as [number, number, number]);
146
+ const color = intent.props.color
147
+ ? toVec3(intent.props.color)
148
+ : ([1, 1, 1] as [number, number, number]);
149
+
150
+ const lightCmd: {
151
+ realmId: number;
152
+ lightId: number;
153
+ kind: LightKind;
154
+ color: [number, number, number, number];
155
+ intensity: number;
156
+ range: number;
157
+ castShadow: boolean;
158
+ position: [number, number, number, number];
159
+ layerMask: number;
160
+ direction?: [number, number, number, number];
161
+ spotInnerOuter?: [number, number];
162
+ } = {
163
+ realmId,
164
+ lightId,
165
+ kind: intent.props.kind ?? ('directional' as LightKind),
166
+ color: [...color, 1] as [number, number, number, number],
167
+ intensity: intent.props.intensity ?? 1.0,
168
+ range: intent.props.range ?? 10.0,
169
+ castShadow: intent.props.castShadow ?? true,
170
+ position: [pos[0], pos[1], pos[2], 1],
171
+ layerMask: 0x7fffffff,
172
+ };
173
+ if (direction) {
174
+ const [dirX, dirY, dirZ] = direction;
175
+ lightCmd.direction = [dirX, dirY, dirZ, 0];
176
+ }
177
+ if (intent.props.spotInnerOuter) {
178
+ lightCmd.spotInnerOuter = toVec2(intent.props.spotInnerOuter);
179
+ }
180
+ enqueueCommand(context.worldId, 'cmd-light-upsert', lightCmd);
181
+
182
+ let store = world.components.get(intent.entityId);
183
+ if (!store) {
184
+ store = new Map();
185
+ world.components.set(intent.entityId, store);
186
+ }
187
+ store.set('Light', {
188
+ type: 'Light',
189
+ id: lightId,
190
+ kind: intent.props.kind ?? ('directional' as LightKind),
191
+ color,
192
+ intensity: intent.props.intensity ?? 1.0,
193
+ range: intent.props.range ?? 10.0,
194
+ castShadow: intent.props.castShadow ?? true,
195
+ direction,
196
+ spotInnerOuter: intent.props.spotInnerOuter
197
+ ? toVec2(intent.props.spotInnerOuter)
198
+ : [0.2, 0.6],
199
+ skipUpdate: true,
200
+ });
201
+
202
+ intentsToRemove.push(i);
203
+ } else if (intent.type === 'detach-component') {
204
+ const store = world.components.get(intent.entityId);
205
+ if (store) {
206
+ const comp = store.get(intent.componentType) as Component | undefined;
207
+ if (comp && 'id' in comp) {
208
+ if (intent.componentType === 'Model') {
209
+ const modelComp = comp as ModelComponent;
210
+ enqueueCommand(context.worldId, 'cmd-model-dispose', {
211
+ realmId,
212
+ modelId: modelComp.id,
213
+ });
214
+ } else if (intent.componentType === 'Camera') {
215
+ const cameraComp = comp as CameraComponent;
216
+ enqueueCommand(context.worldId, 'cmd-camera-dispose', {
217
+ realmId,
218
+ cameraId: cameraComp.id,
219
+ });
220
+ } else if (intent.componentType === 'Light') {
221
+ const lightComp = comp as LightComponent;
222
+ enqueueCommand(context.worldId, 'cmd-light-dispose', {
223
+ realmId,
224
+ lightId: lightComp.id,
225
+ });
226
+ }
227
+ }
228
+ store.delete(intent.componentType);
229
+ }
230
+ intentsToRemove.push(i);
231
+ } else if (intent.type === 'gizmo-draw-line') {
232
+ enqueueCommand(context.worldId, 'cmd-gizmo-draw-line', {
233
+ start: toVec3(intent.start),
234
+ end: toVec3(intent.end),
235
+ color: toVec4(intent.color),
236
+ });
237
+ intentsToRemove.push(i);
238
+ } else if (intent.type === 'gizmo-draw-aabb') {
239
+ enqueueCommand(context.worldId, 'cmd-gizmo-draw-aabb', {
240
+ min: toVec3(intent.min),
241
+ max: toVec3(intent.max),
242
+ color: toVec4(intent.color),
243
+ });
244
+ intentsToRemove.push(i);
245
+ }
246
+ }
247
+
248
+ for (const entityId of world.constraintChangedEntities) {
249
+ const store = world.components.get(entityId);
250
+ if (!store) continue;
251
+
252
+ const matrix = getResolvedEntityTransformMatrix(world, entityId);
253
+ let matrixArray: number[] | undefined;
254
+
255
+ const model = store.get('Model') as ModelComponent | undefined;
256
+ if (model) {
257
+ if (model.skipUpdate) {
258
+ model.skipUpdate = false;
259
+ } else {
260
+ matrixArray = matrixArray ?? copyMatrixToScratch(world, entityId, matrix);
261
+ enqueueCommand(context.worldId, 'cmd-model-upsert', {
262
+ realmId,
263
+ modelId: model.id,
264
+ transform: matrixArray,
265
+ });
266
+ }
267
+ }
268
+
269
+ const camera = store.get('Camera') as CameraComponent | undefined;
270
+ if (camera) {
271
+ if (camera.skipUpdate) {
272
+ camera.skipUpdate = false;
273
+ } else {
274
+ matrixArray = matrixArray ?? copyMatrixToScratch(world, entityId, matrix);
275
+ enqueueCommand(context.worldId, 'cmd-camera-upsert', {
276
+ realmId,
277
+ cameraId: camera.id,
278
+ transform: matrixArray,
279
+ });
280
+ }
281
+ }
282
+
283
+ const light = store.get('Light') as LightComponent | undefined;
284
+ if (light) {
285
+ if (light.skipUpdate) {
286
+ light.skipUpdate = false;
287
+ } else {
288
+ const pos = vec3.create();
289
+ mat4.getTranslation(pos, matrix);
290
+ enqueueCommand(context.worldId, 'cmd-light-upsert', {
291
+ realmId,
292
+ lightId: light.id,
293
+ position: [pos[0], pos[1], pos[2], 1],
294
+ });
295
+ }
296
+ }
297
+ }
298
+
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
+ };
304
+
305
+ /** Backward-compatible alias while migrating existing integrations. */
306
+ export const CoreCommandBuilderSystem = SceneSyncSystem;