@forgeax/engine-assets-runtime 0.0.0-dev.8d955ade1c79

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 (320) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +491 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/aabb.unit.test.d.ts +2 -0
  5. package/dist/__tests__/aabb.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/artifact-integrity.unit.test.d.ts +2 -0
  7. package/dist/__tests__/artifact-integrity.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/artifact-io.unit.test.d.ts +2 -0
  9. package/dist/__tests__/artifact-io.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts +2 -0
  11. package/dist/__tests__/asset-evidence-sdk.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/asset-evidence.unit.test.d.ts +2 -0
  13. package/dist/__tests__/asset-evidence.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/asset-kind.unit.test.d.ts +2 -0
  15. package/dist/__tests__/asset-kind.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts +2 -0
  17. package/dist/__tests__/asset-registry-catalog-reconcile.integration.test.d.ts.map +1 -0
  18. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts +2 -0
  19. package/dist/__tests__/asset-registry-catalog-reconcile.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts +2 -0
  21. package/dist/__tests__/asset-registry-catalog.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  23. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  24. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  25. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts +2 -0
  27. package/dist/__tests__/asset-runtime-error-code-owner.test-d.d.ts.map +1 -0
  28. package/dist/__tests__/catalog-replica.unit.test.d.ts +2 -0
  29. package/dist/__tests__/catalog-replica.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/catalog-source-v2.unit.test.d.ts +2 -0
  31. package/dist/__tests__/catalog-source-v2.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/catalog-source.unit.test.d.ts +2 -0
  33. package/dist/__tests__/catalog-source.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts +2 -0
  35. package/dist/__tests__/catalog-subscription-order.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/catalog-subscription.unit.test.d.ts +2 -0
  37. package/dist/__tests__/catalog-subscription.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts +2 -0
  39. package/dist/__tests__/decode-image-bytes-pass-through.test.d.ts.map +1 -0
  40. package/dist/__tests__/decode-image-bytes.browser.test.d.ts +2 -0
  41. package/dist/__tests__/decode-image-bytes.browser.test.d.ts.map +1 -0
  42. package/dist/__tests__/decode-image-bytes.test-d.d.ts +2 -0
  43. package/dist/__tests__/decode-image-bytes.test-d.d.ts.map +1 -0
  44. package/dist/__tests__/decode-image-bytes.test.d.ts +2 -0
  45. package/dist/__tests__/decode-image-bytes.test.d.ts.map +1 -0
  46. package/dist/__tests__/decode-image-mime-owner.test.d.ts +2 -0
  47. package/dist/__tests__/decode-image-mime-owner.test.d.ts.map +1 -0
  48. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  49. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/errors-asset.unit.test.d.ts +2 -0
  51. package/dist/__tests__/errors-asset.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts +2 -0
  53. package/dist/__tests__/inline-pack-loaders.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/instantiate-context.unit.test.d.ts +2 -0
  55. package/dist/__tests__/instantiate-context.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts +2 -0
  57. package/dist/__tests__/instantiate-publication-fence.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/instantiate.unit.test.d.ts +2 -0
  59. package/dist/__tests__/instantiate.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/loader-input-contract.unit.test.d.ts +2 -0
  61. package/dist/__tests__/loader-input-contract.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/loader-registry.unit.test.d.ts +2 -0
  63. package/dist/__tests__/loader-registry.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts +2 -0
  65. package/dist/__tests__/material-dependency-invalidation.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/material-generation-cache.unit.test.d.ts +2 -0
  67. package/dist/__tests__/material-generation-cache.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/material-load-consumer.test-d.d.ts +2 -0
  69. package/dist/__tests__/material-load-consumer.test-d.d.ts.map +1 -0
  70. package/dist/__tests__/material-ready.integration.test.d.ts +2 -0
  71. package/dist/__tests__/material-ready.integration.test.d.ts.map +1 -0
  72. package/dist/__tests__/material-stale-generation.unit.test.d.ts +2 -0
  73. package/dist/__tests__/material-stale-generation.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts +2 -0
  75. package/dist/__tests__/mesh-bin-morph-v3.test.d.ts.map +1 -0
  76. package/dist/__tests__/mesh-bin.unit.test.d.ts +2 -0
  77. package/dist/__tests__/mesh-bin.unit.test.d.ts.map +1 -0
  78. package/dist/__tests__/mipmap-generator.unit.test.d.ts +2 -0
  79. package/dist/__tests__/mipmap-generator.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts +2 -0
  81. package/dist/__tests__/pack-artifact-transcode-model-owner.test.d.ts.map +1 -0
  82. package/dist/__tests__/pack-basis-load.integration.test.d.ts +2 -0
  83. package/dist/__tests__/pack-basis-load.integration.test.d.ts.map +1 -0
  84. package/dist/__tests__/payload-validate.unit.test.d.ts +2 -0
  85. package/dist/__tests__/payload-validate.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/plugin.test.d.ts +2 -0
  87. package/dist/__tests__/plugin.test.d.ts.map +1 -0
  88. package/dist/__tests__/registry-concurrency.bench.test.d.ts +2 -0
  89. package/dist/__tests__/registry-concurrency.bench.test.d.ts.map +1 -0
  90. package/dist/__tests__/registry-failure-retry.integration.test.d.ts +2 -0
  91. package/dist/__tests__/registry-failure-retry.integration.test.d.ts.map +1 -0
  92. package/dist/__tests__/registry-load-graph.integration.test.d.ts +2 -0
  93. package/dist/__tests__/registry-load-graph.integration.test.d.ts.map +1 -0
  94. package/dist/__tests__/registry-load-state.unit.test.d.ts +2 -0
  95. package/dist/__tests__/registry-load-state.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/registry-package-cache.unit.test.d.ts +2 -0
  97. package/dist/__tests__/registry-package-cache.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/registry-public-ready.unit.test.d.ts +2 -0
  99. package/dist/__tests__/registry-public-ready.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  101. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/scene-handle-fields.unit.test.d.ts +2 -0
  103. package/dist/__tests__/scene-handle-fields.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/scene-payload.unit.test.d.ts +2 -0
  105. package/dist/__tests__/scene-payload.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/scene-publication-fence.test.d.ts +2 -0
  107. package/dist/__tests__/scene-publication-fence.test.d.ts.map +1 -0
  108. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts +2 -0
  109. package/dist/__tests__/scriptable-pack-public-surface.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/static-rig.integration.test.d.ts +2 -0
  111. package/dist/__tests__/static-rig.integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/validate-material.unit.test.d.ts +2 -0
  113. package/dist/__tests__/validate-material.unit.test.d.ts.map +1 -0
  114. package/dist/__tests__/video-pack-loader.integration.test.d.ts +2 -0
  115. package/dist/__tests__/video-pack-loader.integration.test.d.ts.map +1 -0
  116. package/dist/aabb.d.ts +3 -0
  117. package/dist/aabb.d.ts.map +1 -0
  118. package/dist/asset-kind.d.ts +4 -0
  119. package/dist/asset-kind.d.ts.map +1 -0
  120. package/dist/asset-registry.d.ts +554 -0
  121. package/dist/asset-registry.d.ts.map +1 -0
  122. package/dist/builtin-asset-registry.d.ts +22 -0
  123. package/dist/builtin-asset-registry.d.ts.map +1 -0
  124. package/dist/catalog-source.d.ts +24 -0
  125. package/dist/catalog-source.d.ts.map +1 -0
  126. package/dist/decode-image-bytes.d.ts +57 -0
  127. package/dist/decode-image-bytes.d.ts.map +1 -0
  128. package/dist/dynamic-texture-store.d.ts +89 -0
  129. package/dist/dynamic-texture-store.d.ts.map +1 -0
  130. package/dist/errors/asset.d.ts +201 -0
  131. package/dist/errors/asset.d.ts.map +1 -0
  132. package/dist/handles.d.ts +143 -0
  133. package/dist/handles.d.ts.map +1 -0
  134. package/dist/image-error.d.ts +6 -0
  135. package/dist/image-error.d.ts.map +1 -0
  136. package/dist/index.d.ts +34 -0
  137. package/dist/index.d.ts.map +1 -0
  138. package/dist/index.mjs +8359 -0
  139. package/dist/index.mjs.map +1 -0
  140. package/dist/internal/artifact-cache.d.ts +17 -0
  141. package/dist/internal/artifact-cache.d.ts.map +1 -0
  142. package/dist/internal/asset-graph.d.ts +70 -0
  143. package/dist/internal/asset-graph.d.ts.map +1 -0
  144. package/dist/internal/catalog-session.d.ts +60 -0
  145. package/dist/internal/catalog-session.d.ts.map +1 -0
  146. package/dist/internal/decoder-registry.d.ts +19 -0
  147. package/dist/internal/decoder-registry.d.ts.map +1 -0
  148. package/dist/internal/immutable-payload.d.ts +9 -0
  149. package/dist/internal/immutable-payload.d.ts.map +1 -0
  150. package/dist/internal/load-asset.d.ts +31 -0
  151. package/dist/internal/load-asset.d.ts.map +1 -0
  152. package/dist/internal/pack-reader.d.ts +18 -0
  153. package/dist/internal/pack-reader.d.ts.map +1 -0
  154. package/dist/internal/validate-runtime-row.d.ts +7 -0
  155. package/dist/internal/validate-runtime-row.d.ts.map +1 -0
  156. package/dist/internal.d.ts +3 -0
  157. package/dist/internal.d.ts.map +1 -0
  158. package/dist/internal.mjs +329 -0
  159. package/dist/internal.mjs.map +1 -0
  160. package/dist/loader-registry.d.ts +63 -0
  161. package/dist/loader-registry.d.ts.map +1 -0
  162. package/dist/loaders/inline-pack.d.ts +72 -0
  163. package/dist/loaders/inline-pack.d.ts.map +1 -0
  164. package/dist/loaders/mesh-bin.d.ts +21 -0
  165. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  166. package/dist/loaders/pack-artifact.d.ts +20 -0
  167. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  168. package/dist/material/dependency-graph.d.ts +9 -0
  169. package/dist/material/dependency-graph.d.ts.map +1 -0
  170. package/dist/material/generation-cache.d.ts +24 -0
  171. package/dist/material/generation-cache.d.ts.map +1 -0
  172. package/dist/material/inspection.d.ts +24 -0
  173. package/dist/material/inspection.d.ts.map +1 -0
  174. package/dist/material/loader.d.ts +59 -0
  175. package/dist/material/loader.d.ts.map +1 -0
  176. package/dist/material/runtime-shader.d.ts +14 -0
  177. package/dist/material/runtime-shader.d.ts.map +1 -0
  178. package/dist/mesh-bin.d.ts +3 -0
  179. package/dist/mesh-bin.d.ts.map +1 -0
  180. package/dist/mipmap-generator.d.ts +132 -0
  181. package/dist/mipmap-generator.d.ts.map +1 -0
  182. package/dist/payload-validate.d.ts +60 -0
  183. package/dist/payload-validate.d.ts.map +1 -0
  184. package/dist/plugin.d.ts +16 -0
  185. package/dist/plugin.d.ts.map +1 -0
  186. package/dist/registry/artifact-io.d.ts +16 -0
  187. package/dist/registry/artifact-io.d.ts.map +1 -0
  188. package/dist/registry/asset-evidence.d.ts +13 -0
  189. package/dist/registry/asset-evidence.d.ts.map +1 -0
  190. package/dist/registry/catalog-state.d.ts +47 -0
  191. package/dist/registry/catalog-state.d.ts.map +1 -0
  192. package/dist/registry/catalog.d.ts +48 -0
  193. package/dist/registry/catalog.d.ts.map +1 -0
  194. package/dist/registry/instantiate.d.ts +153 -0
  195. package/dist/registry/instantiate.d.ts.map +1 -0
  196. package/dist/registry/load-by-guid.d.ts +247 -0
  197. package/dist/registry/load-by-guid.d.ts.map +1 -0
  198. package/dist/registry/load-state.d.ts +26 -0
  199. package/dist/registry/load-state.d.ts.map +1 -0
  200. package/dist/registry/runtime-projection.d.ts +16 -0
  201. package/dist/registry/runtime-projection.d.ts.map +1 -0
  202. package/dist/registry/scene-publication-fence.d.ts +38 -0
  203. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  204. package/dist/registry/validate-material.d.ts +77 -0
  205. package/dist/registry/validate-material.d.ts.map +1 -0
  206. package/dist/resolve-asset-handle.d.ts +43 -0
  207. package/dist/resolve-asset-handle.d.ts.map +1 -0
  208. package/dist/resolve-tileset-runtime.d.ts +23 -0
  209. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  210. package/dist/scene-handle-fields.d.ts +88 -0
  211. package/dist/scene-handle-fields.d.ts.map +1 -0
  212. package/dist/scene-payload.d.ts +15 -0
  213. package/dist/scene-payload.d.ts.map +1 -0
  214. package/dist/wire-default-loaders.d.ts +29 -0
  215. package/dist/wire-default-loaders.d.ts.map +1 -0
  216. package/package.json +76 -0
  217. package/src/__tests__/aabb.unit.test.ts +97 -0
  218. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  219. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  220. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  221. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  222. package/src/__tests__/asset-kind.unit.test.ts +14 -0
  223. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  224. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  225. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  226. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  227. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  228. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  229. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  230. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  231. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  232. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  233. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  234. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  235. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  236. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  237. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  238. package/src/__tests__/decode-image-mime-owner.test.ts +38 -0
  239. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  240. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  241. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  242. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  243. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  244. package/src/__tests__/instantiate.unit.test.ts +684 -0
  245. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  246. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  247. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  248. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  249. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  250. package/src/__tests__/material-ready.integration.test.ts +508 -0
  251. package/src/__tests__/material-stale-generation.unit.test.ts +147 -0
  252. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  253. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  254. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  255. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  256. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  257. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  258. package/src/__tests__/plugin.test.ts +55 -0
  259. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  260. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  261. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  262. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  263. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  264. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  265. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  266. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  267. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  268. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  269. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +27 -0
  270. package/src/__tests__/static-rig.integration.test.ts +8 -0
  271. package/src/__tests__/validate-material.unit.test.ts +359 -0
  272. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  273. package/src/aabb.ts +107 -0
  274. package/src/asset-kind.ts +6 -0
  275. package/src/asset-registry.ts +2045 -0
  276. package/src/builtin-asset-registry.ts +164 -0
  277. package/src/catalog-source.ts +89 -0
  278. package/src/decode-image-bytes.ts +126 -0
  279. package/src/dynamic-texture-store.ts +234 -0
  280. package/src/errors/asset.ts +347 -0
  281. package/src/handles.ts +186 -0
  282. package/src/image-error.ts +45 -0
  283. package/src/index.ts +189 -0
  284. package/src/internal/artifact-cache.ts +65 -0
  285. package/src/internal/asset-graph.ts +420 -0
  286. package/src/internal/catalog-session.ts +345 -0
  287. package/src/internal/decoder-registry.ts +175 -0
  288. package/src/internal/immutable-payload.ts +20 -0
  289. package/src/internal/load-asset.ts +254 -0
  290. package/src/internal/pack-reader.ts +183 -0
  291. package/src/internal/validate-runtime-row.ts +51 -0
  292. package/src/internal.ts +6 -0
  293. package/src/loader-registry.ts +157 -0
  294. package/src/loaders/inline-pack.ts +837 -0
  295. package/src/loaders/mesh-bin.ts +304 -0
  296. package/src/loaders/pack-artifact.ts +665 -0
  297. package/src/material/dependency-graph.ts +46 -0
  298. package/src/material/generation-cache.ts +177 -0
  299. package/src/material/inspection.ts +46 -0
  300. package/src/material/loader.ts +326 -0
  301. package/src/material/runtime-shader.ts +133 -0
  302. package/src/mesh-bin.ts +5 -0
  303. package/src/mipmap-generator.ts +450 -0
  304. package/src/payload-validate.ts +573 -0
  305. package/src/plugin.ts +48 -0
  306. package/src/registry/artifact-io.ts +259 -0
  307. package/src/registry/asset-evidence.ts +51 -0
  308. package/src/registry/catalog-state.ts +409 -0
  309. package/src/registry/catalog.ts +369 -0
  310. package/src/registry/instantiate.ts +1056 -0
  311. package/src/registry/load-by-guid.ts +1657 -0
  312. package/src/registry/load-state.ts +115 -0
  313. package/src/registry/runtime-projection.ts +63 -0
  314. package/src/registry/scene-publication-fence.ts +256 -0
  315. package/src/registry/validate-material.ts +385 -0
  316. package/src/resolve-asset-handle.ts +147 -0
  317. package/src/resolve-tileset-runtime.ts +72 -0
  318. package/src/scene-handle-fields.ts +221 -0
  319. package/src/scene-payload.ts +207 -0
  320. package/src/wire-default-loaders.ts +79 -0
