@forgeax/engine-runtime 0.1.28 → 0.1.29
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 +98 -0
- package/dist/collect-scene-asset.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +480 -15
- package/dist/index.mjs.map +1 -1
- package/dist/scene-utils/mount-override-fold.d.ts +3 -1
- package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
- package/package.json +37 -32
- package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
- package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
- package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
- package/src/__tests__/asset-registry-mounts.test.ts +174 -734
- package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
- package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
- package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
- package/src/__tests__/collect-scene-asset.test.ts +96 -113
- package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
- package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
- package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
- package/src/__tests__/collect-transient-skip.test.ts +20 -27
- package/src/__tests__/components.test-d.ts +17 -1
- package/src/__tests__/components.unit.test.ts +75 -27
- package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
- package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
- package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
- package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
- package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
- package/src/__tests__/errors.unit.test.ts +13 -0
- package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
- package/src/__tests__/feature-depth-material.fixture.ts +473 -0
- package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
- package/src/__tests__/geometry.unit.test.ts +42 -43
- package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
- package/src/__tests__/handle-migration.test.ts +0 -1
- package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
- package/src/__tests__/instances.test-d.ts +13 -52
- package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
- package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
- package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
- package/src/__tests__/material-publication.browser.test.ts +29 -0
- package/src/__tests__/material-publication.commands.ts +23 -0
- package/src/__tests__/material-publication.dawn.test.ts +24 -0
- package/src/__tests__/material-publication.fixture.ts +349 -0
- package/src/__tests__/material-publication.server.ts +109 -0
- package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
- package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
- package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
- package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
- package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
- package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
- package/src/__tests__/record-worldid-isolation.test.ts +47 -28
- package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
- package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
- package/src/__tests__/render-system-extract.test.ts +61 -0
- package/src/__tests__/render-system-extract.unit.test.ts +21 -7
- package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
- package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
- package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
- package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
- package/src/__tests__/render-system-mega.test.ts +50 -62
- package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
- package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
- package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
- package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
- package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
- package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
- package/src/__tests__/roundtrip-primitive.test.ts +24 -23
- package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
- package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
- package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
- package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
- package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
- package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
- package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
- package/src/__tests__/serialize-scene-asset.test.ts +36 -50
- package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
- package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
- package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
- package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
- package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
- package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
- package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
- package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
- package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
- package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
- package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
- package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
- package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
- package/src/__tests__/transient-fields-collect.test.ts +4 -4
- package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
- package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
- package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
- package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
- package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
- package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
- package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
- package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
- package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
- package/src/__tests__/world-scene-despawn.test.ts +8 -8
- package/src/__tests__/world-scene-instantiate.test.ts +23 -29
- package/src/__tests__/world-scene-nested.test.ts +90 -163
- package/src/__tests__/world-scene-override.test.ts +17 -16
- package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
- package/src/collect-scene-asset.ts +637 -19
- package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
- package/src/index.ts +1 -0
- package/src/scene-utils/mount-override-fold.ts +5 -14
- package/src/__tests__/node-shims.d.ts +0 -31
|
@@ -60,6 +60,7 @@ import { classifyEntityField } from '@forgeax/engine-ecs/externalization';
|
|
|
60
60
|
import { componentSchema } from '@forgeax/engine-ecs/internal';
|
|
61
61
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
62
62
|
import { err, ok, type Result } from '@forgeax/engine-rhi';
|
|
63
|
+
import type { MountOverride, SceneInstanceMount } from '@forgeax/engine-scene';
|
|
63
64
|
import {
|
|
64
65
|
collectSubtree,
|
|
65
66
|
externalizeSceneAsset,
|
|
@@ -67,15 +68,7 @@ import {
|
|
|
67
68
|
worldGetSceneAssetForInstance,
|
|
68
69
|
worldGetSceneInstanceState,
|
|
69
70
|
} from '@forgeax/engine-scene';
|
|
70
|
-
import type {
|
|
71
|
-
Asset,
|
|
72
|
-
Handle,
|
|
73
|
-
LocalEntityId,
|
|
74
|
-
MountOverride,
|
|
75
|
-
SceneAsset,
|
|
76
|
-
SceneEntity,
|
|
77
|
-
SceneInstanceMount,
|
|
78
|
-
} from '@forgeax/engine-types';
|
|
71
|
+
import type { Asset, Handle, LocalEntityId, SceneAsset } from '@forgeax/engine-types';
|
|
79
72
|
import { foldMountOverrides } from './scene-utils/mount-override-fold';
|
|
80
73
|
|
|
81
74
|
/** The collector needs identity lookup, not an authoring registry implementation. */
|
|
@@ -213,6 +206,562 @@ function _serializeOverrideValueHandles(
|
|
|
213
206
|
return ok(out);
|
|
214
207
|
}
|
|
215
208
|
|
|
209
|
+
type LegacyCollectedEntity = {
|
|
210
|
+
readonly localId: number;
|
|
211
|
+
readonly components: Record<string, Record<string, unknown>>;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
type SceneEntityAddress = string | readonly [string, ...string[]];
|
|
215
|
+
type SceneInstanceOverride = {
|
|
216
|
+
readonly target: readonly [string, ...string[]];
|
|
217
|
+
readonly components: Record<string, Record<string, unknown>>;
|
|
218
|
+
};
|
|
219
|
+
type CollectorSceneState = {
|
|
220
|
+
readonly keyByLocalId: Map<number, string>;
|
|
221
|
+
readonly instanceKey?: string;
|
|
222
|
+
readonly bindings: Map<string, EntityHandle>;
|
|
223
|
+
readonly entityToLocalId: Map<EntityHandle, LocalEntityId>;
|
|
224
|
+
readonly overrides: Map<
|
|
225
|
+
LocalEntityId,
|
|
226
|
+
Map<string, { readonly comp: string; readonly field?: string; readonly value: unknown }>
|
|
227
|
+
>;
|
|
228
|
+
readonly mountTimeOverrides: readonly MountOverride[];
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
type CollectedKeyedEntity = {
|
|
232
|
+
readonly components: Record<string, Record<string, unknown>>;
|
|
233
|
+
readonly instance?: {
|
|
234
|
+
readonly source: string;
|
|
235
|
+
readonly overrides?: readonly SceneInstanceOverride[];
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
type SceneStateInfo = {
|
|
240
|
+
readonly root: number;
|
|
241
|
+
readonly state: CollectorSceneState;
|
|
242
|
+
readonly mapping: Uint32Array;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
type SceneStateRelation = {
|
|
246
|
+
readonly parent: SceneStateInfo;
|
|
247
|
+
readonly child: SceneStateInfo;
|
|
248
|
+
readonly key: string;
|
|
249
|
+
readonly memberFirst: number;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
type KeyedCollectError =
|
|
253
|
+
| SceneCollectEntityRefOutOfClosureError
|
|
254
|
+
| SceneCollectAssetGuidUnresolvedError;
|
|
255
|
+
|
|
256
|
+
function isSceneEntityAddress(value: string | readonly string[]): value is SceneEntityAddress {
|
|
257
|
+
return typeof value === 'string' || value.length > 0;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Convert the old private numeric collector result to the keyed author model.
|
|
262
|
+
* Numeric slots remain an implementation detail of the live SceneInstance;
|
|
263
|
+
* this function uses the retained instance key maps and child mapping windows
|
|
264
|
+
* to reconstruct addresses before the public SceneAsset leaves Engine.
|
|
265
|
+
*/
|
|
266
|
+
function collectKeyedSceneAsset(
|
|
267
|
+
world: World,
|
|
268
|
+
registry: SceneAssetGuidLookup,
|
|
269
|
+
_roots: readonly EntityHandle[],
|
|
270
|
+
visited: ReadonlySet<number>,
|
|
271
|
+
ownedEntities: readonly number[],
|
|
272
|
+
legacyEntities: readonly LegacyCollectedEntity[],
|
|
273
|
+
mounts: readonly SceneInstanceMount[],
|
|
274
|
+
anchors: ReadonlyArray<{ entityRaw: number; sourceGuid: string; totalSlots: number }>,
|
|
275
|
+
memberOrigin: ReadonlyMap<number, { anchorRaw: number; memberLocalId: number }>,
|
|
276
|
+
): Result<SceneAsset, KeyedCollectError> {
|
|
277
|
+
const sceneInstanceToken = world.components.resolve('SceneInstance');
|
|
278
|
+
const childOfToken = world.components.resolve('ChildOf');
|
|
279
|
+
const stateInfos: SceneStateInfo[] = [];
|
|
280
|
+
if (sceneInstanceToken !== undefined) {
|
|
281
|
+
const queryResult = world.query({ read: [SceneInstance] });
|
|
282
|
+
if (queryResult.ok) {
|
|
283
|
+
for (const row of queryResult.value) {
|
|
284
|
+
const stateResult = worldGetSceneInstanceState(world, row.entity);
|
|
285
|
+
if (!stateResult.ok) continue;
|
|
286
|
+
const rowValue = row.get(SceneInstance) as unknown as { mapping: ArrayLike<number> };
|
|
287
|
+
stateInfos.push({
|
|
288
|
+
root: row.entity as number,
|
|
289
|
+
state: stateResult.value as unknown as CollectorSceneState,
|
|
290
|
+
mapping: Uint32Array.from(rowValue.mapping as ArrayLike<number>),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
const stateByRoot = new Map<number, SceneStateInfo>();
|
|
296
|
+
for (const info of stateInfos) stateByRoot.set(info.root, info);
|
|
297
|
+
|
|
298
|
+
const parentByChildRoot = new Map<number, SceneStateInfo>();
|
|
299
|
+
const relationCandidates: Array<{ child: SceneStateInfo; parent: SceneStateInfo; key: string }> =
|
|
300
|
+
[];
|
|
301
|
+
for (const child of stateInfos) {
|
|
302
|
+
if (child.state.instanceKey === undefined || childOfToken === undefined) continue;
|
|
303
|
+
const parentResult = world.get(
|
|
304
|
+
child.root as EntityHandle,
|
|
305
|
+
childOfToken as EcsComponent<string>,
|
|
306
|
+
);
|
|
307
|
+
if (!parentResult.ok) continue;
|
|
308
|
+
const carrier = (parentResult.value as Record<string, unknown>).parent as number | undefined;
|
|
309
|
+
if (carrier === undefined) continue;
|
|
310
|
+
for (const parent of stateInfos) {
|
|
311
|
+
const bound = parent.state.bindings.get(child.state.instanceKey);
|
|
312
|
+
if ((bound as number | undefined) === carrier) {
|
|
313
|
+
parentByChildRoot.set(child.root, parent);
|
|
314
|
+
relationCandidates.push({ child, parent, key: child.state.instanceKey });
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const findSequence = (
|
|
321
|
+
haystack: Uint32Array,
|
|
322
|
+
needle: Uint32Array,
|
|
323
|
+
start: number,
|
|
324
|
+
): number | undefined => {
|
|
325
|
+
if (needle.length === 0) return start;
|
|
326
|
+
for (let index = Math.max(0, start); index + needle.length <= haystack.length; index += 1) {
|
|
327
|
+
let match = true;
|
|
328
|
+
for (let offset = 0; offset < needle.length; offset += 1) {
|
|
329
|
+
if (haystack[index + offset] !== needle[offset]) {
|
|
330
|
+
match = false;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (match) return index;
|
|
335
|
+
}
|
|
336
|
+
return undefined;
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const relations: SceneStateRelation[] = [];
|
|
340
|
+
for (const candidate of relationCandidates) {
|
|
341
|
+
const slot = [...candidate.parent.state.keyByLocalId.entries()].find(
|
|
342
|
+
([, value]) => value === candidate.key,
|
|
343
|
+
)?.[0];
|
|
344
|
+
if (slot === undefined) continue;
|
|
345
|
+
const memberFirst = findSequence(candidate.parent.mapping, candidate.child.mapping, slot + 1);
|
|
346
|
+
if (memberFirst === undefined) continue;
|
|
347
|
+
relations.push({ ...candidate, memberFirst });
|
|
348
|
+
}
|
|
349
|
+
const relationsByParent = new Map<number, SceneStateRelation[]>();
|
|
350
|
+
for (const relation of relations) {
|
|
351
|
+
const list = relationsByParent.get(relation.parent.root);
|
|
352
|
+
if (list === undefined) relationsByParent.set(relation.parent.root, [relation]);
|
|
353
|
+
else list.push(relation);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const statePrefixMemo = new Map<number, readonly string[]>();
|
|
357
|
+
const statePrefix = (info: SceneStateInfo, stack = new Set<number>()): readonly string[] => {
|
|
358
|
+
const prior = statePrefixMemo.get(info.root);
|
|
359
|
+
if (prior !== undefined) return prior;
|
|
360
|
+
if (stack.has(info.root)) return [];
|
|
361
|
+
if (info.state.instanceKey === undefined) {
|
|
362
|
+
statePrefixMemo.set(info.root, []);
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
const next = new Set(stack);
|
|
366
|
+
next.add(info.root);
|
|
367
|
+
const parent = parentByChildRoot.get(info.root);
|
|
368
|
+
const prefix =
|
|
369
|
+
parent === undefined
|
|
370
|
+
? [info.state.instanceKey]
|
|
371
|
+
: [...statePrefix(parent, next), info.state.instanceKey];
|
|
372
|
+
statePrefixMemo.set(info.root, prefix);
|
|
373
|
+
return prefix;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const stateSlotAddress = (
|
|
377
|
+
info: SceneStateInfo,
|
|
378
|
+
slot: number,
|
|
379
|
+
prefix: readonly string[],
|
|
380
|
+
stack = new Set<number>(),
|
|
381
|
+
): readonly string[] | undefined => {
|
|
382
|
+
if (slot < 0 || stack.has(info.root)) return undefined;
|
|
383
|
+
const key = info.state.keyByLocalId.get(slot);
|
|
384
|
+
if (key !== undefined) return [...prefix, key];
|
|
385
|
+
const next = new Set(stack);
|
|
386
|
+
next.add(info.root);
|
|
387
|
+
for (const relation of relationsByParent.get(info.root) ?? []) {
|
|
388
|
+
const end = relation.memberFirst + relation.child.mapping.length;
|
|
389
|
+
if (slot >= relation.memberFirst && slot < end) {
|
|
390
|
+
return stateSlotAddress(
|
|
391
|
+
relation.child,
|
|
392
|
+
slot - relation.memberFirst,
|
|
393
|
+
[...prefix, relation.key],
|
|
394
|
+
next,
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const rawToAddress = new Map<number, readonly string[]>();
|
|
402
|
+
const orderedStates = [...stateInfos].sort(
|
|
403
|
+
(a, b) => statePrefix(a).length - statePrefix(b).length,
|
|
404
|
+
);
|
|
405
|
+
for (const info of orderedStates) {
|
|
406
|
+
const prefix = statePrefix(info);
|
|
407
|
+
for (let slot = 0; slot < info.mapping.length; slot += 1) {
|
|
408
|
+
const raw = info.mapping[slot];
|
|
409
|
+
if (raw === undefined || raw === 0xffffffff || !visited.has(raw)) continue;
|
|
410
|
+
const address = stateSlotAddress(info, slot, prefix);
|
|
411
|
+
if (address !== undefined && !rawToAddress.has(raw)) rawToAddress.set(raw, address);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
for (const [raw, origin] of memberOrigin) {
|
|
415
|
+
if (!visited.has(raw) || rawToAddress.has(raw)) continue;
|
|
416
|
+
const child = stateByRoot.get(origin.anchorRaw);
|
|
417
|
+
if (child === undefined) continue;
|
|
418
|
+
const address = stateSlotAddress(child, origin.memberLocalId, statePrefix(child));
|
|
419
|
+
if (address !== undefined) rawToAddress.set(raw, address);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const generatedKeys = new Set<string>();
|
|
423
|
+
const keyForOwnedRaw = new Map<number, string>();
|
|
424
|
+
for (let index = 0; index < ownedEntities.length; index += 1) {
|
|
425
|
+
const raw = ownedEntities[index] as number;
|
|
426
|
+
const known = rawToAddress.get(raw);
|
|
427
|
+
let key = known?.length === 1 ? known[0] : undefined;
|
|
428
|
+
if (key === undefined || key.length === 0 || generatedKeys.has(key)) {
|
|
429
|
+
const base = `entity-${index}`;
|
|
430
|
+
key = base;
|
|
431
|
+
let suffix = 1;
|
|
432
|
+
while (generatedKeys.has(key)) key = `${base}-${suffix++}`;
|
|
433
|
+
}
|
|
434
|
+
generatedKeys.add(key);
|
|
435
|
+
keyForOwnedRaw.set(raw, key);
|
|
436
|
+
rawToAddress.set(raw, [key]);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const slotAddress = (slot: number): SceneEntityAddress | undefined => {
|
|
440
|
+
if (slot >= 0 && slot < ownedEntities.length) {
|
|
441
|
+
const raw = ownedEntities[slot];
|
|
442
|
+
const key = raw === undefined ? undefined : keyForOwnedRaw.get(raw);
|
|
443
|
+
return key;
|
|
444
|
+
}
|
|
445
|
+
const mountIndex = mounts.findIndex((mount) => (mount.localId as unknown as number) === slot);
|
|
446
|
+
if (mountIndex >= 0) {
|
|
447
|
+
const anchor = anchors[mountIndex];
|
|
448
|
+
const child = anchor === undefined ? undefined : stateByRoot.get(anchor.entityRaw);
|
|
449
|
+
const key = child?.state.instanceKey;
|
|
450
|
+
if (key !== undefined) return key;
|
|
451
|
+
}
|
|
452
|
+
for (let index = 0; index < mounts.length; index += 1) {
|
|
453
|
+
const mount = mounts[index];
|
|
454
|
+
if (mount === undefined) continue;
|
|
455
|
+
const first = mount.memberFirst as unknown as number;
|
|
456
|
+
if (slot < first || slot >= first + mount.memberCount) continue;
|
|
457
|
+
const anchor = anchors[index];
|
|
458
|
+
const child = anchor === undefined ? undefined : stateByRoot.get(anchor.entityRaw);
|
|
459
|
+
const mountKey = child?.state.instanceKey;
|
|
460
|
+
if (child === undefined || mountKey === undefined) return undefined;
|
|
461
|
+
const address = stateSlotAddress(child, slot - first, []);
|
|
462
|
+
if (address === undefined) return undefined;
|
|
463
|
+
return [mountKey, ...address] as readonly [string, ...string[]];
|
|
464
|
+
}
|
|
465
|
+
const top = orderedStates.find((info) => statePrefix(info).length === 0);
|
|
466
|
+
if (top !== undefined) {
|
|
467
|
+
const address = stateSlotAddress(top, slot, []);
|
|
468
|
+
if (address !== undefined) {
|
|
469
|
+
return address.length === 1 ? address[0] : (address as readonly [string, ...string[]]);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return undefined;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const convertComponents = (
|
|
476
|
+
entityRaw: number,
|
|
477
|
+
source: Record<string, Record<string, unknown>>,
|
|
478
|
+
): Result<Record<string, Record<string, unknown>>, KeyedCollectError> => {
|
|
479
|
+
const out: Record<string, Record<string, unknown>> = {};
|
|
480
|
+
for (const [componentName, rawFields] of Object.entries(source)) {
|
|
481
|
+
const token = world.components.resolve(componentName);
|
|
482
|
+
if (token === undefined) {
|
|
483
|
+
out[componentName] = { ...rawFields };
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const converted: Record<string, unknown> = {};
|
|
487
|
+
for (const [fieldName, value] of Object.entries(rawFields)) {
|
|
488
|
+
const kind = classifyEntityField(token as EcsComponent, fieldName);
|
|
489
|
+
if (kind === null) {
|
|
490
|
+
converted[fieldName] = value;
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
const convertSlot = (localSlot: unknown): SceneEntityAddress | undefined => {
|
|
494
|
+
if (typeof localSlot !== 'number') return undefined;
|
|
495
|
+
return slotAddress(localSlot);
|
|
496
|
+
};
|
|
497
|
+
if (kind.isArray) {
|
|
498
|
+
if (!Array.isArray(value)) {
|
|
499
|
+
return err(
|
|
500
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
501
|
+
entityRaw,
|
|
502
|
+
`${componentName}.${fieldName}`,
|
|
503
|
+
-1,
|
|
504
|
+
),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const addresses: SceneEntityAddress[] = [];
|
|
508
|
+
for (const item of value) {
|
|
509
|
+
const address = convertSlot(item);
|
|
510
|
+
if (address === undefined || !isSceneEntityAddress(address)) {
|
|
511
|
+
return err(
|
|
512
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
513
|
+
entityRaw,
|
|
514
|
+
`${componentName}.${fieldName}`,
|
|
515
|
+
Number(item),
|
|
516
|
+
),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
addresses.push(address);
|
|
520
|
+
}
|
|
521
|
+
converted[fieldName] = addresses;
|
|
522
|
+
} else if (value === null) {
|
|
523
|
+
converted[fieldName] = null;
|
|
524
|
+
} else {
|
|
525
|
+
const address = convertSlot(value);
|
|
526
|
+
if (address === undefined || !isSceneEntityAddress(address)) {
|
|
527
|
+
return err(
|
|
528
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
529
|
+
entityRaw,
|
|
530
|
+
`${componentName}.${fieldName}`,
|
|
531
|
+
Number(value),
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
converted[fieldName] = address;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
out[componentName] = converted;
|
|
539
|
+
}
|
|
540
|
+
return ok(out);
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const entities: Record<string, CollectedKeyedEntity> = {};
|
|
544
|
+
for (const legacy of legacyEntities) {
|
|
545
|
+
const raw = ownedEntities[legacy.localId];
|
|
546
|
+
if (raw === undefined) continue;
|
|
547
|
+
const key = keyForOwnedRaw.get(raw) as string;
|
|
548
|
+
const converted = convertComponents(raw, legacy.components);
|
|
549
|
+
if (!converted.ok) return converted;
|
|
550
|
+
entities[key] = { components: converted.value };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const resolveOverrideTarget = (
|
|
554
|
+
child: SceneStateInfo | undefined,
|
|
555
|
+
memberFirst: number,
|
|
556
|
+
localId: number,
|
|
557
|
+
): readonly [string, ...string[]] | undefined => {
|
|
558
|
+
if (child === undefined) return undefined;
|
|
559
|
+
const target = stateSlotAddress(child, localId - memberFirst, []);
|
|
560
|
+
if (target === undefined || target.length === 0) return undefined;
|
|
561
|
+
return target as readonly [string, ...string[]];
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const convertOverrideValue = (
|
|
565
|
+
entityRaw: number,
|
|
566
|
+
componentName: string,
|
|
567
|
+
fieldName: string | undefined,
|
|
568
|
+
value: unknown,
|
|
569
|
+
): Result<unknown, KeyedCollectError> => {
|
|
570
|
+
const token = world.components.resolve(componentName);
|
|
571
|
+
if (token === undefined) return ok(value);
|
|
572
|
+
const schema = componentSchema(token) as Record<string, string>;
|
|
573
|
+
const convertFields = (
|
|
574
|
+
field: string,
|
|
575
|
+
fieldValue: unknown,
|
|
576
|
+
): Result<unknown, KeyedCollectError> => {
|
|
577
|
+
const kind = classifyEntityField(token as EcsComponent, field);
|
|
578
|
+
if (kind === null) {
|
|
579
|
+
const sharedType = schema[field];
|
|
580
|
+
const shared: SchemaFieldClass | undefined = sharedType?.startsWith('shared<')
|
|
581
|
+
? { kind: 'shared', scalar: true }
|
|
582
|
+
: sharedType?.startsWith('array<shared<')
|
|
583
|
+
? { kind: 'shared', scalar: false }
|
|
584
|
+
: undefined;
|
|
585
|
+
if (shared === undefined) return ok(fieldValue);
|
|
586
|
+
return _serializeSharedFieldValue(world, registry, shared, fieldValue, field);
|
|
587
|
+
}
|
|
588
|
+
const resolveLiveOrSlot = (item: unknown): SceneEntityAddress | undefined => {
|
|
589
|
+
if (typeof item !== 'number') return undefined;
|
|
590
|
+
const live = rawToAddress.get(item);
|
|
591
|
+
if (live !== undefined) {
|
|
592
|
+
return live.length === 1 ? live[0] : (live as readonly [string, ...string[]]);
|
|
593
|
+
}
|
|
594
|
+
return slotAddress(item);
|
|
595
|
+
};
|
|
596
|
+
if (kind.isArray) {
|
|
597
|
+
if (!Array.isArray(fieldValue)) return ok(fieldValue);
|
|
598
|
+
const mapped: SceneEntityAddress[] = [];
|
|
599
|
+
for (const item of fieldValue) {
|
|
600
|
+
const address = resolveLiveOrSlot(item);
|
|
601
|
+
if (address === undefined || !isSceneEntityAddress(address)) {
|
|
602
|
+
return err(
|
|
603
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
604
|
+
entityRaw,
|
|
605
|
+
`${componentName}.${field}`,
|
|
606
|
+
Number(item),
|
|
607
|
+
),
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
mapped.push(address);
|
|
611
|
+
}
|
|
612
|
+
return ok(mapped);
|
|
613
|
+
}
|
|
614
|
+
if (fieldValue === null) return ok(null);
|
|
615
|
+
const address = resolveLiveOrSlot(fieldValue);
|
|
616
|
+
if (address === undefined || !isSceneEntityAddress(address)) {
|
|
617
|
+
return err(
|
|
618
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
619
|
+
entityRaw,
|
|
620
|
+
`${componentName}.${field}`,
|
|
621
|
+
Number(fieldValue),
|
|
622
|
+
),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
return ok(address);
|
|
626
|
+
};
|
|
627
|
+
if (fieldName !== undefined) return convertFields(fieldName, value);
|
|
628
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return ok(value);
|
|
629
|
+
const result: Record<string, unknown> = {};
|
|
630
|
+
for (const [field, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
631
|
+
const converted = convertFields(field, fieldValue);
|
|
632
|
+
if (!converted.ok) return converted;
|
|
633
|
+
result[field] = converted.value;
|
|
634
|
+
}
|
|
635
|
+
return ok(result);
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
const declarationOverrides = (
|
|
639
|
+
parent: SceneStateInfo | undefined,
|
|
640
|
+
child: SceneStateInfo | undefined,
|
|
641
|
+
mount: SceneInstanceMount,
|
|
642
|
+
anchor: { entityRaw: number },
|
|
643
|
+
): Result<readonly SceneInstanceOverride[] | undefined, KeyedCollectError> => {
|
|
644
|
+
const memberFirst = mount.memberFirst as unknown as number;
|
|
645
|
+
const candidates: MountOverride[] = [];
|
|
646
|
+
if (parent !== undefined) {
|
|
647
|
+
for (const override of parent.state.mountTimeOverrides) {
|
|
648
|
+
const localId = override.localId as unknown as number;
|
|
649
|
+
if (localId >= memberFirst && localId < memberFirst + mount.memberCount)
|
|
650
|
+
candidates.push(override);
|
|
651
|
+
}
|
|
652
|
+
for (const [localId, records] of parent.state.overrides) {
|
|
653
|
+
const numeric = localId as unknown as number;
|
|
654
|
+
if (numeric < memberFirst || numeric >= memberFirst + mount.memberCount) continue;
|
|
655
|
+
for (const record of records.values()) {
|
|
656
|
+
candidates.push({
|
|
657
|
+
localId,
|
|
658
|
+
comp: record.comp,
|
|
659
|
+
...(record.field === undefined ? {} : { field: record.field }),
|
|
660
|
+
value: record.value,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (candidates.length === 0) candidates.push(...(mount.overrides ?? []));
|
|
666
|
+
if (candidates.length === 0 || child === undefined) return ok(undefined);
|
|
667
|
+
const byTarget = new Map<string, SceneInstanceOverride>();
|
|
668
|
+
for (const override of candidates) {
|
|
669
|
+
const localId = override.localId as unknown as number;
|
|
670
|
+
const target = resolveOverrideTarget(child, memberFirst, localId);
|
|
671
|
+
if (target === undefined) {
|
|
672
|
+
return err(
|
|
673
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
674
|
+
anchor.entityRaw,
|
|
675
|
+
'instance.override.target',
|
|
676
|
+
localId,
|
|
677
|
+
),
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const converted = convertOverrideValue(
|
|
681
|
+
anchor.entityRaw,
|
|
682
|
+
override.comp,
|
|
683
|
+
override.field,
|
|
684
|
+
override.value,
|
|
685
|
+
);
|
|
686
|
+
if (!converted.ok) return converted;
|
|
687
|
+
const targetKey = JSON.stringify(target);
|
|
688
|
+
const prior = byTarget.get(targetKey);
|
|
689
|
+
const components: Record<string, Record<string, unknown>> = {};
|
|
690
|
+
if (prior !== undefined) {
|
|
691
|
+
for (const [name, fields] of Object.entries(
|
|
692
|
+
prior.components as Record<string, Record<string, unknown>>,
|
|
693
|
+
)) {
|
|
694
|
+
components[name] = { ...fields };
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const priorFields = components[override.comp] as Record<string, unknown> | undefined;
|
|
698
|
+
const existing: Record<string, unknown> = { ...(priorFields ?? {}) };
|
|
699
|
+
if (
|
|
700
|
+
override.field === undefined &&
|
|
701
|
+
typeof converted.value === 'object' &&
|
|
702
|
+
converted.value !== null
|
|
703
|
+
) {
|
|
704
|
+
Object.assign(existing, converted.value as Record<string, unknown>);
|
|
705
|
+
} else if (override.field !== undefined) {
|
|
706
|
+
existing[override.field] = converted.value;
|
|
707
|
+
}
|
|
708
|
+
components[override.comp] = existing;
|
|
709
|
+
byTarget.set(targetKey, { target, components });
|
|
710
|
+
}
|
|
711
|
+
return ok([...byTarget.values()]);
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
for (let index = 0; index < mounts.length; index += 1) {
|
|
715
|
+
const mount = mounts[index];
|
|
716
|
+
const anchor = anchors[index];
|
|
717
|
+
if (mount === undefined || anchor === undefined) continue;
|
|
718
|
+
const child = stateByRoot.get(anchor.entityRaw);
|
|
719
|
+
const parent = child === undefined ? undefined : parentByChildRoot.get(child.root);
|
|
720
|
+
const instanceKey = child?.state.instanceKey ?? `instance-${index}`;
|
|
721
|
+
const carrierRaw =
|
|
722
|
+
childOfToken === undefined
|
|
723
|
+
? undefined
|
|
724
|
+
: (() => {
|
|
725
|
+
const parentResult = world.get(
|
|
726
|
+
anchor.entityRaw as EntityHandle,
|
|
727
|
+
childOfToken as EcsComponent<string>,
|
|
728
|
+
);
|
|
729
|
+
return parentResult.ok
|
|
730
|
+
? ((parentResult.value as Record<string, unknown>).parent as number | undefined)
|
|
731
|
+
: undefined;
|
|
732
|
+
})();
|
|
733
|
+
const converted = convertComponents(
|
|
734
|
+
carrierRaw ?? anchor.entityRaw,
|
|
735
|
+
(mount.components ?? {}) as Record<string, Record<string, unknown>>,
|
|
736
|
+
);
|
|
737
|
+
if (!converted.ok) return converted;
|
|
738
|
+
if (mount.parent !== undefined) {
|
|
739
|
+
const parentAddress = slotAddress(mount.parent as unknown as number);
|
|
740
|
+
if (parentAddress === undefined) {
|
|
741
|
+
return err(
|
|
742
|
+
new SceneCollectEntityRefOutOfClosureError(
|
|
743
|
+
carrierRaw ?? anchor.entityRaw,
|
|
744
|
+
'ChildOf.parent',
|
|
745
|
+
mount.parent as unknown as number,
|
|
746
|
+
),
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
converted.value.ChildOf = { parent: parentAddress };
|
|
750
|
+
}
|
|
751
|
+
const overrides = declarationOverrides(parent, child, mount, anchor);
|
|
752
|
+
if (!overrides.ok) return overrides;
|
|
753
|
+
entities[instanceKey] = {
|
|
754
|
+
components: converted.value,
|
|
755
|
+
instance: {
|
|
756
|
+
source: String(mount.source),
|
|
757
|
+
...(overrides.value === undefined ? {} : { overrides: overrides.value }),
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return ok({ kind: 'scene', entities });
|
|
763
|
+
}
|
|
764
|
+
|
|
216
765
|
// serializeSceneAssetToPack — emits the current Pack v2/local-artifact envelope.
|
|
217
766
|
export function serializeSceneAssetToPack(
|
|
218
767
|
sceneAsset: SceneAsset,
|
|
@@ -271,7 +820,7 @@ export function rootsToSceneAsset(
|
|
|
271
820
|
// ── Step 1: BFS closure ──
|
|
272
821
|
const visited = new Set<number>();
|
|
273
822
|
for (const root of roots) collectSubtree(world, root, visited);
|
|
274
|
-
if (visited.size === 0) return ok({ kind: 'scene', entities:
|
|
823
|
+
if (visited.size === 0) return ok({ kind: 'scene', entities: {} });
|
|
275
824
|
|
|
276
825
|
const rootRawSet = new Set<number>();
|
|
277
826
|
for (const r of roots) rootRawSet.add(r as number);
|
|
@@ -298,13 +847,55 @@ export function rootsToSceneAsset(
|
|
|
298
847
|
if (rootRawSet.has(er)) continue; // root anchor: don't classify members
|
|
299
848
|
const sr = worldGetSceneInstanceState(world, er as EntityHandle);
|
|
300
849
|
if (!sr.ok) continue;
|
|
301
|
-
|
|
850
|
+
const retainedState = sr.value as unknown as CollectorSceneState;
|
|
851
|
+
for (const [me, lid] of retainedState.entityToLocalId) {
|
|
302
852
|
const mr = me as number;
|
|
303
853
|
if (visited.has(mr) && !anchorEntities.has(mr) && !memberEntities.has(mr)) {
|
|
304
854
|
memberEntities.add(mr);
|
|
305
855
|
memberOrigin.set(mr, { anchorRaw: er, memberLocalId: lid as unknown as number });
|
|
306
856
|
}
|
|
307
857
|
}
|
|
858
|
+
// Mount carriers are structural and therefore absent from
|
|
859
|
+
// `entityToLocalId`, but the parent instance still exposes them through
|
|
860
|
+
// `bindings` and its keyed slot map. Include those slots so a nested
|
|
861
|
+
// instance is folded into its owning child scene rather than emitted as a
|
|
862
|
+
// second top-level instance.
|
|
863
|
+
for (const [key, member] of retainedState.bindings) {
|
|
864
|
+
const mr = member as number;
|
|
865
|
+
const localId = [...retainedState.keyByLocalId.entries()].find(
|
|
866
|
+
([, value]) => value === key,
|
|
867
|
+
)?.[0];
|
|
868
|
+
if (
|
|
869
|
+
localId !== undefined &&
|
|
870
|
+
visited.has(mr) &&
|
|
871
|
+
!anchorEntities.has(mr) &&
|
|
872
|
+
!memberEntities.has(mr)
|
|
873
|
+
) {
|
|
874
|
+
memberEntities.add(mr);
|
|
875
|
+
memberOrigin.set(mr, { anchorRaw: er, memberLocalId: localId });
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// A nested synthetic SceneInstance root is structural too. Its carrier is
|
|
881
|
+
// already claimed by the parent instance above; fold the anchor into that
|
|
882
|
+
// same member origin before allocating parent mounts.
|
|
883
|
+
const childOfForAnchor = world.components.resolve('ChildOf');
|
|
884
|
+
if (childOfForAnchor !== undefined) {
|
|
885
|
+
for (const anchorRaw of [...anchorEntities]) {
|
|
886
|
+
if (rootRawSet.has(anchorRaw) || memberEntities.has(anchorRaw)) continue;
|
|
887
|
+
const parentRes = world.get(
|
|
888
|
+
anchorRaw as EntityHandle,
|
|
889
|
+
childOfForAnchor as EcsComponent<string>,
|
|
890
|
+
);
|
|
891
|
+
if (!parentRes.ok) continue;
|
|
892
|
+
const carrierRaw = (parentRes.value as Record<string, unknown>).parent as number | undefined;
|
|
893
|
+
const origin = carrierRaw === undefined ? undefined : memberOrigin.get(carrierRaw);
|
|
894
|
+
if (origin !== undefined) {
|
|
895
|
+
memberEntities.add(anchorRaw);
|
|
896
|
+
memberOrigin.set(anchorRaw, origin);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
308
899
|
}
|
|
309
900
|
|
|
310
901
|
// Remove inner anchors (members of outer anchors).
|
|
@@ -388,7 +979,12 @@ export function rootsToSceneAsset(
|
|
|
388
979
|
const ownedEntities: number[] = [];
|
|
389
980
|
for (const er of orderedEntities) {
|
|
390
981
|
if (carrierToAnchor.has(er)) continue; // absorbed into its mount
|
|
391
|
-
|
|
982
|
+
// A root SceneInstance is the transient synthetic anchor returned by
|
|
983
|
+
// instantiate(). Its mapped members are authored entities; the anchor
|
|
984
|
+
// itself has no durable declaration and must not be minted as `entity-0`
|
|
985
|
+
// during collect, otherwise each collect/reload cycle adds another root.
|
|
986
|
+
if (rootRawSet.has(er) && anchorEntities.has(er)) continue;
|
|
987
|
+
if (!anchorEntities.has(er) && !memberEntities.has(er)) {
|
|
392
988
|
ownedEntities.push(er);
|
|
393
989
|
}
|
|
394
990
|
}
|
|
@@ -555,7 +1151,10 @@ export function rootsToSceneAsset(
|
|
|
555
1151
|
|
|
556
1152
|
// ── Step 4: Build SceneEntity rows ──
|
|
557
1153
|
const registeredComps = world.components.entries();
|
|
558
|
-
const
|
|
1154
|
+
const legacyEntities: Array<{
|
|
1155
|
+
localId: number;
|
|
1156
|
+
components: Record<string, Record<string, unknown>>;
|
|
1157
|
+
}> = [];
|
|
559
1158
|
|
|
560
1159
|
for (let lid = 0; lid < ownedEntities.length; lid++) {
|
|
561
1160
|
const entityRaw = ownedEntities[lid];
|
|
@@ -626,6 +1225,19 @@ export function rootsToSceneAsset(
|
|
|
626
1225
|
}
|
|
627
1226
|
|
|
628
1227
|
if (entityKind !== null) {
|
|
1228
|
+
// The synthetic root anchor is transient and is intentionally omitted
|
|
1229
|
+
// from the authored entity map. A member's implicit ChildOf edge to
|
|
1230
|
+
// that anchor has no authored counterpart, so omit that one edge
|
|
1231
|
+
// before the keyed remap instead of reporting a false out-of-closure
|
|
1232
|
+
// reference.
|
|
1233
|
+
if (
|
|
1234
|
+
!entityKind.isArray &&
|
|
1235
|
+
typeof rawValue === 'number' &&
|
|
1236
|
+
rootRawSet.has(rawValue) &&
|
|
1237
|
+
anchorEntities.has(rawValue)
|
|
1238
|
+
) {
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
629
1241
|
// Entity / array<entity> field — use shared kernel for remap
|
|
630
1242
|
if (entityKind.isArray) {
|
|
631
1243
|
const arr = _isArrayLike(rawValue)
|
|
@@ -690,12 +1302,18 @@ export function rootsToSceneAsset(
|
|
|
690
1302
|
if (Object.keys(fieldValues).length > 0) components[compName] = fieldValues;
|
|
691
1303
|
}
|
|
692
1304
|
|
|
693
|
-
|
|
1305
|
+
legacyEntities.push({ localId: lid, components });
|
|
694
1306
|
}
|
|
695
1307
|
|
|
696
|
-
return
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
1308
|
+
return collectKeyedSceneAsset(
|
|
1309
|
+
world,
|
|
1310
|
+
registry,
|
|
1311
|
+
roots,
|
|
1312
|
+
visited,
|
|
1313
|
+
ownedEntities,
|
|
1314
|
+
legacyEntities,
|
|
1315
|
+
outMounts,
|
|
1316
|
+
nonRootAnchors,
|
|
1317
|
+
memberOrigin,
|
|
1318
|
+
);
|
|
701
1319
|
}
|