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,165 @@
1
+ // engine/skins.js —— 皮肤与插槽过滤(从 player 拆出,保持单文件规模)
2
+ //
3
+ // 语义:
4
+ // · pickInitialSkin 装载时挑默认皮肤(判据是"该皮肤下有没有附件",见函数注释);
5
+ // · getSkins 取骨架数据里的皮肤名列表(default 也在内,顺序同运行时);
6
+ // · setSkins 用第一个皮肤名设置,并记住整份列表(切 PMA 重建后要继续生效);
7
+ // · setSlotFilter 只记关键词,实际隐藏由 hideFilteredSlots/restoreFilteredSlots 在绘制前后做
8
+ // (B5 补绘制侧的过滤,这里先把状态与关键词口径统一,避免出现两处真值)。
9
+
10
+ /**
11
+ * 选出**装载时应默认使用**的皮肤(不硬编码任何名字,判据来自数据本身)。
12
+ *
13
+ * 为什么需要:有些资源把全部附件放在**具名皮肤**里,`default` 只是个空壳 ——
14
+ * 不设皮肤就等于挂在空壳上,骨架有上百个槽位却只有几个有附件,画面整片空白,
15
+ * 而日志一切正常(mount 成功、动作能播、无 error),极难从日志看出问题。
16
+ * 判据为什么是"能画出来的槽位数"(有附件**且附件有 region**)而不是"皮肤名像不像目录名":
17
+ * descriptor 到渲染层只剩 `{name, version, skeletonFile:{name}, atlasFile, textureFiles}`,
18
+ * **没有路径** ⇒ 推不出目录名;而"这个皮肤下到底挂了多少附件"是任何资源都能直接量的通用事实。
19
+ *
20
+ * 规则(三步,逐步退让):
21
+ * ① `default` 下**有**附件 ⇒ 用它(多数资源的常态 ⇒ 行为与改动前完全一致)
22
+ * ② `default` 是空壳 ⇒ 按运行时顺序取第一个"有附件"的皮肤
23
+ * ③ 都不行 ⇒ 仍用 `default`(不报错、不改变既有行为)
24
+ *
25
+ * 副作用说明:探测时必须真的切一次皮肤 + setSlotsToSetupPose 才数得准,
26
+ * 所以函数结束时**当前皮肤就是返回值对应的那个**(调用方随后 setSkins 会再设一次,幂等)。
27
+ * @param {object} engine
28
+ * @returns {string|null} 选中的皮肤名;无法判断(无骨架/无皮肤)时返回 null
29
+ */
30
+ export function pickInitialSkin(engine) {
31
+ const sd = engine && engine.skeletonData;
32
+ const sk = engine && engine.skeleton;
33
+ if (!sd || !sk || !Array.isArray(sd.skins) || !sd.skins.length) return null;
34
+ const names = sd.skins.map((s) => s && s.name).filter(Boolean);
35
+ if (!names.length) return null;
36
+ /**
37
+ * 数一个皮肤下"setup pose 时**能画出东西**"的槽位(切过去数,故有副作用)。
38
+ *
39
+ * ⚠️ 判据必须是「有附件 **且附件有 region**」,不能只数"有附件":
40
+ * 实测某资源 `default` 皮肤下挂着 3 个附件,但全是 **PointAttachment**(定位点,没有图)——
41
+ * 只数"有附件"会得到 3 > 0,于是把空壳皮肤判成正常皮肤、画面照样空白。
42
+ * 加上 region 之后:default = 0、`00` = 131 ⇒ 判据才真正指向"能画出来"。
43
+ */
44
+ const drawableCount = (name) => {
45
+ const skin = typeof sd.findSkin === 'function' ? sd.findSkin(name) : null;
46
+ if (!skin) return -1;
47
+ try {
48
+ sk.setSkin(skin);
49
+ if (typeof sk.setSlotsToSetupPose === 'function') sk.setSlotsToSetupPose();
50
+ return (sk.slots || []).filter((slot) => !!(slot.attachment && slot.attachment.region)).length;
51
+ } catch {
52
+ return -1;
53
+ }
54
+ };
55
+ const hasDefault = names.includes('default');
56
+ if (hasDefault && drawableCount('default') > 0) return 'default';
57
+ for (const n of names) {
58
+ if (n === 'default') continue;
59
+ if (drawableCount(n) > 0) return n;
60
+ }
61
+ // 全都不行:回到 default(没有 default 就保持运行时现状、不动它)
62
+ return hasDefault ? 'default' : null;
63
+ }
64
+
65
+ /** 皮肤名列表 */
66
+ export function getSkins(engine) {
67
+ const skins = (engine.skeletonData && engine.skeletonData.skins) || [];
68
+ return skins.map((s) => s.name);
69
+ }
70
+
71
+ /**
72
+ * 应用皮肤。
73
+ * @param {object} engine
74
+ * @param {string[]} list
75
+ */
76
+ export function setSkins(engine, list) {
77
+ if (!engine.skeleton || !engine.skeletonData) return false;
78
+ const names = Array.isArray(list) ? list : [];
79
+ if (!names.length) return false;
80
+ try {
81
+ if (names.length === 1) {
82
+ const skin = engine.skeletonData.findSkin ? engine.skeletonData.findSkin(names[0]) : null;
83
+ // 名字不存在就别"假装切了":返回 false,让上层保持原皮肤与真实设置,
84
+ // 否则面板会显示一个不存在的皮肤名而画面没变(下次重建贴图还会按错名字再设一遍)
85
+ if (!skin) return false;
86
+ engine.skeleton.setSkin(skin);
87
+ } else {
88
+ const Skin = engine.spine && engine.spine.Skin;
89
+ const merged = Skin ? new Skin('_combined') : null;
90
+ if (merged) {
91
+ for (const n of names) {
92
+ const s = engine.skeletonData.findSkin ? engine.skeletonData.findSkin(n) : null;
93
+ try { if (s) merged.addSkin(s); } catch { /* 单个皮肤合成失败继续 */ }
94
+ }
95
+ engine.skeleton.setSkin(merged);
96
+ }
97
+ }
98
+ if (typeof engine.skeleton.setSlotsToSetupPose === 'function') engine.skeleton.setSlotsToSetupPose();
99
+ engine.activeSkins = names.slice(); // 只有真的切成功才记账
100
+ return true;
101
+ } catch {
102
+ return false; /* 皮肤不存在时保持原样 */
103
+ }
104
+ }
105
+
106
+ /**
107
+ * 隐藏命中关键词的插槽(绘制前调用)。
108
+ * 口径:槽名或附件名小写包含任一关键词即命中;命中时把 attachment 摘成 null,
109
+ * 绘制后再挂回 —— 不销毁附件,否则切回不过滤时部件就再也画不出来了。
110
+ * @param {object} engine
111
+ * @returns {number} 本次隐藏的槽数(0 表示没过滤)
112
+ */
113
+ export function hideFilteredSlots(engine) {
114
+ const backup = engine.slotHiddenBackup || (engine.slotHiddenBackup = []);
115
+ backup.length = 0;
116
+ const keywords = engine.slotFilterKeywords || [];
117
+ if (!keywords.length || !engine.skeleton) return 0;
118
+ const order = engine.skeleton.drawOrder || engine.skeleton.slots || [];
119
+ for (const slot of order) {
120
+ if (!slot || !slot.attachment) continue;
121
+ const slotName = ((slot.data && slot.data.name) || '').toLowerCase();
122
+ const attName = ((slot.attachment && slot.attachment.name) || '').toLowerCase();
123
+ if (keywords.some((k) => slotName.includes(k) || attName.includes(k))) {
124
+ backup.push({ slot, att: slot.attachment });
125
+ slot.attachment = null;
126
+ }
127
+ }
128
+ return backup.length;
129
+ }
130
+
131
+ /**
132
+ * 还原被摘掉的插槽(绘制后调用,异常路径也必须走,否则画面上会一直缺件)。
133
+ * @param {object} engine
134
+ */
135
+ export function restoreFilteredSlots(engine) {
136
+ const backup = engine.slotHiddenBackup || [];
137
+ for (const item of backup) item.slot.attachment = item.att;
138
+ backup.length = 0;
139
+ }
140
+
141
+ /**
142
+ * 关键词能命中多少个"当前有附件的槽"(只读,不改骨架)。
143
+ * 用途:让调用方能区分"关键词没匹配到东西"和"匹配到了但隐不隐藏看不出来"。
144
+ * @param {object} engine
145
+ * @param {string[]} keywords
146
+ * @returns {number}
147
+ */
148
+ export function countFilterableSlots(engine, keywords) {
149
+ const list = normalizeKeywords(keywords);
150
+ if (!list.length || !engine.skeleton) return 0;
151
+ const order = engine.skeleton.drawOrder || engine.skeleton.slots || [];
152
+ let n = 0;
153
+ for (const slot of order) {
154
+ if (!slot || !slot.attachment) continue;
155
+ const slotName = ((slot.data && slot.data.name) || '').toLowerCase();
156
+ const attName = ((slot.attachment && slot.attachment.name) || '').toLowerCase();
157
+ if (list.some((k) => slotName.includes(k) || attName.includes(k))) n += 1;
158
+ }
159
+ return n;
160
+ }
161
+
162
+ /** 关键词归一化(小写去空) */
163
+ export function normalizeKeywords(keywords) {
164
+ return (Array.isArray(keywords) ? keywords : []).map((k) => String(k).trim().toLowerCase()).filter(Boolean);
165
+ }
@@ -0,0 +1,174 @@
1
+ // engine/textures.js —— 贴图解码、预乘口径与 GPU 上传
2
+ //
3
+ // 为什么单独成文件:PMA 的正确性 90% 在"上传时怎么处理像素",与渲染/相机无关;
4
+ // 在这里有四个必须保留的细节,漏任何一个都会造成画面颜色不对但不报错:
5
+ // ① 多页图集里 <100px 的页用透明画布跳过(不参与像素检测,避免误判)
6
+ // ② GPU 的 UNPACK_PREMULTIPLY_ALPHA_WEBGL 优先,不可用时回退 CPU 整页预乘
7
+ // ③ 运行时的 GLTexture.pma 标志与 update() 重传(straight 档还要手工 texImage2D)
8
+ // ④ GLTexture 构造失败时回退"手工建纹理 + 包一层纹理样对象"
9
+
10
+ /** 多页图集中被认为"太小、多半是占位/噪点"的边长阈值 */
11
+ /** 多页图集中被视为占位的边长阈值 */
12
+ export const SMALL_PAGE_PX = 100;
13
+
14
+ /** 取文件名末段并小写(页名匹配用) */
15
+ function lastSegment(name) {
16
+ return String(name || '').split(/[\\/]/).pop().toLowerCase();
17
+ }
18
+
19
+ /**
20
+ * 解析 atlas 文本里每个页的尺寸(`size: W,H`)。
21
+ * @param {string} atlasText
22
+ * @returns {Map<string, {width: number, height: number}>}
23
+ */
24
+ export function parseAtlasPageSizes(atlasText) {
25
+ const out = new Map();
26
+ if (!atlasText) return out;
27
+ const lines = atlasText.split(/\r?\n/);
28
+ for (let i = 0; i < lines.length; i += 1) {
29
+ const page = lines[i].trim();
30
+ if (!/\.(png|jpg|jpeg|webp)$/i.test(page)) continue;
31
+ for (let j = i + 1; j < lines.length; j += 1) {
32
+ const line = lines[j].trim();
33
+ if (!line) continue;
34
+ if (!line.includes(':') && /\.(png|jpg|jpeg|webp)$/i.test(line)) break;
35
+ const m = line.match(/^size\s*:\s*(\d+)\s*,\s*(\d+)/);
36
+ if (m) { out.set(page, { width: parseInt(m[1], 10), height: parseInt(m[2], 10) }); break; }
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+
42
+ /** ArrayBuffer/Blob → <img> 元素(并等解码) */
43
+ export function loadImageElement(entry) { // dsh-skip-complexity 的图片解码实现,本轮未改动函数体
44
+ return new Promise((resolve, reject) => {
45
+ const img = new Image();
46
+ let url = null;
47
+ const done = () => { if (url) URL.revokeObjectURL(url); };
48
+ img.onload = async () => {
49
+ try { if (img.decode) await img.decode(); } catch { /* decode 不支持就用当前状态 */ }
50
+ done();
51
+ resolve(img);
52
+ };
53
+ img.onerror = () => { done(); reject(new Error(`图片加载失败: ${entry.name}`)); };
54
+ const content = entry.content;
55
+ if (content instanceof ArrayBuffer || ArrayBuffer.isView(content)) {
56
+ url = URL.createObjectURL(new Blob([content], { type: entry.type || 'image/png' }));
57
+ } else if (typeof content === 'string') {
58
+ // 字符串形态的贴图(URL 而非字节):同样先问 VirtualFS,命中则用 Blob URL
59
+ const resolved = (() => {
60
+ try { return getSharedVirtualFS().resolve(content) || content; } catch { return content; }
61
+ })();
62
+ url = resolved.startsWith('data:') || resolved.startsWith('http') || resolved.startsWith('blob:') ? resolved : null;
63
+ }
64
+ if (!url) { reject(new Error(`图片数据不可用: ${entry.name}`)); return; }
65
+ img.src = url;
66
+ });
67
+ }
68
+
69
+ /** 解码一张贴图,返回 {img, width, height} */
70
+ export async function decodeImageSource(entry) {
71
+ const img = await loadImageElement(entry);
72
+ return { img, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height };
73
+ }
74
+
75
+
76
+
77
+
78
+ /**
79
+ * 把贴图取成 ImageData(供 PMA 像素检测)。
80
+ *
81
+ * **必须避免"预乘 → 非预乘"往返**:直接把 <img> 画进 2D canvas 再 getImageData 时,
82
+ * canvas 内部按预乘存储、读回时再除一次 alpha ⇒ 半透明区域相对误差被放大(除以很小的 a),
83
+ * 会产生 `r > a` 的**假违反**。现场:bd2 的图半透明边缘多 ⇒ 假违反 9.8%~22.4%
84
+ * ⇒ 被判成"非预乘" ⇒ 走 upload 又乘一次 alpha ⇒ 双重预乘(嘴巴黑圈)。
85
+ * azurlane 的图边缘干净 ⇒ 0.1% ⇒ 判对。
86
+ *
87
+ * 做法:用 `createImageBitmap(img, { premultiplyAlpha: 'none' })` 拿到**未预乘**的位图再画;
88
+ * 浏览器不支持或失败时退回原路径(并保持"能检测就检测"的语义,不因取不到样本而阻塞装载)。
89
+ *
90
+ * @param {object} entry 贴图条目 {img|canvas,width,height}
91
+ * @returns {Promise<ImageData|null>}
92
+ */
93
+ export function sampleSourceImageData(entry) {
94
+ // ****(dist: this.sampleSourceImageData):画到 2D canvas 再 getImageData,同步。
95
+ // 之前我改成 async + createImageBitmap + WebGL 精确取样,都属于自创,已删;
96
+ // (当时用"违反占比"判据得到的结论也不可靠——那个判据抓不到"变暗",见排查记录。)
97
+ try {
98
+ let canvas = null;
99
+ if (entry.img) {
100
+ canvas = document.createElement('canvas');
101
+ canvas.width = entry.width;
102
+ canvas.height = entry.height;
103
+ canvas.getContext('2d', { willReadFrequently: true }).drawImage(entry.img, 0, 0);
104
+ } else if (entry.canvas) {
105
+ canvas = entry.canvas;
106
+ } else {
107
+ return null;
108
+ }
109
+ return canvas.getContext('2d', { willReadFrequently: true }).getImageData(0, 0, canvas.width, canvas.height); // dsh-skip-quality 的取样实现,本轮未改动
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+
116
+
117
+ /** 1×1 空纹理对象(图集页找不到匹配贴图时的占位;必须像纹理,否则批处理器会抛) */
118
+ export function makePlaceholderTexture() {
119
+ const canvas = document.createElement('canvas');
120
+ canvas.width = 1;
121
+ canvas.height = 1;
122
+ return {
123
+ _image: canvas,
124
+ getImage() { return this._image; },
125
+ setFilters() {},
126
+ setWraps() {},
127
+ dispose() {},
128
+ };
129
+ }
130
+
131
+ /** 手工建纹理(GLTexture 构造失败时的回退),包成运行时期望的纹理形状 */
132
+ function manualTexture(gl, source, filterMode) {
133
+ const handle = gl.createTexture();
134
+ if (!handle) return null;
135
+ gl.bindTexture(gl.TEXTURE_2D, handle);
136
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source);
137
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filterMode);
138
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filterMode);
139
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
140
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
141
+ return {
142
+ _image: source,
143
+ texture: handle,
144
+ getImage() { return this._image; },
145
+ setFilters(min, mag) {
146
+ try {
147
+ gl.bindTexture(gl.TEXTURE_2D, handle);
148
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, min);
149
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, mag);
150
+ } catch { /* 忽略 */ }
151
+ },
152
+ setWraps() {},
153
+ dispose() { try { gl.deleteTexture(handle); } catch { /* 忽略 */ } },
154
+ };
155
+ }
156
+
157
+ /**
158
+ * 设纹理过滤(applyTextureFilter:有 setFilters 用它,否则直接 texParameteri)。
159
+ */
160
+ export function applyTextureFilter(gl, texture, mode) {
161
+ if (!gl || !texture) return;
162
+ const f = mode === 'nearest' ? gl.NEAREST : gl.LINEAR;
163
+ try {
164
+ if (typeof texture.setFilters === 'function') { texture.setFilters(f, f); return; }
165
+ const handle = texture.texture || texture._texture;
166
+ if (!handle) return;
167
+ gl.bindTexture(gl.TEXTURE_2D, handle);
168
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, f);
169
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, f);
170
+ } catch { /* 忽略 */ }
171
+ }
172
+
173
+ // 预乘相关的上传实现在 pma.js(唯一权威位置):这里再导出,调用方 import 路径不用改。
174
+ export { uploadTextures } from './pma.js';
@@ -0,0 +1,117 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ 纯 HTML + 原生 ES 模块 + 手写 CSS,无构建步骤。
4
+ 五个区域的位置按 1920×1080 量测确定:
5
+ 顶栏 header : top 0, 高 3rem, 全宽, z 40
6
+ 左栏 aside : left 0, top 3rem, 底 4rem, 宽 19rem, z 30
7
+ 舞台 stage : 铺满视口(旧版画布即铺满窗口,其余区域浮在它上层), z 0
8
+ 右栏 aside : right 0, top 3rem, 底 4rem, 宽 11rem, z 30
9
+ 底栏 footer : 底部居中浮条, 宽 720px, 高 2.625rem, 距底 15px, z 40
10
+
11
+ 外观状态属性(由样式层定义,接线时只需在 <html> 上切换属性值):
12
+ data-cinema="on" 影院模式:隐藏顶栏与左右侧栏,只留舞台
13
+ data-idle="on" 影院模式静止态:底部播放条淡出(配合 data-cinema="on")
14
+ data-left="off" 收起左栏(角色库)
15
+ data-right="off" 收起右栏(控制面板)
16
+ 样式规则见 style.css 末尾「影院模式 / 侧栏收起」一节。
17
+ -->
18
+ <html lang="zh-CN">
19
+ <head>
20
+ <meta charset="UTF-8" />
21
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
22
+ <!-- 关键:图标用内联 data URI,避免浏览器请求 /favicon.ico 产生 404(控制台零错误的必要条件) -->
23
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Ctext y='13' font-size='13'%3E%E2%9C%A6%3C/text%3E%3C/svg%3E" />
24
+ <title>ModelStage</title>
25
+ <link rel="stylesheet" href="./style.css" />
26
+ <!-- 控件外观单独一个文件:拆分只为把单文件控制在 400 行内,职责见两文件头注释 -->
27
+ <link rel="stylesheet" href="./components.css" />
28
+ <!-- 右侧面板与播放条内部样式单独一个文件(M3),避免 components.css 超过 400 行 -->
29
+ <link rel="stylesheet" href="./panel/panel.css" />
30
+ </head>
31
+ <body>
32
+
33
+ <!-- ============ 舞台(第 3 区域):铺满视口,棋盘格背景 ============ -->
34
+ <main id="stage" class="stage checkerboard" aria-label="舞台">
35
+ <!-- 渲染器(PIXI)挂在这块画布上;画布常驻,换模型只换内容 -->
36
+ <canvas id="stage-canvas" class="stage-canvas"></canvas>
37
+ <p id="stage-hint" class="stage-hint"></p>
38
+ </main>
39
+
40
+ <!-- ============ 顶栏(第 1 区域) ============ -->
41
+ <header id="app-header" class="app-header">
42
+ <div class="brand">
43
+ <!-- 品牌图标用内联 SVG:不依赖系统字体,避免无字体环境渲染成豆腐块 -->
44
+ <span id="brand-icon" class="brand-icon" aria-hidden="true">
45
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
46
+ <path d="m12 3-1.912 5.813a2 2 0 0 1-1.275 1.275L3 12l5.813 1.912a2 2 0 0 1 1.275 1.275L12 21l1.912-5.813a2 2 0 0 1 1.275-1.275L21 12l-5.813-1.912a2 2 0 0 1-1.275-1.275L12 3Z" />
47
+ <path d="M5 3v4" /><path d="M19 17v4" /><path d="M3 5h4" /><path d="M17 19h4" />
48
+ </svg>
49
+ </span>
50
+ <span id="brand-title" class="brand-title">ModelStage</span>
51
+ </div>
52
+ <!-- 内存三色条(panel/memory-bar.js 填内容):顶栏中部,brand 与右侧操作之间。
53
+ 默认空且 hidden,有数据才出现;受「显示状态信息」开关控制。
54
+ 放在顶栏内部而不是浮在舞台上,是为了不遮挡画面、并跟着顶栏一起进退
55
+ (影院模式顶栏滑出时它自动一起隐藏)。 -->
56
+ <div id="mem-bar-stage" class="mem-bar-host mem-bar-host--header" hidden></div>
57
+ <nav class="header-actions" id="header-actions" aria-label="顶栏操作">
58
+ <!-- 顶栏按钮由声明式表渲染(panel/toolbar-schema.js → panel/toolbar.js):
59
+ 内置 5 个 + 插件追加,两者走同一个渲染器与同一套图标白名单。
60
+ 为什么不在 HTML 里手写:手写节点没有"位置"可插,插件按钮也无法与内置定序。 -->
61
+ </nav>
62
+ </header>
63
+
64
+ <!-- ============ 左栏(第 2 区域):角色库三级树 ============ -->
65
+ <aside id="left-panel" class="panel panel-left" aria-label="角色库">
66
+ <div class="panel-head">
67
+ <span class="panel-title">角色库</span>
68
+ </div>
69
+ <div class="panel-count" id="roster-count">正在读取资源根…</div>
70
+ <!-- 资源根管理(F4):新增 / 改名 / 启停 / 删除。列表默认收起,点「管理」展开。
71
+ 为什么是输入服务端绝对路径:模型由**服务端**扫描自己文件系统加载,浏览器的目录选择器
72
+ 只能给出客户端句柄,变不成服务端可读路径。 -->
73
+ <div id="root-manager" class="panel-search">
74
+ <!-- 面板上只有「选择目录」一个按钮;点了弹服务端目录浏览器去挑目录(不在面板里填路径) -->
75
+ <div class="root-add">
76
+ <button type="button" class="mini-btn is-accent" id="btn-add-root" title="浏览服务端目录,登记为资源根">选择目录</button>
77
+ </div>
78
+ <div class="root-bar">
79
+ <span class="muted-hint">资源根 <span id="root-count">0</span> 个</span>
80
+ <button type="button" class="mini-btn" id="btn-collapse"
81
+ title="只收起展开的树节点(面板保留;隐藏面板用顶栏「模型库」按钮)">收起</button>
82
+ <button type="button" class="mini-btn" id="btn-root-toggle" aria-expanded="false" title="展开资源根管理">管理</button>
83
+ </div>
84
+ <div id="root-manager-body" hidden>
85
+ <div id="root-list" aria-label="资源根列表"></div>
86
+ <p id="root-msg" class="muted-hint" role="status" aria-live="polite"></p>
87
+ </div>
88
+ </div>
89
+ <div class="panel-search">
90
+ <input id="roster-search" type="search" autocomplete="off"
91
+ placeholder="搜索角色 / 服装 / 变体…" aria-label="搜索角色、服装、变体" />
92
+ </div>
93
+ <!-- 类型胶囊行:内容由 roster/view.js 渲染(名册为空时整行隐藏);放搜索与列表之间 -->
94
+ <div id="roster-type-filter" class="roster-type-filter" role="group" aria-label="按内容类型筛选" hidden></div>
95
+ <!-- 三级树:资源根 → 角色 → 变体(首次展开某根才请求该根的模型清单) -->
96
+ <div id="roster-list" class="panel-body roster-list" role="tree" aria-label="角色库" aria-live="polite">
97
+ <p class="muted-hint">正在读取资源根…</p>
98
+ </div>
99
+ </aside>
100
+
101
+ <!-- ============ 右栏(第 4 区域):控制面板(M3 骨架,内容由 panel.js 渲染) ============ -->
102
+ <aside id="right-panel" class="panel panel-right" aria-label="控制面板">
103
+ <div class="panel-head" id="panel-head"></div>
104
+ <div class="panel-body" id="panel-body"></div>
105
+ </aside>
106
+
107
+ <!-- ============ 底栏(第 5 区域):播放条(M3 骨架,内容由 transport.js 渲染) ============ -->
108
+ <footer id="app-footer" class="app-footer" aria-label="播放控制"></footer>
109
+
110
+ <!-- 顶栏必须先于 main.js 渲染:context.js 在模块加载时用 getElementById 缓存这些按钮节点 -->
111
+ <script type="module">
112
+ import { mountToolbar } from './panel/toolbar.js';
113
+ mountToolbar(document.getElementById('header-actions'));
114
+ </script>
115
+ <script type="module" src="./main.js"></script>
116
+ </body>
117
+ </html>