@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.
Files changed (113) 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 -32
  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-rendering-materials.browser.test.ts +617 -0
  101. package/src/__tests__/wave1-rendering-p0.browser.test.ts +751 -0
  102. package/src/__tests__/wave1-rendering-recovery.browser.test.ts +666 -0
  103. package/src/__tests__/wave1-shadow-diagnostic.browser.test.ts +582 -0
  104. package/src/__tests__/world-scene-despawn.test.ts +8 -8
  105. package/src/__tests__/world-scene-instantiate.test.ts +23 -29
  106. package/src/__tests__/world-scene-nested.test.ts +90 -163
  107. package/src/__tests__/world-scene-override.test.ts +17 -16
  108. package/src/__tests__/zero-compression-zero-load.integration.test.ts +1 -0
  109. package/src/collect-scene-asset.ts +637 -19
  110. package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
  111. package/src/index.ts +1 -0
  112. package/src/scene-utils/mount-override-fold.ts +5 -14
  113. package/src/__tests__/node-shims.d.ts +0 -31
@@ -53,12 +53,7 @@ describe('world.instantiateScene basic (w17)', () => {
53
53
  const world = new World();
54
54
  const asset: SceneAsset = {
55
55
  kind: 'scene',
56
- entities: [
57
- {
58
- localId: 0 as never,
59
- components: { Transform: { pos: [1, 2, 3] } },
60
- },
61
- ],
56
+ entities: { 'entity-0': { components: { Transform: { pos: [1, 2, 3] } } } },
62
57
  };
63
58
  const handle = registerSceneAsset(world, asset);
64
59
  const r = instantiateScene(world, handle);
@@ -72,11 +67,11 @@ describe('world.instantiateScene basic (w17)', () => {
72
67
  const world = new World();
73
68
  const asset: SceneAsset = {
74
69
  kind: 'scene',
75
- entities: [
76
- { localId: 0 as never, components: { Transform: {} } },
77
- { localId: 1 as never, components: { Transform: {} } },
78
- { localId: 2 as never, components: { Transform: {} } },
79
- ],
70
+ entities: {
71
+ 'entity-0': { components: { Transform: {} } },
72
+ 'entity-1': { components: { Transform: {} } },
73
+ 'entity-2': { components: { Transform: {} } },
74
+ },
80
75
  };
81
76
  const handle = registerSceneAsset(world, asset);
82
77
  const r = instantiateScene(world, handle);
@@ -93,12 +88,7 @@ describe('world.instantiateScene basic (w17)', () => {
93
88
  const world = new World();
94
89
  const asset: SceneAsset = {
95
90
  kind: 'scene',
96
- entities: [
97
- {
98
- localId: 0 as never,
99
- components: { Transform: { pos: [7, 8, 9] } },
100
- },
101
- ],
91
+ entities: { 'entity-0': { components: { Transform: { pos: [7, 8, 9] } } } },
102
92
  };
103
93
  const handle = registerSceneAsset(world, asset);
104
94
  const r = instantiateScene(world, handle);
@@ -124,7 +114,7 @@ describe('world.instantiateScene basic (w17)', () => {
124
114
 
125
115
  const asset: SceneAsset = {
126
116
  kind: 'scene',
127
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
117
+ entities: { 'entity-0': { components: { Transform: {} } } },
128
118
  };
129
119
  const handle = registerSceneAsset(world, asset);
130
120
  const r = instantiateScene(world, handle, parent);
@@ -141,7 +131,7 @@ describe('world.getSceneAssetForInstance (w22)', () => {
141
131
  const world = new World();
142
132
  const asset: SceneAsset = {
143
133
  kind: 'scene',
144
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
134
+ entities: { 'entity-0': { components: { Transform: {} } } },
145
135
  };
146
136
  const handle = registerSceneAsset(world, asset);
147
137
  const r = instantiateScene(world, handle);
@@ -173,16 +163,20 @@ describe('world.instantiateScene perf (w23) — 100 entities x 5 components', ()
173
163
  void C3;
174
164
  void C4;
175
165
  const N = 100;
176
- const entities = Array.from({ length: N }, (_, i) => ({
177
- localId: i as never,
178
- components: {
179
- Transform: {},
180
- SceneNestPerfC1: { a: i },
181
- SceneNestPerfC2: { a: i },
182
- SceneNestPerfC3: { a: i },
183
- SceneNestPerfC4: { a: i },
184
- },
185
- }));
166
+ const entities = Object.fromEntries(
167
+ Array.from({ length: N }, (_, i) => [
168
+ `entity-${i}`,
169
+ {
170
+ components: {
171
+ Transform: {},
172
+ SceneNestPerfC1: { a: i },
173
+ SceneNestPerfC2: { a: i },
174
+ SceneNestPerfC3: { a: i },
175
+ SceneNestPerfC4: { a: i },
176
+ },
177
+ },
178
+ ]),
179
+ );
186
180
  const asset: SceneAsset = { kind: 'scene', entities };
187
181
  const handle = registerSceneAsset(world, asset, [C1, C2, C3, C4]);
188
182
  const t0 = performance.now();
@@ -1,22 +1,6 @@
1
- import * as SceneOwner from '@forgeax/engine-scene';
2
- // feat-20260608-scene-nesting-ecs-fication M2 / w18 (red phase) — nested /
3
- // cross-boundary / deep / multi-mount cases.
4
- //
5
- // Test file location rationale: same as world-scene-instantiate.test.ts —
6
- // uses SceneInstance / Transform tokens (defined in @forgeax/engine-runtime),
7
- // so the test physically lives in runtime; the World methods themselves
8
- // resolve to @forgeax/engine-ecs.
9
- //
10
- // AC anchors:
11
- // - AC-23 (double-nested A->B->C: query<SceneInstance>() returns >=3 roots)
12
- // - AC-24 (cross-boundary: outer entities[].ChildOf.parent points into a
13
- // mount sub-region)
14
- // - AC-25 (deep nesting >=5 layers does not stack-overflow)
15
- // - AC-26 (same prefab mounted multiple times: mappings disjoint, overrides
16
- // do not leak across mount instances)
17
-
18
- import { defineComponent, World } from '@forgeax/engine-ecs';
1
+ import { World } from '@forgeax/engine-ecs';
19
2
  import { SceneInstance } from '@forgeax/engine-render';
3
+ import * as SceneOwner from '@forgeax/engine-scene';
20
4
  import { ChildOf } from '@forgeax/engine-scene';
21
5
  import type { Handle, SceneAsset } from '@forgeax/engine-types';
22
6
  import { err, ok } from '@forgeax/engine-types';
@@ -28,203 +12,146 @@ function registerSceneAsset(world: World, asset: SceneAsset): Handle<'SceneAsset
28
12
  return world.allocSharedRef('SceneAsset', asset);
29
13
  }
30
14
 
31
- describe('AC-23 double-nested A -> B -> C', () => {
32
- it('query<SceneInstance>() returns >=3 SceneInstance roots after instantiating A', () => {
15
+ describe('keyed nested SceneAsset instances', () => {
16
+ it('double-nested A -> B -> C materialises three SceneInstance roots', () => {
33
17
  const world = new World();
34
- // Bottom asset C: 1 entity
35
18
  const assetC: SceneAsset = {
36
19
  kind: 'scene',
37
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
20
+ entities: { 'entity-0': { components: { Transform: {} } } },
38
21
  };
39
22
  const handleC = registerSceneAsset(world, assetC);
40
- // Middle asset B: 1 entity + 1 mount of C
41
23
  const assetB: SceneAsset = {
42
24
  kind: 'scene',
43
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
44
- mounts: [
45
- {
46
- localId: 1 as never,
47
- source: 0,
48
- memberFirst: 2 as never,
49
- memberCount: 1,
50
- },
51
- ],
25
+ entities: {
26
+ 'entity-0': { components: { Transform: {} } },
27
+ child: { components: {}, instance: { source: 'C' } },
28
+ },
52
29
  };
53
30
  const handleB = registerSceneAsset(world, assetB);
54
- // Outer asset A: 1 entity + 1 mount of B
55
31
  const assetA: SceneAsset = {
56
32
  kind: 'scene',
57
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
58
- mounts: [
59
- {
60
- localId: 1 as never,
61
- source: 0,
62
- memberFirst: 2 as never,
63
- memberCount: 3,
64
- },
65
- ],
33
+ entities: {
34
+ 'entity-0': { components: { Transform: {} } },
35
+ child: { components: {}, instance: { source: 'B' } },
36
+ },
66
37
  };
67
38
  const handleA = registerSceneAsset(world, assetA);
68
-
69
- // Wire SceneAsset resolver: each mount.source `0` resolves to the
70
- // applicable child handle. We use a thread-local trick — the
71
- // instantiateScene impl is expected to use a SceneAssetResolver wired
72
- // through engine.assets in production; for the unit test we set a
73
- // resolver directly by stuffing handles into a Map keyed by parent.
74
- // The test relies on World using the same registry the SceneAsset was
75
- // alloc'd into; mount.source -> child handle resolution is done by
76
- // looking up `parent.refs[source]` in the implementation. Since this
77
- // unit test creates raw POD assets in-memory without `refs`, the
78
- // implementation must accept an explicit resolver arm — see
79
- // _setSceneAssetResolver below.
80
- SceneOwner.worldSetSceneAssetResolver(world, (sourceIdx, parentHandle) => {
81
- void sourceIdx;
82
- const parentManagedRaw = parentHandle as unknown as number;
83
- // Parent A handle u32 -> handle B; parent B handle u32 -> handle C.
84
- if (parentManagedRaw === (handleA as unknown as number)) return ok(handleB);
85
- if (parentManagedRaw === (handleB as unknown as number)) return ok(handleC);
39
+ SceneOwner.worldSetSceneAssetResolver(world, (source, parentHandle) => {
40
+ if (typeof source === 'number') return ok(source as Handle<'SceneAsset', 'shared'>);
41
+ const parent = Number(parentHandle);
42
+ if (parent === Number(handleA) && source === 'B') return ok(handleB);
43
+ if (parent === Number(handleB) && source === 'C') return ok(handleC);
86
44
  return err({ code: 'asset-not-found' });
87
45
  });
88
46
 
89
- const r = SceneOwner.worldInstantiateScene(world, handleA);
90
- expect(r.ok).toBe(true);
91
- if (!r.ok) return;
92
-
47
+ const result = SceneOwner.worldInstantiateScene(world, handleA);
48
+ if (!result.ok) throw new Error(JSON.stringify(result.error));
49
+ if (!result.ok) return;
93
50
  let count = 0;
94
51
  const query = world.query({ with: [SceneInstance] }).unwrap();
95
52
  for (const _row of query) count += 1;
96
53
  expect(count).toBeGreaterThanOrEqual(3);
97
54
  });
98
- });
99
55
 
100
- describe('AC-25 deep nesting >=5 layers no stack overflow', () => {
101
- it('5-layer chain instantiates successfully', () => {
56
+ it('five keyed instance layers recurse without stack overflow', () => {
102
57
  const world = new World();
103
- // Build chain L0 ... L5 where Lk mounts L(k+1).
104
- // R2/B-2: mount.memberCount must equal child.totalSlots; recompute
105
- // bottom-up. L5 has no mounts -> totalSlots=1; each k<5 has
106
- // 1 entity + 1 mount.localId + memberCount(=child.totalSlots) slots.
107
58
  const handles: Handle<'SceneAsset', 'shared'>[] = [];
108
- let childTotalSlots = 1; // L5: just one entity
109
- for (let k = 5; k >= 0; k -= 1) {
59
+ for (let layer = 5; layer >= 0; layer -= 1) {
110
60
  const asset: SceneAsset = {
111
61
  kind: 'scene',
112
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
113
- mounts:
114
- k < 5
115
- ? [
116
- {
117
- localId: 1 as never,
118
- source: 0,
119
- memberFirst: 2 as never,
120
- memberCount: childTotalSlots,
121
- },
122
- ]
123
- : [],
62
+ entities: {
63
+ 'entity-0': { components: { Transform: {} } },
64
+ ...(layer < 5
65
+ ? { child: { components: {}, instance: { source: `layer-${layer + 1}` } } }
66
+ : {}),
67
+ },
124
68
  };
125
69
  handles.unshift(registerSceneAsset(world, asset));
126
- // Update for the parent layer above.
127
- // totalSlots = 1 entity + 1 mount.localId + memberCount slots
128
- childTotalSlots = k < 5 ? 1 + 1 + childTotalSlots : 1;
129
70
  }
130
- SceneOwner.worldSetSceneAssetResolver(world, (sourceIdx, parentHandle) => {
131
- void sourceIdx;
132
- const raw = parentHandle as unknown as number;
133
- for (let k = 0; k < handles.length - 1; k += 1) {
134
- if (raw === (handles[k] as unknown as number)) {
135
- const next = handles[k + 1];
136
- if (next === undefined) break;
137
- return ok(next);
138
- }
139
- }
140
- return err({ code: 'asset-not-found' });
71
+ SceneOwner.worldSetSceneAssetResolver(world, (_source, parentHandle) => {
72
+ const parentIndex = handles.findIndex((handle) => Number(handle) === Number(parentHandle));
73
+ const next = handles[parentIndex + 1];
74
+ return next === undefined ? err({ code: 'asset-not-found' }) : ok(next);
141
75
  });
142
- const r = SceneOwner.worldInstantiateScene(world, handles[0] as Handle<'SceneAsset', 'shared'>);
143
- expect(r.ok).toBe(true);
76
+ const result = SceneOwner.worldInstantiateScene(
77
+ world,
78
+ handles[0] as Handle<'SceneAsset', 'shared'>,
79
+ );
80
+ if (!result.ok) throw new Error(JSON.stringify(result.error));
144
81
  });
145
- });
146
82
 
147
- describe('AC-26 same prefab mounted multiple times disjoint mappings', () => {
148
- it('two mounts of the same child each get their own member entities', () => {
83
+ it('same child instance twice gets disjoint child mappings', () => {
149
84
  const world = new World();
150
85
  const child: SceneAsset = {
151
86
  kind: 'scene',
152
- entities: [{ localId: 0 as never, components: { Transform: { pos: [99, 0, 0] } } }],
87
+ entities: { 'entity-0': { components: { Transform: { pos: [99, 0, 0] } } } },
153
88
  };
154
89
  const childHandle = registerSceneAsset(world, child);
155
- // Parent: 1 entity + 2 mounts of `child`
156
90
  const parent: SceneAsset = {
157
91
  kind: 'scene',
158
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
159
- mounts: [
160
- { localId: 1 as never, source: 0, memberFirst: 3 as never, memberCount: 1 },
161
- { localId: 2 as never, source: 0, memberFirst: 4 as never, memberCount: 1 },
162
- ],
92
+ entities: {
93
+ first: { components: {}, instance: { source: 'child' } },
94
+ second: { components: {}, instance: { source: 'child' } },
95
+ },
163
96
  };
164
97
  const parentHandle = registerSceneAsset(world, parent);
165
- SceneOwner.worldSetSceneAssetResolver(world, (sourceIdx, parentH) => {
166
- void sourceIdx;
167
- void parentH;
168
- return ok(childHandle);
98
+ SceneOwner.worldSetSceneAssetResolver(world, (source) => {
99
+ return typeof source === 'number'
100
+ ? ok(source as Handle<'SceneAsset', 'shared'>)
101
+ : source === 'child'
102
+ ? ok(childHandle)
103
+ : err({ code: 'asset-not-found' });
169
104
  });
170
- const r = SceneOwner.worldInstantiateScene(world, parentHandle);
171
- expect(r.ok).toBe(true);
172
- if (!r.ok) return;
173
- const inst = world.get(r.value.root, SceneInstance);
174
- if (!inst.ok) throw new Error('get SceneInstance failed');
175
- expect(inst.value.mapping).toBeInstanceOf(Uint32Array);
176
- if (!(inst.value.mapping instanceof Uint32Array)) return;
177
- // Two mount-time children at memberFirst=3 and memberFirst=4 should be
178
- // distinct entities.
179
- const m3 = inst.value.mapping[3];
180
- const m4 = inst.value.mapping[4];
181
- expect(m3).toBeDefined();
182
- expect(m4).toBeDefined();
183
- expect(m3).not.toBe(m4);
105
+ const result = SceneOwner.worldInstantiateScene(world, parentHandle);
106
+ if (!result.ok) throw new Error(JSON.stringify(result.error));
107
+ if (!result.ok) return;
108
+ const instance = world.get(result.value.root, SceneInstance);
109
+ expect(instance.ok).toBe(true);
110
+ if (!instance.ok) return;
111
+ expect(instance.value.mapping[2]).toBeDefined();
112
+ expect(instance.value.mapping[3]).toBeDefined();
113
+ expect(instance.value.mapping[2]).not.toBe(instance.value.mapping[3]);
184
114
  });
185
- });
186
115
 
187
- describe('AC-24 outer entity ChildOf points into mount sub-region', () => {
188
- it('outer entity carries ChildOf with parent=mapping[memberFirst+k]', () => {
116
+ it('parent components can address a nested child by a key path', () => {
189
117
  const world = new World();
190
118
  const child: SceneAsset = {
191
119
  kind: 'scene',
192
- entities: [
193
- { localId: 0 as never, components: { Transform: {} } },
194
- { localId: 1 as never, components: { Transform: {} } },
195
- ],
120
+ entities: {
121
+ 'entity-0': { components: { Transform: {} } },
122
+ 'entity-1': { components: { Transform: {} } },
123
+ },
196
124
  };
197
125
  const childHandle = registerSceneAsset(world, child);
198
- // Outer scene: entity[0] is independent root; entity[1] declares
199
- // ChildOf {parent: 3} — referencing the FIRST member of the mount at
200
- // localId=2 (memberFirst=3). The runtime must remap localId=3 to the
201
- // live spawned member entity.
202
126
  const parent: SceneAsset = {
203
127
  kind: 'scene',
204
- entities: [
205
- { localId: 0 as never, components: { Transform: {} } },
206
- { localId: 1 as never, components: { Transform: {}, ChildOf: { parent: 3 } } },
207
- ],
208
- mounts: [{ localId: 2 as never, source: 0, memberFirst: 3 as never, memberCount: 2 }],
128
+ entities: {
129
+ 'entity-0': { components: { Transform: {} } },
130
+ 'entity-1': {
131
+ components: { Transform: {}, ChildOf: { parent: ['child', 'entity-0'] } },
132
+ },
133
+ child: { components: {}, instance: { source: 'child' } },
134
+ },
209
135
  };
210
136
  const parentHandle = registerSceneAsset(world, parent);
211
- SceneOwner.worldSetSceneAssetResolver(world, () => ok(childHandle));
212
- const r = SceneOwner.worldInstantiateScene(world, parentHandle);
213
- expect(r.ok).toBe(true);
214
- if (!r.ok) return;
215
- const inst = world.get(r.value.root, SceneInstance);
216
- if (!inst.ok) throw new Error('get failed');
217
- expect(inst.value.mapping).toBeInstanceOf(Uint32Array);
218
- if (!(inst.value.mapping instanceof Uint32Array)) return;
219
- const outerE = inst.value.mapping[1] as unknown as number;
220
- const memberE = inst.value.mapping[3] as unknown as number;
221
- const co = world.get(outerE as never, ChildOf);
222
- expect(co.ok).toBe(true);
223
- if (!co.ok) return;
224
- expect(co.value.parent as unknown as number).toBe(memberE);
137
+ SceneOwner.worldSetSceneAssetResolver(world, (source) =>
138
+ typeof source === 'number'
139
+ ? ok(source as Handle<'SceneAsset', 'shared'>)
140
+ : source === 'child'
141
+ ? ok(childHandle)
142
+ : err({ code: 'asset-not-found' }),
143
+ );
144
+ const result = SceneOwner.worldInstantiateScene(world, parentHandle);
145
+ expect(result.ok).toBe(true);
146
+ if (!result.ok) return;
147
+ const instance = world.get(result.value.root, SceneInstance);
148
+ expect(instance.ok).toBe(true);
149
+ if (!instance.ok) return;
150
+ const outer = instance.value.mapping[1] as number;
151
+ const nested = instance.value.mapping[3] as number;
152
+ const childOf = world.get(outer as never, ChildOf);
153
+ expect(childOf.ok).toBe(true);
154
+ if (!childOf.ok) return;
155
+ expect(childOf.value.parent as unknown as number).toBe(nested);
225
156
  });
226
157
  });
227
-
228
- // Force-reference defineComponent so a future TS pruning pass keeps the
229
- // import alive even if all explicit calls are inlined into a deeper helper.
230
- void defineComponent;
@@ -10,7 +10,7 @@ import * as SceneOwner from '@forgeax/engine-scene';
10
10
  // AND the state map population);
11
11
  // - AC-20 setSceneOverride writes the override + readback equals value;
12
12
  // removeSceneOverride rolls back to the source SceneAsset value.
13
- // - detach/reattach mark the localId in state.detachedLocalIds; member
13
+ // - detach/reattach mark the runtime slot in state.detachedLocalIds; member
14
14
  // entity remains alive.
15
15
  // - setSceneOverride type mismatch -> Err 'scene-override-type-mismatch'
16
16
  // with detail.{ comp, field, expectedType, actualType }.
@@ -33,24 +33,25 @@ describe('AC-19 mount-time override apply (w20)', () => {
33
33
  const world = new World();
34
34
  const child: SceneAsset = {
35
35
  kind: 'scene',
36
- entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
36
+ entities: { 'entity-0': { components: { Transform: { pos: [1, 0, 0] } } } },
37
37
  };
38
38
  const childHandle = registerSceneAsset(world, child);
39
39
  const parent: SceneAsset = {
40
40
  kind: 'scene',
41
- entities: [],
42
- mounts: [
43
- {
44
- localId: 0 as never,
45
- source: 0,
46
- memberFirst: 1 as never,
47
- memberCount: 1,
48
- overrides: [{ localId: 1 as never, comp: 'Transform', field: 'pos', value: [42, 0, 0] }],
41
+ entities: {
42
+ instance: {
43
+ components: {},
44
+ instance: {
45
+ source: 'child',
46
+ overrides: [{ target: ['entity-0'], components: { Transform: { pos: [42, 0, 0] } } }],
47
+ },
49
48
  },
50
- ],
49
+ },
51
50
  };
52
51
  const parentHandle = registerSceneAsset(world, parent);
53
- SceneOwner.worldSetSceneAssetResolver(world, () => ok(childHandle));
52
+ SceneOwner.worldSetSceneAssetResolver(world, (source) =>
53
+ source === 'child' ? ok(childHandle) : ok(childHandle),
54
+ );
54
55
  const r = SceneOwner.worldInstantiateScene(world, parentHandle);
55
56
  if (!r.ok) throw new Error('instantiate failed');
56
57
  const inst = world.get(r.value.root, SceneInstance);
@@ -79,7 +80,7 @@ describe('world.setSceneOverride (w20)', () => {
79
80
  const world = new World();
80
81
  const asset: SceneAsset = {
81
82
  kind: 'scene',
82
- entities: [{ localId: 0 as never, components: { Transform: { pos: [1, 0, 0] } } }],
83
+ entities: { 'entity-0': { components: { Transform: { pos: [1, 0, 0] } } } },
83
84
  };
84
85
  const handle = registerSceneAsset(world, asset);
85
86
  const r = SceneOwner.worldInstantiateScene(world, handle);
@@ -112,7 +113,7 @@ describe('world.setSceneOverride (w20)', () => {
112
113
  registerSceneComponents(world, [Hp]);
113
114
  const asset: SceneAsset = {
114
115
  kind: 'scene',
115
- entities: [{ localId: 0 as never, components: { SceneOverrideHp: {} } }],
116
+ entities: { 'entity-0': { components: { SceneOverrideHp: {} } } },
116
117
  };
117
118
  const handle = registerSceneAsset(world, asset);
118
119
  const r = SceneOwner.worldInstantiateScene(world, handle);
@@ -141,7 +142,7 @@ describe('world.removeSceneOverride (w20)', () => {
141
142
  const world = new World();
142
143
  const asset: SceneAsset = {
143
144
  kind: 'scene',
144
- entities: [{ localId: 0 as never, components: { Transform: { pos: [5, 0, 0] } } }],
145
+ entities: { 'entity-0': { components: { Transform: { pos: [5, 0, 0] } } } },
145
146
  };
146
147
  const handle = registerSceneAsset(world, asset);
147
148
  const r = SceneOwner.worldInstantiateScene(world, handle);
@@ -176,7 +177,7 @@ describe('world.detachSceneMember + reattachSceneMember (w20)', () => {
176
177
  const world = new World();
177
178
  const asset: SceneAsset = {
178
179
  kind: 'scene',
179
- entities: [{ localId: 0 as never, components: { Transform: {} } }],
180
+ entities: { 'entity-0': { components: { Transform: {} } } },
180
181
  };
181
182
  const handle = registerSceneAsset(world, asset);
182
183
  const r = SceneOwner.worldInstantiateScene(world, handle);
@@ -1,4 +1,5 @@
1
1
  // w18 — Integration: compression ratio
2
+ // @perf-budget-skip: intentional multi-megabyte zstd compression-ratio gate.
2
3
  //
3
4
  // AC-05 compression ratio: compress a programmatic ~3.2MB f32 vertex
4
5
  // fixture with zstd, assert compressedSize / originalSize <= 0.70