@vulfram/engine 0.14.8-alpha → 0.19.2-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +106 -0
  2. package/package.json +60 -4
  3. package/src/core.ts +14 -0
  4. package/src/ecs.ts +1 -0
  5. package/src/engine/api.ts +222 -24
  6. package/src/engine/bridge/dispatch.ts +260 -40
  7. package/src/engine/bridge/guards.ts +4 -1
  8. package/src/engine/bridge/protocol.ts +69 -52
  9. package/src/engine/ecs/components.ts +340 -0
  10. package/src/engine/ecs/index.ts +3 -518
  11. package/src/engine/ecs/intents.ts +184 -0
  12. package/src/engine/ecs/systems.ts +26 -0
  13. package/src/engine/intents/store.ts +72 -0
  14. package/src/engine/state.ts +136 -5
  15. package/src/engine/systems/command-intent.ts +159 -14
  16. package/src/engine/systems/constraint-solve.ts +167 -0
  17. package/src/engine/systems/core-command-builder.ts +9 -268
  18. package/src/engine/systems/diagnostics.ts +20 -29
  19. package/src/engine/systems/index.ts +3 -1
  20. package/src/engine/systems/input-mirror.ts +257 -21
  21. package/src/engine/systems/resource-upload.ts +108 -58
  22. package/src/engine/systems/response-decode.ts +86 -15
  23. package/src/engine/systems/scene-sync.ts +305 -0
  24. package/src/engine/systems/ui-bridge.ts +381 -0
  25. package/src/engine/systems/utils.ts +86 -1
  26. package/src/engine/systems/world-lifecycle.ts +43 -114
  27. package/src/engine/window/manager.ts +168 -0
  28. package/src/engine/world/entities.ts +998 -91
  29. package/src/engine/world/mount.ts +195 -0
  30. package/src/engine/world/types.ts +71 -0
  31. package/src/engine/world/world-ui.ts +313 -0
  32. package/src/engine/world/world3d.ts +529 -0
  33. package/src/helpers/collision.ts +487 -0
  34. package/src/helpers/index.ts +2 -0
  35. package/src/helpers/raycast.ts +442 -0
  36. package/src/index.ts +30 -1
  37. package/src/mount.ts +2 -0
  38. package/src/types/cmds/audio.ts +73 -48
  39. package/src/types/cmds/camera.ts +12 -8
  40. package/src/types/cmds/environment.ts +9 -3
  41. package/src/types/cmds/geometry.ts +15 -16
  42. package/src/types/cmds/index.ts +234 -162
  43. package/src/types/cmds/input.ts +39 -0
  44. package/src/types/cmds/light.ts +12 -11
  45. package/src/types/cmds/material.ts +19 -21
  46. package/src/types/cmds/model.ts +17 -15
  47. package/src/types/cmds/realm.ts +23 -0
  48. package/src/types/cmds/system.ts +29 -0
  49. package/src/types/cmds/target.ts +96 -0
  50. package/src/types/cmds/texture.ts +13 -3
  51. package/src/types/cmds/ui.ts +220 -0
  52. package/src/types/cmds/window.ts +41 -204
  53. package/src/types/events/index.ts +4 -1
  54. package/src/types/events/keyboard.ts +2 -2
  55. package/src/types/events/pointer.ts +85 -13
  56. package/src/types/events/system.ts +188 -30
  57. package/src/types/events/ui.ts +21 -0
  58. package/src/types/index.ts +1 -0
  59. package/src/types/json.ts +15 -0
  60. package/src/window.ts +8 -0
  61. package/src/world-ui.ts +2 -0
  62. package/src/world3d.ts +10 -0
  63. package/tsconfig.json +0 -29
@@ -1,114 +1,31 @@
1
1
  import type { EnvironmentConfig } from '../../types/cmds/environment';
2
2
  import type { ShadowConfig } from '../../types/cmds/shadow';
3
- import type { WindowState } from '../../types/kinds';
4
3
  import { enqueueCommand } from '../bridge/dispatch';
5
4
  import type { System } from '../ecs';
6
- import { toVec2, toVec3 } from './utils';
5
+ import { toVec3, toVec4 } from './utils';
7
6
 
