@vulfram/engine 0.17.1-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.
- package/README.md +3 -3
- package/package.json +9 -4
- package/src/engine/api.ts +12 -25
- package/src/engine/bridge/dispatch.ts +3 -8
- package/src/engine/ecs/components.ts +340 -0
- package/src/engine/ecs/index.ts +3 -661
- package/src/engine/ecs/intents.ts +184 -0
- package/src/engine/ecs/systems.ts +26 -0
- package/src/engine/intents/store.ts +72 -0
- package/src/engine/state.ts +3 -3
- package/src/engine/systems/command-intent.ts +11 -16
- package/src/engine/systems/diagnostics.ts +3 -13
- package/src/engine/systems/input-mirror.ts +156 -18
- package/src/engine/systems/resource-upload.ts +12 -14
- package/src/engine/systems/response-decode.ts +17 -0
- package/src/engine/systems/scene-sync.ts +12 -13
- package/src/engine/systems/ui-bridge.ts +31 -10
- package/src/engine/systems/utils.ts +46 -3
- package/src/engine/systems/world-lifecycle.ts +9 -15
- package/src/engine/world/entities.ts +201 -37
- package/src/engine/world/mount.ts +27 -6
- package/src/engine/world/world-ui.ts +77 -30
- package/src/engine/world/world3d.ts +282 -33
- package/src/helpers/collision.ts +487 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/raycast.ts +442 -0
- package/src/types/cmds/geometry.ts +2 -2
- package/src/types/cmds/index.ts +42 -0
- package/src/types/cmds/input.ts +39 -0
- package/src/types/cmds/material.ts +10 -10
- package/src/types/cmds/realm.ts +0 -2
- package/src/types/cmds/system.ts +10 -0
- package/src/types/cmds/target.ts +14 -0
- package/src/types/events/keyboard.ts +2 -2
- package/src/types/events/pointer.ts +43 -0
- package/src/types/events/system.ts +44 -0
|
@@ -6,6 +6,15 @@ import { enqueueCommand } from '../bridge/dispatch';
|
|
|
6
6
|
import type { GeometryProps, System } from '../ecs';
|
|
7
7
|
import { normalizeMaterialOptions, normalizePrimitiveOptions } from './utils';
|
|
8
8
|
|
|
9
|
+
const RESOURCE_INTENT_TYPES = [
|
|
10
|
+
'create-material',
|
|
11
|
+
'dispose-material',
|
|
12
|
+
'create-geometry',
|
|
13
|
+
'dispose-geometry',
|
|
14
|
+
'create-texture',
|
|
15
|
+
'dispose-texture',
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
9
18
|
function buildPrimitiveOptions(
|
|
10
19
|
primitiveProps: Extract<GeometryProps, { type: 'primitive' }>,
|
|
11
20
|
): PrimitiveOptions {
|
|
@@ -77,10 +86,10 @@ function buildPrimitiveOptions(
|
|
|
77
86
|
* - texture (from buffer or solid color)
|
|
78
87
|
*/
|
|
79
88
|
export const ResourceUploadSystem: System = (world, context) => {
|
|
80
|
-
const
|
|
89
|
+
const intents = world.intentStore.takeMany(RESOURCE_INTENT_TYPES);
|
|
81
90
|
|
|
82
|
-
for (let i = 0; i <
|
|
83
|
-
const intent =
|
|
91
|
+
for (let i = 0; i < intents.length; i++) {
|
|
92
|
+
const intent = intents[i];
|
|
84
93
|
if (!intent) continue;
|
|
85
94
|
|
|
86
95
|
if (intent.type === 'create-material') {
|
|
@@ -99,12 +108,10 @@ export const ResourceUploadSystem: System = (world, context) => {
|
|
|
99
108
|
kind: intent.props.kind ?? 'standard',
|
|
100
109
|
options: options,
|
|
101
110
|
});
|
|
102
|
-
intentsToRemove.push(i);
|
|
103
111
|
} else if (intent.type === 'dispose-material') {
|
|
104
112
|
enqueueCommand(context.worldId, 'cmd-material-dispose', {
|
|
105
113
|
materialId: intent.resourceId,
|
|
106
114
|
});
|
|
107
|
-
intentsToRemove.push(i);
|
|
108
115
|
} else if (intent.type === 'create-geometry') {
|
|
109
116
|
if (intent.props.type === 'primitive') {
|
|
110
117
|
const options = normalizePrimitiveOptions(
|
|
@@ -124,12 +131,10 @@ export const ResourceUploadSystem: System = (world, context) => {
|
|
|
124
131
|
entries: intent.props.entries,
|
|
125
132
|
});
|
|
126
133
|
}
|
|
127
|
-
intentsToRemove.push(i);
|
|
128
134
|
} else if (intent.type === 'dispose-geometry') {
|
|
129
135
|
enqueueCommand(context.worldId, 'cmd-geometry-dispose', {
|
|
130
136
|
geometryId: intent.resourceId,
|
|
131
137
|
});
|
|
132
|
-
intentsToRemove.push(i);
|
|
133
138
|
} else if (intent.type === 'create-texture') {
|
|
134
139
|
if (intent.props.source.type === 'color') {
|
|
135
140
|
const cmd: {
|
|
@@ -179,17 +184,10 @@ export const ResourceUploadSystem: System = (world, context) => {
|
|
|
179
184
|
}
|
|
180
185
|
enqueueCommand(context.worldId, 'cmd-texture-create-from-buffer', cmd);
|
|
181
186
|
}
|
|
182
|
-
intentsToRemove.push(i);
|
|
183
187
|
} else if (intent.type === 'dispose-texture') {
|
|
184
188
|
enqueueCommand(context.worldId, 'cmd-texture-dispose', {
|
|
185
189
|
textureId: intent.resourceId,
|
|
186
190
|
});
|
|
187
|
-
intentsToRemove.push(i);
|
|
188
191
|
}
|
|
189
192
|
}
|
|
190
|
-
|
|
191
|
-
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
192
|
-
const idx = intentsToRemove[i];
|
|
193
|
-
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
194
|
-
}
|
|
195
193
|
};
|
|
@@ -5,6 +5,21 @@ import { engineState } from '../state';
|
|
|
5
5
|
const MAX_REALM_CREATE_RETRIES = 8;
|
|
6
6
|
const BASE_REALM_RETRY_DELAY_MS = 32;
|
|
7
7
|
|
|
8
|
+
function flushPendingTargetLayerBinds(world: Parameters<System>[0], worldId: number): void {
|
|
9
|
+
const realmId = world.coreRealmId;
|
|
10
|
+
if (realmId === undefined) return;
|
|
11
|
+
|
|
12
|
+
for (const binding of world.targetLayerBindings.values()) {
|
|
13
|
+
enqueueCommand(worldId, 'cmd-target-layer-upsert', {
|
|
14
|
+
realmId,
|
|
15
|
+
targetId: binding.targetId,
|
|
16
|
+
layout: binding.layout,
|
|
17
|
+
cameraId: binding.cameraId,
|
|
18
|
+
environmentId: binding.environmentId,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
8
23
|
/**
|
|
9
24
|
* Decodes and applies command responses routed to a world.
|
|
10
25
|
*
|
|
@@ -57,6 +72,7 @@ export const ResponseDecodeSystem: System = (world, context) => {
|
|
|
57
72
|
world.realmCreateRetryCount = 0;
|
|
58
73
|
world.nextRealmCreateRetryAtMs = 0;
|
|
59
74
|
markRoutingIndexDirty();
|
|
75
|
+
flushPendingTargetLayerBinds(world, context.worldId);
|
|
60
76
|
}
|
|
61
77
|
} else if (res.type === 'window-create') {
|
|
62
78
|
const created = res.content as {
|
|
@@ -69,6 +85,7 @@ export const ResponseDecodeSystem: System = (world, context) => {
|
|
|
69
85
|
world.realmCreateRetryCount = 0;
|
|
70
86
|
world.nextRealmCreateRetryAtMs = 0;
|
|
71
87
|
markRoutingIndexDirty();
|
|
88
|
+
flushPendingTargetLayerBinds(world, context.worldId);
|
|
72
89
|
}
|
|
73
90
|
if (typeof created.surfaceId === 'number') {
|
|
74
91
|
world.coreSurfaceId = created.surfaceId;
|
|
@@ -15,6 +15,15 @@ import {
|
|
|
15
15
|
toVec4,
|
|
16
16
|
} from './utils';
|
|
17
17
|
|
|
18
|
+
const SCENE_SYNC_INTENT_TYPES = [
|
|
19
|
+
'attach-model',
|
|
20
|
+
'attach-camera',
|
|
21
|
+
'attach-light',
|
|
22
|
+
'detach-component',
|
|
23
|
+
'gizmo-draw-line',
|
|
24
|
+
'gizmo-draw-aabb',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
18
27
|
function copyMatrixToScratch(
|
|
19
28
|
world: Parameters<System>[0],
|
|
20
29
|
entityId: number,
|
|
@@ -39,12 +48,12 @@ function copyMatrixToScratch(
|
|
|
39
48
|
* constraint matrix changed in the current tick.
|
|
40
49
|
*/
|
|
41
50
|
export const SceneSyncSystem: System = (world, context) => {
|
|
42
|
-
const intentsToRemove: number[] = [];
|
|
43
51
|
const realmId = world.coreRealmId;
|
|
44
52
|
if (realmId === undefined) return;
|
|
53
|
+
const intents = world.intentStore.takeMany(SCENE_SYNC_INTENT_TYPES);
|
|
45
54
|
|
|
46
|
-
for (let i = 0; i <
|
|
47
|
-
const intent =
|
|
55
|
+
for (let i = 0; i < intents.length; i++) {
|
|
56
|
+
const intent = intents[i];
|
|
48
57
|
if (!intent) continue;
|
|
49
58
|
|
|
50
59
|
if (intent.type === 'attach-model') {
|
|
@@ -85,7 +94,6 @@ export const SceneSyncSystem: System = (world, context) => {
|
|
|
85
94
|
skipUpdate: true,
|
|
86
95
|
});
|
|
87
96
|
|
|
88
|
-
intentsToRemove.push(i);
|
|
89
97
|
} else if (intent.type === 'attach-camera') {
|
|
90
98
|
const cameraId = world.nextCoreId++;
|
|
91
99
|
const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
|
|
@@ -133,7 +141,6 @@ export const SceneSyncSystem: System = (world, context) => {
|
|
|
133
141
|
binding.cameraId = cameraId;
|
|
134
142
|
}
|
|
135
143
|
|
|
136
|
-
intentsToRemove.push(i);
|
|
137
144
|
} else if (intent.type === 'attach-light') {
|
|
138
145
|
const lightId = world.nextCoreId++;
|
|
139
146
|
const transform = getResolvedEntityTransformMatrix(world, intent.entityId);
|
|
@@ -199,7 +206,6 @@ export const SceneSyncSystem: System = (world, context) => {
|
|
|
199
206
|
skipUpdate: true,
|
|
200
207
|
});
|
|
201
208
|
|
|
202
|
-
intentsToRemove.push(i);
|
|
203
209
|
} else if (intent.type === 'detach-component') {
|
|
204
210
|
const store = world.components.get(intent.entityId);
|
|
205
211
|
if (store) {
|
|
@@ -227,21 +233,18 @@ export const SceneSyncSystem: System = (world, context) => {
|
|
|
227
233
|
}
|
|
228
234
|
store.delete(intent.componentType);
|
|
229
235
|
}
|
|
230
|
-
intentsToRemove.push(i);
|
|
231
236
|
} else if (intent.type === 'gizmo-draw-line') {
|
|
232
237
|
enqueueCommand(context.worldId, 'cmd-gizmo-draw-line', {
|
|
233
238
|
start: toVec3(intent.start),
|
|
234
239
|
end: toVec3(intent.end),
|
|
235
240
|
color: toVec4(intent.color),
|
|
236
241
|
});
|
|
237
|
-
intentsToRemove.push(i);
|
|
238
242
|
} else if (intent.type === 'gizmo-draw-aabb') {
|
|
239
243
|
enqueueCommand(context.worldId, 'cmd-gizmo-draw-aabb', {
|
|
240
244
|
min: toVec3(intent.min),
|
|
241
245
|
max: toVec3(intent.max),
|
|
242
246
|
color: toVec4(intent.color),
|
|
243
247
|
});
|
|
244
|
-
intentsToRemove.push(i);
|
|
245
248
|
}
|
|
246
249
|
}
|
|
247
250
|
|
|
@@ -296,10 +299,6 @@ export const SceneSyncSystem: System = (world, context) => {
|
|
|
296
299
|
}
|
|
297
300
|
}
|
|
298
301
|
|
|
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
302
|
};
|
|
304
303
|
|
|
305
304
|
/** Backward-compatible alias while migrating existing integrations. */
|
|
@@ -11,6 +11,33 @@ import type {
|
|
|
11
11
|
} from '../ecs';
|
|
12
12
|
|
|
13
13
|
const WORLD_ENTITY_ID = 0;
|
|
14
|
+
const UI_INTENT_TYPES = [
|
|
15
|
+
'ui-theme-define',
|
|
16
|
+
'ui-theme-dispose',
|
|
17
|
+
'ui-document-create',
|
|
18
|
+
'ui-document-dispose',
|
|
19
|
+
'ui-document-set-rect',
|
|
20
|
+
'ui-document-set-theme',
|
|
21
|
+
'ui-document-get-tree',
|
|
22
|
+
'ui-document-get-layout-rects',
|
|
23
|
+
'ui-apply-ops',
|
|
24
|
+
'ui-debug-set',
|
|
25
|
+
'ui-focus-set',
|
|
26
|
+
'ui-focus-get',
|
|
27
|
+
'ui-event-trace-set',
|
|
28
|
+
'ui-image-create-from-buffer',
|
|
29
|
+
'ui-image-dispose',
|
|
30
|
+
'ui-clipboard-paste',
|
|
31
|
+
'ui-screenshot-reply',
|
|
32
|
+
'ui-access-kit-action-request',
|
|
33
|
+
'ui-form-upsert',
|
|
34
|
+
'ui-form-dispose',
|
|
35
|
+
'ui-fieldset-upsert',
|
|
36
|
+
'ui-fieldset-dispose',
|
|
37
|
+
'ui-focusable-upsert',
|
|
38
|
+
'ui-focusable-dispose',
|
|
39
|
+
'ui-focus-next',
|
|
40
|
+
] as const;
|
|
14
41
|
|
|
15
42
|
function ensureUiState(world: Parameters<System>[0]): UiStateComponent {
|
|
16
43
|
let worldStore = world.components.get(WORLD_ENTITY_ID);
|
|
@@ -315,20 +342,18 @@ function processUiIntent(
|
|
|
315
342
|
*/
|
|
316
343
|
export const UiBridgeSystem: System = (world, context) => {
|
|
317
344
|
const uiState = ensureUiState(world);
|
|
318
|
-
const intentsToRemove: number[] = [];
|
|
319
345
|
let explicitFocusNavigation = false;
|
|
346
|
+
const intents = world.intentStore.takeMany(UI_INTENT_TYPES);
|
|
320
347
|
|
|
321
|
-
for (let i = 0; i <
|
|
322
|
-
const intent =
|
|
348
|
+
for (let i = 0; i < intents.length; i++) {
|
|
349
|
+
const intent = intents[i];
|
|
323
350
|
if (!intent) continue;
|
|
324
351
|
|
|
325
352
|
if (intent.type === 'ui-focus-next') {
|
|
326
353
|
explicitFocusNavigation = true;
|
|
327
354
|
}
|
|
328
355
|
|
|
329
|
-
|
|
330
|
-
intentsToRemove.push(i);
|
|
331
|
-
}
|
|
356
|
+
processUiIntent(context.worldId, uiState, intent);
|
|
332
357
|
}
|
|
333
358
|
|
|
334
359
|
if (!explicitFocusNavigation) {
|
|
@@ -353,8 +378,4 @@ export const UiBridgeSystem: System = (world, context) => {
|
|
|
353
378
|
}
|
|
354
379
|
}
|
|
355
380
|
|
|
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
381
|
};
|
|
@@ -168,10 +168,53 @@ export function getResolvedEntityTransformMatrix(
|
|
|
168
168
|
entityId: number,
|
|
169
169
|
): mat4 {
|
|
170
170
|
const resolved = world.resolvedEntityTransforms.get(entityId);
|
|
171
|
-
if (!
|
|
172
|
-
return
|
|
171
|
+
if (resolved && !hasDirtyConstraintPath(world, entityId)) {
|
|
172
|
+
return mat4.clone(resolved);
|
|
173
173
|
}
|
|
174
|
-
|
|
174
|
+
|
|
175
|
+
// Fallback for same-frame reads before ConstraintSolve runs:
|
|
176
|
+
// recompute parent-chain world transform directly from local components.
|
|
177
|
+
return resolveEntityWorldTransformImmediate(world, entityId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function hasDirtyConstraintPath(world: WorldState, entityId: number): boolean {
|
|
181
|
+
let current: number | undefined = entityId;
|
|
182
|
+
while (current !== undefined) {
|
|
183
|
+
if (world.constraintDirtyEntities.has(current)) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
current = world.constraintParentByChild.get(current);
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resolveEntityWorldTransformImmediate(
|
|
192
|
+
world: WorldState,
|
|
193
|
+
entityId: number,
|
|
194
|
+
): mat4 {
|
|
195
|
+
const visiting = new Set<number>();
|
|
196
|
+
|
|
197
|
+
const resolveRecursive = (currentId: number): mat4 => {
|
|
198
|
+
if (visiting.has(currentId)) {
|
|
199
|
+
return getEntityLocalTransformMatrix(world, currentId);
|
|
200
|
+
}
|
|
201
|
+
visiting.add(currentId);
|
|
202
|
+
|
|
203
|
+
const local = getEntityLocalTransformMatrix(world, currentId);
|
|
204
|
+
const parentId = world.constraintParentByChild.get(currentId);
|
|
205
|
+
if (parentId === undefined) {
|
|
206
|
+
visiting.delete(currentId);
|
|
207
|
+
return local;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const parentWorld = resolveRecursive(parentId);
|
|
211
|
+
const out = mat4.create();
|
|
212
|
+
mat4.multiply(out, parentWorld, local);
|
|
213
|
+
visiting.delete(currentId);
|
|
214
|
+
return out;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
return resolveRecursive(entityId);
|
|
175
218
|
}
|
|
176
219
|
|
|
177
220
|
/** Compares 4x4 matrices using an absolute epsilon per component. */
|
|
@@ -4,6 +4,12 @@ import { enqueueCommand } from '../bridge/dispatch';
|
|
|
4
4
|
import type { System } from '../ecs';
|
|
5
5
|
import { toVec3, toVec4 } from './utils';
|
|
6
6
|
|
|
7
|
+
const WORLD_LIFECYCLE_INTENT_TYPES = [
|
|
8
|
+
'configure-environment',
|
|
9
|
+
'configure-shadows',
|
|
10
|
+
'send-notification',
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Applies world-scoped lifecycle/configuration intents.
|
|
9
15
|
*
|
|
@@ -13,10 +19,10 @@ import { toVec3, toVec4 } from './utils';
|
|
|
13
19
|
* - notification dispatch
|
|
14
20
|
*/
|
|
15
21
|
export const WorldLifecycleSystem: System = (world, context) => {
|
|
16
|
-
const
|
|
22
|
+
const intents = world.intentStore.takeMany(WORLD_LIFECYCLE_INTENT_TYPES);
|
|
17
23
|
|
|
18
|
-
for (let i = 0; i <
|
|
19
|
-
const intent =
|
|
24
|
+
for (let i = 0; i < intents.length; i++) {
|
|
25
|
+
const intent = intents[i];
|
|
20
26
|
if (!intent) continue;
|
|
21
27
|
|
|
22
28
|
if (intent.type === 'configure-environment') {
|
|
@@ -85,7 +91,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
85
91
|
environmentId: context.worldId,
|
|
86
92
|
});
|
|
87
93
|
}
|
|
88
|
-
intentsToRemove.push(i);
|
|
89
94
|
} else if (intent.type === 'configure-shadows') {
|
|
90
95
|
let windowId = world.primaryWindowId;
|
|
91
96
|
if (windowId === undefined) {
|
|
@@ -94,9 +99,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
94
99
|
break;
|
|
95
100
|
}
|
|
96
101
|
}
|
|
97
|
-
if (windowId === undefined) {
|
|
98
|
-
windowId = world.realmCreateArgs.hostWindowId;
|
|
99
|
-
}
|
|
100
102
|
if (windowId === undefined) continue;
|
|
101
103
|
const c = intent.config || {};
|
|
102
104
|
const config: ShadowConfig = {
|
|
@@ -112,7 +114,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
112
114
|
windowId,
|
|
113
115
|
config,
|
|
114
116
|
});
|
|
115
|
-
intentsToRemove.push(i);
|
|
116
117
|
} else if (intent.type === 'send-notification') {
|
|
117
118
|
enqueueCommand(context.worldId, 'cmd-notification-send', {
|
|
118
119
|
id: `notif-${Date.now()}`,
|
|
@@ -121,13 +122,6 @@ export const WorldLifecycleSystem: System = (world, context) => {
|
|
|
121
122
|
body: intent.message,
|
|
122
123
|
timeout: 5000,
|
|
123
124
|
});
|
|
124
|
-
intentsToRemove.push(i);
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
for (let i = intentsToRemove.length - 1; i >= 0; i--) {
|
|
129
|
-
const idx = intentsToRemove[i];
|
|
130
|
-
if (idx !== undefined) world.pendingIntents.splice(idx, 1);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
127
|
};
|