@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
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { KeyCode } from '../../types/events/keyboard';
|
|
2
|
+
import { enqueueCommand } from '../bridge/dispatch';
|
|
3
|
+
import type {
|
|
4
|
+
InputStateComponent,
|
|
5
|
+
Intent,
|
|
6
|
+
System,
|
|
7
|
+
UiFieldsetScope,
|
|
8
|
+
UiFocusableNode,
|
|
9
|
+
UiFormScope,
|
|
10
|
+
UiStateComponent,
|
|
11
|
+
} from '../ecs';
|
|
12
|
+
|
|
13
|
+
const WORLD_ENTITY_ID = 0;
|
|
14
|
+
|
|
15
|
+
function ensureUiState(world: Parameters<System>[0]): UiStateComponent {
|
|
16
|
+
let worldStore = world.components.get(WORLD_ENTITY_ID);
|
|
17
|
+
if (!worldStore) {
|
|
18
|
+
worldStore = new Map();
|
|
19
|
+
world.components.set(WORLD_ENTITY_ID, worldStore);
|
|
20
|
+
world.entities.add(WORLD_ENTITY_ID);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let uiState = worldStore.get('UiState') as UiStateComponent | undefined;
|
|
24
|
+
if (!uiState) {
|
|
25
|
+
uiState = {
|
|
26
|
+
type: 'UiState',
|
|
27
|
+
forms: new Map(),
|
|
28
|
+
fieldsets: new Map(),
|
|
29
|
+
nodes: new Map(),
|
|
30
|
+
focusByWindow: new Map(),
|
|
31
|
+
};
|
|
32
|
+
worldStore.set('UiState', uiState);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return uiState;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getInputState(world: Parameters<System>[0]): InputStateComponent | undefined {
|
|
39
|
+
const worldStore = world.components.get(WORLD_ENTITY_ID);
|
|
40
|
+
return worldStore?.get('InputState') as InputStateComponent | undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fieldsetKey(formId: string, fieldsetId: string): string {
|
|
44
|
+
return `${formId}::${fieldsetId}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function canFocusNode(
|
|
48
|
+
form: UiFormScope,
|
|
49
|
+
node: UiFocusableNode,
|
|
50
|
+
fieldsets: Map<string, UiFieldsetScope>,
|
|
51
|
+
): boolean {
|
|
52
|
+
if (form.disabled || node.disabled || node.tabIndex < 0) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (form.activeFieldsetId && node.fieldsetId !== form.activeFieldsetId) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (node.fieldsetId) {
|
|
61
|
+
const fieldset = fieldsets.get(fieldsetKey(form.formId, node.fieldsetId));
|
|
62
|
+
if (fieldset?.disabled && fieldset.legendNodeId !== node.nodeId) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sortFocusables(a: UiFocusableNode, b: UiFocusableNode): number {
|
|
71
|
+
const aPositive = a.tabIndex > 0;
|
|
72
|
+
const bPositive = b.tabIndex > 0;
|
|
73
|
+
if (aPositive !== bPositive) return aPositive ? -1 : 1;
|
|
74
|
+
|
|
75
|
+
if (aPositive && bPositive && a.tabIndex !== b.tabIndex) {
|
|
76
|
+
return a.tabIndex - b.tabIndex;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (a.orderHint !== b.orderHint) {
|
|
80
|
+
return a.orderHint - b.orderHint;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return a.nodeId - b.nodeId;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveTargetForm(
|
|
87
|
+
uiState: UiStateComponent,
|
|
88
|
+
windowId: number,
|
|
89
|
+
formId?: string,
|
|
90
|
+
): UiFormScope | undefined {
|
|
91
|
+
if (formId) {
|
|
92
|
+
const form = uiState.forms.get(formId);
|
|
93
|
+
if (form && form.windowId === windowId && !form.disabled) {
|
|
94
|
+
return form;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const focused = uiState.focusByWindow.get(windowId);
|
|
99
|
+
if (focused) {
|
|
100
|
+
const form = uiState.forms.get(focused.formId);
|
|
101
|
+
if (form && !form.disabled) {
|
|
102
|
+
return form;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const forms = Array.from(uiState.forms.values())
|
|
107
|
+
.filter((f) => f.windowId === windowId && !f.disabled)
|
|
108
|
+
.sort((a, b) => a.formId.localeCompare(b.formId));
|
|
109
|
+
|
|
110
|
+
return forms[0];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resolveNextNode(
|
|
114
|
+
uiState: UiStateComponent,
|
|
115
|
+
windowId: number,
|
|
116
|
+
backwards: boolean,
|
|
117
|
+
formId?: string,
|
|
118
|
+
): { form: UiFormScope; nodeId: number } | null {
|
|
119
|
+
const form = resolveTargetForm(uiState, windowId, formId);
|
|
120
|
+
if (!form) return null;
|
|
121
|
+
|
|
122
|
+
const candidates = Array.from(uiState.nodes.values())
|
|
123
|
+
.filter((node) => node.formId === form.formId)
|
|
124
|
+
.filter((node) => canFocusNode(form, node, uiState.fieldsets))
|
|
125
|
+
.sort(sortFocusables);
|
|
126
|
+
|
|
127
|
+
if (candidates.length === 0) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const focused = uiState.focusByWindow.get(windowId);
|
|
132
|
+
const activeNodeId =
|
|
133
|
+
focused?.formId === form.formId ? focused.nodeId : form.activeNodeId;
|
|
134
|
+
const currentIndex = candidates.findIndex((node) => node.nodeId === activeNodeId);
|
|
135
|
+
|
|
136
|
+
let nextIndex = currentIndex;
|
|
137
|
+
if (currentIndex < 0) {
|
|
138
|
+
nextIndex = backwards ? candidates.length - 1 : 0;
|
|
139
|
+
} else {
|
|
140
|
+
nextIndex = backwards ? currentIndex - 1 : currentIndex + 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const cycleMode = form.cycleMode;
|
|
144
|
+
if (nextIndex < 0 || nextIndex >= candidates.length) {
|
|
145
|
+
if (cycleMode === 'clamp') {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
nextIndex = nextIndex < 0 ? candidates.length - 1 : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const target = candidates[nextIndex];
|
|
152
|
+
if (!target) return null;
|
|
153
|
+
return { form, nodeId: target.nodeId };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyFocus(uiState: UiStateComponent, windowId: number, formId: string, nodeId: number): void {
|
|
157
|
+
uiState.focusByWindow.set(windowId, { formId, nodeId });
|
|
158
|
+
const form = uiState.forms.get(formId);
|
|
159
|
+
if (form) {
|
|
160
|
+
form.activeNodeId = nodeId;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function processUiIntent(
|
|
165
|
+
worldId: number,
|
|
166
|
+
uiState: UiStateComponent,
|
|
167
|
+
intent: Intent,
|
|
168
|
+
): boolean {
|
|
169
|
+
switch (intent.type) {
|
|
170
|
+
case 'ui-theme-define':
|
|
171
|
+
enqueueCommand(worldId, 'cmd-ui-theme-define', intent.args);
|
|
172
|
+
return true;
|
|
173
|
+
case 'ui-theme-dispose':
|
|
174
|
+
enqueueCommand(worldId, 'cmd-ui-theme-dispose', intent.args);
|
|
175
|
+
return true;
|
|
176
|
+
case 'ui-document-create':
|
|
177
|
+
enqueueCommand(worldId, 'cmd-ui-document-create', intent.args);
|
|
178
|
+
return true;
|
|
179
|
+
case 'ui-document-dispose':
|
|
180
|
+
enqueueCommand(worldId, 'cmd-ui-document-dispose', intent.args);
|
|
181
|
+
return true;
|
|
182
|
+
case 'ui-document-set-rect':
|
|
183
|
+
enqueueCommand(worldId, 'cmd-ui-document-set-rect', intent.args);
|
|
184
|
+
return true;
|
|
185
|
+
case 'ui-document-set-theme':
|
|
186
|
+
enqueueCommand(worldId, 'cmd-ui-document-set-theme', intent.args);
|
|
187
|
+
return true;
|
|
188
|
+
case 'ui-document-get-tree':
|
|
189
|
+
enqueueCommand(worldId, 'cmd-ui-document-get-tree', intent.args);
|
|
190
|
+
return true;
|
|
191
|
+
case 'ui-document-get-layout-rects':
|
|
192
|
+
enqueueCommand(worldId, 'cmd-ui-document-get-layout-rects', intent.args);
|
|
193
|
+
return true;
|
|
194
|
+
case 'ui-apply-ops':
|
|
195
|
+
enqueueCommand(worldId, 'cmd-ui-apply-ops', intent.args);
|
|
196
|
+
return true;
|
|
197
|
+
case 'ui-debug-set':
|
|
198
|
+
enqueueCommand(worldId, 'cmd-ui-debug-set', intent.args);
|
|
199
|
+
return true;
|
|
200
|
+
case 'ui-focus-set': {
|
|
201
|
+
enqueueCommand(worldId, 'cmd-ui-focus-set', intent.args);
|
|
202
|
+
const form = resolveTargetForm(uiState, intent.args.windowId);
|
|
203
|
+
if (form && form.realmId === intent.args.realmId && form.documentId === intent.args.documentId) {
|
|
204
|
+
applyFocus(uiState, intent.args.windowId, form.formId, intent.args.nodeId ?? 0);
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
case 'ui-focus-get':
|
|
209
|
+
enqueueCommand(worldId, 'cmd-ui-focus-get', intent.args);
|
|
210
|
+
return true;
|
|
211
|
+
case 'ui-event-trace-set':
|
|
212
|
+
enqueueCommand(worldId, 'cmd-ui-event-trace-set', intent.args);
|
|
213
|
+
return true;
|
|
214
|
+
case 'ui-image-create-from-buffer':
|
|
215
|
+
enqueueCommand(worldId, 'cmd-ui-image-create-from-buffer', intent.args);
|
|
216
|
+
return true;
|
|
217
|
+
case 'ui-image-dispose':
|
|
218
|
+
enqueueCommand(worldId, 'cmd-ui-image-dispose', intent.args);
|
|
219
|
+
return true;
|
|
220
|
+
case 'ui-clipboard-paste':
|
|
221
|
+
enqueueCommand(worldId, 'cmd-ui-clipboard-paste', intent.args);
|
|
222
|
+
return true;
|
|
223
|
+
case 'ui-screenshot-reply':
|
|
224
|
+
enqueueCommand(worldId, 'cmd-ui-screenshot-reply', intent.args);
|
|
225
|
+
return true;
|
|
226
|
+
case 'ui-access-kit-action-request':
|
|
227
|
+
enqueueCommand(worldId, 'cmd-ui-access-kit-action-request', intent.args);
|
|
228
|
+
return true;
|
|
229
|
+
case 'ui-form-upsert': {
|
|
230
|
+
const existing = uiState.forms.get(intent.form.formId);
|
|
231
|
+
uiState.forms.set(intent.form.formId, {
|
|
232
|
+
formId: intent.form.formId,
|
|
233
|
+
windowId: intent.form.windowId,
|
|
234
|
+
realmId: intent.form.realmId,
|
|
235
|
+
documentId: intent.form.documentId,
|
|
236
|
+
disabled: intent.form.disabled ?? false,
|
|
237
|
+
cycleMode: intent.form.cycleMode ?? 'wrap',
|
|
238
|
+
activeFieldsetId: intent.form.activeFieldsetId,
|
|
239
|
+
activeNodeId: existing?.activeNodeId,
|
|
240
|
+
});
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
case 'ui-form-dispose': {
|
|
244
|
+
uiState.forms.delete(intent.formId);
|
|
245
|
+
for (const [key, fieldset] of uiState.fieldsets) {
|
|
246
|
+
if (fieldset.formId === intent.formId) {
|
|
247
|
+
uiState.fieldsets.delete(key);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
for (const [nodeId, node] of uiState.nodes) {
|
|
251
|
+
if (node.formId === intent.formId) {
|
|
252
|
+
uiState.nodes.delete(nodeId);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
for (const [windowId, focus] of uiState.focusByWindow) {
|
|
256
|
+
if (focus.formId === intent.formId) {
|
|
257
|
+
uiState.focusByWindow.delete(windowId);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
case 'ui-fieldset-upsert': {
|
|
263
|
+
uiState.fieldsets.set(fieldsetKey(intent.fieldset.formId, intent.fieldset.fieldsetId), {
|
|
264
|
+
formId: intent.fieldset.formId,
|
|
265
|
+
fieldsetId: intent.fieldset.fieldsetId,
|
|
266
|
+
disabled: intent.fieldset.disabled ?? false,
|
|
267
|
+
legendNodeId: intent.fieldset.legendNodeId,
|
|
268
|
+
});
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
case 'ui-fieldset-dispose':
|
|
272
|
+
uiState.fieldsets.delete(fieldsetKey(intent.formId, intent.fieldsetId));
|
|
273
|
+
return true;
|
|
274
|
+
case 'ui-focusable-upsert':
|
|
275
|
+
uiState.nodes.set(intent.focusable.nodeId, {
|
|
276
|
+
formId: intent.focusable.formId,
|
|
277
|
+
nodeId: intent.focusable.nodeId,
|
|
278
|
+
tabIndex: intent.focusable.tabIndex ?? 0,
|
|
279
|
+
fieldsetId: intent.focusable.fieldsetId,
|
|
280
|
+
disabled: intent.focusable.disabled ?? false,
|
|
281
|
+
orderHint: intent.focusable.orderHint ?? intent.focusable.nodeId,
|
|
282
|
+
});
|
|
283
|
+
return true;
|
|
284
|
+
case 'ui-focusable-dispose':
|
|
285
|
+
uiState.nodes.delete(intent.nodeId);
|
|
286
|
+
return true;
|
|
287
|
+
case 'ui-focus-next': {
|
|
288
|
+
const next = resolveNextNode(
|
|
289
|
+
uiState,
|
|
290
|
+
intent.windowId,
|
|
291
|
+
intent.backwards ?? false,
|
|
292
|
+
intent.formId,
|
|
293
|
+
);
|
|
294
|
+
if (!next) return true;
|
|
295
|
+
|
|
296
|
+
enqueueCommand(worldId, 'cmd-ui-focus-set', {
|
|
297
|
+
windowId: next.form.windowId,
|
|
298
|
+
realmId: next.form.realmId,
|
|
299
|
+
documentId: next.form.documentId,
|
|
300
|
+
nodeId: next.nodeId,
|
|
301
|
+
});
|
|
302
|
+
applyFocus(uiState, next.form.windowId, next.form.formId, next.nodeId);
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
default:
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Bridges UI-oriented intents into core UI commands and maintains focus state.
|
|
312
|
+
*
|
|
313
|
+
* It also provides default keyboard Tab navigation when explicit
|
|
314
|
+
* `ui-focus-next` intents are not emitted by the application.
|
|
315
|
+
*/
|
|
316
|
+
export const UiBridgeSystem: System = (world, context) => {
|
|
317
|
+
const uiState = ensureUiState(world);
|
|
318
|
+
const intentsToRemove: number[] = [];
|
|
319
|
+
let explicitFocusNavigation = false;
|
|
320
|
+
|
|
321
|
+
for (let i = 0; i < world.pendingIntents.length; i++) {
|
|
322
|
+
const intent = world.pendingIntents[i];
|
|
323
|
+
if (!intent) continue;
|
|
324
|
+
|
|
325
|
+
if (intent.type === 'ui-focus-next') {
|
|
326
|
+
explicitFocusNavigation = true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (processUiIntent(context.worldId, uiState, intent)) {
|
|
330
|
+
intentsToRemove.push(i);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (!explicitFocusNavigation) {
|
|
335
|
+
const inputState = getInputState(world);
|
|
336
|
+
if (inputState?.keysJustPressed.has(KeyCode.Tab)) {
|
|
337
|
+
const backwards =
|
|
338
|
+
inputState.keysPressed.has(KeyCode.ShiftLeft) ||
|
|
339
|
+
inputState.keysPressed.has(KeyCode.ShiftRight);
|
|
340
|
+
const focusWindowId = world.primaryWindowId;
|
|
341
|
+
if (focusWindowId !== undefined) {
|
|
342
|
+
const next = resolveNextNode(uiState, focusWindowId, backwards);
|
|
343
|
+
if (next) {
|
|
344
|
+
enqueueCommand(context.worldId, 'cmd-ui-focus-set', {
|
|
345
|
+
windowId: next.form.windowId,
|
|
346
|
+
realmId: next.form.realmId,
|
|
347
|
+
documentId: next.form.documentId,
|
|
348
|
+
nodeId: next.nodeId,
|
|
349
|
+
});
|
|
350
|
+
applyFocus(uiState, next.form.windowId, next.form.formId, next.nodeId);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
357
|
+
const idx = intentsToRemove[i];
|
|
358
|
+
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
import type { TransformComponent } from '../ecs';
|
|
10
10
|
import type { WorldState } from '../state';
|
|
11
11
|
|
|
12
|
+
/** Converts array-like numeric values into a fixed-length tuple buffer. */
|
|
12
13
|
export function toTuple(value: ArrayLike<number>, length: number): number[] {
|
|
13
14
|
const result = new Array<number>(length);
|
|
14
15
|
for (let i = 0; i < length; i++) {
|
|
@@ -17,16 +18,19 @@ export function toTuple(value: ArrayLike<number>, length: number): number[] {
|
|
|
17
18
|
return result;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** Normalizes arbitrary 2D vector input to a strict tuple. */
|
|
20
22
|
export function toVec2(value: ArrayLike<number>): [number, number] {
|
|
21
23
|
const result = toTuple(value, 2);
|
|
22
24
|
return [result[0] ?? 0, result[1] ?? 0];
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** Normalizes arbitrary 3D vector input to a strict tuple. */
|
|
25
28
|
export function toVec3(value: ArrayLike<number>): [number, number, number] {
|
|
26
29
|
const result = toTuple(value, 3);
|
|
27
30
|
return [result[0] ?? 0, result[1] ?? 0, result[2] ?? 0];
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
/** Normalizes arbitrary 4D vector input to a strict tuple. */
|
|
30
34
|
export function toVec4(value: ArrayLike<number>): [number, number, number, number] {
|
|
31
35
|
const result = toTuple(value, 4);
|
|
32
36
|
return [
|
|
@@ -37,10 +41,12 @@ export function toVec4(value: ArrayLike<number>): [number, number, number, numbe
|
|
|
37
41
|
];
|
|
38
42
|
}
|
|
39
43
|
|
|
44
|
+
/** Normalizes arbitrary quaternion-like input to a strict `[x, y, z, w]` tuple. */
|
|
40
45
|
export function toQuat(value: ArrayLike<number>): [number, number, number, number] {
|
|
41
46
|
return toVec4(value);
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/** Normalizes standard-material option payload for command serialization. */
|
|
44
50
|
export function normalizeStandardOptions(options: StandardOptions): StandardOptions {
|
|
45
51
|
const normalized: StandardOptions = {
|
|
46
52
|
...options,
|
|
@@ -56,6 +62,7 @@ export function normalizeStandardOptions(options: StandardOptions): StandardOpti
|
|
|
56
62
|
return normalized;
|
|
57
63
|
}
|
|
58
64
|
|
|
65
|
+
/** Normalizes PBR-material option payload for command serialization. */
|
|
59
66
|
export function normalizePbrOptions(options: PbrOptions): PbrOptions {
|
|
60
67
|
return {
|
|
61
68
|
...options,
|
|
@@ -64,6 +71,7 @@ export function normalizePbrOptions(options: PbrOptions): PbrOptions {
|
|
|
64
71
|
};
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/** Normalizes polymorphic material options into strict tuple-backed values. */
|
|
67
75
|
export function normalizeMaterialOptions(
|
|
68
76
|
options: MaterialOptions | undefined,
|
|
69
77
|
): MaterialOptions | undefined {
|
|
@@ -80,6 +88,7 @@ export function normalizeMaterialOptions(
|
|
|
80
88
|
};
|
|
81
89
|
}
|
|
82
90
|
|
|
91
|
+
/** Normalizes primitive options that contain vector payloads. */
|
|
83
92
|
export function normalizePrimitiveOptions(options: PrimitiveOptions): PrimitiveOptions {
|
|
84
93
|
if (options.type === 'cube') {
|
|
85
94
|
const content = options.content as CubeOptions;
|
|
@@ -114,7 +123,11 @@ export function normalizePrimitiveOptions(options: PrimitiveOptions): PrimitiveO
|
|
|
114
123
|
return options;
|
|
115
124
|
}
|
|
116
125
|
|
|
117
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Resolves the local transform matrix for an entity, without applying constraints
|
|
128
|
+
* (for example parent hierarchy composition).
|
|
129
|
+
*/
|
|
130
|
+
export function getEntityLocalTransformMatrix(
|
|
118
131
|
world: WorldState,
|
|
119
132
|
entityId: number,
|
|
120
133
|
): mat4 {
|
|
@@ -145,3 +158,32 @@ export function getEntityTransformMatrix(
|
|
|
145
158
|
}
|
|
146
159
|
return m;
|
|
147
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Returns the latest constraint-resolved world matrix for an entity.
|
|
164
|
+
* Falls back to local transform when no resolved matrix is cached yet.
|
|
165
|
+
*/
|
|
166
|
+
export function getResolvedEntityTransformMatrix(
|
|
167
|
+
world: WorldState,
|
|
168
|
+
entityId: number,
|
|
169
|
+
): mat4 {
|
|
170
|
+
const resolved = world.resolvedEntityTransforms.get(entityId);
|
|
171
|
+
if (!resolved) {
|
|
172
|
+
return getEntityLocalTransformMatrix(world, entityId);
|
|
173
|
+
}
|
|
174
|
+
return mat4.clone(resolved);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Compares 4x4 matrices using an absolute epsilon per component. */
|
|
178
|
+
export function mat4EqualsApprox(
|
|
179
|
+
a: ArrayLike<number>,
|
|
180
|
+
b: ArrayLike<number>,
|
|
181
|
+
epsilon = 1e-6,
|
|
182
|
+
): boolean {
|
|
183
|
+
for (let i = 0; i < 16; i++) {
|
|
184
|
+
if (Math.abs((a[i] ?? 0) - (b[i] ?? 0)) > epsilon) {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
@@ -1,10 +1,17 @@
|
|
|
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 {
|
|
5
|
+
import { toVec3, toVec4 } from './utils';
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Applies world-scoped lifecycle/configuration intents.
|
|
9
|
+
*
|
|
10
|
+
* Covered intents:
|
|
11
|
+
* - environment configuration
|
|
12
|
+
* - shadow configuration
|
|
13
|
+
* - notification dispatch
|
|
14
|
+
*/
|
|
8
15
|
export const WorldLifecycleSystem: System = (world, context) => {
|
|
9
16
|
const intentsToRemove: number[] = [];
|
|
10
17
|
|
|
@@ -12,103 +19,7 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
12
19
|
const intent = world.pendingIntents[i];
|
|
13
20
|
if (!intent) continue;
|
|
14
21
|
|
|
15
|
-
if (intent.type === '
|
|
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
|
-
icon: 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') {
|
|
22
|
+
if (intent.type === 'configure-environment') {
|
|
112
23
|
const config = intent.config as EnvironmentConfig;
|
|
113
24
|
const payload: EnvironmentConfig = {
|
|
114
25
|
msaa: {
|
|
@@ -119,16 +30,74 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
119
30
|
mode: config.skybox.mode,
|
|
120
31
|
intensity: config.skybox.intensity,
|
|
121
32
|
rotation: config.skybox.rotation,
|
|
122
|
-
|
|
33
|
+
groundColor: toVec3(config.skybox.groundColor),
|
|
34
|
+
horizonColor: toVec3(config.skybox.horizonColor),
|
|
35
|
+
skyColor: toVec3(config.skybox.skyColor),
|
|
123
36
|
cubemapTextureId: config.skybox.cubemapTextureId ?? null,
|
|
124
37
|
},
|
|
38
|
+
clearColor: toVec4(config.clearColor ?? [0, 0, 0, 0]),
|
|
39
|
+
post: {
|
|
40
|
+
filterEnabled: config.post.filterEnabled,
|
|
41
|
+
filterExposure: config.post.filterExposure,
|
|
42
|
+
filterGamma: config.post.filterGamma,
|
|
43
|
+
filterSaturation: config.post.filterSaturation,
|
|
44
|
+
filterContrast: config.post.filterContrast,
|
|
45
|
+
filterVignette: config.post.filterVignette,
|
|
46
|
+
filterGrain: config.post.filterGrain,
|
|
47
|
+
filterChromaticAberration: config.post.filterChromaticAberration,
|
|
48
|
+
filterBlur: config.post.filterBlur,
|
|
49
|
+
filterSharpen: config.post.filterSharpen,
|
|
50
|
+
filterTonemapMode: config.post.filterTonemapMode,
|
|
51
|
+
outlineEnabled: config.post.outlineEnabled,
|
|
52
|
+
outlineStrength: config.post.outlineStrength,
|
|
53
|
+
outlineThreshold: config.post.outlineThreshold,
|
|
54
|
+
outlineWidth: config.post.outlineWidth,
|
|
55
|
+
outlineQuality: config.post.outlineQuality,
|
|
56
|
+
filterPosterizeSteps: config.post.filterPosterizeSteps,
|
|
57
|
+
cellShading: config.post.cellShading,
|
|
58
|
+
ssaoEnabled: config.post.ssaoEnabled,
|
|
59
|
+
ssaoStrength: config.post.ssaoStrength,
|
|
60
|
+
ssaoRadius: config.post.ssaoRadius,
|
|
61
|
+
ssaoBias: config.post.ssaoBias,
|
|
62
|
+
ssaoPower: config.post.ssaoPower,
|
|
63
|
+
ssaoBlurRadius: config.post.ssaoBlurRadius,
|
|
64
|
+
ssaoBlurDepthThreshold: config.post.ssaoBlurDepthThreshold,
|
|
65
|
+
bloomEnabled: config.post.bloomEnabled,
|
|
66
|
+
bloomThreshold: config.post.bloomThreshold,
|
|
67
|
+
bloomKnee: config.post.bloomKnee,
|
|
68
|
+
bloomIntensity: config.post.bloomIntensity,
|
|
69
|
+
bloomScatter: config.post.bloomScatter,
|
|
70
|
+
},
|
|
125
71
|
};
|
|
126
|
-
enqueueCommand(context.worldId, 'cmd-environment-
|
|
127
|
-
|
|
72
|
+
enqueueCommand(context.worldId, 'cmd-environment-upsert', {
|
|
73
|
+
environmentId: context.worldId,
|
|
128
74
|
config: payload,
|
|
129
75
|
});
|
|
76
|
+
// Keep current realm->target bindings synchronized with this environment.
|
|
77
|
+
for (const binding of world.targetLayerBindings.values()) {
|
|
78
|
+
binding.environmentId = context.worldId;
|
|
79
|
+
if (world.coreRealmId === undefined) continue;
|
|
80
|
+
enqueueCommand(context.worldId, 'cmd-target-layer-upsert', {
|
|
81
|
+
realmId: world.coreRealmId,
|
|
82
|
+
targetId: binding.targetId,
|
|
83
|
+
layout: binding.layout,
|
|
84
|
+
cameraId: binding.cameraId,
|
|
85
|
+
environmentId: context.worldId,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
130
88
|
intentsToRemove.push(i);
|
|
131
89
|
} else if (intent.type === 'configure-shadows') {
|
|
90
|
+
let windowId = world.primaryWindowId;
|
|
91
|
+
if (windowId === undefined) {
|
|
92
|
+
for (const boundWindowId of world.targetWindowBindings.values()) {
|
|
93
|
+
windowId = boundWindowId;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (windowId === undefined) {
|
|
98
|
+
windowId = world.realmCreateArgs.hostWindowId;
|
|
99
|
+
}
|
|
100
|
+
if (windowId === undefined) continue;
|
|
132
101
|
const c = intent.config || {};
|
|
133
102
|
const config: ShadowConfig = {
|
|
134
103
|
tileResolution: c.tileResolution ?? 1024,
|
|
@@ -140,7 +109,7 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
140
109
|
normalBias: c.normalBias ?? 0.01,
|
|
141
110
|
};
|
|
142
111
|
enqueueCommand(context.worldId, 'cmd-shadow-configure', {
|
|
143
|
-
windowId
|
|
112
|
+
windowId,
|
|
144
113
|
config,
|
|
145
114
|
});
|
|
146
115
|
intentsToRemove.push(i);
|