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,248 @@
1
+ // roster-rows.js —— 名册三级树的「行」渲染(纯 DOM 工厂,无状态)(M2)
2
+ //
3
+ // 做什么:把一条树数据渲染成一个可点击的 treeitem 行,三级别各一个函数。
4
+ // 为什么单独一个文件:
5
+ // 1. 单文件 ≤400 行的硬约束(视图逻辑 + 行结构放一起会超);
6
+ // 2. 职责分离:本文件只管"一行长什么样",roster-view.js 只管"状态怎么变、事件怎么派"。
7
+ // 约定:每行都带 data-* 标识与 tabindex=0(键盘可达),事件由 roster-view.js 在容器上委托。
8
+
9
+ import { variantLabel, variantText } from './list.js';
10
+
11
+ // el 的唯一实现在 infra/dom.js。
12
+ // 本文件自己要用它(rowBase 等 15 处),所以必须 import 绑定到本地作用域;
13
+ // 同时对外转发导出,roster/view.js 的 `import { el, ... } from './rows.js'` 无需改动。
14
+ // 注意:只写 `export { el } from '...'` 是**不绑定本地**的 —— 那些内部调用会变成 undefined。
15
+ import { el } from '../infra/dom.js';
16
+ // 名册徽标的配色类来自面元数据表(badge 字段)
17
+ import { byKind } from '../render/surfaces.js';
18
+
19
+ export { el };
20
+
21
+ /** 建一个树行骨架(三级别共用:data 属性 + treeitem 语义 + 键盘可达) */
22
+ function rowBase(className, dataset) {
23
+ const row = el('div', `tree-row ${className}`);
24
+ Object.assign(row.dataset, dataset);
25
+ row.setAttribute('role', 'treeitem');
26
+ row.tabIndex = 0; // 键盘可达:Enter/空格与点击等价
27
+ return row;
28
+ }
29
+
30
+ /**
31
+ * 展开箭头:内联 SVG 而不是 ▸/▾ 文字。
32
+ * 为什么不用文字:无桌面字体环境(无头 chromium / 精简系统)里 ▸ ▾ 会渲染成豆腐块,
33
+ * 而它是"能不能展开"的唯一视觉提示,必须稳定可见。
34
+ * @param {boolean} open
35
+ */
36
+ function arrowEl(open) {
37
+ const NS = 'http://www.w3.org/2000/svg';
38
+ const svg = document.createElementNS(NS, 'svg');
39
+ svg.setAttribute('viewBox', '0 0 16 16');
40
+ svg.setAttribute('class', 'tree-arrow');
41
+ svg.setAttribute('aria-hidden', 'true');
42
+ const path = document.createElementNS(NS, 'path');
43
+ path.setAttribute('d', open ? 'M4 6l4 4 4-4' : 'M6 4l4 4-4 4');
44
+ path.setAttribute('fill', 'none');
45
+ path.setAttribute('stroke', 'currentColor');
46
+ path.setAttribute('stroke-width', '2');
47
+ path.setAttribute('stroke-linecap', 'round');
48
+ path.setAttribute('stroke-linejoin', 'round');
49
+ svg.append(path);
50
+ return svg;
51
+ }
52
+
53
+ /**
54
+ * 一级:游戏行(按资源根配置的 name 合并后的节点)。
55
+ * 一个游戏可能由多个资源根供给(碧蓝航线 = 3 个根),所以徽标显示根数量、title 列出具体根 id,
56
+ * 数据属性 data-root-ids 供自动化断言核对"这些根都在同一个游戏节点下"。
57
+ * @param {{key: string, name: string, roots: object[]}} game
58
+ * @param {{open: boolean, status: string, charCount: number}} view
59
+ */
60
+ export function renderGameRow(game, view) {
61
+ const rootIds = game.roots.map((r) => r.id);
62
+ const row = rowBase('tree-game', { gameKey: game.key, rootIds: rootIds.join(',') });
63
+ row.setAttribute('aria-expanded', String(view.open));
64
+ if (view.open) row.classList.add('is-open');
65
+
66
+ row.append(arrowEl(view.open));
67
+ row.append(el('span', 'tree-game-name', game.name));
68
+ // 多根时标注根数(单根不标注,避免噪音)
69
+ if (rootIds.length > 1) {
70
+ const badge = el('span', 'tree-badge', `${rootIds.length} 根`);
71
+ badge.title = rootIds.join(' · ');
72
+ row.append(badge);
73
+ } else {
74
+ row.append(el('span', 'tree-badge', rootIds[0] || ''));
75
+ }
76
+
77
+ if (view.status === 'ready') row.append(el('span', 'tree-count', `${view.charCount} 角色`));
78
+ else if (view.status === 'loading') row.append(el('span', 'tree-count', '加载中…'));
79
+ return row;
80
+ }
81
+
82
+ /**
83
+ * 二级:角色行(头像 + 显示名 + 原名标签 + 变体数)。
84
+ * @param {object} game
85
+ * @param {object} c 角色(title/baseName/iconUrl/variants)
86
+ * @param {boolean} open
87
+ */
88
+ export function renderCharRow(game, c, open) {
89
+ const row = rowBase('tree-char', { charKey: c.key, gameKey: game.key });
90
+ row.setAttribute('aria-expanded', String(open));
91
+ if (open) row.classList.add('is-open');
92
+
93
+ row.append(arrowEl(open));
94
+
95
+ if (c.iconUrl) {
96
+ const img = document.createElement('img');
97
+ img.className = 'tree-icon';
98
+ img.src = c.iconUrl;
99
+ img.alt = '';
100
+ img.loading = 'lazy';
101
+ img.decoding = 'async';
102
+ row.append(img);
103
+ } else {
104
+ row.append(el('span', 'tree-icon'));
105
+ }
106
+
107
+ row.append(el('span', 'tree-char-title', c.title));
108
+ // 「原名」标签:归一化键与显示名不同才有信息增量(照 char-tree.js 的去重规则)
109
+ if (c.baseName && c.baseName !== String(c.title)) row.append(el('span', 'tree-orig', c.baseName));
110
+ row.append(el('span', 'tree-count', c.variants.length));
111
+ return row;
112
+ }
113
+
114
+ /**
115
+ * 三级:变体行(类型标签 + 服装名/目录名 + 原名后缀)。
116
+ * 行上带 data-root-id:证明同一角色节点下确实聚合了来自不同资源根的变体。
117
+ * @param {object} game
118
+ * @param {object} c
119
+ * @param {{model: object, nm: object|null}} v
120
+ * @param {string|null} selectedId 当前选中的模型 id
121
+ */
122
+ export function renderVariantRow(game, c, v, selectedId) {
123
+ const m = v.model;
124
+ const { text, origName } = variantText(m, v.nm);
125
+ const row = rowBase('tree-variant', {
126
+ modelId: m.id, gameKey: game.key, charKey: c.key, rootId: m.rootId || '',
127
+ });
128
+
129
+ const isSelected = selectedId === m.id;
130
+ if (isSelected) {
131
+ row.classList.add('is-selected');
132
+ row.setAttribute('aria-selected', 'true');
133
+ }
134
+
135
+ const label = variantLabel(m);
136
+ // 样式类只用来分组(容器单文件用 bundle 类),**不带颜色**。
137
+ // 颜色/归属/可装载性/失败原因全部来自服务端三字段(契约 §一 INV-D1):
138
+ // display.accent 徽标底色(null ⇒ 中性色)
139
+ // display.provider 归属提供者 id(展示名按 /api/plugins 的 label 查)
140
+ // display.candidates 候选提供者(按 matchPriority 降序)
141
+ // support.state ready | unknown | unsupported
142
+ // support.detail 不支持时的 { reason, message }(与装载失败文案同源)
143
+ // loadResult 最近一次真实装载尝试的结果
144
+ // 前端**不做任何判定**:不按 kind 猜色、不自己算"能不能装"、不读 localStorage。
145
+ const meta = byKind(m.kind);
146
+ const kindClass = m.container ? 'bundle' : (meta ? meta.badge : 'spine');
147
+ const typeBadge = el('span', `tree-type tree-type-${kindClass}`, label);
148
+ const display = (m && m.display) || {};
149
+ if (display.accent) typeBadge.style.setProperty('--badge-accent', String(display.accent));
150
+ // 悬浮文案:归属定了说"谁",没定就把候选列出来(用户悬停就知道该找哪个插件)
151
+ const cands = Array.isArray(display.candidates) ? display.candidates : [];
152
+ if (display.provider) typeBadge.title = `归属:${display.provider}`;
153
+ else if (cands.length) typeBadge.title = `归属待定(候选:${cands.join('、')})`;
154
+ row.append(typeBadge);
155
+ row.append(el('span', 'tree-variant-text', text));
156
+ if (origName && origName !== text) row.append(el('span', 'tree-variant-orig', ` · ${origName}`));
157
+
158
+ // ⚠ 只有一处来源:support 判"装不动",或 loadResult 判"上次失败"。
159
+ // 两者的文案都由服务端给(support.detail.message / loadResult.message),前端只展示。
160
+ // 契约保证 support.unsupported 与 loadResult.ok 不并存 ⇒ 这里不会出现"一边说不行一边说刚成功"。
161
+ const support = (m && m.support) || {};
162
+ const loadResult = (m && m.loadResult) || null;
163
+ const warnText = (support.state === 'unsupported' && support.detail && support.detail.message)
164
+ ? String(support.detail.message)
165
+ : (loadResult && loadResult.result === 'failed'
166
+ ? String(loadResult.message || loadResult.reason || '装载失败')
167
+ : '');
168
+ if (warnText) {
169
+ const badge = el('span', 'tree-variant-warn', '⚠');
170
+ // 打标记:applyEntryState 就地刷新时**只认这个标记**找回已有的 ⚠。
171
+ // 不打的话它会以为"这行还没有警告",于是再 append 一个 ⇒ 同一行挂两个 ⚠(实测踩过)。
172
+ badge.dataset.entryWarn = '1';
173
+ badge.title = warnText;
174
+ row.append(badge);
175
+ row.classList.add('is-incomplete');
176
+ }
177
+ row.title = `${origName}${m.srcDir && m.srcDir !== origName ? ` · ${m.srcDir}` : ''}${warnText ? `\n⚠ ${warnText}` : ''}`;
178
+ return row;
179
+ }
180
+
181
+ /**
182
+ * 按条目**就地**刷新徽标:只动命中那一行,**不重渲染整棵名册**。
183
+ *
184
+ * 为什么不能整棵重渲染:名册是带展开态 / 选中态 / 滚动位置的树,
185
+ * 重渲染会把这三样全部丢掉 —— 用户看到的是"点一下模型,列表跳回顶部、节点全折叠"。
186
+ *
187
+ * 服务端返回的 display/support/loadResult 是唯一真相(契约 §一 INV-D1):
188
+ * 本函数只把三个字段"画"到那一行,不接收前端自造的失败记录、不猜颜色。
189
+ * ⇒ 装载成功后服务端 recompute 一变,照着画就对(这就是"当场变色"的全部机制)。
190
+ *
191
+ * @param {object} model 名册条目(只需 id / name)
192
+ * @param {{display?:object, support?:object, loadResult?:object}|null} entryState 服务端三字段;
193
+ * null/缺省 = 该条目已无状态(等价于全部清空)
194
+ * @returns {boolean} 命中并处理了返回 true;该条目当前不在名册里(被折叠/搜索过滤)返回 false
195
+ */
196
+ export function applyEntryState(model, entryState) {
197
+ const id = String((model && (model.id || model.name)) || '');
198
+ if (!id) return false;
199
+ // 不用属性选择器:条目 id 里常有空格 / 斜杠 / 引号,拼进选择器必须转义,
200
+ // 而 CSS.escape 不是所有环境都有。名册数百行量级,直接遍历更稳也更清楚。
201
+ let row = null;
202
+ for (const candidate of document.querySelectorAll('.tree-row.tree-variant')) {
203
+ if (candidate.dataset && candidate.dataset.modelId === id) { row = candidate; break; }
204
+ }
205
+ if (!row) return false;
206
+
207
+ const st = entryState || {};
208
+ const display = st.display || {};
209
+ const support = st.support || {};
210
+ const loadResult = st.loadResult || null;
211
+
212
+ // ① 徽标颜色:只认 display.accent(null ⇒ 中性色,绝不按 kind 猜)
213
+ const typeBadge = row.querySelector('.tree-type');
214
+ if (typeBadge) {
215
+ if (display.accent) typeBadge.style.setProperty('--badge-accent', String(display.accent));
216
+ else typeBadge.style.removeProperty('--badge-accent');
217
+ const cands = Array.isArray(display.candidates) ? display.candidates : [];
218
+ if (display.provider) typeBadge.title = `归属:${display.provider}`;
219
+ else if (cands.length) typeBadge.title = `归属待定(候选:${cands.join('、')})`;
220
+ else typeBadge.title = '';
221
+ }
222
+
223
+ // ② ⚠:support 说"装不动" 或 loadResult 说"上次失败";文案由服务端给
224
+ const warnText = (support.state === 'unsupported' && support.detail && support.detail.message)
225
+ ? String(support.detail.message)
226
+ : (loadResult && loadResult.result === 'failed'
227
+ ? String(loadResult.message || loadResult.reason || '装载失败')
228
+ : '');
229
+ const existing = row.querySelector('[data-entry-warn="1"]');
230
+ if (warnText) {
231
+ if (existing) existing.title = warnText;
232
+ else {
233
+ const badge = el('span', 'tree-variant-warn', '⚠');
234
+ badge.dataset.entryWarn = '1';
235
+ badge.title = warnText;
236
+ row.append(badge);
237
+ }
238
+ row.classList.add('is-incomplete');
239
+ } else {
240
+ if (existing) existing.remove();
241
+ if (!row.querySelector('.tree-variant-warn')) row.classList.remove('is-incomplete');
242
+ }
243
+
244
+ // ③ 行级 title:正文(原名 / 目录)原样保留,只替换末尾那句警告
245
+ const baseTitle = String(row.title || '').replace(/\n⚠ [^\n]*$/, '');
246
+ row.title = warnText ? `${baseTitle}\n⚠ ${warnText}` : baseTitle;
247
+ return true;
248
+ }
@@ -0,0 +1,39 @@
1
+ // roster/snapshot.js —— 把名册状态投影成可读快照(自动化断言与手工排查共用)
2
+ //
3
+ // 为什么需要:断言不该去猜内部结构;这里把"每个游戏有多少角色/变体、展开了哪些、选中了谁、
4
+ // DOM 里真的渲染了几行、最近一次定位动作是什么"一次性摊平,测试只读这份快照。
5
+
6
+ /**
7
+ * @param {{state: object, listEl: HTMLElement, gameStatus: (game: object) => string}} deps
8
+ */
9
+ export function snapshotRoster({ state, listEl, gameStatus }) {
10
+ const games = state.games.map((g) => {
11
+ const chars = state.gameChars.get(g.key);
12
+ return {
13
+ key: g.key,
14
+ name: g.name,
15
+ roots: g.roots.map((r) => r.id),
16
+ status: gameStatus(g),
17
+ sources: g.roots.map((r) => (state.loaded.get(r.id) || {}).source || '').filter(Boolean),
18
+ chars: chars ? chars.chars.length : 0,
19
+ variants: chars ? chars.chars.reduce((n, c) => n + c.variants.length, 0) : 0,
20
+ miss: chars ? chars.stats.miss : 0,
21
+ };
22
+ });
23
+ return {
24
+ games,
25
+ expandedGames: [...state.expandedGames],
26
+ expandedChars: [...state.expandedChars],
27
+ keyword: state.keyword,
28
+ /** 类型筛选的选中集合(**存储原样**,可能含当前名册里不存在的失效项) */
29
+ typeFilter: state.typeFilter ? [...state.typeFilter] : [],
30
+ selected: state.selected,
31
+ /** 最近一次"定位当前角色"的结果(visible / scrolled / skipped-user-scrolling / …) */
32
+ lastReveal: state.lastReveal || null,
33
+ domRows: {
34
+ games: listEl.querySelectorAll('[data-game-key].tree-game').length,
35
+ chars: listEl.querySelectorAll('[data-char-key].tree-char').length,
36
+ variants: listEl.querySelectorAll('[data-model-id]').length,
37
+ },
38
+ };
39
+ }
@@ -0,0 +1,97 @@
1
+ // roster/type-filter.js —— 名册「内容类型」筛选(纯函数,不碰 DOM、不读存储)
2
+ //
3
+ // 与 filter.js 的分工:
4
+ // · filter.js 管**关键词**(角色整组判定:命中角色名或任一变体即显示整组)
5
+ // · 本文件 管**类型**(按条目自带的 kind 归并、计数、过滤)
6
+ // 两者正交:视图层先按类型筛,再按关键词筛(或反过来),结果一致(交集)。
7
+ //
8
+ // 类型从哪来:条目的 `kind`(内容类型)。**未解包的容器条目 kind 可能为 null**,
9
+ // 一律归入 UNKNOWN_TYPE_KEY(显示名「待解包」)—— 否则这些条目在"选中任意类型"时会被全部滤掉,
10
+ // 用户会以为资源包不见了(设计稿 §5.1 的关键结论)。
11
+
12
+ import { UNKNOWN_TYPE_KEY } from '../../shared/type-labels.mjs';
13
+ import { visibleChars } from './filter.js';
14
+
15
+ /**
16
+ * 单个变体的类型键:条目 kind 为空 ⇒ unknown。
17
+ * 为什么看 kind 而不是 container:container 是"装载方式"(要先解包),kind 才是"内容类型"。
18
+ * @param {{model?: {kind?: string|null}}} variant
19
+ * @returns {string}
20
+ */
21
+ export function typeKeyOf(variant) {
22
+ const model = variant && variant.model ? variant.model : variant;
23
+ const kind = model && model.kind != null ? String(model.kind).trim() : '';
24
+ return kind || UNKNOWN_TYPE_KEY;
25
+ }
26
+
27
+ /**
28
+ * 类型计数:**当前关键词下、该类型可见的变体数**(不含类型过滤本身的影响)。
29
+ *
30
+ * 为什么要排除类型过滤:数字要回答"如果我再选它,会有多少"。
31
+ * 若把类型过滤也算进去,选中 Spine 后其它胶囊会立刻变 0,用户就没法判断要不要切换。
32
+ * 为什么仍计入关键词:面板上的数字必须和"现在列表里能看到多少"对得上(设计稿 §5.3)。
33
+ *
34
+ * scope 参数是为将来扩维度(如按资源根)预留的:当前只消费 keyword,新增维度时只加 scope 字段,
35
+ * 不动本函数的排序/计数逻辑(设计稿 §5.4)。
36
+ *
37
+ * @param {{games: object[], gameChars: Map, keyword?: string,
38
+ * scope?: {keyword?: string}}} input
39
+ * @returns {Array<{key: string, count: number}>} 按数量降序,并列按 key 字典序(稳定)
40
+ */
41
+ export function deriveTypeCounts({ games, gameChars, keyword = '', scope = null }) {
42
+ const effectiveKeyword = String((scope && scope.keyword) != null ? scope.keyword : keyword || '').toLowerCase();
43
+ const counts = new Map();
44
+ for (const game of games || []) {
45
+ // 复用关键词过滤的同一口径(角色整组判定),保证"数字"与"列表可见性"不会两套算法
46
+ const chars = visibleChars(game, { gameChars, keyword: effectiveKeyword });
47
+ for (const char of chars) {
48
+ for (const variant of char.variants || []) {
49
+ const typeKey = typeKeyOf(variant);
50
+ counts.set(typeKey, (counts.get(typeKey) || 0) + 1);
51
+ }
52
+ }
53
+ }
54
+ return [...counts.entries()]
55
+ .map(([key, count]) => ({ key, count }))
56
+ .sort((left, right) => (right.count - left.count) || (left.key < right.key ? -1 : left.key > right.key ? 1 : 0));
57
+ }
58
+
59
+ /**
60
+ * 按选中类型过滤角色(并把每个角色的变体裁到命中集合)。
61
+ *
62
+ * 语义:selectedTypes 为空 ⇒ 不过滤(等于"全部");否则 = 并集(命中任一选中类型即保留)。
63
+ * 注意返回的是**新数组/新角色对象**,不改动 state.gameChars 里的原对象(渲染层只读)。
64
+ *
65
+ * @param {object[]} chars 角色数组(每个带 variants)
66
+ * @param {Set<string>|string[]} selectedTypes
67
+ * @returns {object[]}
68
+ */
69
+ export function filterCharsByType(chars, selectedTypes) {
70
+ const selected = selectedTypes instanceof Set ? selectedTypes : new Set(selectedTypes || []);
71
+ if (!selected.size) return chars;
72
+ const kept = [];
73
+ for (const char of chars) {
74
+ const variants = (char.variants || []).filter((variant) => selected.has(typeKeyOf(variant)));
75
+ if (variants.length) kept.push({ ...char, variants });
76
+ }
77
+ return kept;
78
+ }
79
+
80
+ /**
81
+ * 把存储里读到的选中集合收敛成"当前可用"的集合。
82
+ *
83
+ * 失效项(当前类型集合里没有的 key)在**当前页面生命周期内被忽略**,但**不删存储**——
84
+ * 这样某个类型暂时消失(例如最后一个资源包被解包)后再出现时,用户的选中会自动恢复。
85
+ * @param {string[]} storedKeys 存储里读到的 key 列表
86
+ * @param {Iterable<string>} availableKeys 当前名册实际出现的类型
87
+ * @returns {Set<string>} 可用的选中集合
88
+ */
89
+ export function usableTypeSelection(storedKeys, availableKeys) {
90
+ const available = new Set([...availableKeys].map((key) => String(key)));
91
+ const out = new Set();
92
+ for (const raw of Array.isArray(storedKeys) ? storedKeys : []) {
93
+ const key = String(raw == null ? '' : raw);
94
+ if (key && available.has(key)) out.add(key);
95
+ }
96
+ return out;
97
+ }