@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,533 +1,149 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// Coverage:
|
|
6
|
-
// m3-t1: serialize<->parse mounts symmetry round-trip
|
|
7
|
-
// m3-t2: registry.instantiate with mounts overload
|
|
8
|
-
// m3-t3: cyclic mount fail-fast
|
|
1
|
+
// Keyed SceneAsset production and round-trip coverage.
|
|
2
|
+
// The filename remains for the historical test target, while the public
|
|
3
|
+
// contract is now `entities[key].instance`, with no authored mount windows.
|
|
9
4
|
|
|
10
5
|
import type { Asset } from '@forgeax/engine-assets-runtime';
|
|
11
|
-
import { AssetRegistry
|
|
12
|
-
import {
|
|
6
|
+
import { AssetRegistry } from '@forgeax/engine-assets-runtime';
|
|
7
|
+
import { World } from '@forgeax/engine-ecs';
|
|
13
8
|
import { AssetGuid } from '@forgeax/engine-pack/guid';
|
|
14
9
|
import { SceneInstance } from '@forgeax/engine-render';
|
|
15
|
-
import type { Handle, SceneAsset
|
|
16
|
-
import { err, ok } from '@forgeax/engine-types';
|
|
10
|
+
import type { Handle, SceneAsset } from '@forgeax/engine-types';
|
|
17
11
|
import { describe, expect, it } from 'vitest';
|
|
18
|
-
import { serializeSceneAssetToPack } from '../collect-scene-asset';
|
|
12
|
+
import { rootsToSceneAsset, serializeSceneAssetToPack } from '../collect-scene-asset';
|
|
19
13
|
import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
|
|
20
14
|
import { registerSceneComponents } from './helpers/register-scene-components';
|
|
21
15
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
const CHILD_GUID = '11111111-1111-4111-8111-111111111111';
|
|
17
|
+
const PARENT_GUID = '22222222-2222-4222-8222-222222222222';
|
|
18
|
+
const OUT_GUID = '33333333-3333-4333-8333-333333333333';
|
|
25
19
|
|
|
26
|
-
function
|
|
27
|
-
|
|
20
|
+
function parseGuid(value: string): AssetGuid {
|
|
21
|
+
const result = AssetGuid.parse(value);
|
|
22
|
+
if (!result.ok) throw new Error(`bad GUID: ${value}`);
|
|
23
|
+
return result.value;
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return r.value;
|
|
26
|
+
function register(world: World, asset: SceneAsset): Handle<'SceneAsset', 'shared'> {
|
|
27
|
+
registerSceneComponents(world);
|
|
28
|
+
return world.allocSharedRef('SceneAsset', asset);
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
function
|
|
37
|
-
|
|
31
|
+
function catalog(registry: AssetRegistry, guid: string, asset: SceneAsset): void {
|
|
32
|
+
const result = registry.catalog(parseGuid(guid), asset as Asset);
|
|
33
|
+
if (!result.ok) throw new Error(result.error.code);
|
|
38
34
|
}
|
|
39
35
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
ph: Handle<'SceneAsset', 'shared'>,
|
|
48
|
-
ch: Handle<'SceneAsset', 'shared'>,
|
|
49
|
-
): void {
|
|
50
|
-
SceneOwner.worldSetSceneAssetResolver(w, (_s, pH) =>
|
|
51
|
-
(pH as unknown as number) === (ph as unknown as number)
|
|
52
|
-
? ok(ch)
|
|
53
|
-
: err({ code: 'asset-not-found' }),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Access parseScenePayload via public parseAssetPayload -> scene kind cast. */
|
|
58
|
-
function accessParseScenePayload(reg: AssetRegistry) {
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
-
const internal = reg as unknown as {
|
|
61
|
-
parseAssetPayload(kind: string, payload: Record<string, unknown>, refs?: string[]): unknown;
|
|
36
|
+
function childScene(): SceneAsset {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'scene',
|
|
39
|
+
entities: {
|
|
40
|
+
door: { components: { Transform: { pos: [1, 0, 0] } } },
|
|
41
|
+
frame: { components: { Transform: { pos: [0, 0, 0] } } },
|
|
42
|
+
},
|
|
62
43
|
};
|
|
63
|
-
return (
|
|
64
|
-
kind: string,
|
|
65
|
-
payload: Record<string, unknown>,
|
|
66
|
-
refs?: readonly string[] | undefined,
|
|
67
|
-
): SceneAsset | undefined => {
|
|
68
|
-
const result = internal.parseAssetPayload(kind, payload, refs as string[] | undefined);
|
|
69
|
-
if (result === undefined) return undefined;
|
|
70
|
-
if (
|
|
71
|
-
typeof result === 'object' &&
|
|
72
|
-
result !== null &&
|
|
73
|
-
'kind' in result &&
|
|
74
|
-
result.kind === 'scene'
|
|
75
|
-
) {
|
|
76
|
-
return result as SceneAsset;
|
|
77
|
-
}
|
|
78
|
-
return undefined;
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Extract payload and refs from serializeSceneAssetToPack result. */
|
|
83
|
-
function unpackSerialized(
|
|
84
|
-
pack: Record<string, unknown>,
|
|
85
|
-
): { payload: Record<string, unknown>; refs: readonly string[] | undefined } | undefined {
|
|
86
|
-
const assets = pack.assets as readonly Record<string, unknown>[] | undefined;
|
|
87
|
-
if (!assets || assets.length === 0) return undefined;
|
|
88
|
-
const a = assets[0];
|
|
89
|
-
if (!a) return undefined;
|
|
90
|
-
const payload = a.payload as Record<string, unknown> | undefined;
|
|
91
|
-
if (!payload) return undefined;
|
|
92
|
-
return { payload, refs: a.refs as readonly string[] | undefined };
|
|
93
44
|
}
|
|
94
45
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!si.ok) return undefined;
|
|
99
|
-
const src = (si.value as Record<string, unknown>).source as Handle<string, 'shared'>;
|
|
100
|
-
const pr = resolveAssetHandle<SceneAsset>(w, src as unknown as Handle<string, 'shared'>);
|
|
101
|
-
if (!pr.ok) return undefined;
|
|
102
|
-
return (pr.value as SceneAsset).mounts !== undefined ? 'has-mounts' : 'no-mounts';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const G1 = 'cbe42beb-8975-5096-b3a1-3dda4cb4c077';
|
|
106
|
-
const G2 = 'f6af7007-158f-4d92-9e47-93bf2f213e1f';
|
|
107
|
-
const G3 = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
|
108
|
-
|
|
109
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
110
|
-
// m3-t1: serialize<->parse mounts symmetry round-trip
|
|
111
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
112
|
-
|
|
113
|
-
describe('m3-t1 — serialize<->parse mounts symmetry round-trip', () => {
|
|
114
|
-
it('serialize mounts.source GUID -> refs index, parse refs index -> GUID', () => {
|
|
115
|
-
const reg = mkReg();
|
|
116
|
-
const fn = accessParseScenePayload(reg);
|
|
117
|
-
|
|
118
|
-
// Build a SceneAsset with mounts (source as GUID string, post-collect shape).
|
|
119
|
-
const mounts: SceneInstanceMount[] = [
|
|
120
|
-
{
|
|
121
|
-
localId: 1 as never,
|
|
122
|
-
source: G1,
|
|
123
|
-
memberFirst: 3 as never,
|
|
124
|
-
memberCount: 5,
|
|
125
|
-
parent: 0 as never,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
localId: 2 as never,
|
|
129
|
-
source: G2,
|
|
130
|
-
memberFirst: 8 as never,
|
|
131
|
-
memberCount: 2,
|
|
132
|
-
},
|
|
133
|
-
];
|
|
46
|
+
describe('keyed SceneAsset serialize and decode', () => {
|
|
47
|
+
it('serializes nested instance sources to refs and decodes them back to GUIDs', () => {
|
|
48
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
134
49
|
const scene: SceneAsset = {
|
|
135
50
|
kind: 'scene',
|
|
136
|
-
entities:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!serRes.ok) return;
|
|
144
|
-
|
|
145
|
-
const unpacked = unpackSerialized(serRes.value);
|
|
146
|
-
expect(unpacked).toBeDefined();
|
|
147
|
-
if (!unpacked) return;
|
|
148
|
-
const { payload: packPayload, refs } = unpacked;
|
|
149
|
-
|
|
150
|
-
// mounts should be present in the serialized output.
|
|
151
|
-
const serMounts = packPayload.mounts as ReadonlyArray<Record<string, unknown>> | undefined;
|
|
152
|
-
expect(serMounts).toBeDefined();
|
|
153
|
-
expect(serMounts?.length).toBe(2);
|
|
154
|
-
|
|
155
|
-
// source should be refs index (number), not GUID string.
|
|
156
|
-
const sm0 = serMounts?.[0];
|
|
157
|
-
expect(sm0).toBeDefined();
|
|
158
|
-
if (!sm0) return;
|
|
159
|
-
expect(typeof sm0.source).toBe('number');
|
|
160
|
-
expect(sm0.localId).toBe(1);
|
|
161
|
-
expect(sm0.memberFirst).toBe(3);
|
|
162
|
-
expect(sm0.memberCount).toBe(5);
|
|
163
|
-
expect(sm0.parent).toBe(0);
|
|
164
|
-
|
|
165
|
-
const sm1 = serMounts?.[1];
|
|
166
|
-
expect(sm1).toBeDefined();
|
|
167
|
-
if (!sm1) return;
|
|
168
|
-
expect(typeof sm1.source).toBe('number');
|
|
169
|
-
expect(sm1.localId).toBe(2);
|
|
170
|
-
expect(sm1.memberFirst).toBe(8);
|
|
171
|
-
expect(sm1.memberCount).toBe(2);
|
|
172
|
-
expect(sm1.parent).toBeUndefined();
|
|
173
|
-
|
|
174
|
-
// refs should contain both GUIDs.
|
|
175
|
-
expect(refs).toBeDefined();
|
|
176
|
-
expect(refs).toContain(G1);
|
|
177
|
-
expect(refs).toContain(G2);
|
|
178
|
-
|
|
179
|
-
// Verify source index -> GUID round-trip: refs[sourceIndex] === original GUID.
|
|
180
|
-
const idx0 = sm0.source as number;
|
|
181
|
-
const idx1 = sm1.source as number;
|
|
182
|
-
expect(refs?.[idx0]).toBe(G1);
|
|
183
|
-
expect(refs?.[idx1]).toBe(G2);
|
|
184
|
-
|
|
185
|
-
// Parse back: source should be restored to GUID string.
|
|
186
|
-
const parsed = fn('scene', packPayload, refs);
|
|
187
|
-
expect(parsed).toBeDefined();
|
|
188
|
-
if (!parsed) return;
|
|
189
|
-
|
|
190
|
-
expect(parsed.mounts).toBeDefined();
|
|
191
|
-
expect(parsed.mounts?.length).toBe(2);
|
|
192
|
-
|
|
193
|
-
const pm0 = parsed.mounts?.[0];
|
|
194
|
-
expect(pm0).toBeDefined();
|
|
195
|
-
if (!pm0) return;
|
|
196
|
-
expect(pm0.source).toBe(G1);
|
|
197
|
-
expect(pm0.localId).toBe(1);
|
|
198
|
-
expect(pm0.memberFirst).toBe(3);
|
|
199
|
-
expect(pm0.memberCount).toBe(5);
|
|
200
|
-
expect(pm0.parent).toBe(0);
|
|
201
|
-
|
|
202
|
-
const pm1 = parsed.mounts?.[1];
|
|
203
|
-
expect(pm1).toBeDefined();
|
|
204
|
-
if (!pm1) return;
|
|
205
|
-
expect(pm1.source).toBe(G2);
|
|
206
|
-
expect(pm1.localId).toBe(2);
|
|
207
|
-
expect(pm1.memberFirst).toBe(8);
|
|
208
|
-
expect(pm1.memberCount).toBe(2);
|
|
209
|
-
expect(pm1.parent).toBeUndefined();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('scene without mounts serializes without mounts field (back-compat)', () => {
|
|
213
|
-
const reg = mkReg();
|
|
214
|
-
const fn = accessParseScenePayload(reg);
|
|
215
|
-
|
|
216
|
-
const scene: SceneAsset = {
|
|
217
|
-
kind: 'scene',
|
|
218
|
-
entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
|
|
219
|
-
};
|
|
220
|
-
const serResBackcompat = serializeSceneAssetToPack(scene, new Map(), G3);
|
|
221
|
-
expect(serResBackcompat.ok).toBe(true);
|
|
222
|
-
if (!serResBackcompat.ok) return;
|
|
223
|
-
|
|
224
|
-
const unpackedBackcompat = unpackSerialized(serResBackcompat.value);
|
|
225
|
-
expect(unpackedBackcompat).toBeDefined();
|
|
226
|
-
if (!unpackedBackcompat) return;
|
|
227
|
-
// Scene with no mounts: serialize should not add an empty mounts array
|
|
228
|
-
// unless it carries semantic meaning. Parse back should still work.
|
|
229
|
-
const parsedBackcompat = fn('scene', unpackedBackcompat.payload, unpackedBackcompat.refs);
|
|
230
|
-
expect(parsedBackcompat).toBeDefined();
|
|
231
|
-
expect(parsedBackcompat?.entities).toBeDefined();
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('numeric fields (memberFirst/memberCount/localId) preserved exactly', () => {
|
|
235
|
-
const reg = mkReg();
|
|
236
|
-
const fn = accessParseScenePayload(reg);
|
|
237
|
-
|
|
238
|
-
const mounts: SceneInstanceMount[] = [
|
|
239
|
-
{
|
|
240
|
-
localId: 999 as never,
|
|
241
|
-
source: G1,
|
|
242
|
-
memberFirst: 1000 as never,
|
|
243
|
-
memberCount: 42,
|
|
244
|
-
parent: 500 as never,
|
|
245
|
-
},
|
|
246
|
-
];
|
|
247
|
-
const sceneNumeric: SceneAsset = {
|
|
248
|
-
kind: 'scene',
|
|
249
|
-
entities: [],
|
|
250
|
-
mounts,
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
const serResNum = serializeSceneAssetToPack(sceneNumeric, new Map(), G3);
|
|
254
|
-
expect(serResNum.ok).toBe(true);
|
|
255
|
-
if (!serResNum.ok) return;
|
|
256
|
-
|
|
257
|
-
const unpackedNum = unpackSerialized(serResNum.value);
|
|
258
|
-
expect(unpackedNum).toBeDefined();
|
|
259
|
-
if (!unpackedNum) return;
|
|
260
|
-
const packPayloadNum = unpackedNum.payload;
|
|
261
|
-
|
|
262
|
-
const serMountsNum = packPayloadNum.mounts as
|
|
263
|
-
| ReadonlyArray<Record<string, unknown>>
|
|
264
|
-
| undefined;
|
|
265
|
-
expect(serMountsNum).toBeDefined();
|
|
266
|
-
expect(serMountsNum?.[0]?.localId).toBe(999);
|
|
267
|
-
expect(serMountsNum?.[0]?.memberFirst).toBe(1000);
|
|
268
|
-
expect(serMountsNum?.[0]?.memberCount).toBe(42);
|
|
269
|
-
expect(serMountsNum?.[0]?.parent).toBe(500);
|
|
270
|
-
|
|
271
|
-
// Parse back and verify.
|
|
272
|
-
const parsedNum = fn('scene', packPayloadNum, unpackedNum.refs);
|
|
273
|
-
expect(parsedNum).toBeDefined();
|
|
274
|
-
if (!parsedNum?.mounts?.[0]) return;
|
|
275
|
-
expect(parsedNum.mounts[0].localId).toBe(999);
|
|
276
|
-
expect(parsedNum.mounts[0].memberFirst).toBe(1000);
|
|
277
|
-
expect(parsedNum.mounts[0].memberCount).toBe(42);
|
|
278
|
-
expect(parsedNum.mounts[0].parent).toBe(500);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('preserves the atomic publication fence on authored mounts', () => {
|
|
282
|
-
const fence = {
|
|
283
|
-
schemaVersion: 'scene-publication-fence/1' as const,
|
|
284
|
-
sourcePath: 'assets/showcase.pack.ts',
|
|
285
|
-
sourceRevision: 'sha256:source',
|
|
286
|
-
publicationGeneration: 4,
|
|
287
|
-
outputDigest: 'sha256:output',
|
|
288
|
-
outputSetDigest: 'sha256:output-set',
|
|
289
|
-
receiptIdentity: 'sha256:receipt',
|
|
290
|
-
};
|
|
291
|
-
const packed = serializeSceneAssetToPack(
|
|
292
|
-
{
|
|
293
|
-
kind: 'scene',
|
|
294
|
-
entities: [{ localId: 0 as never, components: {} }],
|
|
295
|
-
mounts: [
|
|
296
|
-
{
|
|
297
|
-
localId: 1 as never,
|
|
298
|
-
source: G1,
|
|
299
|
-
memberFirst: 2 as never,
|
|
300
|
-
memberCount: 1,
|
|
301
|
-
publicationFence: fence,
|
|
51
|
+
entities: {
|
|
52
|
+
root: { components: { Transform: { pos: [0, 0, 0] } } },
|
|
53
|
+
house: {
|
|
54
|
+
components: {},
|
|
55
|
+
instance: {
|
|
56
|
+
source: CHILD_GUID,
|
|
57
|
+
overrides: [{ target: ['door'], components: { Transform: { pos: [4, 0, 0] } } }],
|
|
302
58
|
},
|
|
303
|
-
],
|
|
304
|
-
},
|
|
305
|
-
new Map(),
|
|
306
|
-
G3,
|
|
307
|
-
);
|
|
308
|
-
expect(packed.ok).toBe(true);
|
|
309
|
-
if (!packed.ok) return;
|
|
310
|
-
const payload = (packed.value.assets as Array<{ payload: Record<string, unknown> }>)[0]
|
|
311
|
-
?.payload;
|
|
312
|
-
expect((payload?.mounts as Array<Record<string, unknown>>)[0]?.publicationFence).toEqual(fence);
|
|
313
|
-
});
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
317
|
-
// m3-t2: registry.instantiate with mounts overload
|
|
318
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
319
|
-
|
|
320
|
-
describe('m3-t2 — registry.instantiate with mounts overload', () => {
|
|
321
|
-
it('instantiate parent with mount spawns nested child SceneInstance', () => {
|
|
322
|
-
const reg = mkReg();
|
|
323
|
-
const w = new World();
|
|
324
|
-
|
|
325
|
-
// Child scene: two entities with Transform.
|
|
326
|
-
const child: SceneAsset = {
|
|
327
|
-
kind: 'scene',
|
|
328
|
-
entities: [
|
|
329
|
-
{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
|
|
330
|
-
{ localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
|
|
331
|
-
],
|
|
332
|
-
};
|
|
333
|
-
cat(reg, G1, child);
|
|
334
|
-
const ch = rs(w, child);
|
|
335
|
-
|
|
336
|
-
// Parent scene: one owned entity + mount referencing child.
|
|
337
|
-
const parent: SceneAsset = {
|
|
338
|
-
kind: 'scene',
|
|
339
|
-
entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
|
|
340
|
-
mounts: [
|
|
341
|
-
{
|
|
342
|
-
localId: 1 as never,
|
|
343
|
-
source: G1, // GUID string — post-parse / post-collect shape
|
|
344
|
-
memberFirst: 2 as never,
|
|
345
|
-
memberCount: 2,
|
|
346
59
|
},
|
|
347
|
-
|
|
60
|
+
},
|
|
348
61
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
357
|
-
expect(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
// nested SceneInstance root entities.
|
|
366
|
-
let descendantCount = 0;
|
|
367
|
-
let foundMountChild = false;
|
|
368
|
-
for (const c of w.iterDescendants(root)) {
|
|
369
|
-
if (c === root) continue;
|
|
370
|
-
descendantCount++;
|
|
371
|
-
// A child that carries SceneInstance = nested instance root.
|
|
372
|
-
if (w.get(c, SceneInstance).ok) {
|
|
373
|
-
foundMountChild = true;
|
|
62
|
+
const serialized = serializeSceneAssetToPack(scene, new Map(), OUT_GUID);
|
|
63
|
+
expect(serialized.ok).toBe(true);
|
|
64
|
+
if (!serialized.ok) return;
|
|
65
|
+
const packedAsset = (serialized.value.assets as Array<Record<string, unknown>>)[0];
|
|
66
|
+
expect(packedAsset).toBeDefined();
|
|
67
|
+
if (!packedAsset) return;
|
|
68
|
+
const payload = packedAsset.payload as Record<string, unknown>;
|
|
69
|
+
const entities = payload.entities as Record<string, Record<string, unknown>>;
|
|
70
|
+
expect((entities.house?.instance as Record<string, unknown>).source).toBe(0);
|
|
71
|
+
const parsed = (
|
|
72
|
+
registry as unknown as {
|
|
73
|
+
parseAssetPayload(
|
|
74
|
+
kind: string,
|
|
75
|
+
payload: Record<string, unknown>,
|
|
76
|
+
refs?: readonly string[],
|
|
77
|
+
): unknown;
|
|
374
78
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
expect(descendantCount).toBeGreaterThanOrEqual(2);
|
|
378
|
-
expect(foundMountChild).toBe(true);
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('reload path through catalog+instantiate restores mount structure', () => {
|
|
382
|
-
const reg = mkReg();
|
|
383
|
-
const w = new World();
|
|
384
|
-
|
|
385
|
-
// Build a complete round-trip fixture.
|
|
386
|
-
// Step 1: Catalog child scene.
|
|
387
|
-
const child: SceneAsset = {
|
|
79
|
+
).parseAssetPayload('scene', payload, packedAsset.refs as readonly string[]);
|
|
80
|
+
expect(parsed).toMatchObject({
|
|
388
81
|
kind: 'scene',
|
|
389
|
-
entities:
|
|
390
|
-
};
|
|
391
|
-
|
|
392
|
-
const ch = rs(w, child);
|
|
82
|
+
entities: { house: { instance: { source: CHILD_GUID } } },
|
|
83
|
+
});
|
|
84
|
+
});
|
|
393
85
|
|
|
394
|
-
|
|
86
|
+
it('collects a live nested instance back to a keyed declaration', () => {
|
|
87
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
88
|
+
const world = new World();
|
|
89
|
+
const child = childScene();
|
|
90
|
+
catalog(registry, CHILD_GUID, child);
|
|
395
91
|
const parent: SceneAsset = {
|
|
396
92
|
kind: 'scene',
|
|
397
|
-
entities:
|
|
398
|
-
|
|
399
|
-
{
|
|
400
|
-
|
|
401
|
-
source: G1,
|
|
402
|
-
memberFirst: 2 as never,
|
|
403
|
-
memberCount: 1,
|
|
404
|
-
},
|
|
405
|
-
],
|
|
93
|
+
entities: {
|
|
94
|
+
root: { components: { Transform: { pos: [0, 0, 0] } } },
|
|
95
|
+
house: { components: {}, instance: { source: CHILD_GUID } },
|
|
96
|
+
},
|
|
406
97
|
};
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const
|
|
413
|
-
expect(
|
|
414
|
-
if (!
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
let hasSceneInstanceChild = false;
|
|
419
|
-
for (const c of w.iterDescendants(root)) {
|
|
420
|
-
if (c === root) continue;
|
|
421
|
-
if (w.get(c, SceneInstance).ok) {
|
|
422
|
-
hasSceneInstanceChild = true;
|
|
423
|
-
// Verify the SceneInstance.source can be resolved.
|
|
424
|
-
const si = getSceneInstanceSource(w, c);
|
|
425
|
-
expect(si).toBeDefined();
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
expect(hasSceneInstanceChild).toBe(true);
|
|
98
|
+
catalog(registry, PARENT_GUID, parent);
|
|
99
|
+
const result = registry.instantiate(register(world, parent), world);
|
|
100
|
+
expect(result.ok).toBe(true);
|
|
101
|
+
if (!result.ok) return;
|
|
102
|
+
expect(world.get(result.value, SceneInstance).ok).toBe(true);
|
|
103
|
+
const collected = rootsToSceneAsset(registry, world, [result.value]);
|
|
104
|
+
expect(collected.ok).toBe(true);
|
|
105
|
+
if (!collected.ok) return;
|
|
106
|
+
expect(Object.keys(collected.value.entities)).toContain('house');
|
|
107
|
+
expect(collected.value.entities.house?.instance?.source).toBe(CHILD_GUID);
|
|
108
|
+
expect(collected.value.entities.house?.instance?.overrides).toBeUndefined();
|
|
429
109
|
});
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
433
|
-
// m3-t3: cyclic mount fail-fast
|
|
434
|
-
// ═══════════════════════════════════════════════════════════════════════════════
|
|
435
|
-
|
|
436
|
-
describe('m3-t3 — cyclic mount fail-fast', () => {
|
|
437
|
-
it('A mount B, B mount A surfaces pack-cyclic-reference / mount-asset', () => {
|
|
438
|
-
const reg = mkReg();
|
|
439
|
-
const w = new World();
|
|
440
|
-
|
|
441
|
-
// Scene A: mounts scene B (source=G2).
|
|
442
|
-
const sceneA: SceneAsset = {
|
|
443
|
-
kind: 'scene',
|
|
444
|
-
entities: [{ localId: 0 as never, components: { Transform: {} } }],
|
|
445
|
-
mounts: [
|
|
446
|
-
{
|
|
447
|
-
localId: 1 as never,
|
|
448
|
-
source: G2,
|
|
449
|
-
memberFirst: 2 as never,
|
|
450
|
-
memberCount: 1,
|
|
451
|
-
},
|
|
452
|
-
],
|
|
453
|
-
};
|
|
454
|
-
cat(reg, G1, sceneA);
|
|
455
|
-
const ha = rs(w, sceneA);
|
|
456
110
|
|
|
457
|
-
|
|
458
|
-
const
|
|
111
|
+
it('keeps nested declarations isolated across two instances', () => {
|
|
112
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
113
|
+
const world = new World();
|
|
114
|
+
catalog(registry, CHILD_GUID, childScene());
|
|
115
|
+
const parent: SceneAsset = {
|
|
459
116
|
kind: 'scene',
|
|
460
|
-
entities:
|
|
461
|
-
|
|
462
|
-
{
|
|
463
|
-
|
|
464
|
-
source: G1,
|
|
465
|
-
memberFirst: 2 as never,
|
|
466
|
-
memberCount: 1,
|
|
467
|
-
},
|
|
468
|
-
],
|
|
469
|
-
};
|
|
470
|
-
cat(reg, G2, sceneB);
|
|
471
|
-
const hb = rs(w, sceneB);
|
|
472
|
-
|
|
473
|
-
// Wire resolver: A -> B, B -> A.
|
|
474
|
-
swr(w, ha, hb);
|
|
475
|
-
swr(w, hb, ha);
|
|
476
|
-
|
|
477
|
-
// Attempt instantiate via registry — should fail with cycle detection.
|
|
478
|
-
const inst = reg.instantiate(ha, w);
|
|
479
|
-
expect(inst.ok).toBe(false);
|
|
480
|
-
if (inst.ok) return;
|
|
481
|
-
|
|
482
|
-
// Check error shape: must be pack-cyclic-reference with kind='mount-asset'.
|
|
483
|
-
const e = inst.error as {
|
|
484
|
-
code: string;
|
|
485
|
-
detail?: { kind?: string; cycle?: readonly string[] };
|
|
117
|
+
entities: {
|
|
118
|
+
left: { components: {}, instance: { source: CHILD_GUID } },
|
|
119
|
+
right: { components: {}, instance: { source: CHILD_GUID } },
|
|
120
|
+
},
|
|
486
121
|
};
|
|
487
|
-
|
|
122
|
+
const result = registry.instantiate(register(world, parent), world);
|
|
123
|
+
expect(result.ok).toBe(true);
|
|
124
|
+
if (!result.ok) return;
|
|
125
|
+
const nestedRoots = [...world.iterDescendants(result.value)].filter(
|
|
126
|
+
(entity) => world.get(entity, SceneInstance).ok,
|
|
127
|
+
);
|
|
128
|
+
expect(nestedRoots).toHaveLength(2);
|
|
488
129
|
});
|
|
489
130
|
|
|
490
|
-
it('
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
const sceneA: SceneAsset = {
|
|
495
|
-
kind: 'scene',
|
|
496
|
-
entities: [],
|
|
497
|
-
mounts: [{ localId: 0 as never, source: G2, memberFirst: 1 as never, memberCount: 0 }],
|
|
498
|
-
};
|
|
499
|
-
cat(reg, G1, sceneA);
|
|
500
|
-
const ha = rs(w, sceneA);
|
|
501
|
-
|
|
502
|
-
const sceneB: SceneAsset = {
|
|
131
|
+
it('rejects keyed recursive instances before a root is published', () => {
|
|
132
|
+
const registry = new AssetRegistry(makeMockShaderRegistry());
|
|
133
|
+
const world = new World();
|
|
134
|
+
const a: SceneAsset = {
|
|
503
135
|
kind: 'scene',
|
|
504
|
-
entities:
|
|
505
|
-
mounts: [{ localId: 0 as never, source: G3, memberFirst: 1 as never, memberCount: 0 }],
|
|
136
|
+
entities: { b: { components: {}, instance: { source: CHILD_GUID } } },
|
|
506
137
|
};
|
|
507
|
-
|
|
508
|
-
const hb = rs(w, sceneB);
|
|
509
|
-
|
|
510
|
-
const sceneC: SceneAsset = {
|
|
138
|
+
const b: SceneAsset = {
|
|
511
139
|
kind: 'scene',
|
|
512
|
-
entities:
|
|
513
|
-
mounts: [{ localId: 0 as never, source: G1, memberFirst: 1 as never, memberCount: 0 }],
|
|
514
|
-
};
|
|
515
|
-
cat(reg, G3, sceneC);
|
|
516
|
-
const hc = rs(w, sceneC);
|
|
517
|
-
|
|
518
|
-
// Wire: A->B, B->C, C->A.
|
|
519
|
-
swr(w, ha, hb);
|
|
520
|
-
swr(w, hb, hc);
|
|
521
|
-
swr(w, hc, ha);
|
|
522
|
-
|
|
523
|
-
const inst = reg.instantiate(ha, w);
|
|
524
|
-
expect(inst.ok).toBe(false);
|
|
525
|
-
if (inst.ok) return;
|
|
526
|
-
|
|
527
|
-
const e = inst.error as {
|
|
528
|
-
code: string;
|
|
529
|
-
detail?: { kind?: string; cycle?: readonly string[] };
|
|
140
|
+
entities: { a: { components: {}, instance: { source: PARENT_GUID } } },
|
|
530
141
|
};
|
|
531
|
-
|
|
142
|
+
catalog(registry, CHILD_GUID, b);
|
|
143
|
+
catalog(registry, PARENT_GUID, a);
|
|
144
|
+
const before = world.inspect().entityCount;
|
|
145
|
+
const result = registry.instantiate(register(world, a), world);
|
|
146
|
+
expect(result.ok).toBe(false);
|
|
147
|
+
expect(world.inspect().entityCount).toBe(before);
|
|
532
148
|
});
|
|
533
149
|
});
|