@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
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import * as SceneOwner from '@forgeax/engine-scene';
|
|
2
|
-
//
|
|
3
|
-
// cycle fail-fast (AC-13). Constructs a hand-authored SceneAsset graph
|
|
4
|
-
// A -> B -> C -> A via mount.source resolved through a test-only
|
|
5
|
-
// SceneAssetResolver, bypassing the build-time scanner. World's recursive
|
|
6
|
-
// _instantiateSceneRec is expected to detect the cycle via its private
|
|
7
|
-
// stack (Set<Guid>) and fail-fast pack-cyclic-reference + detail.kind:
|
|
8
|
-
// 'mount-asset' + detail.cycle: GUID[] (plan-strategy §D-3).
|
|
2
|
+
// Keyed runtime cycle gate: A -> B -> C -> A through nested `instance` declarations.
|
|
9
3
|
|
|
10
4
|
import { World } from '@forgeax/engine-ecs';
|
|
11
|
-
import { err, ok } from '@forgeax/engine-types';
|
|
12
|
-
// Side-effect import: register Transform / ChildOf etc. via the runtime
|
|
13
|
-
// component barrel so the implementation's resolveComponent('ChildOf') call
|
|
14
|
-
// in _spawnMountEntity sees a defined token.
|
|
15
|
-
import '@forgeax/engine-runtime';
|
|
16
5
|
import type { Handle, SceneAsset } from '@forgeax/engine-types';
|
|
6
|
+
import { err, ok } from '@forgeax/engine-types';
|
|
17
7
|
import { describe, expect, it } from 'vitest';
|
|
18
8
|
import { registerSceneComponents } from './helpers/register-scene-components';
|
|
19
9
|
|
|
@@ -22,36 +12,30 @@ function registerSceneAsset(world: World, asset: SceneAsset): Handle<'SceneAsset
|
|
|
22
12
|
return world.allocSharedRef('SceneAsset', asset);
|
|
23
13
|
}
|
|
24
14
|
|
|
25
|
-
describe('runtime cycle fail-fast (
|
|
26
|
-
it('A -> B -> C -> A surfaces pack-cyclic-reference
|
|
15
|
+
describe('runtime cycle fail-fast (keyed SceneAsset)', () => {
|
|
16
|
+
it('A -> B -> C -> A surfaces pack-cyclic-reference', () => {
|
|
27
17
|
const world = new World();
|
|
28
|
-
// Build mount entries that name source=0 (parent's refs[0]) — the
|
|
29
|
-
// resolver maps each parent handle to a different child handle so the
|
|
30
|
-
// 3-asset chain forms A -> B -> C -> A.
|
|
31
18
|
const assetA: SceneAsset = {
|
|
32
19
|
kind: 'scene',
|
|
33
|
-
entities:
|
|
34
|
-
mounts: [{ localId: 0 as never, source: 0, memberFirst: 1 as never, memberCount: 0 }],
|
|
20
|
+
entities: { a: { components: {}, instance: { source: 'B' } } },
|
|
35
21
|
};
|
|
36
22
|
const assetB: SceneAsset = {
|
|
37
23
|
kind: 'scene',
|
|
38
|
-
entities:
|
|
39
|
-
mounts: [{ localId: 0 as never, source: 0, memberFirst: 1 as never, memberCount: 0 }],
|
|
24
|
+
entities: { b: { components: {}, instance: { source: 'C' } } },
|
|
40
25
|
};
|
|
41
26
|
const assetC: SceneAsset = {
|
|
42
27
|
kind: 'scene',
|
|
43
|
-
entities:
|
|
44
|
-
mounts: [{ localId: 0 as never, source: 0, memberFirst: 1 as never, memberCount: 0 }],
|
|
28
|
+
entities: { c: { components: {}, instance: { source: 'A' } } },
|
|
45
29
|
};
|
|
46
30
|
const handleA = registerSceneAsset(world, assetA);
|
|
47
31
|
const handleB = registerSceneAsset(world, assetB);
|
|
48
32
|
const handleC = registerSceneAsset(world, assetC);
|
|
49
|
-
SceneOwner.worldSetSceneAssetResolver(world, (
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
if (
|
|
33
|
+
SceneOwner.worldSetSceneAssetResolver(world, (source, parentHandle) => {
|
|
34
|
+
if (typeof source === 'number') return ok(source as Handle<'SceneAsset', 'shared'>);
|
|
35
|
+
const parent = Number(parentHandle);
|
|
36
|
+
if (parent === Number(handleA) && source === 'B') return ok(handleB);
|
|
37
|
+
if (parent === Number(handleB) && source === 'C') return ok(handleC);
|
|
38
|
+
if (parent === Number(handleC) && source === 'A') return ok(handleA);
|
|
55
39
|
return err({ code: 'asset-not-found' });
|
|
56
40
|
});
|
|
57
41
|
const r = SceneOwner.worldInstantiateScene(world, handleA);
|
|
@@ -1,257 +1,134 @@
|
|
|
1
|
-
import * as SceneOwner from '@forgeax/engine-scene';
|
|
2
|
-
|
|
3
|
-
// scene-mount-fixed-point.test.ts — AC-09 structural-equivalence fixed-point
|
|
4
|
-
// tests (feat-20260707-engine-world-clone-transient-for-editor-ssot,
|
|
5
|
-
// plan-strategy D-10 + §3.2 fixed-point argument).
|
|
6
|
-
//
|
|
7
|
-
// AC-09: mount-collapse no longer depends on serialized `Children`. After the
|
|
8
|
-
// M2 rewrite, a second rootsToSceneAsset on the reloaded world produces a
|
|
9
|
-
// mounts[] structurally equivalent to the first collect's mounts[] — source
|
|
10
|
-
// GUID / memberCount / parent per-mount equal, owned-entity set cardinality
|
|
11
|
-
// equal, NO Children key, and NO carrier-ghost accumulation across cycles.
|
|
12
|
-
//
|
|
13
|
-
// The scene shape here is the editor spawnGlbSceneAsMount on-disk form: an
|
|
14
|
-
// OWNED wrapper entity W plus a mount whose `parent` is W (NOT the synthetic
|
|
15
|
-
// root). This is the exact shape that exercises the D-8 deferred-wiring hole:
|
|
16
|
-
// on reload the mount carrier must re-acquire its ChildOf-to-W edge, or collect
|
|
17
|
-
// cannot reach the anchor and mounts come back undefined.
|
|
18
|
-
//
|
|
19
|
-
// D-10 semantics: structural equivalence, NOT per-position Children order. The
|
|
20
|
-
// first save->reload cycle may normalize order once; from the second collect
|
|
21
|
-
// onward the output is a strict fixed-point (collect3 === collect2).
|
|
22
|
-
//
|
|
23
|
-
// RED pre-fix (falsification anchor, strategy §5.4 third pillar): before
|
|
24
|
-
// m2t4/m2t5, collect2.mounts is undefined (anchor unreachable) so the
|
|
25
|
-
// equivalence and three-cycle assertions fail. GREEN after the fix.
|
|
26
|
-
|
|
27
1
|
import type { Asset } from '@forgeax/engine-assets-runtime';
|
|
28
2
|
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
29
3
|
import { World } from '@forgeax/engine-ecs';
|
|
30
4
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
5
|
+
import { SceneInstance } from '@forgeax/engine-render';
|
|
31
6
|
import type { Handle, SceneAsset } from '@forgeax/engine-types';
|
|
32
7
|
import { describe, expect, it } from 'vitest';
|
|
33
8
|
import { rootsToSceneAsset, serializeSceneAssetToPack } from '../collect-scene-asset';
|
|
34
9
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
35
10
|
import { registerSceneComponents } from './helpers/register-scene-components';
|
|
36
11
|
|
|
37
|
-
const
|
|
38
|
-
const
|
|
12
|
+
const CHILD = '11111111-1111-4111-8111-111111111111';
|
|
13
|
+
const SAVED = '22222222-2222-4222-8222-222222222222';
|
|
39
14
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const r = AssetGuid.parse(s);
|
|
45
|
-
if (!r.ok) throw new Error(`bad GUID: ${s}`);
|
|
46
|
-
return r.value;
|
|
47
|
-
}
|
|
48
|
-
function cat(reg: AssetRegistry, g: string, p: SceneAsset): void {
|
|
49
|
-
reg.catalog(pg(g), p as Asset);
|
|
50
|
-
}
|
|
51
|
-
function rs(w: World, a: SceneAsset): Handle<'SceneAsset', 'shared'> {
|
|
52
|
-
registerSceneComponents(w);
|
|
53
|
-
return w.allocSharedRef('SceneAsset', a);
|
|
54
|
-
}
|
|
55
|
-
function accessParse(reg: AssetRegistry) {
|
|
56
|
-
const internal = reg as unknown as {
|
|
57
|
-
parseAssetPayload(kind: string, payload: Record<string, unknown>, refs?: string[]): unknown;
|
|
58
|
-
};
|
|
59
|
-
return (payload: Record<string, unknown>, refs?: readonly string[]): SceneAsset =>
|
|
60
|
-
internal.parseAssetPayload('scene', payload, refs as string[] | undefined) as SceneAsset;
|
|
61
|
-
}
|
|
62
|
-
function unpack(pack: Record<string, unknown>): {
|
|
63
|
-
payload: Record<string, unknown>;
|
|
64
|
-
refs: readonly string[] | undefined;
|
|
65
|
-
} {
|
|
66
|
-
const a = (pack.assets as Record<string, unknown>[])[0] as Record<string, unknown>;
|
|
67
|
-
return { payload: a.payload as Record<string, unknown>, refs: a.refs as string[] | undefined };
|
|
15
|
+
function guid(value: string): AssetGuid {
|
|
16
|
+
const result = AssetGuid.parse(value);
|
|
17
|
+
if (!result.ok) throw new Error(`bad GUID: ${value}`);
|
|
18
|
+
return result.value;
|
|
68
19
|
}
|
|
69
|
-
/**
|
|
70
|
-
* Collect from the top-level OWNED roots (SceneInstanceState.rootEntities), not
|
|
71
|
-
* the synthetic root — mirrors the editor's entRootHandles set, which excludes
|
|
72
|
-
* the synthetic root and mount carriers. Collecting from the synthetic root
|
|
73
|
-
* would fold the synthetic root itself in as an extra owned entity.
|
|
74
|
-
*/
|
|
75
|
-
function ownedRoots(w: World, syntheticRoot: import('@forgeax/engine-ecs').EntityHandle): number[] {
|
|
76
|
-
const si = SceneOwner.worldGetSceneInstanceState(w, syntheticRoot);
|
|
77
|
-
if (!si.ok) throw new Error('no SceneInstance state on synthetic root');
|
|
78
|
-
return si.value.rootEntities.map((e) => e as unknown as number);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* D-10 structural equivalence of two SceneAssets: mounts (source, memberCount,
|
|
83
|
-
* parent — memberFirst is derived from the owned-set cardinality and re-derived
|
|
84
|
-
* each collect, so it is NOT part of the equivalence contract) + owned entity
|
|
85
|
-
* set cardinality + per-entity component-type key set. Does NOT assert Children
|
|
86
|
-
* per-position order (mirror is derived from spawn order).
|
|
87
|
-
*/
|
|
88
|
-
function structurallyEquivalent(a: SceneAsset, b: SceneAsset): boolean {
|
|
89
|
-
const am = a.mounts ?? [];
|
|
90
|
-
const bm = b.mounts ?? [];
|
|
91
|
-
if (am.length !== bm.length) return false;
|
|
92
|
-
// Compare mounts as a multiset keyed by (source, memberCount, parent) so a
|
|
93
|
-
// one-time order normalization does not fail equivalence.
|
|
94
|
-
const key = (m: (typeof am)[number]): string =>
|
|
95
|
-
`${String(m.source)}|${m.memberCount}|${String(m.parent)}`;
|
|
96
|
-
const aKeys = am.map(key).sort();
|
|
97
|
-
const bKeys = bm.map(key).sort();
|
|
98
|
-
for (let i = 0; i < aKeys.length; i++) if (aKeys[i] !== bKeys[i]) return false;
|
|
99
20
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
.map((e) =>
|
|
104
|
-
Object.keys(e.components as Record<string, unknown>)
|
|
105
|
-
.sort()
|
|
106
|
-
.join(','),
|
|
107
|
-
)
|
|
108
|
-
.sort();
|
|
109
|
-
const aC = compKeySet(a);
|
|
110
|
-
const bC = compKeySet(b);
|
|
111
|
-
for (let i = 0; i < aC.length; i++) if (aC[i] !== bC[i]) return false;
|
|
112
|
-
return true;
|
|
21
|
+
function register(world: World, asset: SceneAsset): Handle<'SceneAsset', 'shared'> {
|
|
22
|
+
registerSceneComponents(world);
|
|
23
|
+
return world.allocSharedRef('SceneAsset', asset);
|
|
113
24
|
}
|
|
114
25
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if ((e.components as Record<string, unknown>).Children !== undefined) return false;
|
|
119
|
-
}
|
|
120
|
-
return true;
|
|
26
|
+
function catalog(registry: AssetRegistry, id: string, asset: SceneAsset): void {
|
|
27
|
+
const result = registry.catalog(guid(id), asset as Asset);
|
|
28
|
+
if (!result.ok) throw new Error(result.error.code);
|
|
121
29
|
}
|
|
122
30
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
31
|
+
function parse(
|
|
32
|
+
registry: AssetRegistry,
|
|
33
|
+
payload: Record<string, unknown>,
|
|
34
|
+
refs: readonly string[],
|
|
35
|
+
): SceneAsset {
|
|
36
|
+
const result = (
|
|
37
|
+
registry as unknown as {
|
|
38
|
+
parseAssetPayload(
|
|
39
|
+
kind: string,
|
|
40
|
+
payload: Record<string, unknown>,
|
|
41
|
+
refs?: readonly string[],
|
|
42
|
+
): unknown;
|
|
43
|
+
}
|
|
44
|
+
).parseAssetPayload('scene', payload, refs);
|
|
45
|
+
if (result === undefined) throw new Error('scene payload parse failed');
|
|
46
|
+
return result as SceneAsset;
|
|
47
|
+
}
|
|
127
48
|
|
|
128
|
-
|
|
49
|
+
describe('keyed SceneAsset collect fixed point', () => {
|
|
50
|
+
it('collect -> serialize -> reload -> collect preserves nested declaration and keys', () => {
|
|
51
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
52
|
+
const world = new World();
|
|
129
53
|
const child: SceneAsset = {
|
|
130
54
|
kind: 'scene',
|
|
131
|
-
entities:
|
|
55
|
+
entities: { door: { components: { Transform: { pos: [2, 0, 0] } } } },
|
|
132
56
|
};
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// Top scene: owned wrapper W (localId 0) + mount whose parent is W. Exact
|
|
136
|
-
// spawnGlbSceneAsMount on-disk shape (an owned wrapper with a nested mount).
|
|
137
|
-
const top: SceneAsset = {
|
|
57
|
+
catalog(registry, CHILD, child);
|
|
58
|
+
const parent: SceneAsset = {
|
|
138
59
|
kind: 'scene',
|
|
139
|
-
entities:
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
source: G_CHILD,
|
|
144
|
-
memberFirst: 2 as never,
|
|
145
|
-
memberCount: 1,
|
|
146
|
-
parent: 0 as never,
|
|
147
|
-
},
|
|
148
|
-
],
|
|
60
|
+
entities: {
|
|
61
|
+
root: { components: { Transform: {} } },
|
|
62
|
+
house: { components: {}, instance: { source: CHILD } },
|
|
63
|
+
},
|
|
149
64
|
};
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
expect(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
expect(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const c2 = rootsToSceneAsset(reg, w, ownedRoots(w, i1.value) as never);
|
|
182
|
-
expect(c2.ok).toBe(true);
|
|
183
|
-
if (!c2.ok) return;
|
|
184
|
-
|
|
185
|
-
// AC-09: collect2 is structurally equivalent to collect1 (source/memberCount
|
|
186
|
-
// /parent per-mount, owned-set cardinality). This is the core assertion —
|
|
187
|
-
// RED pre-fix because collect2.mounts comes back undefined.
|
|
188
|
-
expect(structurallyEquivalent(c1.value, c2.value)).toBe(true);
|
|
189
|
-
expect(noChildrenKey(c2.value)).toBe(true);
|
|
190
|
-
|
|
191
|
-
// Cycle 3: reload once more and collect — the ghost-accretion point. From
|
|
192
|
-
// the second collect onward the output is a strict fixed-point (D-10).
|
|
193
|
-
const s2 = serializeSceneAssetToPack(c2.value, w.components.entries(), G_TOP);
|
|
194
|
-
expect(s2.ok).toBe(true);
|
|
195
|
-
if (!s2.ok) return;
|
|
196
|
-
const u2 = unpack(s2.value);
|
|
197
|
-
const p2 = parse(u2.payload, u2.refs);
|
|
198
|
-
cat(reg, G_TOP, p2);
|
|
199
|
-
const i2 = reg.instantiate(rs(w, p2), w);
|
|
200
|
-
expect(i2.ok).toBe(true);
|
|
201
|
-
if (!i2.ok) return;
|
|
202
|
-
const c3 = rootsToSceneAsset(reg, w, ownedRoots(w, i2.value) as never);
|
|
203
|
-
expect(c3.ok).toBe(true);
|
|
204
|
-
if (!c3.ok) return;
|
|
205
|
-
|
|
206
|
-
// No carrier ghost accumulation: owned + mount counts are a fixed point.
|
|
207
|
-
expect(c3.value.entities.length).toBe(c2.value.entities.length);
|
|
208
|
-
expect((c3.value.mounts ?? []).length).toBe((c2.value.mounts ?? []).length);
|
|
209
|
-
expect(structurallyEquivalent(c2.value, c3.value)).toBe(true);
|
|
210
|
-
expect(noChildrenKey(c3.value)).toBe(true);
|
|
65
|
+
const first = registry.instantiate(register(world, parent), world);
|
|
66
|
+
expect(first.ok).toBe(true);
|
|
67
|
+
if (!first.ok) return;
|
|
68
|
+
const collected1 = rootsToSceneAsset(registry, world, [first.value]);
|
|
69
|
+
expect(collected1.ok).toBe(true);
|
|
70
|
+
if (!collected1.ok) return;
|
|
71
|
+
expect(collected1.value.entities.house?.instance?.source).toBe(CHILD);
|
|
72
|
+
const packed = serializeSceneAssetToPack(collected1.value, world.components.entries(), SAVED);
|
|
73
|
+
expect(packed.ok).toBe(true);
|
|
74
|
+
if (!packed.ok) return;
|
|
75
|
+
const row = (packed.value.assets as Array<Record<string, unknown>>)[0];
|
|
76
|
+
if (!row) throw new Error('missing serialized scene');
|
|
77
|
+
const decoded = parse(
|
|
78
|
+
registry,
|
|
79
|
+
row.payload as Record<string, unknown>,
|
|
80
|
+
row.refs as readonly string[],
|
|
81
|
+
);
|
|
82
|
+
expect(decoded.entities.house?.instance?.source).toBe(CHILD);
|
|
83
|
+
|
|
84
|
+
const secondWorld = new World();
|
|
85
|
+
catalog(registry, SAVED, decoded);
|
|
86
|
+
const second = registry.instantiate(register(secondWorld, decoded), secondWorld);
|
|
87
|
+
expect(second.ok).toBe(true);
|
|
88
|
+
if (!second.ok) return;
|
|
89
|
+
const collected2 = rootsToSceneAsset(registry, secondWorld, [second.value]);
|
|
90
|
+
expect(collected2.ok).toBe(true);
|
|
91
|
+
if (!collected2.ok) return;
|
|
92
|
+
expect(Object.keys(collected2.value.entities)).toEqual(Object.keys(collected1.value.entities));
|
|
93
|
+
expect(collected2.value.entities.house?.instance).toEqual(
|
|
94
|
+
collected1.value.entities.house?.instance,
|
|
95
|
+
);
|
|
211
96
|
});
|
|
212
97
|
|
|
213
|
-
it('
|
|
214
|
-
const
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
// Flat scene: two owned entities, no mounts.
|
|
218
|
-
const flat: SceneAsset = {
|
|
98
|
+
it('a scene without nested instances remains a keyed two entity asset', () => {
|
|
99
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
100
|
+
const world = new World();
|
|
101
|
+
const asset: SceneAsset = {
|
|
219
102
|
kind: 'scene',
|
|
220
|
-
entities:
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
103
|
+
entities: {
|
|
104
|
+
root: { components: { Transform: {} } },
|
|
105
|
+
child: { components: { Transform: {}, ChildOf: { parent: 'root' } } },
|
|
106
|
+
},
|
|
224
107
|
};
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (!c1.ok) return;
|
|
235
|
-
expect((c1.value.mounts ?? []).length).toBe(0);
|
|
236
|
-
expect(c1.value.entities.length).toBe(2);
|
|
237
|
-
|
|
238
|
-
const s1 = serializeSceneAssetToPack(c1.value, w.components.entries(), G_TOP);
|
|
239
|
-
expect(s1.ok).toBe(true);
|
|
240
|
-
if (!s1.ok) return;
|
|
241
|
-
const u1 = unpack(s1.value);
|
|
242
|
-
const p1 = parse(u1.payload, u1.refs);
|
|
243
|
-
cat(reg, G_TOP, p1);
|
|
244
|
-
const i1 = reg.instantiate(rs(w, p1), w);
|
|
245
|
-
expect(i1.ok).toBe(true);
|
|
246
|
-
if (!i1.ok) return;
|
|
247
|
-
const c2 = rootsToSceneAsset(reg, w, ownedRoots(w, i1.value) as never);
|
|
248
|
-
expect(c2.ok).toBe(true);
|
|
249
|
-
if (!c2.ok) return;
|
|
108
|
+
const root = registry.instantiate(register(world, asset), world);
|
|
109
|
+
expect(root.ok).toBe(true);
|
|
110
|
+
if (!root.ok) return;
|
|
111
|
+
const collected = rootsToSceneAsset(registry, world, [root.value]);
|
|
112
|
+
expect(collected.ok).toBe(true);
|
|
113
|
+
if (!collected.ok) return;
|
|
114
|
+
expect(Object.keys(collected.value.entities)).toEqual(['root', 'child']);
|
|
115
|
+
expect(collected.value.entities.child?.components.ChildOf).toEqual({ parent: 'root' });
|
|
116
|
+
});
|
|
250
117
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
118
|
+
it('the transient SceneInstance state is not emitted as an authored component', () => {
|
|
119
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
120
|
+
const world = new World();
|
|
121
|
+
const asset: SceneAsset = {
|
|
122
|
+
kind: 'scene',
|
|
123
|
+
entities: { root: { components: { Transform: {} } } },
|
|
124
|
+
};
|
|
125
|
+
const root = registry.instantiate(register(world, asset), world);
|
|
126
|
+
expect(root.ok).toBe(true);
|
|
127
|
+
if (!root.ok) return;
|
|
128
|
+
expect(world.get(root.value, SceneInstance).ok).toBe(true);
|
|
129
|
+
const collected = rootsToSceneAsset(registry, world, [root.value]);
|
|
130
|
+
expect(collected.ok).toBe(true);
|
|
131
|
+
if (!collected.ok) return;
|
|
132
|
+
expect(collected.value.entities.root?.components.SceneInstance).toBeUndefined();
|
|
256
133
|
});
|
|
257
134
|
});
|