@forgeax/engine-runtime 0.1.27 → 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.
Files changed (114) hide show
  1. package/README.md +98 -0
  2. package/dist/collect-scene-asset.d.ts.map +1 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.mjs +480 -15
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/scene-utils/mount-override-fold.d.ts +3 -1
  8. package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
  9. package/package.json +37 -30
  10. package/src/__tests__/animation-graph-collect-roundtrip.test.ts +4 -2
  11. package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +5 -4
  12. package/src/__tests__/asset-registry-guid-reverse.test.ts +22 -27
  13. package/src/__tests__/asset-registry-mounts.test.ts +174 -734
  14. package/src/__tests__/asset-registry-scene.cases.unit.test.ts +9 -18
  15. package/src/__tests__/asset-registry.recursive.spec.ts +23 -29
  16. package/src/__tests__/camera-ortho.cases.unit.test.ts +12 -3
  17. package/src/__tests__/collect-scene-asset.test.ts +96 -113
  18. package/src/__tests__/collect-scene-mount-collapse.test.ts +64 -66
  19. package/src/__tests__/collect-transient-children-absent.test.ts +11 -17
  20. package/src/__tests__/collect-transient-roundtrip.test.ts +12 -13
  21. package/src/__tests__/collect-transient-skip.test.ts +20 -27
  22. package/src/__tests__/components.test-d.ts +17 -1
  23. package/src/__tests__/components.unit.test.ts +75 -27
  24. package/src/__tests__/composite-skybox-cross-world.browser.test.ts +4 -2
  25. package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +8 -4
  26. package/src/__tests__/dawn/instances-shadow.dawn.test.ts +4 -1
  27. package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +66 -92
  28. package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +200 -4
  29. package/src/__tests__/errors.unit.test.ts +13 -0
  30. package/src/__tests__/feature-depth-material.dawn.test.ts +23 -0
  31. package/src/__tests__/feature-depth-material.fixture.ts +473 -0
  32. package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +14 -14
  33. package/src/__tests__/geometry.unit.test.ts +42 -43
  34. package/src/__tests__/gpu-resource-release.cases.unit.test.ts +16 -9
  35. package/src/__tests__/handle-migration.test.ts +0 -1
  36. package/src/__tests__/instances-renderer-lifecycle.integration.test.ts +20 -14
  37. package/src/__tests__/instances.test-d.ts +13 -52
  38. package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +42 -80
  39. package/src/__tests__/lazy-catalog.cases.unit.test.ts +17 -3
  40. package/src/__tests__/loader-registry.cases.unit.test.ts +4 -4
  41. package/src/__tests__/material-publication.browser.test.ts +29 -0
  42. package/src/__tests__/material-publication.commands.ts +23 -0
  43. package/src/__tests__/material-publication.dawn.test.ts +24 -0
  44. package/src/__tests__/material-publication.fixture.ts +349 -0
  45. package/src/__tests__/material-publication.server.ts +109 -0
  46. package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +73 -96
  47. package/src/__tests__/pbr-pipeline.unit.test.ts +25 -11
  48. package/src/__tests__/point-light-shadow.browser.test.ts +8 -2
  49. package/src/__tests__/point-light-shadow.unit.test.ts +37 -8
  50. package/src/__tests__/record-all-topology.cases.unit.test.ts +52 -0
  51. package/src/__tests__/record-draw-branch.cases.unit.test.ts +1 -0
  52. package/src/__tests__/record-worldid-isolation.test.ts +47 -28
  53. package/src/__tests__/render-error-exhaustive.test-d.ts +24 -0
  54. package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +15 -4
  55. package/src/__tests__/render-system-extract.test.ts +61 -0
  56. package/src/__tests__/render-system-extract.unit.test.ts +21 -7
  57. package/src/__tests__/render-system-fold-bucket.unit.test.ts +33 -1
  58. package/src/__tests__/render-system-fold-material.unit.test.ts +32 -6
  59. package/src/__tests__/render-system-fold-modegate.unit.test.ts +26 -4
  60. package/src/__tests__/render-system-fold.dawn.test.ts +4 -1
  61. package/src/__tests__/render-system-mega.test.ts +50 -62
  62. package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +0 -1
  63. package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +0 -3
  64. package/src/__tests__/renderer-draw-world.cases.unit.test.ts +16 -513
  65. package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +48 -43
  66. package/src/__tests__/rhi-null-command-flow.unit.test.ts +9 -7
  67. package/src/__tests__/roots-to-scene-asset.test.ts +27 -28
  68. package/src/__tests__/roundtrip-primitive.test.ts +24 -23
  69. package/src/__tests__/scene-equirect-roundtrip.test.ts +5 -16
  70. package/src/__tests__/scene-instantiate-cycle-runtime.test.ts +13 -29
  71. package/src/__tests__/scene-mount-fixed-point.test.ts +104 -227
  72. package/src/__tests__/scene-mount-roundtrip.test.ts +95 -961
  73. package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +36 -182
  74. package/src/__tests__/scene-sugar-instantiate.test.ts +20 -15
  75. package/src/__tests__/serialize-mounts-roundtrip.test.ts +108 -492
  76. package/src/__tests__/serialize-scene-asset.test.ts +36 -50
  77. package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -1
  78. package/src/__tests__/shadow-fields-observable.dawn.test.ts +34 -19
  79. package/src/__tests__/shadow-mesh-ssbo-order.test.ts +5 -12
  80. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +6 -4
  81. package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +50 -2
  82. package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +50 -2
  83. package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +50 -2
  84. package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +50 -2
  85. package/src/__tests__/skin-palette-extract.cases.unit.test.ts +2 -0
  86. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +93 -48
  87. package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +6 -2
  88. package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +1 -1
  89. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +11 -3
  90. package/src/__tests__/template-game-default-api-contract.test.ts +21 -21
  91. package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +8 -5
  92. package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +3 -0
  93. package/src/__tests__/transient-fields-collect.test.ts +4 -4
  94. package/src/__tests__/transient-runtime-behavior.test.ts +9 -9
  95. package/src/__tests__/vfx-depth-material.dawn.test.ts +42 -0
  96. package/src/__tests__/vfx-mesh-lighting.browser.test.ts +16 -0
  97. package/src/__tests__/vfx-mesh-lighting.commands.ts +14 -0
  98. package/src/__tests__/vfx-mesh-lighting.dawn.test.ts +16 -0
  99. package/src/__tests__/vfx-mesh-lighting.fixture.ts +883 -0
  100. package/src/__tests__/wave1-dynamic-geometry.browser.test.ts +326 -0
  101. package/src/__tests__/wave1-rendering-materials.browser.test.ts +617 -0
  102. package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
  103. package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
  104. package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
  105. package/src/__tests__/world-scene-despawn.test.ts +8 -8
  106. package/src/__tests__/world-scene-instantiate.test.ts +23 -29
  107. package/src/__tests__/world-scene-nested.test.ts +90 -163
  108. package/src/__tests__/world-scene-override.test.ts +17 -16
  109. package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
  110. package/src/collect-scene-asset.ts +637 -19
  111. package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
  112. package/src/index.ts +1 -0
  113. package/src/scene-utils/mount-override-fold.ts +5 -14
  114. package/src/__tests__/node-shims.d.ts +0 -31
