modelstage 0.0.0-stage → 0.5.24

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 (286) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +101 -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 +58 -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 +647 -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 +334 -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/web/README.md +193 -0
  126. package/src/web/app/anim-list.js +222 -0
  127. package/src/web/app/background-solids.js +150 -0
  128. package/src/web/app/backgrounds.js +152 -0
  129. package/src/web/app/camera-log.js +28 -0
  130. package/src/web/app/context.js +72 -0
  131. package/src/web/app/dir-picker.js +242 -0
  132. package/src/web/app/entry-report.js +71 -0
  133. package/src/web/app/memory-bars.js +91 -0
  134. package/src/web/app/memory-budget.js +228 -0
  135. package/src/web/app/memory-guard.js +204 -0
  136. package/src/web/app/model-actions.js +682 -0
  137. package/src/web/app/renderer-setting.js +70 -0
  138. package/src/web/app/root-manager.js +254 -0
  139. package/src/web/app/status.js +138 -0
  140. package/src/web/app/topbar-actions.js +357 -0
  141. package/src/web/app/transport-actions.js +297 -0
  142. package/src/web/app/ui-state.js +559 -0
  143. package/src/web/app/url-params.js +171 -0
  144. package/src/web/app/view-settings.js +186 -0
  145. package/src/web/components.css +465 -0
  146. package/src/web/debug/diag-hud.js +95 -0
  147. package/src/web/debug/drag.js +369 -0
  148. package/src/web/debug/log.js +320 -0
  149. package/src/web/debug/model-cmp.js +292 -0
  150. package/src/web/debug/panel.css +474 -0
  151. package/src/web/debug/panel.js +712 -0
  152. package/src/web/debug/race-probe.js +82 -0
  153. package/src/web/debug/upload.js +130 -0
  154. package/src/web/engine/adapters/atlas.js +47 -0
  155. package/src/web/engine/adapters/index.js +22 -0
  156. package/src/web/engine/adapters/renderer.js +223 -0
  157. package/src/web/engine/adapters/skeleton.js +105 -0
  158. package/src/web/engine/adapters/texture.js +64 -0
  159. package/src/web/engine/animation.js +181 -0
  160. package/src/web/engine/appearance.js +136 -0
  161. package/src/web/engine/camera-adapter.js +212 -0
  162. package/src/web/engine/effect-chain.js +138 -0
  163. package/src/web/engine/engine-state.js +120 -0
  164. package/src/web/engine/filters.js +224 -0
  165. package/src/web/engine/fit.js +197 -0
  166. package/src/web/engine/frame-gate.js +33 -0
  167. package/src/web/engine/gl-driver.js +56 -0
  168. package/src/web/engine/gl.js +77 -0
  169. package/src/web/engine/index.js +20 -0
  170. package/src/web/engine/loader.js +55 -0
  171. package/src/web/engine/model-info.js +26 -0
  172. package/src/web/engine/pipeline.js +200 -0
  173. package/src/web/engine/player.js +427 -0
  174. package/src/web/engine/pma/blend.js +76 -0
  175. package/src/web/engine/pma/detect.js +118 -0
  176. package/src/web/engine/pma/index.js +8 -0
  177. package/src/web/engine/pma/resolve.js +112 -0
  178. package/src/web/engine/pma/upload.js +127 -0
  179. package/src/web/engine/pma.js +5 -0
  180. package/src/web/engine/render-frame.js +93 -0
  181. package/src/web/engine/runtime-capabilities.js +209 -0
  182. package/src/web/engine/runtime.js +37 -0
  183. package/src/web/engine/skins.js +165 -0
  184. package/src/web/engine/textures.js +174 -0
  185. package/src/web/index.html +115 -0
  186. package/src/web/infra/api.js +531 -0
  187. package/src/web/infra/diag.js +509 -0
  188. package/src/web/infra/document.js +269 -0
  189. package/src/web/infra/dom.js +31 -0
  190. package/src/web/infra/events.mjs +27 -0
  191. package/src/web/infra/log.js +153 -0
  192. package/src/web/infra/mem-stats.js +112 -0
  193. package/src/web/infra/plugin-host-registration.mjs +285 -0
  194. package/src/web/infra/plugin-host-report.mjs +32 -0
  195. package/src/web/infra/plugin-host-unload.mjs +79 -0
  196. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  197. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  198. package/src/web/infra/plugin-host.js +628 -0
  199. package/src/web/infra/ready.js +461 -0
  200. package/src/web/infra/schedule.js +51 -0
  201. package/src/web/infra/shortcuts.js +129 -0
  202. package/src/web/infra/slot-registry.js +402 -0
  203. package/src/web/infra/vfs.js +247 -0
  204. package/src/web/live2d/bridge.js +400 -0
  205. package/src/web/live2d/model.js +321 -0
  206. package/src/web/live2d/overlay.js +142 -0
  207. package/src/web/live2d/pixi-app.js +111 -0
  208. package/src/web/live2d/playback-probe.js +92 -0
  209. package/src/web/live2d/probe-geometry.js +329 -0
  210. package/src/web/live2d/probe.js +119 -0
  211. package/src/web/live2d/release.js +66 -0
  212. package/src/web/live2d/runtime-adapter.js +289 -0
  213. package/src/web/live2d/stage-ctx.js +152 -0
  214. package/src/web/live2d/stage.js +204 -0
  215. package/src/web/live2d/vfs-settings.js +174 -0
  216. package/src/web/live2d/view.js +89 -0
  217. package/src/web/main.js +629 -0
  218. package/src/web/panel/background-list.js +373 -0
  219. package/src/web/panel/bg-source.js +291 -0
  220. package/src/web/panel/color-picker.css +199 -0
  221. package/src/web/panel/color-picker.js +278 -0
  222. package/src/web/panel/dialog.css +116 -0
  223. package/src/web/panel/dialog.js +173 -0
  224. package/src/web/panel/dom.js +35 -0
  225. package/src/web/panel/icons.js +58 -0
  226. package/src/web/panel/memory-bar.js +195 -0
  227. package/src/web/panel/panel-settings.js +1263 -0
  228. package/src/web/panel/panel.css +1304 -0
  229. package/src/web/panel/render-choice.js +104 -0
  230. package/src/web/panel/schema.js +784 -0
  231. package/src/web/panel/settings-io.js +291 -0
  232. package/src/web/panel/shell-controls.js +274 -0
  233. package/src/web/panel/slider.js +210 -0
  234. package/src/web/panel/toolbar-schema.js +111 -0
  235. package/src/web/panel/toolbar.js +195 -0
  236. package/src/web/panel/transport-picker.js +375 -0
  237. package/src/web/panel/transport.js +321 -0
  238. package/src/web/panel/view.js +553 -0
  239. package/src/web/plugin/unpack-worker.js +185 -0
  240. package/src/web/render/camera/constants.js +60 -0
  241. package/src/web/render/camera/index.js +123 -0
  242. package/src/web/render/camera/spine.js +124 -0
  243. package/src/web/render/capture.js +30 -0
  244. package/src/web/render/interaction.js +527 -0
  245. package/src/web/render/live2d.js +627 -0
  246. package/src/web/render/playback.js +47 -0
  247. package/src/web/render/reset-view.js +29 -0
  248. package/src/web/render/spine.js +386 -0
  249. package/src/web/render/surface-switch.js +176 -0
  250. package/src/web/render/surface.js +571 -0
  251. package/src/web/render/surfaces.js +70 -0
  252. package/src/web/roster/filter.js +31 -0
  253. package/src/web/roster/list.js +276 -0
  254. package/src/web/roster/persist.js +52 -0
  255. package/src/web/roster/reveal.js +77 -0
  256. package/src/web/roster/rows.js +248 -0
  257. package/src/web/roster/snapshot.js +37 -0
  258. package/src/web/roster/view.js +670 -0
  259. package/src/web/spine/canvas-api-groups.js +267 -0
  260. package/src/web/spine/canvas-api.js +97 -0
  261. package/src/web/spine/canvas-document.js +80 -0
  262. package/src/web/spine/canvas-engine.js +193 -0
  263. package/src/web/spine/canvas-entry.js +28 -0
  264. package/src/web/spine/canvas-pixels.js +36 -0
  265. package/src/web/spine/canvas-runtime.js +127 -0
  266. package/src/web/spine/canvas-view.js +108 -0
  267. package/src/web/spine/canvas.js +7 -0
  268. package/src/web/spine/runtime-hint.js +73 -0
  269. package/src/web/spine/runtimes.js +212 -0
  270. package/src/web/style.css +493 -0
  271. package/tool/lib/bundle-probe-node.mjs +121 -0
  272. package/tool/lib/check-perms.mjs +114 -0
  273. package/tool/lib/contract-checks.mjs +582 -0
  274. package/tool/lib/cubism-core.mjs +122 -0
  275. package/tool/lib/export-bundle.mjs +328 -0
  276. package/tool/lib/paths.mjs +20 -0
  277. package/tool/lib/runtime-guides.mjs +65 -0
  278. package/tool/lib/runtime-package.mjs +403 -0
  279. package/tool/lib/spine-meta.mjs +227 -0
  280. package/tool/lib/spine-motions.mjs +104 -0
  281. package/tool/lib/spine-runtime.mjs +131 -0
  282. package/tool/lib/test-lock.mjs +178 -0
  283. package/tool/lib/test-report.mjs +133 -0
  284. package/tool/parse-jsonc.mjs +86 -0
  285. package/tool/resolve-live2d-runtimes.mjs +276 -0
  286. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,289 @@
