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,321 @@
1
+ // transport.js —— 底部播放条渲染与接线
2
+ //
3
+ // 做什么:按 panel-schema.js 的 TRANSPORT_CONTROLS 渲染播放条上的 8 个控件,
4
+ // 并把调用方提供了 handler 的控件接上线;没提供 handler 的保持 disabled + data-pending。
5
+ // 铁律:不允许"看着能点却没反应"——只有 handler 存在的控件才会被解锁。
6
+ //
7
+ // 动态选项:动作/皮肤下拉的选项由模型数据填充(setOptions),渲染时的 options 只是占位文案。
8
+ //
9
+ // 自绘下拉本身(触发器 + listbox + 键盘 + 背景选项源)在 panel/transport-picker.js;
10
+ // 本文件只负责"有哪些控件、怎么分组、怎么和 app 层接线"。
11
+
12
+ import { TRANSPORT_CONTROLS } from './schema.js';
13
+ import { GROUP_ICONS } from './icons.js';
14
+ import { el } from './dom.js';
15
+ import { setBackgroundColor, setCustomBackground } from '../app/view-settings.js';
16
+ import { stageStatus } from '../app/status.js';
17
+ import {
18
+ BG_MODES, applyBgValue, bindBgSync, createPicker, currentBgValue, refreshBgOptions, renderBgSubmenu,
19
+ } from './transport-picker.js';
20
+
21
+ /** 统一打死"未接线"标记 */
22
+ function markPending(node, id, title) {
23
+ node.dataset.pending = 'true';
24
+ if (id) node.id = id;
25
+ node.disabled = true;
26
+ node.title = `${title || '待接线'}(M6 接线)`;
27
+ return node;
28
+ }
29
+
30
+ /** 解锁控件:去掉 pending 与 disabled,换上真实提示 */
31
+ function unwirePending(node, title) {
32
+ delete node.dataset.pending;
33
+ node.disabled = false;
34
+ node.title = title || '';
35
+ return node;
36
+ }
37
+
38
+ /** 单个控件(按钮 / 下拉) */
39
+ function renderControl(ctl) {
40
+ // 自绘下拉:返回 {root, setOptions...} 句柄由调用方登记(不在这里建,避免丢句柄)
41
+ if (ctl.custom === 'picker') return null;
42
+ if (ctl.kind === 'select') {
43
+ const sel = document.createElement('select');
44
+ sel.className = 'transport-select';
45
+ markPending(sel, ctl.id, ctl.title || ctl.label);
46
+ for (const opt of ctl.options || []) {
47
+ const o = document.createElement('option');
48
+ o.value = opt;
49
+ o.textContent = opt;
50
+ sel.append(o);
51
+ }
52
+ sel.value = ctl.value || (ctl.options && ctl.options[0]) || '';
53
+ sel.setAttribute('aria-label', ctl.label);
54
+ return sel;
55
+ }
56
+ // 文字放进独立 span:播放/暂停按钮的文字要能随状态换(图标与文字必须同义)
57
+ const btn = el('button', 'mini-btn');
58
+ btn.append(el('span', 'btn-label', ctl.label));
59
+ btn.type = 'button';
60
+ markPending(btn, ctl.id, ctl.title || ctl.label);
61
+ return btn;
62
+ }
63
+
64
+ /**
65
+ * 把播放条内容挂到 footer 容器里(容器本身在 index.html,负责定位)。
66
+ * @param {HTMLElement} footerEl
67
+ * @param {Record<string, Function>} handlers 控件 id → 回调(存在即视为已接线)
68
+ * @returns {{
69
+ * controls: number,
70
+ * wired: string[],
71
+ * setOptions: (id: string, options: Array<{text:string,value:string}>, value?: string) => boolean,
72
+ * setPressed: (id: string, on: boolean) => void,
73
+ * setSelectValue: (id: string, value: string) => void,
74
+ * pressed: (id: string) => boolean|null,
75
+ * }} 播放条能力
76
+ */
77
+ /**
78
+ * 播放/暂停按钮的双态图标(lucide Play / Pause 的 path 原文)。
79
+ * 语义:**播放中显示"暂停"图标**(点它=暂停),暂停后显示"播放"三角。
80
+ */
81
+ const PLAY_ICONS = {
82
+ play: '<polygon points="6 3 20 12 6 21 6 3"/>',
83
+ pause: '<rect x="14" y="4" width="4" height="16" rx="1"/><rect x="6" y="4" width="4" height="16" rx="1"/>',
84
+ };
85
+
86
+ /**
87
+ * 图标对应的文案(与图标同义:写的是"点它会做什么",不是"当前是什么状态")。
88
+ * 播放中 → 图标是暂停符号 → 文案「暂停」;已暂停 → 图标是三角 → 文案「播放」。
89
+ * 两者必须同源派生,否则会出现"图标说暂停、tooltip 说播放/暂停"的错位。
90
+ */
91
+ const PLAY_LABELS = { play: '播放', pause: '暂停' };
92
+
93
+ /** 底栏按钮图标(内联 SVG,与顶栏同一套线条风格;不引图标库) */
94
+ // 皮肤/动作/背景与右栏是同一个功能,图标**直接引右栏那套**(GROUP_ICONS),
95
+ // 两处各抄一份 path 必然漂移(曾出现:右栏动作是网格图标、底栏是波形图标)。
96
+ // 上一个/下一个/播放/重置视图右栏没有对应功能,图标留在这里。
97
+ const ICONS = {
98
+ 'transport-prev': '<polygon points="19 20 9 12 19 4 19 20"/><line x1="5" y1="19" x2="5" y2="5"/>',
99
+ 'transport-play': '<polygon points="6 3 20 12 6 21 6 3"/>',
100
+ 'transport-next': '<polygon points="5 4 15 12 5 20 5 4"/><line x1="19" y1="5" x2="19" y2="19"/>',
101
+ 'transport-reset-view': '<path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 3v6h6"/>',
102
+ // 以下三个与右栏同名功能共用一份 path
103
+ 'transport-bg': GROUP_ICONS.theme,
104
+ 'transport-skin': GROUP_ICONS.skin,
105
+ 'transport-motion': GROUP_ICONS.motion,
106
+ };
107
+
108
+ /** 给按钮前置一个图标(非按钮/未配图标则原样返回) */
109
+ function withIcon(node, id) {
110
+ const body = ICONS[id];
111
+ if (!body || !node || node.tagName !== 'BUTTON') return node;
112
+ const wrap = document.createElement('span');
113
+ wrap.className = 'btn-icon';
114
+ wrap.setAttribute('aria-hidden', 'true');
115
+ wrap.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${body}</svg>`;
116
+ node.prepend(wrap);
117
+ node.classList.add('has-icon');
118
+ return node;
119
+ }
120
+
121
+ export function mountTransport(footerEl, handlers = {}) {
122
+ footerEl.replaceChildren();
123
+
124
+ // 分组由数组派生:center:true 的控件单独成中块,它之前→左块、之后→右块。
125
+ // 这样调整控件顺序只需改 schema 的数组,不必同步维护第二份分组表(以前漏改一处就错位)。
126
+ const ids = TRANSPORT_CONTROLS.map((c) => c.id);
127
+ const centerIdx = TRANSPORT_CONTROLS.findIndex((c) => c.center === true);
128
+ const groups = centerIdx < 0
129
+ ? [ids] // 没标 center ⇒ 单块兜底
130
+ : [ids.slice(0, centerIdx), [ids[centerIdx]], ids.slice(centerIdx + 1)];
131
+ const byId = new Map(TRANSPORT_CONTROLS.map((c) => [c.id, c]));
132
+ const nodes = new Map();
133
+ /** 自绘下拉的句柄(id → picker),setOptions/setSelectValue 走它 */
134
+ const pickers = new Map();
135
+ /** 自带回调的 picker(不依赖 handlers 即可用):背景选择器属于这一类 */
136
+ const selfWired = new Set();
137
+
138
+ for (let gi = 0; gi < groups.length; gi += 1) {
139
+ const groupIds = groups[gi];
140
+ // 空块直接跳过:否则会占掉一个 grid 列(center 标在数组首项时左块就是空的)
141
+ if (!groupIds.length) continue;
142
+ const box = el('div', 'transport-group');
143
+ // 显式标出这块该落在哪一列:跳过空块后,剩下的块不能靠 DOM 顺序去猜列
144
+ // (否则 center 标在首项时,中块会被排到第 1 列、偏移 300+px)
145
+ box.dataset.slot = centerIdx < 0 ? 'left' : ['left', 'center', 'right'][gi];
146
+ for (const id of groupIds) {
147
+ const ctl = byId.get(id);
148
+ if (!ctl) continue;
149
+ if (id === 'transport-bg') {
150
+ // 背景:内置 4 模式 + 服务端自定义图,选中即应用(不再"点一次换一个"的循环切换)。
151
+ // 这里不接 handlers['transport-bg']:那个是旧的循环切换动作,接了就会"选了又跳下一项"。
152
+ const bgPicker = createPicker(
153
+ { ...ctl, icon: ICONS[ctl.id], options: BG_MODES.slice(), value: currentBgValue() },
154
+ (value) => applyBgValue(value),
155
+ {
156
+ onBeforeOpen: () => { refreshBgOptions(bgPicker).catch(() => { /* 刷新失败沿用现有选项 */ }); },
157
+ /**
158
+ * 一级里点「纯色 / 自定义」→ 侧向弹出二级(色块列表 / 图片列表)。
159
+ * 二级内容由 bg-source.renderSourceGroup 渲染,与右栏那份是同一套代码;
160
+ * 选中后:应用 + 关掉两级 + 让一级重新对齐(勾选与按钮回显都靠 refreshBgOptions)。
161
+ */
162
+ onSubmenu: (groupId, host) => {
163
+ renderBgSubmenu(host, groupId, ({ groupId: g, item }) => {
164
+ if (g === 'solid') {
165
+ setBackgroundColor(item.color);
166
+ stageStatus(`背景 → ${item.name}`, 'info', { show: true });
167
+ } else {
168
+ setCustomBackground(item.url);
169
+ document.dispatchEvent(new CustomEvent('panel:bg-applied', { detail: { mode: 'custom' } }));
170
+ stageStatus(`背景 → ${item.name}`, 'info', { show: true });
171
+ }
172
+ try { bgPicker.closeSub(); } catch { /* 忽略 */ }
173
+ bgPicker.close();
174
+ refreshBgOptions(bgPicker).catch(() => { /* 忽略 */ });
175
+ }).catch(() => { /* 二级渲染失败:保持一级 */ });
176
+ },
177
+ },
178
+ );
179
+ pickers.set(id, bgPicker);
180
+ selfWired.add(id);
181
+ nodes.set(id, bgPicker.root);
182
+ box.append(bgPicker.root);
183
+ refreshBgOptions(bgPicker).catch(() => { /* 首次拉取失败:保留内置 4 项 */ });
184
+ bindBgSync(bgPicker);
185
+ continue;
186
+ }
187
+ if (ctl.custom === 'picker') {
188
+ // 自绘下拉:回调直接接到 handlers[id](与原生 select 的 change 同义)
189
+ const picker = createPicker(
190
+ {
191
+ ...ctl,
192
+ icon: ICONS[ctl.id],
193
+ options: (ctl.options || []).map((o) => (typeof o === 'string' ? { text: o, value: o } : o)),
194
+ },
195
+ (value) => {
196
+ const h = handlers[id];
197
+ if (typeof h !== 'function') { document.documentElement.dataset.pickerError = `no-handler:${id}`; return; }
198
+ try {
199
+ h(value);
200
+ document.documentElement.dataset.pickerError = '';
201
+ } catch (err) {
202
+ // 不再静默:把错误留痕,验收脚本可读(选择本身已更新,不至于"点了没反应"却查不出原因)
203
+ document.documentElement.dataset.pickerError = String((err && err.message) || err);
204
+ }
205
+ },
206
+ );
207
+ pickers.set(id, picker);
208
+ nodes.set(id, picker.root);
209
+ box.append(picker.root);
210
+ continue;
211
+ }
212
+ const node = withIcon(renderControl(ctl), id);
213
+ nodes.set(id, node);
214
+ box.append(node);
215
+ }
216
+ footerEl.append(box);
217
+ }
218
+
219
+ // 接线:只有提供 handler 的控件才解锁
220
+ const wired = [];
221
+ for (const [id, node] of nodes) {
222
+ // 自绘下拉的"已接线"判定:自带回调的(背景)不依赖 handlers;
223
+ // 其余(动作/皮肤)的选中回调会转调 handlers[id],没有 handler 仍算未接线。
224
+ if (pickers.has(id)) {
225
+ if (selfWired.has(id) || typeof handlers[id] === 'function') wired.push(id);
226
+ continue;
227
+ }
228
+ const handler = handlers[id];
229
+ if (typeof handler !== 'function') continue;
230
+ const ctl = byId.get(id);
231
+ unwirePending(node, ctl ? (ctl.title || ctl.label) : '');
232
+ if (node.tagName === 'SELECT') {
233
+ node.addEventListener('change', () => {
234
+ try { handler(node.value); } catch { /* 回调失败保持当前选择 */ }
235
+ });
236
+ } else {
237
+ node.addEventListener('click', () => {
238
+ try { handler(); } catch { /* 回调失败不改状态 */ }
239
+ });
240
+ }
241
+ wired.push(id);
242
+ }
243
+
244
+ /** 重建下拉选项(动作/皮肤在模型加载后填充) */
245
+ function setOptions(id, options, value) {
246
+ const pk = pickers.get(id);
247
+ if (pk) return pk.setOptions(options, value) > 0;
248
+ const node = nodes.get(id);
249
+ if (!node || node.tagName !== 'SELECT' || !Array.isArray(options)) return false;
250
+ node.replaceChildren();
251
+ for (const opt of options) {
252
+ const o = document.createElement('option');
253
+ o.value = opt.value;
254
+ o.textContent = opt.text;
255
+ node.append(o);
256
+ }
257
+ if (typeof value === 'string') node.value = value;
258
+ node.dataset.filled = String(options.length);
259
+ return true;
260
+ }
261
+
262
+ /** 按钮按下态(播放暂停这类开关型按钮) */
263
+ /**
264
+ * 换图标(仅播放/暂停这种双态按钮用):清空 svg 子节点后写入新 path。
265
+ * 图标与 aria-pressed 一起由 app 层同一份状态派生,面板不自己判断播放状态。
266
+ * @param {string} id 控件 id
267
+ * @param {'play'|'pause'} name 变体名
268
+ */
269
+ function setIcon(id, name) {
270
+ const node = nodes.get(id);
271
+ const body = PLAY_ICONS[name];
272
+ if (!node || !body) return false;
273
+ const svg = node.querySelector('svg');
274
+ if (!svg) return false;
275
+ svg.innerHTML = body; // 与 withIcon 同一套包装,只换 path 内容
276
+ const label = PLAY_LABELS[name];
277
+ if (label) {
278
+ node.title = label; // tooltip:点了会发生什么
279
+ node.setAttribute('aria-label', label); // 无障碍名与之一致
280
+ const text = node.querySelector('.btn-label');
281
+ if (text) text.textContent = label; // 按钮上的可见文字同源(否则会"图标变、文字不动")
282
+ }
283
+ return true;
284
+ }
285
+
286
+ function setPressed(id, on) {
287
+ const node = nodes.get(id);
288
+ if (!node) return;
289
+ node.setAttribute('aria-pressed', String(!!on));
290
+ }
291
+
292
+ /** 下拉当前值(不再触发回调) */
293
+ function setSelectValue(id, value) {
294
+ const pk = pickers.get(id);
295
+ if (pk) { pk.setValue(value); return; }
296
+ const node = nodes.get(id);
297
+ if (node && node.tagName === 'SELECT' && typeof value === 'string') node.value = value;
298
+ }
299
+
300
+ /** 读按钮按下态 */
301
+ function pressed(id) {
302
+ const node = nodes.get(id);
303
+ return node ? node.getAttribute('aria-pressed') === 'true' : null;
304
+ }
305
+
306
+ // 初始为"播放中":按语义,播放中显示暂停图标 + aria-pressed=true
307
+ setIcon('transport-play', 'pause');
308
+ setPressed('transport-play', true);
309
+
310
+ return {
311
+ controls: footerEl.querySelectorAll('button, select').length,
312
+ wired,
313
+ setOptions,
314
+ setPressed,
315
+ setIcon,
316
+ /** 自绘下拉句柄(测试/断言用:isOpen / value) */
317
+ picker: (id) => pickers.get(id) || null,
318
+ setSelectValue,
319
+ pressed,
320
+ };
321
+ }