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,292 @@
1
+ // model-cmp.js —— 逐模型差异采集(诊断专用,不改任何渲染行为)
2
+ //
3
+ // 依据:docs/设计/相机.md 第 4 节。
4
+ // 为什么用 extract 而不是 readPixels:Live2D 的 PIXI 画布是 preserveDrawingBuffer=false,
5
+ // 直接 readPixels 全 0,不能当证据;extract.canvas 走离屏渲染,拿到的是"实际画出来的结果"。
6
+ //
7
+ // 判定思路(本轮核心):把画布四边内侧与"美术包围盒之外、画布之内"的 alpha 采出来 ——
8
+ // outsideArtAlphaRatio > 0.01 ⇒ 画布被我们涂满了(渲染侧问题);
9
+ // = 0 ⇒ 边缘本身就是美术内容。
10
+
11
+ /** 判定「有内容」的 alpha 阈值(0-255) */
12
+ const ALPHA_ART = 8;
13
+ /** 像素分析的最大边长(降采样上限,控制单次 getImageData 的内存) */
14
+ const MAX_ANALYZE = 1024;
15
+ /** 四边内侧采样带宽(像素) */
16
+ const EDGE_BAND = 2;
17
+
18
+ /**
19
+ * 把 extract 出来的 canvas 画到离屏 2D 画布上并读像素(extract 的 canvas 可能不可直接 getImageData)。
20
+ * @param {HTMLCanvasElement} cv
21
+ * @returns {{data: Uint8ClampedArray, w: number, h: number}|{error: string}}
22
+ */
23
+ function readPixelsOf(cv) {
24
+ const w = Math.max(1, Math.round(cv.width || 0));
25
+ const h = Math.max(1, Math.round(cv.height || 0));
26
+ // 降采样后再读像素:6000×6000 的画布一次 getImageData 就是 144MB,多状态会拖死页面。
27
+ // 只缩不放(≤ MAX_ANALYZE),比值类指标(opaqueRatio / outsideArtAlphaRatio / 四边 alpha)不受影响;
28
+ // 长度类指标按 scaleBack 换回原画布单位,保证不同模型之间可比。
29
+ const scale = Math.min(1, MAX_ANALYZE / Math.max(w, h));
30
+ const aw = Math.max(1, Math.round(w * scale));
31
+ const ah = Math.max(1, Math.round(h * scale));
32
+ try {
33
+ const off = document.createElement('canvas');
34
+ off.width = aw;
35
+ off.height = ah;
36
+ const ctx = off.getContext('2d', { willReadFrequently: true });
37
+ ctx.clearRect(0, 0, aw, ah);
38
+ ctx.drawImage(cv, 0, 0, w, h, 0, 0, aw, ah);
39
+ const data = ctx.getImageData(0, 0, aw, ah).data;
40
+ // 立刻释放大画布:6000×6000 的 backing store 约 144MB,多模型多状态不释放会拖死标签页
41
+ try { cv.width = 1; cv.height = 1; } catch { /* 忽略 */ }
42
+ return { data, w: aw, h: ah, srcW: w, srcH: h, scaleBack: w / aw };
43
+ } catch (e) {
44
+ return { error: String((e && e.message) || e) };
45
+ }
46
+ }
47
+
48
+ /** 不透明像素包围盒 + 不透明占比 */
49
+ function measureBBox(data, w, h) {
50
+ let minX = w;
51
+ let minY = h;
52
+ let maxX = -1;
53
+ let maxY = -1;
54
+ let opaque = 0;
55
+ for (let y = 0; y < h; y += 1) {
56
+ const row = y * w * 4;
57
+ for (let x = 0; x < w; x += 1) {
58
+ if (data[row + x * 4 + 3] > ALPHA_ART) {
59
+ opaque += 1;
60
+ if (x < minX) minX = x;
61
+ if (x > maxX) maxX = x;
62
+ if (y < minY) minY = y;
63
+ if (y > maxY) maxY = y;
64
+ }
65
+ }
66
+ }
67
+ const empty = maxX < minX || maxY < minY;
68
+ return {
69
+ bboxX: empty ? 0 : minX,
70
+ bboxY: empty ? 0 : minY,
71
+ bboxW: empty ? 0 : maxX - minX + 1,
72
+ bboxH: empty ? 0 : maxY - minY + 1,
73
+ opaqueRatio: +(opaque / (w * h)).toFixed(4),
74
+ };
75
+ }
76
+
77
+ /** 四边内侧 alpha 均值(判断"边缘是否被涂色") */
78
+ function measureEdges(data, w, h) {
79
+ const avgOf = (x0, x1, y0, y1) => {
80
+ let sum = 0;
81
+ let n = 0;
82
+ for (let y = y0; y < y1; y += 1) {
83
+ for (let x = x0; x < x1; x += 1) {
84
+ sum += data[(y * w + x) * 4 + 3];
85
+ n += 1;
86
+ }
87
+ }
88
+ return n ? +(sum / n / 255).toFixed(4) : 0;
89
+ };
90
+ const bx = Math.min(EDGE_BAND, w);
91
+ const by = Math.min(EDGE_BAND, h);
92
+ return {
93
+ top: avgOf(0, w, 0, by),
94
+ bottom: avgOf(0, w, h - by, h),
95
+ left: avgOf(0, bx, 0, h),
96
+ right: avgOf(w - bx, w, 0, h),
97
+ };
98
+ }
99
+
100
+ /** 美术包围盒之外、画布之内的平均 alpha(本轮的关键指标) */
101
+ function measureOutsideArt(data, w, h, bbox) {
102
+ const { bboxX, bboxY, bboxW, bboxH } = bbox;
103
+ let sum = 0;
104
+ let n = 0;
105
+ for (let y = 0; y < h; y += 1) {
106
+ const inY = y >= bboxY && y < bboxY + bboxH;
107
+ const row = y * w * 4;
108
+ for (let x = 0; x < w; x += 1) {
109
+ if (inY && x >= bboxX && x < bboxX + bboxW) continue;
110
+ sum += data[row + x * 4 + 3];
111
+ n += 1;
112
+ }
113
+ }
114
+ return n ? +(sum / n / 255).toFixed(4) : 0;
115
+ }
116
+
117
+ /**
118
+ * 分析一张 extract 出来的画布。
119
+ * @param {HTMLCanvasElement} cv
120
+ * @returns {object} { w, h, bboxW, bboxH, bboxX, bboxY, opaqueRatio, edgeAlpha, outsideArtAlphaRatio } 或 { *_error }
121
+ */
122
+ export function analyzeExtractCanvas(cv) {
123
+ const px = readPixelsOf(cv);
124
+ if (px.error) return { w: cv.width, h: cv.height, extract_error: px.error };
125
+ const bbox = measureBBox(px.data, px.w, px.h);
126
+ const back = px.scaleBack || 1;
127
+ return {
128
+ w: px.srcW,
129
+ h: px.srcH,
130
+ bboxX: Math.round(bbox.bboxX * back),
131
+ bboxY: Math.round(bbox.bboxY * back),
132
+ bboxW: Math.round(bbox.bboxW * back),
133
+ bboxH: Math.round(bbox.bboxH * back),
134
+ opaqueRatio: bbox.opaqueRatio,
135
+ edgeAlpha: measureEdges(px.data, px.w, px.h),
136
+ outsideArtAlphaRatio: measureOutsideArt(px.data, px.w, px.h, bbox),
137
+ analyzeW: px.w,
138
+ analyzeH: px.h,
139
+ };
140
+ }
141
+
142
+ /** 落盘通道:POST /api/diag,tag=MODELCMP(失败静默,绝不影响页面) */
143
+ function postDiag(rec) {
144
+ try {
145
+ // **刻意不加超时信号**(这不是漏了):这是一条 fire-and-forget 的诊断落盘,
146
+ // 失败本来就被 .catch 吞掉、不影响页面任何功能。
147
+ // 加 AbortSignal.timeout 反而有害:页面卸载/切到后台时超时会抛 AbortError,
148
+ // 而这类"We don't care 的旁路请求"被中断属于正常现象,会污染控制台与诊断日志。
149
+ fetch('/api/diag', {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ // 带上本页 sid:否则这些条目落到 anonymous 会话,GET /api/diag?sid=… 取不到
153
+ body: JSON.stringify({ sid: (typeof window !== 'undefined' && window.__DIAG__ && window.__DIAG__.sid) || undefined, entries: [{ ts: Date.now(), level: 'info', tag: 'MODELCMP', message: JSON.stringify(rec) }] }),
154
+ }).catch(() => {});
155
+ } catch { /* 忽略 */ }
156
+ }
157
+
158
+ /**
159
+ * 写一条采集记录:控制台单行 + `__WEB__.modelCmp` 数组 + diag 落盘。
160
+ * @param {object} rec
161
+ * @returns {object} 同一条记录
162
+ */
163
+ export function recordModelCmp(rec) {
164
+ const bag = (window.__WEB__ = window.__WEB__ || {});
165
+ if (!Array.isArray(bag.modelCmp)) bag.modelCmp = [];
166
+ bag.modelCmp.push(rec);
167
+ try { console.log('[MODELCMP] ' + JSON.stringify(rec)); } catch { /* 忽略 */ }
168
+ postDiag(rec);
169
+ return rec;
170
+ }
171
+
172
+ // ---------------------------------------------------------------- 记录构建
173
+ // 字段名 / 单位 / 采集时机三处统一,不同模型之间才可比(见 doc 第 4 节)。
174
+
175
+ /** 安全取值:失败时返回 { <key>_error },不丢弃整条记录(否则 diff 会误判为"缺失即不同") */
176
+ function safePick(key, fn) {
177
+ try {
178
+ return fn();
179
+ } catch (e) {
180
+ return { [`${key}_error`]: String((e && e.message) || e) };
181
+ }
182
+ }
183
+
184
+ /** Cubism 内部尺寸(模型自身坐标系) */
185
+ function cmpInternalSize(internal) {
186
+ if (!internal) return null;
187
+ return { w: internal.width, h: internal.height, originalW: internal.originalWidth, originalH: internal.originalHeight };
188
+ }
189
+
190
+ /** Cubism 画布尺寸(内部单位,不是 CSS 像素) */
191
+ function cmpCanvasSize(core) {
192
+ if (!core) return null;
193
+ const w = typeof core.getCanvasWidth === 'function' ? core.getCanvasWidth() : null;
194
+ const h = typeof core.getCanvasHeight === 'function' ? core.getCanvasHeight() : null;
195
+ return { w, h };
196
+ }
197
+
198
+ /** PIXI 显示对象(width/height 是未缩放尺寸,scale 是当前缩放) */
199
+ function cmpDisplay(model) {
200
+ return {
201
+ w: model.width,
202
+ h: model.height,
203
+ x: model.x,
204
+ y: model.y,
205
+ scale: model.scale ? model.scale.x : null,
206
+ rotation: model.rotation,
207
+ pivot: model.pivot ? { x: model.pivot.x, y: model.pivot.y } : null,
208
+ anchor: model.anchor ? { x: model.anchor.x, y: model.anchor.y } : null,
209
+ };
210
+ }
211
+
212
+ /** 世界坐标系包围盒(含当前缩放/位移) */
213
+ function cmpBounds(model) {
214
+ const b = model.getBounds();
215
+ return { x: b.x, y: b.y, w: b.width, h: b.height };
216
+ }
217
+
218
+ /** 纹理尺寸(判断是否被压缩/裁剪) */
219
+ function cmpTextures(model) {
220
+ return (model.textures || []).map((t) => {
221
+ const bt = t && t.baseTexture ? t.baseTexture : null;
222
+ return { w: bt ? bt.width : (t ? t.width : null), h: bt ? bt.height : (t ? t.height : null) };
223
+ });
224
+ }
225
+
226
+ /**
227
+ * 我们算出来的适配值(纯算术还原,不重置模型):
228
+ * basisW/H = 当前包围盒 ÷ 当前缩放(等价于 scale=1 时的包围盒)
229
+ */
230
+ function cmpFit(model, size) {
231
+ const b = model.getBounds();
232
+ const scale = model.scale ? model.scale.x || 1 : 1;
233
+ const basisW = b.width / scale;
234
+ const basisH = b.height / scale;
235
+ const W = size ? size.width : null;
236
+ const H = size ? size.height : null;
237
+ const fitScale = W && H && basisW && basisH ? Math.min(W / basisW, H / basisH) : null;
238
+ return {
239
+ basis: 'bounds',
240
+ basisW: +basisW.toFixed(2),
241
+ basisH: +basisH.toFixed(2),
242
+ fitScale: fitScale == null ? null : +(fitScale * 0.98).toFixed(4),
243
+ appliedScale: +Number(scale).toFixed(4),
244
+ offsetX: +Number(model.x).toFixed(2),
245
+ offsetY: +Number(model.y).toFixed(2),
246
+ margin: 0.98,
247
+ };
248
+ }
249
+
250
+ /** model3 结构特征(版本 / 布局 / 动作组 / 物理 / 姿势) */
251
+ function cmpModel3(internal, meta) {
252
+ const settings = internal && internal.settings ? internal.settings : null;
253
+ const json = settings && (settings.json || settings) ? (settings.json || settings) : null;
254
+ const refs = json && json.FileReferences ? json.FileReferences : null;
255
+ const motions = refs && refs.Motions ? Object.keys(refs.Motions) : [];
256
+ return {
257
+ version: (internal && internal.cubismVersion) || (meta && meta.cubism) || (settings && settings.version) || null,
258
+ hasLayout: !!(json && json.Layout),
259
+ motionGroups: motions.length,
260
+ physics: !!(refs && refs.Physics) || !!(meta && meta.physics),
261
+ pose: !!(refs && refs.Pose),
262
+ expressions: refs && Array.isArray(refs.Expressions) ? refs.Expressions.length : 0,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * 构建并落盘一条 Live2D 的 MODELCMP 记录(只读;不重置视图)。
268
+ * @param {{app: object, model: object, internal: object, size: object, dpr: number,
269
+ * meta: object, modelId: string, zoomRatio: number, stageMode: string, tag: string}} ctx
270
+ * @returns {object} 记录
271
+ */
272
+ export function collectLive2DModelCmp(ctx) {
273
+ const { app, model, internal, size, dpr, meta, modelId, zoomRatio, stageMode, tag } = ctx || {};
274
+ const rec = { ts: Date.now(), tag, stageMode: stageMode || null };
275
+ if (!model || !app || !app.renderer) {
276
+ rec.collect_error = 'no model/app';
277
+ return recordModelCmp(rec);
278
+ }
279
+ rec.id = modelId;
280
+ rec.name = (meta && meta.name) || modelId;
281
+ rec.internalSize = safePick('internalSize', () => cmpInternalSize(internal));
282
+ rec.canvasSize = safePick('canvasSize', () => cmpCanvasSize(internal && internal.coreModel));
283
+ rec.display = safePick('display', () => cmpDisplay(model));
284
+ rec.bounds = safePick('bounds', () => cmpBounds(model));
285
+ rec.extract = safePick('extract', () => analyzeExtractCanvas(app.renderer.extract.canvas(model)));
286
+ rec.fit = safePick('fit', () => cmpFit(model, size));
287
+ rec.textures = safePick('textures', () => cmpTextures(model));
288
+ rec.model3 = safePick('model3', () => cmpModel3(internal, meta));
289
+ rec.viewport = safePick('viewport', () => ({ w: size ? size.width : null, h: size ? size.height : null, dpr: dpr == null ? null : dpr }));
290
+ rec.camera = { scale: +(Number(zoomRatio) || 1).toFixed(4) };
291
+ return recordModelCmp(rec);
292
+ }