@@ -0,0 +1,837 @@
1
+ // @forgeax/engine-assets-runtime -- inline pack-payload loader bodies
2
+ // (feat-20260705-runtime-tier2-decomposition M1 / w4, D-4 F1 straight-cut).
3
+ // Pure move from asset-registry.ts; zero identifier changes.
4
+
5
+ import { PROCEDURAL_FLOATS_PER_VERTEX } from '@forgeax/engine-geometry';
6
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
7
+ import type {
8
+ AddressMode,
9
+ AnimationChannel,
10
+ AnimationGraph,
11
+ AnimationGraphNode,
12
+ Asset,
13
+ AssetGuid as AssetGuidType,
14
+ AudioClipAsset,
15
+ CompareFunction,
16
+ FilterMode,
17
+ LoadContext,
18
+ Loader,
19
+ MaterialAsset,
20
+ MaterialPass,
21
+ MaterialTextureCoordinates,
22
+ ParseErrorDetail,
23
+ ParticleEffectAsset,
24
+ MeshAsset as TypesMeshAsset,
25
+ } from '@forgeax/engine-types';
26
+ import { resolveMaterialTextureCoordinates } from '@forgeax/engine-types';
27
+ import { MeshBinAssetError } from '../errors/asset';
28
+ import { parseScenePayload } from '../scene-payload';
29
+ import { unpackMeshBinV4 } from './mesh-bin';
30
+ import { renderPipelineLoader, tilesetLoader } from './pack-artifact';
31
+
32
+ // === Inline pack-payload loader bodies (feat-20260603-asset-import-loader-injection
33
+ // M1 / w4) ===
34
+ //
35
+ // The eight `if (kind === ...)` arms that lived inside
36
+ // `AssetRegistry.parseAssetPayload` (research Finding 1) are extracted here as
37
+ // module-level `{ kind, load }` objects so they register into a
38
+ // `LoaderRegistry` (D-1) and can be imported by `wireDefaultLoaders` (w5). The
39
+ // body logic is copied verbatim — M1 is a pure refactor (AC-03), no behavioural
40
+ // change. Each parses an inline `.pack.json` payload synchronously and returns
41
+ // the `Asset` POD or `undefined` (parse rejected). The `scene` arm routes its
42
+ // structured out-of-bounds-ref error back through the LoaderOutput return
43
+ // value instead of the old shared instance slot (D-8 channel replaced by F21).
44
+
45
+ /** mesh loader — Float32Array / Uint16Array | Uint32Array normalisation -> MeshAsset.
46
+ *
47
+ * feat-20260611: skinIndex (Uint16Array) and skinWeight (Float32Array) accept
48
+ * both their native typed-array shape (in-memory: dawn smoke / direct
49
+ * `register` test) AND `number[]` (post-`JSON.stringify` shape produced by the
50
+ * dev-server / build-mode pack-body round-trip — `JSON.stringify(pack) -> fetch
51
+ * -> JSON.parse` flattens every typed array to a plain Array). Same dual
52
+ * contract `skeletonLoader` / `animationClipLoader` already honour (PR #350);
53
+ * without the array arm, every Fox.glb / Khronos skinned glTF surfaces as
54
+ * `asset-parse-failed` on the browser path while dawn smoke stays green.
55
+ */
56
+ export const meshLoader: Loader = {
57
+ kind: 'mesh',
58
+ load(payload) {
59
+ const vertexData = payload.vertices;
60
+ const indexData = payload.indices;
61
+ const rawAttributes = (payload.attributes as Record<string, unknown> | undefined) ?? {};
62
+ const attributes: Record<string, unknown> = { ...rawAttributes };
63
+ const rawAabb = payload.aabb;
64
+ let aabb: Float32Array | undefined;
65
+ if (rawAabb instanceof Float32Array) {
66
+ aabb = rawAabb;
67
+ } else if (Array.isArray(rawAabb)) {
68
+ aabb = new Float32Array(rawAabb as number[]);
69
+ } else if (rawAabb !== undefined) {
70
+ return undefined;
71
+ }
72
+
73
+ let morphTargets: TypesMeshAsset['morphTargets'];
74
+ const rawMorphTargets = payload.morphTargets;
75
+ if (rawMorphTargets !== undefined) {
76
+ if (
77
+ !Array.isArray(rawMorphTargets) ||
78
+ rawMorphTargets.length < 1 ||
79
+ rawMorphTargets.length > 8
80
+ ) {
81
+ return undefined;
82
+ }
83
+ const parsedTargets: NonNullable<TypesMeshAsset['morphTargets']>[number][] = [];
84
+ for (const rawTarget of rawMorphTargets) {
85
+ if (typeof rawTarget !== 'object' || rawTarget === null) return undefined;
86
+ const source = rawTarget as Record<string, unknown>;
87
+ const target: {
88
+ position?: Float32Array;
89
+ normal?: Float32Array;
90
+ tangent?: Float32Array;
91
+ } = {};
92
+ for (const [key, value] of Object.entries(source)) {
93
+ if (key !== 'position' && key !== 'normal' && key !== 'tangent') return undefined;
94
+ if (value instanceof Float32Array) target[key] = new Float32Array(value);
95
+ else if (Array.isArray(value)) target[key] = new Float32Array(value as number[]);
96
+ else return undefined;
97
+ }
98
+ if (Object.keys(target).length === 0) return undefined;
99
+ parsedTargets.push(target);
100
+ }
101
+ morphTargets = parsedTargets;
102
+ }
103
+ let morphWeights: Float32Array | undefined;
104
+ const rawMorphWeights = payload.morphWeights;
105
+ if (rawMorphWeights !== undefined) {
106
+ if (rawMorphWeights instanceof Float32Array) morphWeights = new Float32Array(rawMorphWeights);
107
+ else if (Array.isArray(rawMorphWeights))
108
+ morphWeights = new Float32Array(rawMorphWeights as number[]);
109
+ else return undefined;
110
+ if (morphTargets !== undefined && morphWeights.length !== morphTargets.length)
111
+ return undefined;
112
+ }
113
+
114
+ const skinIndexRaw = rawAttributes.skinIndex;
115
+ if (skinIndexRaw instanceof Uint16Array) {
116
+ attributes.skinIndex = skinIndexRaw;
117
+ } else if (Array.isArray(skinIndexRaw)) {
118
+ attributes.skinIndex = new Uint16Array(skinIndexRaw as number[]);
119
+ } else if (skinIndexRaw !== undefined) {
120
+ return undefined;
121
+ }
122
+
123
+ const skinWeightRaw = rawAttributes.skinWeight;
124
+ if (skinWeightRaw instanceof Float32Array) {
125
+ attributes.skinWeight = skinWeightRaw;
126
+ } else if (Array.isArray(skinWeightRaw)) {
127
+ attributes.skinWeight = new Float32Array(skinWeightRaw as number[]);
128
+ } else if (skinWeightRaw !== undefined) {
129
+ return undefined;
130
+ }
131
+
132
+ let vertices: Float32Array;
133
+ let indices: Uint16Array | Uint32Array | undefined;
134
+
135
+ if (vertexData instanceof Float32Array) {
136
+ vertices = vertexData;
137
+ } else if (Array.isArray(vertexData)) {
138
+ vertices = new Float32Array(vertexData as number[]);
139
+ } else {
140
+ return undefined;
141
+ }
142
+
143
+ // bug-20260610: index width must follow vertex count, not a hard-coded
144
+ // Uint16Array. A glTF mesh (e.g. Sponza, ~192k merged verts) overflows
145
+ // Uint16; round-tripping through Uint16Array silently wraps and
146
+ // `mesh-vertex-stride-mismatch` then fires because `maxIndex + 1` no
147
+ // longer equals `vertexCount`. Mirrors `meshIrToMeshAsset` in
148
+ // packages/gltf/src/bridge.ts which picks Uint32 above 0xffff.
149
+ //
150
+ // feat-20260612 M2 fixup: when the input carries an empty index array
151
+ // (Fox.glb-style non-indexed primitives flattened through the mesh-bin
152
+ // sidecar with `ilen=0`), drop the indices field rather than emit a
153
+ // 0-byte typed array. The downstream `gpu-resource-store` chooses the
154
+ // indexed-vs-vertex-only path on `mesh.indices !== undefined`; a 0-byte
155
+ // typed array still satisfies !== undefined and triggers a 0-size IBO
156
+ // allocation, whose `setIndexBuffer(buffer.slice(0..0), ...)` panics
157
+ // wgpu's `BufferSlice` "buffer slices can not be empty" assertion.
158
+ if (indexData instanceof Uint16Array || indexData instanceof Uint32Array) {
159
+ indices = indexData.length > 0 ? indexData : undefined;
160
+ } else if (Array.isArray(indexData)) {
161
+ const arr = indexData as number[];
162
+ if (arr.length === 0) {
163
+ indices = undefined;
164
+ } else {
165
+ const vertexCount = vertices.length / PROCEDURAL_FLOATS_PER_VERTEX;
166
+ const useUint32 = vertexCount > 0xffff;
167
+ indices = useUint32 ? new Uint32Array(arr) : new Uint16Array(arr);
168
+ }
169
+ } else if (indexData === undefined) {
170
+ indices = undefined;
171
+ } else {
172
+ return undefined;
173
+ }
174
+
175
+ // feat-20260608 M5 / w27: pack-payload mesh assets default to a single
176
+ // triangle-list submesh covering the full index/vertex range. Inline
177
+ // .pack.json mesh payloads do not carry submesh tables (single-prim
178
+ // legacy shape); render code unconditionally reads `submeshes[0]`.
179
+ // vertexCount stored as full vertices.length (downstream computes per-
180
+ // attribute strides; submesh keeps the buffer-element-count for now).
181
+ //
182
+ // bug-20260610: when the payload carries an explicit `submeshes` table
183
+ // (gltf importer emits one per primitive), respect it. The
184
+ // `triangle-list 0..indices.length` default fits only single-prim packs.
185
+ const payloadSubmeshes = payload.submeshes;
186
+ const rawSubmeshes =
187
+ Array.isArray(payloadSubmeshes) && payloadSubmeshes.length > 0
188
+ ? (payloadSubmeshes as unknown as TypesMeshAsset['submeshes'])
189
+ : [
190
+ {
191
+ indexOffset: 0,
192
+ indexCount: indices?.length ?? 0,
193
+ vertexCount: vertices.length,
194
+ topology: 'triangle-list' as const,
195
+ },
196
+ ];
197
+ const payloadSlots = payload.materialSlots;
198
+ const materialSlots: TypesMeshAsset['materialSlots'] = Array.isArray(payloadSlots)
199
+ ? payloadSlots.map((raw, slotIndex) => {
200
+ if (typeof raw !== 'object' || raw === null)
201
+ return { slotName: `LegacySlot_${slotIndex}` };
202
+ const slot = raw as Record<string, unknown>;
203
+ let defaultMaterial: AssetGuidType | undefined;
204
+ if (slot.defaultMaterial instanceof Uint8Array) {
205
+ defaultMaterial = slot.defaultMaterial as AssetGuidType;
206
+ } else if (typeof slot.defaultMaterial === 'string') {
207
+ const parsed = AssetGuid.parse(slot.defaultMaterial);
208
+ if (!parsed.ok) return { slotName: `LegacySlot_${slotIndex}` };
209
+ defaultMaterial = parsed.value;
210
+ }
211
+ return {
212
+ slotName:
213
+ typeof slot.slotName === 'string' && slot.slotName.trim().length > 0
214
+ ? slot.slotName
215
+ : `LegacySlot_${slotIndex}`,
216
+ ...(typeof slot.sourceKey === 'string' ? { sourceKey: slot.sourceKey } : {}),
217
+ ...(defaultMaterial !== undefined ? { defaultMaterial } : {}),
218
+ };
219
+ })
220
+ : rawSubmeshes.map((_, slotIndex) => ({ slotName: `LegacySlot_${slotIndex}` }));
221
+ const submeshes = rawSubmeshes.map((submesh, submeshIndex) => ({
222
+ ...submesh,
223
+ materialSlot: Number.isInteger((submesh as { materialSlot?: unknown }).materialSlot)
224
+ ? (submesh as { materialSlot: number }).materialSlot
225
+ : submeshIndex,
226
+ }));
227
+
228
+ return {
229
+ kind: 'mesh',
230
+ vertices,
231
+ ...(indices !== undefined ? { indices } : {}),
232
+ attributes: attributes as TypesMeshAsset['attributes'],
233
+ ...(aabb !== undefined ? { aabb } : {}),
234
+ ...(morphTargets !== undefined ? { morphTargets } : {}),
235
+ ...(morphWeights !== undefined ? { morphWeights } : {}),
236
+ submeshes,
237
+ materialSlots,
238
+ };
239
+ },
240
+ loadPack(input, ctx) {
241
+ const artifact = input.artifacts.body;
242
+ if (artifact === undefined) return meshLoader.load(input.payload, input.refs, ctx);
243
+ const decoded = unpackMeshBinV4(artifact.bytes, input.guid);
244
+ if (!decoded.ok) return { ok: false, error: decoded.error } as never;
245
+ const materialSlots = decoded.value.materialSlots.map((slot) => {
246
+ const refIndex = slot.defaultMaterialRef;
247
+ const ref = refIndex === undefined ? undefined : input.refs[refIndex];
248
+ if (refIndex !== undefined && ref === undefined) return undefined;
249
+ if (ref === undefined) {
250
+ return {
251
+ slotName: String(slot.slotName),
252
+ ...(typeof slot.sourceKey === 'string' ? { sourceKey: slot.sourceKey } : {}),
253
+ };
254
+ }
255
+ const parsed = AssetGuid.parse(ref);
256
+ if (!parsed.ok) return undefined;
257
+ return {
258
+ slotName: String(slot.slotName),
259
+ ...(typeof slot.sourceKey === 'string' ? { sourceKey: slot.sourceKey } : {}),
260
+ defaultMaterial: parsed.value,
261
+ };
262
+ });
263
+ if (materialSlots.some((slot) => slot === undefined)) {
264
+ return {
265
+ ok: false,
266
+ error: new MeshBinAssetError({
267
+ sourceKey: input.guid,
268
+ expected: 'material slot references must resolve through the pack refs table',
269
+ actual: 'material reference is out of bounds or is not a valid AssetGuid',
270
+ reason: 'metadata-invalid',
271
+ actualFacts: { field: 'metadata' },
272
+ }),
273
+ } as never;
274
+ }
275
+ return meshLoader.load(
276
+ {
277
+ vertices: decoded.value.vertices,
278
+ ...(decoded.value.indices !== undefined ? { indices: decoded.value.indices } : {}),
279
+ submeshes: decoded.value.submeshes,
280
+ materialSlots,
281
+ ...(decoded.value.aabb !== undefined ? { aabb: decoded.value.aabb } : {}),
282
+ ...(decoded.value.morphTargets !== undefined
283
+ ? { morphTargets: decoded.value.morphTargets }
284
+ : {}),
285
+ ...(decoded.value.morphWeights !== undefined
286
+ ? { morphWeights: decoded.value.morphWeights }
287
+ : {}),
288
+ attributes: decoded.value.attributes,
289
+ },
290
+ input.refs,
291
+ ctx,
292
+ );
293
+ },
294
+ };
295
+
296
+ /** scene loader — delegates to parseScenePayload; routes structured ref error via ctx. */
297
+ export const sceneLoader: Loader = {
298
+ kind: 'scene',
299
+ load(payload, refs, _ctx: LoadContext) {
300
+ const result = parseScenePayload(payload, refs === undefined ? undefined : [...refs]);
301
+ if (result === undefined) return undefined;
302
+ // Structured ParseSceneError (has an `index` field absent on SceneAsset):
303
+ // return it inline through LoaderOutput so the caller (parseAndReturnAsset)
304
+ // can build a precise AssetError without a shared instance slot (F21).
305
+ if ('index' in result) {
306
+ return { ok: false, error: result as ParseErrorDetail };
307
+ }
308
+ return result as Asset;
309
+ },
310
+ };
311
+
312
+ /**
313
+ * feat-20260613-material-paramschema-driven-binding M4 / w22 (D-5 graceful):
314
+ * the legacy hardcoded texture-field allowlist Set has been removed
315
+ * (AC-03). The materialLoader now consults `ctx.getMaterialShaderTextureFieldNames`
316
+ * (paramSchema-derived via derive()) to know which values fields carry
317
+ * refs[] indices. When the shader is not yet registered (cross-worktree
318
+ * shader-late-register path, plan R-4), the loader falls back to attempting
319
+ * resolution on every int-typed paramValue in [0, refs.length) — M4 / w23's
320
+ * extract-layer paramSchema validation catches misclassifications and routes
321
+ * unresolved texture slots through `MISSING_TEXTURE_HANDLE`.
322
+ */
323
+ function collectShaderTextureFieldNames(
324
+ passesFromPayload: unknown,
325
+ ctx: LoadContext,
326
+ ): ReadonlySet<string> | undefined {
327
+ if (!Array.isArray(passesFromPayload) || passesFromPayload.length === 0) return undefined;
328
+ const lookup = ctx.getMaterialShaderTextureFieldNames;
329
+ if (lookup === undefined) return undefined;
330
+ const collected = new Set<string>();
331
+ let anyResolved = false;
332
+ for (const pass of passesFromPayload) {
333
+ const shaderId = (pass as { program?: { module?: unknown } }).program?.module;
334
+ if (typeof shaderId !== 'string' || shaderId.length === 0) continue;
335
+ const fields = lookup(shaderId);
336
+ if (fields === undefined) continue;
337
+ anyResolved = true;
338
+ for (const name of fields) collected.add(name);
339
+ }
340
+ return anyResolved ? collected : undefined;
341
+ }
342
+
343
+ function refGuidAt(refs: readonly unknown[] | undefined, index: number): string | undefined {
344
+ if (!Number.isInteger(index) || index < 0 || index >= (refs?.length ?? 0)) return undefined;
345
+ const guid = refs?.[index];
346
+ if (typeof guid === 'string') return guid;
347
+ if (typeof guid === 'object' && guid !== null && 'guid' in guid) {
348
+ const nestedGuid = (guid as { guid?: unknown }).guid;
349
+ return typeof nestedGuid === 'string' ? nestedGuid : undefined;
350
+ }
351
+ return undefined;
352
+ }
353
+
354
+ function isIdentityTextureCoordinates(value: unknown): boolean {
355
+ if (value === undefined) return true;
356
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
357
+ const coordinates = value as Record<string, unknown>;
358
+ if (Object.keys(coordinates).some((key) => key !== 'set' && key !== 'transform')) return false;
359
+ const transform = coordinates.transform;
360
+ if (
361
+ transform !== undefined &&
362
+ (typeof transform !== 'object' || transform === null || Array.isArray(transform))
363
+ ) {
364
+ return false;
365
+ }
366
+ if (
367
+ transform !== undefined &&
368
+ Object.keys(transform as Record<string, unknown>).some(
369
+ (key) => key !== 'offset' && key !== 'scale' && key !== 'rotation',
370
+ )
371
+ ) {
372
+ return false;
373
+ }
374
+ const resolved = resolveMaterialTextureCoordinates(coordinates as MaterialTextureCoordinates);
375
+ return (
376
+ resolved.set === 0 &&
377
+ resolved.transform.offset[0] === 0 &&
378
+ resolved.transform.offset[1] === 0 &&
379
+ resolved.transform.scale[0] === 1 &&
380
+ resolved.transform.scale[1] === 1 &&
381
+ resolved.transform.rotation === 0
382
+ );
383
+ }
384
+
385
+ function compactTextureValue(value: Record<string, unknown>): unknown {
386
+ const compact = { ...value };
387
+ if (isIdentityTextureCoordinates(compact.coordinates)) delete compact.coordinates;
388
+ return compact;
389
+ }
390
+
391
+ /** material loader — passes + values + serialized parent GUID -> parentGuid. */
392
+ export const materialLoader: Loader = {
393
+ kind: 'material',
394
+ load(payload, refs, ctx: LoadContext) {
395
+ const matPayload = payload;
396
+ const passesFromPayload = matPayload.passes;
397
+ const rawParamValues = (matPayload.values as Record<string, unknown>) ?? {};
398
+
399
+ let parentGuid: string | undefined;
400
+ if (typeof matPayload.parent === 'string') {
401
+ parentGuid = matPayload.parent;
402
+ } else if (typeof matPayload.parent === 'number') {
403
+ const idx = matPayload.parent;
404
+ const refsArr = refs ?? [];
405
+ if (idx >= 0 && idx < refsArr.length) {
406
+ parentGuid = refGuidAt(refsArr, idx);
407
+ }
408
+ if (parentGuid === undefined) {
409
+ return undefined;
410
+ }
411
+ }
412
+
413
+ // bug-20260610: values fields that are typed `handle<TextureAsset>`
414
+ // arrive on disk as a refs[] index (small int 0..refs.length-1). The
415
+ // build-time gltfImporter writes these as refs indices, mirroring the
416
+ // scene's HANDLE_FIELD_NAMES treatment.
417
+ //
418
+ // feat-20260613-material-paramschema-driven-binding M4 / w22 (D-5 graceful):
419
+ // texture-field discovery now derives from the registered shader's
420
+ // paramSchema via `ctx.getMaterialShaderTextureFieldNames`. When the
421
+ // shader is registered (the common case), only declared texture fields
422
+ // are resolved — identical to the old hardcoded-Set behaviour without the
423
+ // SSOT duplication. Authored material parameters are an equally valid
424
+ // schema source and remain available before shader registration; this is
425
+ // what keeps a scalar zero (for example metallic) distinct from texture
426
+ // ref index zero. Only legacy payloads with neither schema retain the
427
+ // try-every-integer fallback.
428
+ const values: Record<string, unknown> = { ...rawParamValues };
429
+ if (refs && refs.length > 0) {
430
+ const shaderTextureFields = collectShaderTextureFieldNames(passesFromPayload, ctx);
431
+ const authoredTextureFields = Array.isArray(matPayload.parameters)
432
+ ? new Set(
433
+ matPayload.parameters
434
+ .filter(
435
+ (parameter): parameter is { name: string; type: string } =>
436
+ typeof parameter === 'object' &&
437
+ parameter !== null &&
438
+ 'name' in parameter &&
439
+ typeof parameter.name === 'string' &&
440
+ 'type' in parameter &&
441
+ parameter.type === 'texture',
442
+ )
443
+ .map((parameter) => parameter.name),
444
+ )
445
+ : undefined;
446
+ const textureFields =
447
+ authoredTextureFields !== undefined
448
+ ? new Set([...(shaderTextureFields ?? []), ...authoredTextureFields])
449
+ : shaderTextureFields;
450
+ // Walk every value so the structured MaterialTextureValue shape remains
451
+ // resolvable even when a shader is registered with an empty/late
452
+ // paramSchema (pbr-skin currently takes this path). Scalar values still
453
+ // require schema membership below, so metallic/roughness integers are
454
+ // never mistaken for refs indices merely because the schema is empty.
455
+ const candidateFields = Object.keys(values);
456
+ for (const fieldName of candidateFields) {
457
+ const value = values[fieldName];
458
+ if (typeof value === 'number' && Number.isInteger(value)) {
459
+ if (textureFields !== undefined && !textureFields.has(fieldName)) {
460
+ continue;
461
+ }
462
+ const refGuid = refGuidAt(refs, value);
463
+ if (refGuid === undefined) {
464
+ // Only emit a parse-error breadcrumb when the field is declared as
465
+ // a texture by the shader paramSchema (the OOB is unambiguous).
466
+ // For the graceful "try every int" fallback, OOB simply means
467
+ // "this scalar was not a refs index" — don't spam parse errors.
468
+ if (textureFields !== undefined) delete values[fieldName];
469
+ continue;
470
+ }
471
+ values[fieldName] = textureFields === undefined ? refGuid : { texture: refGuid };
472
+ continue;
473
+ }
474
+
475
+ // glTF materials use the engine's structured MaterialTextureValue
476
+ // shape (`{ texture: <refs index> }`), not the legacy scalar form.
477
+ // Resolve that nested index at the same loader boundary; leaving zero
478
+ // here is a valid-looking handle that silently renders the material
479
+ // white and never reaches the browser console as an exception.
480
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
481
+ const textureIndex = (value as { texture?: unknown }).texture;
482
+ if (typeof textureIndex !== 'number' || !Number.isInteger(textureIndex)) continue;
483
+ const textureGuid = refGuidAt(refs, textureIndex);
484
+ if (textureGuid === undefined) {
485
+ if (textureFields !== undefined) delete values[fieldName];
486
+ continue;
487
+ }
488
+ const resolved: Record<string, unknown> = {
489
+ ...(value as Record<string, unknown>),
490
+ texture: textureGuid,
491
+ };
492
+ const samplerIndex = resolved.sampler;
493
+ if (typeof samplerIndex === 'number' && Number.isInteger(samplerIndex)) {
494
+ const samplerGuid = refGuidAt(refs, samplerIndex);
495
+ if (samplerGuid === undefined) delete resolved.sampler;
496
+ else resolved.sampler = samplerGuid;
497
+ }
498
+ values[fieldName] = compactTextureValue(resolved);
499
+ }
500
+ }
501
+ }
502
+
503
+ if (Array.isArray(passesFromPayload) && passesFromPayload.length > 0) {
504
+ return {
505
+ kind: 'material',
506
+ passes: passesFromPayload as readonly MaterialPass[],
507
+ parameters: matPayload.parameters,
508
+ values,
509
+ colorSpace: matPayload.colorSpace,
510
+ parentGuid,
511
+ } as MaterialAsset & { parentGuid?: string };
512
+ }
513
+
514
+ if (parentGuid !== undefined) {
515
+ return {
516
+ kind: 'material',
517
+ parameters: matPayload.parameters,
518
+ values,
519
+ colorSpace: matPayload.colorSpace,
520
+ parentGuid,
521
+ } as unknown as MaterialAsset & { parentGuid?: string };
522
+ }
523
+
524
+ return undefined;
525
+ },
526
+ };
527
+
528
+ /** sampler loader — preserve the serialised SamplerAsset descriptor. */
529
+ export const samplerLoader: Loader = {
530
+ kind: 'sampler',
531
+ load(payload) {
532
+ const filters = new Set<FilterMode>(['nearest', 'linear']);
533
+ const mipmapFilters = new Set<GPUMipmapFilterMode>(['nearest', 'linear']);
534
+ const addressModes = new Set<AddressMode>(['clamp-to-edge', 'repeat', 'mirror-repeat']);
535
+ const compareFunctions = new Set<CompareFunction>([
536
+ 'never',
537
+ 'less',
538
+ 'equal',
539
+ 'less-equal',
540
+ 'greater',
541
+ 'not-equal',
542
+ 'greater-equal',
543
+ 'always',
544
+ ]);
545
+ const stringField = <T extends string>(
546
+ key: string,
547
+ allowed: ReadonlySet<T>,
548
+ ): T | undefined | null => {
549
+ const value = payload[key];
550
+ if (value === undefined) return undefined;
551
+ return typeof value === 'string' && allowed.has(value as T) ? (value as T) : null;
552
+ };
553
+ const magFilter = stringField('magFilter', filters);
554
+ const minFilter = stringField('minFilter', filters);
555
+ const mipmapFilter = stringField('mipmapFilter', mipmapFilters);
556
+ const addressModeU = stringField('addressModeU', addressModes);
557
+ const addressModeV = stringField('addressModeV', addressModes);
558
+ const addressModeW = stringField('addressModeW', addressModes);
559
+ const compare = stringField('compare', compareFunctions);
560
+ if (
561
+ magFilter === null ||
562
+ minFilter === null ||
563
+ mipmapFilter === null ||
564
+ addressModeU === null ||
565
+ addressModeV === null ||
566
+ addressModeW === null ||
567
+ compare === null
568
+ ) {
569
+ return undefined;
570
+ }
571
+ const numericField = (key: string): number | undefined | null => {
572
+ const value = payload[key];
573
+ if (value === undefined) return undefined;
574
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
575
+ };
576
+ const lodMinClamp = numericField('lodMinClamp');
577
+ const lodMaxClamp = numericField('lodMaxClamp');
578
+ const maxAnisotropy = numericField('maxAnisotropy');
579
+ if (lodMinClamp === null || lodMaxClamp === null || maxAnisotropy === null) return undefined;
580
+ return {
581
+ kind: 'sampler',
582
+ ...(magFilter === undefined ? {} : { magFilter }),
583
+ ...(minFilter === undefined ? {} : { minFilter }),
584
+ ...(mipmapFilter === undefined ? {} : { mipmapFilter }),
585
+ ...(addressModeU === undefined ? {} : { addressModeU }),
586
+ ...(addressModeV === undefined ? {} : { addressModeV }),
587
+ ...(addressModeW === undefined ? {} : { addressModeW }),
588
+ ...(lodMinClamp === undefined ? {} : { lodMinClamp }),
589
+ ...(lodMaxClamp === undefined ? {} : { lodMaxClamp }),
590
+ ...(compare === undefined ? {} : { compare }),
591
+ ...(maxAnisotropy === undefined ? {} : { maxAnisotropy }),
592
+ };
593
+ },
594
+ };
595
+
596
+ /** skeleton loader — inverseBindMatrices stride validation.
597
+ *
598
+ * bug-20260611: accept both `Float32Array` (in-memory: dawn smoke / direct
599
+ * `register` test) AND `number[]` (post-`JSON.stringify` shape: `normaliseForPack`
600
+ * in @forgeax/engine-import flattens every typed array to a plain Array so
601
+ * `JSON.stringify(pack)` survives the dev-server / build-mode round-trip --
602
+ * the same dual contract `meshLoader` already honours). Without the array arm
603
+ * the .pack.json -> fetch -> JSON.parse path lands a plain object whose
604
+ * `instanceof Float32Array` check fails, surfacing as `asset-parse-failed`
605
+ * for any glTF carrying a Skin (e.g. Khronos Fox.glb).
606
+ */
607
+ export const skeletonLoader: Loader = {
608
+ kind: 'skeleton',
609
+ load(payload) {
610
+ const ibmRaw = payload.inverseBindMatrices;
611
+ const jointCount = typeof payload.jointCount === 'number' ? payload.jointCount : 0;
612
+ let ibm: Float32Array;
613
+ if (ibmRaw instanceof Float32Array) {
614
+ ibm = ibmRaw;
615
+ } else if (Array.isArray(ibmRaw)) {
616
+ ibm = new Float32Array(ibmRaw as number[]);
617
+ } else {
618
+ return undefined;
619
+ }
620
+ if (ibm.byteLength !== jointCount * 64) return undefined;
621
+ return {
622
+ kind: 'skeleton',
623
+ inverseBindMatrices: ibm,
624
+ jointCount,
625
+ };
626
+ },
627
+ };
628
+
629
+ /** skin loader — skeletonGuid + jointPaths validation. */
630
+ export const skinLoader: Loader = {
631
+ kind: 'skin',
632
+ load(payload) {
633
+ const skeletonGuid = payload.skeletonGuid;
634
+ const jointPathsRaw = payload.jointPaths;
635
+ if (typeof skeletonGuid !== 'string') return undefined;
636
+ if (!Array.isArray(jointPathsRaw)) return undefined;
637
+ const jointPaths: string[] = [];
638
+ for (const item of jointPathsRaw) {
639
+ if (typeof item !== 'string') return undefined;
640
+ jointPaths.push(item);
641
+ }
642
+ return { kind: 'skin', skeletonGuid, jointPaths };
643
+ },
644
+ };
645
+
646
+ /** animation-clip loader — channels / sampler validation.
647
+ *
648
+ * bug-20260611: sampler.input / sampler.output accept both `Float32Array`
649
+ * (in-memory) and `number[]` (post-`JSON.stringify` shape produced by
650
+ * `normaliseForPack`). Same dual contract as `skeletonLoader` /
651
+ * `meshLoader`; without it the dev `.pack.json` round-trip surfaces every
652
+ * skinned-with-animation glTF as `asset-parse-failed`.
653
+ */
654
+ export const animationClipLoader: Loader = {
655
+ kind: 'animation-clip',
656
+ load(payload) {
657
+ const duration = typeof payload.duration === 'number' ? payload.duration : 0;
658
+ const channelsRaw = payload.channels;
659
+ if (!Array.isArray(channelsRaw)) return undefined;
660
+ const channels: AnimationChannel[] = [];
661
+ for (const ch of channelsRaw) {
662
+ if (typeof ch !== 'object' || ch === null) return undefined;
663
+ const chObj = ch as Record<string, unknown>;
664
+ const targetId = chObj.targetId;
665
+ const property = chObj.property;
666
+ const samplerObj = chObj.sampler as Record<string, unknown> | undefined;
667
+ if (
668
+ typeof targetId !== 'string' ||
669
+ !/^[0-9a-f]{32}$/.test(targetId) ||
670
+ Object.keys(chObj).some(
671
+ (key) => key !== 'targetId' && key !== 'property' && key !== 'sampler',
672
+ )
673
+ ) {
674
+ return undefined;
675
+ }
676
+ if (
677
+ property !== 'translation' &&
678
+ property !== 'rotation' &&
679
+ property !== 'scale' &&
680
+ property !== 'weights'
681
+ )
682
+ return undefined;
683
+ if (samplerObj === undefined) return undefined;
684
+ const inputRaw = samplerObj.input;
685
+ const outputRaw = samplerObj.output;
686
+ const interpolation = samplerObj.interpolation;
687
+ let input: Float32Array;
688
+ if (inputRaw instanceof Float32Array) {
689
+ input = inputRaw;
690
+ } else if (Array.isArray(inputRaw)) {
691
+ input = new Float32Array(inputRaw as number[]);
692
+ } else {
693
+ return undefined;
694
+ }
695
+ let output: Float32Array;
696
+ if (outputRaw instanceof Float32Array) {
697
+ output = outputRaw;
698
+ } else if (Array.isArray(outputRaw)) {
699
+ output = new Float32Array(outputRaw as number[]);
700
+ } else {
701
+ return undefined;
702
+ }
703
+ if (interpolation !== 'LINEAR' && interpolation !== 'STEP') return undefined;
704
+ channels.push({
705
+ targetId: targetId as AnimationChannel['targetId'],
706
+ property: property as AnimationChannel['property'],
707
+ sampler: { input, output, interpolation },
708
+ });
709
+ }
710
+ return { kind: 'animation-clip', duration, channels };
711
+ },
712
+ };
713
+
714
+ /** animation-graph loader — pack payload -> AnimationGraph POD.
715
+ *
716
+ * feat-20260713-animation-state-machine-plugin M4 / w30 (plan D-4 landing (3)):
717
+ * the inverse of `serializeAnimationGraph` (runtime). Each Clip leaf's `clip`
718
+ * field arrives as a `refs[]` index (small int); the loader resolves it back to
719
+ * the GUID string verbatim, leaving GUID -> handle re-resolution to the ECS/use
720
+ * time (the same "GUID at load, handle at use" contract materialLoader honours
721
+ * for parentGuid / values texture fields, asset-registry D-19). Blend/Add
722
+ * node references + `root` are intra-graph node indices, validated against the
723
+ * node count. A malformed payload (out-of-range refs / node index, bad node
724
+ * shape, non-finite weight) returns `undefined` so the load-by-guid path
725
+ * surfaces the existing `asset-parse-failed` error code (AC-14).
726
+ *
727
+ * OOS-7: reconstructs only an engine-authored graph's topology; no DCC-import
728
+ * metadata is consumed.
729
+ */
730
+ export const animationGraphLoader: Loader = {
731
+ kind: 'animation-graph',
732
+ load(payload, refs) {
733
+ const rawNodes = payload.nodes;
734
+ const root = payload.root;
735
+ if (!Array.isArray(rawNodes)) return undefined;
736
+ const nodeCount = rawNodes.length;
737
+ const isNodeIndex = (value: unknown): value is number =>
738
+ typeof value === 'number' && Number.isInteger(value) && value >= 0 && value < nodeCount;
739
+ if (!isNodeIndex(root)) return undefined;
740
+ const refsArr = refs ?? [];
741
+
742
+ const nodes: AnimationGraphNode[] = [];
743
+ for (const rawNode of rawNodes) {
744
+ if (typeof rawNode !== 'object' || rawNode === null) return undefined;
745
+ const node = rawNode as Record<string, unknown>;
746
+ const weight = node.weight;
747
+ if (typeof weight !== 'number' || !Number.isFinite(weight)) return undefined;
748
+
749
+ if (node.type === 'clip') {
750
+ const refIndex = node.clip;
751
+ if (typeof refIndex !== 'number' || !Number.isInteger(refIndex)) return undefined;
752
+ if (refIndex < 0 || refIndex >= refsArr.length) return undefined;
753
+ const guid = refsArr[refIndex];
754
+ if (typeof guid !== 'string') return undefined;
755
+ // GUID verbatim at load; re-resolved to a handle at use time (D-19).
756
+ nodes.push({
757
+ type: 'clip',
758
+ clip: guid,
759
+ weight,
760
+ });
761
+ } else if (node.type === 'blend') {
762
+ const children = node.children;
763
+ if (!Array.isArray(children) || !children.every(isNodeIndex)) return undefined;
764
+ nodes.push({ type: 'blend', children: [...(children as number[])], weight });
765
+ } else if (node.type === 'add') {
766
+ const base = node.base;
767
+ const additive = node.additive;
768
+ if (!isNodeIndex(base)) return undefined;
769
+ if (!Array.isArray(additive) || !additive.every(isNodeIndex)) return undefined;
770
+ nodes.push({ type: 'add', base, additive: [...(additive as number[])], weight });
771
+ } else {
772
+ return undefined;
773
+ }
774
+ }
775
+
776
+ return { kind: 'animation-graph', nodes, root } as AnimationGraph;
777
+ },
778
+ };
779
+
780
+ /** audio loader -- restore the durable descriptor; host decode stays elsewhere. */
781
+ export const audioLoader: Loader = {
782
+ kind: 'audio',
783
+ load(payload) {
784
+ const sourceKey = payload.sourceKey;
785
+ const mediaType = payload.mediaType;
786
+ if (typeof sourceKey !== 'string' || sourceKey.length === 0) return undefined;
787
+ if (typeof mediaType !== 'string' || mediaType.length === 0) return undefined;
788
+ const rawBytes = payload.bytes;
789
+ const bytes =
790
+ rawBytes instanceof Uint8Array
791
+ ? rawBytes
792
+ : Array.isArray(rawBytes)
793
+ ? new Uint8Array(rawBytes as number[])
794
+ : undefined;
795
+ return {
796
+ kind: 'audio',
797
+ sourceKey,
798
+ mediaType,
799
+ ...(bytes === undefined ? {} : { bytes }),
800
+ } as AudioClipAsset;
801
+ },
802
+ };
803
+
804
+ /** particle loader -- restore the complete cooked program without a GPU device. */
805
+ export const particleEffectLoader: Loader = {
806
+ kind: 'particle-effect',
807
+ load(payload) {
808
+ if (typeof payload.program !== 'object' || payload.program === null) return undefined;
809
+ if (typeof (payload.program as Record<string, unknown>).fingerprint !== 'string') {
810
+ return undefined;
811
+ }
812
+ return { ...payload, kind: 'particle-effect' } as ParticleEffectAsset;
813
+ },
814
+ };
815
+
816
+ export { renderPipelineLoader, tilesetLoader } from './pack-artifact';
817
+
818
+ /**
819
+ * The eight inline pack-payload loaders, in the historical `if`-chain order
820
+ * (the animation-graph loader, feat-20260713 M4 / w30, appends after the clip
821
+ * loader). `wireDefaultLoaders` (w5) registers these plus the texture / font /
822
+ * equirect loaders (w6) and the audio placeholder (w8).
823
+ */
824
+ export const INLINE_PACK_LOADERS: readonly Loader[] = [
825
+ meshLoader,
826
+ sceneLoader,
827
+ samplerLoader,
828
+ materialLoader,
829
+ skeletonLoader,
830
+ skinLoader,
831
+ animationClipLoader,
832
+ animationGraphLoader,
833
+ renderPipelineLoader,
834
+ tilesetLoader,
835
+ audioLoader,
836
+ particleEffectLoader,
837
+ ];