@forgeax/engine-assets-runtime 0.1.3 → 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 (313) 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 +2 -1
  136. package/dist/internal.d.ts.map +1 -1
  137. package/dist/internal.mjs +317 -3
  138. package/dist/internal.mjs.map +1 -1
  139. package/dist/loader-registry.d.ts +63 -0
  140. package/dist/loader-registry.d.ts.map +1 -0
  141. package/dist/loaders/inline-pack.d.ts +72 -0
  142. package/dist/loaders/inline-pack.d.ts.map +1 -0
  143. package/dist/loaders/mesh-bin.d.ts +21 -0
  144. package/dist/loaders/mesh-bin.d.ts.map +1 -0
  145. package/dist/loaders/pack-artifact.d.ts +20 -0
  146. package/dist/loaders/pack-artifact.d.ts.map +1 -0
  147. package/dist/material/dependency-graph.d.ts +9 -0
  148. package/dist/material/dependency-graph.d.ts.map +1 -0
  149. package/dist/material/generation-cache.d.ts +24 -0
  150. package/dist/material/generation-cache.d.ts.map +1 -0
  151. package/dist/material/inspection.d.ts +24 -0
  152. package/dist/material/inspection.d.ts.map +1 -0
  153. package/dist/material/loader.d.ts +59 -0
  154. package/dist/material/loader.d.ts.map +1 -0
  155. package/dist/material/runtime-shader.d.ts +14 -0
  156. package/dist/material/runtime-shader.d.ts.map +1 -0
  157. package/dist/mesh-bin.d.ts +3 -0
  158. package/dist/mesh-bin.d.ts.map +1 -0
  159. package/dist/mipmap-generator.d.ts +132 -0
  160. package/dist/mipmap-generator.d.ts.map +1 -0
  161. package/dist/payload-validate.d.ts +60 -0
  162. package/dist/payload-validate.d.ts.map +1 -0
  163. package/dist/plugin.d.ts +16 -0
  164. package/dist/plugin.d.ts.map +1 -0
  165. package/dist/registry/artifact-io.d.ts +16 -0
  166. package/dist/registry/artifact-io.d.ts.map +1 -0
  167. package/dist/registry/asset-evidence.d.ts +13 -0
  168. package/dist/registry/asset-evidence.d.ts.map +1 -0
  169. package/dist/registry/catalog-state.d.ts +47 -0
  170. package/dist/registry/catalog-state.d.ts.map +1 -0
  171. package/dist/registry/catalog.d.ts +48 -0
  172. package/dist/registry/catalog.d.ts.map +1 -0
  173. package/dist/registry/instantiate.d.ts +153 -0
  174. package/dist/registry/instantiate.d.ts.map +1 -0
  175. package/dist/registry/load-by-guid.d.ts +247 -0
  176. package/dist/registry/load-by-guid.d.ts.map +1 -0
  177. package/dist/registry/load-state.d.ts +26 -0
  178. package/dist/registry/load-state.d.ts.map +1 -0
  179. package/dist/registry/runtime-projection.d.ts +16 -0
  180. package/dist/registry/runtime-projection.d.ts.map +1 -0
  181. package/dist/registry/scene-publication-fence.d.ts +38 -0
  182. package/dist/registry/scene-publication-fence.d.ts.map +1 -0
  183. package/dist/registry/validate-material.d.ts +77 -0
  184. package/dist/registry/validate-material.d.ts.map +1 -0
  185. package/dist/resolve-asset-handle.d.ts +43 -0
  186. package/dist/resolve-asset-handle.d.ts.map +1 -0
  187. package/dist/resolve-tileset-runtime.d.ts +23 -0
  188. package/dist/resolve-tileset-runtime.d.ts.map +1 -0
  189. package/dist/scene-handle-fields.d.ts +88 -0
  190. package/dist/scene-handle-fields.d.ts.map +1 -0
  191. package/dist/scene-payload.d.ts +15 -0
  192. package/dist/scene-payload.d.ts.map +1 -0
  193. package/dist/wire-default-loaders.d.ts +29 -0
  194. package/dist/wire-default-loaders.d.ts.map +1 -0
  195. package/package.json +22 -9
  196. package/src/__tests__/aabb.unit.test.ts +97 -0
  197. package/src/__tests__/artifact-integrity.unit.test.ts +99 -0
  198. package/src/__tests__/artifact-io.unit.test.ts +127 -0
  199. package/src/__tests__/asset-evidence-sdk.integration.test.ts +32 -0
  200. package/src/__tests__/asset-evidence.unit.test.ts +32 -0
  201. package/src/__tests__/{asset-kind.test.ts → asset-kind.unit.test.ts} +1 -1
  202. package/src/__tests__/asset-registry-catalog-reconcile.integration.test.ts +74 -0
  203. package/src/__tests__/asset-registry-catalog-reconcile.unit.test.ts +89 -0
  204. package/src/__tests__/asset-registry-catalog.unit.test.ts +170 -0
  205. package/src/__tests__/asset-registry.pack-cache.test.ts +96 -0
  206. package/src/__tests__/asset-registry.unit.test.ts +511 -0
  207. package/src/__tests__/asset-runtime-error-code-owner.test-d.ts +63 -0
  208. package/src/__tests__/catalog-replica.unit.test.ts +196 -0
  209. package/src/__tests__/catalog-source-v2.unit.test.ts +158 -0
  210. package/src/__tests__/catalog-source.unit.test.ts +126 -0
  211. package/src/__tests__/catalog-subscription-order.unit.test.ts +65 -0
  212. package/src/__tests__/catalog-subscription.unit.test.ts +28 -0
  213. package/src/__tests__/decode-image-bytes-pass-through.test.ts +34 -0
  214. package/src/__tests__/decode-image-bytes.browser.test.ts +102 -0
  215. package/src/__tests__/decode-image-bytes.test-d.ts +108 -0
  216. package/src/__tests__/decode-image-bytes.test.ts +90 -0
  217. package/src/__tests__/decode-image-mime-owner.test.ts +2 -5
  218. package/src/__tests__/dynamic-texture-store.unit.test.ts +115 -0
  219. package/src/__tests__/errors-asset.unit.test.ts +92 -0
  220. package/src/__tests__/inline-pack-loaders.unit.test.ts +548 -0
  221. package/src/__tests__/instantiate-context.unit.test.ts +65 -0
  222. package/src/__tests__/instantiate-publication-fence.unit.test.ts +158 -0
  223. package/src/__tests__/instantiate.unit.test.ts +684 -0
  224. package/src/__tests__/loader-input-contract.unit.test.ts +89 -0
  225. package/src/__tests__/loader-registry.unit.test.ts +101 -0
  226. package/src/__tests__/material-dependency-invalidation.unit.test.ts +14 -0
  227. package/src/__tests__/material-generation-cache.unit.test.ts +46 -0
  228. package/src/__tests__/material-load-consumer.test-d.ts +42 -0
  229. package/src/__tests__/material-ready.integration.test.ts +492 -0
  230. package/src/__tests__/material-stale-generation.unit.test.ts +152 -0
  231. package/src/__tests__/mesh-bin-morph-v3.test.ts +22 -0
  232. package/src/__tests__/mesh-bin.unit.test.ts +152 -0
  233. package/src/__tests__/mipmap-generator.unit.test.ts +202 -0
  234. package/src/__tests__/pack-artifact-transcode-model-owner.test.ts +32 -0
  235. package/src/__tests__/pack-basis-load.integration.test.ts +168 -0
  236. package/src/__tests__/payload-validate.unit.test.ts +285 -0
  237. package/src/__tests__/plugin.test.ts +55 -0
  238. package/src/__tests__/registry-concurrency.bench.test.ts +40 -0
  239. package/src/__tests__/registry-failure-retry.integration.test.ts +69 -0
  240. package/src/__tests__/registry-load-graph.integration.test.ts +246 -0
  241. package/src/__tests__/registry-load-state.unit.test.ts +35 -0
  242. package/src/__tests__/registry-package-cache.unit.test.ts +53 -0
  243. package/src/__tests__/registry-public-ready.unit.test.ts +38 -0
  244. package/src/__tests__/resolve-asset-handle.unit.test.ts +238 -0
  245. package/src/__tests__/scene-handle-fields.unit.test.ts +129 -0
  246. package/src/__tests__/scene-payload.unit.test.ts +151 -0
  247. package/src/__tests__/scene-publication-fence.test.ts +129 -0
  248. package/src/__tests__/scriptable-pack-public-surface.test-d.ts +16 -0
  249. package/src/__tests__/static-rig.integration.test.ts +8 -0
  250. package/src/__tests__/validate-material.unit.test.ts +359 -0
  251. package/src/__tests__/video-pack-loader.integration.test.ts +83 -0
  252. package/src/aabb.ts +107 -0
  253. package/src/asset-registry.ts +2040 -0
  254. package/src/builtin-asset-registry.ts +164 -0
  255. package/src/catalog-source.ts +14 -70
  256. package/src/decode-image-bytes.ts +126 -0
  257. package/src/dynamic-texture-store.ts +234 -0
  258. package/src/errors/asset.ts +347 -0
  259. package/src/handles.ts +180 -0
  260. package/src/image-error.ts +45 -0
  261. package/src/index.ts +177 -19
  262. package/src/internal.ts +2 -0
  263. package/src/loader-registry.ts +157 -0
  264. package/src/loaders/inline-pack.ts +837 -0
  265. package/src/loaders/mesh-bin.ts +304 -0
  266. package/src/loaders/pack-artifact.ts +665 -0
  267. package/src/material/dependency-graph.ts +46 -0
  268. package/src/material/generation-cache.ts +177 -0
  269. package/src/material/inspection.ts +46 -0
  270. package/src/material/loader.ts +326 -0
  271. package/src/material/runtime-shader.ts +133 -0
  272. package/src/mesh-bin.ts +5 -0
  273. package/src/mipmap-generator.ts +450 -0
  274. package/src/payload-validate.ts +573 -0
  275. package/src/plugin.ts +48 -0
  276. package/src/registry/artifact-io.ts +259 -0
  277. package/src/registry/asset-evidence.ts +51 -0
  278. package/src/registry/catalog-state.ts +409 -0
  279. package/src/registry/catalog.ts +369 -0
  280. package/src/registry/instantiate.ts +1056 -0
  281. package/src/registry/load-by-guid.ts +1657 -0
  282. package/src/registry/load-state.ts +115 -0
  283. package/src/registry/runtime-projection.ts +63 -0
  284. package/src/registry/scene-publication-fence.ts +256 -0
  285. package/src/registry/validate-material.ts +386 -0
  286. package/src/resolve-asset-handle.ts +147 -0
  287. package/src/resolve-tileset-runtime.ts +72 -0
  288. package/src/scene-handle-fields.ts +221 -0
  289. package/src/scene-payload.ts +207 -0
  290. package/src/wire-default-loaders.ts +79 -0
  291. package/dist/__tests__/asset-graph-red.integration.test.d.ts +0 -2
  292. package/dist/__tests__/asset-graph-red.integration.test.d.ts.map +0 -1
  293. package/dist/__tests__/asset-kind.test.d.ts +0 -2
  294. package/dist/__tests__/asset-kind.test.d.ts.map +0 -1
  295. package/dist/__tests__/asset-registry-core.integration.test.d.ts +0 -2
  296. package/dist/__tests__/asset-registry-core.integration.test.d.ts.map +0 -1
  297. package/dist/__tests__/asset-registry-public-api.test-d.d.ts +0 -2
  298. package/dist/__tests__/asset-registry-public-api.test-d.d.ts.map +0 -1
  299. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts +0 -2
  300. package/dist/__tests__/asset-runtime-core-lifecycle.integration.test.d.ts.map +0 -1
  301. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts +0 -2
  302. package/dist/__tests__/asset-runtime-snapshot.unit.test.d.ts.map +0 -1
  303. package/dist/__tests__/catalog-session-red.unit.test.d.ts +0 -2
  304. package/dist/__tests__/catalog-session-red.unit.test.d.ts.map +0 -1
  305. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts +0 -2
  306. package/dist/__tests__/registry-lifecycle-red.integration.test.d.ts.map +0 -1
  307. package/src/__tests__/asset-graph-red.integration.test.ts +0 -113
  308. package/src/__tests__/asset-registry-core.integration.test.ts +0 -161
  309. package/src/__tests__/asset-registry-public-api.test-d.ts +0 -31
  310. package/src/__tests__/asset-runtime-core-lifecycle.integration.test.ts +0 -79
  311. package/src/__tests__/asset-runtime-snapshot.unit.test.ts +0 -23
  312. package/src/__tests__/catalog-session-red.unit.test.ts +0 -276
  313. package/src/__tests__/registry-lifecycle-red.integration.test.ts +0 -80