@@ -66,14 +66,16 @@ describe('m2-t1 — Form 2: root itself is SceneInstance', () => {
66
66
  const w = new World();
67
67
  const child: SceneAsset = {
68
68
  kind: 'scene',
69
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
69
+ entities: { 'entity-0': { components: { Transform: {} } } },
70
70
  };
71
71
  reg.catalog(pg(G1), child as Asset);
72
72
 
73
73
  const parent: SceneAsset = {
74
74
  kind: 'scene',
75
- entities: [{ localId: 0 as never, components: { Transform: { pos: [10, 0, 0] } } }],
76
- mounts: [{ localId: 1 as never, source: 0, memberFirst: 2 as never, memberCount: 1 }],
75
+ entities: {
76
+ 'entity-0': { components: { Transform: { pos: [10, 0, 0] } } },
77
+ child: { components: {}, instance: { source: G1 } },
78
+ },
77
79
  };
78
80
  const ch = rs(w, child);
79
81
  const ph = rs(w, parent);
@@ -91,16 +93,15 @@ describe('m2-t1 — Form 2: root itself is SceneInstance', () => {
91
93
  // Root is SceneInstance → root's own entities are owned.
92
94
  // Child's member entities are excluded.
93
95
  const s = res.value;
94
- expect(s.entities.length).toBeGreaterThanOrEqual(1); // parent's own entity
95
- for (const e of s.entities) {
96
+ expect(Object.keys(s.entities).length).toBeGreaterThanOrEqual(1); // parent's own entity
97
+ for (const e of Object.values(s.entities)) {
96
98
  expect(
97
99
  (e.components as Record<string, Record<string, unknown>>).SceneInstance,
98
100
  ).toBeUndefined();
99
101
  }
100
102
  // Child anchor produces a mount.
101
- expect(s.mounts).toBeDefined();
102
- expect(s.mounts?.length).toBe(1);
103
- expect(s.mounts?.[0]?.source).toBe(G1);
103
+ const childInstance = Object.values(s.entities).find((entity) => entity.instance);
104
+ expect(childInstance?.instance?.source).toBe(G1);
104
105
  });
105
106
  });
106
107
 
@@ -111,10 +112,10 @@ describe('m2-t2 — Form 1: deep anchor with ChildOf parent', () => {
111
112
  const w = new World();
112
113
  const child: SceneAsset = {
113
114
  kind: 'scene',
114
- entities: [
115
- { localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
116
- { localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
117
- ],
115
+ entities: {
116
+ 'entity-0': { components: { Transform: { pos: [1, 0, 0] } } },
117
+ 'entity-1': { components: { Transform: { pos: [2, 0, 0] } } },
118
+ },
118
119
  };
119
120
  reg.catalog(pg(G1), child as Asset);
120
121
  const ch = rs(w, child);
@@ -144,10 +145,9 @@ describe('m2-t2 — Form 1: deep anchor with ChildOf parent', () => {
144
145
 
145
146
  // Outer root is NOT SceneInstance → no mount for it.
146
147
  // Child anchor produces a mount; its member entities excluded from owned.
147
- expect(res.value.mounts).toBeDefined();
148
- expect(res.value.mounts?.length).toBe(1);
149
- expect(res.value.mounts?.[0]?.source).toBe(G1);
150
- for (const e of res.value.entities) {
148
+ const childInstance = Object.values(res.value.entities).find((entity) => entity.instance);
149
+ expect(childInstance?.instance?.source).toBe(G1);
150
+ for (const e of Object.values(res.value.entities)) {
151
151
  expect(
152
152
  (e.components as Record<string, Record<string, unknown>>).SceneInstance,
153
153
  ).toBeUndefined();
@@ -162,15 +162,17 @@ describe('m2-t3 — AC-03 window invariants', () => {
162
162
  const w = new World();
163
163
  const ca: SceneAsset = {
164
164
  kind: 'scene',
165
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
165
+ entities: { 'entity-0': { components: { Transform: {} } } },
166
166
  };
167
167
  reg.catalog(pg(G1), ca as Asset);
168
168
  const ch = rs(w, ca);
169
169
 
170
170
  const pa: SceneAsset = {
171
171
  kind: 'scene',
172
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
173
- mounts: [{ localId: 1 as never, source: 0, memberFirst: 2 as never, memberCount: 1 }],
172
+ entities: {
173
+ 'entity-0': { components: { Transform: {} } },
174
+ child: { components: {}, instance: { source: G1 } },
175
+ },
174
176
  };
175
177
  const ph = rs(w, pa);
176
178
  wr(w, ph, ch);
@@ -182,7 +184,7 @@ describe('m2-t3 — AC-03 window invariants', () => {
182
184
  const res = rootsToSceneAsset(reg, w, [inst.value.root]);
183
185
  expect(res.ok).toBe(true);
184
186
  if (!res.ok) return;
185
- expect(res.value.mounts?.[0]?.memberCount).toBe(1);
187
+ expect(Object.values(res.value.entities).filter((entity) => entity.instance)).toHaveLength(1);
186
188
  });
187
189
 
188
190
  it('sparse localId does not shrink window', () => {
@@ -190,18 +192,20 @@ describe('m2-t3 — AC-03 window invariants', () => {
190
192
  const w = new World();
191
193
  const ca: SceneAsset = {
192
194
  kind: 'scene',
193
- entities: [
194
- { localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } },
195
- { localId: 1 as never, components: { Transform: { pos: [2, 0, 0] } } },
196
- ],
195
+ entities: {
196
+ 'entity-0': { components: { Transform: { pos: [1, 0, 0] } } },
197
+ 'entity-1': { components: { Transform: { pos: [2, 0, 0] } } },
198
+ },
197
199
  };
198
200
  reg.catalog(pg(G1), ca as Asset);
199
201
  const ch = rs(w, ca);
200
202
 
201
203
  const pa: SceneAsset = {
202
204
  kind: 'scene',
203
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
204
- mounts: [{ localId: 1 as never, source: 0, memberFirst: 2 as never, memberCount: 2 }],
205
+ entities: {
206
+ 'entity-0': { components: { Transform: {} } },
207
+ child: { components: {}, instance: { source: G1 } },
208
+ },
205
209
  };
206
210
  const ph = rs(w, pa);
207
211
  wr(w, ph, ch);
@@ -220,7 +224,7 @@ describe('m2-t3 — AC-03 window invariants', () => {
220
224
  const res = rootsToSceneAsset(reg, w, [inst.value.root]);
221
225
  expect(res.ok).toBe(true);
222
226
  if (!res.ok) return;
223
- expect(res.value.mounts?.[0]?.memberCount).toBe(2); // totalSlots, not live count
227
+ expect(Object.values(res.value.entities).filter((entity) => entity.instance)).toHaveLength(1); // keyed instance survives sparse members
224
228
  });
225
229
 
226
230
  it('multiple child mounts have non-overlapping windows', () => {
@@ -228,14 +232,14 @@ describe('m2-t3 — AC-03 window invariants', () => {
228
232
  const w = new World();
229
233
  const cA: SceneAsset = {
230
234
  kind: 'scene',
231
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
235
+ entities: { 'entity-0': { components: { Transform: {} } } },
232
236
  };
233
237
  const cB: SceneAsset = {
234
238
  kind: 'scene',
235
- entities: [
236
- { localId: 0 as never, components: { Transform: {} } },
237
- { localId: 1 as never, components: { Transform: {} } },
238
- ],
239
+ entities: {
240
+ 'entity-0': { components: { Transform: {} } },
241
+ 'entity-1': { components: { Transform: {} } },
242
+ },
239
243
  };
240
244
  reg.catalog(pg(G1), cA as Asset);
241
245
  reg.catalog(pg(G2), cB as Asset);
@@ -244,20 +248,19 @@ describe('m2-t3 — AC-03 window invariants', () => {
244
248
 
245
249
  const pa: SceneAsset = {
246
250
  kind: 'scene',
247
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
248
- mounts: [
249
- { localId: 1 as never, source: 0, memberFirst: 3 as never, memberCount: 1 },
250
- { localId: 2 as never, source: 1, memberFirst: 4 as never, memberCount: 2 },
251
- ],
251
+ entities: {
252
+ 'entity-0': { components: { Transform: {} } },
253
+ childA: { components: {}, instance: { source: G1 } },
254
+ childB: { components: {}, instance: { source: G2 } },
255
+ },
252
256
  };
253
257
  const ph = rs(w, pa);
254
- SceneOwner.worldSetSceneAssetResolver(w, (sIdx, pH) =>
255
- (pH as unknown as number) !== (ph as unknown as number)
256
- ? err({ code: 'asset-not-found' })
257
- : sIdx === 0
258
- ? ok(hA)
259
- : ok(hB),
260
- );
258
+ SceneOwner.worldSetSceneAssetResolver(w, (sIdx, pH) => {
259
+ if ((pH as unknown as number) !== (ph as unknown as number))
260
+ return err({ code: 'asset-not-found' });
261
+ if (typeof sIdx === 'number') return ok(sIdx as unknown as Handle<'SceneAsset', 'shared'>);
262
+ return String(sIdx).toLowerCase() === G1 ? ok(hA) : ok(hB);
263
+ });
261
264
 
262
265
  const inst = SceneOwner.worldInstantiateScene(w, ph);
263
266
  expect(inst.ok).toBe(true);
@@ -268,27 +271,18 @@ describe('m2-t3 — AC-03 window invariants', () => {
268
271
  if (!s.ok) continue;
269
272
  const c = resolveAssetHandle<SceneAsset>(w, s.value as unknown as Handle<string, 'shared'>);
270
273
  if (!c.ok) continue;
271
- soi(reg, c.value as SceneAsset, c.value.entities.length === 1 ? G1 : G2);
274
+ soi(reg, c.value as SceneAsset, Object.keys(c.value.entities).length === 1 ? G1 : G2);
272
275
  }
273
276
 
274
277
  const res = rootsToSceneAsset(reg, w, [inst.value.root]);
275
278
  expect(res.ok).toBe(true);
276
279
  if (!res.ok) return;
277
280
 
278
- const ms = res.value.mounts ?? [];
279
- expect(ms.length).toBe(2);
280
- const W = ms
281
- .map((m) => ({ f: m.memberFirst as number, c: m.memberCount }))
282
- .sort((a, b) => a.f - b.f);
283
- for (let i = 0; i < W.length - 1; i++) {
284
- const cur = W[i];
285
- const next = W[i + 1];
286
- if (!cur || !next) throw new Error('window index out of range');
287
- expect(cur.f + cur.c).toBeLessThanOrEqual(next.f);
288
- }
289
- const owned = new Set(res.value.entities.map((e) => e.localId as unknown as number));
290
- for (const w of W)
291
- for (let lid = w.f; lid < w.f + w.c; lid++) expect(owned.has(lid)).toBe(false);
281
+ const instances = Object.values(res.value.entities).flatMap((entity) =>
282
+ entity.instance ? [entity.instance] : [],
283
+ );
284
+ expect(instances).toHaveLength(2);
285
+ expect(new Set(instances.map((instance) => instance.source))).toEqual(new Set([G1, G2]));
292
286
  });
293
287
  });
294
288
 
@@ -299,15 +293,17 @@ describe('m2-t4 — graft preservation', () => {
299
293
  const w = new World();
300
294
  const ca: SceneAsset = {
301
295
  kind: 'scene',
302
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
296
+ entities: { 'entity-0': { components: { Transform: {} } } },
303
297
  };
304
298
  reg.catalog(pg(G1), ca as Asset);
305
299
  const ch = rs(w, ca);
306
300
 
307
301
  const pa: SceneAsset = {
308
302
  kind: 'scene',
309
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
310
- mounts: [{ localId: 1 as never, source: 0, memberFirst: 2 as never, memberCount: 1 }],
303
+ entities: {
304
+ 'entity-0': { components: { Transform: {} } },
305
+ child: { components: {}, instance: { source: G1 } },
306
+ },
311
307
  };
312
308
  const ph = rs(w, pa);
313
309
  wr(w, ph, ch);
@@ -337,7 +333,7 @@ describe('m2-t4 — graft preservation', () => {
337
333
 
338
334
  // Prop should survive as owned entity (not eaten by mount).
339
335
  let propFound = false;
340
- for (const e of res.value.entities) {
336
+ for (const e of Object.values(res.value.entities)) {
341
337
  if ((e.components as Record<string, Record<string, unknown>>).ChildOf) {
342
338
  propFound = true;
343
339
  break;
@@ -351,15 +347,17 @@ describe('m2-t4 — graft preservation', () => {
351
347
  const w = new World();
352
348
  const ca: SceneAsset = {
353
349
  kind: 'scene',
354
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
350
+ entities: { 'entity-0': { components: { Transform: {} } } },
355
351
  };
356
352
  reg.catalog(pg(G1), ca as Asset);
357
353
  const ch = rs(w, ca);
358
354
 
359
355
  const pa: SceneAsset = {
360
356
  kind: 'scene',
361
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
362
- mounts: [{ localId: 1 as never, source: 0, memberFirst: 2 as never, memberCount: 1 }],
357
+ entities: {
358
+ 'entity-0': { components: { Transform: {} } },
359
+ child: { components: {}, instance: { source: G1 } },
360
+ },
363
361
  };
364
362
  const ph = rs(w, pa);
365
363
  wr(w, ph, ch);
@@ -25,7 +25,7 @@ function makeRegistry(): AssetRegistry {
25
25
  return new AssetRegistry(makeMockShaderRegistry());
26
26
  }
27
27
 
28
- function localId(n: number): LocalEntityId {
28
+ function _localId(n: number): LocalEntityId {
29
29
  return n as LocalEntityId;
30
30
  }
31
31
 
@@ -37,10 +37,7 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
37
37
  it('2-level hierarchy: no entity has Children in collect output', () => {
38
38
  const asset: SceneAsset = {
39
39
  kind: 'scene',
40
- entities: [
41
- { localId: localId(0), components: {} },
42
- { localId: localId(1), components: {} },
43
- ],
40
+ entities: { 'entity-0': { components: {} }, 'entity-1': { components: {} } },
44
41
  };
45
42
 
46
43
  const world = new World();
@@ -59,12 +56,12 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
59
56
  if (!collected.ok) return;
60
57
 
61
58
  // Verify: no entity in the collected scene has 'Children' component.
62
- for (const ent of collected.value.entities) {
59
+ for (const ent of Object.values(collected.value.entities)) {
63
60
  expect(hasComp(ent, 'Children')).toBe(false);
64
61
  }
65
62
 
66
63
  // Also verify no entity has 'SceneInstance'.
67
- for (const ent of collected.value.entities) {
64
+ for (const ent of Object.values(collected.value.entities)) {
68
65
  expect(hasComp(ent, 'SceneInstance')).toBe(false);
69
66
  }
70
67
  });
@@ -76,18 +73,15 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
76
73
  // Level 3: leaf scene instantiated under mid scene entity.
77
74
  const leafAsset: SceneAsset = {
78
75
  kind: 'scene',
79
- entities: [{ localId: localId(0), components: {} }],
76
+ entities: { 'entity-0': { components: {} } },
80
77
  };
81
78
  const midAsset: SceneAsset = {
82
79
  kind: 'scene',
83
- entities: [{ localId: localId(0), components: {} }],
80
+ entities: { 'entity-0': { components: {} } },
84
81
  };
85
82
  const outerAsset: SceneAsset = {
86
83
  kind: 'scene',
87
- entities: [
88
- { localId: localId(0), components: {} },
89
- { localId: localId(1), components: {} },
90
- ],
84
+ entities: { 'entity-0': { components: {} }, 'entity-1': { components: {} } },
91
85
  };
92
86
 
93
87
  const world = new World();
@@ -131,10 +125,10 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
131
125
 
132
126
  // Hierarchy: outer root + (2 outer children) + mid root + (1 mid child) + leaf root + (1 leaf child) >= 7 entities.
133
127
  // But the exact count depends on mount-collapse logic — focus on the Children assertion.
134
- expect(collected.value.entities.length).toBeGreaterThan(0);
128
+ expect(Object.keys(collected.value.entities).length).toBeGreaterThan(0);
135
129
 
136
130
  // No entity has Children or SceneInstance.
137
- for (const ent of collected.value.entities) {
131
+ for (const ent of Object.values(collected.value.entities)) {
138
132
  expect(hasComp(ent, 'Children')).toBe(false);
139
133
  expect(hasComp(ent, 'SceneInstance')).toBe(false);
140
134
  }
@@ -143,7 +137,7 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
143
137
  it('degenerate: zero-hierarchy scene has no Children', () => {
144
138
  const asset: SceneAsset = {
145
139
  kind: 'scene',
146
- entities: [{ localId: localId(0), components: {} }],
140
+ entities: { 'entity-0': { components: {} } },
147
141
  };
148
142
 
149
143
  const world = new World();
@@ -161,7 +155,7 @@ describe('m1t3 — Children absent from collect (AC-04)', () => {
161
155
  expect(collected.ok).toBe(true);
162
156
  if (!collected.ok) return;
163
157
 
164
- for (const ent of collected.value.entities) {
158
+ for (const ent of Object.values(collected.value.entities)) {
165
159
  expect(hasComp(ent, 'Children')).toBe(false);
166
160
  }
167
161
  });
@@ -39,7 +39,7 @@ function makeRegistry(): AssetRegistry {
39
39
  return new AssetRegistry(makeMockShaderRegistry());
40
40
  }
41
41
 
42
- function localId(n: number): LocalEntityId {
42
+ function _localId(n: number): LocalEntityId {
43
43
  return n as LocalEntityId;
44
44
  }
45
45
 
@@ -55,11 +55,11 @@ describe('m1t4 — AC-05 roundtrip regression (red-first)', () => {
55
55
  // After instantiateScene, the synthetic root has Children populated by mirror hook.
56
56
  const asset: SceneAsset = {
57
57
  kind: 'scene',
58
- entities: [
59
- { localId: localId(0), components: {} },
60
- { localId: localId(1), components: {} },
61
- { localId: localId(2), components: {} },
62
- ],
58
+ entities: {
59
+ 'entity-0': { components: {} },
60
+ 'entity-1': { components: {} },
61
+ 'entity-2': { components: {} },
62
+ },
63
63
  };
64
64
 
65
65
  const world = new World();
@@ -128,7 +128,7 @@ describe('m1t4 — AC-05 roundtrip regression (red-first)', () => {
128
128
  it('(c) degenerate: zero parent-child scene round-trips', () => {
129
129
  const asset: SceneAsset = {
130
130
  kind: 'scene',
131
- entities: [{ localId: localId(0), components: {} }],
131
+ entities: { 'entity-0': { components: {} } },
132
132
  };
133
133
 
134
134
  const world = new World();
@@ -172,7 +172,7 @@ describe('m1t4 — AC-05 roundtrip regression (red-first)', () => {
172
172
  // world produced. The transient world column is reconstructed by propagate from
173
173
  // the persisted local TRS -- proving skipping it loses nothing.
174
174
  describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
175
- function localId2(n: number): LocalEntityId {
175
+ function _localId2(n: number): LocalEntityId {
176
176
  return n as LocalEntityId;
177
177
  }
178
178
 
@@ -180,9 +180,8 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
180
180
  // Non-identity local TRS so the world mat4 is a real, distinguishable value.
181
181
  const asset: SceneAsset = {
182
182
  kind: 'scene',
183
- entities: [
184
- {
185
- localId: localId2(0),
183
+ entities: {
184
+ 'entity-0': {
186
185
  components: {
187
186
  Transform: {
188
187
  pos: [3, 4, 5],
@@ -191,7 +190,7 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
191
190
  },
192
191
  },
193
192
  },
194
- ],
193
+ },
195
194
  };
196
195
 
197
196
  const world = new World();
@@ -213,7 +212,7 @@ describe('w1 — AC-04 transient world roundtrip numeric equivalence', () => {
213
212
  if (!collected.ok) return;
214
213
 
215
214
  // Serialized output must not carry world (transient).
216
- for (const ent of collected.value.entities) {
215
+ for (const ent of Object.values(collected.value.entities)) {
217
216
  const t = (ent.components as Record<string, Record<string, unknown>>).Transform;
218
217
  if (t !== undefined) expect('world' in t).toBe(false);
219
218
  expect((ent.components as Record<string, unknown>).GlobalTransform).toBeUndefined();
@@ -24,7 +24,7 @@ function makeRegistry(): AssetRegistry {
24
24
  return new AssetRegistry(makeMockShaderRegistry());
25
25
  }
26
26
 
27
- function localId(n: number): LocalEntityId {
27
+ function _localId(n: number): LocalEntityId {
28
28
  return n as LocalEntityId;
29
29
  }
30
30
 
@@ -41,7 +41,7 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
41
41
  it('(a) SceneInstance absent from collect output', () => {
42
42
  const asset: SceneAsset = {
43
43
  kind: 'scene',
44
- entities: [{ localId: localId(0), components: {} }],
44
+ entities: { 'entity-0': { components: {} } },
45
45
  };
46
46
 
47
47
  const world = new World();
@@ -58,7 +58,7 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
58
58
  expect(collected.ok).toBe(true);
59
59
  if (!collected.ok) return;
60
60
 
61
- for (const ent of collected.value.entities) {
61
+ for (const ent of Object.values(collected.value.entities)) {
62
62
  expect(hasComp(ent, 'SceneInstance')).toBe(false);
63
63
  }
64
64
  });
@@ -68,10 +68,7 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
68
68
  // (instance-level isRoot && ChildOf skip is independent of transient).
69
69
  const asset: SceneAsset = {
70
70
  kind: 'scene',
71
- entities: [
72
- { localId: localId(0), components: {} },
73
- { localId: localId(1), components: {} },
74
- ],
71
+ entities: { 'entity-0': { components: {} }, 'entity-1': { components: {} } },
75
72
  };
76
73
 
77
74
  const world = new World();
@@ -88,34 +85,27 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
88
85
  expect(collected.ok).toBe(true);
89
86
  if (!collected.ok) return;
90
87
 
91
- // root + 2 children = 3 entities.
92
- // Children (non-root) should have ChildOf. Root should NOT have ChildOf.
93
- const rootWithChildOf = false;
88
+ // Both entities are authored roots. Their runtime ChildOf edges point to
89
+ // the transient synthetic root and are therefore omitted on collect.
94
90
  let childrenWithChildOf = 0;
95
91
 
96
- for (const ent of collected.value.entities) {
92
+ for (const ent of Object.values(collected.value.entities)) {
97
93
  if (hasComp(ent, 'ChildOf')) {
98
94
  childrenWithChildOf++;
99
95
  }
100
96
  }
101
97
 
102
- // At least one non-root entity has ChildOf.
103
- // Root ChildOf is skipped by isRoot && ChildOf instance-level check.
104
- expect(childrenWithChildOf).toBeGreaterThan(0);
105
- // No root entity should have ChildOf (the only root is the synthetic scene root).
106
- // We verify by counting: all entities with ChildOf are children, root has none.
107
- // The sum indicates root coverage is correct.
108
- expect(rootWithChildOf).toBe(false);
98
+ expect(childrenWithChildOf).toBe(0);
109
99
  });
110
100
 
111
101
  it('(c) non-root entities retain ChildOf', () => {
112
102
  // Non-root entities (instantiated from SceneAsset localIds) must carry ChildOf.
113
103
  const asset: SceneAsset = {
114
104
  kind: 'scene',
115
- entities: [
116
- { localId: localId(0), components: {} },
117
- { localId: localId(1), components: {} },
118
- ],
105
+ entities: {
106
+ 'entity-0': { components: {} },
107
+ 'entity-1': { components: { ChildOf: { parent: 'entity-0' } } },
108
+ },
119
109
  };
120
110
 
121
111
  const world = new World();
@@ -132,9 +122,12 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
132
122
  expect(collected.ok).toBe(true);
133
123
  if (!collected.ok) return;
134
124
 
135
- // root + 2 children = 3 entities. 2 children have ChildOf.
136
- const childOfEntities = collected.value.entities.filter((e) => hasComp(e, 'ChildOf'));
137
- expect(childOfEntities.length).toBe(2);
125
+ // The explicit authored edge survives; the implicit synthetic-root edges
126
+ // are omitted by the collector.
127
+ const childOfEntities = Object.values(collected.value.entities).filter((e) =>
128
+ hasComp(e, 'ChildOf'),
129
+ );
130
+ expect(childOfEntities.length).toBe(1);
138
131
 
139
132
  // Each child ChildOf.parent should point to root handle.
140
133
  for (const c of childOfEntities) {
@@ -149,7 +142,7 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
149
142
 
150
143
  const asset: SceneAsset = {
151
144
  kind: 'scene',
152
- entities: [{ localId: localId(0), components: { M1T2_Ctrl: { val: 42, label: 'hello' } } }],
145
+ entities: { 'entity-0': { components: { M1T2_Ctrl: { val: 42, label: 'hello' } } } },
153
146
  };
154
147
 
155
148
  const world = new World();
@@ -166,7 +159,7 @@ describe('m1t2 — collect transient skip (AC-02 + AC-03)', () => {
166
159
  expect(collected.ok).toBe(true);
167
160
  if (!collected.ok) return;
168
161
 
169
- const found = collected.value.entities.find((e) => hasComp(e, 'M1T2_Ctrl'));
162
+ const found = Object.values(collected.value.entities).find((e) => hasComp(e, 'M1T2_Ctrl'));
170
163
  expect(found).toBeDefined();
171
164
  const c = (found?.components as Record<string, Record<string, unknown>>).M1T2_Ctrl;
172
165
  expect(c).toBeDefined();
@@ -69,7 +69,7 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
69
69
  expectTypeOf<{ materials: readonly [0] }>().not.toMatchTypeOf<SpawnData>();
70
70
  });
71
71
 
72
- it('Camera data shape has 21 fields (18 number + clearColor array + autoAspect boolean + target)', () => {
72
+ it('Camera data shape includes output exposure and color-grading fields', () => {
73
73
  type Data = ShapeOf<SchemaOf<typeof Camera>>;
74
74
  expectTypeOf<keyof Data>().toEqualTypeOf<
75
75
  | 'fov'
@@ -83,7 +83,15 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
83
83
  | 'top'
84
84
  | 'tonemap'
85
85
  | 'exposure'
86
+ | 'exposureMode'
87
+ | 'compensationEv'
88
+ | 'rangeEv'
89
+ | 'rates'
86
90
  | 'whitePoint'
91
+ | 'temperature'
92
+ | 'tint'
93
+ | 'colorLut'
94
+ | 'colorLutStrength'
87
95
  | 'antialias'
88
96
  | 'historyVersion'
89
97
  | 'bloom'
@@ -102,7 +110,15 @@ describe('w7 type-level - 5 component schemas yield exact data shapes via ShapeO
102
110
  // plan-strategy section 2.3 D-1 (f32 enum encoding).
103
111
  expectTypeOf<Data['tonemap']>().toEqualTypeOf<number>();
104
112
  expectTypeOf<Data['exposure']>().toEqualTypeOf<number>();
113
+ expectTypeOf<Data['exposureMode']>().toEqualTypeOf<number>();
114
+ expectTypeOf<Data['compensationEv']>().toEqualTypeOf<number>();
115
+ expectTypeOf<Data['rangeEv']>().toEqualTypeOf<Float32Array>();
116
+ expectTypeOf<Data['rates']>().toEqualTypeOf<Float32Array>();
105
117
  expectTypeOf<Data['whitePoint']>().toEqualTypeOf<number>();
118
+ expectTypeOf<Data['temperature']>().toEqualTypeOf<number>();
119
+ expectTypeOf<Data['tint']>().toEqualTypeOf<number>();
120
+ expectTypeOf<Data['colorLut']>().toEqualTypeOf<Handle<'TextureAsset', 'shared'>>();
121
+ expectTypeOf<Data['colorLutStrength']>().toEqualTypeOf<number>();
106
122
  expectTypeOf<Data['historyVersion']>().toEqualTypeOf<number>();
107
123
  // feat-20260709 M3: clear-color quartet collapsed into one inline
108
124
  // array<f32,4> column; read side resolves to Float32Array (mirrors the