@vulfram/engine 0.5.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.
- package/README.md +106 -0
- package/package.json +55 -4
- package/src/core.ts +14 -0
- package/src/ecs.ts +1 -0
- package/src/engine/api.ts +234 -23
- package/src/engine/bridge/dispatch.ts +265 -40
- package/src/engine/bridge/guards.ts +4 -1
- package/src/engine/bridge/protocol.ts +72 -54
- package/src/engine/ecs/index.ts +187 -42
- package/src/engine/state.ts +133 -2
- package/src/engine/systems/command-intent.ts +153 -3
- package/src/engine/systems/constraint-solve.ts +167 -0
- package/src/engine/systems/core-command-builder.ts +9 -265
- package/src/engine/systems/diagnostics.ts +20 -19
- package/src/engine/systems/index.ts +3 -1
- package/src/engine/systems/input-mirror.ts +101 -3
- package/src/engine/systems/resource-upload.ts +96 -44
- package/src/engine/systems/response-decode.ts +69 -15
- package/src/engine/systems/scene-sync.ts +306 -0
- package/src/engine/systems/ui-bridge.ts +360 -0
- package/src/engine/systems/utils.ts +43 -1
- package/src/engine/systems/world-lifecycle.ts +72 -103
- package/src/engine/window/manager.ts +168 -0
- package/src/engine/world/entities.ts +931 -33
- package/src/engine/world/mount.ts +174 -0
- package/src/engine/world/types.ts +71 -0
- package/src/engine/world/world-ui.ts +266 -0
- package/src/engine/world/world3d.ts +280 -0
- package/src/index.ts +30 -1
- package/src/mount.ts +2 -0
- package/src/types/cmds/audio.ts +189 -0
- package/src/types/cmds/camera.ts +18 -13
- package/src/types/cmds/environment.ts +47 -4
- package/src/types/cmds/geometry.ts +18 -16
- package/src/types/cmds/index.ts +203 -132
- package/src/types/cmds/light.ts +17 -13
- package/src/types/cmds/material.ts +14 -13
- package/src/types/cmds/model.ts +40 -16
- package/src/types/cmds/realm.ts +25 -0
- package/src/types/cmds/render-graph.ts +49 -0
- package/src/types/cmds/resources.ts +4 -0
- package/src/types/cmds/shadow.ts +7 -7
- package/src/types/cmds/system.ts +29 -0
- package/src/types/cmds/target.ts +82 -0
- package/src/types/cmds/texture.ts +19 -5
- package/src/types/cmds/ui.ts +220 -0
- package/src/types/cmds/window.ts +41 -204
- package/src/types/events/index.ts +4 -1
- package/src/types/events/pointer.ts +42 -13
- package/src/types/events/system.ts +150 -7
- package/src/types/events/ui.ts +21 -0
- package/src/types/index.ts +1 -0
- package/src/types/json.ts +15 -0
- package/src/types/kinds.ts +3 -0
- package/src/window.ts +8 -0
- package/src/world-ui.ts +2 -0
- package/src/world3d.ts +10 -0
- package/tsconfig.json +0 -29
package/src/engine/ecs/index.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -114,6 +132,8 @@ export interface ModelProps {
|
|
|
114
132
|
materialId?: number;
|
|
115
133
|
castShadow?: boolean;
|
|
116
134
|
receiveShadow?: boolean;
|
|
135
|
+
castOutline?: boolean;
|
|
136
|
+
outlineColor?: [number, number, number, number];
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
/**
|
|
@@ -177,6 +197,91 @@ export interface WindowStateComponent {
|
|
|
177
197
|
focusChangedThisFrame: boolean;
|
|
178
198
|
}
|
|
179
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
|
+
|
|
180
285
|
/**
|
|
181
286
|
* Resource Properties
|
|
182
287
|
*/
|
|
@@ -226,6 +331,8 @@ export interface SphereOptions {
|
|
|
226
331
|
export interface CylinderOptions {
|
|
227
332
|
radius?: number;
|
|
228
333
|
height?: number;
|
|
334
|
+
sectors?: number;
|
|
335
|
+
/** @deprecated use `sectors` */
|
|
229
336
|
segments?: number;
|
|
230
337
|
}
|
|
231
338
|
|
|
@@ -235,7 +342,11 @@ export interface CylinderOptions {
|
|
|
235
342
|
export interface TorusOptions {
|
|
236
343
|
majorRadius?: number;
|
|
237
344
|
minorRadius?: number;
|
|
345
|
+
majorSegments?: number;
|
|
346
|
+
minorSegments?: number;
|
|
347
|
+
/** @deprecated use `majorSegments` */
|
|
238
348
|
radialSegments?: number;
|
|
349
|
+
/** @deprecated use `minorSegments` */
|
|
239
350
|
tubularSegments?: number;
|
|
240
351
|
}
|
|
241
352
|
|
|
@@ -290,6 +401,10 @@ export type Component =
|
|
|
290
401
|
| TagComponent
|
|
291
402
|
| InputStateComponent
|
|
292
403
|
| WindowStateComponent
|
|
404
|
+
| GamepadStateComponent
|
|
405
|
+
| SystemEventStateComponent
|
|
406
|
+
| UiEventStateComponent
|
|
407
|
+
| UiStateComponent
|
|
293
408
|
| CustomComponent;
|
|
294
409
|
|
|
295
410
|
/**
|
|
@@ -300,7 +415,7 @@ export type Component =
|
|
|
300
415
|
*/
|
|
301
416
|
export interface CustomComponent {
|
|
302
417
|
type: string;
|
|
303
|
-
data:
|
|
418
|
+
data: JsonObject;
|
|
304
419
|
}
|
|
305
420
|
|
|
306
421
|
/**
|
|
@@ -333,7 +448,7 @@ export type PropertyType =
|
|
|
333
448
|
*/
|
|
334
449
|
export interface SchemaProperty {
|
|
335
450
|
type: PropertyType;
|
|
336
|
-
default?:
|
|
451
|
+
default?: JsonValue;
|
|
337
452
|
optional?: boolean;
|
|
338
453
|
}
|
|
339
454
|
|
|
@@ -344,36 +459,6 @@ export interface ComponentSchema {
|
|
|
344
459
|
[key: string]: SchemaProperty;
|
|
345
460
|
}
|
|
346
461
|
|
|
347
|
-
/**
|
|
348
|
-
* Window update properties.
|
|
349
|
-
*/
|
|
350
|
-
export interface WindowProps {
|
|
351
|
-
title?: string;
|
|
352
|
-
position?: [number, number];
|
|
353
|
-
size?: [number, number];
|
|
354
|
-
state?: WindowState;
|
|
355
|
-
resizable?: boolean;
|
|
356
|
-
decorations?: boolean;
|
|
357
|
-
cursorVisible?: boolean;
|
|
358
|
-
cursorGrab?: CursorGrabMode;
|
|
359
|
-
icon?: string;
|
|
360
|
-
cursorIcon?: CursorIcon;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Window creation properties.
|
|
365
|
-
*/
|
|
366
|
-
export interface CreateWindowProps {
|
|
367
|
-
title: string;
|
|
368
|
-
size: [number, number];
|
|
369
|
-
position: [number, number];
|
|
370
|
-
canvasId?: string;
|
|
371
|
-
borderless?: boolean;
|
|
372
|
-
resizable?: boolean;
|
|
373
|
-
transparent?: boolean;
|
|
374
|
-
initialState?: WindowState;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
462
|
/**
|
|
378
463
|
* Intents:
|
|
379
464
|
* Requests to change the state of the World.
|
|
@@ -381,11 +466,6 @@ export interface CreateWindowProps {
|
|
|
381
466
|
*/
|
|
382
467
|
|
|
383
468
|
export type Intent =
|
|
384
|
-
| { type: 'create-window'; props: CreateWindowProps }
|
|
385
|
-
| { type: 'close-window' }
|
|
386
|
-
| { type: 'update-window'; props: WindowProps }
|
|
387
|
-
| { type: 'request-attention'; attentionType?: UserAttentionType }
|
|
388
|
-
| { type: 'focus-window' }
|
|
389
469
|
| { type: 'configure-environment'; config: EnvironmentConfig }
|
|
390
470
|
| {
|
|
391
471
|
type: 'request-resource-list';
|
|
@@ -451,10 +531,75 @@ export type Intent =
|
|
|
451
531
|
max: [number, number, number];
|
|
452
532
|
color: [number, number, number, number];
|
|
453
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
|
+
}
|
|
454
599
|
| {
|
|
455
600
|
type: 'custom';
|
|
456
601
|
name: string;
|
|
457
|
-
data:
|
|
602
|
+
data: JsonObject;
|
|
458
603
|
};
|
|
459
604
|
|
|
460
605
|
/**
|
package/src/engine/state.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|