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,321 @@
1
+ // live2d/model.js —— 模型装载与动作表
2
+ //
3
+ // 装载顺序有讲究(历史上踩过 BUG-3):
4
+ // 先 showOverlay 让覆盖层可见 → 再建 PIXI。反过来的话 PIXI 在 display:none 下读到
5
+ // 0 尺寸、停在默认 800×600,首次可见时若不重新校准就只剩一块"内容矩形"。
6
+ //
7
+ // 动作表来自 model3.json(fetchDefinition),不用接口元数据 —— 那里 motions/expressions 是空的。
8
+ //
9
+ // 内部文件(moc3 / 贴图 / physics / 动作)优先走 VirtualFS(见 vfs-settings.js),
10
+ // 库不支持或语义变化时**回落 URL 路径**(可退化,绝不白屏)。
11
+ //
12
+ // 结构:所有函数都在模块级、只接收 ctx/显式参数(工厂里不再嵌一堆闭包),
13
+ // 这样每个函数都能单独审阅,工厂只做装配。
14
+ import { createVfsSettings } from './vfs-settings.js';
15
+ // 模型实例统一经 adapter 建;URL 回落路径的 vfs→blob 转换也在那边(与 spine 侧共用同一套 VFS.resolve)
16
+ // 重命名:本文件自己导出一个 createModel(ctx)(对外 API),别与 adapter 的同名函数撞
17
+ import { createModel as createModelInstance } from './runtime-adapter.js';
18
+
19
+ // ---------------------------------------------------------------- 纯函数
20
+
21
+ /** 从 model3.json 里取动作/表情表 */
22
+ function definitionParts(json) {
23
+ const ref = (json && json.FileReferences) || {};
24
+ return {
25
+ motions: ref.Motions || {},
26
+ expressions: (ref.Expressions || []).map((e, i) => ({ name: (e && e.Name) || `exp${i + 1}` })),
27
+ };
28
+ }
29
+
30
+ /** 动作条目的名字(兼容字符串 / { Name } / { File } 三种写法) */
31
+ function motionNameOf(mo) {
32
+ if (typeof mo === 'string') return mo;
33
+ if (!mo) return '';
34
+ return String(mo.Name || mo.File || '');
35
+ }
36
+
37
+ /**
38
+ * 动作标签 = **动作名**(文件名去后缀;.motion3 / .motion3.json / .json 都算)。
39
+ *
40
+ * 为什么不带 group 前缀:Cubism 的动作文件名通常与 group 同名,拼出来就是
41
+ * `idle / idle` 这种自我重复,与 Spine 面的"名称"不一致;group 仍保留在
42
+ * motionMap 的 value 里用于播放,不丢信息。
43
+ */
44
+ function motionLabel(_group, name) {
45
+ return String(name || '').split('/').pop().replace(/\.motion3(\.json)?$/i, '').replace(/\.json$/i, '');
46
+ }
47
+
48
+ /** 采样点统计:总数与非透明点数 */
49
+ function probeSummary(probe) {
50
+ const list = probe || [];
51
+ return { total: list.length, opaque: list.filter((x) => x.rgba && x.rgba[3] > 0).length };
52
+ }
53
+
54
+ // ---------------------------------------------------------------- 定义 / 动作表
55
+
56
+ /**
57
+ * 读模型定义(model3.json):动作表 + 原始 json(后者是 vfs-settings 的原料)。
58
+ * 读失败不抛:动作列表为空但装载仍可继续。
59
+ */
60
+ async function fetchDefinition(ctx, model) {
61
+ // 定义由 Document 带来(meta.extras.json):渲染器不再自己读模型定义文件。
62
+ // 没有定义时降级为空动作表(留一条 warn),**不发请求** —— 取资源是 Assets 层的职责。
63
+ const inline = model && model.meta && model.meta.extras && model.meta.extras.json;
64
+ if (!inline) {
65
+ ctx.note('模型定义未随 Document 提供(动作列表为空)', 'warn');
66
+ return { motions: {}, expressions: [], json: null };
67
+ }
68
+ return { ...definitionParts(inline), json: inline };
69
+ }
70
+
71
+ /**
72
+ * 按 meta.motions 重建标签表(idle 排最前;同名追加 #n)。
73
+ * @returns {string[]} 标签列表
74
+ */
75
+ function buildMotionMap(st, meta) {
76
+ st.motionMap = new Map();
77
+ const motions = (meta && meta.motions) || {};
78
+ const groups = Object.keys(motions);
79
+ groups.sort((a, b) => (a === 'idle' ? -1 : b === 'idle' ? 1 : 0));
80
+ for (const group of groups) {
81
+ (motions[group] || []).forEach((mo, index) => {
82
+ // 同名动作(不同 group 或同 group 多个 motion)用 _2/_3 区分,**不丢条目**
83
+ const base = motionLabel(group, motionNameOf(mo));
84
+ let label = base;
85
+ let n = 2;
86
+ while (st.motionMap.has(label)) label = `${base}_${n++}`;
87
+ st.motionMap.set(label, { group: String(group), index });
88
+ });
89
+ }
90
+ return [...st.motionMap.keys()];
91
+ }
92
+
93
+ /** 动作标签全集 */
94
+ function motionLabels(st) {
95
+ return [...st.motionMap.keys()];
96
+ }
97
+
98
+ // ---------------------------------------------------------------- 舞台准备
99
+
100
+ /** 清掉上一个模型(同一覆盖层只留一个) */
101
+ function detachPrevious(st, app) {
102
+ if (!st.model) return;
103
+ try { app.stage.removeChild(st.model); } catch { /* 忽略 */ }
104
+ try { st.model.destroy({ children: true }); } catch { /* 忽略 */ }
105
+ st.model = null;
106
+ }
107
+
108
+ /**
109
+ * 准备阶段:运行时库 → 覆盖层可见 → PIXI app(顺序决定 BUG-3 是否复现)。
110
+ * @returns {Promise<object>} PIXI app
111
+ */
112
+ async function prepareStage(ctx) {
113
+ await ctx.api.pixiApp.ensureLibs();
114
+ ctx.api.overlay.showOverlay();
115
+ const app = await ctx.api.pixiApp.ensureApp();
116
+ detachPrevious(ctx.st, app);
117
+ return app;
118
+ }
119
+
120
+ // ---------------------------------------------------------------- 实例创建(VFS 优先)
121
+
122
+ /** 构造 VFS settings(缺定义 / 缺 vfs / **散目录模型** 时返回 null) */
123
+ function buildVfsSettingsOrNull(ctx, def, entryUrl) {
124
+ if (!def || !def.json) return null;
125
+ // 散目录模型(entryUrl 是 http 路径、不是 vfs://)**不装** VFS resolver:
126
+ // VFS 只登记容器产物(注册点只有 infra/plugin-host.js,prefix 契约 vfs://<provider>/<root>/…),
127
+ // 散目录模型的每个内部文件去查它**必然 miss** —— 白跑一次查找 + 每个文件刷一条 warn,
128
+ // 会把"容器真缺文件"这类真问题淹掉(实测:散目录模型 命中 0 / 回落 3;容器模型 命中 4 / 回落 0)。
129
+ // 返回 null 在这里是**正常状态**、不是异常:调用方看到 null 直接走原生 URL 路径
130
+ // (对散目录模型来说网络本来就是唯一正确来源),而且不会打 warn ——
131
+ // warnSettingsUnavailable 只在 via.reason 存在时才说话,null 没有 reason。
132
+ // 真正该 warn 的情况仍会 warn:entryUrl 是 vfs:// 但 vfs 缺失/不可用(由 createVfsSettings 的 reason 报)。
133
+ //
134
+ // ⚠️ 这条路的**已知代价**(接受,别当 bug 去"优化"):
135
+ // settings 分支是用**已经 fetch 好的** model3.json 构造的(不再请求入口文件);
136
+ // 走原生 URL 分支时 `Live2DModel.from(url)` 会让库**自己再取一次 model3.json**。
137
+ // 实测:散目录模型内部资源 18 条(去重后与改前完全一致)+ 多一次入口请求(2308 字节)。
138
+ // 因为 /files/* 带 `Cache-Control: no-store`,这一次是真请求(不是缓存命中),
139
+ // 但同源/局域网、2.3KB、一次装载一次 —— 换来的是少一层"必然 miss 的查找"和一条噪音 warn。
140
+ // 若将来要省掉这次请求,正确做法是**改回 settings 分支并把 warn 降级**,而不是在这里加缓存分支。
141
+ if (!/^vfs:\/\//i.test(String(entryUrl || ''))) return null;
142
+ return createVfsSettings({ json: def.json, entryUrl, vfs: ctx.vfs, log: ctx.note });
143
+ }
144
+
145
+ /** settings 不可用时的 warn(reason 说明为什么没启用) */
146
+ function warnSettingsUnavailable(note, via) {
147
+ if (via && via.reason) note(`VFS settings 未启用(${via.reason}),走 URL 路径`, 'warn');
148
+ }
149
+
150
+ /**
151
+ * 把「VFS 命中/回落」发布到 `<html data-vfs-stats>`,供机器读取(api-verifiable-frontend 的落地)。
152
+ *
153
+ * 为什么需要:stats 原本只赋给 `st.vfsStats`,全仓没有任何读取方 ——
154
+ * 唯一的出口是那条 info 日志,排查时只能翻日志。挂 dataset 后
155
+ * `document.documentElement.dataset.vfsStats` 就是 `{"hit":4,"miss":0}`,脚本/面板都能直接读。
156
+ * 传 null 表示清掉(切模型 / 回落 URL 路径 / 释放)。
157
+ * 命名与既有 dataset 一致(camelCase:transportMotion / memoryGuard / screenshot)。
158
+ */
159
+ function publishVfsStats(stats) {
160
+ try {
161
+ const el = typeof document !== 'undefined' ? document.documentElement : null;
162
+ if (!el) return;
163
+ if (!stats) delete el.dataset.vfsStats;
164
+ else el.dataset.vfsStats = JSON.stringify({ hit: stats.hits.length, miss: stats.missing.length });
165
+ } catch { /* dataset 只是可观测性,失败绝不影响装载 */ }
166
+ }
167
+
168
+ /**
169
+ * VFS 分支:建实例成功就返回实例,任何异常都返回 null(交给调用方回落)。
170
+ * **降级语义(防白屏兜底),改这里时不许丢**:
171
+ * ① settings 构造器 / resolveURL 不存在 → warn + null
172
+ * ② 库升级导致 resolveURL 一次都没被调用 → 当失败处理 + warn + null
173
+ * ③ 装载抛错 → warn + null
174
+ * @returns {Promise<object|null>}
175
+ */
176
+ async function tryVfsInstance(ctx, def, entryUrl) {
177
+ const via = buildVfsSettingsOrNull(ctx, def, entryUrl);
178
+ if (!via || !via.settings) {
179
+ warnSettingsUnavailable(ctx.note, via);
180
+ return null;
181
+ }
182
+ try {
183
+ const inst = await createModelInstance(via.settings, { autoInteract: false });
184
+ if (!via.stats.calls) throw new Error('resolveURL 未被调用');
185
+ ctx.st.vfsStats = via.stats;
186
+ publishVfsStats(via.stats);
187
+ ctx.note(`Live2D 内部资源经 VFS:命中 ${via.stats.hits.length} 项,回落 ${via.stats.missing.length} 项`);
188
+ return inst;
189
+ } catch (e) {
190
+ ctx.note(`VFS 装载失败,回落 URL 路径:${(e && e.message) || e}`, 'warn');
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * 建实例:内部文件优先走 VFS,不可用时回落 URL 路径。
197
+ *
198
+ * 回落路径**也必须**把 vfs:// 换成可 fetch 的 blob:库不认识自定义协议,
199
+ * 直接喂 vfs:// 会 `Failed to construct 'URL': Invalid URL`。
200
+ * 这条是"库版本不支持时的唯一退路",退路本身不能是坏的。
201
+ * 转换函数注入给 adapter,复用同一个 VFS 句柄(不在这里另建解析器)。
202
+ */
203
+ async function createInstance(ctx, def, entryUrl, url) {
204
+ const inst = await tryVfsInstance(ctx, def, entryUrl);
205
+ if (inst) return inst;
206
+ ctx.st.vfsStats = null;
207
+ publishVfsStats(null); // 回落 URL 路径:清掉上一次的读数,别让 dataset 说谎
208
+ const vfs = ctx.vfs;
209
+ return createModelInstance(url, {
210
+ autoInteract: false,
211
+ resolveVfs: (raw) => {
212
+ const text = String(raw == null ? '' : raw);
213
+ try {
214
+ if (vfs && typeof vfs.resolve === 'function') {
215
+ const hit = vfs.resolve(text);
216
+ if (hit) return hit;
217
+ }
218
+ } catch { /* 解析失败按"不可用"处理,由 adapter 抛明确错误 */ }
219
+ // 不是 vfs:// 的地址(散目录模型的 /files/… 或 http 绝对地址)**本来就是可 fetch 的**,原样交回。
220
+ // 必要性:adapter.createModel 对 URL 形态要求 resolveVfs 返回非空,否则抛
221
+ // 「模型地址无法解析」——散目录模型在 B1 之后正是走这条分支,不原样返回就会卡在 loading。
222
+ // 对 vfs:// 仍返回 null(保持"解析不出来就是坏地址"的原语义,交给 adapter 报错)。
223
+ return /^vfs:\/\//i.test(text) ? null : text;
224
+ },
225
+ });
226
+ }
227
+
228
+ // ---------------------------------------------------------------- 挂载与收尾
229
+
230
+ /**
231
+ * 挂上实例并写入"当前模型"状态。
232
+ * modelUrl 记成**实际使用的入口**(可能是 vfs://):动作时长与动作文件的相对路径都以它为基准,
233
+ * 沿用原始 http 目录会让这些引用跑去网络(VFS misses 虚高、动作文件不从内存出)。
234
+ * @returns {string[]} 动作标签
235
+ */
236
+ function attachInstance(ctx, instance, app, model, def, entryUrl) {
237
+ const { st } = ctx;
238
+ app.stage.addChild(instance);
239
+ st.model = instance;
240
+ st.modelId = model.id;
241
+ st.zoomRatio = 1;
242
+ st.panX = 0;
243
+ st.panY = 0;
244
+ st.meta = {
245
+ ...model,
246
+ modelUrl: entryUrl || model.modelUrl,
247
+ motions: def.motions,
248
+ expressions: def.expressions,
249
+ // 动作时长来自 Document(Assets 层预取);桥接层按"标签 → 动作文件"查这张表
250
+ motionDurations: (model && model.meta && model.meta.motionDurations) || {},
251
+ };
252
+ return buildMotionMap(st, st.meta);
253
+ }
254
+
255
+ /**
256
+ * 清屏自检:模型以外区域必须透明,否则不透明底色会把 CSS 背景盖住
257
+ * (真机表现就是"内容被框在矩形里",以前只能靠人眼发现)。
258
+ */
259
+ async function checkClearTransparency(ctx) {
260
+ const size = ctx.surfaceSize ? ctx.surfaceSize() : null;
261
+ const pts = [[8, 8], [ctx.width8(), 8], [8, ctx.height8()], [Math.floor((size ? size.width : 1920) / 2), 8]];
262
+ try {
263
+ const { total, opaque } = probeSummary(await ctx.api.probe.readCanvasPixels(pts));
264
+ if (total && opaque === total) ctx.note(`画布清屏不透明(${opaque}/${total} 点 alpha>0),CSS 背景会被遮住`, 'warn');
265
+ else ctx.note(`画布清屏自检通过(${total - opaque}/${total} 点透明)`);
266
+ } catch { /* 自检失败不影响渲染 */ }
267
+ }
268
+
269
+ /** 首帧后采一次 diff 基线(只采一次,避免污染时序;模型已换则放弃) */
270
+ function collectBaseline(ctx, instance) {
271
+ requestAnimationFrame(() => requestAnimationFrame(() => {
272
+ if (ctx.st.model !== instance) return;
273
+ try { ctx.api.probe.collectCmp('default'); } catch { /* 采集失败不影响渲染 */ }
274
+ }));
275
+ }
276
+
277
+ /**
278
+ * 装载并显示一个 Live2D 模型(只做编排)。
279
+ * @param {object} ctx 运行时上下文
280
+ * @param {object} model 条目(含 id / name / entry.url 或 modelUrl)
281
+ * @returns {Promise<{labels:string[], expressions:string[], instance:object}>}
282
+ */
283
+ async function loadModel(ctx, model) {
284
+ const app = await prepareStage(ctx);
285
+ // 入口地址优先取 Document.assets.entry.url(已由 Assets 层经 VirtualFS 解析)
286
+ // 优先 ref(解析前的原始地址,可能是 vfs://):settings.url 与内部相对引用都以它为基址,
287
+ // 换成解析后的 blob 会让 url.resolve 拼出 blob:http://…/motions/x 这种无效地址。
288
+ const entryAsset = (model && model.assets && model.assets.entry) || {};
289
+ const entryUrl = entryAsset.ref || entryAsset.url || ctx.entryUrlOf(model);
290
+ const url = ctx.resolveAsset(entryUrl);
291
+ if (!url) throw new Error(`Live2D 模型缺少入口地址(entry.url):${model && model.name}`);
292
+ const t0 = performance.now();
293
+ const def = await fetchDefinition(ctx, model);
294
+ const instance = await createInstance(ctx, def, entryUrl, url);
295
+ const labels = attachInstance(ctx, instance, app, model, def, entryUrl);
296
+ if (typeof app.start === 'function') app.start();
297
+ ctx.api.pixiApp.fitAppToOverlay();
298
+ ctx.api.view.applyZoom();
299
+ ctx.st.active = true;
300
+ await checkClearTransparency(ctx);
301
+ ctx.note(`Live2D 已渲染 ${model.name}(Cubism ${model.cubism || '?'},动作 ${labels.length},表情 ${def.expressions.length},`
302
+ + `耗时 ${Math.round(performance.now() - t0)}ms)`);
303
+ collectBaseline(ctx, instance);
304
+ return { labels, expressions: def.expressions.map((e) => e.name), instance };
305
+ }
306
+
307
+ /**
308
+ * 创建模型模块(只做装配)。
309
+ * @param {object} ctx 运行时上下文
310
+ * @returns {{fetchDefinition:Function, motionNameOf:Function, motionLabel:Function, buildMotionMap:Function, motionLabels:Function, loadModel:Function}}
311
+ */
312
+ export function createModel(ctx) {
313
+ return {
314
+ fetchDefinition: (model) => fetchDefinition(ctx, model),
315
+ motionNameOf,
316
+ motionLabel,
317
+ buildMotionMap: (meta) => buildMotionMap(ctx.st, meta),
318
+ motionLabels: () => motionLabels(ctx.st),
319
+ loadModel: (model) => loadModel(ctx, model),
320
+ };
321
+ }
@@ -0,0 +1,142 @@
1
+ // live2d/overlay.js —— 覆盖层容器与画布的 DOM 生命周期
2
+ //
3
+ // 覆盖层是一个 absolute + overflow:hidden 的 div,里面放 PIXI 用的 canvas。
4
+ // · 它 pointer-events:none ⇒ 指针事件下穿到舞台,交互路径与 Spine 完全一致;
5
+ // · 元素由 Surface.ensureOverlay 创建,**尺寸也由 overlay.setBox 写入**
6
+ // (容器 style + canvas attr/style 一处收口)。本模块只管显隐与结构,
7
+ // 不再写任何内联尺寸 —— 历史上这里写过 px,导致旋转后容器钉在旧尺寸、把画布裁掉。
8
+ import { OVERLAY_ID, OVERLAY_CANVAS_ID } from './stage-ctx.js';
9
+
10
+ /** 取尺寸几何:入参优先,否则向 Surface 句柄要(兼容 {geometry()} 与 {size,dpr}) */
11
+ function resolveBoxGeometry(ctx, geo) {
12
+ if (geo && geo.cssW && geo.cssH) return geo;
13
+ try { return ctx.surfaceGeometry ? ctx.surfaceGeometry() : null; } catch { return null; }
14
+ }
15
+
16
+ /** 生效像素比:优先 res,其次 dpr,都没有就当 1 */
17
+ function pickResolution(g) {
18
+ if (Number(g.res) > 0) return Number(g.res);
19
+ if (Number(g.dpr) > 0) return Number(g.dpr);
20
+ return 1;
21
+ }
22
+
23
+ /**
24
+ * 创建覆盖层模块。
25
+ * @param {object} ctx 运行时上下文
26
+ * @returns {{buildOverlay:Function, setBox:Function, showOverlay:Function, hide:Function}}
27
+ */
28
+ export function createOverlay(ctx) {
29
+ const { st, note, surfaceSize, getOverlayHost } = ctx;
30
+
31
+ /**
32
+ * 取覆盖层容器(**由 Surface.ensureOverlay 创建**,本函数只取用)。
33
+ *
34
+ * 兜底:若 Surface 还没建(独立使用本模块/异常时序),这里按同样的结构补一个,
35
+ * 但尺寸仍由 overlay.setBox 写 —— 本模块不再拥有"尺寸"这件事。
36
+ */
37
+ function buildOverlay() {
38
+ // 复用条件必须**同时**满足:容器还在 DOM 里 **且** 里面确实有覆盖层 canvas。
39
+ //
40
+ // 为什么必须查 canvas(这个条件是踩过坑才补上的):
41
+ // PIXI 的 `app.destroy(true)` 第一参数是 removeView —— 它会把 canvas 从容器里摘掉。
42
+ // 渲染面"卸载"(同面换模型会走 unmount)正是这么做的;此时容器 div 仍在 DOM、
43
+ // `st.overlay.isConnected` 仍为 true,只看 isConnected 就会**复用空容器** ⇒
44
+ // ensureApp 用 getElementById 取 canvas 得到 null ⇒ PIXI 自建 canvas 但不插入 DOM ⇒
45
+ // 渲染在游离画布上,页面一片空白,而 stats 照样报"已加载"。
46
+ // 所以:容器可复用,但**canvas 丢了必须补建**(不整块重建,省掉重建容器的副作用)。
47
+ const hasCanvas = (el) => !!(el && el.querySelector && el.querySelector(`#${OVERLAY_CANVAS_ID}`));
48
+ if (st.overlay && st.overlay.isConnected && hasCanvas(st.overlay)) return st.overlay;
49
+ const host = (typeof getOverlayHost === 'function' ? getOverlayHost() : null)
50
+ || document.getElementById('stage') || document.body;
51
+ let el = document.getElementById(OVERLAY_ID)
52
+ || (st.overlay && st.overlay.isConnected ? st.overlay : null);
53
+ if (!el) {
54
+ el = document.createElement('div');
55
+ el.id = OVERLAY_ID;
56
+ // 关键:pointer-events:none ⇒ 事件下穿到 Spine 画布(规则在 style.css 的 #live2d-overlay)
57
+ host.appendChild(el);
58
+ }
59
+ if (!hasCanvas(el)) {
60
+ const cv = document.createElement('canvas');
61
+ cv.id = OVERLAY_CANVAS_ID;
62
+ el.appendChild(cv);
63
+ }
64
+ st.overlay = el;
65
+ return el;
66
+ }
67
+
68
+ /** 写容器盒(只在真的变了才写,避免每帧触发样式重算) */
69
+ function setContainerBox(el, pxW, pxH) {
70
+ if (el.style.width !== pxW) el.style.width = pxW;
71
+ if (el.style.height !== pxH) el.style.height = pxH;
72
+ }
73
+
74
+ /** 写画布:attr = css×res(backing store),style = css px(显示尺寸,两者必须同时改) */
75
+ function setCanvasBox(cv, w, h, res, pxW, pxH) {
76
+ const attrW = Math.max(1, Math.round(w * res));
77
+ const attrH = Math.max(1, Math.round(h * res));
78
+ if (cv.width !== attrW) cv.width = attrW;
79
+ if (cv.height !== attrH) cv.height = attrH;
80
+ if (cv.style.width !== pxW) cv.style.width = pxW;
81
+ if (cv.style.height !== pxH) cv.style.height = pxH;
82
+ }
83
+
84
+ /**
85
+ * 覆盖层尺寸的**唯一写入点**(容器 style + 内部 canvas 的 attr/style)。
86
+ *
87
+ * 为什么必须单点且显式声明:覆盖层是 absolute + overflow:hidden,可见区域由容器内联 px 决定。
88
+ * 只 resize PIXI 而不改容器,画布会被旧宽度裁掉 —— 真机旋转后表现为
89
+ * "模型挤在左侧窄带、右侧露出棋盘格",而手动重载模型(重走 showOverlay)就恢复。
90
+ * Surface 只负责建元素(ensureOverlay,初值 100%),尺寸同步归渲染器自己。
91
+ *
92
+ * 尺寸来源只用 Surface 下发的 geometry,不读 DOM。
93
+ * @param {{cssW:number, cssH:number, dpr?:number, res?:number}} [geo] 缺省时向 Surface 句柄取
94
+ * @returns {boolean} 是否写入成功
95
+ */
96
+ function setBox(geo) {
97
+ const g = resolveBoxGeometry(ctx, geo);
98
+ if (!g) return false;
99
+ const el = document.getElementById(OVERLAY_ID);
100
+ if (!el) return false;
101
+ const w = Math.max(1, Math.round(g.cssW));
102
+ const h = Math.max(1, Math.round(g.cssH));
103
+ const pxW = `${w}px`;
104
+ const pxH = `${h}px`;
105
+ setContainerBox(el, pxW, pxH);
106
+ const cv = document.getElementById(OVERLAY_CANVAS_ID);
107
+ if (cv) setCanvasBox(cv, w, h, pickResolution(g), pxW, pxH);
108
+ return true;
109
+ }
110
+
111
+ /**
112
+ * 显示覆盖层(不改变 active;active 由 loadForEngine 决定)。
113
+ *
114
+ * 本函数**只管显隐**,不写任何尺寸:容器与画布的尺寸统一由 setBox() 写。
115
+ * 历史教训:这里曾按装载时的尺寸写内联 px,旋转后容器钉在旧 px、把画布裁掉,
116
+ * 表现为"模型挤在左侧窄带",重载模型(重走本函数)才恢复。
117
+ */
118
+ function showOverlay() {
119
+ const el = buildOverlay();
120
+ el.style.display = 'block';
121
+ // 已有 PIXI 实例(二次显示):立即按句柄尺寸重校准渲染器与视图
122
+ if (st.app) ctx.api.pixiApp.fitAppToOverlay();
123
+ ctx.api.view.syncRect();
124
+ }
125
+
126
+ function hide() {
127
+ const el = document.getElementById(OVERLAY_ID);
128
+ if (el) el.style.display = 'none';
129
+ if (st.model) {
130
+ try { st.app && st.app.stage.removeChild(st.model); } catch { /* 忽略 */ }
131
+ try { st.model.destroy({ children: true }); } catch { /* 忽略 */ }
132
+ st.model = null;
133
+ }
134
+ try { if (st.app && typeof st.app.stop === 'function') st.app.stop(); } catch { /* 忽略 */ }
135
+ st.active = false;
136
+ st.modelId = null;
137
+ note('Live2D 已卸载(覆盖层隐藏、模型销毁、渲染循环停止)');
138
+ return true;
139
+ }
140
+
141
+ return { buildOverlay, setBox, showOverlay, hide };
142
+ }
@@ -0,0 +1,111 @@
1
+ // live2d/pixi-app.js —— 运行时库懒加载与 PIXI 实例
2
+ //
3
+ // 库顺序不可颠倒(cubismcore 必须最先:pixi-live2d-display 初始化时会检查它)。
4
+ // 渲染器尺寸只认 Surface 下发的几何(fitAppToOverlay 的入参或句柄),本模块不读 DOM 尺寸。
5
+ // 注意:覆盖层**容器与画布的尺寸**不在这里写 —— 那是 overlay.setBox 的唯一职责。
6
+ import { OVERLAY_CANVAS_ID } from './stage-ctx.js';
7
+ // 库加载与 Application 构造统一走 adapter(版本差异只在那里判断)
8
+ import { loadRuntime, createApplication } from './runtime-adapter.js';
9
+
10
+ /**
11
+ * 创建 PIXI 应用模块。
12
+ * @param {object} ctx 运行时上下文
13
+ * @returns {{ensureLibs:Function, fitAppToOverlay:Function, ensureApp:Function}}
14
+ */
15
+ export function createPixiApp(ctx) {
16
+ const { st, note } = ctx;
17
+
18
+ /**
19
+ * 确保运行时库已就绪。
20
+ * 加载序列与"失败不缓存"都由 runtime-adapter.loadRuntime 负责(单例 Promise),
21
+ * 这里只做本 ctx 的一次性提示,不再自己拼 <script>。
22
+ */
23
+ function ensureLibs() {
24
+ if (!st.libsReady) {
25
+ note('首次使用 Live2D,按需加载运行时库…');
26
+ st.libsReady = loadRuntime(note).catch((err) => {
27
+ // adapter 内部也会清它的单例缓存;这里清本 ctx 的,保证下一次激活真的重试
28
+ st.libsReady = null;
29
+ throw err;
30
+ });
31
+ }
32
+ return st.libsReady;
33
+ }
34
+
35
+ /** 取几何:入参优先,否则问 Surface(兼容 {geometry()} 与 {size,dpr} 两种形态) */
36
+ function resolveGeo(geo) {
37
+ if (geo && geo.cssW && geo.cssH) return geo;
38
+ try { return ctx.surfaceGeometry ? ctx.surfaceGeometry() : null; } catch { return null; }
39
+ }
40
+
41
+ /** 取 Surface 期望的分辨率(?res=N 覆盖时与 dpr 不同);拿不到返回 null */
42
+ function forcedResOf() {
43
+ try { return ctx.handleRef && ctx.handleRef.geometry ? ctx.handleRef.geometry().res : null; } catch { return null; }
44
+ }
45
+
46
+ /** 让 PIXI 分辨率跟 Surface 的期望值一致(画布 attr/style 由 overlay.setBox 统一写) */
47
+ function syncRendererResolution(app, forced) {
48
+ if (!forced) return;
49
+ if (app.renderer.resolution !== forced) app.renderer.resolution = forced;
50
+ }
51
+
52
+ /**
53
+ * 按 Surface 下发的几何校准 PIXI 渲染器尺寸。
54
+ * 尺寸唯一来源 = Surface.geometry();本函数不读 DOM,也不写覆盖层的 CSS/attr
55
+ * (覆盖层容器与画布的尺寸由 overlay.setBox 单点写入)。
56
+ * @param {{cssW:number, cssH:number, dpr:number, res:number}} [geo]
57
+ * @returns {boolean} 是否成功校准
58
+ */
59
+ function fitAppToOverlay(geo) {
60
+ const app = st.app;
61
+ if (!app || !app.renderer) return false;
62
+ const g = resolveGeo(geo);
63
+ if (!g) return false;
64
+ const w = Math.max(1, Math.round(g.cssW));
65
+ const h = Math.max(1, Math.round(g.cssH));
66
+ const res = app.renderer.resolution || 1;
67
+ syncRendererResolution(app, forcedResOf());
68
+ const curW = Math.round((app.renderer.width || 0) / res);
69
+ const curH = Math.round((app.renderer.height || 0) / res);
70
+ if (curW !== w || curH !== h) app.renderer.resize(w, h);
71
+ return true;
72
+ }
73
+
74
+ async function ensureApp() {
75
+ if (st.app) return st.app;
76
+ const el = ctx.api.overlay.buildOverlay();
77
+ // 兜底:buildOverlay 已保证容器里有 canvas;这里再取一次仍可能为空(例如被外部脚本摘掉)。
78
+ // 拿不到就补建并塞回容器 —— 绝不能让 PIXI 拿到 view:null(它会自建一张不在 DOM 里的游离画布,
79
+ // 渲染正常但页面空白,且像素采样照样能读到有效像素,极难发现)。
80
+ let canvas = document.getElementById(OVERLAY_CANVAS_ID);
81
+ if (!canvas && el) {
82
+ canvas = document.createElement('canvas');
83
+ canvas.id = OVERLAY_CANVAS_ID;
84
+ canvas.classList.add('live2d-pixi-canvas');
85
+ el.appendChild(canvas);
86
+ }
87
+ const opts = {
88
+ view: canvas,
89
+ backgroundAlpha: 0,
90
+ antialias: true,
91
+ autoDensity: true,
92
+ resolution: (ctx.handleRef && ctx.handleRef.geometry) ? (ctx.handleRef.geometry().res || 1) : 1,
93
+ };
94
+ // 库加载与 Application 构造都走 runtime-adapter:版本差异(8.x 异步 init / 6.x 构造给参)
95
+ // 只在 adapter 里判断,本文件不再出现 PIXI.Application 的具体构造形状
96
+ const app = await createApplication(opts);
97
+ st.app = app;
98
+ // 强制透明清屏:实测 PIXI 6.5.3 下 opts.backgroundAlpha 未落到 renderer,
99
+ // canvas 每帧被不透明黑填满(readPixels 得到 [0,0,0,255]),
100
+ // 于是 Live2D 画布整块盖住 #stage 的 CSS 背景,只剩模型区域可见 = 看起来"内容被框在矩形里"。
101
+ try {
102
+ app.renderer.backgroundAlpha = 0;
103
+ app.renderer.backgroundColor = 0x000000;
104
+ app.renderer.clearBeforeRender = true;
105
+ } catch { /* 忽略 */ }
106
+ fitAppToOverlay();
107
+ return app;
108
+ }
109
+
110
+ return { ensureLibs, fitAppToOverlay, ensureApp };
111
+ }
@@ -0,0 +1,92 @@
1
+ // live2d/playback-probe.js —— 播放/当前动作探针(引擎侧判据)
2
+ //
3
+ // 为什么单独成模块:
4
+ // ① 验收必须读**引擎真实状态**(motionManager / coreModel 的队列实例、时间轴、参数),
5
+ // 而不是我们自己的 currentLabel —— 否则就是"自己证明自己",测不出真实播放;
6
+ // ② bridge 已经很长,探针属于只读能力,搬出来不改变播放语义。
7
+ //
8
+ // 约定:入参 st 是 Live2D 阶段状态(model / app / motionMap / currentLabel)。
9
+
10
+ export function createViewProbe(st) {
11
+ /**
12
+ * 引擎侧播放探针:**直接读 Cubism 的 motionManager / coreModel**,
13
+ * 不用我们自己的 currentLabel 自证(那只能说明"我们以为在播什么")。
14
+ *
15
+ * 返回字段与验收口径一一对应:
16
+ * · playing 引擎是否真在播
17
+ * · queueInstances 队列里 Motion 实例的稳定编号(同一对象恒等 → 可直接判断"是否换成了新动作")
18
+ * · timeSec 当前动作时间轴(切换后应从 0 重新推进)
19
+ * · priority 当前优先级
20
+ * · paramAngleX 参数快照(最强证据:不同动作的取值轨迹不同)
21
+ * @returns {object}
22
+ */
23
+ function playbackProbe() {
24
+ const found = motionHostModel();
25
+ const mm = found && found.internalModel && found.internalModel.motionManager;
26
+ const core = found && found.internalModel && found.internalModel.coreModel;
27
+ const entries = mm && mm.queueManager && mm.queueManager._motions;
28
+ const list = entries ? Array.from(entries) : null;
29
+ const entry = list && list[0] ? list[0] : null;
30
+ let paramAngleX = null;
31
+ try { paramAngleX = core ? Number(core.getParameterValueById('ParamAngleX')) : null; } catch { /* 忽略 */ }
32
+ // 时间轴:Cubism 4 用「队列条目的 _stateTimeSeconds − 起点」,切换后会从 0 重新推进;
33
+ // `queueManager._userTimeSeconds` 在本版 pixi 里恒为 0(读它永远看不到推进,这也是上一轮
34
+ // 误判"没播"的原因之一),所以不再用它当判据。
35
+ const started = entry && Number.isFinite(entry._startTimeSeconds) ? entry._startTimeSeconds : null;
36
+ const state = entry && Number.isFinite(entry._stateTimeSeconds) ? entry._stateTimeSeconds : null;
37
+ return {
38
+ playing: mm ? !!mm.playing : null,
39
+ queueLen: list ? list.length : null,
40
+ queueInstances: list ? list.map(instanceTag) : null,
41
+ timeSec: started !== null && state !== null ? Number((state - started).toFixed(3)) : null,
42
+ durationSec: entry && entry._motion && Number.isFinite(entry._motion._loopDurationSeconds) ? entry._motion._loopDurationSeconds : null,
43
+ priority: mm && mm.state ? mm.state.currentPriority : null,
44
+ paramAngleX,
45
+ currentLabel: st.currentLabel || null,
46
+ };
47
+ }
48
+
49
+ /** 队列条目 → 稳定编号(WeakMap 记忆同一对象;不改动引擎对象本身) */
50
+ const instanceIds = new WeakMap();
51
+ let instanceSeq = 0;
52
+ function instanceTag(entry) {
53
+ if (!entry || typeof entry !== 'object') return null;
54
+ if (!instanceIds.has(entry)) instanceIds.set(entry, `obj#${++instanceSeq}`);
55
+ return instanceIds.get(entry);
56
+ }
57
+
58
+ /** 找到带 internalModel 的模型节点(覆盖层里模型可能在 stage 的下一层) */
59
+ function motionHostModel() {
60
+ if (st.model && st.model.internalModel) return st.model;
61
+ const root = st.app && st.app.stage;
62
+ let hit = null;
63
+ const walk = (node, depth) => {
64
+ if (!node || hit || depth > 4) return;
65
+ if (node.internalModel && node.internalModel.motionManager) { hit = node; return; }
66
+ const kids = node.children || [];
67
+ for (const kid of kids) walk(kid, depth + 1);
68
+ };
69
+ walk(root, 0);
70
+ return hit;
71
+ }
72
+
73
+ /**
74
+ * 当前动作(唯一真相 = 我们自己的 currentLabel)。
75
+ * 为什么不用 motionManager.currentGroup/currentIndex:本版 pixi-live2d-display 没有这两个字段,
76
+ * 读到的是 mm.state 里的陈旧快照(实测连点三个动作后仍停在第一个),会让人误判"没切过去"。
77
+ * 引擎侧字段仅作兜底(老版本才有)。
78
+ * @returns {{label:string|null, group:string|null, index:number|null}}
79
+ */
80
+ function currentMotionOf() {
81
+ const label = st.currentLabel || null;
82
+ const hit = label && st.motionMap ? st.motionMap.get(label) : null;
83
+ if (hit) return { label, group: hit.group, index: hit.index };
84
+ try {
85
+ const mm = st.model && st.model.internalModel && st.model.internalModel.motionManager;
86
+ if (mm && mm.currentGroup !== undefined) return { label, group: mm.currentGroup, index: mm.currentIndex ?? null };
87
+ } catch { /* 忽略 */ }
88
+ return { label, group: null, index: null };
89
+ }
90
+
91
+ return { currentMotion: currentMotionOf, playbackProbe };
92
+ }