@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.
- package/README.md +106 -0
- package/package.json +60 -4
- package/src/core.ts +14 -0
- package/src/ecs.ts +1 -0
- package/src/engine/api.ts +222 -24
- package/src/engine/bridge/dispatch.ts +260 -40
- package/src/engine/bridge/guards.ts +4 -1
- package/src/engine/bridge/protocol.ts +69 -52
- package/src/engine/ecs/components.ts +340 -0
- package/src/engine/ecs/index.ts +3 -518
- 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 +136 -5
- package/src/engine/systems/command-intent.ts +159 -14
- package/src/engine/systems/constraint-solve.ts +167 -0
- package/src/engine/systems/core-command-builder.ts +9 -268
- package/src/engine/systems/diagnostics.ts +20 -29
- package/src/engine/systems/index.ts +3 -1
- package/src/engine/systems/input-mirror.ts +257 -21
- package/src/engine/systems/resource-upload.ts +108 -58
- package/src/engine/systems/response-decode.ts +86 -15
- package/src/engine/systems/scene-sync.ts +305 -0
- package/src/engine/systems/ui-bridge.ts +381 -0
- package/src/engine/systems/utils.ts +86 -1
- package/src/engine/systems/world-lifecycle.ts +43 -114
- package/src/engine/window/manager.ts +168 -0
- package/src/engine/world/entities.ts +998 -91
- package/src/engine/world/mount.ts +195 -0
- package/src/engine/world/types.ts +71 -0
- package/src/engine/world/world-ui.ts +313 -0
- package/src/engine/world/world3d.ts +529 -0
- package/src/helpers/collision.ts +487 -0
- package/src/helpers/index.ts +2 -0
- package/src/helpers/raycast.ts +442 -0
- package/src/index.ts +30 -1
- package/src/mount.ts +2 -0
- package/src/types/cmds/audio.ts +73 -48
- package/src/types/cmds/camera.ts +12 -8
- package/src/types/cmds/environment.ts +9 -3
- package/src/types/cmds/geometry.ts +15 -16
- package/src/types/cmds/index.ts +234 -162
- package/src/types/cmds/input.ts +39 -0
- package/src/types/cmds/light.ts +12 -11
- package/src/types/cmds/material.ts +19 -21
- package/src/types/cmds/model.ts +17 -15
- package/src/types/cmds/realm.ts +23 -0
- package/src/types/cmds/system.ts +29 -0
- package/src/types/cmds/target.ts +96 -0
- package/src/types/cmds/texture.ts +13 -3
- 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/keyboard.ts +2 -2
- package/src/types/events/pointer.ts +85 -13
- package/src/types/events/system.ts +188 -30
- package/src/types/events/ui.ts +21 -0
- package/src/types/index.ts +1 -0
- package/src/types/json.ts +15 -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
|
@@ -7,6 +7,60 @@ import { getWorldOrThrow, requireInitialized } from './guards';
|
|
|
7
7
|
* Limits for Backpressure management.
|
|
8
8
|
*/
|
|
9
9
|
const MAX_BATCH_COMMANDS = 2048;
|
|
10
|
+
const MAX_BATCH_ESTIMATED_BYTES = 60 * 1024;
|
|
11
|
+
|
|
12
|
+
function estimateSerializedBytes(value: unknown): number {
|
|
13
|
+
if (value === null || value === undefined) return 1;
|
|
14
|
+
const valueType = typeof value;
|
|
15
|
+
if (valueType === 'boolean') return 1;
|
|
16
|
+
if (valueType === 'number') return 8;
|
|
17
|
+
if (valueType === 'string') return 2 + (value as string).length * 2;
|
|
18
|
+
if (ArrayBuffer.isView(value)) return (value as ArrayBufferView).byteLength + 8;
|
|
19
|
+
if (value instanceof ArrayBuffer) return value.byteLength + 8;
|
|
20
|
+
|
|
21
|
+
if (Array.isArray(value)) {
|
|
22
|
+
let total = 8;
|
|
23
|
+
for (let i = 0; i < value.length; i++) {
|
|
24
|
+
total += estimateSerializedBytes(value[i]);
|
|
25
|
+
}
|
|
26
|
+
return total;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (valueType === 'object') {
|
|
30
|
+
const record = value as Record<string, unknown>;
|
|
31
|
+
let total = 8;
|
|
32
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
33
|
+
if (nested === undefined) continue;
|
|
34
|
+
total += 2 + key.length * 2;
|
|
35
|
+
total += estimateSerializedBytes(nested);
|
|
36
|
+
}
|
|
37
|
+
return total;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return 4;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function estimateEnvelopeBytes(envelope: { id: number; type: string; content: unknown }): number {
|
|
44
|
+
return 24 + envelope.type.length * 2 + estimateSerializedBytes(envelope.content);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function maybeCompactQueue<T>(queue: T[], head: number): number {
|
|
48
|
+
if (head <= 0) return head;
|
|
49
|
+
if (head === queue.length) {
|
|
50
|
+
queue.length = 0;
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
if (head >= 1024 && head * 2 >= queue.length) {
|
|
54
|
+
queue.copyWithin(0, head);
|
|
55
|
+
queue.length -= head;
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
return head;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function markRoutingIndexDirty(): void {
|
|
62
|
+
engineState.routingIndex.dirty = true;
|
|
63
|
+
}
|
|
10
64
|
|
|
11
65
|
/**
|
|
12
66
|
* Enqueues a command to be sent to the core in the next tick.
|
|
@@ -21,13 +75,45 @@ export function enqueueCommand<T extends EngineCmd['type']>(
|
|
|
21
75
|
const world = getWorldOrThrow(worldId);
|
|
22
76
|
const id = engineState.nextCommandId++;
|
|
23
77
|
|
|
24
|
-
// Contract:
|
|
25
|
-
const typedContent =
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
// Contract: realmId resolves from world core realm; windowId may default to primary binding.
|
|
79
|
+
const typedContent = {
|
|
80
|
+
...(content as Record<string, unknown>),
|
|
81
|
+
} as Record<string, unknown>;
|
|
82
|
+
if (
|
|
83
|
+
'windowId' in typedContent &&
|
|
84
|
+
typedContent.windowId == null &&
|
|
85
|
+
world.primaryWindowId !== undefined
|
|
86
|
+
) {
|
|
87
|
+
typedContent.windowId = world.primaryWindowId;
|
|
88
|
+
}
|
|
89
|
+
if ('realmId' in typedContent && typedContent.realmId == null) {
|
|
90
|
+
// Avoid host-side readiness guards: keep logical-id flow stable.
|
|
91
|
+
// Core validates realm existence and may apply internal fallbacks.
|
|
92
|
+
typedContent.realmId = world.coreRealmId ?? world.worldId;
|
|
28
93
|
}
|
|
29
94
|
|
|
30
|
-
world.pendingCommands.push({
|
|
95
|
+
world.pendingCommands.push({
|
|
96
|
+
id,
|
|
97
|
+
type,
|
|
98
|
+
content: typedContent as unknown as Extract<
|
|
99
|
+
EngineCmd,
|
|
100
|
+
{ type: T }
|
|
101
|
+
>['content'],
|
|
102
|
+
});
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Enqueues a command not scoped to a specific world (window management, etc).
|
|
108
|
+
*/
|
|
109
|
+
export function enqueueGlobalCommand<T extends EngineCmd['type']>(
|
|
110
|
+
type: T,
|
|
111
|
+
content: Extract<EngineCmd, { type: T }>['content'],
|
|
112
|
+
): number {
|
|
113
|
+
requireInitialized();
|
|
114
|
+
const id = engineState.nextCommandId++;
|
|
115
|
+
engineState.globalCommandTracker.add(id);
|
|
116
|
+
engineState.globalPendingCommands.push({ id, type, content });
|
|
31
117
|
return id;
|
|
32
118
|
}
|
|
33
119
|
|
|
@@ -37,63 +123,192 @@ export function enqueueCommand<T extends EngineCmd['type']>(
|
|
|
37
123
|
*/
|
|
38
124
|
export function collectCommands(): void {
|
|
39
125
|
let collectedCount = 0;
|
|
126
|
+
let collectedBytes = 0;
|
|
127
|
+
|
|
128
|
+
const globalAvailable =
|
|
129
|
+
engineState.globalPendingCommands.length - engineState.globalPendingCommandsHead;
|
|
130
|
+
if (globalAvailable > 0) {
|
|
131
|
+
const start = engineState.globalPendingCommandsHead;
|
|
132
|
+
let end = start;
|
|
133
|
+
while (
|
|
134
|
+
end < engineState.globalPendingCommands.length &&
|
|
135
|
+
collectedCount < MAX_BATCH_COMMANDS &&
|
|
136
|
+
collectedBytes < MAX_BATCH_ESTIMATED_BYTES
|
|
137
|
+
) {
|
|
138
|
+
const cmd = engineState.globalPendingCommands[end]!;
|
|
139
|
+
const estimateBytes = estimateEnvelopeBytes(cmd);
|
|
140
|
+
if (collectedBytes > 0 && collectedBytes + estimateBytes > MAX_BATCH_ESTIMATED_BYTES) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
engineState.commandBatch.push(cmd);
|
|
144
|
+
collectedBytes += estimateBytes;
|
|
145
|
+
collectedCount++;
|
|
146
|
+
end++;
|
|
147
|
+
}
|
|
148
|
+
engineState.globalPendingCommandsHead = end;
|
|
149
|
+
engineState.globalPendingCommandsHead = maybeCompactQueue(
|
|
150
|
+
engineState.globalPendingCommands,
|
|
151
|
+
engineState.globalPendingCommandsHead,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
40
154
|
|
|
41
155
|
for (const [worldId, world] of engineState.worlds) {
|
|
42
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
collectedCount >= MAX_BATCH_COMMANDS ||
|
|
158
|
+
collectedBytes >= MAX_BATCH_ESTIMATED_BYTES
|
|
159
|
+
) {
|
|
43
160
|
// Backpressure: If we reached the limit, stop collecting for this frame.
|
|
44
161
|
// Remaining commands will wait for the next frame.
|
|
45
162
|
break;
|
|
46
163
|
}
|
|
47
164
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
165
|
+
const worldAvailable = world.pendingCommands.length - world.pendingCommandsHead;
|
|
166
|
+
if (worldAvailable > 0) {
|
|
167
|
+
const start = world.pendingCommandsHead;
|
|
168
|
+
let end = start;
|
|
169
|
+
while (
|
|
170
|
+
end < world.pendingCommands.length &&
|
|
171
|
+
collectedCount < MAX_BATCH_COMMANDS &&
|
|
172
|
+
collectedBytes < MAX_BATCH_ESTIMATED_BYTES
|
|
173
|
+
) {
|
|
174
|
+
const cmd = world.pendingCommands[end]!;
|
|
175
|
+
const estimateBytes = estimateEnvelopeBytes(cmd);
|
|
176
|
+
if (collectedBytes > 0 && collectedBytes + estimateBytes > MAX_BATCH_ESTIMATED_BYTES) {
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
58
179
|
engineState.commandBatch.push(cmd);
|
|
59
180
|
engineState.commandTracker.set(cmd.id, worldId);
|
|
181
|
+
collectedBytes += estimateBytes;
|
|
182
|
+
collectedCount++;
|
|
183
|
+
end++;
|
|
60
184
|
}
|
|
61
|
-
|
|
185
|
+
world.pendingCommandsHead = end;
|
|
186
|
+
world.pendingCommandsHead = maybeCompactQueue(
|
|
187
|
+
world.pendingCommands,
|
|
188
|
+
world.pendingCommandsHead,
|
|
189
|
+
);
|
|
62
190
|
}
|
|
63
191
|
}
|
|
64
192
|
}
|
|
65
193
|
|
|
194
|
+
function ensureRoutingIndex(): void {
|
|
195
|
+
if (!engineState.routingIndex.dirty) return;
|
|
196
|
+
|
|
197
|
+
const byWindowId = new Map<number, number[]>();
|
|
198
|
+
const byRealmId = new Map<number, number[]>();
|
|
199
|
+
const byTargetId = new Map<number, number[]>();
|
|
200
|
+
|
|
201
|
+
for (const [worldId, world] of engineState.worlds) {
|
|
202
|
+
for (const boundWindowId of world.boundWindowIds) {
|
|
203
|
+
const ids = byWindowId.get(boundWindowId);
|
|
204
|
+
if (ids) ids.push(worldId);
|
|
205
|
+
else byWindowId.set(boundWindowId, [worldId]);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (typeof world.coreRealmId === 'number') {
|
|
209
|
+
const ids = byRealmId.get(world.coreRealmId);
|
|
210
|
+
if (ids) ids.push(worldId);
|
|
211
|
+
else byRealmId.set(world.coreRealmId, [worldId]);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
for (const targetId of world.targetLayerBindings.keys()) {
|
|
215
|
+
const ids = byTargetId.get(targetId);
|
|
216
|
+
if (ids) ids.push(worldId);
|
|
217
|
+
else byTargetId.set(targetId, [worldId]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
engineState.routingIndex.byWindowId = byWindowId;
|
|
222
|
+
engineState.routingIndex.byRealmId = byRealmId;
|
|
223
|
+
engineState.routingIndex.byTargetId = byTargetId;
|
|
224
|
+
engineState.routingIndex.dirty = false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function extractEventScopeIds(event: EngineEvent): {
|
|
228
|
+
windowId?: number;
|
|
229
|
+
realmId?: number;
|
|
230
|
+
targetId?: number;
|
|
231
|
+
} {
|
|
232
|
+
const content = event.content as
|
|
233
|
+
| { data?: Record<string, unknown> }
|
|
234
|
+
| Record<string, unknown>;
|
|
235
|
+
const scopedDataCandidate =
|
|
236
|
+
'data' in content && content.data && typeof content.data === 'object'
|
|
237
|
+
? content.data
|
|
238
|
+
: content;
|
|
239
|
+
|
|
240
|
+
if (!scopedDataCandidate || typeof scopedDataCandidate !== 'object') {
|
|
241
|
+
return {};
|
|
242
|
+
}
|
|
243
|
+
const scopedData = scopedDataCandidate as Record<string, unknown>;
|
|
244
|
+
|
|
245
|
+
const windowId =
|
|
246
|
+
typeof scopedData.windowId === 'number' ? scopedData.windowId : undefined;
|
|
247
|
+
const realmId =
|
|
248
|
+
typeof scopedData.realmId === 'number' ? scopedData.realmId : undefined;
|
|
249
|
+
const targetId =
|
|
250
|
+
typeof scopedData.targetId === 'number' ? scopedData.targetId : undefined;
|
|
251
|
+
|
|
252
|
+
return { windowId, realmId, targetId };
|
|
253
|
+
}
|
|
254
|
+
|
|
66
255
|
/**
|
|
67
256
|
* Routes core events to their respective worlds based on windowId.
|
|
68
257
|
*/
|
|
69
258
|
export function routeEvents(events: EngineEvent[]): void {
|
|
259
|
+
ensureRoutingIndex();
|
|
260
|
+
const worldsByWindowId = engineState.routingIndex.byWindowId;
|
|
261
|
+
const worldsByRealmId = engineState.routingIndex.byRealmId;
|
|
262
|
+
const worldsByTargetId = engineState.routingIndex.byTargetId;
|
|
263
|
+
|
|
70
264
|
for (const event of events) {
|
|
71
|
-
|
|
72
|
-
// Events have structure: { type, content: { event, data: { windowId, ... } } }
|
|
73
|
-
let windowId: number | undefined;
|
|
74
|
-
|
|
75
|
-
if ('data' in event.content && event.content.data) {
|
|
76
|
-
const eventData = event.content.data as unknown as Record<
|
|
77
|
-
string,
|
|
78
|
-
unknown
|
|
79
|
-
>;
|
|
80
|
-
windowId =
|
|
81
|
-
typeof eventData.windowId === 'number' ? eventData.windowId : undefined;
|
|
82
|
-
} else if ('windowId' in event.content) {
|
|
83
|
-
const eventContent = event.content as Record<string, unknown>;
|
|
84
|
-
windowId =
|
|
85
|
-
typeof eventContent.windowId === 'number'
|
|
86
|
-
? eventContent.windowId
|
|
87
|
-
: undefined;
|
|
88
|
-
}
|
|
265
|
+
const { windowId, realmId, targetId } = extractEventScopeIds(event);
|
|
89
266
|
|
|
90
267
|
if (windowId !== undefined) {
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
93
|
-
|
|
268
|
+
const worldIds = worldsByWindowId.get(windowId);
|
|
269
|
+
if (!worldIds) {
|
|
270
|
+
continue;
|
|
94
271
|
}
|
|
95
|
-
|
|
96
|
-
|
|
272
|
+
for (let i = 0; i < worldIds.length; i++) {
|
|
273
|
+
const world = engineState.worlds.get(worldIds[i]!);
|
|
274
|
+
if (world) {
|
|
275
|
+
world.inboundEvents.push(event);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (realmId !== undefined) {
|
|
282
|
+
const worldIds = worldsByRealmId.get(realmId);
|
|
283
|
+
if (!worldIds) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
for (let i = 0; i < worldIds.length; i++) {
|
|
287
|
+
const world = engineState.worlds.get(worldIds[i]!);
|
|
288
|
+
if (world) {
|
|
289
|
+
world.inboundEvents.push(event);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (targetId !== undefined) {
|
|
296
|
+
const worldIds = worldsByTargetId.get(targetId);
|
|
297
|
+
if (!worldIds) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
for (let i = 0; i < worldIds.length; i++) {
|
|
301
|
+
const world = engineState.worlds.get(worldIds[i]!);
|
|
302
|
+
if (world) {
|
|
303
|
+
world.inboundEvents.push(event);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Broadcast non-scoped events (system/audio/ui-async notifications).
|
|
310
|
+
for (const world of engineState.worlds.values()) {
|
|
311
|
+
world.inboundEvents.push(event);
|
|
97
312
|
}
|
|
98
313
|
}
|
|
99
314
|
}
|
|
@@ -110,6 +325,11 @@ export function routeResponses(responses: CommandResponseEnvelope[]): void {
|
|
|
110
325
|
world.inboundResponses.push(res);
|
|
111
326
|
}
|
|
112
327
|
engineState.commandTracker.delete(res.id);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (engineState.globalCommandTracker.has(res.id)) {
|
|
331
|
+
engineState.globalInboundResponses.push(res);
|
|
332
|
+
engineState.globalCommandTracker.delete(res.id);
|
|
113
333
|
}
|
|
114
334
|
}
|
|
115
335
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EngineError } from '../errors';
|
|
2
2
|
import { engineState, type WorldState } from '../state';
|
|
3
3
|
|
|
4
|
+
/** Ensures engine runtime is initialized and not disposed. */
|
|
4
5
|
export function requireInitialized(): void {
|
|
5
6
|
if (engineState.status === 'disposed') {
|
|
6
7
|
throw new EngineError('Disposed', 'Engine has been disposed.');
|
|
@@ -10,6 +11,7 @@ export function requireInitialized(): void {
|
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
13
|
|
|
14
|
+
/** Resolves world state or throws when world id is unknown. */
|
|
13
15
|
export function getWorldOrThrow(worldId: number): WorldState {
|
|
14
16
|
const world = engineState.worlds.get(worldId);
|
|
15
17
|
if (!world) {
|
|
@@ -18,11 +20,12 @@ export function getWorldOrThrow(worldId: number): WorldState {
|
|
|
18
20
|
return world;
|
|
19
21
|
}
|
|
20
22
|
|
|
23
|
+
/** Ensures entity exists in world entity set. */
|
|
21
24
|
export function ensureEntity(world: WorldState, entityId: number): void {
|
|
22
25
|
if (!world.entities.has(entityId)) {
|
|
23
26
|
throw new EngineError(
|
|
24
27
|
'EntityNotFound',
|
|
25
|
-
`Entity ${entityId} not found in World ${world.
|
|
28
|
+
`Entity ${entityId} not found in World ${world.worldId}.`,
|
|
26
29
|
);
|
|
27
30
|
}
|
|
28
31
|
}
|
|
@@ -5,7 +5,6 @@ import type {
|
|
|
5
5
|
EngineCmdEnvelope,
|
|
6
6
|
} from '../../types/cmds';
|
|
7
7
|
import type { EngineEvent } from '../../types/events';
|
|
8
|
-
import { engineState } from '../state';
|
|
9
8
|
|
|
10
9
|
/**
|
|
11
10
|
* Vulfram Core/ABI Invariants:
|
|
@@ -46,6 +45,73 @@ export type CoreCommandBatch = EngineCmdEnvelope[];
|
|
|
46
45
|
const packr = new Packr({ useRecords: false });
|
|
47
46
|
const unpackr = new Unpackr({ useRecords: false });
|
|
48
47
|
|
|
48
|
+
function normalizeForMsgpack(value: unknown): unknown {
|
|
49
|
+
if (value === undefined || value === null) return value;
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
let out: unknown[] | undefined;
|
|
52
|
+
for (let i = 0; i < value.length; i++) {
|
|
53
|
+
const item = value[i];
|
|
54
|
+
const normalizedItem = normalizeForMsgpack(item);
|
|
55
|
+
const normalized = normalizedItem === undefined ? null : normalizedItem;
|
|
56
|
+
if (!out) {
|
|
57
|
+
if (normalized !== item) {
|
|
58
|
+
out = value.slice(0, i);
|
|
59
|
+
out.push(normalized);
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
out.push(normalized);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out ?? value;
|
|
66
|
+
}
|
|
67
|
+
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === 'object') {
|
|
71
|
+
const input = value as Record<string, unknown>;
|
|
72
|
+
const entries = Object.entries(input);
|
|
73
|
+
let out: Record<string, unknown> | undefined;
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < entries.length; i++) {
|
|
76
|
+
const [key, item] = entries[i]!;
|
|
77
|
+
if (item === undefined) continue;
|
|
78
|
+
const normalized = normalizeForMsgpack(item);
|
|
79
|
+
if (normalized === undefined) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!out) {
|
|
83
|
+
if (normalized !== item) {
|
|
84
|
+
out = {};
|
|
85
|
+
for (let j = 0; j < i; j++) {
|
|
86
|
+
const [prevKey, prevItem] = entries[j]!;
|
|
87
|
+
if (prevItem !== undefined) {
|
|
88
|
+
out[prevKey] = prevItem;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out[key] = normalized;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
out[key] = normalized;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!out) {
|
|
98
|
+
for (const [, item] of entries) {
|
|
99
|
+
if (item === undefined) {
|
|
100
|
+
out = {};
|
|
101
|
+
for (const [prevKey, prevItem] of entries) {
|
|
102
|
+
if (prevItem !== undefined) {
|
|
103
|
+
out[prevKey] = prevItem;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return out ?? value;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
49
115
|
/**
|
|
50
116
|
* Minimal Command Payload Requirement:
|
|
51
117
|
* Most scene/window commands include a `windowId` so the Core can route them
|
|
@@ -60,57 +126,8 @@ export interface BaseCommandArgs {
|
|
|
60
126
|
* Serializes a batch of commands into a byte buffer ready for vulframSendQueue.
|
|
61
127
|
*/
|
|
62
128
|
export function serializeBatch(batch: CoreCommandBatch): TransportBuffer {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const hasModel =
|
|
66
|
-
types.includes('cmd-model-create') || types.includes('cmd-model-update');
|
|
67
|
-
const hasResource = types.some(
|
|
68
|
-
(type) =>
|
|
69
|
-
type === 'cmd-primitive-geometry-create' ||
|
|
70
|
-
type === 'cmd-geometry-create' ||
|
|
71
|
-
type === 'cmd-material-create' ||
|
|
72
|
-
type === 'cmd-texture-create-from-buffer' ||
|
|
73
|
-
type === 'cmd-texture-create-solid-color',
|
|
74
|
-
);
|
|
75
|
-
if (hasModel || hasResource) {
|
|
76
|
-
console.debug('[Debug] BatchTypes', types);
|
|
77
|
-
const modelCreate = batch.find((cmd) => cmd.type === 'cmd-model-create');
|
|
78
|
-
if (modelCreate) {
|
|
79
|
-
console.debug(
|
|
80
|
-
'[Debug] ModelCreatePayload',
|
|
81
|
-
JSON.stringify(modelCreate.content),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
const primitiveGeo = batch.find(
|
|
85
|
-
(cmd) => cmd.type === 'cmd-primitive-geometry-create',
|
|
86
|
-
);
|
|
87
|
-
if (primitiveGeo) {
|
|
88
|
-
console.debug(
|
|
89
|
-
'[Debug] GeometryPayload',
|
|
90
|
-
JSON.stringify(primitiveGeo.content),
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
const materialCreate = batch.find(
|
|
94
|
-
(cmd) => cmd.type === 'cmd-material-create',
|
|
95
|
-
);
|
|
96
|
-
if (materialCreate) {
|
|
97
|
-
console.debug(
|
|
98
|
-
'[Debug] MaterialPayload',
|
|
99
|
-
JSON.stringify(materialCreate.content),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
const textureCreate = batch.find(
|
|
103
|
-
(cmd) => cmd.type === 'cmd-texture-create-from-buffer',
|
|
104
|
-
);
|
|
105
|
-
if (textureCreate) {
|
|
106
|
-
console.debug(
|
|
107
|
-
'[Debug] TexturePayload',
|
|
108
|
-
JSON.stringify(textureCreate.content),
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return packr.pack(batch);
|
|
129
|
+
const normalizedBatch = normalizeForMsgpack(batch) as CoreCommandBatch;
|
|
130
|
+
return packr.pack(normalizedBatch);
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
/**
|