@forgeax/engine-scene 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 +50 -0
- package/dist/__tests__/keyed-scene.integration.test.d.ts +2 -0
- package/dist/__tests__/keyed-scene.integration.test.d.ts.map +1 -0
- package/dist/assets/scene-decoder.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +848 -229
- package/dist/index.mjs.map +1 -1
- package/dist/instances/binding.d.ts +7 -5
- package/dist/instances/binding.d.ts.map +1 -1
- package/dist/instances/externalization.d.ts +1 -1
- package/dist/instances/externalization.d.ts.map +1 -1
- package/dist/instances/keyed.d.ts +38 -0
- package/dist/instances/keyed.d.ts.map +1 -0
- package/dist/instances/legacy.d.ts +20 -0
- package/dist/instances/legacy.d.ts.map +1 -0
- package/dist/instances/runtime-types.d.ts +20 -0
- package/dist/instances/runtime-types.d.ts.map +1 -0
- package/dist/instances/scene-instances.d.ts +27 -37
- package/dist/instances/scene-instances.d.ts.map +1 -1
- package/dist/instances/state.d.ts +6 -1
- package/dist/instances/state.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/__tests__/asset-owner.integration.test.ts +4 -7
- package/src/__tests__/flat-propagation.perf.test.ts +18 -17
- package/src/__tests__/keyed-scene.integration.test.ts +269 -0
- package/src/__tests__/scene-binding.integration.test.ts +141 -32
- package/src/__tests__/structural.test.ts +4 -1
- package/src/assets/scene-decoder.ts +123 -33
- package/src/collect-subtree.ts +2 -2
- package/src/index.ts +10 -1
- package/src/instances/binding.ts +26 -19
- package/src/instances/externalization.ts +115 -97
- package/src/instances/keyed.ts +505 -0
- package/src/instances/legacy.ts +108 -0
- package/src/instances/runtime-types.ts +21 -0
- package/src/instances/scene-instances.ts +220 -83
- package/src/instances/state.ts +6 -1
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { defineComponent, World } from '@forgeax/engine-ecs';
|
|
2
|
+
import { err, type Handle, ok, type SceneAsset } from '@forgeax/engine-types';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
import { ChildOf, Children, compileKeyedSceneAsset, type KeyedSceneCompileContext } from '../index';
|
|
5
|
+
|
|
6
|
+
const Marker = defineComponent('KeyedSceneMarker', { value: 'i32' });
|
|
7
|
+
const DirectionalLight = defineComponent('DirectionalLight', { shadowFilter: 'f32' });
|
|
8
|
+
|
|
9
|
+
function register(world: World): void {
|
|
10
|
+
for (const component of [ChildOf, Children, Marker, DirectionalLight])
|
|
11
|
+
world.components.register(component).unwrap();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function compile(
|
|
15
|
+
world: World,
|
|
16
|
+
handle: Handle<'SceneAsset', 'shared'>,
|
|
17
|
+
asset: SceneAsset,
|
|
18
|
+
assets: ReadonlyMap<number, SceneAsset>,
|
|
19
|
+
handles: ReadonlyMap<string, Handle<'SceneAsset', 'shared'>>,
|
|
20
|
+
): ReturnType<typeof compileKeyedSceneAsset> {
|
|
21
|
+
const context: KeyedSceneCompileContext = {
|
|
22
|
+
resolveSource: (source, _parent) => {
|
|
23
|
+
const child = handles.get(source);
|
|
24
|
+
return child === undefined ? err({ code: 'missing-source' }) : ok(child);
|
|
25
|
+
},
|
|
26
|
+
resolveAsset: (child) => {
|
|
27
|
+
const value = assets.get(Number(child));
|
|
28
|
+
return value === undefined ? err({ code: 'missing-asset' }) : ok(value);
|
|
29
|
+
},
|
|
30
|
+
stack: new Set([Number(handle)]),
|
|
31
|
+
};
|
|
32
|
+
return compileKeyedSceneAsset(world, handle, asset, context);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('keyed SceneAsset compiler', () => {
|
|
36
|
+
it('sorts keys, resolves same-scene and nested addresses, and derives private slots', () => {
|
|
37
|
+
const world = new World();
|
|
38
|
+
register(world);
|
|
39
|
+
const child: SceneAsset = {
|
|
40
|
+
kind: 'scene',
|
|
41
|
+
entities: {
|
|
42
|
+
'door/slash': { components: { KeyedSceneMarker: { value: 2 } } },
|
|
43
|
+
leaf: { components: { KeyedSceneMarker: { value: 3 } } },
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const parent: SceneAsset = {
|
|
47
|
+
kind: 'scene',
|
|
48
|
+
entities: {
|
|
49
|
+
z: {
|
|
50
|
+
components: {
|
|
51
|
+
KeyedSceneMarker: { value: 1 },
|
|
52
|
+
ChildOf: { parent: 'root' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
instance: {
|
|
56
|
+
components: { ChildOf: { parent: 'root' } },
|
|
57
|
+
instance: {
|
|
58
|
+
source: 'child-guid',
|
|
59
|
+
overrides: [{ target: ['door/slash'], components: { KeyedSceneMarker: { value: 9 } } }],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
root: {
|
|
63
|
+
components: {
|
|
64
|
+
KeyedSceneMarker: { value: 0 },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const childHandle = world.allocSharedRef('SceneAsset', child);
|
|
70
|
+
const parentHandle = world.allocSharedRef('SceneAsset', parent);
|
|
71
|
+
const result = compile(
|
|
72
|
+
world,
|
|
73
|
+
parentHandle,
|
|
74
|
+
parent,
|
|
75
|
+
new Map([
|
|
76
|
+
[Number(childHandle), child],
|
|
77
|
+
[Number(parentHandle), parent],
|
|
78
|
+
]),
|
|
79
|
+
new Map([['child-guid', childHandle]]),
|
|
80
|
+
);
|
|
81
|
+
expect(result.ok).toBe(true);
|
|
82
|
+
if (!result.ok) return;
|
|
83
|
+
expect([...result.value.keyByLocalId.values()]).toEqual(['root', 'z', 'instance']);
|
|
84
|
+
expect(result.value.resolveAddress('root')).toBe(0);
|
|
85
|
+
expect(result.value.resolveAddress(['instance', 'door/slash'])).toBe(3);
|
|
86
|
+
expect(result.value.asset.mounts?.[0]?.memberCount).toBe(2);
|
|
87
|
+
expect(result.value.asset.mounts?.[0]?.overrides?.[0]?.localId).toBe(3);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects hierarchy cycles before producing a runtime projection', () => {
|
|
91
|
+
const world = new World();
|
|
92
|
+
register(world);
|
|
93
|
+
const handle = world.allocSharedRef('SceneAsset', { kind: 'scene', entities: {} });
|
|
94
|
+
const asset: SceneAsset = {
|
|
95
|
+
kind: 'scene',
|
|
96
|
+
entities: {
|
|
97
|
+
a: { components: { ChildOf: { parent: 'b' } } },
|
|
98
|
+
b: { components: { ChildOf: { parent: 'a' } } },
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const result = compile(world, handle, asset, new Map([[Number(handle), asset]]), new Map());
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
if (result.ok) return;
|
|
104
|
+
expect(result.error).toMatchObject({ detail: { reason: 'hierarchy cycle' } });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('rejects hierarchy cycles that cross an instance carrier', () => {
|
|
108
|
+
const world = new World();
|
|
109
|
+
register(world);
|
|
110
|
+
const child: SceneAsset = {
|
|
111
|
+
kind: 'scene',
|
|
112
|
+
entities: { leaf: { components: {} } },
|
|
113
|
+
};
|
|
114
|
+
const parent: SceneAsset = {
|
|
115
|
+
kind: 'scene',
|
|
116
|
+
entities: {
|
|
117
|
+
root: { components: { ChildOf: { parent: 'instance' } } },
|
|
118
|
+
instance: {
|
|
119
|
+
components: { ChildOf: { parent: 'root' } },
|
|
120
|
+
instance: { source: 'child-guid' },
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const childHandle = world.allocSharedRef('SceneAsset', child);
|
|
125
|
+
const parentHandle = world.allocSharedRef('SceneAsset', parent);
|
|
126
|
+
const result = compile(
|
|
127
|
+
world,
|
|
128
|
+
parentHandle,
|
|
129
|
+
parent,
|
|
130
|
+
new Map([
|
|
131
|
+
[Number(childHandle), child],
|
|
132
|
+
[Number(parentHandle), parent],
|
|
133
|
+
]),
|
|
134
|
+
new Map([['child-guid', childHandle]]),
|
|
135
|
+
);
|
|
136
|
+
expect(result).toMatchObject({
|
|
137
|
+
ok: false,
|
|
138
|
+
error: { detail: { reason: 'hierarchy cycle' } },
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('rejects a carrier parented to a root in its own child scene', () => {
|
|
143
|
+
const world = new World();
|
|
144
|
+
register(world);
|
|
145
|
+
const child: SceneAsset = {
|
|
146
|
+
kind: 'scene',
|
|
147
|
+
entities: {
|
|
148
|
+
root: { components: {} },
|
|
149
|
+
door: { components: { ChildOf: { parent: 'root' } } },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const parent: SceneAsset = {
|
|
153
|
+
kind: 'scene',
|
|
154
|
+
entities: {
|
|
155
|
+
outer: {
|
|
156
|
+
components: { ChildOf: { parent: ['outer', 'door'] } },
|
|
157
|
+
instance: { source: 'child-guid' },
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const childHandle = world.allocSharedRef('SceneAsset', child);
|
|
162
|
+
const parentHandle = world.allocSharedRef('SceneAsset', parent);
|
|
163
|
+
const result = compile(
|
|
164
|
+
world,
|
|
165
|
+
parentHandle,
|
|
166
|
+
parent,
|
|
167
|
+
new Map([
|
|
168
|
+
[Number(childHandle), child],
|
|
169
|
+
[Number(parentHandle), parent],
|
|
170
|
+
]),
|
|
171
|
+
new Map([['child-guid', childHandle]]),
|
|
172
|
+
);
|
|
173
|
+
expect(result).toMatchObject({
|
|
174
|
+
ok: false,
|
|
175
|
+
error: { detail: { reason: 'hierarchy cycle' } },
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('keeps every segment of a deeply nested address', () => {
|
|
180
|
+
const world = new World();
|
|
181
|
+
register(world);
|
|
182
|
+
const leaf: SceneAsset = { kind: 'scene', entities: { door: { components: {} } } };
|
|
183
|
+
const inside: SceneAsset = {
|
|
184
|
+
kind: 'scene',
|
|
185
|
+
entities: { inside: { components: {}, instance: { source: 'leaf-guid' } } },
|
|
186
|
+
};
|
|
187
|
+
const outer: SceneAsset = {
|
|
188
|
+
kind: 'scene',
|
|
189
|
+
entities: { outer: { components: {}, instance: { source: 'inside-guid' } } },
|
|
190
|
+
};
|
|
191
|
+
const leafHandle = world.allocSharedRef('SceneAsset', leaf);
|
|
192
|
+
const insideHandle = world.allocSharedRef('SceneAsset', inside);
|
|
193
|
+
const outerHandle = world.allocSharedRef('SceneAsset', outer);
|
|
194
|
+
const result = compile(
|
|
195
|
+
world,
|
|
196
|
+
outerHandle,
|
|
197
|
+
outer,
|
|
198
|
+
new Map([
|
|
199
|
+
[Number(leafHandle), leaf],
|
|
200
|
+
[Number(insideHandle), inside],
|
|
201
|
+
[Number(outerHandle), outer],
|
|
202
|
+
]),
|
|
203
|
+
new Map([
|
|
204
|
+
['leaf-guid', leafHandle],
|
|
205
|
+
['inside-guid', insideHandle],
|
|
206
|
+
]),
|
|
207
|
+
);
|
|
208
|
+
expect(result.ok).toBe(true);
|
|
209
|
+
if (!result.ok) return;
|
|
210
|
+
expect(result.value.resolveAddress(['outer', 'inside', 'door'])).toBe(2);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('rejects unknown component fields before runtime projection', () => {
|
|
214
|
+
const world = new World();
|
|
215
|
+
register(world);
|
|
216
|
+
const asset: SceneAsset = {
|
|
217
|
+
kind: 'scene',
|
|
218
|
+
entities: { root: { components: { KeyedSceneMarker: { typo: 1 } } } },
|
|
219
|
+
};
|
|
220
|
+
const handle = world.allocSharedRef('SceneAsset', asset);
|
|
221
|
+
const result = compile(world, handle, asset, new Map([[Number(handle), asset]]), new Map());
|
|
222
|
+
expect(result).toMatchObject({
|
|
223
|
+
ok: false,
|
|
224
|
+
error: {
|
|
225
|
+
detail: {
|
|
226
|
+
reason: 'unknown component field',
|
|
227
|
+
component: 'KeyedSceneMarker',
|
|
228
|
+
field: 'typo',
|
|
229
|
+
entity: 'root',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('migrates numeric 0.1.27 entity addresses and pcfKernelSize at compile time', () => {
|
|
236
|
+
const world = new World();
|
|
237
|
+
register(world);
|
|
238
|
+
const asset = {
|
|
239
|
+
kind: 'scene',
|
|
240
|
+
entities: {
|
|
241
|
+
'0': { components: { KeyedSceneMarker: { value: 0 } } },
|
|
242
|
+
'1': {
|
|
243
|
+
components: {
|
|
244
|
+
ChildOf: { parent: 0 },
|
|
245
|
+
DirectionalLight: { pcfKernelSize: 3 },
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
} as unknown as SceneAsset;
|
|
250
|
+
const handle = world.allocSharedRef('SceneAsset', asset);
|
|
251
|
+
const result = compile(world, handle, asset, new Map([[Number(handle), asset]]), new Map());
|
|
252
|
+
expect(result).toMatchObject({
|
|
253
|
+
ok: true,
|
|
254
|
+
value: {
|
|
255
|
+
asset: {
|
|
256
|
+
entities: [
|
|
257
|
+
{ components: { KeyedSceneMarker: { value: 0 } } },
|
|
258
|
+
{
|
|
259
|
+
components: {
|
|
260
|
+
ChildOf: { parent: 0 },
|
|
261
|
+
DirectionalLight: { shadowFilter: 2 },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
});
|
|
@@ -5,61 +5,80 @@ import {
|
|
|
5
5
|
resolveSceneEntity,
|
|
6
6
|
type SceneEntityRef,
|
|
7
7
|
sceneEntity,
|
|
8
|
-
|
|
8
|
+
sceneEntityAddressKey,
|
|
9
|
+
validateSceneEntityKeys,
|
|
9
10
|
} from '../instances/binding.js';
|
|
10
11
|
import { externalizeSceneAsset } from '../instances/externalization.js';
|
|
11
12
|
|
|
12
|
-
describe('SceneEntityRef instance
|
|
13
|
-
|
|
13
|
+
describe('SceneEntityRef instance address', () => {
|
|
14
|
+
const bindings = (address: SceneEntityRef['address'], value: number) =>
|
|
15
|
+
new Map([[sceneEntityAddressKey(address), value]]);
|
|
16
|
+
|
|
17
|
+
it('resolves by scene source and authored entity address', () => {
|
|
14
18
|
const ref: SceneEntityRef = sceneEntity('level/main', 'player');
|
|
15
19
|
const first = resolveSceneEntity(ref, {
|
|
16
20
|
sceneSourceKey: 'level/main',
|
|
17
|
-
bindings:
|
|
21
|
+
bindings: bindings('player', 11),
|
|
18
22
|
});
|
|
19
23
|
const second = resolveSceneEntity(ref, {
|
|
20
24
|
sceneSourceKey: 'level/main',
|
|
21
|
-
bindings:
|
|
25
|
+
bindings: bindings('player', 22),
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
expect(first).toEqual({ ok: true, value: 11 });
|
|
25
29
|
expect(second).toEqual({ ok: true, value: 22 });
|
|
26
30
|
});
|
|
27
31
|
|
|
28
|
-
it('rejects a missing or cross-instance
|
|
32
|
+
it('rejects a missing or cross-instance address with a closed error', () => {
|
|
29
33
|
const result = resolveSceneEntity(sceneEntity('level/main', 'camera'), {
|
|
30
34
|
sceneSourceKey: 'level/main',
|
|
31
|
-
bindings:
|
|
35
|
+
bindings: bindings('player', 11),
|
|
32
36
|
});
|
|
33
37
|
|
|
34
38
|
expect(result).toMatchObject({
|
|
35
39
|
ok: false,
|
|
36
40
|
error: {
|
|
37
41
|
code: 'scene-binding-missing',
|
|
38
|
-
expected: expect.stringContaining('
|
|
42
|
+
expected: expect.stringContaining('entity key'),
|
|
39
43
|
hint: expect.stringContaining('declare'),
|
|
40
|
-
detail: { sceneSourceKey: 'level/main',
|
|
44
|
+
detail: { sceneSourceKey: 'level/main', address: 'camera' },
|
|
41
45
|
},
|
|
42
46
|
});
|
|
43
47
|
});
|
|
44
48
|
|
|
45
49
|
it('rejects a reference from another concrete SceneInstance', () => {
|
|
46
|
-
const
|
|
47
|
-
const result = resolveSceneEntity(ref, {
|
|
50
|
+
const result = resolveSceneEntity(sceneEntity('level/other', 'player'), {
|
|
48
51
|
sceneSourceKey: 'level/main',
|
|
49
|
-
bindings:
|
|
52
|
+
bindings: bindings('player', 11),
|
|
50
53
|
});
|
|
51
54
|
|
|
52
55
|
expect(result).toMatchObject({
|
|
53
56
|
ok: false,
|
|
54
57
|
error: {
|
|
55
58
|
code: 'scene-binding-wrong-instance',
|
|
56
|
-
detail: { sceneSourceKey: 'level/other',
|
|
59
|
+
detail: { sceneSourceKey: 'level/other', address: 'player' },
|
|
57
60
|
},
|
|
58
61
|
});
|
|
59
62
|
});
|
|
60
63
|
|
|
61
|
-
it('
|
|
62
|
-
const
|
|
64
|
+
it('requires an explicit anonymous source key for direct POD scenes', () => {
|
|
65
|
+
const anonymous = resolveSceneEntity(sceneEntity('', 'player'), {
|
|
66
|
+
sceneSourceKey: '',
|
|
67
|
+
bindings: bindings('player', 11),
|
|
68
|
+
});
|
|
69
|
+
const fabricated = resolveSceneEntity(sceneEntity('unrelated/source', 'player'), {
|
|
70
|
+
sceneSourceKey: '',
|
|
71
|
+
bindings: bindings('player', 11),
|
|
72
|
+
});
|
|
73
|
+
expect(anonymous).toEqual({ ok: true, value: 11 });
|
|
74
|
+
expect(fabricated).toMatchObject({
|
|
75
|
+
ok: false,
|
|
76
|
+
error: { code: 'scene-binding-wrong-instance' },
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('rejects duplicate authored entity keys before publication', () => {
|
|
81
|
+
const result = validateSceneEntityKeys('level/main', ['player', 'player']);
|
|
63
82
|
|
|
64
83
|
expect(result).toMatchObject({
|
|
65
84
|
ok: false,
|
|
@@ -67,31 +86,57 @@ describe('SceneEntityRef instance binding', () => {
|
|
|
67
86
|
code: 'scene-binding-duplicate',
|
|
68
87
|
expected: expect.stringContaining('unique'),
|
|
69
88
|
hint: expect.stringContaining('rename'),
|
|
70
|
-
detail: { sceneSourceKey: 'level/main',
|
|
89
|
+
detail: { sceneSourceKey: 'level/main', address: 'player' },
|
|
71
90
|
},
|
|
72
91
|
});
|
|
73
92
|
});
|
|
74
93
|
|
|
75
|
-
it('
|
|
76
|
-
const ref = sceneEntity('level/main', '
|
|
77
|
-
|
|
94
|
+
it('supports nested addresses without using display names or paths', () => {
|
|
95
|
+
const ref = sceneEntity('level/main', ['house', 'door']);
|
|
96
|
+
expect(ref).toEqual({ sceneSourceKey: 'level/main', address: ['house', 'door'] });
|
|
97
|
+
expect(ref).not.toHaveProperty('name');
|
|
98
|
+
expect(ref).not.toHaveProperty('path');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('treats a one-segment address array as the same direct key', () => {
|
|
102
|
+
const direct = resolveSceneEntity(sceneEntity('level/main', 'player'), {
|
|
103
|
+
sceneSourceKey: 'level/main',
|
|
104
|
+
bindings: bindings('player', 11),
|
|
105
|
+
});
|
|
106
|
+
const array = resolveSceneEntity(sceneEntity('level/main', ['player']), {
|
|
78
107
|
sceneSourceKey: 'level/main',
|
|
79
|
-
bindings:
|
|
108
|
+
bindings: bindings(['player'], 11),
|
|
80
109
|
});
|
|
110
|
+
expect(direct).toEqual({ ok: true, value: 11 });
|
|
111
|
+
expect(array).toEqual(direct);
|
|
112
|
+
});
|
|
81
113
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
114
|
+
it('keeps literal and nested addresses distinct', () => {
|
|
115
|
+
const literal = '["house","door"]';
|
|
116
|
+
const nested = ['house', 'door'] as const;
|
|
117
|
+
const map = new Map([
|
|
118
|
+
[sceneEntityAddressKey(literal), 11],
|
|
119
|
+
[sceneEntityAddressKey(nested), 22],
|
|
120
|
+
]);
|
|
121
|
+
const literalResult = resolveSceneEntity(sceneEntity('level/main', literal), {
|
|
122
|
+
sceneSourceKey: 'level/main',
|
|
123
|
+
bindings: map,
|
|
124
|
+
});
|
|
125
|
+
const nestedResult = resolveSceneEntity(sceneEntity('level/main', nested), {
|
|
126
|
+
sceneSourceKey: 'level/main',
|
|
127
|
+
bindings: map,
|
|
128
|
+
});
|
|
129
|
+
expect(literalResult).toEqual({ ok: true, value: 11 });
|
|
130
|
+
expect(nestedResult).toEqual({ ok: true, value: 22 });
|
|
85
131
|
});
|
|
86
132
|
|
|
87
|
-
it('preserves scene
|
|
133
|
+
it('preserves keyed scene data through decode and externalization', async () => {
|
|
88
134
|
const envelope = {
|
|
89
135
|
guid: '00000000-0000-0000-0000-000000000021',
|
|
90
136
|
kind: 'scene',
|
|
91
137
|
payload: {
|
|
92
138
|
kind: 'scene' as const,
|
|
93
|
-
|
|
94
|
-
entities: [{ localId: 0, bindingKey: 'player', components: {} }],
|
|
139
|
+
entities: { player: { components: {} } },
|
|
95
140
|
},
|
|
96
141
|
refs: [],
|
|
97
142
|
artifacts: {},
|
|
@@ -103,22 +148,86 @@ describe('SceneEntityRef instance binding', () => {
|
|
|
103
148
|
});
|
|
104
149
|
expect(decoded).toMatchObject({
|
|
105
150
|
ok: true,
|
|
106
|
-
value: {
|
|
107
|
-
sourceKey: 'world/main',
|
|
108
|
-
entities: [{ bindingKey: 'player' }],
|
|
109
|
-
},
|
|
151
|
+
value: { entities: { player: { components: {} } } },
|
|
110
152
|
});
|
|
111
153
|
if (!decoded.ok) return;
|
|
112
154
|
|
|
113
155
|
const externalized = externalizeSceneAsset(decoded.value, () => ({}));
|
|
156
|
+
expect(externalized).toMatchObject({
|
|
157
|
+
ok: true,
|
|
158
|
+
value: { payload: { entities: { player: { components: {} } } } },
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('keeps legacy binding keys when decoding an array SceneAsset', async () => {
|
|
163
|
+
const envelope = {
|
|
164
|
+
guid: '00000000-0000-0000-0000-000000000022',
|
|
165
|
+
kind: 'scene',
|
|
166
|
+
payload: {
|
|
167
|
+
kind: 'scene' as const,
|
|
168
|
+
entities: [
|
|
169
|
+
{ localId: 0, bindingKey: 'root', components: {} },
|
|
170
|
+
{ localId: 1, bindingKey: 'player', components: { ChildOf: { parent: 0 } } },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
refs: [],
|
|
174
|
+
artifacts: {},
|
|
175
|
+
} as unknown as Parameters<typeof sceneAssetDecoder.decode>[0]['envelope'];
|
|
176
|
+
const decoded = await sceneAssetDecoder.decode({
|
|
177
|
+
envelope,
|
|
178
|
+
artifacts: { read: async () => ok(new Uint8Array()) },
|
|
179
|
+
signal: new AbortController().signal,
|
|
180
|
+
});
|
|
181
|
+
expect(decoded).toMatchObject({
|
|
182
|
+
ok: true,
|
|
183
|
+
value: {
|
|
184
|
+
entities: {
|
|
185
|
+
root: { components: {} },
|
|
186
|
+
player: { components: { ChildOf: { parent: 'root' } } },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('migrates the 0.1.27 numeric template fields at the externalization boundary', () => {
|
|
193
|
+
const legacy = {
|
|
194
|
+
kind: 'scene',
|
|
195
|
+
entities: [
|
|
196
|
+
{ localId: 0, components: { Name: { value: 'Root' } } },
|
|
197
|
+
{
|
|
198
|
+
localId: 1,
|
|
199
|
+
components: {
|
|
200
|
+
ChildOf: { parent: 0 },
|
|
201
|
+
DirectionalLight: { pcfKernelSize: 3 },
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
} as unknown as import('@forgeax/engine-types').SceneAsset;
|
|
206
|
+
const externalized = externalizeSceneAsset(legacy, (componentName) => {
|
|
207
|
+
if (componentName === 'ChildOf') return { parent: 'entity' };
|
|
208
|
+
if (componentName === 'DirectionalLight') {
|
|
209
|
+
return { pcfKernelSize: 'f32', shadowFilter: 'f32' };
|
|
210
|
+
}
|
|
211
|
+
return {};
|
|
212
|
+
});
|
|
114
213
|
expect(externalized).toMatchObject({
|
|
115
214
|
ok: true,
|
|
116
215
|
value: {
|
|
117
216
|
payload: {
|
|
118
|
-
|
|
119
|
-
|
|
217
|
+
entities: {
|
|
218
|
+
'1': {
|
|
219
|
+
components: {
|
|
220
|
+
ChildOf: { parent: '0' },
|
|
221
|
+
DirectionalLight: { shadowFilter: 2 },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
120
225
|
},
|
|
121
226
|
},
|
|
122
227
|
});
|
|
228
|
+
if (!externalized.ok) return;
|
|
229
|
+
expect(externalized.value.payload.entities).not.toHaveProperty(
|
|
230
|
+
'1.components.DirectionalLight.pcfKernelSize',
|
|
231
|
+
);
|
|
123
232
|
});
|
|
124
233
|
});
|
|
@@ -15,7 +15,10 @@ describe('SceneInstance structure characterization', () => {
|
|
|
15
15
|
hasWorldStateWeakMap: source.includes('const sceneWorldStates = new WeakMap'),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
// The keyed authoring compiler lives in keyed.ts; this is the post-migration
|
|
19
|
+
// runtime projection baseline, with a small guard against re-growing the
|
|
20
|
+
// former numeric/mount implementation.
|
|
21
|
+
expect(snapshot.lines).toBeLessThan(1700);
|
|
19
22
|
expect(snapshot.exportedFunctions).toBe(27);
|
|
20
23
|
expect(snapshot.hasStatePayload).toBe(false);
|
|
21
24
|
expect(snapshot.hasWorldStateWeakMap).toBe(false);
|