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,784 @@
|
|
|
1
|
+
// panel-schema.js —— 右侧控制面板 + 底部播放条的**声明式**控件表(M3)
|
|
2
|
+
//
|
|
3
|
+
// 做什么:把"面板里有哪些分组、每组有哪些控件、控件有哪些选项"集中成纯数据,
|
|
4
|
+
// 渲染逻辑(panel.js)与样式(panel.css)都从这份表来。
|
|
5
|
+
// 为什么用声明式:
|
|
6
|
+
// 1. M6 接线时只需给控件补 `id`/取值逻辑,不用再动 DOM 结构;
|
|
7
|
+
// 2. 控件清单可被 test/panel-actions.js 直接枚举核对(数量对齐、disabled 断言);
|
|
8
|
+
// 3. 单个文件不会因为"控件多"而膨胀(分组表 ≈ 数据,不掺渲染代码)。
|
|
9
|
+
//
|
|
10
|
+
// 取值口径(取值域按实际行为实测定,不自己编):
|
|
11
|
+
// · 9 个手风琴分组名、设置组 10 个子项的标题与选项文字逐字照旧;
|
|
12
|
+
// · 控件可声明 `pending: true` 表示暂不可用,渲染成 disabled + data-pending。
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {{kind: string, label?: string, options?: Array<string|{text: string, title?: string}>,
|
|
16
|
+
* buttons?: string[], note?: string, id?: string}} Control
|
|
17
|
+
* @typedef {{id: string, title: string, defaultOpen?: boolean, controls: Control[]}} Group
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 与渲染器无关的控件(无能力信息时唯一允许出现的项)。
|
|
22
|
+
* 为什么需要:面板回落策略是「无能力信息 ⇒ 只显示通用项」,而不是"默认全支持"
|
|
23
|
+
* —— 默认全支持会让"声明了但没实现"的缺陷静默存在(见 docs/契约/渲染面.md §五)。
|
|
24
|
+
*/
|
|
25
|
+
// 滑块的取值域与默认值统一由 slider.js 提供(避免"面板写一套、映射写另一套")
|
|
26
|
+
import { MIX_DEFAULT, MIX_MAX, MIX_MIN, MIX_STEP, SPEED_DEFAULT, SPEED_MAX, SPEED_MIN, SPEED_STEP } from './slider.js';
|
|
27
|
+
|
|
28
|
+
export const RENDERER_INDEPENDENT = [
|
|
29
|
+
// 背景类全部与渲染器无关(外壳行为):纯色预设列表也是外壳数据,不依赖任何渲染能力
|
|
30
|
+
// 纯色/图片不再是独立控件:背景统一走 ctl-bg-mode(模式)+ ctl-bg-sources(公共列表容器),
|
|
31
|
+
// 白名单里原先的 ctl-bg-color / ctl-bg-image 已随改造废弃,故不再列(无声明即无意义)。
|
|
32
|
+
'ctl-theme', 'ctl-bg-mode', 'ctl-bg-sources',
|
|
33
|
+
'ctl-hang-guard', 'ctl-memory-guard', 'ctl-memory-bar', 'ctl-show-status', 'ctl-debug-log', 'ctl-settings-io',
|
|
34
|
+
// 动作组的循环开关由应用层定时器推进、动作列表内容由应用层填,
|
|
35
|
+
// 与"渲染器是否声明某项能力"无关(两面都要有)。
|
|
36
|
+
'ctl-anim-loop', 'ctl-anim-list',
|
|
37
|
+
// 运行时版本读数:只读文本,跟着渲染器自己报的值走,不参与能力判断。
|
|
38
|
+
'ctl-runtime',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/** 占位说明(M3 不接线;M6 会把动态内容填进这些分组) */
|
|
42
|
+
const PLACEHOLDER = (what) => ({ kind: 'note', note: `${what}(M6 接线)` });
|
|
43
|
+
|
|
44
|
+
/** 右侧面板的 9 个分组(顺序) */
|
|
45
|
+
export const PANEL_GROUPS = [
|
|
46
|
+
{
|
|
47
|
+
id: 'motion',
|
|
48
|
+
title: '动作',
|
|
49
|
+
defaultOpen: true,
|
|
50
|
+
controls: [
|
|
51
|
+
// 动作组只剩循环播放一个开关(管"播完接谁")。
|
|
52
|
+
// 「混合模式」开关已移除:过渡强度改由「播放」组的「混合时长」滑块统一控制
|
|
53
|
+
// (0 秒 = 不混合 = 原来的"混合模式关"),两个开关 + 一个时长属于三重控制,多余。
|
|
54
|
+
{ kind: 'switch', label: '循环播放', id: 'ctl-anim-loop', core: true, title: '播完之后接谁:开=按列表顺序连播、末尾回到第一条;关=停留当前动作重复播' },
|
|
55
|
+
{ kind: 'dynamic-list', label: '动作列表', id: 'ctl-anim-list', core: true, note: '选中模型后逐条列出动作,点一条即播放' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'skin',
|
|
60
|
+
title: '皮肤',
|
|
61
|
+
controls: [
|
|
62
|
+
// 「皮肤混合」开关已移除:换肤过渡同样由「播放」组的「混合时长」一个数控制(0 = 不混合)。
|
|
63
|
+
// 皮肤列表:加载模型后按 skeletonData.skins 重建;默认 default
|
|
64
|
+
// capabilityKey: skin —— 渲染面不声明就置灰(Live2D 面没有皮肤概念)
|
|
65
|
+
{ kind: 'chips', layout: 'list', label: '皮肤列表', id: 'ctl-skin-list', core: true, capabilityKey: 'skin', defaultValue: 'default', emptyText: '默认皮肤', options: [{ text: 'default', value: 'default' }] },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'play',
|
|
70
|
+
title: '播放',
|
|
71
|
+
controls: [
|
|
72
|
+
// 顺序速度 → 混合时长(两个连续值滑块,标签左、当前值右)
|
|
73
|
+
// 播放速度:统一入口(spine 缩动画时间轴 / Live2D 缩整帧时间)
|
|
74
|
+
{
|
|
75
|
+
kind: 'slider',
|
|
76
|
+
label: '速度',
|
|
77
|
+
id: 'ctl-speed',
|
|
78
|
+
capabilityKey: 'speed',
|
|
79
|
+
core: true,
|
|
80
|
+
min: SPEED_MIN,
|
|
81
|
+
max: SPEED_MAX,
|
|
82
|
+
step: SPEED_STEP,
|
|
83
|
+
defaultValue: SPEED_DEFAULT,
|
|
84
|
+
format: 'x', // 显示 1.5x
|
|
85
|
+
// 非线性双段:左半 0.1~1.0 细调、右半 1.0~3.0 粗调,1.0 恰在正中(映射见 view.js SLIDER_CURVES)
|
|
86
|
+
curve: 'speed',
|
|
87
|
+
title: '动画播放速率 0.1–3。spine 缩放动画时间轴;Live2D 缩放整帧时间(物理/呼吸/眨眼会一起变快)。双击数值复位 1',
|
|
88
|
+
},
|
|
89
|
+
// 混合时长:切换动作/皮肤时的交叉淡化秒数。**0 秒即关闭混合**(硬切),
|
|
90
|
+
// 取代了原来的两处「混合模式」开关(一个数同时表达"开不开"和"开多久")。
|
|
91
|
+
{
|
|
92
|
+
kind: 'slider',
|
|
93
|
+
label: '混合时长',
|
|
94
|
+
id: 'ctl-mix-time',
|
|
95
|
+
capabilityKey: 'mixTime',
|
|
96
|
+
core: true,
|
|
97
|
+
min: MIX_MIN,
|
|
98
|
+
max: MIX_MAX,
|
|
99
|
+
step: MIX_STEP,
|
|
100
|
+
defaultValue: MIX_DEFAULT,
|
|
101
|
+
format: 's', // 显示 0.85s
|
|
102
|
+
zeroText: '关', // 0 秒显示"关",不写 0.00s(语义更直白)
|
|
103
|
+
title: '切换动作/皮肤时的交叉淡化时长(0–1 秒)。拖到最左(0)=不混合、硬切;双击数值复位 0.2s',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'filter',
|
|
109
|
+
title: '滤镜',
|
|
110
|
+
controls: [
|
|
111
|
+
// id 取自引擎内置预设表(original/bright/soft/mono/warm/cool/fresh/sepia/tealorange/cyber/negative/woodcut)
|
|
112
|
+
{
|
|
113
|
+
kind: 'chips',
|
|
114
|
+
label: '滤镜预设',
|
|
115
|
+
id: 'ctl-filter-preset', capabilityKey: 'presetFilter',
|
|
116
|
+
defaultValue: 'original',
|
|
117
|
+
options: [
|
|
118
|
+
{ text: '原画', value: 'original' },
|
|
119
|
+
{ text: '亮部增强', value: 'bright' },
|
|
120
|
+
{ text: '亮部柔和', value: 'soft' },
|
|
121
|
+
{ text: '黑白', value: 'mono' },
|
|
122
|
+
{ text: '暖色', value: 'warm' },
|
|
123
|
+
{ text: '冷色', value: 'cool' },
|
|
124
|
+
{ text: '日系清新', value: 'fresh' },
|
|
125
|
+
{ text: '棕褐复古', value: 'sepia' },
|
|
126
|
+
{ text: '青橙电影', value: 'tealorange' },
|
|
127
|
+
{ text: '赛博朋克', value: 'cyber' },
|
|
128
|
+
{ text: '负片', value: 'negative' },
|
|
129
|
+
{ text: '版画二值', value: 'woodcut' },
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'pma',
|
|
136
|
+
title: 'PMA 预乘',
|
|
137
|
+
controls: [
|
|
138
|
+
// 的四档:自动推断 / 强制已预乘 / 强制非预乘 / 上传时转预乘(切换都会重传贴图)
|
|
139
|
+
{
|
|
140
|
+
kind: 'segmented',
|
|
141
|
+
label: 'PMA 预乘',
|
|
142
|
+
id: 'ctl-pma', capabilityKey: 'pma',
|
|
143
|
+
// 2×2 网格(用户已经习惯这个排列),下方跟随四行只读诊断
|
|
144
|
+
layout: 'grid-2',
|
|
145
|
+
diag: 'pma',
|
|
146
|
+
options: ['自动', '已预乘', '非预乘', '转预乘'],
|
|
147
|
+
titles: ['按像素抽样自动推断', '强制按已预乘处理', '强制按非预乘处理(运行时 4.1+ 才原生支持,较早版本会降级为转预乘)', '上传时在 GPU 上转成预乘'],
|
|
148
|
+
values: ['auto', 'premultiplied', 'straight', 'upload'],
|
|
149
|
+
defaultValue: 'auto',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: 'runtime',
|
|
155
|
+
title: '运行时',
|
|
156
|
+
controls: [
|
|
157
|
+
// 只读:运行时由模型骨架版本决定,引擎不支持手动切换(切了也只会加载失败)
|
|
158
|
+
{ kind: 'readonly', label: '运行时版本', id: 'ctl-runtime', core: true, note: '按模型骨架版本自动选择' },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'theme',
|
|
163
|
+
title: '主题背景',
|
|
164
|
+
controls: [
|
|
165
|
+
// 主题换的是整站配色(CSS 变量层),背景模式换的是舞台底色,两件独立的事
|
|
166
|
+
{
|
|
167
|
+
// 用 chips 而不是原生 <select>:面板里其它"单选预设组"(滤镜预设等)都是自绘 chips,
|
|
168
|
+
// 原生下拉在视觉上是另一套东西。主题只有 5 项、名字 2~5 字,chips 换行两排完全可接受。
|
|
169
|
+
// 注意:**不要**给它加 capabilityKey —— 主题是外壳行为(在 RENDERER_INDEPENDENT 里),
|
|
170
|
+
// 加了会在 Live2D 面被置灰。
|
|
171
|
+
kind: 'chips',
|
|
172
|
+
label: '主题',
|
|
173
|
+
id: 'ctl-theme', core: true,
|
|
174
|
+
defaultValue: 'dark-cyan',
|
|
175
|
+
options: [
|
|
176
|
+
{ text: '深空青', value: 'dark-cyan' },
|
|
177
|
+
{ text: '暖陶暗色', value: 'eye-gray' },
|
|
178
|
+
{ text: '日间纸', value: 'day-paper' },
|
|
179
|
+
{ text: '霓虹夜', value: 'neon-violet' },
|
|
180
|
+
{ text: '云蓝', value: 'cloud-blue' },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
kind: 'segmented',
|
|
185
|
+
label: '背景模式',
|
|
186
|
+
id: 'ctl-bg-mode', core: true,
|
|
187
|
+
// 带 ▾ 的两项点下去会展开二级列表(纯色预设 / 自定义图片);另两项直接切换
|
|
188
|
+
options: ['棋盘格', '纯色 ▾', '透明', '自定义 ▾'],
|
|
189
|
+
values: ['checkerboard', 'solid', 'transparent', 'custom'],
|
|
190
|
+
defaultValue: 'checkerboard',
|
|
191
|
+
titles: ['棋盘格底纹', '纯色底色', '透明(露出页面底色)', '本地图片背景(选一张图片作为舞台背景)'],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
// 背景来源的**公共列表区**:纯色预设与自定义图片共用这一个位置。
|
|
195
|
+
// 为什么合并(原来上下一处一个,切换要上下找):切组时视线不动、按钮行不位移。
|
|
196
|
+
// label 留空 —— 具体组名与数量由渲染层在内容顶部出小标题(「纯色预设 · 5 个」)。
|
|
197
|
+
kind: 'dynamic-list',
|
|
198
|
+
label: '',
|
|
199
|
+
id: 'ctl-bg-sources', core: true,
|
|
200
|
+
},
|
|
201
|
+
],
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'debug',
|
|
205
|
+
title: '调试绘制',
|
|
206
|
+
controls: [
|
|
207
|
+
// 7 个独立开关(可同时开);键名与引擎 debugDraw 的字段一一对应
|
|
208
|
+
// 注:不含「点(points)」——各版本 Spine 运行时的 SkeletonDebugRenderer 都没有 drawPoints,
|
|
209
|
+
// 该项永远画不出来,列入声明会让面板出现一个永远无效的开关
|
|
210
|
+
{ kind: 'switch', label: '骨骼', id: 'ctl-debug-bones', debugKey: 'bones' },
|
|
211
|
+
{ kind: 'switch', label: '网格', id: 'ctl-debug-meshes', debugKey: 'meshes' },
|
|
212
|
+
{ kind: 'switch', label: '包围盒', id: 'ctl-debug-boundingBoxes', debugKey: 'boundingBoxes' },
|
|
213
|
+
{ kind: 'switch', label: '路径', id: 'ctl-debug-paths', debugKey: 'paths' },
|
|
214
|
+
{ kind: 'switch', label: '区域', id: 'ctl-debug-regions', debugKey: 'regions' },
|
|
215
|
+
{ kind: 'switch', label: '裁剪', id: 'ctl-debug-clipping', debugKey: 'clipping' },
|
|
216
|
+
{ kind: 'switch', label: '外壳', id: 'ctl-debug-hulls', debugKey: 'hulls' },
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'settings',
|
|
221
|
+
title: '设置',
|
|
222
|
+
defaultOpen: true,
|
|
223
|
+
controls: [
|
|
224
|
+
// values 与 options 一一对应:面板只负责把"点了哪个选项"翻成机器值,再交给接线方
|
|
225
|
+
{ kind: 'segmented', label: '渲染帧率', id: 'ctl-fps', capabilityKey: 'fps', options: ['90 FPS', '60 FPS', '30 FPS'], values: [90, 60, 30], defaultValue: 60, readout: true },
|
|
226
|
+
{ kind: 'segmented', label: '渲染像素比(DPR)', id: 'ctl-dpr', capabilityKey: 'dpr', options: ['自动', '1', '1.5', '2'], values: ['auto', 1, 1.5, 2], defaultValue: 'auto' },
|
|
227
|
+
{
|
|
228
|
+
kind: 'segmented',
|
|
229
|
+
label: '初始缩放',
|
|
230
|
+
id: 'ctl-fit', capabilityKey: 'fit',
|
|
231
|
+
values: ['subject', 'all', 'animation'],
|
|
232
|
+
defaultValue: 'subject',
|
|
233
|
+
options: [
|
|
234
|
+
{ text: '主体优先', title: '排除游离特效/背景,以人物主体适配(推荐)' },
|
|
235
|
+
{ text: '全部部件', title: '以所有部件的整体包围盒适配' },
|
|
236
|
+
{ text: '动画全程', title: '采样全部动画帧,保证播放过程不超出屏幕' },
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
// 默认「最邻近」(不是双线性)—— 与实机观感一致
|
|
240
|
+
{ kind: 'segmented', label: '纹理过滤', id: 'ctl-filter', capabilityKey: 'textureFilter', options: ['双线性', '最邻近'], values: ['linear', 'nearest'], defaultValue: 'nearest' },
|
|
241
|
+
{ kind: 'segmented', label: '抗锯齿(MSAA)', id: 'ctl-msaa', capabilityKey: 'msaa', options: ['自动', '关闭'], values: ['auto', 'off'], defaultValue: 'auto' },
|
|
242
|
+
// 注:不设「模型导入模式(替换/追加)」——本项目的模型来源是资源根目录扫描,
|
|
243
|
+
// 不存在"往当前场景导入单个模型"这条路径,留着会是一个点了没意义的入口
|
|
244
|
+
{
|
|
245
|
+
// 内存容量保护:与下面「防卡死保护」并列但**不是一回事**
|
|
246
|
+
// (前者防 OOM / 页面崩溃;后者防画面停住没人知道)。护」这个开关历史上绑的是内存预检,
|
|
247
|
+
// 本实现沿用同名的那个是帧停滞看门狗,所以这里新开一项,不改 hang-guard 的语义。
|
|
248
|
+
kind: 'switch',
|
|
249
|
+
label: '内存保护(解包准入 + 堆占用)',
|
|
250
|
+
id: 'ctl-memory-guard', core: true, defaultOn: true,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
// 内存三色条容器:内容由 panel/memory-bar.js 填(总 / 可用 / 服务端已占用)。
|
|
254
|
+
// 与舞台左下状态行那条是**同一个组件**,不复制实现。
|
|
255
|
+
kind: 'dynamic-list',
|
|
256
|
+
label: '',
|
|
257
|
+
id: 'ctl-memory-bar', core: true,
|
|
258
|
+
},
|
|
259
|
+
{ kind: 'switch', label: '防卡死保护', id: 'ctl-hang-guard', core: true, defaultOn: true },
|
|
260
|
+
{ kind: 'switch', label: '显示状态信息', id: 'ctl-show-status', core: true, defaultOn: true },
|
|
261
|
+
{ kind: 'switch', label: '调试日志', id: 'ctl-debug-log', core: true, defaultOn: true },
|
|
262
|
+
{
|
|
263
|
+
kind: 'actions',
|
|
264
|
+
id: 'ctl-settings-io',
|
|
265
|
+
core: true,
|
|
266
|
+
buttons: [
|
|
267
|
+
{ text: '导出设置', title: '把当前面板设置导出成 JSON 文件' },
|
|
268
|
+
{ text: '导入设置', title: '导入之前导出的 JSON(项目配置走 POST /api/config/import,前端偏好写本地存储)' },
|
|
269
|
+
{ text: '恢复默认', title: '清空本地保存的设置、面板回到声明默认值并复位视图' },
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
];
|
|
275
|
+
|
|
276
|
+
/** 底部播放条的控件。
|
|
277
|
+
*
|
|
278
|
+
* **这里的数组顺序就是底栏的显示顺序**,分组不用另写一份:
|
|
279
|
+
* 标了 `center: true` 的控件单独成中块,它之前的都归左块、之后的都归右块
|
|
280
|
+
* (见 panel/transport.js 的派生逻辑)。调整位置只改这个数组即可。
|
|
281
|
+
*
|
|
282
|
+
* 当前顺序:主题背景 · 上一个 · 皮肤列表 · 暂停播放 · 动作列表 · 下一个 · 重置视图
|
|
283
|
+
*/
|
|
284
|
+
export const TRANSPORT_CONTROLS = [
|
|
285
|
+
{ kind: 'select', custom: 'picker', id: 'transport-bg', label: '背景', title: '选择舞台背景(内置模式或已上传的自定义图)', value: 'mode:checkerboard', options: [] },
|
|
286
|
+
{ kind: 'button', id: 'transport-prev', label: '上一个', title: '上一个' },
|
|
287
|
+
{ kind: 'select', custom: 'picker', id: 'transport-skin', label: '皮肤', value: 'default', options: ['default'] },
|
|
288
|
+
// 文案与图标同义(写"点了会做什么"):初始为播放中 ⇒ 图标是暂停符号、文案「暂停」;
|
|
289
|
+
// 运行时由 transport.setIcon 随状态同步,不再写死"播放/暂停"。
|
|
290
|
+
// center: true ⇒ 这一项固定落在底栏正中(其余控件自动分到左右两块)。
|
|
291
|
+
{ kind: 'button', id: 'transport-play', label: '暂停', title: '暂停', center: true },
|
|
292
|
+
// 动作下拉不预置任何真实动作:选项由模型动作列表填充(meta.motions / skeletonData.animations)。
|
|
293
|
+
// 这里的单条是**占位文案**(value 为空,不会被当成可播动作),M6 接线时整体替换 options。
|
|
294
|
+
// 自绘下拉(custom:'picker'):原生 select 无法统一视觉与键盘语义
|
|
295
|
+
{ kind: 'select', custom: 'picker', id: 'transport-motion', label: '动作', value: '', options: ['(选中模型后列出动作)'] },
|
|
296
|
+
{ kind: 'button', id: 'transport-next', label: '下一个', title: '下一个' },
|
|
297
|
+
{ kind: 'button', id: 'transport-reset-view', label: '重置视图', title: '重置视图' },
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* 按 id 前缀推导能力键(**兜底路径**,只在控件没有显式声明时使用)。
|
|
302
|
+
* 推导规则:
|
|
303
|
+
* · `ctl-debug-<key>` → 调试项 { kind: 'debug', key }(如 ctl-debug-bones → bones)
|
|
304
|
+
* · `ctl-<key>` → 设置项 { kind: 'setting', key }(如 ctl-fps → fps)
|
|
305
|
+
* · 其它形态 → null(推导不出来,不猜;调用方按"不支持"处理)
|
|
306
|
+
* @param {string} id 控件 id
|
|
307
|
+
* @returns {{kind: string, key: string}|null}
|
|
308
|
+
*/
|
|
309
|
+
/**
|
|
310
|
+
* 插件控件的 **kind 白名单**(契约 §9.2)。未列出的一律拒绝:
|
|
311
|
+
* 插件只能提交声明式数据,不能借控件形态塞任意 DOM / 内联样式 / 事件处理器字符串。
|
|
312
|
+
*/
|
|
313
|
+
export const PLUGIN_CONTROL_KINDS = ['segmented', 'toggle', 'button'];
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 插件控件 kind → 渲染层 kind 的**唯一映射点**。
|
|
317
|
+
*
|
|
318
|
+
* 为什么必须有这一层:插件契约用的是 `segmented / toggle / button`,
|
|
319
|
+
* 而渲染层(view.js 的 renderControl)认的是 `segmented / switch / actions / …`。
|
|
320
|
+
* 两套词汇直接撞的后果实测过:`toggle`/`button` 落到 view 的 default 分支,
|
|
321
|
+
* 渲染成一个空 `<p class="ctl-note">` —— 没有 data-ctl、没有 label,
|
|
322
|
+
* 表现就是"控件消失了"(置灰原因也一起丢了,违反契约 §9.3 的降级可见)。
|
|
323
|
+
* 新增 kind 时只改这里,两路(老接口 registerControls 与插槽)同时生效。
|
|
324
|
+
*/
|
|
325
|
+
const PLUGIN_KIND_TO_VIEW = { segmented: 'segmented', toggle: 'switch', button: 'actions' };
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* 校验并归一"插件提交的一节控件"。
|
|
329
|
+
*
|
|
330
|
+
* 分三级处置(契约 §9.2 / §9.3 的差别就在这儿):
|
|
331
|
+
* · **整节拒绝**(返回 ok:false):group 名不存在、controls 不是数组或为空 —— 绝不静默新建分组,
|
|
332
|
+
* 否则插件一多就长出一堆没人认识的分组;
|
|
333
|
+
* · **单控件拒绝**(进 rejects):kind 不在白名单、缺 id、id 与已占用重名、segmented 缺 options;
|
|
334
|
+
* · **单控件置灰**(进 disabled):id 在清单 effects[] 里声明过,但 registerEffect 没成功 ——
|
|
335
|
+
* 这类要出现在界面上并给出原因,而不是消失(消失后用户会以为自己记错了)。
|
|
336
|
+
*
|
|
337
|
+
* @param {{pluginId:string, group:string, controls:Array}} section 宿主收到的原始节
|
|
338
|
+
* @param {object} ctx
|
|
339
|
+
* @param {Set<string>} ctx.groupTitles 既有分组名集合(用 title 匹配,与契约示例一致)
|
|
340
|
+
* @param {Set<string>} ctx.registeredEffects 注册成功的效果 id
|
|
341
|
+
* @param {Set<string>} ctx.takenIds 已被占用的控件 id(内置 + 先前插件)
|
|
342
|
+
* @returns {{ok:boolean, reason?:string, title?:string, controls?:Array, rejects?:Array}}
|
|
343
|
+
*/
|
|
344
|
+
export function inferCapabilityKey(id) {
|
|
345
|
+
if (typeof id !== 'string') return null;
|
|
346
|
+
const debugMatch = /^ctl-debug-(.+)$/.exec(id);
|
|
347
|
+
if (debugMatch) return { kind: 'debug', key: debugMatch[1] };
|
|
348
|
+
const settingMatch = /^ctl-(.+)$/.exec(id);
|
|
349
|
+
if (settingMatch) return { kind: 'setting', key: settingMatch[1] };
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 解析控件对应的能力键:**显式声明永远优先**,推导只是兜底且必须告警。
|
|
355
|
+
* · debugKey / capabilityKey 有显式声明 → 直接采用(不做任何推导);
|
|
356
|
+
* · core === true(主题/背景/运行时/IO 等核心项)→ 返回 null,不参与能力过滤;
|
|
357
|
+
* · 其余:按 id 推导;成功与失败都发 warn(成功时提示补显式声明)。
|
|
358
|
+
* @param {object} ctl 控件
|
|
359
|
+
* @param {(message: string) => void} [onWarn] 告警出口(缺省静默)
|
|
360
|
+
* @returns {{kind: string, key: string}|null} null 表示核心项
|
|
361
|
+
*/
|
|
362
|
+
export function resolveCapabilityKey(ctl, onWarn) {
|
|
363
|
+
const warn = typeof onWarn === 'function' ? onWarn : () => {};
|
|
364
|
+
if (ctl && typeof ctl.debugKey === 'string' && ctl.debugKey) return { kind: 'debug', key: ctl.debugKey };
|
|
365
|
+
if (ctl && typeof ctl.capabilityKey === 'string' && ctl.capabilityKey) return { kind: 'setting', key: ctl.capabilityKey };
|
|
366
|
+
if (ctl && ctl.core === true) return null;
|
|
367
|
+
const id = ctl && ctl.id ? ctl.id : '';
|
|
368
|
+
const inferred = inferCapabilityKey(id);
|
|
369
|
+
if (inferred) {
|
|
370
|
+
warn(`面板控件 ${id} 缺少能力键声明,已按 id 推导为 ${inferred.kind}.${inferred.key}(建议补显式声明)`);
|
|
371
|
+
return inferred;
|
|
372
|
+
}
|
|
373
|
+
warn(`面板控件 ${id || (ctl && ctl.label) || '(无 id)'} 无法确定能力键,按“不支持”处理`);
|
|
374
|
+
return { kind: 'unsupported', key: '' };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* 按渲染器能力过滤面板 schema(纯函数,不改原 schema)。
|
|
379
|
+
* 规则:
|
|
380
|
+
* · 调试项:不在 capabilities.debugDraw 里 ⇒ 整项隐藏;
|
|
381
|
+
* · 设置项:不在 capabilities.settings 里 ⇒ disabled + reason;
|
|
382
|
+
* · 核心项(core: true):原样保留;
|
|
383
|
+
* · 推导不出能力键:disabled + reason(不猜)并告警;
|
|
384
|
+
* · 过滤后为空的分组整组移除。
|
|
385
|
+
* @param {Array} groups 面板分组
|
|
386
|
+
* @param {{settings?: string[], debugDraw?: string[]}} capabilities 渲染器能力
|
|
387
|
+
* @param {{onWarn?: (message: string) => void}} [options] 告警出口
|
|
388
|
+
* @returns {Array} 过滤后的新分组数组
|
|
389
|
+
*/
|
|
390
|
+
export function applyCapabilities(groups, capabilities, options = {}) {
|
|
391
|
+
const hasCaps = !!(capabilities && Array.isArray(capabilities.settings));
|
|
392
|
+
const settings = new Set((capabilities && capabilities.settings) || []);
|
|
393
|
+
const debugDraw = new Set((capabilities && capabilities.debugDraw) || []);
|
|
394
|
+
const onWarn = options && typeof options.onWarn === 'function' ? options.onWarn : null;
|
|
395
|
+
// keepDisabled:即使能力不匹配也要**保留**的控件 id(插件已声明、但 registerEffect 没成功的那些)。
|
|
396
|
+
// 为什么单独开这个口:面板能力契约 §3.1 规定"不支持的能力项根本不构建",
|
|
397
|
+
// 但插件契约 §9.3 要求相反 —— 已声明未生效的控件必须**置灰并给出原因**,
|
|
398
|
+
// 消失会让用户以为自己记错了。两种诉求都成立,所以用显式白名单区分,而不是放宽统一规则。
|
|
399
|
+
const keepDisabled = options && options.keepDisabled instanceof Set ? options.keepDisabled : null;
|
|
400
|
+
const reason = '当前渲染器不支持';
|
|
401
|
+
const list = Array.isArray(groups) ? groups : [];
|
|
402
|
+
const independent = new Set(RENDERER_INDEPENDENT);
|
|
403
|
+
return list
|
|
404
|
+
.map((group) => {
|
|
405
|
+
const controls = [];
|
|
406
|
+
for (const ctl of group.controls || []) {
|
|
407
|
+
// ① 手动隐藏(最高优先级):隐藏即**永不构建**,两面都不出现,不再看能力声明。
|
|
408
|
+
// 设计意图:有些开关产品上就不想让用户改(如动作混合时长),
|
|
409
|
+
// 与"这一面不支持"是两件事,所以它排在最前、且不受 capabilities 影响。
|
|
410
|
+
if (ctl && ctl.hidden === true) continue;
|
|
411
|
+
// 回落策略(契约 §五):拿不到能力信息 ⇒ 按「无渲染器」处理,只保留与渲染器无关的项。
|
|
412
|
+
// 判据用显式清单而不是 core 标记:皮肤/部件筛除这类也标了 core,但它们依赖渲染器。
|
|
413
|
+
// 旧行为是"默认全支持",会让"声明了却没实现"的缺陷静默存在。
|
|
414
|
+
if (!hasCaps) {
|
|
415
|
+
if (ctl && ctl.id && independent.has(ctl.id)) controls.push({ ...ctl });
|
|
416
|
+
else if (!ctl || !ctl.id) controls.push({ ...ctl }); // 无 id 的纯展示项(说明/列表占位)保留
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const resolved = resolveCapabilityKey(ctl, onWarn);
|
|
420
|
+
if (!resolved) { controls.push({ ...ctl }); continue; } // 核心项原样保留
|
|
421
|
+
if (resolved.kind === 'debug') {
|
|
422
|
+
if (debugDraw.has(resolved.key)) controls.push({ ...ctl });
|
|
423
|
+
continue; // 不支持 ⇒ 整项隐藏(不是禁用)
|
|
424
|
+
}
|
|
425
|
+
if (resolved.kind === 'unsupported') {
|
|
426
|
+
controls.push({ ...ctl, disabled: true, reason: '无法确定能力键' });
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// 当前渲染器没有这项能力 ⇒ **不构建**(面板里根本不存在这个控件)。
|
|
430
|
+
// 依据:面板能力契约 §3.1「能力不支持的项必须根本不构建(无 DOM、无监听、无样式计算)」;
|
|
431
|
+
// §四十九「这类东西对这种模型不存在 → 不加载」。
|
|
432
|
+
// 注意别和"隐藏"混淆:隐藏=先构建再 display:none(契约明令禁止);
|
|
433
|
+
// 这里是不进 controls,等于面板里从来没有这一项——Live2D 没有 PMA 口径、没有 MSAA
|
|
434
|
+
// 上下文级开关、没有骨架 fit 三模式,就不该在 Live2D 面画出这几项。
|
|
435
|
+
if (!settings.has(resolved.key)) {
|
|
436
|
+
// 能力不匹配:默认不构建;插件已声明项例外 —— 保留并置灰,让"为什么没有生效"可见
|
|
437
|
+
if (keepDisabled && ctl.id && keepDisabled.has(String(ctl.id))) {
|
|
438
|
+
controls.push({ ...ctl, disabled: true, reason: ctl.disabledReason || reason });
|
|
439
|
+
}
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
controls.push({ ...ctl });
|
|
443
|
+
}
|
|
444
|
+
return { ...group, controls };
|
|
445
|
+
})
|
|
446
|
+
.filter((group) => group.controls.length > 0);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* 把插槽登记(`panel.group.<groupId>`)合进面板分组表 —— **纯函数,返回新数组**。
|
|
451
|
+
*
|
|
452
|
+
* 为什么在装配期(panel-settings.js 调用)而不是渲染期:
|
|
453
|
+
* · 渲染层(view.js)只认"拿到什么画什么",它不该知道"控件是插件来的";
|
|
454
|
+
* · 审批(拒绝/warn)、能力白名单并入、SPEC 登记都发生在装配期,聚合放这里才同源;
|
|
455
|
+
* · 结果**绝不写回** `PANEL_GROUPS` 静态导出 —— 那份同时被 uiState SPEC 派生与浏览器测试的
|
|
456
|
+
* 控件分类门禁遍历,写回会让插件控件被当成"必须显式归类"的内置控件(误报孤儿能力键)。
|
|
457
|
+
*
|
|
458
|
+
* 排序:组内先内置(保持原序),插件控件按 order 升序追加在后(缺省 1000)。
|
|
459
|
+
* 冲突:同一组内 id 重名 ⇒ 后者被拒 + onReject 回报(不静默覆盖)。
|
|
460
|
+
*
|
|
461
|
+
* @param {Array} baseGroups 内置分组(或用别的聚合结果做底)
|
|
462
|
+
* @param {Array<{pluginId:string,name:string,id:string,order:number,label:string,spec:object}>} entries 插槽登记
|
|
463
|
+
* @param {{onReject?: (rej: object) => void}} [opts]
|
|
464
|
+
* @returns {{groups: Array, added: number, rejected: number, capabilityKeys: string[]}}
|
|
465
|
+
*/
|
|
466
|
+
/**
|
|
467
|
+
* 键值插槽 `panel.control.<key>`:**替换**已存在的内置控件(设计 §8.2)。
|
|
468
|
+
*
|
|
469
|
+
* 与分组插槽的分工:分组插槽"加控件",键值插槽"换控件"——所以这里
|
|
470
|
+
* · 只按 target(slot 名后缀)找**既有**控件,找不到就跳过(注册期已拒,这里不制造孤儿)
|
|
471
|
+
* · 替换后仍用**原控件 id**:接线(WIRED_IDS)、能力键、禁用判定都按 id 走,
|
|
472
|
+
* 换 id 等于把这三处一起打断(那是"新建"不是"替换")
|
|
473
|
+
* @param {Array} groups 已合并的(内置 + 分组插槽)控件表
|
|
474
|
+
* @param {Array} entries 插槽登记项
|
|
475
|
+
* @returns {{groups: Array, replaced: number}} 新数组,不改入参
|
|
476
|
+
*/
|
|
477
|
+
export function applyKeyedControlReplacements(groups, entries) {
|
|
478
|
+
const list = (Array.isArray(entries) ? entries : [])
|
|
479
|
+
.filter((e) => e && /^panel\.control\./.test(String(e.name || '')));
|
|
480
|
+
const out = (Array.isArray(groups) ? groups : []).map((g) => ({ ...g, controls: (g.controls || []).slice() }));
|
|
481
|
+
if (!list.length) return { groups: out, replaced: 0, controls: [] };
|
|
482
|
+
let replaced = 0;
|
|
483
|
+
/** 被替换的控件清单(装配层要拿它登记"控件 id → 插件设置键"的映射与 SPEC 条目) */
|
|
484
|
+
const controls = [];
|
|
485
|
+
for (const entry of list) {
|
|
486
|
+
const target = String(entry.target || '');
|
|
487
|
+
if (!target) continue;
|
|
488
|
+
const desc = slotSpecToControl(entry, { settings: {} });
|
|
489
|
+
if (!desc) continue;
|
|
490
|
+
for (const g of out) {
|
|
491
|
+
const i = (g.controls || []).findIndex((c) => String((c && c.id) || '') === target);
|
|
492
|
+
if (i < 0) continue;
|
|
493
|
+
const base = g.controls[i];
|
|
494
|
+
// 合并口径:形状取插件的(选项集/类型可不同),身份与能力继承内置的(id / capabilityKey / settingKey)
|
|
495
|
+
const next = {
|
|
496
|
+
...base,
|
|
497
|
+
...desc,
|
|
498
|
+
id: base.id,
|
|
499
|
+
// **值域与身份绑定**:替换型控件只换形状,存值键改成插件自己的 setting
|
|
500
|
+
// (内置控件那条 SPEC 条目原样保留 ⇒ 内置键全程没人写,卸载后连"恢复"都不需要)
|
|
501
|
+
setting: String(entry.setting || ''),
|
|
502
|
+
label: String(entry.label || base.label || base.id),
|
|
503
|
+
pluginId: String(entry.pluginId || ''),
|
|
504
|
+
replacedBy: String(entry.pluginId || ''),
|
|
505
|
+
// 插件的 onAction 在装配层登记(这里只留一份引用,便于排障)
|
|
506
|
+
onAction: typeof (entry.spec && entry.spec.onAction) === 'function' ? entry.spec.onAction : base.onAction,
|
|
507
|
+
};
|
|
508
|
+
g.controls[i] = next;
|
|
509
|
+
// 回报的是**完整描述子**(含 kind/options/values/defaultValue):
|
|
510
|
+
// 装配层要拿它给"插件设置键"登记 SPEC 条目 —— 缺了这些字段,工厂会以
|
|
511
|
+
// "无法确定默认值"为由拒登(表现:点了没反应、值根本存不进去)
|
|
512
|
+
controls.push({ ...next });
|
|
513
|
+
replaced += 1;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return { groups: out, replaced, controls };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function mergeSlotControls(baseGroups, entries, opts = {}) {
|
|
521
|
+
const base = Array.isArray(baseGroups) ? baseGroups : [];
|
|
522
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
523
|
+
// 注意括号位置:要判的是"onReject 是不是函数",不是 `typeof (opts && .... === 'function')`
|
|
524
|
+
// ——后者恒为字符串 "boolean"(真值),会把 undefined 赋给 report,调用时抛 "report is not a function"。
|
|
525
|
+
const report = (opts && typeof opts.onReject === 'function') ? opts.onReject : () => {};
|
|
526
|
+
if (!list.length) return { groups: base, added: 0, rejected: 0, capabilityKeys: [] };
|
|
527
|
+
|
|
528
|
+
const groups = base.map((g) => ({ ...g, controls: (g.controls || []).slice() }));
|
|
529
|
+
const byId = new Map(groups.map((g) => [String(g.id || ''), g]));
|
|
530
|
+
const taken = new Set();
|
|
531
|
+
for (const g of groups) for (const c of g.controls || []) if (c && c.id) taken.add(String(c.id));
|
|
532
|
+
|
|
533
|
+
const acc = {
|
|
534
|
+
added: 0, rejected: 0, capabilityKeys: [], byGroup: new Map(),
|
|
535
|
+
// 内置组标题:新建分组允许与之同名,但要在诊断里提示(设计 §七·一)
|
|
536
|
+
builtinTitles: new Set(groups.map((g) => String(g.title || ''))),
|
|
537
|
+
// 新建的插件分组(最后统一追加到所有内置组之后)
|
|
538
|
+
newGroups: [], seq: 0,
|
|
539
|
+
};
|
|
540
|
+
for (const entry of list) resolveSlotEntry(entry, taken, byId, acc, report, opts);
|
|
541
|
+
|
|
542
|
+
for (const [groupId, bucket] of acc.byGroup) {
|
|
543
|
+
const group = byId.get(groupId);
|
|
544
|
+
if (!group) continue;
|
|
545
|
+
bucket.sort((a, b) => a.order - b.order); // order 小在前;相同 order 按注册先后(sort 稳定)
|
|
546
|
+
group.controls = [...group.controls, ...bucket.map((b) => b.ctl)];
|
|
547
|
+
}
|
|
548
|
+
// 内置组 order 0~99 保持在原位;插件组缺省 1000,按 order 再按注册先后追加在最后
|
|
549
|
+
acc.newGroups.sort((a, b) => a.order - b.order || a.seq - b.seq);
|
|
550
|
+
for (const ng of acc.newGroups) groups.push(ng.group);
|
|
551
|
+
return { groups, added: acc.added, rejected: acc.rejected, capabilityKeys: acc.capabilityKeys };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* 处理单条插槽登记:校验分组/重名/render 形状,合格就按组分桶(排序交给调用方)。
|
|
556
|
+
* 抽出来的理由:mergeSlotControls 主体要读得动,且"哪条为什么被拒"集中在这一处。
|
|
557
|
+
* @param {object} entry 插槽登记
|
|
558
|
+
* @param {Set<string>} taken 已被占用的控件 id(含内置与本轮已加的)
|
|
559
|
+
* @param {Map<string, object>} byId groupId → 分组
|
|
560
|
+
* @param {{added:number, rejected:number, capabilityKeys:string[], byGroup:Map}} acc 累加器
|
|
561
|
+
* @param {(rej: object) => void} report 拒绝回报
|
|
562
|
+
*/
|
|
563
|
+
function resolveSlotEntry(entry, taken, byId, acc, report, opts = {}) {
|
|
564
|
+
const m = /^panel\.group\.(.+)$/.exec(String((entry && entry.name) || ''));
|
|
565
|
+
if (!m) {
|
|
566
|
+
acc.rejected += 1;
|
|
567
|
+
report({ pluginId: entry && entry.pluginId, slot: entry && entry.name, reason: '该插槽类型暂不参与渲染(只有 panel.group.* 进面板)' });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const groupId = m[1];
|
|
571
|
+
let group = byId.get(groupId);
|
|
572
|
+
if (!group) {
|
|
573
|
+
group = createPluginGroup(entry, groupId, byId, acc, report);
|
|
574
|
+
if (!group) return;
|
|
575
|
+
}
|
|
576
|
+
const id = String((entry && entry.id) || '');
|
|
577
|
+
if (!id || taken.has(id)) {
|
|
578
|
+
acc.rejected += 1;
|
|
579
|
+
report({ pluginId: entry && entry.pluginId, slot: entry && entry.name, id, reason: taken.has(id) ? `控件 id「${id}」与已有控件重名` : '控件缺 id' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
// owner props:settings 传**真实值**(当前值 ?? 声明默认值),S4 会再加 geometry/activeSurface/document
|
|
583
|
+
const props = typeof opts.buildProps === 'function' ? opts.buildProps(entry) : {};
|
|
584
|
+
const desc = slotSpecToControl(entry, props);
|
|
585
|
+
if (!desc) {
|
|
586
|
+
acc.rejected += 1;
|
|
587
|
+
report({ pluginId: entry && entry.pluginId, slot: entry && entry.name, id, reason: 'render() 没有返回可用的控件描述(kind 必须是已知类型)' });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
// 老接口迁移来的控件带 effectId:效果没注册成功 ⇒ 置灰 + 原因(与迁移前语义一致,契约 §9.3)
|
|
591
|
+
const effectId = String((entry && entry.spec && entry.spec.effectId) || '');
|
|
592
|
+
const registered = new Set((opts && opts.registeredEffects) || []);
|
|
593
|
+
const effectMissing = !!effectId && !registered.has(effectId);
|
|
594
|
+
const ctl = {
|
|
595
|
+
...desc,
|
|
596
|
+
id,
|
|
597
|
+
label: String((entry && entry.label) || id),
|
|
598
|
+
pluginId: String((entry && entry.pluginId) || ''),
|
|
599
|
+
// 存值键:给了 spec.setting 就用它(与清单设置键统一),否则沿用 control id(兼容既有插件)
|
|
600
|
+
setting: String((entry && entry.setting) || ''),
|
|
601
|
+
...(effectMissing ? {
|
|
602
|
+
disabled: true,
|
|
603
|
+
// reason 与 disabledReason 两个键都要给:
|
|
604
|
+
// applyCapabilities 读 disabledReason,mountPanel 建 disabledMap 时读的是 reason ——
|
|
605
|
+
// 只给一个会出现"置灰了但文案是通用的「当前渲染器不支持」"(实测踩到)。
|
|
606
|
+
reason: `效果「${effectId}」未注册成功(插件脚本可能加载失败或被禁用)`,
|
|
607
|
+
disabledReason: `效果「${effectId}」未注册成功(插件脚本可能加载失败或被禁用)`,
|
|
608
|
+
} : {}),
|
|
609
|
+
// 能力键显式声明为该控件 id:装配期把 id 并进 capabilities.settings,渲染层据此认它
|
|
610
|
+
capabilityKey: id,
|
|
611
|
+
// 插件的动作回调(面板点击时派发;由装配层负责调用并兜住异常)
|
|
612
|
+
onAction: typeof (entry.spec || {}).onAction === 'function' ? entry.spec.onAction : null,
|
|
613
|
+
};
|
|
614
|
+
taken.add(id);
|
|
615
|
+
acc.capabilityKeys.push(id);
|
|
616
|
+
acc.added += 1;
|
|
617
|
+
const bucket = acc.byGroup.get(groupId) || [];
|
|
618
|
+
bucket.push({ ctl, order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 1000 });
|
|
619
|
+
acc.byGroup.set(groupId, bucket);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* 插件新建分组(设计 §七·一):id 必须带 `plugin.<pluginId>.` 前缀,标题由插件给。
|
|
624
|
+
*
|
|
625
|
+
* 规则与理由:
|
|
626
|
+
* · 前缀强制 —— 两个插件撞同一个分组 id 时,光看 id 就能判归属;不要求前缀的话
|
|
627
|
+
* "pluginA 建了 settings2、pluginB 也想建 settings2"只能靠注册顺序决定谁赢,排查时无从下手。
|
|
628
|
+
* · 允许与内置组**同名**(title 相同、id 不同)—— 标题是给人看的,插件想叫「设置」不算错;
|
|
629
|
+
* 但要在诊断里提示一句,避免"我以为在改内置组"这种误会。
|
|
630
|
+
* · 缺省 order 1000(内置组 0~99)⇒ 永远排在内置组之后;同 order 按注册先后。
|
|
631
|
+
* @returns {object|null} 建好的分组;被拒返回 null(原因已 report)
|
|
632
|
+
*/
|
|
633
|
+
function createPluginGroup(entry, groupId, byId, acc, report) {
|
|
634
|
+
const pid = String((entry && entry.pluginId) || '');
|
|
635
|
+
const prefix = `plugin.${pid}.`;
|
|
636
|
+
if (!pid || !groupId.startsWith(prefix) || groupId.length <= prefix.length) {
|
|
637
|
+
acc.rejected += 1;
|
|
638
|
+
report({
|
|
639
|
+
pluginId: pid || '未知插件', slot: entry && entry.name, code: 'bad-group-id',
|
|
640
|
+
reason: `分组「${groupId}」不存在;新建分组的 id 必须写成 plugin.<pluginId>.<name>`
|
|
641
|
+
+ `(当前插件 ${pid || '未知'},应形如 ${pid ? prefix + '<name>' : 'plugin.<pluginId>.<name>'})`,
|
|
642
|
+
});
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const spec = (entry && entry.spec) || {};
|
|
646
|
+
const title = String(spec.groupLabel || spec.title || groupId.slice(prefix.length));
|
|
647
|
+
if (acc.builtinTitles.has(title)) {
|
|
648
|
+
// 允许,但要留痕:这条提示是给排障与插件作者看的
|
|
649
|
+
report({
|
|
650
|
+
pluginId: pid, slot: entry && entry.name, code: 'title-collision',
|
|
651
|
+
reason: `新建分组「${groupId}」的标题「${title}」与内置组同名(允许;请确认你不是想往内置组加控件)`,
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
const group = {
|
|
655
|
+
id: groupId,
|
|
656
|
+
title,
|
|
657
|
+
pluginId: pid,
|
|
658
|
+
// 缺省收起:插件分组的内容用户未必关心,别一进来就把面板撑长
|
|
659
|
+
defaultOpen: !!spec.groupDefaultOpen,
|
|
660
|
+
controls: [],
|
|
661
|
+
};
|
|
662
|
+
byId.set(groupId, group);
|
|
663
|
+
acc.newGroups.push({
|
|
664
|
+
group,
|
|
665
|
+
order: Number.isFinite(Number(spec.groupOrder)) ? Number(spec.groupOrder) : 1000,
|
|
666
|
+
seq: acc.seq++,
|
|
667
|
+
});
|
|
668
|
+
return group;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* 插槽声明的 `render(props)` 结果 → 面板控件描述(view.js 已有的 kind)。
|
|
673
|
+
*
|
|
674
|
+
* 为什么在这里翻译:render() 是**插件侧**的形状({kind:'segmented', options}),
|
|
675
|
+
* 而面板控件需要宿主口径(segmented 要 values、开关要 defaultOn)。
|
|
676
|
+
* 翻译集中在装配期一处,渲染层就不用认识"插件控件"这个概念。
|
|
677
|
+
* @param {{id:string,label:string,spec:object}} entry
|
|
678
|
+
* @returns {object|null}
|
|
679
|
+
*/
|
|
680
|
+
function slotSpecToControl(entry, props) {
|
|
681
|
+
const spec = (entry && entry.spec) || {};
|
|
682
|
+
if (typeof spec.render !== 'function') return null;
|
|
683
|
+
let out = null;
|
|
684
|
+
try {
|
|
685
|
+
// props 由装配层构造:settings 是**真实值**(当前值 ?? 声明默认值),S4 再扩 geometry/activeSurface/document
|
|
686
|
+
out = spec.render(props || { settings: {} });
|
|
687
|
+
} catch {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
if (!out || typeof out !== 'object') return null;
|
|
691
|
+
const kind = String(out.kind || '');
|
|
692
|
+
// kind 必须落在**插件契约**的三种之内(segmented / toggle / button)——
|
|
693
|
+
// 超出就返回 null 由装配期拒绝并给原因,而不是放一个渲染层不认的 kind 出去
|
|
694
|
+
// (那种情况会落到 view 的 default 分支,渲染成空 <p class="ctl-note">,用户看到的是"控件消失")。
|
|
695
|
+
const viewKind = PLUGIN_KIND_TO_VIEW[kind];
|
|
696
|
+
if (!viewKind) return null;
|
|
697
|
+
if (kind === 'segmented') {
|
|
698
|
+
const options = Array.isArray(out.options) ? out.options.map(String) : [];
|
|
699
|
+
if (!options.length) return null;
|
|
700
|
+
return { kind: viewKind, options, values: options.slice(), defaultValue: options[0] };
|
|
701
|
+
}
|
|
702
|
+
if (kind === 'toggle') return { kind: viewKind, defaultOn: !!out.value };
|
|
703
|
+
// button → actions:形状按渲染层口径给 buttons[](renderActions 遍历的是它)
|
|
704
|
+
return { kind: viewKind, buttons: [String(out.label || entry.label || '执行')] };
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* 由控件 id 反查它需要的能力键与"该能力对应哪个设置键"。
|
|
709
|
+
* 面板只按 capabilityKey 判定可用性,按 settingKey 下发值给渲染器(二者一般同名,
|
|
710
|
+
* 例外是 debug 类:能力键是 debugDraw 的一项,没有 settingKey)。
|
|
711
|
+
* @param {string} id 控件 id(如 ctl-fps / ctl-debug-bones)
|
|
712
|
+
* @returns {{kind: string, capabilityKey: string, settingKey: string|null}|null}
|
|
713
|
+
*/
|
|
714
|
+
export function settingKeyOf(id) {
|
|
715
|
+
const ctl = CONTROL_INDEX.get(id);
|
|
716
|
+
if (!ctl) return null;
|
|
717
|
+
const resolved = resolveCapabilityKey(ctl, null);
|
|
718
|
+
if (!resolved || resolved.kind === 'unsupported') return null;
|
|
719
|
+
if (resolved.kind === 'debug') return { kind: 'debug', capabilityKey: resolved.key, settingKey: null };
|
|
720
|
+
return { kind: 'setting', capabilityKey: resolved.key, settingKey: resolved.key };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/** 控件索引(id → 控件定义),供反查能力键与默认值 */
|
|
724
|
+
const CONTROL_INDEX = (() => {
|
|
725
|
+
const map = new Map();
|
|
726
|
+
for (const group of PANEL_GROUPS) {
|
|
727
|
+
for (const ctl of group.controls || []) {
|
|
728
|
+
if (ctl && ctl.id) map.set(ctl.id, ctl);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return map;
|
|
732
|
+
})();
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* 能力自检(面板能力契约 §四):声明了就必须能落地。
|
|
736
|
+
* 对 capabilities 的每一项调 renderer.setSetting / renderer.getSetting:
|
|
737
|
+
* · setSetting 返回非 true 或抛异常 ⇒ 违规「声明支持但未实现」
|
|
738
|
+
* · getSetting 返回 null ⇒ 违规「无法回显生效值」
|
|
739
|
+
* debugDraw 的项走 setSetting('debug:<key>'),同样要求返回 true。
|
|
740
|
+
* @param {{capabilities?: {settings?: string[], debugDraw?: string[]},
|
|
741
|
+
* setSetting?: Function, getSetting?: Function}} renderer
|
|
742
|
+
* @returns {{passed: boolean, checked: number, violations: Array<{key: string, expect: string, actual: string}>}}
|
|
743
|
+
*/
|
|
744
|
+
export function selfCheck(renderer) {
|
|
745
|
+
const violations = [];
|
|
746
|
+
const caps = (renderer && renderer.capabilities) || {};
|
|
747
|
+
const settings = Array.isArray(caps.settings) ? caps.settings : [];
|
|
748
|
+
const debugDraw = Array.isArray(caps.debugDraw) ? caps.debugDraw : [];
|
|
749
|
+
const canSet = renderer && typeof renderer.setSetting === 'function';
|
|
750
|
+
const canGet = renderer && typeof renderer.getSetting === 'function';
|
|
751
|
+
/** 取一项的当前生效值(拿不到则退回控件声明的默认值) */
|
|
752
|
+
const probeValue = (key) => {
|
|
753
|
+
let cur = null;
|
|
754
|
+
try { cur = canGet ? renderer.getSetting(key) : null; } catch { cur = null; }
|
|
755
|
+
if (cur !== null && cur !== undefined) return cur;
|
|
756
|
+
const ctl = CONTROL_INDEX.get(`ctl-${key}`) || findControlByCapability(key);
|
|
757
|
+
if (!ctl) return null;
|
|
758
|
+
return ctl.defaultValue !== undefined ? ctl.defaultValue : (ctl.defaultOn !== undefined ? ctl.defaultOn : null);
|
|
759
|
+
};
|
|
760
|
+
const applyAndCheck = (key, label) => {
|
|
761
|
+
if (!canSet) { violations.push({ key: label, expect: '渲染器实现 setSetting', actual: '缺失' }); return; }
|
|
762
|
+
const value = probeValue(key);
|
|
763
|
+
let ok = false;
|
|
764
|
+
try { ok = renderer.setSetting(key, value) === true; }
|
|
765
|
+
catch (e) { violations.push({ key: label, expect: 'setSetting 不抛异常', actual: `抛异常:${String((e && e.message) || e)}` }); return; }
|
|
766
|
+
if (!ok) { violations.push({ key: label, expect: 'setSetting 返回 true', actual: '返回 false(声明支持但未实现)' }); return; }
|
|
767
|
+
let got = null;
|
|
768
|
+
try { got = canGet ? renderer.getSetting(key) : null; } catch { got = null; }
|
|
769
|
+
if (got === null || got === undefined) violations.push({ key: label, expect: 'getSetting 回显生效值', actual: String(got) });
|
|
770
|
+
};
|
|
771
|
+
// 用"同值回灌"而不是写入新值:自检不能改变用户现场(fit 会重置取景、msaa 会重建上下文)
|
|
772
|
+
for (const key of settings) applyAndCheck(key, key);
|
|
773
|
+
for (const key of debugDraw) applyAndCheck(`debug:${key}`, `debug:${key}`);
|
|
774
|
+
return { passed: violations.length === 0, checked: settings.length + debugDraw.length, violations };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** 按能力键找控件(用于取默认值;同一能力可能有多个控件,取第一个带默认值的) */
|
|
778
|
+
function findControlByCapability(key) {
|
|
779
|
+
for (const ctl of CONTROL_INDEX.values()) {
|
|
780
|
+
const resolved = resolveCapabilityKey(ctl, null);
|
|
781
|
+
if (resolved && resolved.kind === 'setting' && resolved.key === key) return ctl;
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|