modelstage 0.0.0-stage → 0.5.28

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 (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +137 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +63 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +654 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +396 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/shared/type-labels.mjs +31 -0
  126. package/src/web/README.md +193 -0
  127. package/src/web/app/anim-list.js +222 -0
  128. package/src/web/app/background-solids.js +150 -0
  129. package/src/web/app/backgrounds.js +152 -0
  130. package/src/web/app/camera-log.js +28 -0
  131. package/src/web/app/context.js +72 -0
  132. package/src/web/app/dir-picker.js +242 -0
  133. package/src/web/app/entry-report.js +71 -0
  134. package/src/web/app/memory-bars.js +91 -0
  135. package/src/web/app/memory-budget.js +228 -0
  136. package/src/web/app/memory-guard.js +204 -0
  137. package/src/web/app/model-actions.js +682 -0
  138. package/src/web/app/renderer-setting.js +70 -0
  139. package/src/web/app/root-manager.js +254 -0
  140. package/src/web/app/status.js +138 -0
  141. package/src/web/app/topbar-actions.js +357 -0
  142. package/src/web/app/transport-actions.js +297 -0
  143. package/src/web/app/ui-state.js +559 -0
  144. package/src/web/app/url-params.js +171 -0
  145. package/src/web/app/view-settings.js +186 -0
  146. package/src/web/components.css +524 -0
  147. package/src/web/debug/diag-hud.js +95 -0
  148. package/src/web/debug/drag.js +369 -0
  149. package/src/web/debug/log.js +320 -0
  150. package/src/web/debug/model-cmp.js +292 -0
  151. package/src/web/debug/panel.css +474 -0
  152. package/src/web/debug/panel.js +712 -0
  153. package/src/web/debug/race-probe.js +82 -0
  154. package/src/web/debug/upload.js +130 -0
  155. package/src/web/engine/adapters/atlas.js +47 -0
  156. package/src/web/engine/adapters/index.js +22 -0
  157. package/src/web/engine/adapters/renderer.js +223 -0
  158. package/src/web/engine/adapters/skeleton.js +105 -0
  159. package/src/web/engine/adapters/texture.js +64 -0
  160. package/src/web/engine/animation.js +181 -0
  161. package/src/web/engine/appearance.js +136 -0
  162. package/src/web/engine/camera-adapter.js +212 -0
  163. package/src/web/engine/effect-chain.js +138 -0
  164. package/src/web/engine/engine-state.js +120 -0
  165. package/src/web/engine/filters.js +224 -0
  166. package/src/web/engine/fit.js +197 -0
  167. package/src/web/engine/frame-gate.js +33 -0
  168. package/src/web/engine/gl-driver.js +56 -0
  169. package/src/web/engine/gl.js +77 -0
  170. package/src/web/engine/index.js +20 -0
  171. package/src/web/engine/loader.js +55 -0
  172. package/src/web/engine/model-info.js +26 -0
  173. package/src/web/engine/pipeline.js +200 -0
  174. package/src/web/engine/player.js +427 -0
  175. package/src/web/engine/pma/blend.js +76 -0
  176. package/src/web/engine/pma/detect.js +118 -0
  177. package/src/web/engine/pma/index.js +8 -0
  178. package/src/web/engine/pma/resolve.js +112 -0
  179. package/src/web/engine/pma/upload.js +127 -0
  180. package/src/web/engine/pma.js +5 -0
  181. package/src/web/engine/render-frame.js +93 -0
  182. package/src/web/engine/runtime-capabilities.js +209 -0
  183. package/src/web/engine/runtime.js +37 -0
  184. package/src/web/engine/skins.js +165 -0
  185. package/src/web/engine/textures.js +174 -0
  186. package/src/web/index.html +117 -0
  187. package/src/web/infra/api.js +531 -0
  188. package/src/web/infra/diag.js +509 -0
  189. package/src/web/infra/document.js +269 -0
  190. package/src/web/infra/dom.js +31 -0
  191. package/src/web/infra/events.mjs +27 -0
  192. package/src/web/infra/log.js +153 -0
  193. package/src/web/infra/mem-stats.js +112 -0
  194. package/src/web/infra/plugin-host-registration.mjs +285 -0
  195. package/src/web/infra/plugin-host-report.mjs +32 -0
  196. package/src/web/infra/plugin-host-unload.mjs +79 -0
  197. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  198. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  199. package/src/web/infra/plugin-host.js +628 -0
  200. package/src/web/infra/ready.js +461 -0
  201. package/src/web/infra/schedule.js +51 -0
  202. package/src/web/infra/shortcuts.js +129 -0
  203. package/src/web/infra/slot-registry.js +402 -0
  204. package/src/web/infra/vfs.js +247 -0
  205. package/src/web/live2d/bridge.js +400 -0
  206. package/src/web/live2d/model.js +321 -0
  207. package/src/web/live2d/overlay.js +142 -0
  208. package/src/web/live2d/pixi-app.js +111 -0
  209. package/src/web/live2d/playback-probe.js +92 -0
  210. package/src/web/live2d/probe-geometry.js +329 -0
  211. package/src/web/live2d/probe.js +119 -0
  212. package/src/web/live2d/release.js +66 -0
  213. package/src/web/live2d/runtime-adapter.js +289 -0
  214. package/src/web/live2d/stage-ctx.js +152 -0
  215. package/src/web/live2d/stage.js +204 -0
  216. package/src/web/live2d/vfs-settings.js +174 -0
  217. package/src/web/live2d/view.js +89 -0
  218. package/src/web/main.js +629 -0
  219. package/src/web/panel/background-list.js +373 -0
  220. package/src/web/panel/bg-source.js +291 -0
  221. package/src/web/panel/color-picker.css +199 -0
  222. package/src/web/panel/color-picker.js +278 -0
  223. package/src/web/panel/dialog.css +116 -0
  224. package/src/web/panel/dialog.js +173 -0
  225. package/src/web/panel/dom.js +35 -0
  226. package/src/web/panel/icons.js +58 -0
  227. package/src/web/panel/memory-bar.js +195 -0
  228. package/src/web/panel/panel-settings.js +1263 -0
  229. package/src/web/panel/panel.css +1304 -0
  230. package/src/web/panel/render-choice.js +104 -0
  231. package/src/web/panel/schema.js +784 -0
  232. package/src/web/panel/settings-io.js +291 -0
  233. package/src/web/panel/shell-controls.js +274 -0
  234. package/src/web/panel/slider.js +210 -0
  235. package/src/web/panel/toolbar-schema.js +111 -0
  236. package/src/web/panel/toolbar.js +195 -0
  237. package/src/web/panel/transport-picker.js +375 -0
  238. package/src/web/panel/transport.js +321 -0
  239. package/src/web/panel/view.js +553 -0
  240. package/src/web/plugin/unpack-worker.js +185 -0
  241. package/src/web/render/camera/constants.js +60 -0
  242. package/src/web/render/camera/index.js +123 -0
  243. package/src/web/render/camera/spine.js +124 -0
  244. package/src/web/render/capture.js +30 -0
  245. package/src/web/render/interaction.js +527 -0
  246. package/src/web/render/live2d.js +627 -0
  247. package/src/web/render/playback.js +47 -0
  248. package/src/web/render/reset-view.js +29 -0
  249. package/src/web/render/spine.js +386 -0
  250. package/src/web/render/surface-switch.js +176 -0
  251. package/src/web/render/surface.js +571 -0
  252. package/src/web/render/surfaces.js +70 -0
  253. package/src/web/roster/filter.js +31 -0
  254. package/src/web/roster/list.js +276 -0
  255. package/src/web/roster/persist.js +92 -0
  256. package/src/web/roster/reveal.js +77 -0
  257. package/src/web/roster/rows.js +248 -0
  258. package/src/web/roster/snapshot.js +39 -0
  259. package/src/web/roster/type-filter.js +97 -0
  260. package/src/web/roster/view.js +761 -0
  261. package/src/web/spine/canvas-api-groups.js +267 -0
  262. package/src/web/spine/canvas-api.js +97 -0
  263. package/src/web/spine/canvas-document.js +80 -0
  264. package/src/web/spine/canvas-engine.js +193 -0
  265. package/src/web/spine/canvas-entry.js +28 -0
  266. package/src/web/spine/canvas-pixels.js +36 -0
  267. package/src/web/spine/canvas-runtime.js +127 -0
  268. package/src/web/spine/canvas-view.js +108 -0
  269. package/src/web/spine/canvas.js +7 -0
  270. package/src/web/spine/runtime-hint.js +73 -0
  271. package/src/web/spine/runtimes.js +212 -0
  272. package/src/web/style.css +493 -0
  273. package/tool/lib/bundle-probe-node.mjs +121 -0
  274. package/tool/lib/paths.mjs +20 -0
  275. package/tool/lib/runtime-guides.mjs +65 -0
  276. package/tool/lib/runtime-package.mjs +403 -0
  277. package/tool/lib/spine-meta.mjs +227 -0
  278. package/tool/lib/spine-motions.mjs +104 -0
  279. package/tool/parse-jsonc.mjs +86 -0
  280. package/tool/resolve-live2d-runtimes.mjs +276 -0
  281. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,112 @@
1
+ // engine/pma/resolve.js —— 决策:**唯一**算"怎么画"的地方(档位 + 探测结论 + 运行时能力 → 一份 decision)
2
+ //
3
+ // 上传(upload.js)与混合(blend.js / render-frame)都必须从这里派生,不允许各自再判断一次。
4
+ // 本文件不读 DOM、不碰 GL —— 纯函数,便于单测钉住每一档。
5
+
6
+ /**
7
+ * 档位 → decision。字段含义:
8
+ * effectiveMode 最终生效的路径:premultiplied(源图已预乘)/ upload(上传时乘)/ straight(直通混合)
9
+ * needPremultiply 是否**需要在上传时乘一次**(只有 upload 路径为 true)
10
+ * blendMode 混合口径:premultiplied(src 因子用 ONE)/ straight(用 SRC_ALPHA)
11
+ * degraded 是否因运行时能力不足而降级(显式 straight 落到 upload 时才是 true)
12
+ * @param {string} mode auto | premultiplied | straight | upload
13
+ * @param {{finalIsPremultiplied: boolean}} detection
14
+ * @param {boolean} supportsStraight 运行时是否支持非预乘直通混合
15
+ */
16
+ export function resolvePmaInfo(mode, detection, supportsStraight) {
17
+ const auto = detection && detection.finalIsPremultiplied;
18
+ switch (mode) {
19
+ case 'premultiplied':
20
+ return { effectiveMode: 'premultiplied', needPremultiply: false, blendMode: 'premultiplied', runtimeSupportsStraight: supportsStraight, degraded: false };
21
+ case 'straight':
22
+ return supportsStraight
23
+ ? { effectiveMode: 'straight', needPremultiply: false, blendMode: 'straight', runtimeSupportsStraight: supportsStraight, degraded: false }
24
+ : { effectiveMode: 'upload', needPremultiply: true, blendMode: 'premultiplied', runtimeSupportsStraight: supportsStraight, degraded: true };
25
+ case 'upload':
26
+ return { effectiveMode: 'upload', needPremultiply: true, blendMode: 'premultiplied', runtimeSupportsStraight: supportsStraight, degraded: false };
27
+ case 'auto':
28
+ default: {
29
+ // 三态处理:明确预乘 / 明确非预乘 / **检测不出**
30
+ // 检测不出(auto 为 null/undefined)时**按已预乘**,对齐既有口径三条兜底(init-default / safe-fallback /
31
+ // atlas 未声明)都默认 true 的做法。
32
+ // 理由与「沿袭既有」无关,而是**失败代价不对称**:
33
+ // · 兜底成"非预乘" ⇒ needPremultiply=true ⇒ 若图其实是预乘的 ⇒ **双重预乘 ⇒ 明显黑圈**
34
+ // · 兜底成"已预乘" ⇒ 不乘 ⇒ 若图其实是直通 ⇒ 只是混合略暗
35
+ // 少乘的可接受程度远高于多乘,所以不确定时选"不乘"。
36
+ if (auto === false) {
37
+ // 检测为"非预乘"。**运行时支持直通时,这一档必须与显式 straight 完全等价**:
38
+ // 都走 straight 混合、都不预乘。不看能力就无条件 upload(上传时预乘 + premultiplied 混合)
39
+ // 会让"自动"与"用户强制选非预乘"对同一张图给出两种画面 —— 实测就是面部/眼周白斑。
40
+ // 能力不允许时才退回 upload(与显式 straight 的降级方向一致,但 degraded 保持 false:
41
+ // 这不是"降级",而是这台运行时唯一可行的路径,与改动前逐字段相同)。
42
+ return supportsStraight
43
+ ? { effectiveMode: 'straight', needPremultiply: false, blendMode: 'straight', runtimeSupportsStraight: supportsStraight, degraded: false }
44
+ : { effectiveMode: 'upload', needPremultiply: true, blendMode: 'premultiplied', runtimeSupportsStraight: supportsStraight, degraded: false };
45
+ }
46
+ return { effectiveMode: 'premultiplied', needPremultiply: false, blendMode: 'premultiplied', runtimeSupportsStraight: supportsStraight, degraded: false };
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * 上传口径:这一页贴图要不要在 `texImage2D` 那一下让浏览器乘一次 alpha。
53
+ *
54
+ * 三种输入(恰好是 `resolvePmaInfo` 的三个出口),只有第三种必须乘:
55
+ * · 源图已预乘(`effectiveMode:'premultiplied'`,needPremultiply=false)⇒ **不乘**:再乘就是双重预乘(明显黑圈)
56
+ * · 源图 straight 且走 straight 路径(`effectiveMode:'straight'`,needPremultiply=false)⇒ **不乘**:
57
+ * 乘了就和 straight 混合错配(数据是预乘、blend 却按直通)
58
+ * · 源图 straight 且走 upload 路径(`effectiveMode:'upload'`,needPremultiply=true)⇒ **乘**:
59
+ * 运行时不支持直通混合,只能把数据也预乘,让"预乘数据 + 预乘 blend"配套(不乘就是白斑)
60
+ *
61
+ * 为什么单列成纯函数:`needPremultiply` 曾只作诊断、没有任何地方执行它,
62
+ * 于是 upload 路径「要求预乘却没乘」⇒ GPU 上是直通数据、却按预乘混合绘制 ⇒ 半透明边缘发白。
63
+ * 抽出来才能用单测把三种输入钉死,避免再次退化成"硬编码 false"。
64
+ * @param {{needPremultiply?: boolean}} info `resolvePmaInfo` 的结果
65
+ * @returns {boolean} 是否在上传时预乘
66
+ */
67
+ export function shouldPremultiplyOnUpload(info) {
68
+ return !!(info && info.needPremultiply === true);
69
+ }
70
+
71
+ /**
72
+ * 上传口径的**完整派生**:`{ unpack, pmaFlag }`。
73
+ *
74
+ * 两个字段都是"从 decision 读出来"的派生量,不允许调用方再自己判断一次:
75
+ * unpack 上传时要不要让浏览器乘一次 alpha —— 只有 upload 路径要(见 shouldPremultiplyOnUpload)
76
+ * pmaFlag **上传完成后 GPU 上的数据是否预乘**:
77
+ * premultiplied(源图本来就预乘,原样传)⇒ true
78
+ * upload (上传时乘过一次,现在也是预乘)⇒ **true**
79
+ * straight (原样传,仍是直通) ⇒ false
80
+ * 早前这里写成 `effectiveMode === 'premultiplied'`,把 upload 路径也标成 false ——
81
+ * 与"它其实已经预乘"自相矛盾。虽有该字段的消费方目前是保留路径(无运行时带 pma 属性),
82
+ * 但语义必须与数据真实形态一致,否则下一个人会照它做判断。
83
+ * @param {{effectiveMode?: string, needPremultiply?: boolean}} info `resolvePmaInfo` 的结果
84
+ * @returns {{unpack: boolean, pmaFlag: boolean}}
85
+ */
86
+ export function planTextureUpload(info) {
87
+ const mode = (info && info.effectiveMode) || '';
88
+ return {
89
+ unpack: shouldPremultiplyOnUpload(info),
90
+ pmaFlag: mode === 'premultiplied' || mode === 'upload',
91
+ };
92
+ }
93
+
94
+ // 混合口径只有两种结果,预建成冻结常量:planBlend 在**每帧**都会被调用,
95
+ // 每次 new 一个对象等于每帧一次 GC 压力。返回共享常量既零分配也不会被误改。
96
+ const BLEND_PREMULTIPLIED = Object.freeze({ premultiplied: true });
97
+ const BLEND_STRAIGHT = Object.freeze({ premultiplied: false });
98
+
99
+ /**
100
+ * 混合口径派生:只回答一件事 —— **绘制时按预乘还是直通混合**。
101
+ *
102
+ * `premultiplied` 直接喂给绘制层(决定 blend 的 src 因子是 `ONE` 还是 `SRC_ALPHA`、
103
+ * 以及顶点色是否乘 alpha)。"要不要改写 blendFunc"这件事**不在这里回答**:
104
+ * blend.js 的 `installBlendHooks` 在每个上下文上装一次钩子,改写与否由它自己在
105
+ * 每次 `blendFunc` 调用时按 `pmaInfo.blendMode` 判断 —— 这里再给一个 needHook 字段
106
+ * 只会多出一份没人消费的平行语义(早前版本就这么干过,已删)。
107
+ * @param {{blendMode?: string}} info `resolvePmaInfo` 的结果
108
+ * @returns {{premultiplied: boolean}} 冻结的共享常量(零分配)
109
+ */
110
+ export function planBlend(info) {
111
+ return (info && info.blendMode === 'premultiplied') ? BLEND_PREMULTIPLIED : BLEND_STRAIGHT;
112
+ }
@@ -0,0 +1,127 @@
1
+ // engine/pma/upload.js —— 上传:按 decision 把贴图传到 GPU(**全项目唯一的纹理上传路径**)
2
+ //
3
+ // 上传口径只从 resolve.js 的 shouldPremultiplyOnUpload 派生,本文件不自己判断"要不要乘"。
4
+ // 关键:预乘要在 texImage2D 那一下由浏览器完成,**不经 2D canvas 中转** ——
5
+ // 2D canvas 的后备存储是预乘的,texImage2D(canvas) 在 UNPACK=false 时会把 alpha 除回去
6
+ // (un-premultiply)⇒ "CPU 先乘一遍"白做,上传到 GPU 仍是直通数据。
7
+ // 所以要么用 Uint8Array 源、要么用原图 + UNPACK 开关,才能真正控制上传字节。
8
+
9
+ import { planTextureUpload } from './resolve.js';
10
+
11
+ /**
12
+ * 按 PMA 口径上传贴图。
13
+ * pma 语义:`pma = true` 表示**这张图(现在)已经是预乘的**。
14
+ * @param {{gl: WebGLRenderingContext, GLTextureCtor: Function|null, entries: Map,
15
+ * effectiveMode: string, needPremultiply: boolean, textureFilter: string,
16
+ * log?: Function, onUpload?: Function}} ctx
17
+ * @returns {Promise<Map<string, object>>} 页名 → 纹理对象
18
+ */
19
+ export async function uploadTextures(ctx) {
20
+ const { gl, GLTextureCtor, entries, effectiveMode, needPremultiply, textureFilter, log = () => {}, onUpload } = ctx;
21
+ // 上传口径整份来自 decision(不在这里二次判断),见 resolve.js 的 planTextureUpload
22
+ const { unpack, pmaFlag } = planTextureUpload({ effectiveMode, needPremultiply });
23
+ const plan = {
24
+ unpack,
25
+ pmaFlag,
26
+ // 诊断标签用"走的哪条路"(upload / straight / premultiplied),不能拿 pmaFlag 代替它:
27
+ // pmaFlag 回答的是"上传后数据是不是预乘"(upload 路径也是 true),两者语义不同。
28
+ mode: effectiveMode,
29
+ textureFilter,
30
+ log,
31
+ onUpload,
32
+ };
33
+ const out = new Map();
34
+ for (const [pageName, entry] of (entries || new Map())) {
35
+ const tex = uploadOnePage(gl, GLTextureCtor, pageName, entry, plan);
36
+ if (tex) out.set(pageName, tex);
37
+ }
38
+ if (out.size === 0) throw new Error('没有成功上传任何贴图到 GPU,无法创建 TextureAtlas');
39
+ return out;
40
+ }
41
+
42
+ /**
43
+ * 上传单页(GLTexture 优先,失败回退手动上传)。
44
+ * @returns {object|null} 纹理对象
45
+ */
46
+ function uploadOnePage(gl, GLTextureCtor, pageName, entry, plan) {
47
+ const source = entry.img || entry.canvas || null; // 原图(或透明占位画布)直传
48
+ if (!source) return null;
49
+ if (GLTextureCtor) {
50
+ const viaCtor = uploadViaGLTexture(gl, GLTextureCtor, pageName, entry, source, plan);
51
+ if (viaCtor) return viaCtor;
52
+ }
53
+ return uploadManually(gl, pageName, entry, source, plan);
54
+ }
55
+
56
+ /** 用运行时的 GLTexture 上传(构造期即完成上传) */
57
+ function uploadViaGLTexture(gl, GLTextureCtor, pageName, entry, source, plan) {
58
+ const prevUnpack = readUnpack(gl);
59
+ try {
60
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, plan.unpack);
61
+ const tex = new GLTextureCtor(gl, source);
62
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, prevUnpack);
63
+ applyTextureFilterLike(gl, tex, plan.textureFilter);
64
+ notify(plan, { page: pageName, mode: plan.mode,
65
+ unpack: plan.unpack, source: entry.img ? 'img' : 'canvas', path: 'GLTexture', texHasPma: 'pma' in tex });
66
+ return tex;
67
+ } catch (e) {
68
+ plan.log('warn', `创建 GLTexture 失败 ${pageName}: ${(e && e.message) || e},回退手动上传`, 'loader');
69
+ try { gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, prevUnpack); } catch { /* 忽略 */ }
70
+ return null;
71
+ }
72
+ }
73
+
74
+ /** 回退:手动 createTexture + texImage2D(同样按 plan.unpack 决定是否让浏览器乘) */
75
+ function uploadManually(gl, pageName, entry, source, plan) {
76
+ const tex = gl.createTexture();
77
+ if (!tex) return null;
78
+ const prevUnpack = readUnpack(gl);
79
+ gl.bindTexture(gl.TEXTURE_2D, tex);
80
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, plan.unpack);
81
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
82
+ gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, prevUnpack);
83
+ const filterMode = plan.textureFilter === 'nearest' ? gl.NEAREST : gl.LINEAR;
84
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode);
85
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode);
86
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
87
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
88
+ notify(plan, { page: pageName, mode: plan.mode,
89
+ unpack: plan.unpack, source: 'manual', path: 'manual', texHasPma: false });
90
+ return {
91
+ _image: source, getImage() { return this._image; },
92
+ setFilters(min, mag) {
93
+ try {
94
+ gl.bindTexture(gl.TEXTURE_2D, tex);
95
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, min);
96
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, mag);
97
+ } catch { /* 忽略 */ }
98
+ },
99
+ setWraps() {}, dispose() { try { gl.deleteTexture(tex); } catch { /* 忽略 */ } }, texture: tex,
100
+ };
101
+ }
102
+
103
+ /** 读当前 UNPACK 状态(拿不到返回 false) */
104
+ function readUnpack(gl) {
105
+ try { return gl.getParameter(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL); } catch { return false; }
106
+ }
107
+
108
+ /** 观测回调(默认打到 debug 日志;不参与逻辑) */
109
+ function notify(plan, detail) {
110
+ try {
111
+ if (typeof plan.onUpload === 'function') plan.onUpload(detail);
112
+ else plan.log('debug', 'pma.upload', detail);
113
+ } catch { /* 观测不能影响上传 */ }
114
+ }
115
+
116
+ /** 过滤参数(与 textures.js 的 applyTextureFilter 等价;此处内联以避免循环依赖) */
117
+ function applyTextureFilterLike(gl, texture, mode) {
118
+ const filterMode = mode === 'nearest' ? gl.NEAREST : gl.LINEAR;
119
+ try {
120
+ if (typeof texture.setFilters === 'function') texture.setFilters(filterMode, filterMode);
121
+ else {
122
+ gl.bindTexture(gl.TEXTURE_2D, texture.texture || texture);
123
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode);
124
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode);
125
+ }
126
+ } catch { /* 过滤设置失败不影响可用性 */ }
127
+ }
@@ -0,0 +1,5 @@
1
+ // engine/pma.js —— 转发层:实现已按职责拆到 `pma/` 目录(detect / resolve / upload / blend)。
2
+ //
3
+ // 保留本文件是为了**对外导入路径不变**:textures.js / appearance.js / pipeline.js
4
+ // 都还写 `./pma.js`(textures.js 那层再转发也保留)。真正的代码在本目录的 index.js 里再导出。
5
+ export * from './pma/index.js';
@@ -0,0 +1,93 @@
1
+ // engine/render-frame.js —— 帧循环与绘制(从 player 抽出,控制单文件规模)
2
+ //
3
+ // 帧调度选择:**渲染器自建 rAF(方案 A)**,与现状一致;
4
+ // StageSurface 只负责尺寸与背景,不接管渲染帧(统一调度属 B6 收尾,需同时改 surface 与 canvas.js)。
5
+
6
+ import { hideFilteredSlots, restoreFilteredSlots } from './skins.js';
7
+ import { createFrameGate } from './frame-gate.js';
8
+ import { planBlend } from './pma.js';
9
+
10
+ /** 绘制一帧 */
11
+ export function renderFrame(engine) {
12
+ // 重建期间不出帧:loadingModel 之前只有"设"没有人"读",这里让它真的生效
13
+ // (重建会把 skeleton/renderer 短暂处于半换血状态,画出来只会闪)
14
+ if (engine.loadingModel) return;
15
+ const { gl, skeleton, renderTarget } = engine;
16
+ if (!gl || !skeleton || !renderTarget || !engine.adapters) return;
17
+ gl.clearColor(0, 0, 0, 0);
18
+ gl.clear(gl.COLOR_BUFFER_BIT);
19
+ // 混合口径由 pma 决策派生出(不是纹理是否预乘):planBlend 返回共享常量,热路径零分配
20
+ const blendPma = planBlend(engine.pmaInfo).premultiplied;
21
+ // 部件过滤要在绘制前摘件、绘制后挂回(finally 保证异常路径也不缺件)
22
+ hideFilteredSlots(engine);
23
+ let ok = false;
24
+ try {
25
+ ok = engine.adapters.renderer.draw(renderTarget, skeleton, blendPma);
26
+ // 调试层叠在主绘制之上(全关时 drawDebug 自身直接返回,不碰 GL)
27
+ engine.adapters.renderer.drawDebug(renderTarget, skeleton, engine.debugDraw, blendPma);
28
+ } finally {
29
+ restoreFilteredSlots(engine);
30
+ }
31
+ if (!ok && !engine.renderWarned) {
32
+ engine.renderWarned = true;
33
+ engine.log('error', '渲染失败:适配器未能绘制(详见能力告警)', 'renderer');
34
+ }
35
+ }
36
+
37
+ /** 首帧:更新一次状态并立即绘制 */
38
+ export function presentFirstFrame(engine) {
39
+ if (!engine.skeleton || !engine.renderer) {
40
+ engine.pendingPresent = false;
41
+ return;
42
+ }
43
+ try {
44
+ if (engine.state && engine.state.apply) {
45
+ engine.state.update(0);
46
+ engine.state.apply(engine.skeleton);
47
+ }
48
+ if (engine.adapters) engine.adapters.skeleton.updateWorldTransform(engine.skeleton);
49
+ renderFrame(engine);
50
+ } catch { /* 首帧失败留给主循环 */ }
51
+ engine.pendingPresent = false;
52
+ }
53
+
54
+ /** 起主循环(幂等:已在跑则只把 isPlaying 置真) */
55
+ export function startLoop(engine) {
56
+ if (engine.animationId !== null) { engine.isPlaying = true; return; }
57
+ engine.isPlaying = true;
58
+ engine.lastTime = performance.now();
59
+ engine.fpsLastTime = engine.lastTime;
60
+ engine.fpsFrames = 0;
61
+ // 帧率门控用累加器(见 frame-gate.js):单门限会把每帧的零头丢掉,
62
+ // 长期平均偏低,且相位抖动时会连放两帧(90Hz 屏上表现为帧率超过目标)。
63
+ engine.frameGate = createFrameGate(() => engine.fpsLimit);
64
+ engine.lastTickAt = engine.lastTime;
65
+ const loop = () => {
66
+ if (!engine.isPlaying) return;
67
+ engine.animationId = requestAnimationFrame(loop);
68
+ const now = performance.now();
69
+ // 先算"距上一 tick 的间隔"再判门控:动画时钟 lastTime 只在实际渲染时推进,语义不变
70
+ const tickDelta = now - engine.lastTickAt;
71
+ engine.lastTickAt = now;
72
+ if (!engine.frameGate.shouldRender(tickDelta)) return;
73
+ let dt = (now - engine.lastTime) / 1000;
74
+ engine.lastTime = now;
75
+ dt = Math.min(dt, 0.1);
76
+ engine.fpsFrames += 1;
77
+ if (now - engine.fpsLastTime >= 500) {
78
+ engine.currentFps = (engine.fpsFrames * 1000) / (now - engine.fpsLastTime);
79
+ engine.fpsFrames = 0;
80
+ engine.fpsLastTime = now;
81
+ }
82
+ engine.update(dt);
83
+ renderFrame(engine);
84
+ };
85
+ loop();
86
+ }
87
+
88
+ /** 停主循环 */
89
+ export function stopLoop(engine) {
90
+ if (engine.animationId !== null) cancelAnimationFrame(engine.animationId);
91
+ engine.animationId = null;
92
+ engine.isPlaying = false;
93
+ }
@@ -0,0 +1,209 @@
1
+ // engine/runtime-capabilities.js —— 运行时能力探测(一次,缓存)
2
+ //
3
+ // 设计原则:
4
+ // · **只探测,不看版本号**:拿到命名空间后逐项问"这个方法/类在不在";
5
+ // · 版本号仅用于日志与 getRendererInfo 展示,不参与任何行为分支;
6
+ // · 探测结果即"能力矩阵":渲染/纹理/图集/骨架四组,业务层只读该矩阵;
7
+ // · 探测失败不静默:写进 `warns`,由调用方统一打到 log(warn)。
8
+ //
9
+ // 为什么必须这样做(实测教训):切片按 4.x 的调用写死了 `texture.bind()`,
10
+ // 而 3.8 的纹理对象没有 bind ⇒ 每帧抛错、画面全空且错误被 catch 进 lastError,极难定位。
11
+
12
+ /** 在命名空间顶层与 `webgl` 子命名空间里找类/方法(3.x 的 WebGL 类只在子命名空间) */
13
+ export function pickMember(ns, name) {
14
+ if (!ns) return null;
15
+ if (ns[name] != null) return ns[name];
16
+ const w = ns.webgl;
17
+ if (w && w[name] != null) return w[name];
18
+ return null;
19
+ }
20
+
21
+ /** 探测某个类的原型上是否有指定方法 */
22
+ function hasProtoMethod(cls, name) {
23
+ try { return !!(cls && cls.prototype && typeof cls.prototype[name] === 'function'); } catch { return false; }
24
+ }
25
+
26
+ /** 构造签名探测:用 `Function.prototype.length` 判断必填参数个数 */
27
+ function arityOf(cls) {
28
+ try { return cls && typeof cls === 'function' ? cls.length : 0; } catch { return 0; }
29
+ }
30
+
31
+ /** 诊断开关:URL 里 `?missing-capability=<key>` 可强制某项探测为 false,用于验证降级路径 */
32
+ function simulatedMissing() {
33
+ try {
34
+ const v = new URLSearchParams(window.location.search).get('missing-capability');
35
+ return v ? new Set(v.split(',').map((s) => s.trim()).filter(Boolean)) : new Set();
36
+ } catch {
37
+ return new Set();
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 一次性取出运行时成员(顺序与原实现逐条一致,**不要重排**)。
43
+ * 为什么集中在一处调用而不是各段自己取:成员读取是"读事实",
44
+ * 集中取能保证四段只做"判断"、不各自重取同一成员 —— 顺序一变,
45
+ * `pickMember` 的调用序列就变了,等价验证会当场失败。
46
+ * @param {object} spine 运行时命名空间
47
+ */
48
+ function collectMembers(spine) {
49
+ return {
50
+ Texture: pickMember(spine, 'Texture'),
51
+ GLTexture: pickMember(spine, 'GLTexture'),
52
+ TextureAtlas: pickMember(spine, 'TextureAtlas'),
53
+ SceneRenderer: pickMember(spine, 'SceneRenderer'),
54
+ SkeletonRenderer: pickMember(spine, 'SkeletonRenderer'),
55
+ OrthoCamera: pickMember(spine, 'OrthoCamera'),
56
+ PolygonBatcher: pickMember(spine, 'PolygonBatcher'),
57
+ SkeletonJson: pickMember(spine, 'SkeletonJson'),
58
+ SkeletonBinary: pickMember(spine, 'SkeletonBinary'),
59
+ AtlasAttachmentLoader: pickMember(spine, 'AtlasAttachmentLoader'),
60
+ Vector2: pickMember(spine, 'Vector2'),
61
+ Skin: pickMember(spine, 'Skin'),
62
+ // 调试绘制渲染器:3.x/4.x 都叫 SkeletonDebugRenderer(个别版本叫 DebugRenderer),没有就只能不画
63
+ SkeletonDebugRenderer: pickMember(spine, 'SkeletonDebugRenderer') || pickMember(spine, 'DebugRenderer'),
64
+ };
65
+ }
66
+
67
+ /** 纹理组:3.x 的 GLTexture 有 bind()/setFilters();4.x 也有,但若换成 Texture 基类则没有 */
68
+ function probeTexture(m, missing, say) {
69
+ const hasBind = say(!missing.has('textureBind') && (hasProtoMethod(m.GLTexture, 'bind') || hasProtoMethod(m.Texture, 'bind')),
70
+ 'texture.bind', '纹理对象没有 bind()(批处理器需要它)');
71
+ const hasSetFilters = hasProtoMethod(m.GLTexture, 'setFilters') || hasProtoMethod(m.Texture, 'setFilters');
72
+ const hasDispose = hasProtoMethod(m.GLTexture, 'dispose') || hasProtoMethod(m.Texture, 'dispose');
73
+ return { hasBind, hasSetFilters, hasDispose, glTextureCtor: m.GLTexture, textureCtor: m.Texture };
74
+ }
75
+
76
+ /** 图集组:4.x 的 TextureAtlas 只吃 (data);3.x 吃 (data, resolver) */
77
+ function probeAtlas(m, missing, say) {
78
+ const arity = arityOf(m.TextureAtlas);
79
+ const acceptsResolver = say(!missing.has('atlasResolver') && arity >= 2,
80
+ 'atlas.resolver', `TextureAtlas 构造只接受 ${arity} 个参数,纹理需要构造后补挂`);
81
+ return { ctor: m.TextureAtlas, acceptsResolver, arity };
82
+ }
83
+
84
+ /** 渲染器组:3.x SceneRenderer(canvas, gl) 自带相机;4.x 可能只有 SkeletonRenderer(gl) */
85
+ function probeRenderer(m, warns) {
86
+ const sceneArity = arityOf(m.SceneRenderer);
87
+ const kind = m.SceneRenderer ? 'scene' : (m.SkeletonRenderer ? 'skeleton' : 'none');
88
+ if (kind === 'none') warns.push('renderer:没有 SceneRenderer / SkeletonRenderer,无法渲染');
89
+ const hasCameraOnScene = !!(m.SceneRenderer && m.SceneRenderer.prototype && 'camera' in m.SceneRenderer.prototype);
90
+ return {
91
+ kind,
92
+ sceneCtor: m.SceneRenderer,
93
+ skeletonCtor: m.SkeletonRenderer,
94
+ sceneArity,
95
+ hasCameraOnScene,
96
+ orthoCtor: m.OrthoCamera,
97
+ orthoArity: arityOf(m.OrthoCamera),
98
+ batcherCtor: m.PolygonBatcher,
99
+ // 调试绘制(bones/meshes/... 八类):没有构造器时该能力整体不可用,由调用方降级
100
+ debugCtor: m.SkeletonDebugRenderer,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * 骨架组:四个构造器 + skeletonOps 的四项方法探测。
106
+ *
107
+ * `pickMember(spine,'Skeleton')` **只取一次**(局部变量 Skeleton)。
108
+ * 早先这里是 5 次(setToSetupPose 那行因短路写了两遍、其余三项各一次),当时刻意不合并,
109
+ * 理由是"合并会改变成员读取次数这个可观测事实",留给后续单独一轮处理 —— 本轮就是那一轮:
110
+ * 已合并,读取次数从 5 次(无 Spine 时因短路为 4 次)降为 1 次。
111
+ * 四项探测结果逐项不变;等价脚本的判据也从"读取序列相同"改成"探测结果相同"
112
+ * (见 tool/.tmp-caps-equiv.mjs 的 diff 注释:读取次数是该优化的目标本身,不能同时当等价标尺)。
113
+ */
114
+ function probeSkeleton(spine, m, warns) {
115
+ const Skeleton = pickMember(spine, 'Skeleton');
116
+ const skeletonOps = {
117
+ setToSetupPose: !!(Skeleton && hasProtoMethod(Skeleton, 'setToSetupPose')),
118
+ updateWorldTransform: hasProtoMethod(Skeleton, 'updateWorldTransform'),
119
+ getBounds: hasProtoMethod(Skeleton, 'getBounds'),
120
+ setSkin: hasProtoMethod(Skeleton, 'setSkin'),
121
+ };
122
+ for (const [k, ok] of Object.entries(skeletonOps)) {
123
+ if (!ok) warns.push(`skeletonOps.${k}:缺失(相关功能会降级)`);
124
+ }
125
+ return {
126
+ skeleton: { jsonCtor: m.SkeletonJson, binaryCtor: m.SkeletonBinary, attachmentLoaderCtor: m.AtlasAttachmentLoader, vector2: m.Vector2 },
127
+ ops: skeletonOps,
128
+ };
129
+ }
130
+
131
+ /**
132
+ * 精简矩阵的构造(给 getRendererInfo 展示用)。由返回对象的 `matrix()` 传 this 进来调用,
133
+ * 抽出成独立函数只是为了让 detectCapabilities 本体保持"只做编排"。
134
+ * @param {object} caps detectCapabilities 的产出(必须有 texture/atlas/renderer/skeleton/skeletonOps)
135
+ */
136
+ function matrixOf(caps) {
137
+ return {
138
+ textureBind: caps.texture.hasBind,
139
+ textureFilters: caps.texture.hasSetFilters,
140
+ atlasResolver: caps.atlas.acceptsResolver,
141
+ rendererKind: caps.renderer.kind,
142
+ sceneHasCamera: caps.renderer.hasCameraOnScene,
143
+ batcher: !!caps.renderer.batcherCtor,
144
+ skeletonJson: !!caps.skeleton.jsonCtor,
145
+ skeletonBinary: !!caps.skeleton.binaryCtor,
146
+ orthoCamera: !!caps.renderer.orthoCtor,
147
+ skeletonDebug: !!caps.renderer.debugCtor,
148
+ ops: { ...caps.skeletonOps },
149
+ };
150
+ }
151
+
152
+ /**
153
+ * 探测一个运行时命名空间的能力。
154
+ * @param {object} spine 运行时命名空间
155
+ * @param {string} version 版本号(仅记录)
156
+ * @returns {object} 能力矩阵
157
+ */
158
+ export function detectCapabilities(spine, version) {
159
+ const missing = simulatedMissing();
160
+ const warns = [];
161
+ /** say:探测结果收集器 —— 不 ok 就记一条 warn,并把 ok 原样返回(供三元/&& 链式使用) */
162
+ const say = (ok, key, reason) => { if (!ok) warns.push(`${key}:${reason}`); return ok; };
163
+
164
+ const m = collectMembers(spine);
165
+ const texture = probeTexture(m, missing, say);
166
+ const atlas = probeAtlas(m, missing, say);
167
+ const renderer = probeRenderer(m, warns);
168
+ const sk = probeSkeleton(spine, m, warns);
169
+
170
+ return {
171
+ version, // 仅用于展示
172
+ texture,
173
+ atlas,
174
+ renderer,
175
+ skeleton: sk.skeleton,
176
+ skinCtor: m.Skin,
177
+ skeletonOps: sk.ops,
178
+ warns,
179
+ /**
180
+ * 精简矩阵(给 getRendererInfo 展示用)。
181
+ * 这是**返回对象上的方法**,把 `this` 交给 matrixOf 构造 ——
182
+ * 不要改成箭头函数(箭头函数没有自己的 this,会读到模块作用域);
183
+ * 也不要把 capabilities 深拷贝 / JSON 序列化后跨线程传(this 会失效,调用即抛错)。
184
+ */
185
+ matrix() { return matrixOf(this); },
186
+ };
187
+ }
188
+
189
+ /** 按版本缓存探测结果(同一运行时只探测一次) */
190
+ const cache = new Map();
191
+
192
+ /**
193
+ * 取(或首次探测)某版本的能力矩阵。
194
+ * @param {object} spine
195
+ * @param {string} version
196
+ * @returns {object}
197
+ */
198
+ export function capabilitiesFor(spine, version) {
199
+ const key = String(version || 'unknown');
200
+ if (cache.has(key)) return cache.get(key);
201
+ const caps = detectCapabilities(spine, key);
202
+ cache.set(key, caps);
203
+ return caps;
204
+ }
205
+
206
+ /** 清空缓存(测试/重建上下文时用) */
207
+ export function resetCapabilities() {
208
+ cache.clear();
209
+ }
@@ -0,0 +1,37 @@
1
+ // engine/runtime.js —— 取 Spine 运行时命名空间(按骨架版本,带降级)
2
+ //
3
+ // 为什么不在这里自己加载源码:运行时管理已由 `spine/runtimes.js` 负责(版本清单、源码缓存、
4
+ // 隔离执行、能力探测)。引擎只消费它的结果,避免出现两套版本选择逻辑。
5
+
6
+ /**
7
+ * 解析某骨架版本对应的运行时命名空间。
8
+ * @param {object} manager SpineRuntimeManager 实例(由调用方注入,引擎不自己 new)
9
+ * @param {string} version 骨架版本(如 '3.8' / '4.1')
10
+ * @returns {Promise<{spine: object, version: string, supportsStraightAlpha: boolean}>}
11
+ */
12
+ export async function resolveRuntime(manager, version) {
13
+ if (!manager || typeof manager.getRuntimeWithFallback !== 'function') {
14
+ throw new Error('engine: 缺少运行时管理器(需要 getRuntimeWithFallback)');
15
+ }
16
+ const entry = await manager.getRuntimeWithFallback(version);
17
+ if (!entry || !entry.spine) throw new Error(`engine: 运行时 ${version} 不可用`);
18
+ return { spine: entry.spine, version: entry.version, supportsStraightAlpha: entry.supportsStraightAlpha !== false };
19
+ }
20
+
21
+ /**
22
+ * 按名字取运行时里的类。
23
+ * 为什么需要:目录里 7 个版本的命名空间形状不同 ——
24
+ * · 3.x:核心类(TextureAtlas/SkeletonJson/Skeleton…)在顶层,WebGL 类只挂在 `spine.webgl`;
25
+ * · 4.x:全部平铺在顶层(没有 `webgl` 子命名空间)。
26
+ * 统一走这个名字解析,避免每处都写一遍兼容分支。
27
+ * @param {object} spine 运行时命名空间
28
+ * @param {string} name 类名
29
+ * @returns {Function|null}
30
+ */
31
+ export function pickClass(spine, name) {
32
+ if (!spine) return null;
33
+ if (typeof spine[name] === 'function') return spine[name];
34
+ const w = spine.webgl;
35
+ if (w && typeof w[name] === 'function') return w[name];
36
+ return null;
37
+ }