@@ -0,0 +1,573 @@
1
+ // @forgeax/engine-assets-runtime -- register-time payload validation
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 type {
7
+ Asset,
8
+ AssetErrorDetail,
9
+ TilesetAsset,
10
+ MeshAsset as TypesMeshAsset,
11
+ } from '@forgeax/engine-types';
12
+ import { ASSET_ERROR_HINTS, AssetError, countExtraUvSets } from '@forgeax/engine-types';
13
+
14
+ /**
15
+ * Register-stage fail-fast for `kind: 'mesh'` payloads whose vertices buffer
16
+ * is not the canonical 12-floats-per-vertex layout.
17
+ *
18
+ * Validation spec (plan-strategy D-3):
19
+ * (a) asset.kind !== 'mesh' -> return null immediately
20
+ * (b) vertices.length === 0 && indices.length === 0 -> return null (empty mesh legal)
21
+ * (c) vertices.length % PROCEDURAL_FLOATS_PER_VERTEX !== 0 -> `AssetError` with
22
+ * code='mesh-vertex-stride-mismatch', detail = { vertexCount: 0,
23
+ * floatsPerVertex: vertices.length / PROCEDURAL_FLOATS_PER_VERTEX } (non-integer)
24
+ * (d) otherwise compute vertexCount = vertices.length / PROCEDURAL_FLOATS_PER_VERTEX;
25
+ * scan indices for maxIndex;
26
+ * if maxIndex + 1 !== vertexCount -> same AssetError shape with
27
+ * detail = { vertexCount: maxIndex + 1, floatsPerVertex: vertices.length / (maxIndex + 1) }
28
+ *
29
+ * Isomorphic with `validateMaterialPayload` — both are private module-level helpers,
30
+ * both return `AssetError | null`, and both are called from `register()` at entry.
31
+ *
32
+ * Anchors: charter P3 (structured failure); plan-strategy D-2 (gate at register stage);
33
+ * plan-strategy D-3 (three-branch validation: empty / non-divisible-12 / maxIndex mismatch);
34
+ * plan-strategy D-5 (physical location co-located with validateMaterialPayload).
35
+ */
36
+ export function validateMeshPayload(asset: Asset): AssetError | null {
37
+ if (asset.kind !== 'mesh') return null;
38
+
39
+ // feat-20260604-mesh-topology-debug-draw M5 / w13: semantic topology gate
40
+ // (plan-strategy D-A2).
41
+ //
42
+ // feat-20260608 M2 / w9: topology is now per-submesh (MeshAsset.submeshes[]).
43
+ // All topology + submesh-empty + index-OOB validation runs here.
44
+ const submeshes = (asset as TypesMeshAsset).submeshes;
45
+ if (submeshes.length === 0) {
46
+ // TypesMeshAsset POD does not carry an inline GUID; the registry assigns
47
+ // one on register, but at validation time we only have the payload. Emit
48
+ // a stable sentinel so the closed AssetErrorDetail union still narrows.
49
+ const guid = '<no-guid>';
50
+ return new AssetError({
51
+ code: 'mesh-asset-submeshes-empty',
52
+ expected: 'submeshes array has at least one Submesh entry',
53
+ hint: ASSET_ERROR_HINTS['mesh-asset-submeshes-empty'],
54
+ detail: { meshAssetGuid: guid },
55
+ });
56
+ }
57
+
58
+ const materialSlots = (asset as TypesMeshAsset).materialSlots;
59
+ if (!Array.isArray(materialSlots) || materialSlots.length === 0) {
60
+ return new AssetError({
61
+ code: 'mesh-asset-material-slot-index-out-of-range',
62
+ expected: 'a non-empty materialSlots table for a mesh with submeshes',
63
+ hint: ASSET_ERROR_HINTS['mesh-asset-material-slot-index-out-of-range'],
64
+ detail: {
65
+ meshAssetGuid: '<no-guid>',
66
+ submeshIndex: 0,
67
+ materialSlot: -1,
68
+ materialSlotCount: 0,
69
+ },
70
+ });
71
+ }
72
+ const slotNames = new Set<string>();
73
+ const sourceKeys = new Set<string>();
74
+ for (let slotIndex = 0; slotIndex < materialSlots.length; slotIndex++) {
75
+ const slot = materialSlots[slotIndex];
76
+ const slotName = slot?.slotName.trim() ?? '';
77
+ const sourceKey = slot?.sourceKey?.trim();
78
+ if (slotName.length === 0 || slotNames.has(slotName)) {
79
+ return new AssetError({
80
+ code: 'asset-invalid-value',
81
+ expected: `materialSlots[${slotIndex}].slotName is non-empty and unique`,
82
+ hint: 'give every Mesh material slot a stable unique slotName',
83
+ detail: {
84
+ field: `materialSlots[${slotIndex}].slotName`,
85
+ value: slotName,
86
+ reason: 'empty-or-duplicate',
87
+ },
88
+ });
89
+ }
90
+ slotNames.add(slotName);
91
+ if (sourceKey !== undefined && sourceKey.length > 0) {
92
+ if (sourceKeys.has(sourceKey)) {
93
+ return new AssetError({
94
+ code: 'asset-invalid-value',
95
+ expected: `materialSlots[${slotIndex}].sourceKey is unique when present`,
96
+ hint: 'derive a stable unique source identity for every imported Mesh material slot',
97
+ detail: {
98
+ field: `materialSlots[${slotIndex}].sourceKey`,
99
+ value: sourceKey,
100
+ reason: 'duplicate',
101
+ },
102
+ });
103
+ }
104
+ sourceKeys.add(sourceKey);
105
+ }
106
+ }
107
+
108
+ const hasIndices = (asset.indices?.length ?? 0) > 0;
109
+ const indexBufferLength = asset.indices?.length ?? 0;
110
+ for (let i = 0; i < submeshes.length; i++) {
111
+ const sm = submeshes[i];
112
+ if (sm === undefined) continue;
113
+ const topology = sm.topology;
114
+ if (
115
+ !Number.isInteger(sm.materialSlot) ||
116
+ sm.materialSlot < 0 ||
117
+ sm.materialSlot >= materialSlots.length
118
+ ) {
119
+ return new AssetError({
120
+ code: 'mesh-asset-material-slot-index-out-of-range',
121
+ expected: `submesh[${i}].materialSlot in [0, ${materialSlots.length})`,
122
+ hint: ASSET_ERROR_HINTS['mesh-asset-material-slot-index-out-of-range'],
123
+ detail: {
124
+ meshAssetGuid: '<no-guid>',
125
+ submeshIndex: i,
126
+ materialSlot: sm.materialSlot,
127
+ materialSlotCount: materialSlots.length,
128
+ },
129
+ });
130
+ }
131
+ if ((topology === 'line-strip' || topology === 'triangle-strip') && !hasIndices) {
132
+ return new AssetError({
133
+ code: 'asset-invalid-value',
134
+ expected: `submesh[${i}] strip topology carries an index buffer`,
135
+ hint: 'line-strip / triangle-strip meshes must provide indices; add MeshAsset.indices or use line-list / triangle-list',
136
+ detail: {
137
+ field: `submeshes[${i}].topology`,
138
+ value: topology,
139
+ reason: 'strip-topology-without-indices',
140
+ },
141
+ });
142
+ }
143
+ if (asset.vertices.length === 0 && topology !== 'triangle-list') {
144
+ return new AssetError({
145
+ code: 'asset-invalid-value',
146
+ expected: `submesh[${i}]: empty geometry uses 'triangle-list'`,
147
+ hint: 'a zero-vertex mesh has nothing to draw; change submesh topology to triangle-list or provide vertices',
148
+ detail: {
149
+ field: `submeshes[${i}].topology`,
150
+ value: topology,
151
+ reason: 'empty-geometry-non-default-topology',
152
+ },
153
+ });
154
+ }
155
+ // feat-20260608 M2 / w9: index-range-out-of-bounds per submesh
156
+ if (sm.indexOffset + sm.indexCount > indexBufferLength) {
157
+ // TypesMeshAsset POD does not carry an inline GUID; the registry assigns
158
+ // one on register, but at validation time we only have the payload. Emit
159
+ // a stable sentinel so the closed AssetErrorDetail union still narrows.
160
+ const guid = '<no-guid>';
161
+ return new AssetError({
162
+ code: 'mesh-submesh-index-range-out-of-bounds',
163
+ expected: `submesh[${i}].indexOffset + indexCount <= index buffer length (${indexBufferLength})`,
164
+ hint: ASSET_ERROR_HINTS['mesh-submesh-index-range-out-of-bounds'],
165
+ detail: {
166
+ submeshIndex: i,
167
+ indexOffset: sm.indexOffset,
168
+ indexCount: sm.indexCount,
169
+ indexBufferLength,
170
+ meshAssetGuid: guid,
171
+ },
172
+ });
173
+ }
174
+ }
175
+
176
+ // indices is optional (vertex-only meshes omit it); read defensively. The
177
+ // stride invariant below stays null-safe for vertex-only meshes (D-A4).
178
+ if (asset.vertices.length === 0 && (asset.indices?.length ?? 0) === 0) return null;
179
+
180
+ // Skin-aware stride: when MeshAsset.attributes carries skinIndex + skinWeight,
181
+ // the bridge promotes the interleaved buffer to 18 floats/vertex (12 base +
182
+ // 4 uint16x4 packed via aliased Uint16 view at slots 12-13 + 4 float weights
183
+ // at slots 14-17).
184
+ // feat-20260629 multi-uv: extra UV sets (uv1..uv7) add 2 floats each to the
185
+ // interleaved stride, pushed after skin data (canonical order).
186
+ const attrs = (asset as TypesMeshAsset).attributes;
187
+ const isSkinned =
188
+ attrs !== undefined && attrs.skinIndex !== undefined && attrs.skinWeight !== undefined;
189
+ const extraUvSets = countExtraUvSets(attrs);
190
+ const baseFloatsPerVertex = isSkinned ? 18 : PROCEDURAL_FLOATS_PER_VERTEX;
191
+ const floatsPerVertex = baseFloatsPerVertex + extraUvSets * 2;
192
+
193
+ if (asset.vertices.length % floatsPerVertex !== 0) {
194
+ return new AssetError({
195
+ code: 'mesh-vertex-stride-mismatch',
196
+ expected: isSkinned
197
+ ? '18 floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4 + skinIndex u16x4 + skinWeight vec4)'
198
+ : `${PROCEDURAL_FLOATS_PER_VERTEX} floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4)`,
199
+ hint: ASSET_ERROR_HINTS['mesh-vertex-stride-mismatch'],
200
+ detail: {
201
+ vertexCount: 0,
202
+ floatsPerVertex: asset.vertices.length / floatsPerVertex,
203
+ },
204
+ });
205
+ }
206
+
207
+ const vertexCount = asset.vertices.length / floatsPerVertex;
208
+ // Vertex-only meshes (no indices) skip the maxIndex-vs-vertexCount invariant:
209
+ // there is no index buffer to bound-check against the vertex array (D-A4).
210
+ const indices = asset.indices;
211
+ if (indices === undefined || indices.length === 0) return null;
212
+ let maxIndex = 0;
213
+ for (let i = 0; i < indices.length; i++) {
214
+ const idx = indices[i];
215
+ if (idx !== undefined && idx > maxIndex) maxIndex = idx;
216
+ }
217
+
218
+ if (maxIndex + 1 !== vertexCount) {
219
+ return new AssetError({
220
+ code: 'mesh-vertex-stride-mismatch',
221
+ expected: isSkinned
222
+ ? '18 floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4 + skinIndex u16x4 + skinWeight vec4)'
223
+ : `${PROCEDURAL_FLOATS_PER_VERTEX} floats per vertex (= position vec3 + normal vec3 + uv vec2 + tangent vec4)`,
224
+ hint: ASSET_ERROR_HINTS['mesh-vertex-stride-mismatch'],
225
+ detail: {
226
+ vertexCount: maxIndex + 1,
227
+ floatsPerVertex: vertexCount > 0 ? asset.vertices.length / (maxIndex + 1) : 0,
228
+ },
229
+ });
230
+ }
231
+
232
+ return null;
233
+ }
234
+
235
+ // === Tileset / Tilemap / TileLayer validators (feat-20260608 M0 baseline rebuild) ===
236
+ //
237
+ // R-6 first-error path (plan-strategy §R-6 ordering, M1 extended):
238
+ // (1) `atlases.length >= 1` -> 'tileset-tile-entry-malformed'
239
+ // .field='atlases' .scope='tileset-asset' (M1)
240
+ // (2) region rectangle stays in atlas -> 'tileset-region-index-out-of-range' (M0)
241
+ // (3) `region.atlasIndex` in atlases range -> 'tileset-tile-entry-malformed'
242
+ // .field='atlasIndex' .scope='tileset-asset' (M1)
243
+ // (4) `tiles[i].regionIndex` in regions -> 'tileset-region-index-out-of-range' (M0)
244
+ // (5) tile entry widthCells / heightCells -> 'tileset-tile-entry-malformed'
245
+ // / pivotX / pivotY / collider .field=<field> .scope='tile-entry'
246
+ // .tileEntryIndex=i (M1)
247
+ //
248
+ // Tilemap / TileLayer register-time invariants use `AssetError
249
+ // 'asset-invalid-value'` with `.detail = { field, value, reason }` so the
250
+ // closed AssetErrorDetail discriminated union narrows uniformly (charter
251
+ // P4 consistent abstraction with `validateMeshPayload`).
252
+
253
+ /**
254
+ * Optional atlas extent for `validateTilesetPayload`. When omitted, the
255
+ * region-rectangle bounds-check is skipped — only the
256
+ * `tiles[].regionIndex` in `[0, regions.length)` invariant runs.
257
+ */
258
+ export interface TilesetValidateOptions {
259
+ readonly atlasWidth?: number;
260
+ readonly atlasHeight?: number;
261
+ }
262
+
263
+ /**
264
+ * Construct an `AssetError` for the M1 `tileset-tile-entry-malformed`
265
+ * code with structured `.detail` (closed 7-variant `.field` enum +
266
+ * 2-variant `.scope` + optional `.tileEntryIndex`). The helper centralises
267
+ * the boilerplate so each call site stays a one-liner (charter F1 single
268
+ * affordance) and the `.detail` shape stays SSOT-aligned with
269
+ * `AssetTilesetTileEntryMalformedDetail`.
270
+ */
271
+ function tileEntryMalformed(args: {
272
+ tilesetGuid: string;
273
+ field: 'widthCells' | 'heightCells' | 'pivotX' | 'pivotY' | 'collider' | 'atlases' | 'atlasIndex';
274
+ scope: 'tileset-asset' | 'tile-entry';
275
+ tileEntryIndex?: number;
276
+ expected: string;
277
+ }): AssetError {
278
+ const detail: AssetErrorDetail = {
279
+ code: 'tileset-tile-entry-malformed',
280
+ field: args.field,
281
+ scope: args.scope,
282
+ tilesetGuid: args.tilesetGuid,
283
+ ...(args.tileEntryIndex !== undefined ? { tileEntryIndex: args.tileEntryIndex } : {}),
284
+ expected: args.expected,
285
+ hint: ASSET_ERROR_HINTS['tileset-tile-entry-malformed'],
286
+ };
287
+ return new AssetError({
288
+ code: 'tileset-tile-entry-malformed',
289
+ expected: args.expected,
290
+ hint: ASSET_ERROR_HINTS['tileset-tile-entry-malformed'],
291
+ detail,
292
+ });
293
+ }
294
+
295
+ /**
296
+ * Validate the shape of a `TilesetTileCollider` value (R-6 step 5d).
297
+ * Returns `null` when the collider is well-formed, otherwise an
298
+ * `AssetError` with code `'tileset-tile-entry-malformed'` and
299
+ * `.detail.field = 'collider'` (charter P3 closed schema).
300
+ *
301
+ * Rules per variant:
302
+ * - `'none'` -- always accepted.
303
+ * - `'rect'` -- `rect.length === 4`; each component in `[0, 1]`;
304
+ * `w > 0`, `h > 0`; `x + w <= 1`, `y + h <= 1`.
305
+ * - `'polygon'` -- `points.length >= 3`; each point's `x` and `y` in
306
+ * `[0, 1]`.
307
+ * - any other `type` discriminator surfaces the same `.field='collider'`
308
+ * fail-fast (unreachable through the typed surface but defends
309
+ * against unchecked JSON loaders, charter P4 fail-fast in depth).
310
+ */
311
+ function validateColliderShape(
312
+ collider: NonNullable<TilesetTileEntryColliderField>,
313
+ tilesetGuid: string,
314
+ tileEntryIndex: number,
315
+ ): AssetError | null {
316
+ if (collider.type === 'none') return null;
317
+ if (collider.type === 'rect') {
318
+ const rect = collider.rect;
319
+ if (!Array.isArray(rect) || rect.length !== 4) {
320
+ return tileEntryMalformed({
321
+ tilesetGuid,
322
+ field: 'collider',
323
+ scope: 'tile-entry',
324
+ tileEntryIndex,
325
+ expected: `tiles[${tileEntryIndex}].collider.rect length === 4`,
326
+ });
327
+ }
328
+ const [rx, ry, rw, rh] = rect;
329
+ const valid =
330
+ typeof rx === 'number' &&
331
+ typeof ry === 'number' &&
332
+ typeof rw === 'number' &&
333
+ typeof rh === 'number' &&
334
+ rx >= 0 &&
335
+ ry >= 0 &&
336
+ rw > 0 &&
337
+ rh > 0 &&
338
+ rx + rw <= 1 &&
339
+ ry + rh <= 1;
340
+ if (!valid) {
341
+ return tileEntryMalformed({
342
+ tilesetGuid,
343
+ field: 'collider',
344
+ scope: 'tile-entry',
345
+ tileEntryIndex,
346
+ expected: `tiles[${tileEntryIndex}].collider.rect in [0, 1]^2 with w > 0, h > 0, x + w <= 1, y + h <= 1`,
347
+ });
348
+ }
349
+ return null;
350
+ }
351
+ if (collider.type === 'polygon') {
352
+ const points = collider.points;
353
+ if (!Array.isArray(points) || points.length < 3) {
354
+ return tileEntryMalformed({
355
+ tilesetGuid,
356
+ field: 'collider',
357
+ scope: 'tile-entry',
358
+ tileEntryIndex,
359
+ expected: `tiles[${tileEntryIndex}].collider.points length >= 3`,
360
+ });
361
+ }
362
+ for (let j = 0; j < points.length; j++) {
363
+ const p = points[j];
364
+ if (
365
+ !Array.isArray(p) ||
366
+ p.length !== 2 ||
367
+ typeof p[0] !== 'number' ||
368
+ typeof p[1] !== 'number' ||
369
+ p[0] < 0 ||
370
+ p[0] > 1 ||
371
+ p[1] < 0 ||
372
+ p[1] > 1
373
+ ) {
374
+ return tileEntryMalformed({
375
+ tilesetGuid,
376
+ field: 'collider',
377
+ scope: 'tile-entry',
378
+ tileEntryIndex,
379
+ expected: `tiles[${tileEntryIndex}].collider.points[${j}] in [0, 1]^2`,
380
+ });
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+ // Unknown discriminator (only reachable via unchecked JSON loaders bypassing
386
+ // the typed surface). Closed enum -> fail-fast (charter P3).
387
+ return tileEntryMalformed({
388
+ tilesetGuid,
389
+ field: 'collider',
390
+ scope: 'tile-entry',
391
+ tileEntryIndex,
392
+ expected: `tiles[${tileEntryIndex}].collider.type in {'none', 'rect', 'polygon'}`,
393
+ });
394
+ }
395
+
396
+ /**
397
+ * Internal alias for the optional `collider` field on `TilesetTileEntry`.
398
+ * Localised to keep the validator helper signature math-free.
399
+ */
400
+ type TilesetTileEntryColliderField = TilesetAsset['tiles'][number]['collider'];
401
+
402
+ /**
403
+ * Validate a `TilesetAsset` payload at register time (M0 baseline rebuild
404
+ * + M1 schema extension). Returns `null` on success or an `AssetError`
405
+ * carrying the first-error details (charter P3 fail-fast). R-6 ordering:
406
+ *
407
+ * 1. `atlases.length >= 1` -- 'tileset-tile-entry-malformed' .field='atlases'.
408
+ * 2. Region rectangle escapes atlas extent (when extent is supplied) --
409
+ * M0 code 'tileset-region-index-out-of-range'.
410
+ * 3. `regions[i].atlasIndex` in `[0, atlases.length)` -- 'tileset-tile-entry-malformed'
411
+ * .field='atlasIndex'.
412
+ * 4. `tiles[i].regionIndex` in `[0, regions.length)` -- M0 code.
413
+ * 5. Per-tile-entry boundaries -- 'tileset-tile-entry-malformed'
414
+ * .field=widthCells | heightCells | pivotX | pivotY | collider,
415
+ * in that order, .scope='tile-entry' .tileEntryIndex=i.
416
+ */
417
+ export function validateTilesetPayload(
418
+ asset: TilesetAsset,
419
+ opts: TilesetValidateOptions = {},
420
+ ): AssetError | null {
421
+ const assetGuid = '<no-guid>';
422
+ // (1) atlases empty fail-fast (M1 R-6 top-level invariant).
423
+ if (asset.atlases.length < 1) {
424
+ return tileEntryMalformed({
425
+ tilesetGuid: assetGuid,
426
+ field: 'atlases',
427
+ scope: 'tileset-asset',
428
+ expected: 'atlases.length >= 1',
429
+ });
430
+ }
431
+
432
+ const regionCount = asset.regions.length;
433
+ const atlasesLength = asset.atlases.length;
434
+ // (2) region rectangle bounds-check. Negative coords / non-positive size are
435
+ // rejected regardless of whether an atlas extent is supplied; the optional
436
+ // atlasWidth / atlasHeight tightens the upper bound when present.
437
+ const atlasWidth = opts.atlasWidth;
438
+ const atlasHeight = opts.atlasHeight;
439
+ for (let i = 0; i < regionCount; i++) {
440
+ const region = asset.regions[i];
441
+ if (region === undefined) continue;
442
+ const negativeOrZero = region.x < 0 || region.y < 0 || region.width <= 0 || region.height <= 0;
443
+ const exceedsAtlas =
444
+ typeof atlasWidth === 'number' &&
445
+ typeof atlasHeight === 'number' &&
446
+ (region.x + region.width > atlasWidth || region.y + region.height > atlasHeight);
447
+ if (negativeOrZero || exceedsAtlas) {
448
+ return new AssetError({
449
+ code: 'tileset-region-index-out-of-range',
450
+ expected: `regions[${i}] rectangle (x/y >= 0, width/height > 0${
451
+ typeof atlasWidth === 'number' && typeof atlasHeight === 'number'
452
+ ? `, x + width <= ${atlasWidth}, y + height <= ${atlasHeight}`
453
+ : ''
454
+ })`,
455
+ hint: ASSET_ERROR_HINTS['tileset-region-index-out-of-range'],
456
+ detail: {
457
+ code: 'tileset-region-index-out-of-range',
458
+ tilesetGuid: assetGuid,
459
+ tileId: 0,
460
+ regionIndex: i,
461
+ regionCount,
462
+ },
463
+ });
464
+ }
465
+ // (3) per-region atlasIndex bounds-check (M1; optional field defaults 0).
466
+ if (region.atlasIndex !== undefined) {
467
+ const ai = region.atlasIndex;
468
+ if (!Number.isInteger(ai) || ai < 0 || ai >= atlasesLength) {
469
+ return tileEntryMalformed({
470
+ tilesetGuid: assetGuid,
471
+ field: 'atlasIndex',
472
+ scope: 'tileset-asset',
473
+ expected: `regions[${i}].atlasIndex in [0, ${atlasesLength})`,
474
+ });
475
+ }
476
+ }
477
+ }
478
+ // (4) tile entry regionIndex in [0, regions.length) (M0 code).
479
+ for (let i = 0; i < asset.tiles.length; i++) {
480
+ const entry = asset.tiles[i];
481
+ if (entry === undefined) continue;
482
+ const ri = entry.regionIndex;
483
+ if (!Number.isInteger(ri) || ri < 0 || ri >= regionCount) {
484
+ return new AssetError({
485
+ code: 'tileset-region-index-out-of-range',
486
+ expected: `tiles[${i}].regionIndex in [0, ${regionCount})`,
487
+ hint: ASSET_ERROR_HINTS['tileset-region-index-out-of-range'],
488
+ detail: {
489
+ code: 'tileset-region-index-out-of-range',
490
+ tilesetGuid: assetGuid,
491
+ tileId: i + 1,
492
+ regionIndex: ri,
493
+ regionCount,
494
+ },
495
+ });
496
+ }
497
+ }
498
+ // (5) per-tile-entry M1 boundaries -- widthCells > heightCells > pivotX >
499
+ // pivotY > collider, per plan-strategy §R-6 first-error order. Each loop
500
+ // iteration runs the full sub-order on entry i before advancing to i+1 so
501
+ // the deterministic global order is (i=0 widthCells, i=0 heightCells, ...,
502
+ // i=0 collider, i=1 widthCells, ...). Tests in tileset-asset-validate.test.ts
503
+ // R-6 block exercise the sub-order on a single entry.
504
+ for (let i = 0; i < asset.tiles.length; i++) {
505
+ const entry = asset.tiles[i];
506
+ if (entry === undefined) continue;
507
+ if (entry.widthCells !== undefined) {
508
+ const w = entry.widthCells;
509
+ if (!Number.isFinite(w) || w <= 0 || w > 64) {
510
+ return tileEntryMalformed({
511
+ tilesetGuid: assetGuid,
512
+ field: 'widthCells',
513
+ scope: 'tile-entry',
514
+ tileEntryIndex: i,
515
+ expected: `tiles[${i}].widthCells in (0, 64]`,
516
+ });
517
+ }
518
+ }
519
+ if (entry.heightCells !== undefined) {
520
+ const h = entry.heightCells;
521
+ if (!Number.isFinite(h) || h <= 0 || h > 64) {
522
+ return tileEntryMalformed({
523
+ tilesetGuid: assetGuid,
524
+ field: 'heightCells',
525
+ scope: 'tile-entry',
526
+ tileEntryIndex: i,
527
+ expected: `tiles[${i}].heightCells in (0, 64]`,
528
+ });
529
+ }
530
+ }
531
+ if (entry.pivotX !== undefined) {
532
+ const px = entry.pivotX;
533
+ if (!Number.isFinite(px) || px < 0 || px > 1) {
534
+ return tileEntryMalformed({
535
+ tilesetGuid: assetGuid,
536
+ field: 'pivotX',
537
+ scope: 'tile-entry',
538
+ tileEntryIndex: i,
539
+ expected: `tiles[${i}].pivotX in [0, 1]`,
540
+ });
541
+ }
542
+ }
543
+ if (entry.pivotY !== undefined) {
544
+ const py = entry.pivotY;
545
+ if (!Number.isFinite(py) || py < 0 || py > 1) {
546
+ return tileEntryMalformed({
547
+ tilesetGuid: assetGuid,
548
+ field: 'pivotY',
549
+ scope: 'tile-entry',
550
+ tileEntryIndex: i,
551
+ expected: `tiles[${i}].pivotY in [0, 1]`,
552
+ });
553
+ }
554
+ }
555
+ if (entry.collider !== undefined) {
556
+ const colliderErr = validateColliderShape(entry.collider, assetGuid, i);
557
+ if (colliderErr !== null) return colliderErr;
558
+ }
559
+ }
560
+ return null;
561
+ }
562
+
563
+ /**
564
+ * Compute the implicit atlas extent of a `TilesetAsset` from its grid metadata
565
+ * (`columns * tileWidth` x `rows * tileHeight`). Used by the register-time
566
+ * region-bounds check when the caller does not supply explicit extents.
567
+ */
568
+ export function inferAtlasExtent(asset: TilesetAsset): { atlasWidth: number; atlasHeight: number } {
569
+ return {
570
+ atlasWidth: asset.columns * asset.tileWidth,
571
+ atlasHeight: asset.rows * asset.tileHeight,
572
+ };
573
+ }
package/src/plugin.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { Plugin } from '@forgeax/engine-plugin';
2
+ import type { Loader } from '@forgeax/engine-types';
3
+
4
+ import type { AssetRegistry } from './asset-registry';
5
+ import type { PackLoader } from './loader-registry';
6
+
7
+ declare module '@forgeax/engine-plugin' {
8
+ interface EngineContextServices {
9
+ assets?: AssetRegistry;
10
+ }
11
+ }
12
+
13
+ /** Provide one renderer-owned asset registry to the current Cordis realm. */
14
+ export function assetsPlugin(assets: AssetRegistry): Plugin {
15
+ return {
16
+ name: 'assets',
17
+ provide: 'assets',
18
+ apply(ctx) {
19
+ ctx.provide('assets', assets);
20
+ },
21
+ };
22
+ }
23
+
24
+ /** Reversibly register one runtime asset loader. */
25
+ export function assetLoaderPlugin(loader: Loader<unknown>): Plugin {
26
+ return {
27
+ name: `asset-loader:${loader.kind}`,
28
+ inject: ['assets'],
29
+ apply(ctx) {
30
+ const assets = ctx.assets;
31
+ if (assets === undefined) throw new Error('asset loader plugin requires the assets service');
32
+ ctx.effect(() => assets.loaders.register(loader), `assets/loader:${loader.kind}`);
33
+ },
34
+ };
35
+ }
36
+
37
+ /** Reversibly register one pack loader. */
38
+ export function packLoaderPlugin(loader: PackLoader): Plugin {
39
+ return {
40
+ name: `pack-loader:${loader.kind}`,
41
+ inject: ['assets'],
42
+ apply(ctx) {
43
+ const assets = ctx.assets;
44
+ if (assets === undefined) throw new Error('pack loader plugin requires the assets service');
45
+ ctx.effect(() => assets.loaders.registerPackLoader(loader), `assets/pack:${loader.kind}`);
46
+ },
47
+ };
48
+ }