@forgeax/engine-assets-runtime 0.1.4 → 0.1.6

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 (311) hide show
  1. package/README.md +442 -175
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  5. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  11. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  27. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  29. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  31. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  33. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  39. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  41. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  43. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  45. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  46. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  47. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  48. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  49. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  50. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  51. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  52. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  53. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  54. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  55. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  56. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  57. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  58. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  59. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  60. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  61. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  62. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  64. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  66. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  67. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  68. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  69. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  70. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  71. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  72. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  73. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  74. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  76. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  77. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  78. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  79. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  80. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  81. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  82. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  83. package/dist/__tests__/plugin.test.d.ts +2 -0
  84. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  85. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  86. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  87. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  88. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  89. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  90. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  91. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  92. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  93. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  94. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  95. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  96. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  97. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  98. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  99. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  100. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  101. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  102. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  103. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  104. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  105. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  106. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  107. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  108. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  109. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  110. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  111. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  112. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  113. package/dist/aabb.d.ts +3 -0
  114. package/dist/aabb.d.ts.map +1 -0
  115. package/dist/asset-registry.d.ts +552 -0
  116. package/dist/asset-registry.d.ts.map +1 -0
  117. package/dist/builtin-asset-registry.d.ts +22 -0
  118. package/dist/builtin-asset-registry.d.ts.map +1 -0
  119. package/dist/catalog-source.d.ts +2 -1
  120. package/dist/catalog-source.d.ts.map +1 -1
  121. package/dist/decode-image-bytes.d.ts +57 -0
  122. package/dist/decode-image-bytes.d.ts.map +1 -0
  123. package/dist/dynamic-texture-store.d.ts +89 -0
  124. package/dist/dynamic-texture-store.d.ts.map +1 -0
  125. package/dist/errors/asset.d.ts +201 -0
  126. package/dist/errors/asset.d.ts.map +1 -0
  127. package/dist/handles.d.ts +141 -0
  128. package/dist/handles.d.ts.map +1 -0
  129. package/dist/image-error.d.ts +6 -0
  130. package/dist/image-error.d.ts.map +1 -0
  131. package/dist/index.d.ts +32 -3
  132. package/dist/index.d.ts.map +1 -1
  133. package/dist/index.mjs +7218 -114
  134. package/dist/index.mjs.map +1 -1
  135. package/dist/internal.d.ts +1 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/loader-registry.d.ts +63 -0
  138. package/dist/loader-registry.d.ts.map +1 -0
  139. package/dist/loaders/inline-pack.d.ts +72 -0
  140. package/dist/loaders/inline-pack.d.ts.map +1 -0
  141. package/dist/loaders/mesh-bin.d.ts +21 -0
  142. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  143. package/dist/loaders/pack-artifact.d.ts +20 -0
  144. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  145. package/dist/material/dependency-graph.d.ts +9 -0
  146. package/dist/material/dependency-graph.d.ts.map +1 -0
  147. package/dist/material/generation-cache.d.ts +24 -0
  148. package/dist/material/generation-cache.d.ts.map +1 -0
  149. package/dist/material/inspection.d.ts +24 -0
  150. package/dist/material/inspection.d.ts.map +1 -0
  151. package/dist/material/loader.d.ts +59 -0
  152. package/dist/material/loader.d.ts.map +1 -0
  153. package/dist/material/runtime-shader.d.ts +14 -0
  154. package/dist/material/runtime-shader.d.ts.map +1 -0
  155. package/dist/mesh-bin.d.ts +3 -0
  156. package/dist/mesh-bin.d.ts.map +1 -0
  157. package/dist/mipmap-generator.d.ts +132 -0
  158. package/dist/mipmap-generator.d.ts.map +1 -0
  159. package/dist/payload-validate.d.ts +60 -0
  160. package/dist/payload-validate.d.ts.map +1 -0
  161. package/dist/plugin.d.ts +16 -0
  162. package/dist/plugin.d.ts.map +1 -0
  163. package/dist/registry/artifact-io.d.ts +16 -0
  164. package/dist/registry/artifact-io.d.ts.map +1 -0
  165. package/dist/registry/asset-evidence.d.ts +13 -0
  166. package/dist/registry/asset-evidence.d.ts.map +1 -0
  167. package/dist/registry/catalog-state.d.ts +47 -0
  168. package/dist/registry/catalog-state.d.ts.map +1 -0
  169. package/dist/registry/catalog.d.ts +48 -0
  170. package/dist/registry/catalog.d.ts.map +1 -0
  171. package/dist/registry/instantiate.d.ts +153 -0
  172. package/dist/registry/instantiate.d.ts.map +1 -0
  173. package/dist/registry/load-by-guid.d.ts +247 -0
  174. package/dist/registry/load-by-guid.d.ts.map +1 -0
  175. package/dist/registry/load-state.d.ts +26 -0
  176. package/dist/registry/load-state.d.ts.map +1 -0
  177. package/dist/registry/runtime-projection.d.ts +16 -0
  178. package/dist/registry/runtime-projection.d.ts.map +1 -0
  179. package/dist/registry/scene-publication-fence.d.ts +38 -0
  180. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  181. package/dist/registry/validate-material.d.ts +77 -0
  182. package/dist/registry/validate-material.d.ts.map +1 -0
  183. package/dist/resolve-asset-handle.d.ts +43 -0
  184. package/dist/resolve-asset-handle.d.ts.map +1 -0
  185. package/dist/resolve-tileset-runtime.d.ts +23 -0
  186. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  187. package/dist/scene-handle-fields.d.ts +88 -0
  188. package/dist/scene-handle-fields.d.ts.map +1 -0
  189. package/dist/scene-payload.d.ts +15 -0
  190. package/dist/scene-payload.d.ts.map +1 -0
  191. package/dist/wire-default-loaders.d.ts +29 -0
  192. package/dist/wire-default-loaders.d.ts.map +1 -0
  193. package/package.json +22 -9
  194. package/src/__tests__/aabb.unit.test.ts +97 -0
  195. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  196. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  197. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  198. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  199. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  200. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  201. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  202. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  203. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  204. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  205. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  206. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  207. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  208. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  209. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  210. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  211. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  212. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  213. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  214. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  215. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  216. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  217. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  218. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  219. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  220. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  221. package/src/__tests__/instantiate.unit.test.ts +684 -0
  222. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  223. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  224. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  225. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  226. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  227. package/src/__tests__/material-ready.integration.test.ts +492 -0
  228. package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
  229. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  230. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  231. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  232. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  233. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  234. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  235. package/src/__tests__/plugin.test.ts +55 -0
  236. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  237. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  238. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  239. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  240. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  241. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  242. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  243. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  244. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  245. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  246. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
  247. package/src/__tests__/static-rig.integration.test.ts +8 -0
  248. package/src/__tests__/validate-material.unit.test.ts +359 -0
  249. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  250. package/src/aabb.ts +107 -0
  251. package/src/asset-registry.ts +2040 -0
  252. package/src/builtin-asset-registry.ts +164 -0
  253. package/src/catalog-source.ts +14 -70
  254. package/src/decode-image-bytes.ts +126 -0
  255. package/src/dynamic-texture-store.ts +234 -0
  256. package/src/errors/asset.ts +347 -0
  257. package/src/handles.ts +180 -0
  258. package/src/image-error.ts +45 -0
  259. package/src/index.ts +177 -19
  260. package/src/internal.ts +1 -0
  261. package/src/loader-registry.ts +157 -0
  262. package/src/loaders/inline-pack.ts +837 -0
  263. package/src/loaders/mesh-bin.ts +304 -0
  264. package/src/loaders/pack-artifact.ts +665 -0
  265. package/src/material/dependency-graph.ts +46 -0
  266. package/src/material/generation-cache.ts +177 -0
  267. package/src/material/inspection.ts +46 -0
  268. package/src/material/loader.ts +326 -0
  269. package/src/material/runtime-shader.ts +133 -0
  270. package/src/mesh-bin.ts +5 -0
  271. package/src/mipmap-generator.ts +450 -0
  272. package/src/payload-validate.ts +573 -0
  273. package/src/plugin.ts +48 -0
  274. package/src/registry/artifact-io.ts +259 -0
  275. package/src/registry/asset-evidence.ts +51 -0
  276. package/src/registry/catalog-state.ts +409 -0
  277. package/src/registry/catalog.ts +369 -0
  278. package/src/registry/instantiate.ts +1056 -0
  279. package/src/registry/load-by-guid.ts +1657 -0
  280. package/src/registry/load-state.ts +115 -0
  281. package/src/registry/runtime-projection.ts +63 -0
  282. package/src/registry/scene-publication-fence.ts +256 -0
  283. package/src/registry/validate-material.ts +386 -0
  284. package/src/resolve-asset-handle.ts +147 -0
  285. package/src/resolve-tileset-runtime.ts +72 -0
  286. package/src/scene-handle-fields.ts +221 -0
  287. package/src/scene-payload.ts +207 -0
  288. package/src/wire-default-loaders.ts +79 -0
  289. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  290. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  291. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  292. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  294. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  297. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  298. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  301. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  302. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  304. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  305. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  306. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  307. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  308. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  309. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  310. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  311. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,684 @@
