modelstage 0.0.0-stage → 0.5.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (286) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +267 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +101 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +58 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +647 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +334 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/web/README.md +193 -0
  126. package/src/web/app/anim-list.js +222 -0
  127. package/src/web/app/background-solids.js +150 -0
  128. package/src/web/app/backgrounds.js +152 -0
  129. package/src/web/app/camera-log.js +28 -0
  130. package/src/web/app/context.js +72 -0
  131. package/src/web/app/dir-picker.js +242 -0
  132. package/src/web/app/entry-report.js +71 -0
  133. package/src/web/app/memory-bars.js +91 -0
  134. package/src/web/app/memory-budget.js +228 -0
  135. package/src/web/app/memory-guard.js +204 -0
  136. package/src/web/app/model-actions.js +682 -0
  137. package/src/web/app/renderer-setting.js +70 -0
  138. package/src/web/app/root-manager.js +254 -0
  139. package/src/web/app/status.js +138 -0
  140. package/src/web/app/topbar-actions.js +357 -0
  141. package/src/web/app/transport-actions.js +297 -0
  142. package/src/web/app/ui-state.js +559 -0
  143. package/src/web/app/url-params.js +171 -0
  144. package/src/web/app/view-settings.js +186 -0
  145. package/src/web/components.css +465 -0
  146. package/src/web/debug/diag-hud.js +95 -0
  147. package/src/web/debug/drag.js +369 -0
  148. package/src/web/debug/log.js +320 -0
  149. package/src/web/debug/model-cmp.js +292 -0
  150. package/src/web/debug/panel.css +474 -0
  151. package/src/web/debug/panel.js +712 -0
  152. package/src/web/debug/race-probe.js +82 -0
  153. package/src/web/debug/upload.js +130 -0
  154. package/src/web/engine/adapters/atlas.js +47 -0
  155. package/src/web/engine/adapters/index.js +22 -0
  156. package/src/web/engine/adapters/renderer.js +223 -0
  157. package/src/web/engine/adapters/skeleton.js +105 -0
  158. package/src/web/engine/adapters/texture.js +64 -0
  159. package/src/web/engine/animation.js +181 -0
  160. package/src/web/engine/appearance.js +136 -0
  161. package/src/web/engine/camera-adapter.js +212 -0
  162. package/src/web/engine/effect-chain.js +138 -0
  163. package/src/web/engine/engine-state.js +120 -0
  164. package/src/web/engine/filters.js +224 -0
  165. package/src/web/engine/fit.js +197 -0
  166. package/src/web/engine/frame-gate.js +33 -0
  167. package/src/web/engine/gl-driver.js +56 -0
  168. package/src/web/engine/gl.js +77 -0
  169. package/src/web/engine/index.js +20 -0
  170. package/src/web/engine/loader.js +55 -0
  171. package/src/web/engine/model-info.js +26 -0
  172. package/src/web/engine/pipeline.js +200 -0
  173. package/src/web/engine/player.js +427 -0
  174. package/src/web/engine/pma/blend.js +76 -0
  175. package/src/web/engine/pma/detect.js +118 -0
  176. package/src/web/engine/pma/index.js +8 -0
  177. package/src/web/engine/pma/resolve.js +112 -0
  178. package/src/web/engine/pma/upload.js +127 -0
  179. package/src/web/engine/pma.js +5 -0
  180. package/src/web/engine/render-frame.js +93 -0
  181. package/src/web/engine/runtime-capabilities.js +209 -0
  182. package/src/web/engine/runtime.js +37 -0
  183. package/src/web/engine/skins.js +165 -0
  184. package/src/web/engine/textures.js +174 -0
  185. package/src/web/index.html +115 -0
  186. package/src/web/infra/api.js +531 -0
  187. package/src/web/infra/diag.js +509 -0
  188. package/src/web/infra/document.js +269 -0
  189. package/src/web/infra/dom.js +31 -0
  190. package/src/web/infra/events.mjs +27 -0
  191. package/src/web/infra/log.js +153 -0
  192. package/src/web/infra/mem-stats.js +112 -0
  193. package/src/web/infra/plugin-host-registration.mjs +285 -0
  194. package/src/web/infra/plugin-host-report.mjs +32 -0
  195. package/src/web/infra/plugin-host-unload.mjs +79 -0
  196. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  197. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  198. package/src/web/infra/plugin-host.js +628 -0
  199. package/src/web/infra/ready.js +461 -0
  200. package/src/web/infra/schedule.js +51 -0
  201. package/src/web/infra/shortcuts.js +129 -0
  202. package/src/web/infra/slot-registry.js +402 -0
  203. package/src/web/infra/vfs.js +247 -0
  204. package/src/web/live2d/bridge.js +400 -0
  205. package/src/web/live2d/model.js +321 -0
  206. package/src/web/live2d/overlay.js +142 -0
  207. package/src/web/live2d/pixi-app.js +111 -0
  208. package/src/web/live2d/playback-probe.js +92 -0
  209. package/src/web/live2d/probe-geometry.js +329 -0
  210. package/src/web/live2d/probe.js +119 -0
  211. package/src/web/live2d/release.js +66 -0
  212. package/src/web/live2d/runtime-adapter.js +289 -0
  213. package/src/web/live2d/stage-ctx.js +152 -0
  214. package/src/web/live2d/stage.js +204 -0
  215. package/src/web/live2d/vfs-settings.js +174 -0
  216. package/src/web/live2d/view.js +89 -0
  217. package/src/web/main.js +629 -0
  218. package/src/web/panel/background-list.js +373 -0
  219. package/src/web/panel/bg-source.js +291 -0
  220. package/src/web/panel/color-picker.css +199 -0
  221. package/src/web/panel/color-picker.js +278 -0
  222. package/src/web/panel/dialog.css +116 -0
  223. package/src/web/panel/dialog.js +173 -0
  224. package/src/web/panel/dom.js +35 -0
  225. package/src/web/panel/icons.js +58 -0
  226. package/src/web/panel/memory-bar.js +195 -0
  227. package/src/web/panel/panel-settings.js +1263 -0
  228. package/src/web/panel/panel.css +1304 -0
  229. package/src/web/panel/render-choice.js +104 -0
  230. package/src/web/panel/schema.js +784 -0
  231. package/src/web/panel/settings-io.js +291 -0
  232. package/src/web/panel/shell-controls.js +274 -0
  233. package/src/web/panel/slider.js +210 -0
  234. package/src/web/panel/toolbar-schema.js +111 -0
  235. package/src/web/panel/toolbar.js +195 -0
  236. package/src/web/panel/transport-picker.js +375 -0
  237. package/src/web/panel/transport.js +321 -0
  238. package/src/web/panel/view.js +553 -0
  239. package/src/web/plugin/unpack-worker.js +185 -0
  240. package/src/web/render/camera/constants.js +60 -0
  241. package/src/web/render/camera/index.js +123 -0
  242. package/src/web/render/camera/spine.js +124 -0
  243. package/src/web/render/capture.js +30 -0
  244. package/src/web/render/interaction.js +527 -0
  245. package/src/web/render/live2d.js +627 -0
  246. package/src/web/render/playback.js +47 -0
  247. package/src/web/render/reset-view.js +29 -0
  248. package/src/web/render/spine.js +386 -0
  249. package/src/web/render/surface-switch.js +176 -0
  250. package/src/web/render/surface.js +571 -0
  251. package/src/web/render/surfaces.js +70 -0
  252. package/src/web/roster/filter.js +31 -0
  253. package/src/web/roster/list.js +276 -0
  254. package/src/web/roster/persist.js +52 -0
  255. package/src/web/roster/reveal.js +77 -0
  256. package/src/web/roster/rows.js +248 -0
  257. package/src/web/roster/snapshot.js +37 -0
  258. package/src/web/roster/view.js +670 -0
  259. package/src/web/spine/canvas-api-groups.js +267 -0
  260. package/src/web/spine/canvas-api.js +97 -0
  261. package/src/web/spine/canvas-document.js +80 -0
  262. package/src/web/spine/canvas-engine.js +193 -0
  263. package/src/web/spine/canvas-entry.js +28 -0
  264. package/src/web/spine/canvas-pixels.js +36 -0
  265. package/src/web/spine/canvas-runtime.js +127 -0
  266. package/src/web/spine/canvas-view.js +108 -0
  267. package/src/web/spine/canvas.js +7 -0
  268. package/src/web/spine/runtime-hint.js +73 -0
  269. package/src/web/spine/runtimes.js +212 -0
  270. package/src/web/style.css +493 -0
  271. package/tool/lib/bundle-probe-node.mjs +121 -0
  272. package/tool/lib/check-perms.mjs +114 -0
  273. package/tool/lib/contract-checks.mjs +582 -0
  274. package/tool/lib/cubism-core.mjs +122 -0
  275. package/tool/lib/export-bundle.mjs +328 -0
  276. package/tool/lib/paths.mjs +20 -0
  277. package/tool/lib/runtime-guides.mjs +65 -0
  278. package/tool/lib/runtime-package.mjs +403 -0
  279. package/tool/lib/spine-meta.mjs +227 -0
  280. package/tool/lib/spine-motions.mjs +104 -0
  281. package/tool/lib/spine-runtime.mjs +131 -0
  282. package/tool/lib/test-lock.mjs +178 -0
  283. package/tool/lib/test-report.mjs +133 -0
  284. package/tool/parse-jsonc.mjs +86 -0
  285. package/tool/resolve-live2d-runtimes.mjs +276 -0
  286. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,185 @@