7
+ const WORLD_LIFECYCLE_INTENT_TYPES = [
8
+ 'configure-environment',
9
+ 'configure-shadows',
10
+ 'send-notification',
11
+ ] as const;
12
+
13
+ /**
14
+ * Applies world-scoped lifecycle/configuration intents.
15
+ *
16
+ * Covered intents:
17
+ * - environment configuration
18
+ * - shadow configuration
19
+ * - notification dispatch
20
+ */
8
21
  export const WorldLifecycleSystem: System = (world, context) => {
9
- const intentsToRemove: number[] = [];
22
+ const intents = world.intentStore.takeMany(WORLD_LIFECYCLE_INTENT_TYPES);
10
23
 
11
- for (let i = 0; i < world.pendingIntents.length; i++) {
12
- const intent = world.pendingIntents[i];
24
+ for (let i = 0; i < intents.length; i++) {
25
+ const intent = intents[i];
13
26
  if (!intent) continue;
14
27
 
15
- if (intent.type === 'create-window') {
16
- const cmd = {
17
- windowId: context.worldId,
18
- title: intent.props.title,
19
- size: toVec2(intent.props.size),
20
- position: toVec2(intent.props.position),
21
- borderless: intent.props.borderless ?? false,
22
- resizable: intent.props.resizable ?? true,
23
- transparent: intent.props.transparent ?? false,
24
- initialState: intent.props.initialState ?? ('windowed' as WindowState),
25
- } as const;
26
- const payload: typeof cmd & { canvasId?: string } = { ...cmd };
27
- if (intent.props.canvasId !== undefined) {
28
- payload.canvasId = intent.props.canvasId;
29
- }
30
- enqueueCommand(context.worldId, 'cmd-window-create', payload);
31
- intentsToRemove.push(i);
32
- } else if (intent.type === 'close-window') {
33
- enqueueCommand(context.worldId, 'cmd-window-close', {
34
- windowId: context.worldId,
35
- });
36
- intentsToRemove.push(i);
37
- } else if (intent.type === 'update-window') {
38
- const p = intent.props;
39
- if (p.title !== undefined) {
40
- enqueueCommand(context.worldId, 'cmd-window-set-title', {
41
- windowId: context.worldId,
42
- title: p.title,
43
- });
44
- }
45
- if (p.position !== undefined) {
46
- enqueueCommand(context.worldId, 'cmd-window-set-position', {
47
- windowId: context.worldId,
48
- position: p.position,
49
- });
50
- }
51
- if (p.size !== undefined) {
52
- enqueueCommand(context.worldId, 'cmd-window-set-size', {
53
- windowId: context.worldId,
54
- size: p.size,
55
- });
56
- }
57
- if (p.state !== undefined) {
58
- enqueueCommand(context.worldId, 'cmd-window-set-state', {
59
- windowId: context.worldId,
60
- state: p.state,
61
- });
62
- }
63
- if (p.resizable !== undefined) {
64
- enqueueCommand(context.worldId, 'cmd-window-set-resizable', {
65
- windowId: context.worldId,
66
- resizable: p.resizable,
67
- });
68
- }
69
- if (p.decorations !== undefined) {
70
- enqueueCommand(context.worldId, 'cmd-window-set-decorations', {
71
- windowId: context.worldId,
72
- decorations: p.decorations,
73
- });
74
- }
75
- if (p.cursorVisible !== undefined) {
76
- enqueueCommand(context.worldId, 'cmd-window-set-cursor-visible', {
77
- windowId: context.worldId,
78
- visible: p.cursorVisible,
79
- });
80
- }
81
- if (p.cursorGrab !== undefined) {
82
- enqueueCommand(context.worldId, 'cmd-window-set-cursor-grab', {
83
- windowId: context.worldId,
84
- mode: p.cursorGrab,
85
- });
86
- }
87
- if (p.icon !== undefined) {
88
- enqueueCommand(context.worldId, 'cmd-window-set-icon', {
89
- windowId: context.worldId,
90
- bufferId: p.icon,
91
- });
92
- }
93
- if (p.cursorIcon !== undefined) {
94
- enqueueCommand(context.worldId, 'cmd-window-set-cursor-icon', {
95
- windowId: context.worldId,
96
- icon: p.cursorIcon,
97
- });
98
- }
99
- intentsToRemove.push(i);
100
- } else if (intent.type === 'request-attention') {
101
- enqueueCommand(context.worldId, 'cmd-window-request-attention', {
102
- windowId: context.worldId,
103
- attentionType: intent.attentionType,
104
- });
105
- intentsToRemove.push(i);
106
- } else if (intent.type === 'focus-window') {
107
- enqueueCommand(context.worldId, 'cmd-window-focus', {
108
- windowId: context.worldId,
109
- });
110
- intentsToRemove.push(i);
111
- } else if (intent.type === 'configure-environment') {
28
+ if (intent.type === 'configure-environment') {
112
29
  const config = intent.config as EnvironmentConfig;
113
30
  const payload: EnvironmentConfig = {
114
31
  msaa: {
@@ -124,6 +41,7 @@ export const WorldLifecycleSystem: System = (world, context) => {
124
41
  skyColor: toVec3(config.skybox.skyColor),
125
42
  cubemapTextureId: config.skybox.cubemapTextureId ?? null,
126
43
  },
44
+ clearColor: toVec4(config.clearColor ?? [0, 0, 0, 0]),
127
45
  post: {
128
46
  filterEnabled: config.post.filterEnabled,
129
47
  filterExposure: config.post.filterExposure,
@@ -157,12 +75,31 @@ export const WorldLifecycleSystem: System = (world, context) => {
157
75
  bloomScatter: config.post.bloomScatter,
158
76
  },
159
77
  };
160
- enqueueCommand(context.worldId, 'cmd-environment-update', {
161
- windowId: context.worldId,
78
+ enqueueCommand(context.worldId, 'cmd-environment-upsert', {
79
+ environmentId: context.worldId,
162
80
  config: payload,
163
81
  });
164
- intentsToRemove.push(i);
82
+ // Keep current realm->target bindings synchronized with this environment.
83
+ for (const binding of world.targetLayerBindings.values()) {
84
+ binding.environmentId = context.worldId;
85
+ if (world.coreRealmId === undefined) continue;
86
+ enqueueCommand(context.worldId, 'cmd-target-layer-upsert', {
87
+ realmId: world.coreRealmId,
88
+ targetId: binding.targetId,
89
+ layout: binding.layout,
90
+ cameraId: binding.cameraId,
91
+ environmentId: context.worldId,
92
+ });
93
+ }
165
94
  } else if (intent.type === 'configure-shadows') {
95
+ let windowId = world.primaryWindowId;
96
+ if (windowId === undefined) {
97
+ for (const boundWindowId of world.targetWindowBindings.values()) {
98
+ windowId = boundWindowId;
99
+ break;
100
+ }
101
+ }
102
+ if (windowId === undefined) continue;
166
103
  const c = intent.config || {};
167
104
  const config: ShadowConfig = {
168
105
  tileResolution: c.tileResolution ?? 1024,
@@ -174,10 +111,9 @@ export const WorldLifecycleSystem: System = (world, context) => {
174
111
  normalBias: c.normalBias ?? 0.01,
175
112
  };
176
113
  enqueueCommand(context.worldId, 'cmd-shadow-configure', {
177
- windowId: context.worldId,
114
+ windowId,
178
115
  config,
179
116
  });
180
- intentsToRemove.push(i);
181
117
  } else if (intent.type === 'send-notification') {
182
118
  enqueueCommand(context.worldId, 'cmd-notification-send', {
183
119
  id: `notif-${Date.now()}`,
@@ -186,13 +122,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
186
122
  body: intent.message,
187
123
  timeout: 5000,
188
124
  });
189
- intentsToRemove.push(i);
190
125
  }
191
126
  }
192
-
193
- for (let i = intentsToRemove.length - 1; i >= 0; i--) {
194
- const idx = intentsToRemove[i];
195
- if (idx !== undefined) world.pendingIntents.splice(idx, 1);
196
- }
197
-
198
127
  };
@@ -0,0 +1,168 @@
1
+ import type { CmdWindowCloseArgs, CmdWindowCreateArgs } from '../../types/cmds/window';
2
+ import type {
3
+ CursorGrabMode,
4
+ CursorIcon,
5
+ UserAttentionType,
6
+ WindowState,
7
+ } from '../../types/kinds';
8
+ import { enqueueGlobalCommand } from '../bridge/dispatch';
9
+ import { requireInitialized } from '../bridge/guards';
10
+ import { engineState } from '../state';
11
+ import type { CommandId, WindowId } from '../world/types';
12
+ import { asCommandId, asWindowId } from '../world/types';
13
+
14
+ export interface WindowProps {
15
+ title?: string;
16
+ position?: [number, number];
17
+ size?: [number, number];
18
+ state?: WindowState;
19
+ resizable?: boolean;
20
+ decorations?: boolean;
21
+ cursorVisible?: boolean;
22
+ cursorGrab?: CursorGrabMode;
23
+ icon?: number;
24
+ cursorIcon?: CursorIcon;
25
+ }
26
+
27
+ export interface CreateWindowProps {
28
+ title: string;
29
+ size: [number, number];
30
+ position: [number, number];
31
+ canvasId?: string;
32
+ borderless?: boolean;
33
+ resizable?: boolean;
34
+ transparent?: boolean;
35
+ initialState?: WindowState;
36
+ }
37
+
38
+ function allocateWindowId(): WindowId {
39
+ while (engineState.usedWindowIds.has(engineState.nextWindowId)) {
40
+ engineState.nextWindowId++;
41
+ }
42
+ const id = engineState.nextWindowId++;
43
+ engineState.usedWindowIds.add(id);
44
+ return asWindowId(id);
45
+ }
46
+
47
+ /**
48
+ * Creates a window and returns a typed window id plus the command id.
49
+ *
50
+ * IDs are auto-generated by default to avoid collisions. You can pass `windowId`
51
+ * only when integrating with an existing host-level ID contract.
52
+ */
53
+ export function createWindow(
54
+ props: CreateWindowProps,
55
+ windowId?: WindowId,
56
+ ): { windowId: WindowId; commandId: CommandId } {
57
+ requireInitialized();
58
+ const resolvedWindowId = windowId ?? allocateWindowId();
59
+ if (windowId !== undefined) {
60
+ engineState.usedWindowIds.add(windowId as number);
61
+ }
62
+ const payload: CmdWindowCreateArgs = {
63
+ windowId: resolvedWindowId as number,
64
+ title: props.title,
65
+ size: props.size,
66
+ position: props.position,
67
+ canvasId: props.canvasId,
68
+ borderless: props.borderless ?? false,
69
+ resizable: props.resizable ?? true,
70
+ transparent: props.transparent ?? false,
71
+ initialState: props.initialState ?? 'windowed',
72
+ };
73
+ const commandId = enqueueGlobalCommand('cmd-window-create', payload);
74
+ engineState.pendingWindowCreateByCommandId.set(
75
+ commandId,
76
+ resolvedWindowId as number,
77
+ );
78
+ return { windowId: resolvedWindowId, commandId: asCommandId(commandId) };
79
+ }
80
+
81
+ export function closeWindow(windowId: WindowId): CommandId {
82
+ requireInitialized();
83
+ const payload: CmdWindowCloseArgs = { windowId: windowId as number };
84
+ const commandId = enqueueGlobalCommand('cmd-window-close', payload);
85
+ engineState.pendingWindowCloseByCommandId.set(commandId, windowId as number);
86
+ return asCommandId(commandId);
87
+ }
88
+
89
+ /**
90
+ * Applies partial window updates.
91
+ *
92
+ * Depending on the provided fields, this may enqueue up to three commands
93
+ * (`measurement`, `state`, `cursor`) and returns all resulting command ids.
94
+ */
95
+ export function updateWindow(windowId: WindowId, props: WindowProps): CommandId[] {
96
+ requireInitialized();
97
+ const commandIds: CommandId[] = [];
98
+
99
+ if (props.position !== undefined || props.size !== undefined) {
100
+ commandIds.push(asCommandId(
101
+ enqueueGlobalCommand('cmd-window-measurement', {
102
+ windowId: windowId as number,
103
+ position: props.position,
104
+ size: props.size,
105
+ }),
106
+ ));
107
+ }
108
+ if (
109
+ props.title !== undefined ||
110
+ props.state !== undefined ||
111
+ props.resizable !== undefined ||
112
+ props.decorations !== undefined ||
113
+ props.icon !== undefined
114
+ ) {
115
+ commandIds.push(asCommandId(
116
+ enqueueGlobalCommand('cmd-window-state', {
117
+ windowId: windowId as number,
118
+ title: props.title,
119
+ state: props.state,
120
+ resizable: props.resizable,
121
+ decorations: props.decorations,
122
+ iconBufferId: props.icon,
123
+ }),
124
+ ));
125
+ }
126
+ if (
127
+ props.cursorVisible !== undefined ||
128
+ props.cursorGrab !== undefined ||
129
+ props.cursorIcon !== undefined
130
+ ) {
131
+ commandIds.push(asCommandId(
132
+ enqueueGlobalCommand('cmd-window-cursor', {
133
+ windowId: windowId as number,
134
+ visible: props.cursorVisible,
135
+ mode: props.cursorGrab,
136
+ icon: props.cursorIcon,
137
+ }),
138
+ ));
139
+ }
140
+
141
+ return commandIds;
142
+ }
143
+
144
+ /**
145
+ * Requests user attention for a window.
146
+ *
147
+ * This does not guarantee foreground focus; behavior depends on host platform policy.
148
+ */
149
+ export function requestAttention(
150
+ windowId: WindowId,
151
+ attentionType?: UserAttentionType,
152
+ ): CommandId {
153
+ requireInitialized();
154
+ return asCommandId(enqueueGlobalCommand('cmd-window-state', {
155
+ windowId: windowId as number,
156
+ action: 'request-attention',
157
+ attentionType,
158
+ }));
159
+ }
160
+
161
+ /** Requests input focus for a window. */
162
+ export function focusWindow(windowId: WindowId): CommandId {
163
+ requireInitialized();
164
+ return asCommandId(enqueueGlobalCommand('cmd-window-state', {
165
+ windowId: windowId as number,
166
+ action: 'focus',
167
+ }));
168
+ }