1
+ // live2d/runtime-adapter.js —— Live2D 运行时的**唯一入口**
2
+ //
3
+ // 为什么要有这一层:运行时库换过世代,API 名字与调用形状都变过(settings 类名、
4
+ // Application 的 init 时机等)。如果让业务代码各自判断,加一个版本就要改多处,必漏。
5
+ // 这里把"版本差异"收成一张兼容表 + 几个中性函数,业务侧只说"要什么",不认具体类名。
6
+ //
7
+ // **允许出现旧类名字样的唯一位置**(grep 门禁):PIXI.live2d.* / Cubism4ModelSettings 等
8
+ // 只在本文件里出现,其它业务文件一律走这里的函数。
9
+
10
+ // 缺库提示(把服务端给的完整指引拼进错误消息);与 spine 侧共用同一个组装模块
11
+ import { live2dMissingHint } from '../spine/runtime-hint.js';
12
+
13
+ /** 运行时库(顺序不可颠倒:cubismcore 必须最先,pixi-live2d-display 初始化会检查它) */
14
+ export const RUNTIME_LIBS = [
15
+ '/live2d-lib/live2dcubismcore.min.js',
16
+ '/live2d-lib/live2d.min.js',
17
+ '/live2d-lib/pixi.min.js',
18
+ '/live2d-lib/index.min.js',
19
+ ];
20
+
21
+ /**
22
+ * 版本差异矩阵 —— **唯一真相**(代码与文档共用)。
23
+ * 一行 = 一个世代;加新版本只需加一行,业务代码零改动。
24
+ *
25
+ * settings[]. 同一能力的候选类名,按"该世代的新名 → 兼容旧名"顺序尝试
26
+ * appInit 构造 PIXI.Application 的策略:
27
+ * 'ctor' = new Application(opts)(6.x 同步构造)
28
+ * 'async' = new Application() + await app.init(opts)(8.x)
29
+ * 'auto' = 运行时探测 prototype.init(未登记世代的兜底)
30
+ * filter/model 两代的 Filter 构造与 Live2DModel.from 签名一致,这里没有分叉
31
+ */
32
+ const VERSION_MATRIX = {
33
+ 6: {
34
+ label: 'v6',
35
+ appInit: 'ctor',
36
+ settings: {
37
+ cubism4: ['Cubism4ModelSettings', 'CubismModelSettings'],
38
+ cubism2: ['Cubism2ModelSettings', 'CubismLegacyModelSettings'],
39
+ },
40
+ },
41
+ 8: {
42
+ label: 'v8',
43
+ appInit: 'async',
44
+ settings: {
45
+ cubism4: ['CubismModelSettings', 'Cubism4ModelSettings'],
46
+ cubism2: ['CubismLegacyModelSettings', 'Cubism2ModelSettings'],
47
+ },
48
+ },
49
+ };
50
+
51
+ /** 未登记世代的兜底:候选名取并集、appInit 走能力探测(新版本不会立刻挂) */
52
+ const FALLBACK_PROFILE = {
53
+ label: 'unknown',
54
+ appInit: 'auto',
55
+ settings: {
56
+ cubism4: ['CubismModelSettings', 'Cubism4ModelSettings'],
57
+ cubism2: ['CubismLegacyModelSettings', 'Cubism2ModelSettings'],
58
+ },
59
+ };
60
+
61
+ /** 按运行时实际 PIXI 主版本取矩阵行(取不到就用兜底) */
62
+ function profile() {
63
+ const PIXI = pixiLib();
64
+ const major = PIXI && PIXI.VERSION ? Number(String(PIXI.VERSION).split('.')[0]) : NaN;
65
+ return VERSION_MATRIX[major] || FALLBACK_PROFILE;
66
+ }
67
+
68
+ /** 当前世代的标签(runtimeInfo / 排障用) */
69
+ export function runtimeFamily() {
70
+ return profile().label;
71
+ }
72
+
73
+ /** loadRuntime 的单例 Promise:并发调用共用同一次加载 */
74
+ let loadingPromise = null;
75
+
76
+ function pixiLib() {
77
+ return (typeof window !== 'undefined' && window.PIXI) ? window.PIXI : null;
78
+ }
79
+
80
+ /**
81
+ * <script> 加载失败时浏览器只给通用 error —— **读不到响应体**。
82
+ * 而服务端在 404/503 时已经把完整指引(官网 URL、目标目录、"不代下"的原因)写在 JSON error 里,
83
+ * 不取回来那句提示就白给了。所以补一次 fetch 只为接上那段文本(不是重试,不改变加载行为)。
84
+ * @param {string} url
85
+ * @returns {Promise<string>} 服务端 error 文本;取不到返回空串
86
+ */
87
+ async function readServerError(url) {
88
+ try {
89
+ // 5s:这一步是在 <script> **已失败**之后补读一次响应体,结果会被 await 进 reject 之前,
90
+ // 所以它自己绝不能挂住 —— 否则"加载失败"永远不 reject,Live2D 激活会一直悬着。
91
+ // 5s 依据:服务端这条是纯本地 JSON(服务端已实测毫秒级返回),5s 只作兜底。
92
+ const r = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(5000) });
93
+ const text = await r.text();
94
+ try {
95
+ const j = JSON.parse(text);
96
+ return (j && j.error) || '';
97
+ } catch {
98
+ return '';
99
+ }
100
+ } catch {
101
+ return '';
102
+ }
103
+ }
104
+
105
+ /** 注入一个 <script>;失败要摘掉标签,否则重试时页面上留着坏标签 */
106
+ function injectScript(src) {
107
+ return new Promise((resolve, reject) => {
108
+ const el = document.createElement('script');
109
+ el.src = src;
110
+ el.onload = () => resolve(src);
111
+ el.onerror = () => {
112
+ el.remove();
113
+ // 把服务端的完整指引拼进错误消息 ⇒ 上层 stageLog 原样打进调试日志窗口
114
+ readServerError(src).then((detail) => {
115
+ reject(new Error(live2dMissingHint(src, detail)));
116
+ });
117
+ };
118
+ document.head.appendChild(el);
119
+ });
120
+ }
121
+
122
+ /**
123
+ * 注入全部运行时脚本(幂等)。
124
+ * 失败**不缓存**:一次瞬时失败(服务重启、网络抖动)若留着 rejected promise,
125
+ * Live2D 就永久激活不了,只能刷新页面。
126
+ * @param {(msg:string, level?:string)=>void} [note]
127
+ * @returns {Promise<void>}
128
+ */
129
+ export async function loadRuntime(note = () => {}) {
130
+ if (loadingPromise) return loadingPromise;
131
+ loadingPromise = (async () => {
132
+ for (const src of RUNTIME_LIBS) {
133
+ await injectScript(src);
134
+ note(`运行时已加载:${src.split('/').pop()}`);
135
+ }
136
+ const PIXI = pixiLib();
137
+ if (!PIXI || !PIXI.live2d) {
138
+ throw new Error('PIXI.live2d 不存在:运行时库顺序或版本不匹配(live2dcubismcore 必须最先)');
139
+ }
140
+ note(`Live2D 运行时就绪(PIXI ${PIXI.VERSION || 'unknown'})`);
141
+ })().catch((err) => {
142
+ loadingPromise = null; // 失败不缓存
143
+ throw err;
144
+ });
145
+ return loadingPromise;
146
+ }
147
+
148
+ /** 运行时是否已就绪(同步判断,不触发加载) */
149
+ export function isReady() {
150
+ const PIXI = pixiLib();
151
+ return !!(PIXI && PIXI.live2d);
152
+ }
153
+
154
+ /** 等运行时就绪(没加载过就先加载) */
155
+ export async function waitReady(note) {
156
+ if (isReady()) return;
157
+ await loadRuntime(note);
158
+ }
159
+
160
+ /**
161
+ * 实际版本与解析结果(排障/验收用)。
162
+ * 抓不到的写 unknown —— 版本写错比 unknown 危险。
163
+ * @returns {{pixi:string, display:string, cubism2:string, settingsCtorName:string|null}}
164
+ */
165
+ export function runtimeInfo() {
166
+ const PIXI = pixiLib();
167
+ const lib = PIXI && PIXI.live2d ? PIXI.live2d : null;
168
+ const prof = profile();
169
+ const ctorName = lib ? (prof.settings.cubism4.find((n) => typeof lib[n] === 'function')
170
+ || prof.settings.cubism2.find((n) => typeof lib[n] === 'function')
171
+ || null) : null;
172
+ // cubism core 的版本只能运行时问
173
+ let cubismCore = 'unknown';
174
+ try {
175
+ const ver = window.Live2DCubismCore && window.Live2DCubismCore.Version
176
+ && typeof window.Live2DCubismCore.Version.csmGetVersion === 'function'
177
+ ? window.Live2DCubismCore.Version.csmGetVersion() : null;
178
+ if (ver) cubismCore = String(ver);
179
+ } catch { /* 取不到就 unknown */ }
180
+ return {
181
+ pixi: (PIXI && PIXI.VERSION) || 'unknown',
182
+ display: 'unknown', // index.min.js 是 UMD 包装,无版本字符串(VERSIONS.json 里人工填)
183
+ cubism2: 'unknown', // live2d.min.js 混淆代码,无版本字符串
184
+ cubismCore,
185
+ settingsCtorName: ctorName,
186
+ };
187
+ }
188
+
189
+ /**
190
+ * 取设置构造器(版本差异只在这里收敛)。
191
+ *
192
+ * **取不到就抛 version-unsupported,不再 return null 静默降级**:
193
+ * 过去的做法是返回 null、上层 warn 一句然后回落 URL 路径,结果是
194
+ * "vfs:// 直接交给库 → Invalid URL → 模型对象残缺 → doDrawModel 报 undefined[0]",
195
+ * 一串症状掩盖了真正原因(类名对不上)。宁可当场报清楚。
196
+ * @param {'cubism4'|'cubism2'} kind
197
+ * @returns {Function}
198
+ */
199
+ export function getSettingsCtor(kind) {
200
+ const PIXI = pixiLib();
201
+ const lib = PIXI && PIXI.live2d ? PIXI.live2d : null;
202
+ if (!lib) throw new Error('version-unsupported: PIXI.live2d 不存在(运行时未加载)');
203
+ const names = profile().settings[kind === 'cubism2' ? 'cubism2' : 'cubism4'];
204
+ for (const name of names) {
205
+ if (typeof lib[name] === 'function') return lib[name];
206
+ }
207
+ const available = Object.keys(lib).filter((k) => /Settings$/.test(k));
208
+ throw new Error(`version-unsupported: 找不到 ${kind} 的设置类(试过 ${names.join(' / ')})`
209
+ + `;库实际导出的 Settings 类有:${available.join(', ') || '(无)'}`);
210
+ }
211
+
212
+ /**
213
+ * 建模型实例(兼容 settings 实例与 URL 两种入参)。
214
+ *
215
+ * **URL 回落路径也要把 vfs:// 换成可 fetch 的 blob**:库不认识自定义协议,
216
+ * 直接喂 vfs:// 会 `Failed to construct 'URL': Invalid URL`。这里是"库版本不支持时的
217
+ * 唯一退路",退路本身不能是坏的。
218
+ * @param {object|string} source settings 实例,或模型定义地址
219
+ * @param {{autoInteract?:boolean, resolveVfs?:(url:string)=>string|null}} [opts]
220
+ */
221
+ export async function createModel(source, opts = {}) {
222
+ const PIXI = pixiLib();
223
+ if (!PIXI || !PIXI.live2d || !PIXI.live2d.Live2DModel) {
224
+ throw new Error('Live2DModel 不可用:运行时未加载或版本不支持');
225
+ }
226
+ let arg = source;
227
+ if (typeof source === 'string') {
228
+ // URL 形态:先换 blob(转换函数由调用方注入,复用它的 VFS 句柄,不在这里另建一个)
229
+ const resolved = typeof opts.resolveVfs === 'function' ? opts.resolveVfs(source) : null;
230
+ if (!resolved) {
231
+ throw new Error(`模型地址无法解析(不是可 fetch 的地址):${source}`);
232
+ }
233
+ arg = resolved;
234
+ }
235
+ return PIXI.live2d.Live2DModel.from(arg, { autoInteract: opts.autoInteract === true });
236
+ }
237
+
238
+ /**
239
+ * 建 PIXI.Application(8.x 是异步 init,6.x 是构造时给 options)。
240
+ * @param {object} opts 传给 Application 的选项
241
+ * @returns {Promise<object>} 已就绪的 app
242
+ */
243
+ export async function createApplication(opts = {}) {
244
+ const PIXI = pixiLib();
245
+ if (!PIXI || typeof PIXI.Application !== 'function') throw new Error('PIXI.Application 不可用');
246
+ // 策略来自版本矩阵;未登记世代('auto')按 prototype.init 能力探测兜底
247
+ const mode = profile().appInit;
248
+ const useAsync = mode === 'async'
249
+ || (mode === 'auto' && typeof PIXI.Application.prototype.init === 'function');
250
+ if (!useAsync) return new PIXI.Application(opts); // 6.x:构造时给 options
251
+ const app = new PIXI.Application(); // 8.x:先构造再 await init
252
+ await app.init(opts);
253
+ return app;
254
+ }
255
+
256
+ /**
257
+ * 把模型挂到舞台上(挂载方式收在一处,业务侧不关心 addChild 细节)。
258
+ * @param {object} app
259
+ * @param {object} model
260
+ */
261
+ export function attachToStage(app, model) {
262
+ if (!app || !app.stage || !model) return false;
263
+ if (typeof app.stage.addChild === 'function') app.stage.addChild(model);
264
+ return true;
265
+ }
266
+
267
+ /**
268
+ * 设/清滤镜(含 filterArea 固定整屏;摘除时只脱引用不 destroy)。
269
+ * @param {object} app
270
+ * @param {object[]|null} filters null/空数组 = 摘掉
271
+ * @returns {boolean}
272
+ */
273
+ export function setFilters(app, filters) {
274
+ if (!app || !app.stage) return false;
275
+ if (!filters || !filters.length) {
276
+ app.stage.filters = null;
277
+ return true;
278
+ }
279
+ app.stage.filterArea = app.screen;
280
+ app.stage.filters = filters;
281
+ return true;
282
+ }
283
+
284
+ /** 建一个滤镜实例(Filter 构造签名两代一致,但仍收在这里以满足"业务侧不碰 PIXI.*") */
285
+ export function createFilter(fragmentSrc, uniforms) {
286
+ const PIXI = pixiLib();
287
+ if (!PIXI || typeof PIXI.Filter !== 'function') return null;
288
+ return new PIXI.Filter(null, fragmentSrc, uniforms);
289
+ }
@@ -0,0 +1,152 @@
1
+ // live2d/stage-ctx.js —— Live2D 舞台的运行时上下文(状态 + 薄工具)
2
+ //
3
+ // 为什么单独成模块:整个 Live2D 分支是"一个对象 + 一堆方法"的形状,状态(st / handleRef)
4
+ // 是它们共同的隐式参数。把这个上下文集中到一处,其余模块按职责切开时才有明确的边界。
5
+ //
6
+ // 约定:ctx.st 对子模块可见(读写)。这些函数本来就是同一个对象的内部实现,
7
+ // 再套一层 getter/setter 只增加噪音,换不来真实约束。
8
+ // ctx.api 是装配期注册表:stage.js 依次建好各模块后挂进来,模块之间不直接 import(避免环)。
9
+ import { CAMERA } from '../render/camera/constants.js';
10
+
11
+ /** 覆盖层容器 id */
12
+ export const OVERLAY_ID = 'live2d-overlay';
13
+ /** 覆盖层内 canvas 的 id */
14
+ export const OVERLAY_CANVAS_ID = 'live2d-overlay-canvas';
15
+ /** 运行时库(顺序不可颠倒:cubismcore 必须最先,pixi-live2d-display 初始化会检查它) */
16
+ export const LIBS = [
17
+ '/live2d-lib/live2dcubismcore.min.js',
18
+ '/live2d-lib/live2d.min.js',
19
+ '/live2d-lib/pixi.min.js',
20
+ '/live2d-lib/index.min.js',
21
+ ];
22
+ /** 缩放范围唯一来源:render/camera/constants.js(禁止在此写裸数字) */
23
+ export const ZOOM_MIN = CAMERA.MIN;
24
+ export const ZOOM_MAX = CAMERA.MAX;
25
+
26
+ /**
27
+ * 判定"带 scheme 的引用"(拼接目录时用):任意 scheme 或 data: 都不要拼目录。
28
+ *
29
+ * 为什么拼接前必须判:模型定义里允许写绝对 URL,拼上去会变成
30
+ * `/files/dir/http://host/x.motion3.json` 这种无效地址(曾导致 14 次 miss)。
31
+ * 注意这里包含 `vfs://` —— 它的作用是"别再前缀目录",不是"可直连"。
32
+ * @param {string} url
33
+ * @returns {boolean}
34
+ */
35
+ export function isAbsoluteRef(url) {
36
+ const text = String(url == null ? '' : url);
37
+ if (!text) return false;
38
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(text)) return true;
39
+ return /^data:/i.test(text); // data: 可能不带 //
40
+ }
41
+
42
+ /**
43
+ * 判定"浏览器可直接取用"的地址(resolveAsset 短接用):http/https/blob/data。
44
+ *
45
+ * **不能包含 vfs://**:vfs:// 是虚拟地址,必须交给 VirtualFS 解析成 blob/真实地址;
46
+ * 当成"可直连"放行会让浏览器直接 fetch('vfs://…') → Network error
47
+ * (这条我自己先踩了一次:短接后 vfs:// 装载全挂、hits 变 0)。
48
+ * @param {string} url
49
+ * @returns {boolean}
50
+ */
51
+ export function isDirectUrl(url) {
52
+ const text = String(url == null ? '' : url);
53
+ if (!text) return false;
54
+ if (/^(https?|blob):/i.test(text)) return true;
55
+ return /^data:/i.test(text);
56
+ }
57
+
58
+ /** 创建运行时上下文。
59
+ * @param {{vfs?: object, getOverlayHost?: Function, log?: Function}} options
60
+ * @returns {object} ctx(st / handleRef / 薄工具 / api 注册表)
61
+ */
62
+ export function createStageContext({ vfs, getOverlayHost, log } = {}) {
63
+ const ctx = {
64
+ vfs: vfs || null,
65
+ getOverlayHost: typeof getOverlayHost === 'function' ? getOverlayHost : null,
66
+ /** 装配期注册表:overlay / pixiApp / view / probe / model 依次挂进来 */
67
+ api: {},
68
+ };
69
+ /** StageSurface 句柄(画布/尺寸/DPR 由 Surface 提供;为空时退回自建覆盖层) */
70
+ ctx.handleRef = null;
71
+
72
+ const st = {
73
+ app: null, // 渲染应用实例(经 runtime-adapter 创建)
74
+ model: null, // 模型实例(经 runtime-adapter 创建)
75
+ libsReady: null, // 库加载 Promise
76
+ active: false, // 是否处于激活态(引擎 l2dActive 依赖它)
77
+ zoomRatio: 1, // 缩放倍数(唯一写入方是 bridge.setZoomRatio)
78
+ panX: 0,
79
+ panY: 0,
80
+ modelId: null, // 当前模型 id(引擎用它判断复用)
81
+ meta: null, // 当前模型元数据(motions / expressions)
82
+ motionMap: new Map(), // 标签 -> { group, index }
83
+ };
84
+ ctx.st = st;
85
+
86
+ /** 统一日志出口(缺省静默) */
87
+ function note(message, level = 'info') {
88
+ if (typeof log === 'function') {
89
+ try { log(message, level); } catch { /* 日志失败不影响渲染 */ }
90
+ }
91
+ }
92
+
93
+ /** 资源入口经 VirtualFS 解析(命中用 Blob URL,未命中回落原 URL) */
94
+ function resolveAsset(url) {
95
+ if (!url) return null;
96
+ // 已经可直接取用(http/https/blob/data)就不必问 VFS:它对这些永远 miss
97
+ // 注意 vfs:// 不在此列 —— 那是虚拟地址,必须由 VirtualFS 解析
98
+ if (isDirectUrl(url)) return url;
99
+ try {
100
+ const hit = vfs && typeof vfs.resolve === 'function' ? vfs.resolve(url) : null;
101
+ return hit || url;
102
+ } catch {
103
+ return url;
104
+ }
105
+ }
106
+
107
+ /** 取条目的入口地址(兼容 entry.url / modelUrl) */
108
+ function entryUrlOf(doc) {
109
+ if (!doc) return null;
110
+ if (doc.entry && doc.entry.url) return doc.entry.url;
111
+ return doc.modelUrl || null;
112
+ }
113
+
114
+ /**
115
+ * Surface 下发的几何(唯一尺寸来源)。
116
+ *
117
+ * 必须兼容两种"句柄"形态,因为调用方给的并不总是同一个东西:
118
+ * · 真 handle:`{ geometry(), dpr, ... }` —— Surface 在 mount / render(handle) 时传;
119
+ * · 轻量盒: `{ size: { width, height }, dpr }` —— render/live2d.js 在 resize() 里传。
120
+ * 早期只认第一种,于是渲染器 resize 那条路径上 `surfaceGeometry()` 恒为 null:
121
+ * 渲染器自己 resize 了 PIXI,但覆盖层**容器**的 CSS 盒拿不到尺寸、没人更新 ⇒
122
+ * 画布被旧容器裁掉(真机旋转后"模型挤在左侧窄带")。这里补上第二种形态即可。
123
+ *
124
+ * @returns {{cssW:number, cssH:number, dpr:number, res:number}|null}
125
+ */
126
+ function surfaceGeometry() {
127
+ try {
128
+ const h = ctx.handleRef;
129
+ if (!h) return null;
130
+ if (typeof h.geometry === 'function') return h.geometry();
131
+ const size = h.size || null;
132
+ if (size && size.width > 0 && size.height > 0) {
133
+ // 轻量盒没有 res 字段时用 dpr 顶上(Surface 的 res 缺省就是设备比)
134
+ const res = Number(h.res) > 0 ? Number(h.res) : (Number(h.dpr) > 0 ? Number(h.dpr) : 1);
135
+ return { cssW: size.width, cssH: size.height, dpr: res, res };
136
+ }
137
+ return null;
138
+ } catch { return null; }
139
+ }
140
+
141
+ /** Surface 下发的 CSS 尺寸 */
142
+ function surfaceSize() {
143
+ const g = surfaceGeometry();
144
+ return g ? { width: g.cssW, height: g.cssH } : null;
145
+ }
146
+
147
+ function width8() { const g = surfaceGeometry(); return g ? g.cssW : 0; }
148
+ function height8() { const g = surfaceGeometry(); return g ? g.cssH : 0; }
149
+
150
+ Object.assign(ctx, { note, resolveAsset, entryUrlOf, surfaceGeometry, surfaceSize, width8, height8 });
151
+ return ctx;
152
+ }
@@ -0,0 +1,204 @@
1
+ // live2d/stage.js —— Live2D 渲染器装配(原生 ES 模块,无构建)
2
+ //
3
+ // 角色:作为「2D 渲染器」的 Live2D 分支实现,与 Spine 共用同一套相机与交互。
4
+ // · 覆盖层 canvas 覆盖在 Spine 画布之上(pointer-events:none)⇒ 指针事件落到舞台区域,
5
+ // 由交互层驱动当前渲染器的相机;Live2D 侧的相机动作经 bridge 落到 PIXI。
6
+ // · 本文件只做**装配**:建上下文 → 依次建各职责模块 → 组装 handle → 装诊断 HUD。
7
+ // 具体实现在同目录下:
8
+ // stage-ctx.js 状态与薄工具(st / handleRef / 尺寸 / vfs 解析 / 常量)
9
+ // overlay.js 覆盖层容器与画布的 DOM 显隐(尺寸由 overlay.setBox 写)
10
+ // pixi-app.js 运行时库懒加载与 PIXI 实例、渲染器尺寸校准
11
+ // view.js 视图数学:computeFit / applyZoom / syncRect / syncSize
12
+ // probe.js 只读诊断:像素采样 / MODELCMP / rendererInfo / rendererSize
13
+ // probe-geometry.js 只读诊断:inspectGeometry(几何取证)
14
+ // model.js 模型装载与动作表
15
+ // bridge.js 相机 backend 与播放控制(对外能力面)
16
+ //
17
+ // 资源入口先经 VirtualFS.resolve()(命中用 Blob URL,未命中回落原 URL)——不 patch PIXI 加载器。
18
+ import { installDiagHud } from '../debug/diag-hud.js';
19
+ import { createStageContext, OVERLAY_CANVAS_ID } from './stage-ctx.js';
20
+ import { releaseLive2DResources } from './release.js';
21
+ import { createOverlay } from './overlay.js';
22
+ import { createPixiApp } from './pixi-app.js';
23
+ import { createView } from './view.js';
24
+ import { createProbe } from './probe.js';
25
+ import { createProbeGeometry } from './probe-geometry.js';
26
+ import { createModel } from './model.js';
27
+ import { createBridge } from './bridge.js';
28
+
29
+ /**
30
+ * 创建 Live2D 舞台。
31
+ * @param {{
32
+ * vfs?: { resolve: (url: string) => string|null },
33
+ * getOverlayHost?: () => HTMLElement|null,
34
+ * log?: (message: string, level?: 'info'|'warn'|'error') => void,
35
+ * }} options
36
+ */
37
+ export function createLive2DStage({ vfs, getOverlayHost, log } = {}) {
38
+ const ctx = createStageContext({ vfs, getOverlayHost, log });
39
+
40
+ // 装配顺序 = 依赖顺序(无环):
41
+ // overlay → pixiApp → view → probe → probeGeometry → model → bridge
42
+ // 为什么用 ctx.api 注册表而不是模块间直接 import:模块之间是"互相调用"的关系
43
+ // (overlay 要 pixiApp.fitAppToOverlay、model 要 view.applyZoom…),直接 import 会成环;
44
+ // 装配期一次性挂进来,运行期只按名字取。
45
+ const overlay = createOverlay(ctx);
46
+ ctx.api.overlay = overlay;
47
+ const pixiApp = createPixiApp(ctx);
48
+ ctx.api.pixiApp = pixiApp;
49
+ const view = createView(ctx);
50
+ ctx.api.view = view;
51
+ const probe = createProbe(ctx);
52
+ ctx.api.probe = probe;
53
+ const probeGeometry = createProbeGeometry(ctx);
54
+ ctx.api.probeGeometry = probeGeometry;
55
+ const model = createModel(ctx);
56
+ ctx.api.model = model;
57
+ const bridge = createBridge(ctx);
58
+ ctx.api.bridge = bridge;
59
+
60
+ // 诊断 HUD 句柄(延迟到首次模型加载成功后安装,见 model.loadModel 末尾)
61
+ let diagHandle = null;
62
+
63
+ /**
64
+ * 装载入口(渲染器调它)。
65
+ * @param {object} doc 文档/条目
66
+ * @param {object} handle StageSurface 句柄
67
+ */
68
+ /**
69
+ * 断言"Live2D 画布真的在页面上"。
70
+ *
71
+ * 为什么需要这个断言(血的教训):
72
+ * PIXI 在**游离 canvas** 上渲染时,画面完全不可见,但 `readPixels`/像素采样**照样能读到有效像素**——
73
+ * 于是"ratio>0 / alpha=255"这类判据会把白屏判成正常。唯一可靠的判据是 DOM 关系:
74
+ * canvas 在容器里、容器在文档里、且 `app.view` 就是这张 canvas。
75
+ * @returns {{ok: boolean, detail: object}}
76
+ */
77
+ function assertCanvasAttached() {
78
+ const app = ctx.st.app;
79
+ const el = (ctx.st.overlay && ctx.st.overlay.isConnected) ? ctx.st.overlay : document.getElementById('live2d-overlay');
80
+ const canvas = document.getElementById(OVERLAY_CANVAS_ID);
81
+ const inContainer = !!(canvas && el && typeof el.contains === 'function' && el.contains(canvas));
82
+ const inDom = !!(canvas && canvas.isConnected);
83
+ const viewOk = !!(app && app.view && app.view.isConnected === true);
84
+ const viewIsCanvas = !!(app && canvas && app.view === canvas);
85
+ return {
86
+ ok: inContainer && inDom && viewOk && viewIsCanvas,
87
+ detail: {
88
+ hasCanvas: !!canvas, inContainer, inDom, viewOk, viewIsCanvas,
89
+ appViewId: app && app.view ? (app.view.id || '(无 id)') : null,
90
+ },
91
+ };
92
+ }
93
+
94
+ async function mount(doc, handle) {
95
+ ctx.handleRef = handle || null;
96
+ const entry = doc && (doc.model || doc);
97
+ const result = await bridge.loadForEngine(entry);
98
+ // 装载后立刻校验"画布在页面上":同面切换曾因 canvas 被 PIXI 摘掉而整屏空白,
99
+ // 而 stats/像素采样全都报正常;这里让缺陷当场变成一条 warn(自检也会读它)。
100
+ const chk = assertCanvasAttached();
101
+ if (!chk.ok) ctx.note(`Live2D 画布不在页面上(画面将不可见):${JSON.stringify(chk.detail)}`, 'warn');
102
+ return result;
103
+ }
104
+
105
+ /**
106
+ * 卸载 / 彻底释放:**同一条释放路径**(实现在 live2d/release.js,便于静态回归)。
107
+ *
108
+ * 为什么 live2d 两面不做区分(与 spine 相反):
109
+ * PIXI `app.destroy(true)` 的 removeView 会摘掉 canvas 只留容器,留下「容器在、canvas 不在」
110
+ * 的中间态;一旦下游按容器复用,PIXI 就会渲染到**不在 DOM 的游离画布**上——页面全白,
111
+ * 而 stats 与像素采样全都正常(曾真实发生)。app 与覆盖层 DOM 重建成本都很低,
112
+ * 不值得为"保留 DOM"维护第二套生命周期。下次 mount 由 buildOverlay 全量重建。
113
+ * spine 相反:重建引擎要重注入运行时源码并按版本选运行时,代价高,所以那边保留引擎。
114
+ */
115
+ function unmount() {
116
+ // **只卸内容、保留引擎**(与 spine 同语义):surface-switch 对"同一渲染器换内容"选 unmount,
117
+ // 本意就是复用引擎/PIXI 覆盖层。
118
+ //
119
+ // 为什么改(实测依据):原来 unmount 与 dispose 走同一条 release 路径 ⇒ 每次 live2d→live2d
120
+ // 都 `app.destroy(true)` 再新建 PIXI.Application ⇒ 每切一次净增 2 个存活 WebGL 上下文
121
+ // (1 个匿名 0x0 探测画布 + 1 个 overlay canvas),累积到浏览器上限(~16)后最旧上下文被丢弃
122
+ // ⇒ 画布黑(现场:"加载过那个角色的 Live2D/资源包之后,后续请求全挂")。
123
+ // 而"容器在、canvas 不在"的旧坑是 **destroy(true) 的 removeView** 造成的——只要 unmount 不销毁 app,
124
+ // 这个中间态就不会出现。
125
+ if (ctx.st.app) {
126
+ try { if (ctx.st.model) { ctx.st.app.stage.removeChild(ctx.st.model); } } catch { /* 忽略 */ }
127
+ try { if (ctx.st.model) ctx.st.model.destroy({ children: true }); } catch { /* 忽略 */ }
128
+ ctx.st.model = null;
129
+ ctx.st.modelId = null;
130
+ try { if (typeof ctx.st.app.stop === 'function') ctx.st.app.stop(); } catch { /* 忽略 */ }
131
+ try { if (ctx.st.overlay) ctx.st.overlay.style.display = 'none'; } catch { /* 忽略 */ }
132
+ ctx.st.active = false;
133
+ stopDiagOnUnmount();
134
+ return true;
135
+ }
136
+ // 没有 app(异常时序):退回完整释放,保证不留半截状态
137
+ releaseLive2DResources({ st: ctx.st, note: ctx.note, stopDiag: takeDiagStop() });
138
+ return true;
139
+ }
140
+
141
+ /** 停诊断 HUD(unmount 走轻路径时也要停,否则 HUD 会在隐藏的覆盖层上空转) */
142
+ function stopDiagOnUnmount() {
143
+ try { const stop = takeDiagStop(); if (typeof stop === 'function') stop(); } catch { /* 忽略 */ }
144
+ }
145
+
146
+ /** 彻底释放:与 unmount 同一路径(渲染器实例是否丢弃由调用方决定) */
147
+ function dispose() {
148
+ releaseLive2DResources({ st: ctx.st, note: ctx.note, stopDiag: takeDiagStop() });
149
+ }
150
+
151
+ /** 取出并清空诊断 HUD 停止函数(幂等:第二次返回 null) */
152
+ function takeDiagStop() {
153
+ const h = diagHandle;
154
+ diagHandle = null;
155
+ return h && typeof h.stop === 'function' ? () => h.stop() : null;
156
+ }
157
+
158
+ // 诊断 HUD:仅在 ?diag=1 时创建;所有取值都延迟到调用时(此处 app 可能为 null)
159
+ diagHandle = installDiagHud({
160
+ getRendererInfo: () => {
161
+ try {
162
+ const w = typeof window !== 'undefined' ? window.__WEB__ : null;
163
+ return w && w.live2d && typeof w.live2d.rendererInfo === 'function' ? w.live2d.rendererInfo() : null;
164
+ } catch { return null; }
165
+ },
166
+ readCanvasPixels: (...args) => probe.readCanvasPixels(...args),
167
+ overlayCanvasId: OVERLAY_CANVAS_ID,
168
+ log: (m) => ctx.note(m),
169
+ });
170
+
171
+ // 对外面:成员集合与顺序属于契约,增删需同步渲染器与面板
172
+ return {
173
+ id: 'live2d',
174
+ bridge,
175
+ mount,
176
+ unmount,
177
+ dispose,
178
+ show: (m) => bridge.loadForEngine(m),
179
+ hide: () => overlay.hide(),
180
+ isActive: () => ctx.st.active,
181
+ debug: () => bridge.readState(),
182
+ /** 诊断:按当前状态采一条 MODELCMP(不改渲染) */
183
+ collectCmp: (tag) => probe.collectCmp(tag),
184
+ syncRect: () => view.syncRect(),
185
+ readCanvasPixels: (points) => probe.readCanvasPixels(points),
186
+ /** 只读:取 PIXI app 引用(诊断实验用,不改变渲染逻辑) */
187
+ getApp() { return ctx.st.app; },
188
+ rendererInfo: () => probe.rendererInfo(),
189
+ inspectGeometry: () => probeGeometry.inspectGeometry(),
190
+ rendererSize: () => probe.rendererSize(),
191
+ /**
192
+ * 尺寸同步:Surface 量好尺寸后调用(渲染器不自己读 DOM)。
193
+ * @param {object} handle StageSurface 句柄
194
+ */
195
+ syncSize: (handle) => view.syncSize(handle),
196
+ /** 写一条日志(供渲染器记录切换断言) */
197
+ note: (message) => ctx.note(message),
198
+ /**
199
+ * 断言"画布真的在页面上"(切换/装载后自检用)。
200
+ * 像素采样在游离 canvas 上照样能读到有效像素,所以可见性只能用 DOM 关系判定。
201
+ */
202
+ assertCanvasAttached,
203
+ };
204
+ }