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,329 @@
1
+ // live2d/probe-geometry.js —— 只读诊断:模型运行时几何(inspectGeometry)
2
+ //
3
+ // 这一段是"取证型"代码:displayObject 尺寸 / 包围盒 / Cubism 画布尺寸 / clipping mask /
4
+ // drawable 与 part 的可用 API 全部列出来,用于判断 fit 口径与"内容被框在矩形里"的成因。
5
+ //
6
+ // 为什么拆成这么多小函数:早期它是一个 128 行、圈复杂度 95 的大函数,读的人只能顺序扫下来
7
+ // 才知道每段在取证什么。现在按「取证哪一类事实」分组,每段只回答一个问题:
8
+ // readBounds 模型包围盒
9
+ // readCoreCanvas Cubism 画布尺寸(coreModel.getCanvasWidth/Height)
10
+ // readInternalSize internalModel 的尺寸与原始尺寸
11
+ // readFitRatios 两种 fit 口径的比值(判断该用哪个基准)
12
+ // readClipProbe clipping mask 相关字段("内容被框住"的第一嫌疑)
13
+ // readDrawableProbe drawable / part 的可用 API 与顶点范围
14
+ // inspectGeometry 只做编排与组装,保证**返回结构逐键不变**(它是 handle 上的对外方法)。
15
+ import { CAMERA } from '../render/camera/constants.js';
16
+
17
+ /** 取模型包围盒(getBounds 可能抛,抛了就当没有) */
18
+ function readBounds(model) {
19
+ try {
20
+ const bb = model.getBounds();
21
+ if (!bb) return null;
22
+ return { x: +bb.x.toFixed(2), y: +bb.y.toFixed(2), w: +bb.width.toFixed(2), h: +bb.height.toFixed(2) };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+
28
+ /** 取 Cubism 画布尺寸与 coreModel 引用 */
29
+ function readCoreCanvas(internal) {
30
+ const core = internal && internal.coreModel;
31
+ const hasW = core && typeof core.getCanvasWidth === 'function';
32
+ const hasH = core && typeof core.getCanvasHeight === 'function';
33
+ return {
34
+ core,
35
+ coreW: hasW ? core.getCanvasWidth() : null,
36
+ coreH: hasH ? core.getCanvasHeight() : null,
37
+ };
38
+ }
39
+
40
+ /** 取 internalModel 的尺寸(含未缩放的原始尺寸) */
41
+ function readInternalSize(internal) {
42
+ if (!internal) return null;
43
+ return { w: internal.width, h: internal.height, originalW: internal.originalWidth, originalH: internal.originalHeight };
44
+ }
45
+
46
+ /**
47
+ * 两种 fit 口径的比值。
48
+ * 当前实现用 getBounds 口径并乘 0.98(历史值,仅做对照,不重置模型);
49
+ * 期望口径是 Cubism 画布 + 常量边距。ratio 用来判断两者差多少。
50
+ */
51
+ function readFitRatios(b, coreW, coreH, vpW, vpH) {
52
+ const curFit = b && vpW && vpH ? Math.min(vpW / b.w, vpH / b.h) * 0.98 : null;
53
+ const coreFit = coreW && coreH && vpW && vpH ? Math.min(vpW / coreW, vpH / coreH) * (1 - CAMERA.FIT_MARGIN) : null;
54
+ return { curFit, coreFit, ratio: curFit && coreFit ? +(curFit / coreFit).toFixed(3) : null };
55
+ }
56
+
57
+ /** 在候选键里找一个"数组状"字段,返回 { key, len }(不同 Cubism 版本字段名不同) */
58
+ function pickArrayLike(obj, keys) {
59
+ for (const k of keys) {
60
+ try {
61
+ const v = obj && obj[k];
62
+ if (v && typeof v.length === 'number') return { key: k, len: v.length };
63
+ } catch { /* 不可读字段 */ }
64
+ }
65
+ return null;
66
+ }
67
+
68
+ /** clipping 管理器上的两个"上下文列表"(mask / draw) */
69
+ function readClipLists(cm) {
70
+ if (!cm) return { maskList: null, drawList: null };
71
+ return {
72
+ maskList: pickArrayLike(cm, ['_clippingContextListForMask', 'clippingContextListForMask', '_clippingContextList', 'clippingContextList']),
73
+ drawList: pickArrayLike(cm, ['_clippingContextListForDraw', 'clippingContextListForDraw', '_clippingContextListForDraws']),
74
+ };
75
+ }
76
+
77
+ /** 对象上与 clip|mask 有关的键名(判断字段是否换名) */
78
+ function clipKeyNames(obj) {
79
+ if (!obj) return [];
80
+ return Object.keys(obj).filter((k) => /clip|mask/i.test(k)).slice(0, 12);
81
+ }
82
+
83
+ /** clipping mask 取证:拿不到就报 err,拿到就只回字段值 */
84
+ function readClipProbe(internal) {
85
+ try {
86
+ const r = internal && internal.renderer;
87
+ if (!r) return { err: 'no renderer' };
88
+ const cm = r._clippingManager || r.clippingManager || null;
89
+ const { maskList, drawList } = readClipLists(cm);
90
+ return {
91
+ hasClippingManager: !!cm,
92
+ maskList,
93
+ drawList,
94
+ maskBufferSize: cm && cm._clippingMaskBufferSize != null ? cm._clippingMaskBufferSize : null,
95
+ cmKeys: clipKeyNames(cm),
96
+ rendererClipKeys: clipKeyNames(r),
97
+ // 历史字段:曾想读网格数,目前恒为 null(保留键名不破坏对外结构)
98
+ meshCount: null,
99
+ };
100
+ } catch (e) {
101
+ return { err: String(e && e.message).slice(0, 80) };
102
+ }
103
+ }
104
+
105
+ /** 取 part id(不同版本的返回值类型不同:字符串 / CubismString / 数字) */
106
+ function readPartId(raw, i) {
107
+ try {
108
+ if (typeof raw.getPartId !== 'function') return null;
109
+ const id = raw.getPartId(i);
110
+ if (!id) return null;
111
+ if (typeof id === 'string') return id;
112
+ if (typeof id.getString === 'function') return id.getString();
113
+ if (id.s !== undefined) return String(id.s);
114
+ return String(id);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /** 顶点范围(只算包围盒,用于判断 drawable 是否落在画布某一块) */
121
+ function readVertexBbox(raw, i, vertexCount) {
122
+ if (typeof raw.getDrawableVertexPositions !== 'function' || !(vertexCount > 0)) return null;
123
+ const v = raw.getDrawableVertexPositions(i);
124
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
125
+ for (let k = 0; k + 1 < v.length; k += 2) {
126
+ const x = v[k], y = v[k + 1];
127
+ if (x < minX) minX = x;
128
+ if (x > maxX) maxX = x;
129
+ if (y < minY) minY = y;
130
+ if (y > maxY) maxY = y;
131
+ }
132
+ return { x: +minX.toFixed(3), y: +minY.toFixed(3), w: +(maxX - minX).toFixed(3), h: +(maxY - minY).toFixed(3) };
133
+ }
134
+
135
+ /** 读 drawable 的父 part 索引(API 不存在或抛错都当没有) */
136
+ function readParentPart(raw, i, has) {
137
+ try {
138
+ if (!has('getDrawableParentPartIndex')) return null;
139
+ return raw.getDrawableParentPartIndex(i);
140
+ } catch {
141
+ return null;
142
+ }
143
+ }
144
+
145
+ /** 单个 drawable 的取证条目 */
146
+ function readDrawableItem(raw, i, has) {
147
+ const item = { i, parentPart: null, partId: null, vertexCount: null, bbox: null };
148
+ item.parentPart = readParentPart(raw, i, has);
149
+ if (item.parentPart != null && item.parentPart >= 0) item.partId = readPartId(raw, item.parentPart);
150
+ try { if (has('getDrawableVertexCount')) item.vertexCount = raw.getDrawableVertexCount(i); } catch { /* 忽略 */ }
151
+ try {
152
+ const box = readVertexBbox(raw, i, item.vertexCount);
153
+ if (box) item.bbox = box;
154
+ } catch (e) {
155
+ item.bboxErr = String(e && e.message).slice(0, 40);
156
+ }
157
+ return item;
158
+ }
159
+
160
+ /** coreModel 上哪些 API 可用(决定能取证到什么粒度) */
161
+ function readApiAvailability(has) {
162
+ return {
163
+ getDrawableCount: has('getDrawableCount'),
164
+ getDrawableVertexPositions: has('getDrawableVertexPositions'),
165
+ getDrawableVertexCount: has('getDrawableVertexCount'),
166
+ getDrawableParentPartIndex: has('getDrawableParentPartIndex'),
167
+ getPartCount: has('getPartCount'),
168
+ getPartId: has('getPartId'),
169
+ };
170
+ }
171
+
172
+ /** 枚举对象自身可枚举的方法(对象上可能有会抛的 getter,逐个兜住) */
173
+ function listOwnMethods(obj) {
174
+ const names = [];
175
+ try {
176
+ for (const k in obj) {
177
+ try { if (typeof obj[k] === 'function') names.push(k); } catch { /* 不可读字段 */ }
178
+ }
179
+ } catch { /* 整体不可枚举 */ }
180
+ return names;
181
+ }
182
+
183
+ /** 收集某一层原型上的方法名(该层不可读就返回空) */
184
+ function collectProtoNames(obj, proto) {
185
+ const names = [];
186
+ try {
187
+ Object.getOwnPropertyNames(proto).forEach((n) => {
188
+ try { if (typeof obj[n] === 'function') names.push(n); } catch { /* 不可读字段 */ }
189
+ });
190
+ } catch { /* 该层原型不可读 */ }
191
+ return names;
192
+ }
193
+
194
+ /** 沿原型链追加方法名(层数有上限,避免异常对象拖死) */
195
+ function listProtoMethods(obj, names) {
196
+ let proto = null;
197
+ try { proto = Object.getPrototypeOf(obj); } catch { proto = null; }
198
+ let guard = 0;
199
+ while (proto && proto !== Object.prototype && guard < 4) {
200
+ names.push(...collectProtoNames(obj, proto));
201
+ try { proto = Object.getPrototypeOf(proto); } catch { proto = null; }
202
+ guard += 1;
203
+ }
204
+ return names;
205
+ }
206
+
207
+ /** 收集对象自身 + 原型链上的方法名 */
208
+ function collectMethodNames(obj) {
209
+ return listProtoMethods(obj, listOwnMethods(obj));
210
+ }
211
+
212
+ /** 枚举一个对象(含原型链)上"看起来与几何相关"的方法名,用于发现未公开的 API */
213
+ function probeMethodNames(obj, label, depth) {
214
+ if (!obj || typeof obj !== 'object' || depth > 3) return null;
215
+ const names = collectMethodNames(obj);
216
+ const hit = [...new Set(names)].filter((n) => /draw|part|texture|vertex|canvas|bounds|geometry/i.test(n));
217
+ return {
218
+ label,
219
+ ctor: obj.constructor ? obj.constructor.name : typeof obj,
220
+ methodCount: names.length,
221
+ matched: hit.slice(0, 18),
222
+ };
223
+ }
224
+
225
+ /** 列出几个可能的 API 宿主对象,逐个枚举方法(只读,用于定位未公开接口) */
226
+ function readApiPaths(core, internal) {
227
+ const out = [];
228
+ const targets = [
229
+ [core, 'coreModel', 0],
230
+ [core && core._model, 'coreModel._model', 1],
231
+ [core && core._model && core._model._model, 'coreModel._model._model', 2],
232
+ [core && core._model && core._model.drawables, 'coreModel._model.drawables', 2],
233
+ [internal && internal.renderer, 'internalModel.renderer', 1],
234
+ ];
235
+ for (const [obj, label, depth] of targets) {
236
+ const hit = probeMethodNames(obj, label, depth);
237
+ if (hit) out.push(hit);
238
+ }
239
+ return out;
240
+ }
241
+
242
+ /** 取 coreModel 上真正带 API 的 raw 对象(不同版本挂载层级不同) */
243
+ function readRawCore(internal) {
244
+ const core = internal && internal.coreModel;
245
+ if (!core) return null;
246
+ return core._model ? core._model : core;
247
+ }
248
+
249
+ /** part id 列表(上限 12 个,够判断命名风格即可) */
250
+ function readPartIds(raw, partCount) {
251
+ const partIds = [];
252
+ const max = Math.min(partCount || 0, 12);
253
+ for (let i = 0; i < max; i += 1) partIds.push(readPartId(raw, i));
254
+ return partIds;
255
+ }
256
+
257
+ /** drawable 取证条目(上限 8 个) */
258
+ function readDrawableSamples(raw, count, has) {
259
+ const sample = [];
260
+ const max = Math.min(count || 0, 8);
261
+ for (let i = 0; i < max; i += 1) sample.push(readDrawableItem(raw, i, has));
262
+ return sample;
263
+ }
264
+
265
+ /** drawable / part 取证(主体包围盒问题的第二嫌疑) */
266
+ function readDrawableProbe(internal) {
267
+ try {
268
+ const raw = readRawCore(internal);
269
+ if (!raw) return { err: 'no coreModel' };
270
+ const has = (k) => typeof raw[k] === 'function';
271
+ const count = has('getDrawableCount') ? raw.getDrawableCount() : null;
272
+ const partCount = has('getPartCount') ? raw.getPartCount() : null;
273
+ return {
274
+ coreType: raw.constructor ? raw.constructor.name : typeof raw,
275
+ drawableCount: count,
276
+ partCount,
277
+ partIds: readPartIds(raw, partCount),
278
+ api: readApiAvailability(has),
279
+ sample: readDrawableSamples(raw, count, has),
280
+ apiPaths: readApiPaths(internal && internal.coreModel, internal),
281
+ };
282
+ } catch (e) {
283
+ return { err: String(e && e.message).slice(0, 80) };
284
+ }
285
+ }
286
+
287
+ /**
288
+ * 创建几何取证模块。
289
+ * @param {object} ctx 运行时上下文
290
+ * @returns {{inspectGeometry:Function}}
291
+ */
292
+ export function createProbeGeometry(ctx) {
293
+ const { st, surfaceSize } = ctx;
294
+
295
+ /**
296
+ * 只读:模型运行时几何(**不触发任何重排**)。
297
+ *
298
+ * 返回结构是对外契约(handle.inspectGeometry),键名与嵌套形状不得变化:
299
+ * display / bounds / coreCanvas / internalSize / viewport /
300
+ * fitByBounds / fitByCore / ratio / clipProbe / drawableProbe
301
+ * @returns {object|null}
302
+ */
303
+ function inspectGeometry() {
304
+ const model = st.model;
305
+ if (!model) return null;
306
+ const b = readBounds(model);
307
+ const internal = model.internalModel || null;
308
+ const { core, coreW, coreH } = readCoreCanvas(internal);
309
+ const size = surfaceSize();
310
+ const vpW = size ? size.width : null;
311
+ const vpH = size ? size.height : null;
312
+ const { curFit, coreFit, ratio } = readFitRatios(b, coreW, coreH, vpW, vpH);
313
+ void core;
314
+ return {
315
+ display: { w: model.width, h: model.height, scale: model.scale ? +model.scale.x.toFixed(4) : null, x: model.x, y: model.y },
316
+ bounds: b,
317
+ coreCanvas: { w: coreW, h: coreH },
318
+ internalSize: readInternalSize(internal),
319
+ viewport: { w: vpW, h: vpH },
320
+ fitByBounds: curFit,
321
+ fitByCore: coreFit,
322
+ ratio,
323
+ clipProbe: readClipProbe(internal),
324
+ drawableProbe: readDrawableProbe(internal),
325
+ };
326
+ }
327
+
328
+ return { inspectGeometry };
329
+ }
@@ -0,0 +1,119 @@
1
+ // live2d/probe.js —— 只读诊断(渲染器状态 / 像素采样 / MODELCMP 采集)
2
+ //
3
+ // 全部只读:不重置视图、不改渲染、不触发重排。之所以独立成文件,
4
+ // 是因为这些函数是"排查用"的,和渲染主链路没有调用关系,混在一起会掩盖主干。
5
+ // 注:rendererInfo 里的 __transparentClear* 等字段对应的 GL 包装代码已删除,
6
+ // 这些键现在恒为 null/false(历史遗留),保留是为了不改变对外返回键集合。
7
+ import { collectLive2DModelCmp } from '../debug/model-cmp.js';
8
+
9
+ /** 渲染器自身的清屏配置(PIXI 字段,不做 GL 查询) */
10
+ function readClearConfig(r, gl) {
11
+ return {
12
+ backgroundAlpha: r.backgroundAlpha,
13
+ backgroundColor: r.backgroundColor,
14
+ clearBeforeRender: r.clearBeforeRender,
15
+ resolution: r.resolution,
16
+ width: r.width,
17
+ height: r.height,
18
+ rendererType: r.type,
19
+ dirty: !!r.dirty,
20
+ // 以下三项对应的 GL 包装代码已删除,现恒为 null/false(历史遗留,保留键名)
21
+ wrapped: !!(gl && gl.__transparentClearInstalled),
22
+ clearCalls: gl ? gl.__transparentClearCalls : null,
23
+ clearBufferCalls: gl && gl.__clearBufferCalls ? { ...gl.__clearBufferCalls } : null,
24
+ wrappedChained: gl ? !!gl.__transparentClearChained : null,
25
+ };
26
+ }
27
+
28
+ /** 逐项读一个 GL 参数(读失败返回 null,绝不抛) */
29
+ function glParam(gl, name, map) {
30
+ try {
31
+ const v = gl.getParameter(gl[name]);
32
+ return map ? map(v) : v;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /** GL 状态里与"清屏/裁剪"相关的项(每项都可能抛,逐项兜住) */
39
+ function readGlState(gl) {
40
+ if (!gl) return {};
41
+ const out = {};
42
+ try { out.contextLost = gl.isContextLost(); } catch { out.contextLost = null; }
43
+ try { out.scissorTest = gl.isEnabled(gl.SCISSOR_TEST); } catch { out.scissorTest = null; }
44
+ out.scissorBox = glParam(gl, 'SCISSOR_BOX', (v) => (v ? Array.from(v) : null));
45
+ out.clearColor = glParam(gl, 'COLOR_CLEAR_VALUE', (v) => (v ? Array.from(v) : null));
46
+ // 最可疑的一项:alpha 写入被关掉时,清什么都保持旧 alpha ⇒ 出现"这块透明那块不透明"
47
+ out.colorWriteMask = glParam(gl, 'COLOR_WRITEMASK', (v) => (v ? Array.from(v) : null));
48
+ out.samples = glParam(gl, 'SAMPLES');
49
+ try {
50
+ const a = gl.getContextAttributes ? gl.getContextAttributes() : null;
51
+ out.contextAlpha = a ? a.alpha : null;
52
+ out.antialias = a ? a.antialias : null;
53
+ } catch { /* 忽略 */ }
54
+ return out;
55
+ }
56
+
57
+ /**
58
+ * 创建只读诊断模块。
59
+ * @param {object} ctx 运行时上下文
60
+ * @returns {{readCanvasPixels:Function, collectCmp:Function, rendererInfo:Function, rendererSize:Function}}
61
+ */
62
+ export function createProbe(ctx) {
63
+ const { st, surfaceSize } = ctx;
64
+
65
+ async function readCanvasPixels(points = []) {
66
+ const app = st.app;
67
+ if (!app || !app.renderer) return null;
68
+ const gl = app.renderer.gl;
69
+ if (!gl) return null;
70
+ const res = app.renderer.resolution || 1;
71
+ const h = app.renderer.height;
72
+ const buf = new Uint8Array(4);
73
+ const out = [];
74
+ await new Promise((r) => requestAnimationFrame(() => r()));
75
+ try { app.renderer.render(app.stage); } catch { /* 忽略 */ }
76
+ for (const p of points) {
77
+ const px = Math.round(p[0] * res);
78
+ const py = Math.round(h - p[1] * res);
79
+ try {
80
+ gl.readPixels(px, py, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, buf);
81
+ out.push({ x: p[0], y: p[1], rgba: [buf[0], buf[1], buf[2], buf[3]] });
82
+ } catch (e) { out.push({ x: p[0], y: p[1], err: String(e && e.message).slice(0, 40) }); }
83
+ }
84
+ return out;
85
+ }
86
+
87
+ function collectCmp(tag) {
88
+ const rootEl = document.getElementById('stage');
89
+ return collectLive2DModelCmp({
90
+ app: st.app,
91
+ model: st.model,
92
+ internal: st.model && st.model.internalModel ? st.model.internalModel : null,
93
+ size: surfaceSize(),
94
+ dpr: ctx.handleRef ? ctx.handleRef.dpr : null,
95
+ meta: st.meta,
96
+ modelId: st.modelId,
97
+ zoomRatio: st.zoomRatio,
98
+ stageMode: rootEl ? rootEl.getAttribute('data-stage-mode') : null,
99
+ tag: String(tag || 'default'),
100
+ });
101
+ }
102
+
103
+ function rendererInfo() {
104
+ const app = st.app;
105
+ if (!app || !app.renderer) return null;
106
+ const r = app.renderer;
107
+ const gl = r.gl || null;
108
+ return { ...readClearConfig(r, gl), ...readGlState(gl) };
109
+ }
110
+
111
+ function rendererSize() {
112
+ if (!st.app || !st.app.renderer) return null;
113
+ const r = st.app.renderer;
114
+ const size = surfaceSize() || { width: 0, height: 0 };
115
+ return { w: r.width, h: r.height, res: r.resolution || 1, cssW: size.width, cssH: size.height };
116
+ }
117
+
118
+ return { readCanvasPixels, collectCmp, rendererInfo, rendererSize };
119
+ }
@@ -0,0 +1,66 @@
1
+ // live2d/release.js —— Live2D 面的统一资源释放(unmount 与 dispose 共用)
2
+ //
3
+ // 为什么单独成模块:这段是「契约 §五-7(unmount/dispose 不得遗留自建 GL 资源)」的落地实现,
4
+ // 也是曾经出过 bug 的地方(PIXI 的 destroy(true) 会摘掉 canvas 只留容器,下游按容器复用 ⇒
5
+ // 渲染到不在 DOM 的游离画布上、页面全白而 stats 与像素采样都正常)。
6
+ // 抽出来之后可以用 `node --test` 做静态回归(最小 DOM 垫片即可),不必每次都开浏览器验。
7
+ //
8
+ // 释放清单(顺序有意义:先停诊断 → 藏容器 → 销毁 app → 移除容器 → 复位状态):
9
+ // ① 诊断 HUD 句柄停掉(它会定时取数据,留着会读到已销毁的 app)
10
+ // ② 覆盖层隐藏(视觉上立刻消失,避免销毁过程中的中间帧露出来)
11
+ // ③ PIXI app.destroy(true) —— 释放 WebGL 上下文与整棵 display tree(自建 GL 资源的唯一持有者)
12
+ // ④ 移除 #live2d-overlay 容器(不做"保留容器以便快速切回":那个中间态正是上面那个 bug 的来源)
13
+ // ⑤ 状态复位(否则 isActive()/debug() 继续报告"还有模型在显示")
14
+
15
+ export const OVERLAY_ID = 'live2d-overlay';
16
+
17
+ /**
18
+ * 释放 Live2D 面持有的全部资源与状态。
19
+ * @param {{
20
+ * st: object, // live2d 舞台状态(app/model/modelId/meta/motionMap/vfsStats/active/overlay/libsReady)
21
+ * note?: Function, // 日志通道
22
+ * stopDiag?: Function|null, // 停诊断 HUD(可选)
23
+ * doc?: object, // 文档对象(默认 globalThis.document,便于测试注入)
24
+ * }} opts
25
+ * @returns {boolean} 是否真的释放了什么(用于幂等与日志判断)
26
+ */
27
+ export function releaseLive2DResources({ st, note, stopDiag, doc } = {}) {
28
+ const d = doc || (typeof document !== 'undefined' ? document : null);
29
+ if (!st) return false;
30
+ const hadApp = !!st.app;
31
+ const hadOverlay = !!(d && d.getElementById && d.getElementById(OVERLAY_ID));
32
+
33
+ try { if (typeof stopDiag === 'function') stopDiag(); } catch { /* 停诊断失败不阻塞释放 */ }
34
+ try { if (st.overlay && typeof st.overlay.style === 'object') st.overlay.style.display = 'none'; } catch { /* 忽略 */ }
35
+ // **app 常驻**:不 destroy(`destroy(true)` 的 removeView 会摘掉 canvas,那才是孤儿上下文的来源)。
36
+ // 每次 spine↔live2d 切换都 `new PIXI.Application()` 会新建 1 个匿名探测上下文且永不释放,
37
+ // 累积到浏览器上限(~16)就丢最旧 ⇒ 画布黑(实测:来回切 15 次即到顶)。
38
+ // 所以释放只做三件事:停渲染循环、释放纹理(GPU 内存大头)、隐藏覆盖层;app/canvas/context 保留,
39
+ // 下次切回来直接复用(只重建纹理,比 new Application 便宜得多)。
40
+ if (st.app) {
41
+ try { if (st.model) { st.app.stage.removeChild(st.model); } } catch { /* 忽略 */ }
42
+ try { if (st.model) st.model.destroy({ children: true, texture: true, baseTexture: true }); } catch { /* 忽略 */ }
43
+ try { if (typeof st.app.stop === 'function') st.app.stop(); } catch { /* 忽略 */ }
44
+ // 释放纹理缓存(PIXI 的内存大头);拿不到就跳过,不影响正确性
45
+ try {
46
+ const r = st.app.renderer;
47
+ if (r && r.textureGC && typeof r.textureGC.run === 'function') r.textureGC.run();
48
+ } catch { /* 忽略 */ }
49
+ try { if (st.overlay && typeof st.overlay.style === 'object') st.overlay.style.display = 'none'; } catch { /* 忽略 */ }
50
+ }
51
+ // 覆盖层 DOM 保留(app 常驻 ⇒ 它的 canvas 还得留着,摘掉就是旧坑"容器在、canvas 不在")
52
+ st.overlay = (d && d.getElementById) ? (d.getElementById(OVERLAY_ID) || st.overlay) : st.overlay;
53
+ st.libsReady = null;
54
+ st.model = null;
55
+ st.modelId = null;
56
+ st.meta = null;
57
+ st.motionMap = new Map();
58
+ st.vfsStats = null;
59
+ // 与 model.js 的 publishVfsStats 对称:释放后 dataset 里的读数必须消失,否则下一次排查会读到旧值
60
+ try { if (d && d.documentElement) delete d.documentElement.dataset.vfsStats; } catch { /* 忽略 */ }
61
+ st.active = false;
62
+
63
+ const did = hadApp || hadOverlay;
64
+ if (did && typeof note === 'function') note('Live2D 已释放(纹理释放、渲染停止、覆盖层隐藏;app 常驻复用)');
65
+ return did;
66
+ }