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.
- package/LICENSE +21 -0
- package/README.md +274 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +137 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +63 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +654 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +396 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/shared/type-labels.mjs +31 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +524 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +117 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +92 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +39 -0
- package/src/web/roster/type-filter.js +97 -0
- package/src/web/roster/view.js +761 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// panel/transport-picker.js —— 底栏「自绘下拉」的完整实现
|
|
2
|
+
//
|
|
3
|
+
// 从 transport.js 抽出来的原因:那块代码(触发器 + listbox + 键盘语义 + 背景选项源)占了
|
|
4
|
+
// 半个文件,混在"渲染编排 + 接线"里读不下去。这里只负责"一个下拉怎么工作",
|
|
5
|
+
// 不知道底栏有几个控件、也不知道它们怎么分组。
|
|
6
|
+
//
|
|
7
|
+
// 对外只暴露四件事:
|
|
8
|
+
// createPicker(ctl, onPick, hooks) 建一个下拉
|
|
9
|
+
// refreshBgOptions(picker) 重新拉背景选项(内置 4 模式 + 服务端自定义图)
|
|
10
|
+
// applyBgValue(value) 应用一次背景选择(mode:xxx / img:url)
|
|
11
|
+
// bindBgSync(picker) 任何来源改了背景都让下拉重新对齐
|
|
12
|
+
|
|
13
|
+
import { el } from './dom.js';
|
|
14
|
+
import { listBgSources, renderSourceGroup, selectedSourceId } from './bg-source.js';
|
|
15
|
+
import { listBackgrounds } from '../app/backgrounds.js';
|
|
16
|
+
import { applyBackground, readViewSettings, setCustomBackground } from '../app/view-settings.js';
|
|
17
|
+
import { stageLog, stageStatus } from '../app/status.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 自绘下拉的键盘语义(WAI-ARIA combobox 惯用法):
|
|
21
|
+
* Esc 关闭并把焦点还给触发按钮
|
|
22
|
+
* ↑/↓ 打开(若关闭)或移动高亮
|
|
23
|
+
* Enter/空格 确认当前高亮项并关闭
|
|
24
|
+
* 抽成模块级函数是为了让 createPicker 本体保持可读(单函数长度门禁)。
|
|
25
|
+
* @param {KeyboardEvent} ev
|
|
26
|
+
* @param {{isOpen:Function, open:Function, close:Function, count:Function,
|
|
27
|
+
* activeIndex:Function, setActive:Function, pickActive:Function}} api
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* 按选项清单重建列表项(纯 DOM 构建,便于测试与复用)。
|
|
31
|
+
* @param {Array<{text:string,value:string}>} options
|
|
32
|
+
* @param {(value:string) => void} onPickItem 点某项时回调
|
|
33
|
+
* @param {HTMLElement} list 目标 <ul>
|
|
34
|
+
* @returns {Array<{value:string, text:string, node:HTMLLIElement}>}
|
|
35
|
+
*/
|
|
36
|
+
function buildPickerItems(options, onPickItem, list, onSubmenu) {
|
|
37
|
+
list.replaceChildren();
|
|
38
|
+
const items = [];
|
|
39
|
+
for (const opt of options || []) {
|
|
40
|
+
// 分组标题:只做视觉分隔(role=presentation),不进可选项数组,键盘不会停在它上面
|
|
41
|
+
if (opt.group) {
|
|
42
|
+
const head = el('li', 'transport-picker-group', opt.text);
|
|
43
|
+
head.setAttribute('role', 'presentation');
|
|
44
|
+
list.append(head);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const li = el('li', 'transport-picker-item', opt.text);
|
|
48
|
+
li.setAttribute('role', 'option');
|
|
49
|
+
li.dataset.value = String(opt.value);
|
|
50
|
+
li.tabIndex = -1;
|
|
51
|
+
if (opt.submenu) {
|
|
52
|
+
// 有二级:行尾加 ▸,点它不"选中"而是展开侧向二级(键盘 Enter 同样走这条)
|
|
53
|
+
li.dataset.submenu = String(opt.submenu);
|
|
54
|
+
li.append(el('span', 'transport-picker-arrow', '▸'));
|
|
55
|
+
li.addEventListener('click', () => {
|
|
56
|
+
if (typeof onSubmenu === 'function') onSubmenu(String(opt.submenu), li);
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
li.addEventListener('click', () => onPickItem(String(opt.value)));
|
|
60
|
+
}
|
|
61
|
+
items.push({ value: String(opt.value), text: opt.text, node: li, submenu: opt.submenu || null });
|
|
62
|
+
list.append(li);
|
|
63
|
+
}
|
|
64
|
+
return items;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function handlePickerKey(ev, api) {
|
|
68
|
+
const key = ev.key;
|
|
69
|
+
if (key === 'Escape') { if (api.isOpen()) { api.close(); ev.preventDefault(); } return; }
|
|
70
|
+
if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
71
|
+
if (!api.isOpen()) { api.open(); ev.preventDefault(); return; }
|
|
72
|
+
const step = key === 'ArrowDown' ? 1 : -1;
|
|
73
|
+
const next = Math.max(0, Math.min(api.count() - 1, (api.activeIndex() < 0 ? 0 : api.activeIndex()) + step));
|
|
74
|
+
api.setActive(next);
|
|
75
|
+
ev.preventDefault();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (key === 'Enter' || key === ' ') {
|
|
79
|
+
if (!api.isOpen()) return;
|
|
80
|
+
api.pickActive();
|
|
81
|
+
ev.preventDefault();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 自绘下拉(替代原生 <select>):触发按钮 + 弹出列表。
|
|
87
|
+
*
|
|
88
|
+
* 为什么不用原生 select:视觉无法与本实现设计语言统一(动作/皮肤列表同样是自绘的,保持一致),
|
|
89
|
+
* 而且原生 select 的键盘行为在各浏览器不一致,无法保证"上下键移动高亮、Esc 关闭并回焦"。
|
|
90
|
+
*
|
|
91
|
+
* 结构(无障碍语义照 WAI-ARIA combobox/listbox):
|
|
92
|
+
* .transport-picker[data-ctl]
|
|
93
|
+
* button.transport-picker-btn[aria-haspopup=listbox][aria-expanded]
|
|
94
|
+
* span.picker-label(显示当前项文案)+ svg chevron
|
|
95
|
+
* ul.transport-picker-list[role=listbox] > li[role=option][aria-selected]
|
|
96
|
+
*
|
|
97
|
+
* @param {{id:string, label:string, title?:string, options:Array<{text:string,value:string}>, value:string}} ctl
|
|
98
|
+
* @param {(value:string) => void} onPick 选中回调(只有真实选中才触发,与原生 select 的 change 同义)
|
|
99
|
+
*/
|
|
100
|
+
export function createPicker(ctl, onPick, hooks = {}) {
|
|
101
|
+
const root = el('div', 'transport-picker');
|
|
102
|
+
root.dataset.ctl = ctl.id;
|
|
103
|
+
const btn = el('button', 'transport-picker-btn');
|
|
104
|
+
btn.type = 'button';
|
|
105
|
+
btn.setAttribute('aria-haspopup', 'listbox');
|
|
106
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
107
|
+
btn.setAttribute('aria-label', ctl.label || '选择');
|
|
108
|
+
if (ctl.title) btn.title = ctl.title;
|
|
109
|
+
// 图标由调用方传入(图标表在 transport.js,这里不重复维护一份)
|
|
110
|
+
const iconBody = ctl.icon;
|
|
111
|
+
if (iconBody) {
|
|
112
|
+
const wrap = el('span', 'btn-icon');
|
|
113
|
+
wrap.setAttribute('aria-hidden', 'true');
|
|
114
|
+
wrap.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${iconBody}</svg>`;
|
|
115
|
+
btn.append(wrap);
|
|
116
|
+
btn.classList.add('has-icon');
|
|
117
|
+
}
|
|
118
|
+
const labelEl = el('span', 'picker-label', '');
|
|
119
|
+
btn.append(labelEl);
|
|
120
|
+
const list = el('ul', 'transport-picker-list');
|
|
121
|
+
list.setAttribute('role', 'listbox');
|
|
122
|
+
list.hidden = true;
|
|
123
|
+
// 二级菜单(侧向弹出):内容由调用方的 hooks.onSubmenu 用统一渲染层填
|
|
124
|
+
const sub = el('div', 'transport-picker-sub');
|
|
125
|
+
sub.hidden = true;
|
|
126
|
+
root.append(btn, list, sub);
|
|
127
|
+
|
|
128
|
+
let items = [];
|
|
129
|
+
let activeIndex = -1;
|
|
130
|
+
let current = ctl.value || '';
|
|
131
|
+
|
|
132
|
+
/** 更新触发按钮上的文案 */
|
|
133
|
+
function paintLabel() {
|
|
134
|
+
const hit = items.find((it) => it.value === current);
|
|
135
|
+
labelEl.textContent = hit ? hit.text : (items[0] ? items[0].text : '');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 高亮某一项(-1 = 不高亮) */
|
|
139
|
+
function paintActive() {
|
|
140
|
+
items.forEach((it, i) => {
|
|
141
|
+
it.node.classList.toggle('is-active', i === activeIndex);
|
|
142
|
+
it.node.setAttribute('aria-selected', String(it.value === current));
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function closeSub() {
|
|
147
|
+
sub.hidden = true;
|
|
148
|
+
sub.replaceChildren();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function close(returnFocus = false) {
|
|
152
|
+
closeSub();
|
|
153
|
+
if (list.hidden) return;
|
|
154
|
+
list.hidden = true;
|
|
155
|
+
btn.setAttribute('aria-expanded', 'false');
|
|
156
|
+
root.classList.remove('is-open');
|
|
157
|
+
activeIndex = -1;
|
|
158
|
+
paintActive();
|
|
159
|
+
if (returnFocus) { try { btn.focus(); } catch { /* 忽略 */ } }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function open() {
|
|
163
|
+
// 影院模式下底栏整体被收起,不自作主张弹出
|
|
164
|
+
if (document.documentElement.dataset.cinema === 'on') return;
|
|
165
|
+
// 打开前刷新(背景图列表会随上传/删除变化,不刷新就会列出过期项)
|
|
166
|
+
if (typeof hooks.onBeforeOpen === 'function') { try { hooks.onBeforeOpen(); } catch { /* 刷新失败就用现有选项 */ } }
|
|
167
|
+
list.hidden = false;
|
|
168
|
+
btn.setAttribute('aria-expanded', 'true');
|
|
169
|
+
root.classList.add('is-open');
|
|
170
|
+
const cur = items.findIndex((it) => it.value === current);
|
|
171
|
+
activeIndex = cur >= 0 ? cur : 0;
|
|
172
|
+
paintActive();
|
|
173
|
+
try { (items[activeIndex] || {}).node.focus(); } catch { /* 忽略 */ }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 选中(真实选择):更新显示 + 通知调用方(与原生 change 一致,不重复触发同值) */
|
|
177
|
+
function pick(value, { silent = false } = {}) {
|
|
178
|
+
const hit = items.find((it) => it.value === value);
|
|
179
|
+
if (!hit) return false;
|
|
180
|
+
const changed = value !== current;
|
|
181
|
+
current = value;
|
|
182
|
+
paintLabel();
|
|
183
|
+
paintActive();
|
|
184
|
+
if (!silent && changed) onPick(value);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** 重建选项(换模型/换面时调用):建行细节在模块级 buildPickerItems */
|
|
189
|
+
function setOptions(next, value) {
|
|
190
|
+
items = buildPickerItems(next, (v) => { pick(v); close(true); }, list, (groupId, node) => {
|
|
191
|
+
// 展开二级:把容器交给调用方渲染(内容与右栏同一份 renderSourceGroup)
|
|
192
|
+
items.forEach((it) => it.node.classList.toggle('is-sub', it.node === node));
|
|
193
|
+
sub.hidden = false;
|
|
194
|
+
if (typeof hooks.onSubmenu === 'function') {
|
|
195
|
+
try { hooks.onSubmenu(String(groupId), sub); } catch { closeSub(); }
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
if (typeof value === 'string') current = value;
|
|
199
|
+
if (!items.some((it) => it.value === current)) current = items[0] ? items[0].value : '';
|
|
200
|
+
paintLabel();
|
|
201
|
+
paintActive();
|
|
202
|
+
return items.length;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 触发按钮:点击开合
|
|
206
|
+
btn.addEventListener('click', () => { if (list.hidden) open(); else close(); });
|
|
207
|
+
// 键盘:↓/↑ 移动高亮、Enter 确认、Esc 关闭并回焦(逻辑在模块级 handlePickerKey)
|
|
208
|
+
root.addEventListener('keydown', (ev) => handlePickerKey(ev, {
|
|
209
|
+
isOpen: () => !list.hidden,
|
|
210
|
+
open,
|
|
211
|
+
close: () => close(true),
|
|
212
|
+
count: () => items.length,
|
|
213
|
+
activeIndex: () => activeIndex,
|
|
214
|
+
setActive: (i) => { activeIndex = i; paintActive(); try { (items[i] || {}).node.focus(); } catch { /* 忽略 */ } },
|
|
215
|
+
pickActive: () => { const hit = items[activeIndex]; if (hit) { pick(hit.value); close(true); } },
|
|
216
|
+
}));
|
|
217
|
+
// 点外部收起
|
|
218
|
+
document.addEventListener('click', (ev) => { if (!root.contains(ev.target)) close(); });
|
|
219
|
+
|
|
220
|
+
setOptions(ctl.options && ctl.options.length
|
|
221
|
+
? ctl.options.map((o) => (typeof o === 'string' ? { text: o, value: o } : o))
|
|
222
|
+
: [], current);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
root,
|
|
226
|
+
setOptions,
|
|
227
|
+
setValue: (v) => { current = String(v); paintLabel(); paintActive(); },
|
|
228
|
+
/**
|
|
229
|
+
* 按钮上的"当前项预览"(小色块 / 缩略图)。
|
|
230
|
+
* 用 CSS 变量下发颜色与图片地址(运行时数据,不写内联 background 字面量)。
|
|
231
|
+
* @param {{swatch?:string, thumb?:string}|null} preview 传 null 清掉预览
|
|
232
|
+
*/
|
|
233
|
+
setPreview: (preview) => {
|
|
234
|
+
let block = btn.querySelector('.picker-prev');
|
|
235
|
+
if (!preview) { if (block) block.remove(); return; }
|
|
236
|
+
if (!block) { block = el('span', 'picker-prev'); btn.insertBefore(block, btn.firstChild); }
|
|
237
|
+
if (preview.swatch) block.style.setProperty('--prev-swatch', preview.swatch);
|
|
238
|
+
else block.style.removeProperty('--prev-swatch');
|
|
239
|
+
if (preview.thumb) block.style.setProperty('--prev-thumb', `url("${preview.thumb}")`);
|
|
240
|
+
else block.style.removeProperty('--prev-thumb');
|
|
241
|
+
block.dataset.kind = preview.thumb ? 'thumb' : 'swatch';
|
|
242
|
+
},
|
|
243
|
+
closeSub,
|
|
244
|
+
open,
|
|
245
|
+
close: () => close(false),
|
|
246
|
+
isOpen: () => !list.hidden,
|
|
247
|
+
value: () => current,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 底栏「背景」的一级菜单(与右栏四个档位一一对应)。
|
|
253
|
+
*
|
|
254
|
+
* 结构是**两级**:棋盘格 / 透明 直接选;纯色 ▸ / 自定义 ▸ 展开侧向二级(色块列表 / 图片列表),
|
|
255
|
+
* 二级内容由 panel/bg-source.js 的统一渲染层出,与右栏那份是同一套代码。
|
|
256
|
+
* 这里的初值只用于"数据还没拉回来"的第一帧,真实文案(含当前项名与 ✓)由 refreshBgOptions 重算。
|
|
257
|
+
*/
|
|
258
|
+
export const BG_MODES = [
|
|
259
|
+
{ text: '棋盘格', value: 'mode:checkerboard' },
|
|
260
|
+
{ text: '透明', value: 'mode:transparent' },
|
|
261
|
+
{ text: '纯色', value: 'g:solid', submenu: 'solid' },
|
|
262
|
+
{ text: '自定义', value: 'g:custom', submenu: 'custom' },
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 当前背景在**一级菜单**里对应的值:内置模式用 `mode:<模式>`,纯色/自定义用档位值 `g:<组>`。
|
|
267
|
+
* 具体是哪一条色/图不在这里体现(二级菜单里打勾),一级只标"哪一档"。
|
|
268
|
+
* @param {{mode?:string}} [background] 缺省读当前设置
|
|
269
|
+
* @returns {string}
|
|
270
|
+
*/
|
|
271
|
+
export function currentBgValue(background) {
|
|
272
|
+
const bg = background || (readViewSettings().background || {});
|
|
273
|
+
const mode = String(bg.mode || 'checkerboard');
|
|
274
|
+
if (mode === 'solid') return 'g:solid';
|
|
275
|
+
if (mode === 'custom') return 'g:custom';
|
|
276
|
+
return `mode:${mode === 'transparent' ? 'transparent' : 'checkerboard'}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 应用一个背景选择值。
|
|
281
|
+
* @param {string} value `mode:<模式>` 或 `img:<url>`
|
|
282
|
+
* @returns {boolean} 是否落地
|
|
283
|
+
*/
|
|
284
|
+
/** url → 自定义图名字(切图时状态行要显示人话,而不是一串 url) */
|
|
285
|
+
const bgImageNames = new Map();
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 应用一个背景选择值,并写一条状态(与皮肤/动作切换保持一致:让用户看到"切成了什么")。
|
|
289
|
+
* @param {string} value `mode:<模式>` 或 `img:<url>`
|
|
290
|
+
* @returns {boolean} 是否落地
|
|
291
|
+
*/
|
|
292
|
+
export function applyBgValue(value) {
|
|
293
|
+
const v = String(value || '');
|
|
294
|
+
if (v.startsWith('img:')) {
|
|
295
|
+
const url = v.slice(4);
|
|
296
|
+
const ok = setCustomBackground(url);
|
|
297
|
+
document.dispatchEvent(new CustomEvent('panel:bg-applied', { detail: { mode: 'custom' } }));
|
|
298
|
+
const bgName = bgImageNames.get(url) || '自定义图片';
|
|
299
|
+
if (ok) stageStatus(`背景 → ${bgName}`, 'info', { show: true });
|
|
300
|
+
else stageLog('warn', `背景 → ${bgName}(舞台未就绪)`, { url, reason: 'stage-not-ready' });
|
|
301
|
+
return ok;
|
|
302
|
+
}
|
|
303
|
+
if (v.startsWith('mode:')) {
|
|
304
|
+
const mode = v.slice(5);
|
|
305
|
+
applyBackground(mode); // 内部会广播 panel:bg-applied,底栏/右栏据此同步
|
|
306
|
+
const text = (BG_MODES.find((m) => m.value === `mode:${mode}`) || {}).text || mode;
|
|
307
|
+
stageStatus(`背景 → ${text}`, 'info', { show: true });
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* 重新拉取背景选项(内置 4 模式 + 服务端自定义图,自定义图带分组标题)。
|
|
315
|
+
* 没有自定义图时只列内置 4 项——不出现空的「自定义图片」分组。
|
|
316
|
+
* @param {object} picker createPicker 句柄
|
|
317
|
+
* @returns {Promise<number>} 选项条数
|
|
318
|
+
*/
|
|
319
|
+
export async function refreshBgOptions(picker) {
|
|
320
|
+
if (!picker) return 0;
|
|
321
|
+
const sources = await listBgSources();
|
|
322
|
+
const sel = selectedSourceId(readViewSettings().background || {}, sources);
|
|
323
|
+
// 两组各自找自己的命中条目:不能共用一个 item,否则会出现「自定义 · 暖暗」这种串味
|
|
324
|
+
const findIn = (pool) => (sel.itemId ? (pool || []).find((x) => x.id === sel.itemId) || null : null);
|
|
325
|
+
const solidItem = sel.groupId === 'solid' ? findIn(sources.solid) : null;
|
|
326
|
+
const customItem = sel.groupId === 'custom' ? findIn(sources.custom) : null;
|
|
327
|
+
|
|
328
|
+
const tick = (on) => (on ? '✓ ' : '');
|
|
329
|
+
const opts = [
|
|
330
|
+
{ text: `${tick(sel.groupId === 'builtin' && sel.id === 'checkerboard')}棋盘格`, value: 'mode:checkerboard' },
|
|
331
|
+
{ text: `${tick(sel.groupId === 'builtin' && sel.id === 'transparent')}透明`, value: 'mode:transparent' },
|
|
332
|
+
// 一级只标"哪一档";有具体命中的条目就把名字带出来,按钮回显因此自然是「纯色 · 暖暗」
|
|
333
|
+
{ text: `${tick(sel.groupId === 'solid')}${solidItem ? `纯色 · ${solidItem.name}` : '纯色'}`, value: 'g:solid', submenu: 'solid' },
|
|
334
|
+
{ text: `${tick(sel.groupId === 'custom')}${customItem ? `自定义 · ${customItem.name}` : '自定义'}`, value: 'g:custom', submenu: 'custom' },
|
|
335
|
+
];
|
|
336
|
+
picker.setOptions(opts, currentBgValue());
|
|
337
|
+
// 按钮上的小预览:纯色给色块、自定义给缩略图;内置模式清掉
|
|
338
|
+
if (typeof picker.setPreview === 'function') {
|
|
339
|
+
picker.setPreview(solidItem ? { swatch: solidItem.color }
|
|
340
|
+
: (customItem ? { thumb: customItem.url } : null));
|
|
341
|
+
}
|
|
342
|
+
return opts.length;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* 渲染某一组的二级菜单内容(供底栏 picker 的 onSubmenu 调用)。
|
|
347
|
+
* 与右栏共用 panel/bg-source.js 的同一个渲染函数 ⇒ 两处永远一致。
|
|
348
|
+
* @param {HTMLElement} host 二级容器
|
|
349
|
+
* @param {'solid'|'custom'} groupId
|
|
350
|
+
* @param {(payload:{groupId:string, item:object})=>void} onPick 选中回调
|
|
351
|
+
* @returns {Promise<number>} 条数
|
|
352
|
+
*/
|
|
353
|
+
export async function renderBgSubmenu(host, groupId, onPick) {
|
|
354
|
+
const sources = await listBgSources();
|
|
355
|
+
const sel = selectedSourceId(readViewSettings().background || {}, sources);
|
|
356
|
+
const items = groupId === 'custom' ? sources.custom : sources.solid;
|
|
357
|
+
const { count } = renderSourceGroup(host, groupId, items, {
|
|
358
|
+
variant: 'menu',
|
|
359
|
+
selectedItemId: sel.itemId || null,
|
|
360
|
+
onApply: (item) => { if (typeof onPick === 'function') onPick({ groupId, item }); },
|
|
361
|
+
});
|
|
362
|
+
return count;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 任何来源(右栏档位 / 右栏列表 / 底栏自己)改了背景,都让底栏下拉重新对齐。
|
|
367
|
+
* 只刷新自己、不再往回广播,避免来回触发。
|
|
368
|
+
*/
|
|
369
|
+
export function bindBgSync(picker) {
|
|
370
|
+
if (typeof document === 'undefined') return;
|
|
371
|
+
document.addEventListener('panel:bg-applied', () => {
|
|
372
|
+
refreshBgOptions(picker).catch(() => { /* 刷新失败保持现状 */ });
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|