1
+ // @forgeax/engine-assets-runtime -- AssetRegistry.instantiate / instantiateFlat
2
+ // coverage (fix issue #709). Drives the scene-instantiate collaboration module
3
+ // (instantiate.ts) end-to-end through a real World + the two-tier handle
4
+ // resolver. The assertions check the structured Result surface (charter P3:
5
+ // instantiate never throws for an expected failure) rather than a specific
6
+ // spawn outcome, so the coverage does not couple to the full ECS scene-spawn
7
+ // prerequisites (node env, no GPU).
8
+
9
+ import { defineComponent, World } from '@forgeax/engine-ecs';
10
+ import type { Asset, SceneAsset } from '@forgeax/engine-types';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { AssetRegistry } from '../asset-registry';
13
+ import { resolveAssetHandle } from '../resolve-asset-handle';
14
+ import { parseScenePayload } from '../scene-payload';
15
+
16
+ const T709Tag = defineComponent('T709Tag', { value: 'f32' });
17
+ const T709MaterialCarrier = defineComponent('T709MaterialCarrier', {
18
+ materials: 'array<shared<MaterialAsset>>',
19
+ });
20
+ const MeshFilterComponent = defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
21
+ const MeshRendererComponent = defineComponent('MeshRenderer', {
22
+ materials: 'array<shared<MaterialAsset>>',
23
+ });
24
+ const Transform = defineComponent('Transform', {
25
+ posX: 'f32',
26
+ posY: 'f32',
27
+ posZ: 'f32',
28
+ });
29
+ const ChildOf = defineComponent('ChildOf', { parent: 'entity' });
30
+ const SceneInstance = defineComponent('SceneInstance', {
31
+ source: 'shared<SceneAsset>',
32
+ mapping: 'array<entity>',
33
+ state: 'unique<SceneInstanceState>',
34
+ });
35
+
36
+ const SCENE_COMPONENTS = [
37
+ T709Tag,
38
+ T709MaterialCarrier,
39
+ MeshFilterComponent,
40
+ MeshRendererComponent,
41
+ Transform,
42
+ ChildOf,
43
+ SceneInstance,
44
+ ] as const;
45
+
46
+ const MATERIAL_GUID = '11111111-1111-4111-8111-111111111111';
47
+ const CHILD_A_GUID = '22222222-2222-4222-8222-222222222222';
48
+ const CHILD_B_GUID = '33333333-3333-4333-8333-333333333333';
49
+ const PARENT_GUID = '44444444-4444-4444-8444-444444444444';
50
+ const MIGRATION_MESH_GUID = '55555555-5555-4555-8555-555555555555';
51
+ const MIGRATION_MATERIAL_A_GUID = '66666666-6666-4666-8666-666666666666';
52
+ const MIGRATION_MATERIAL_B_GUID = '77777777-7777-4777-8777-777777777777';
53
+ const MIGRATION_SCENE_GUID = '88888888-8888-4888-8888-888888888888';
54
+ const MIGRATION_MATERIAL_C_GUID = '99999999-9999-4999-8999-999999999999';
55
+ const MIGRATION_CHILD_SCENE_GUID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa';
56
+ const MIGRATION_ALT_MESH_GUID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb';
57
+ const MIGRATION_GRANDCHILD_SCENE_GUID = 'cccccccc-cccc-4ccc-8ccc-cccccccccccc';
58
+
59
+ function makeRegistry(): AssetRegistry {
60
+ return new AssetRegistry({
61
+ getMaterialShaderManifest: vi.fn().mockReturnValue(undefined),
62
+ findMaterialArtifact: vi.fn().mockReturnValue({ ok: false, error: new Error('mock') }),
63
+ getPipeline: vi.fn().mockReturnValue(undefined),
64
+ installMaterialArtifact: vi.fn(),
65
+ inspect: vi.fn().mockReturnValue({ materialShaders: [] }),
66
+ } as unknown as import('@forgeax/engine-shader').ShaderRegistry);
67
+ }
68
+
69
+ function makeWorld(): World {
70
+ const world = new World();
71
+ for (const component of SCENE_COMPONENTS) {
72
+ world.components.register(component).unwrap();
73
+ }
74
+ return world;
75
+ }
76
+
77
+ function twoEntityScene(): SceneAsset {
78
+ return {
79
+ kind: 'scene',
80
+ entities: [
81
+ { localId: 0 as never, components: { T709Tag: { value: 1 } } },
82
+ { localId: 1 as never, components: { T709Tag: { value: 2 } } },
83
+ ],
84
+ mounts: [],
85
+ } as unknown as SceneAsset;
86
+ }
87
+
88
+ function isResult(v: unknown): v is { ok: boolean } {
89
+ return typeof v === 'object' && v !== null && typeof (v as { ok?: unknown }).ok === 'boolean';
90
+ }
91
+
92
+ describe('AssetRegistry.instantiate', () => {
93
+ it('resolves a catalogued scene handle and returns a structured Result', () => {
94
+ const reg = makeRegistry();
95
+ const world = makeWorld();
96
+ const scene = twoEntityScene();
97
+ const handle = world.allocSharedRef('SceneAsset', scene);
98
+ const res = reg.instantiate(handle, world);
99
+ expect(isResult(res)).toBe(true);
100
+ if (res.ok) expect(typeof res.value).toBe('number');
101
+ });
102
+
103
+ it('returns an error Result when the handle does not resolve to a scene', () => {
104
+ const reg = makeRegistry();
105
+ const world = makeWorld();
106
+ const handle = world.allocSharedRef('SceneAsset', { kind: 'material' } as unknown as Asset);
107
+ const res = reg.instantiate(handle as never, world);
108
+ expect(res.ok).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe('AssetRegistry.instantiateFlat', () => {
113
+ it('drives the flat scene-materialise path to a structured Result', () => {
114
+ const reg = makeRegistry();
115
+ const world = makeWorld();
116
+ const handle = world.allocSharedRef('SceneAsset', twoEntityScene());
117
+ const res = reg.instantiateFlat(handle, world);
118
+ expect(isResult(res)).toBe(true);
119
+ if (res.ok) expect(Array.isArray(res.value)).toBe(true);
120
+ });
121
+ });
122
+
123
+ describe('scene graph GUID handle ownership', () => {
124
+ it('mints one handle per GUID across sibling mount recursion', () => {
125
+ const reg = makeRegistry();
126
+ const world = makeWorld();
127
+ const material = {
128
+ kind: 'material',
129
+ passes: [
130
+ {
131
+ name: 'Forward',
132
+ program: { module: 'forgeax::default-unlit' },
133
+ renderState: { tags: { LightMode: 'Forward' } },
134
+ },
135
+ ],
136
+ values: {},
137
+ } as unknown as Asset;
138
+ const child = (value: number): SceneAsset => ({
139
+ kind: 'scene',
140
+ entities: [
141
+ {
142
+ localId: 0 as never,
143
+ components: {
144
+ T709Tag: { value },
145
+ T709MaterialCarrier: { materials: [MATERIAL_GUID] },
146
+ },
147
+ },
148
+ ],
149
+ });
150
+ const parent: SceneAsset = {
151
+ kind: 'scene',
152
+ entities: [],
153
+ mounts: [
154
+ { localId: 0 as never, source: CHILD_A_GUID, memberFirst: 1 as never, memberCount: 1 },
155
+ { localId: 2 as never, source: CHILD_B_GUID, memberFirst: 3 as never, memberCount: 1 },
156
+ { localId: 4 as never, source: CHILD_A_GUID, memberFirst: 5 as never, memberCount: 1 },
157
+ ],
158
+ };
159
+ reg.catalog(MATERIAL_GUID, material);
160
+ reg.catalog(CHILD_A_GUID, child(1));
161
+ reg.catalog(CHILD_B_GUID, child(2));
162
+ reg.catalog(PARENT_GUID, parent);
163
+
164
+ const resolved = reg._resolveSceneGuids(parent, world, PARENT_GUID);
165
+
166
+ expect(resolved).toMatchObject({ ok: true });
167
+ if (!resolved.ok) return;
168
+ const childHandles = resolved.value.mounts?.map((mount) => mount.source) ?? [];
169
+ expect(childHandles).toHaveLength(3);
170
+ expect(childHandles[2]).toBe(childHandles[0]);
171
+ const resolvedChildren = childHandles.map((handle) =>
172
+ resolveAssetHandle<SceneAsset>(world, handle as never).unwrap(),
173
+ );
174
+ const materialHandles = resolvedChildren.map(
175
+ (scene) =>
176
+ (
177
+ scene.entities[0]?.components as Record<
178
+ string,
179
+ { readonly materials?: readonly number[] }
180
+ >
181
+ ).T709MaterialCarrier?.materials?.[0],
182
+ );
183
+ expect(materialHandles[0]).toBeGreaterThanOrEqual(1024);
184
+ expect(materialHandles[1]).toBe(materialHandles[0]);
185
+ expect(materialHandles[2]).toBe(materialHandles[0]);
186
+ });
187
+
188
+ it('reuses a catalogued payload handle across separate scene resolutions', () => {
189
+ const reg = makeRegistry();
190
+ const world = makeWorld();
191
+ const material = {
192
+ kind: 'material',
193
+ passes: [
194
+ {
195
+ name: 'Forward',
196
+ program: { module: 'forgeax::default-unlit' },
197
+ renderState: { tags: { LightMode: 'Forward' } },
198
+ },
199
+ ],
200
+ values: {},
201
+ } as unknown as Asset;
202
+ const scene: SceneAsset = {
203
+ kind: 'scene',
204
+ entities: [
205
+ {
206
+ localId: 0 as never,
207
+ components: {
208
+ T709MaterialCarrier: { materials: [MATERIAL_GUID] },
209
+ },
210
+ },
211
+ ],
212
+ };
213
+ expect(reg.catalog(MATERIAL_GUID, material).ok).toBe(true);
214
+
215
+ const first = reg._resolveSceneGuids(scene, world);
216
+ const second = reg._resolveSceneGuids(scene, world);
217
+
218
+ if (!first.ok) throw first.error;
219
+ if (!second.ok) throw second.error;
220
+ expect(first).toMatchObject({ ok: true });
221
+ expect(second).toMatchObject({ ok: true });
222
+ const handleOf = (resolved: SceneAsset): number | undefined =>
223
+ (
224
+ resolved.entities[0]?.components as Record<
225
+ string,
226
+ { readonly materials?: readonly number[] }
227
+ >
228
+ ).T709MaterialCarrier?.materials?.[0];
229
+ expect(handleOf(second.value)).toBe(handleOf(first.value));
230
+ expect(world.sharedRefs._liveCount()).toBe(1);
231
+ });
232
+ });
233
+
234
+ describe('serialized v2 scene material override migration', () => {
235
+ function registerMigrationAssets(reg: AssetRegistry): void {
236
+ const material = {
237
+ kind: 'material',
238
+ passes: [
239
+ {
240
+ name: 'Forward',
241
+ program: { module: 'forgeax::default-unlit' },
242
+ renderState: { tags: { LightMode: 'Forward' } },
243
+ },
244
+ ],
245
+ values: {},
246
+ } as unknown as Asset;
247
+ expect(reg.catalog(MIGRATION_MATERIAL_A_GUID, material).ok).toBe(true);
248
+ expect(reg.catalog(MIGRATION_MATERIAL_B_GUID, { ...material } as Asset).ok).toBe(true);
249
+ expect(reg.catalog(MIGRATION_MATERIAL_C_GUID, material).ok).toBe(true);
250
+ const meshResult = reg.catalog(MIGRATION_MESH_GUID, {
251
+ kind: 'mesh',
252
+ vertices: new Float32Array(12),
253
+ indices: new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0]),
254
+ attributes: {},
255
+ submeshes: [
256
+ {
257
+ indexOffset: 0,
258
+ indexCount: 3,
259
+ vertexCount: 1,
260
+ topology: 'triangle-list',
261
+ materialSlot: 1,
262
+ },
263
+ {
264
+ indexOffset: 3,
265
+ indexCount: 3,
266
+ vertexCount: 1,
267
+ topology: 'triangle-list',
268
+ materialSlot: 0,
269
+ },
270
+ {
271
+ indexOffset: 6,
272
+ indexCount: 3,
273
+ vertexCount: 1,
274
+ topology: 'triangle-list',
275
+ materialSlot: 1,
276
+ },
277
+ ],
278
+ materialSlots: [
279
+ { slotName: 'Body', sourceKey: 'material:body' },
280
+ { slotName: 'Accent', sourceKey: 'material:accent' },
281
+ ],
282
+ } as unknown as Asset);
283
+ expect(meshResult.ok).toBe(true);
284
+ const baseMesh = reg.assetCatalog.get(MIGRATION_MESH_GUID)?.payload;
285
+ if (baseMesh?.kind !== 'mesh') throw new Error('migration mesh missing');
286
+ expect(
287
+ reg.catalog(MIGRATION_ALT_MESH_GUID, {
288
+ ...baseMesh,
289
+ submeshes: baseMesh.submeshes.map((section, index) => ({
290
+ ...section,
291
+ materialSlot: index === 1 ? 1 : 0,
292
+ })),
293
+ }).ok,
294
+ ).toBe(true);
295
+ }
296
+
297
+ function parseSerialized(
298
+ materialRefIndices: readonly number[],
299
+ prefix?: readonly number[],
300
+ ): SceneAsset {
301
+ const parsed = parseScenePayload(
302
+ {
303
+ entities: [
304
+ ...(prefix === undefined
305
+ ? []
306
+ : [
307
+ {
308
+ localId: 16,
309
+ components: {
310
+ MeshFilter: { assetHandle: 0 },
311
+ MeshRenderer: { materials: prefix },
312
+ },
313
+ },
314
+ ]),
315
+ {
316
+ localId: 17,
317
+ components: {
318
+ MeshFilter: { assetHandle: 0 },
319
+ MeshRenderer: { materials: materialRefIndices },
320
+ },
321
+ },
322
+ ],
323
+ },
324
+ [
325
+ MIGRATION_MESH_GUID,
326
+ MIGRATION_MATERIAL_A_GUID,
327
+ MIGRATION_MATERIAL_B_GUID,
328
+ MIGRATION_MATERIAL_C_GUID,
329
+ ],
330
+ );
331
+ if (parsed === undefined || !('kind' in parsed))
332
+ throw new Error('serialized scene fixture did not parse');
333
+ return parsed;
334
+ }
335
+
336
+ function mountChildScene(): SceneAsset {
337
+ return {
338
+ kind: 'scene',
339
+ entities: [
340
+ {
341
+ localId: 0 as never,
342
+ components: {
343
+ MeshFilter: { assetHandle: MIGRATION_MESH_GUID as never },
344
+ MeshRenderer: { materials: [] },
345
+ },
346
+ },
347
+ ],
348
+ };
349
+ }
350
+
351
+ it('collapses equal per-section overrides in the resolved scene saved by the Editor', () => {
352
+ const reg = makeRegistry();
353
+ const world = makeWorld();
354
+ registerMigrationAssets(reg);
355
+ const scene = parseSerialized([1, 2, 1]);
356
+ reg.catalog(MIGRATION_SCENE_GUID, scene, [
357
+ {
358
+ guid: MIGRATION_MESH_GUID,
359
+ sceneEntityId: 17,
360
+ sourceField: { componentName: 'MeshFilter', fieldName: 'assetHandle' },
361
+ },
362
+ {
363
+ guid: MIGRATION_MATERIAL_A_GUID,
364
+ sceneEntityId: 17,
365
+ sourceField: { componentName: 'MeshRenderer', fieldName: 'materials', arrayIndex: 0 },
366
+ },
367
+ {
368
+ guid: MIGRATION_MATERIAL_B_GUID,
369
+ sceneEntityId: 17,
370
+ sourceField: { componentName: 'MeshRenderer', fieldName: 'materials', arrayIndex: 1 },
371
+ },
372
+ {
373
+ guid: MIGRATION_MATERIAL_A_GUID,
374
+ sceneEntityId: 17,
375
+ sourceField: { componentName: 'MeshRenderer', fieldName: 'materials', arrayIndex: 2 },
376
+ },
377
+ ]);
378
+
379
+ const result = reg._resolveSceneGuids(scene, world, MIGRATION_SCENE_GUID);
380
+ if (!result.ok) throw result.error;
381
+ const materials = (result.value.entities[0]?.components.MeshRenderer as { materials: number[] })
382
+ .materials;
383
+ expect(materials).toHaveLength(2);
384
+ expect(resolveAssetHandle<Asset>(world, materials[0] as never).unwrap()).toBe(
385
+ reg.assetCatalog.get(MIGRATION_MATERIAL_B_GUID)?.payload,
386
+ );
387
+ expect(resolveAssetHandle<Asset>(world, materials[1] as never).unwrap()).toBe(
388
+ reg.assetCatalog.get(MIGRATION_MATERIAL_A_GUID)?.payload,
389
+ );
390
+ });
391
+
392
+ it('fails before rewriting the serialized scene when section overrides conflict', () => {
393
+ const reg = makeRegistry();
394
+ const world = makeWorld();
395
+ registerMigrationAssets(reg);
396
+ const scene = parseSerialized([1, 2, 3], [1, 2, 1]);
397
+ const before = JSON.stringify(scene);
398
+ reg.catalog(MIGRATION_SCENE_GUID, scene);
399
+ const liveBefore = world.sharedRefs._liveCount();
400
+
401
+ const result = reg._resolveSceneGuids(scene, world, MIGRATION_SCENE_GUID);
402
+ expect(result.ok).toBe(false);
403
+ if (!result.ok) {
404
+ expect(result.error.detail).toMatchObject({
405
+ code: 'mesh-material-slot-override-conflict',
406
+ meshGuid: MIGRATION_MESH_GUID,
407
+ sceneGuid: MIGRATION_SCENE_GUID,
408
+ entityId: 17,
409
+ materialSlot: 1,
410
+ submeshIndices: [0, 2],
411
+ overrideGuids: [MIGRATION_MATERIAL_A_GUID, MIGRATION_MATERIAL_C_GUID],
412
+ });
413
+ }
414
+ expect(JSON.stringify(scene)).toBe(before);
415
+ expect(world.sharedRefs._liveCount()).toBe(liveBefore);
416
+ });
417
+
418
+ it('preflights nested mount conflicts before parent references touch the World', () => {
419
+ const reg = makeRegistry();
420
+ const world = makeWorld();
421
+ registerMigrationAssets(reg);
422
+ const child = parseSerialized([1, 2, 3]);
423
+ const parent: SceneAsset = {
424
+ kind: 'scene',
425
+ entities: [
426
+ {
427
+ localId: 0 as never,
428
+ components: { T709MaterialCarrier: { materials: [MIGRATION_MATERIAL_B_GUID] } },
429
+ },
430
+ ],
431
+ mounts: [
432
+ {
433
+ localId: 1 as never,
434
+ source: MIGRATION_CHILD_SCENE_GUID,
435
+ memberFirst: 2 as never,
436
+ memberCount: 1,
437
+ },
438
+ ],
439
+ };
440
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, child);
441
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
442
+ const liveBefore = world.sharedRefs._liveCount();
443
+
444
+ const result = reg._resolveSceneGuids(parent, world, MIGRATION_SCENE_GUID);
445
+ expect(result.ok).toBe(false);
446
+ expect(world.sharedRefs._liveCount()).toBe(liveBefore);
447
+ });
448
+
449
+ it('migrates PATCH mount materials using the mounted member mesh', () => {
450
+ const reg = makeRegistry();
451
+ const world = makeWorld();
452
+ registerMigrationAssets(reg);
453
+ const child = mountChildScene();
454
+ const parent: SceneAsset = {
455
+ kind: 'scene',
456
+ entities: [],
457
+ mounts: [
458
+ {
459
+ localId: 0 as never,
460
+ source: MIGRATION_CHILD_SCENE_GUID,
461
+ memberFirst: 1 as never,
462
+ memberCount: 1,
463
+ overrides: [
464
+ {
465
+ localId: 1 as never,
466
+ comp: 'MeshRenderer',
467
+ field: 'materials',
468
+ value: [
469
+ MIGRATION_MATERIAL_A_GUID,
470
+ MIGRATION_MATERIAL_B_GUID,
471
+ MIGRATION_MATERIAL_A_GUID,
472
+ ],
473
+ },
474
+ ],
475
+ },
476
+ ],
477
+ };
478
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, child);
479
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
480
+
481
+ const result = reg._resolveSceneGuids(parent, world, MIGRATION_SCENE_GUID);
482
+ if (!result.ok) throw result.error;
483
+ const materials = result.value.mounts?.[0]?.overrides?.[0]?.value as number[];
484
+ expect(materials).toHaveLength(2);
485
+ expect(resolveAssetHandle<Asset>(world, materials[0] as never).unwrap()).toBe(
486
+ reg.assetCatalog.get(MIGRATION_MATERIAL_B_GUID)?.payload,
487
+ );
488
+ expect(resolveAssetHandle<Asset>(world, materials[1] as never).unwrap()).toBe(
489
+ reg.assetCatalog.get(MIGRATION_MATERIAL_A_GUID)?.payload,
490
+ );
491
+ });
492
+
493
+ it('applies an earlier MeshFilter override before migrating UPSERT mount materials', () => {
494
+ const reg = makeRegistry();
495
+ const world = makeWorld();
496
+ registerMigrationAssets(reg);
497
+ const child = mountChildScene();
498
+ const parent: SceneAsset = {
499
+ kind: 'scene',
500
+ entities: [],
501
+ mounts: [
502
+ {
503
+ localId: 0 as never,
504
+ source: MIGRATION_CHILD_SCENE_GUID,
505
+ memberFirst: 1 as never,
506
+ memberCount: 1,
507
+ overrides: [
508
+ {
509
+ localId: 1 as never,
510
+ comp: 'MeshFilter',
511
+ field: 'assetHandle',
512
+ value: MIGRATION_ALT_MESH_GUID,
513
+ },
514
+ {
515
+ localId: 1 as never,
516
+ comp: 'MeshRenderer',
517
+ value: {
518
+ materials: [
519
+ MIGRATION_MATERIAL_A_GUID,
520
+ MIGRATION_MATERIAL_B_GUID,
521
+ MIGRATION_MATERIAL_A_GUID,
522
+ ],
523
+ },
524
+ },
525
+ ],
526
+ },
527
+ ],
528
+ };
529
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, child);
530
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
531
+
532
+ const result = reg._resolveSceneGuids(parent, world, MIGRATION_SCENE_GUID);
533
+ if (!result.ok) throw result.error;
534
+ const value = result.value.mounts?.[0]?.overrides?.[1]?.value as { materials: number[] };
535
+ expect(value.materials).toHaveLength(2);
536
+ expect(resolveAssetHandle<Asset>(world, value.materials[0] as never).unwrap()).toBe(
537
+ reg.assetCatalog.get(MIGRATION_MATERIAL_A_GUID)?.payload,
538
+ );
539
+ expect(resolveAssetHandle<Asset>(world, value.materials[1] as never).unwrap()).toBe(
540
+ reg.assetCatalog.get(MIGRATION_MATERIAL_B_GUID)?.payload,
541
+ );
542
+ });
543
+
544
+ it('rejects conflicting mount materials before any World mutation', () => {
545
+ const reg = makeRegistry();
546
+ const world = makeWorld();
547
+ registerMigrationAssets(reg);
548
+ const child = mountChildScene();
549
+ const parent: SceneAsset = {
550
+ kind: 'scene',
551
+ entities: [],
552
+ mounts: [
553
+ {
554
+ localId: 0 as never,
555
+ source: MIGRATION_CHILD_SCENE_GUID,
556
+ memberFirst: 1 as never,
557
+ memberCount: 1,
558
+ overrides: [
559
+ {
560
+ localId: 1 as never,
561
+ comp: 'MeshRenderer',
562
+ field: 'materials',
563
+ value: [
564
+ MIGRATION_MATERIAL_A_GUID,
565
+ MIGRATION_MATERIAL_B_GUID,
566
+ MIGRATION_MATERIAL_C_GUID,
567
+ ],
568
+ },
569
+ ],
570
+ },
571
+ ],
572
+ };
573
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, child);
574
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
575
+ const liveBefore = world.sharedRefs._liveCount();
576
+
577
+ const result = reg._resolveSceneGuids(parent, world, MIGRATION_SCENE_GUID);
578
+ expect(result.ok).toBe(false);
579
+ expect(world.sharedRefs._liveCount()).toBe(liveBefore);
580
+ });
581
+
582
+ it('migrates an override that targets a nested mount member slot', () => {
583
+ const reg = makeRegistry();
584
+ const world = makeWorld();
585
+ registerMigrationAssets(reg);
586
+ const grandchild = mountChildScene();
587
+ const child: SceneAsset = {
588
+ kind: 'scene',
589
+ entities: [],
590
+ mounts: [
591
+ {
592
+ localId: 0 as never,
593
+ source: MIGRATION_GRANDCHILD_SCENE_GUID,
594
+ memberFirst: 1 as never,
595
+ memberCount: 1,
596
+ },
597
+ ],
598
+ };
599
+ const parent: SceneAsset = {
600
+ kind: 'scene',
601
+ entities: [],
602
+ mounts: [
603
+ {
604
+ localId: 0 as never,
605
+ source: MIGRATION_CHILD_SCENE_GUID,
606
+ memberFirst: 1 as never,
607
+ memberCount: 2,
608
+ overrides: [
609
+ {
610
+ localId: 2 as never,
611
+ comp: 'MeshRenderer',
612
+ field: 'materials',
613
+ value: [
614
+ MIGRATION_MATERIAL_A_GUID,
615
+ MIGRATION_MATERIAL_B_GUID,
616
+ MIGRATION_MATERIAL_A_GUID,
617
+ ],
618
+ },
619
+ ],
620
+ },
621
+ ],
622
+ };
623
+ reg.catalog(MIGRATION_GRANDCHILD_SCENE_GUID, grandchild);
624
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, child);
625
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
626
+
627
+ const result = reg._resolveSceneGuids(parent, world, MIGRATION_SCENE_GUID);
628
+ if (!result.ok) throw result.error;
629
+ const materials = result.value.mounts?.[0]?.overrides?.[0]?.value as number[];
630
+ expect(materials).toHaveLength(2);
631
+ expect(resolveAssetHandle<Asset>(world, materials[0] as never).unwrap()).toBe(
632
+ reg.assetCatalog.get(MIGRATION_MATERIAL_B_GUID)?.payload,
633
+ );
634
+ });
635
+
636
+ it('migrates material arrays authored on the mount entity itself', () => {
637
+ const reg = makeRegistry();
638
+ const world = makeWorld();
639
+ registerMigrationAssets(reg);
640
+ const parent: SceneAsset = {
641
+ kind: 'scene',
642
+ entities: [],
643
+ mounts: [
644
+ {
645
+ localId: 0 as never,
646
+ source: MIGRATION_CHILD_SCENE_GUID,
647
+ memberFirst: 1 as never,
648
+ memberCount: 1,
649
+ components: {
650
+ MeshFilter: { assetHandle: MIGRATION_MESH_GUID as never },
651
+ MeshRenderer: {
652
+ materials: [
653
+ MIGRATION_MATERIAL_A_GUID,
654
+ MIGRATION_MATERIAL_B_GUID,
655
+ MIGRATION_MATERIAL_A_GUID,
656
+ ] as never,
657
+ },
658
+ },
659
+ },
660
+ ],
661
+ };
662
+ reg.catalog(MIGRATION_CHILD_SCENE_GUID, mountChildScene());
663
+ reg.catalog(MIGRATION_SCENE_GUID, parent);
664
+
665
+ const parentHandle = world.allocSharedRef('SceneAsset', parent);
666
+ const result = reg.instantiateFlat(parentHandle, world);
667
+ if (!result.ok) throw result.error;
668
+ const carrier = result.value.find((entity) => world.get(entity, MeshFilterComponent).ok);
669
+ expect(carrier).toBeDefined();
670
+ if (carrier === undefined) return;
671
+ const meshHandle = world.get(carrier, MeshFilterComponent).unwrap().assetHandle;
672
+ const materials = world.get(carrier, MeshRendererComponent).unwrap().materials;
673
+ expect(resolveAssetHandle<Asset>(world, meshHandle as never).unwrap()).toBe(
674
+ reg.assetCatalog.get(MIGRATION_MESH_GUID)?.payload,
675
+ );
676
+ expect(materials).toHaveLength(2);
677
+ expect(resolveAssetHandle<Asset>(world, materials[0] as never).unwrap()).toBe(
678
+ reg.assetCatalog.get(MIGRATION_MATERIAL_B_GUID)?.payload,
679
+ );
680
+ expect(resolveAssetHandle<Asset>(world, materials[1] as never).unwrap()).toBe(
681
+ reg.assetCatalog.get(MIGRATION_MATERIAL_A_GUID)?.payload,
682
+ );
683
+ });
684
+ });