1
+ // src/web/plugin/unpack-worker.js —— 资源包解包 Worker(classic worker,不是 module worker)
2
+ //
3
+ // 为什么是 classic:插件 lib 全是 **classic script**,它们靠 `globalThis.__ULB__` 逐级挂载、
4
+ // 靠 `plugin.json` 的 `files[]` **加载顺序**建立依赖。module worker 的 import 语义与此不符,
5
+ // 而 classic worker 的 `importScripts()` 天然按调用顺序同步执行 —— 与浏览器里注入 <script> 等价。
6
+ //
7
+ // 为什么搬进 worker:LZMA 解压实测 300~620ms/MB(解压后),8.28MB 的包要 3.9 秒。
8
+ // 这段是**纯同步 CPU**,留在主线程就是 4 秒白屏冻死;放进 worker 后主线程照常渲染。
9
+ //
10
+ // 消息协议(只有 4 种,刻意保持最小):
11
+ // ← {t:'init', base, files[]} 主线程告诉它插件目录与加载顺序
12
+ // → {t:'ready'} lib 载好,provider 已捕获
13
+ // → {t:'progress', phase, detail} 进度(块表 / 开始解压 / 完成)
14
+ // ← {t:'unpack', id, file:{rootId,rel}, bytes}
15
+ // → {t:'done', id, res:{prefix,entry,files:[[path, Blob|Uint8Array, type]]}, ms}
16
+ // → {t:'fail', id, code, message}
17
+ //
18
+ // 硬约束:
19
+ // · 产物出口**两种形态**:插件交付的 Blob 原样透传(= 零拷贝,Blob 过通道是引用语义);
20
+ // 非 Blob(LZMA 中间结果是普通 Array / TypedArray)才归一成 Uint8Array,并带上 MIME type
21
+ // 供主线程按需重建 Blob。Array 过通道还是 Array,所以那一路必须先归一,否则主线程按字节处理会错位。
22
+ // · 只有 Uint8Array 那一路进 transfer 列表(transferable 只认 ArrayBuffer 系;Blob 不能也不需 transfer)
23
+
24
+ // 插件 index.js 顶层读 `window.SpineViewerPlugins`(懒获取宿主面)。
25
+ // worker 里没有 window ⇒ 用 self 兜上,再挂一个**宽容的 stub**:只捕获 register,
26
+ // 其余成员一律 no-op(插件可能调用 log/emit 等非必需能力)。
27
+ self.window = self;
28
+
29
+ let captured = null; // 插件 register 进来的 provider 记录
30
+ let loaded = false; // lib 是否已载入
31
+
32
+ function installHostStub() {
33
+ const noop = () => {};
34
+ const stub = {
35
+ // 唯一的真实现:捕获插件注册(我们要的就是它的 resolve)
36
+ register(rec) { captured = rec; return true; },
37
+ // worker 里没有文件系统/网络:插件若真要额外取文件,明确抛错而不是静默返回空
38
+ fetchFile() { throw new Error('unpack-worker:worker 内不提供文件读取(字节随消息传入)'); },
39
+ // 进度回调给主线程(插件若有 post 通道就顺手用上)
40
+ post(msg) { self.postMessage(msg); },
41
+ };
42
+ self.SpineViewerPlugins = new Proxy(stub, {
43
+ get(target, key) { return key in target ? target[key] : noop; },
44
+ });
45
+ }
46
+
47
+ /** 把 Array / ArrayBuffer / TypedArray / **Blob** 统一成 Uint8Array(跨线程前必须做) */
48
+ async function toU8(v) {
49
+ if (v instanceof Uint8Array) return v;
50
+ if (v instanceof ArrayBuffer) return new Uint8Array(v);
51
+ if (ArrayBuffer.isView(v)) return new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
52
+ if (Array.isArray(v)) return Uint8Array.from(v); // ← LZMA 的普通 Array 走这条
53
+ // 插件产物实际是 **Blob**(vfs.register 只收 Blob/字符串),必须读出字节再传
54
+ if (v && typeof v.arrayBuffer === 'function') return new Uint8Array(await v.arrayBuffer());
55
+ return new Uint8Array(0);
56
+ }
57
+
58
+ // 先挂一次(函数声明提升,此处赋值即可);载完 lib 后 init 分支里会**再挂一次**(见那里的说明)
59
+ self.onmessage = handleMessage;
60
+
61
+ async function handleMessage(ev) {
62
+ const msg = ev.data || {};
63
+ try {
64
+ if (msg.t === 'init') {
65
+ installHostStub();
66
+ // 按 plugin.json 的 files[] 顺序逐条载入(顺序即依赖,不许自己排)
67
+ for (const rel of msg.files || []) importScripts(new URL(rel, msg.base).href);
68
+ loaded = true;
69
+ // ⚠️ 必须**重新挂 onmessage**:lib/lzma-d-min.js 是上游的 worker 版 IIFE,它检测到
70
+ // "有 onmessage、没有 window.document" 就把自己的 worker 协议处理器赋给 onmessage
71
+ // (源文件末尾那段自举)。不重挂,后续 unpack 消息会被它吃掉 ⇒ 表现成
72
+ // "ready 之后再无消息、解包永远不返回"(这个坑实测踩过一次)。
73
+ self.onmessage = handleMessage;
74
+ self.postMessage({ t: 'ready', hasProvider: !!captured, providerId: captured && captured.id });
75
+ return;
76
+ }
77
+
78
+ if (msg.t === 'unpack') {
79
+ if (!loaded || !captured || typeof captured.resolve !== 'function') {
80
+ self.postMessage({ t: 'fail', id: msg.id, code: 'ULB_WORKER_NO_PROVIDER', message: 'worker 里没有可用的 provider.resolve(插件注册失败)' });
81
+ return;
82
+ }
83
+ // 取字节的两种方式(方向④):
84
+ // · msg.bytes 已给 ⇒ 直接用(调用方本来就读过了)
85
+ // · 只有 msg.url ⇒ **worker 自己拉**。这一步是本轮的核心:主线程不再把 19MB 读进来
86
+ // 再 postMessage 拷一遍(实测那一段 3918ms/19.3MB,是首帧最大的单笔开销)。
87
+ // fetch 与页面同源,自动带 cookie;失败要如实回 fail,让主线程按老规矩回落。
88
+ let bytes;
89
+ if (msg.bytes) {
90
+ bytes = toU8(msg.bytes);
91
+ } else if (msg.url) {
92
+ const tf = Date.now();
93
+ let res;
94
+ try {
95
+ res = await fetch(msg.url, { credentials: 'same-origin' });
96
+ } catch (e) {
97
+ self.postMessage({ t: 'fail', id: msg.id, code: 'ULB_WORKER_FETCH', message: `worker 拉包失败:${(e && e.message) || e}` });
98
+ return;
99
+ }
100
+ if (!res || !res.ok) {
101
+ self.postMessage({ t: 'fail', id: msg.id, code: 'ULB_WORKER_FETCH', message: `worker 拉包失败:HTTP ${res ? res.status : '?'}` });
102
+ return;
103
+ }
104
+ bytes = toU8(await res.arrayBuffer());
105
+ self.postMessage({
106
+ t: 'progress', id: msg.id, phase: 'fetched',
107
+ detail: { bytes: bytes.byteLength, fetchMs: Date.now() - tf },
108
+ });
109
+ } else {
110
+ self.postMessage({ t: 'fail', id: msg.id, code: 'ULB_WORKER_NO_BYTES', message: '既没有 bytes 也没有 url' });
111
+ return;
112
+ }
113
+ const t0 = Date.now();
114
+
115
+ // 注意:这里**不调 captured.canHandle**。
116
+ // canHandle 走 bundleProbe.run → unityfs.parse(完整解压)+ serialized 解析;
117
+ // 实测把它放进 worker 会长时间无响应(36 秒未返回,原因未定位),所以回退。
118
+ // 类型判定改由「主线程调用 canHandle 时不传 bytes」解耦:插件对无 bytes 的调用直接返回 true
119
+ // (插件 index.js 的 canHandle 开头 `if (!bytes) return true`),主线程因此零解析成本;
120
+ // 真正的类型判定交给下面 resolve 的成败 —— 认不了就抛错,由主线程换下一个候选。
121
+
122
+ // 进度①:读块表(`blocksSummary` 只解 blocksInfo、不碰数据块,实测 ~5ms)
123
+ try {
124
+ const ufs = self.__ULB__ && self.__ULB__.unityfs;
125
+ if (ufs && typeof ufs.blocksSummary === 'function') {
126
+ const s = ufs.blocksSummary(bytes);
127
+ const head = ufs.readHeader(bytes);
128
+ const algoNames = { 0: 'none', 1: 'LZMA', 2: 'LZ4', 3: 'LZ4HC', 4: 'LZHAM' };
129
+ const algo = [...new Set(head.blocks.map((b) => algoNames[b.flags & 0x3f] || '?'))].join('+');
130
+ self.postMessage({
131
+ t: 'progress', id: msg.id, phase: 'blocks',
132
+ detail: {
133
+ algo,
134
+ blocks: s.blockCount,
135
+ decompressedMB: +(s.decompressedBytes / 1048576).toFixed(2),
136
+ // 经验系数:LZMA 约 470ms/MB(实测 2.76~33.4MB 区间 300~620),用于给用户一个量级预期
137
+ estimateMs: algo.includes('LZMA') ? Math.round((s.decompressedBytes / 1048576) * 470) : 0,
138
+ },
139
+ });
140
+ }
141
+ } catch { /* 读块表失败不影响解压,静默跳过 */ }
142
+
143
+ // 进度②:开始解压(块级进度钩不到 —— 那在插件内部循环里,本轮不改插件实现)
144
+ self.postMessage({ t: 'progress', id: msg.id, phase: 'unpacking' });
145
+
146
+ const res = await captured.resolve({ ...msg.file, bytes, host: self.SpineViewerPlugins });
147
+ if (!res || typeof res !== 'object' || !res.files) {
148
+ self.postMessage({ t: 'fail', id: msg.id, code: 'ULB_WORKER_BAD_RESULT', message: 'resolve 没有返回 files' });
149
+ return;
150
+ }
151
+
152
+ // 出口归一:**Blob 原样透传,其余才转 Uint8Array**
153
+ // 为什么不再一律转 Uint8Array:插件产物本来就是 Blob(vfs.register 只收 Blob 或字符串),
154
+ // 而 Blob → Uint8Array 要整份读出来(44MB 级的包就是一次全量复制),
155
+ // 主线程收到后再 new Blob([u8]) 又复制一次 —— 一进一出两次都是白做的。
156
+ // Blob 过 postMessage 是**引用/快照语义**(不复制字节),原样传就等于零拷贝到主线程。
157
+ const files = [];
158
+ const transfer = [];
159
+ const src = res.files instanceof Map ? res.files : Object.entries(res.files);
160
+ for (const [path, val] of src) {
161
+ const type = (val && typeof val.type === 'string') ? val.type : '';
162
+ if (typeof Blob !== 'undefined' && val instanceof Blob) {
163
+ // 零拷贝:不读、不转,也不能进 transfer 列表(transferable 只认 ArrayBuffer 系)
164
+ files.push([path, val, type]);
165
+ continue;
166
+ }
167
+ const u8 = await toU8(val);
168
+ files.push([path, u8, type]);
169
+ if (u8.buffer instanceof ArrayBuffer) transfer.push(u8.buffer);
170
+ }
171
+ self.postMessage({
172
+ t: 'done', id: msg.id,
173
+ res: { prefix: res.prefix, entry: res.entry, files },
174
+ ms: Date.now() - t0,
175
+ }, transfer);
176
+ return;
177
+ }
178
+ } catch (e) {
179
+ self.postMessage({
180
+ t: 'fail', id: msg.id || 0,
181
+ code: (e && e.code) || 'ULB_WORKER_FAIL',
182
+ message: String((e && e.message) || e).slice(0, 300),
183
+ });
184
+ }
185
+ };
@@ -0,0 +1,60 @@
1
+ // render/camera/constants.js —— 相机唯一常量来源
2
+ //
3
+ // 为什么单独放一个模块:缩放范围、档位步长、fit 边距这三个数**只允许在这里出现**。
4
+ // 任何逻辑文件里再写裸数字(0.125 / 16 / 0.1 / 0.05)都视为违规 ——
5
+ // 调范围时只改这一处,两套渲染(Spine / Live2D)才不会各自漂移。
6
+ // 数值来源:引擎 `SpineEngine.SCALE_MIN/SCALE_MAX/FIT_MARGIN`(= 0.125 / 16 / 0.05),
7
+ // 档位步长 0.1 对应引擎 `userScale *= (1 + t*0.1)` 的一档 ±10%。
8
+
9
+ // 面 → 相机覆盖值的键由元数据表给(不在这里写 kind 二分)
10
+ import { byKind } from '../surfaces.js';
11
+
12
+ /** 相机通用参数(Spine 与 Live2D 共用同一份) */
13
+ export const CAMERA = Object.freeze({
14
+ /** 最小缩放(12.5%) */
15
+ MIN: 0.125,
16
+ /** 最大缩放(1600%) */
17
+ MAX: 16,
18
+ /** 一档步长:scale *= 1 + dir * STEP(±10%) */
19
+ STEP: 0.1,
20
+ /** fit 边距:最终系数 = (1 - FIT_MARGIN) */
21
+ FIT_MARGIN: 0.05,
22
+ });
23
+
24
+ /**
25
+ * Live2D 专用覆盖值(默认空 ⇒ 与 CAMERA 完全一致)。
26
+ * 将来若需要为保护画质而限制 Live2D 的放大范围,**只在这里覆盖**,不改任何逻辑。
27
+ * 例:{ MAX: 8 } 表示 Live2D 最多 8×,其余仍用 CAMERA。
28
+ */
29
+ export const LIVE2D_OVERRIDE = Object.freeze({});
30
+
31
+ /**
32
+ * 主体包围盒排除名单(部件名不区分大小写、按子串匹配)。
33
+ * 用途:Live2D 没有骨骼/插槽语义,靠部件名剔除场景板与特效,逼近 Spine 的 `subject` 语义。
34
+ */
35
+ export const SUBJECT_EXCLUDE = Object.freeze(['bg', 'back', 'background', 'effect', 'mask', 'scene', 'kazari']);
36
+
37
+ /**
38
+ * 各面的相机覆盖值表(键 = 面元数据表的 cameraKind)。
39
+ * 加新面时若要限制它的缩放范围,在这里加一个键 —— 不必改 limitsFor 的判断。
40
+ */
41
+ const CAMERA_OVERRIDES = Object.freeze({
42
+ live2d: LIVE2D_OVERRIDE,
43
+ });
44
+
45
+ /**
46
+ * 取某个后端实际生效的 limits(CAMERA 叠加该面的覆盖值)。
47
+ *
48
+ * 覆盖关系查 render/surfaces.js 的 cameraKind,不再写 `kind === 'live2d'`:
49
+ * 未知 kind ⇒ 不叠加覆盖、直接用通用值。这里刻意**不抛错**:
50
+ * 相机范围有通用兜底,不像画布/面模式那样写错就错乱,所以"查不到覆盖表键"等同于"这个面没有特殊限制"。
51
+ * @param {'spine'|'live2d'} kind
52
+ * @returns {{min: number, max: number}}
53
+ */
54
+ export function limitsFor(kind) {
55
+ const meta = byKind(kind);
56
+ const override = meta ? (CAMERA_OVERRIDES[meta.cameraKind] || null) : null;
57
+ const min = override && typeof override.MIN === 'number' ? override.MIN : CAMERA.MIN;
58
+ const max = override && typeof override.MAX === 'number' ? override.MAX : CAMERA.MAX;
59
+ return { min, max };
60
+ }
@@ -0,0 +1,123 @@
1
+ // render/camera/index.js —— 相机统一入口
2
+ //
3
+ // 设计:外部只看到一套语义(档位 / 范围 / 锚点 / 复位 / 平移单位),差异全部收进 backend。
4
+ // · 共用(本文件唯一实现):`scale *= (1 + dir*STEP)`、clamp 到 limits、复位归 1、节流入口
5
+ // · backend 只负责「读/写状态 + 怎么落地」:
6
+ // getScale() / setScale(v) / pan(dx, dy) / resetView(keepScale)
7
+ // · fit 是能力差异(Spine 有三种模式,Live2D 只有主体包围盒),因此不进本文件;
8
+ // 由 backend 自己暴露 `fit?()`,渲染器在需要时调用。
9
+ //
10
+ // 数值一律取自 `./constants.js`(本文件不出现裸数字)。
11
+
12
+ import { CAMERA, limitsFor } from './constants.js';
13
+
14
+ /**
15
+ * 创建统一相机。
16
+ *
17
+ * @param {{
18
+ * kind?: 'spine'|'live2d',
19
+ * backend: {
20
+ * getScale: () => number|null,
21
+ * setScale: (v: number) => void,
22
+ * pan: (dx: number, dy: number) => void,
23
+ * resetView: (keepScale: boolean) => void,
24
+ * },
25
+ * log?: (message: string, level?: 'info'|'warn') => void,
26
+ * }} options
27
+ */
28
+ export function createCamera({ kind = 'spine', backend, log } = {}) {
29
+ if (!backend || typeof backend.getScale !== 'function' || typeof backend.setScale !== 'function') {
30
+ throw new Error('createCamera: backend 需要提供 getScale/setScale');
31
+ }
32
+ const limits = limitsFor(kind);
33
+
34
+ /** 统一日志出口(缺省静默) */
35
+ let logger = typeof log === 'function' ? log : null;
36
+ function note(message, level = 'info') {
37
+ if (logger) {
38
+ try { logger(message, level); } catch { /* 日志失败不影响相机 */ }
39
+ }
40
+ }
41
+
42
+ /** 夹取到 limits(唯一钳制点) */
43
+ function clamp(v) {
44
+ const n = Number(v);
45
+ if (!Number.isFinite(n)) return null;
46
+ return Math.min(limits.max, Math.max(limits.min, n));
47
+ }
48
+
49
+ /** 读当前缩放(backend 是唯一真相) */
50
+ function scale() {
51
+ try {
52
+ const v = backend.getScale();
53
+ return Number.isFinite(v) ? v : null;
54
+ } catch { return null; }
55
+ }
56
+
57
+ /** 写缩放(夹取后交给 backend) */
58
+ function zoomTo(value) {
59
+ const v = clamp(value);
60
+ if (v === null) return scale();
61
+ try { backend.setScale(v); } catch (err) { note(`setScale 失败:${err && err.message}`, 'warn'); }
62
+ return scale();
63
+ }
64
+
65
+ /**
66
+ * 缩放步进(与引擎 `userScale *= 1 + t*STEP` 完全一致)。
67
+ *
68
+ * `dir` 的**符号给方向、绝对值给档数**:交互层按帧合并滚轮事件时会把一次帧内的
69
+ * 多个 tick 累加成一个数传进来(如 +7),若只取符号就会把幅度丢掉——用户快速滚轮
70
+ * 反而只缩一档,手感明显回退。传 ±1 时行为与原来逐字一致。
71
+ * @param {number} dir 方向 + 档数:>0 放大、<0 缩小(|dir| 档)
72
+ */
73
+ function zoomStep(dir) {
74
+ const cur = scale();
75
+ if (!Number.isFinite(cur)) return null;
76
+ const sign = dir > 0 ? 1 : dir < 0 ? -1 : 0;
77
+ if (sign === 0) return cur;
78
+ const steps = Math.max(1, Math.trunc(Math.abs(Number(dir) || 1)));
79
+ const next = clamp(cur * (1 + sign * CAMERA.STEP) ** steps);
80
+ if (next === null) return cur;
81
+ try { backend.setScale(next); } catch (err) { note(`setScale 失败:${err && err.message}`, 'warn'); }
82
+ note(`zoomStep(${sign > 0 ? '+' : '-'}${steps}) ${cur.toFixed(4)} → ${next.toFixed(4)}(范围 ${limits.min}~${limits.max})`);
83
+ return scale();
84
+ }
85
+
86
+ /**
87
+ * 平移:参数单位固定为「屏幕像素增量」,内部乘子由 backend 决定。
88
+ * @param {number} dx
89
+ * @param {number} dy
90
+ */
91
+ function panBy(dx, dy) {
92
+ try { backend.pan(Number(dx) || 0, Number(dy) || 0); } catch (err) { note(`pan 失败:${err && err.message}`, 'warn'); }
93
+ return null;
94
+ }
95
+
96
+ /**
97
+ * 复位:先归 1(keepScale=false),再让 backend 重新适配。
98
+ * @param {{keepScale?: boolean}} [opts]
99
+ */
100
+ function reset(opts = {}) {
101
+ const keep = !!opts.keepScale;
102
+ if (!keep) {
103
+ try { backend.setScale(1); } catch { /* 忽略 */ }
104
+ }
105
+ try { backend.resetView(keep); } catch (err) { note(`resetView 失败:${err && err.message}`, 'warn'); }
106
+ note(`reset(keepScale=${keep})→ scale=${scale()}`);
107
+ return scale();
108
+ }
109
+
110
+ /** 设置日志出口(交互层在 boot 时挂上) */
111
+ function setLogger(fn) {
112
+ logger = typeof fn === 'function' ? fn : null;
113
+ }
114
+
115
+ /** 只读信息(排查/断言用) */
116
+ function info() {
117
+ return { kind, limits: { ...limits }, step: CAMERA.STEP, scale: scale() };
118
+ }
119
+
120
+ return { kind, zoomStep, zoomTo, panBy, reset, scale, setLogger, note, info };
121
+ }
122
+
123
+ export default createCamera;
@@ -0,0 +1,124 @@
1
+ // render/camera/spine.js —— 相机统一入口(Spine / Live2D 共用,全部委托引擎原生方法)
2
+ //
3
+ // 设计约束(重要):
4
+ // 1. 本文件**不做任何坐标换算**:没有 ppw、没有 ×2、没有锚点公式、没有 exp 曲线。
5
+ // 缩放档位、像素倍率、flip、Live2D 委托全部由引擎内部处理:
6
+ // engine.applyZoom(t) userScale *= (1 + t*0.1),clamp [SCALE_MIN, SCALE_MAX]
7
+ // engine.setCameraZoom(s) 直接设 userScale
8
+ // engine.applyPan(dx, dy) dx/dy 是屏幕像素差值,方向为「内容跟随手指」
9
+ // engine.resetCamera() 重新 fit(受 fitMode 影响)
10
+ // engine.getCameraZoom() 只读当前 userScale
11
+ // 2. 本文件**不判断 l2dActive()**:引擎三个方法开头已有 L2D 分支并委托 bridge,
12
+ // 在这里再写一份就会出现两套真相。
13
+ // 3. 所有动作都输出一条 CAMERA 日志(含输入侧),供真机操作时逐条核对。
14
+
15
+ /**
16
+ * @param {{getEngine: () => any}} options getEngine 每次调用时取当前引擎(MSAA 重建后引擎实例会换)
17
+ */
18
+ import { CAMERA, limitsFor } from './constants.js';
19
+
20
+ export function createStageCamera({ getEngine } = {}) {
21
+ /** 日志输出函数(由宿主注入,缺省静默) */
22
+ let logger = null;
23
+
24
+ /** 取可用引擎;没有相机能力时返回 null,调用方据此跳过 */
25
+ function engineOf() {
26
+ const engine = typeof getEngine === 'function' ? getEngine() : null;
27
+ return engine && typeof engine.getCameraZoom === 'function' ? engine : null;
28
+ }
29
+
30
+ /** 写一条 CAMERA 日志(日志失败不影响相机) */
31
+ /** panBy 逐帧日志的节流时间戳(毫秒) */
32
+ let lastPanLogAt = 0;
33
+
34
+ function emit(message, data) {
35
+ if (typeof logger !== 'function') return;
36
+ try { logger(message, data); } catch { /* 忽略 */ }
37
+ }
38
+
39
+ /** 读数包装:拿不到引擎时返回 null(而不是抛错) */
40
+ function readScale(engine) {
41
+ try { return Number(engine.getCameraZoom()); } catch { return null; }
42
+ }
43
+
44
+ const camera = {
45
+ /** 缩放一档。dir :滚轮向下 = -1,其余 = +1 */
46
+ zoomStep(dir) {
47
+ const engine = engineOf();
48
+ if (!engine) return null;
49
+ const before = readScale(engine);
50
+ const lim = limitsFor('spine');
51
+ // 唯一钳制点:先把目标值夹进范围,再交给引擎(引擎本身不再夹)
52
+ const target = Math.max(lim.min, Math.min(lim.max, (before == null ? 1 : before) * (1 + (dir > 0 ? 1 : -1) * CAMERA.STEP)));
53
+ try { engine.setCameraZoom(target); } catch { return before; }
54
+ const after = readScale(engine);
55
+ emit(`zoomStep(${dir > 0 ? '+1' : '-1'}) → userScale ${before} → ${after}`, { before, after });
56
+ return after;
57
+ },
58
+
59
+ /** 缩放到指定 userScale(捏合:起始 userScale × 距离比) */
60
+ zoomTo(scale) {
61
+ const engine = engineOf();
62
+ if (!engine || !Number.isFinite(scale)) return null;
63
+ const before = readScale(engine);
64
+ const lim = limitsFor('spine');
65
+ try { engine.setCameraZoom(Math.max(lim.min, Math.min(lim.max, scale))); } catch { return before; }
66
+ const after = readScale(engine);
67
+ emit(`zoomTo(${scale}) → userScale ${before} → ${after}`, { before, after });
68
+ return after;
69
+ },
70
+
71
+ /** 平移。dx/dy 是屏幕像素差值,原样交给引擎(倍率与 flip 由引擎处理) */
72
+ panBy(dx, dy) {
73
+ const engine = engineOf();
74
+ if (!engine || !Number.isFinite(dx) || !Number.isFinite(dy)) return null;
75
+ let posBefore = null;
76
+ try {
77
+ const cam = engine.camera;
78
+ if (cam && cam.position) posBefore = { x: cam.position.x, y: cam.position.y };
79
+ } catch { /* 读不到位置不影响平移 */ }
80
+ try { engine.applyPan(dx, dy); } catch { return posBefore; }
81
+ let posAfter = null;
82
+ try {
83
+ const cam = engine.camera;
84
+ if (cam && cam.position) posAfter = { x: cam.position.x, y: cam.position.y };
85
+ } catch { /* 同上 */ }
86
+ // 逐帧节流:一次拖动会调用几十次,全记会把日志刷满;这里 400ms 最多留一条过程样本,
87
+ // 整体位移由交互层抬手时汇总(drag total)。
88
+ const now = Date.now();
89
+ if (now - lastPanLogAt >= 400) {
90
+ lastPanLogAt = now;
91
+ emit(`panBy(${dx > 0 ? '+' : ''}${dx}, ${dy > 0 ? '+' : ''}${dy}) → position ${posBefore ? `(${posBefore.x.toFixed(2)},${posBefore.y.toFixed(2)})` : '?'} → ${posAfter ? `(${posAfter.x.toFixed(2)},${posAfter.y.toFixed(2)})` : '?'}`, { dx, dy, posBefore, posAfter });
92
+ }
93
+ return posAfter;
94
+ },
95
+
96
+ /** 视图复位:交给引擎重新 fit */
97
+ reset() {
98
+ const engine = engineOf();
99
+ if (!engine || typeof engine.resetCamera !== 'function') return null;
100
+ try { engine.resetCamera(); } catch { return readScale(engine); }
101
+ const scale = readScale(engine);
102
+ emit(`reset() → userScale ${scale}, fit 完成`, { scale });
103
+ return scale;
104
+ },
105
+
106
+ /** 只读当前 userScale */
107
+ scale() {
108
+ const engine = engineOf();
109
+ return engine ? readScale(engine) : null;
110
+ },
111
+
112
+ /** 写一条 CAMERA 日志(供交互层汇报输入侧动作,与上面同一路通道) */
113
+ note(message, data) {
114
+ emit(message, data);
115
+ },
116
+
117
+ /** 注入日志输出(宿主把 debug-log 接进来)。传 null 关闭 */
118
+ setLogger(fn) {
119
+ logger = typeof fn === 'function' ? fn : null;
120
+ },
121
+ };
122
+
123
+ return camera;
124
+ }
@@ -0,0 +1,30 @@
1
+ // render/capture.js —— 「导出截图」的唯一入口(与 reset-view.js / playback.js 同一口径)
2
+ //
3
+ // 为什么收口:截图以前在调用点写死 `#stage-canvas`(Spine 画布),
4
+ // Live2D 的画面在覆盖层自己的 canvas 上,结果在 Live2D 面导出的是"上次 Spine 的残留帧"。
5
+ // 现在调用方只说"给我当前面的画面",由渲染器自己知道该从哪取像素。
6
+
7
+ /**
8
+ * 取当前渲染面的画面。
9
+ * @param {object|null} surface 舞台表面(提供 activeRenderer())
10
+ * @returns {{canvas: HTMLCanvasElement, width: number, height: number, source: string}|null}
11
+ * source 标识实际通路:'renderer.capture' / 'none' / 'no-renderer'
12
+ */
13
+ export function captureActive(surface) {
14
+ const renderer = surface && typeof surface.activeRenderer === 'function' ? surface.activeRenderer() : null;
15
+ if (!renderer) return null;
16
+ if (typeof renderer.capture !== 'function') return null;
17
+ try {
18
+ const shot = renderer.capture();
19
+ if (!shot || !shot.canvas || !shot.width || !shot.height) return null;
20
+ return {
21
+ canvas: shot.canvas, width: shot.width, height: shot.height,
22
+ source: 'renderer.capture',
23
+ digest: shot.digest || 0,
24
+ // nonEmpty 向后兼容(老断言用它判"不是空白图"):渲染器给了就用,否则用指纹兜底
25
+ nonEmpty: typeof shot.nonEmpty === 'number' ? shot.nonEmpty : (shot.digest ? 1 : 0),
26
+ };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }