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,171 @@
|
|
|
1
|
+
// app/url-params.js —— 用 URL 参数一次性指定「装载哪个模型 / 用哪个运行时」
|
|
2
|
+
//
|
|
3
|
+
// 为什么做这个(而不是面板按钮):排查场景是"同一个模型换运行时看现象变不变",
|
|
4
|
+
// URL 天然可复制、可脚本化、可进回归用例;面板按钮给人点,测试与复现都用不上。
|
|
5
|
+
//
|
|
6
|
+
// 硬边界(三条,改这里时不许破):
|
|
7
|
+
// ① **只影响这一次装载**:覆盖值不落 uiState / localStorage,被消费后立刻清空。
|
|
8
|
+
// 默认行为永远是"按骨架版本自动挑运行时"—— 那是正确性保证(骨架 4.1.20 只能用 4.1 运行时)。
|
|
9
|
+
// ② 解析到模型后走 `selectModel`(与点名册点选**同一条装载路径**),不新开装载实现。
|
|
10
|
+
// ③ 指定了非法值要**明确报错**,绝不静默忽略 —— 静默会让排查得出"换了没区别"的错误结论。
|
|
11
|
+
//
|
|
12
|
+
// 形如:/?model=<rootId>/<rel>&runtime=spine-4.1
|
|
13
|
+
// model 先按条目 id 全等匹配,未命中再按 `rootId/rel` 全等匹配(两种写法都认)
|
|
14
|
+
// runtime 可选;不传 = 自动(现状行为)
|
|
15
|
+
|
|
16
|
+
import { getJson } from '../infra/api.js';
|
|
17
|
+
import { selectModel } from './model-actions.js';
|
|
18
|
+
import { setForcedRuntime } from '../spine/canvas-runtime.js';
|
|
19
|
+
// 取清单走 roster 的**唯一入口**(失败记 diag warn + 回传 reason),与名册那条路同一份实现
|
|
20
|
+
import { fetchRootEntries } from '../roster/list.js';
|
|
21
|
+
// 深链准入与运行时类型都查面元数据表(不再写死 spine/live2d 字面量)
|
|
22
|
+
import { byKind, knownKinds } from '../render/surfaces.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 取全量 catalog 条目(跨所有资源根)。
|
|
26
|
+
* 为什么不用名册里的已加载数据:名册按游戏/角色分组且可能未展开,
|
|
27
|
+
* 而这里要"精确匹配任意一条",直接问服务端最可靠。
|
|
28
|
+
* @returns {Promise<{entries: object[], failures: string[], emptyRoots: string[]}>}
|
|
29
|
+
* failures = 清单**取不到**的根(带原因)—— 查询失败,可能要重试
|
|
30
|
+
* emptyRoots = 清单拿到了但**一条都没有**、且服务端给了 **rootWarning**(明确说这一根怎么了)
|
|
31
|
+
* ⚠️ 不要用 `warnings` 拼这一档:那是通用说明("有 N 个插件能解包"),
|
|
32
|
+
* 拿它拼因果会得出"因为有两个插件所以没模型"这种错误结论。
|
|
33
|
+
*/
|
|
34
|
+
async function fetchAllEntries() {
|
|
35
|
+
const roots = await getJson('/api/roots').catch(() => null);
|
|
36
|
+
const list = (roots && (roots.roots || roots.items)) || [];
|
|
37
|
+
const out = [];
|
|
38
|
+
const failures = [];
|
|
39
|
+
const emptyRoots = [];
|
|
40
|
+
for (const r of list) {
|
|
41
|
+
const id = r && (r.id || r.name);
|
|
42
|
+
if (!id) continue;
|
|
43
|
+
const { entries, reason, rootWarning } = await fetchRootEntries(id);
|
|
44
|
+
if (reason) failures.push(`${id}(${reason})`);
|
|
45
|
+
// 只在"一条都没有"且**服务端明确说这一根有问题**(rootWarning 非空)时才收进来。
|
|
46
|
+
// 不拿 warnings 兜底:那是通用说明(如"有 2 个插件能解包"),
|
|
47
|
+
// 拿它拼因果会得出"因为有两个插件所以没模型"这种错误结论。
|
|
48
|
+
else if (!entries.length && rootWarning) emptyRoots.push(`${id}(${rootWarning.slice(0, 80)})`);
|
|
49
|
+
for (const e of entries) out.push({ ...e, rootId: e.rootId || id });
|
|
50
|
+
}
|
|
51
|
+
return { entries: out, failures, emptyRoots };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** 两种写法都认:先 id 全等,再 `rootId/rel` 全等 */
|
|
55
|
+
function matchEntry(entries, arg) {
|
|
56
|
+
const want = String(arg || '').trim();
|
|
57
|
+
if (!want) return { hit: null, by: '' };
|
|
58
|
+
const byId = entries.filter((e) => String(e.id || '') === want);
|
|
59
|
+
if (byId.length === 1) return { hit: byId[0], by: 'id' };
|
|
60
|
+
if (byId.length > 1) return { hit: null, by: 'id', ambiguous: byId };
|
|
61
|
+
const byRel = entries.filter((e) => `${e.rootId || ''}/${e.rel || ''}` === want);
|
|
62
|
+
if (byRel.length === 1) return { hit: byRel[0], by: 'rootId/rel' };
|
|
63
|
+
if (byRel.length > 1) return { hit: null, by: 'rootId/rel', ambiguous: byRel };
|
|
64
|
+
return { hit: null, by: '' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 解析 runtime 参数:`spine-4.1` 或裸 `4.1`;`live2d-*` 明确不支持 */
|
|
68
|
+
function parseRuntime(arg) {
|
|
69
|
+
const raw = String(arg || '').trim();
|
|
70
|
+
if (!raw) return { kind: null, version: null };
|
|
71
|
+
if (/^live2d[-:]/i.test(raw)) return { kind: 'live2d', version: raw.replace(/^live2d[-:]/i, '') };
|
|
72
|
+
if (/^spine[-:]/i.test(raw)) return { kind: 'spine', version: raw.replace(/^spine[-:]/i, '') };
|
|
73
|
+
return { kind: 'spine', version: raw }; // 裸版本号默认按 spine 解释
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 报告一条 URL 参数相关的失败:**状态行(用户可见)+ 日志(面板/上传)** 两条都写。
|
|
78
|
+
*
|
|
79
|
+
* 这里不再自己找 DOM 写文本:`#stage-msg` / `[data-role="stage-msg"]` 在全仓都不存在
|
|
80
|
+
* (status.js 的状态行由 renderStatusLine 统一渲染),那段是死代码,已删。
|
|
81
|
+
* 统一走 stageStatus ⇒ 同一份文案、同一个渲染入口,不会出现"两处各写一份、样式不一致"。
|
|
82
|
+
*/
|
|
83
|
+
function report(message, level = 'error') {
|
|
84
|
+
import('./status.js').then((m) => {
|
|
85
|
+
if (level === 'error') m.stageLog('error', message, { source: 'url-params', reason: 'url-params' });
|
|
86
|
+
m.stageStatus(message, level === 'error' ? 'error' : 'warn', { show: true });
|
|
87
|
+
}).catch(() => { /* 状态模块不可用:静默放弃(URL 参数只是增强入口,不该让页面挂掉) */ });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 应用 URL 参数(在资源根加载完成后调用一次)。
|
|
92
|
+
* 无参数时**完全不动**任何状态(返回 null),保证默认行为零变化。
|
|
93
|
+
* @returns {Promise<{model?:string, runtime?:string}|null>}
|
|
94
|
+
*/
|
|
95
|
+
export async function applyUrlParams() {
|
|
96
|
+
const q = new URLSearchParams(window.location.search);
|
|
97
|
+
const modelArg = q.get('model');
|
|
98
|
+
const runtimeArg = q.get('runtime');
|
|
99
|
+
if (!modelArg && !runtimeArg) return null;
|
|
100
|
+
|
|
101
|
+
if (!modelArg) {
|
|
102
|
+
report('URL 参数 runtime= 需要同时提供 model= 才知道要装载哪个模型(形如 /?model=<rootId>/<rel>&runtime=spine-4.1)');
|
|
103
|
+
return { runtime: runtimeArg };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { entries, failures, emptyRoots } = await fetchAllEntries();
|
|
107
|
+
const { hit, by, ambiguous } = matchEntry(entries, modelArg);
|
|
108
|
+
|
|
109
|
+
if (!hit) {
|
|
110
|
+
const sample = entries.slice(0, 5)
|
|
111
|
+
.map((e) => ` · id=${e.id || '(无)'} 或 ${e.rootId || ''}/${e.rel || ''}`)
|
|
112
|
+
.join('\n');
|
|
113
|
+
// 把"为什么匹配不到"分两档带进提示(否则深链失效看起来就像"模型不存在"):
|
|
114
|
+
// ① 某个根的清单**根本没取到**(路径配错/服务端拒绝)—— 该去查配置或服务
|
|
115
|
+
// ② 某个根**取到了但一条都没有**、且服务端有话说(典型:path 指到空目录/不存在)
|
|
116
|
+
// 两档措辞刻意不同:把 ② 说成"取不到"会让"要不要重试"这类判断失准。
|
|
117
|
+
const failed = failures.length
|
|
118
|
+
? `。另外有 ${failures.length} 个根的清单取不到:${failures.slice(0, 3).join(';')}${failures.length > 3 ? ' …' : ''}`
|
|
119
|
+
: '';
|
|
120
|
+
const empty = emptyRoots.length
|
|
121
|
+
? `。另外有 ${emptyRoots.length} 个根存在但没扫出任何模型:${emptyRoots.slice(0, 2).join(';')}${emptyRoots.length > 2 ? ' …' : ''}`
|
|
122
|
+
: '';
|
|
123
|
+
report(ambiguous && ambiguous.length
|
|
124
|
+
? `URL 参数 model=${modelArg} 命中 ${ambiguous.length} 条(按 ${by} 匹配),无法确定用哪一条;请改用更精确的写法`
|
|
125
|
+
: `URL 参数 model=${modelArg} 没有匹配到任何条目(两种写法都支持:条目 id,或 rootId/rel)${failed}${empty}。前 5 个候选:\n${sample}`);
|
|
126
|
+
return { model: modelArg };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const rt = parseRuntime(runtimeArg);
|
|
130
|
+
// 深链准入:只有声明 deepLink 的面才允许通过 ?model= 直接指定。
|
|
131
|
+
// hit.kind 为空是**正常情况**(容器条目解包前不知道内容类型),交给后面判,不在这里报错。
|
|
132
|
+
const hitMeta = byKind(hit.kind);
|
|
133
|
+
if (hitMeta && hitMeta.deepLink === false) {
|
|
134
|
+
report(`URL 参数 model= 指向的条目类型(${hitMeta.label})不支持深链直接装载;请在页面里手动选择`);
|
|
135
|
+
return { model: modelArg };
|
|
136
|
+
}
|
|
137
|
+
if (hit.kind && !hitMeta) {
|
|
138
|
+
report(`URL 参数 model= 指向的条目类型「${hit.kind}」不是受支持的渲染面`
|
|
139
|
+
+ `(已知:${knownKinds().join(' / ')})`);
|
|
140
|
+
return { model: modelArg };
|
|
141
|
+
}
|
|
142
|
+
// 运行时类型对应的面查元数据表(不再写死 'live2d' / 'spine' 两个字面量)。
|
|
143
|
+
// 未知 kind 显式拒绝:以前会一路静默什么也不做,排查时只看到"参数没效果"。
|
|
144
|
+
const rtMeta = byKind(rt.kind);
|
|
145
|
+
if (rt.kind && !rtMeta) {
|
|
146
|
+
report(`URL 参数 runtime= 的类型「${rt.kind}」不是受支持的渲染面(已知:${knownKinds().join(' / ')})`);
|
|
147
|
+
return { model: modelArg, runtime: runtimeArg };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Live2D 的运行时由服务端探测决定(/live2d-lib/* 的映射是进程级),按请求切换会变成有状态
|
|
151
|
+
// ⇒ 明确拒绝而不是静默忽略(忽略会让排查得出"换了没区别"的错误结论)
|
|
152
|
+
if (rtMeta && rtMeta.id === 'live2d') {
|
|
153
|
+
let actual = '未知';
|
|
154
|
+
try {
|
|
155
|
+
const ad = await import('../live2d/runtime-adapter.js');
|
|
156
|
+
const info = ad.runtimeInfo();
|
|
157
|
+
actual = `${info.pixi ? `pixi ${info.pixi}` : ''}${info.settingsCtorName ? ` / ${info.settingsCtorName}` : ''}` || '未知';
|
|
158
|
+
} catch { /* 拿不到就写未知 */ }
|
|
159
|
+
report(`Live2D 运行时由服务端探测决定,暂不支持按请求指定(当前生效:${actual});`
|
|
160
|
+
+ `去掉 runtime= 让服务端自动选择`);
|
|
161
|
+
return { model: modelArg, runtime: runtimeArg };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Spine:把版本作为**本次装载**的覆盖值(canvas-engine 消费一次后立即清空)
|
|
165
|
+
if (rtMeta && rtMeta.id === 'spine' && rt.version) {
|
|
166
|
+
setForcedRuntime(rt.version);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await selectModel(hit, { syncTree: true });
|
|
170
|
+
return { model: modelArg, runtime: runtimeArg, matchedBy: by };
|
|
171
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// app/view-settings.js —— 展示类设置的持久化与落地(主题 / 背景)
|
|
2
|
+
//
|
|
3
|
+
// 这些设置与渲染器无关:主题只改 CSS 变量所在属性,背景由 Surface 写 data-bg 后交给 CSS。
|
|
4
|
+
// 存储结构:{ uiTheme, background:{mode,color,imageUrl}, panel, layout, debug }。
|
|
5
|
+
// 全站只有这一个偏好键 —— 面板值、布局态、调试浮层都在里面,不另开键。
|
|
6
|
+
|
|
7
|
+
import { state } from './context.js';
|
|
8
|
+
import { defaultSolidColor } from './background-solids.js';
|
|
9
|
+
|
|
10
|
+
/** 视图设置存储键:主题、背景等展示类设置(全站唯一的偏好键,面板/布局/调试段都在里面) */
|
|
11
|
+
export const VIEW_SETTINGS_KEY = 'modelstage-settings-v2';
|
|
12
|
+
|
|
13
|
+
/** 已下线的视图设置字段:读到就地清掉,避免"看不见的设置还在生效/还在占存储" */
|
|
14
|
+
const DEPRECATED_VIEW_KEYS = ['partFilterEnabled', 'partFilterKeywords'];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 读视图设置(结构:{uiTheme, background:{mode,color,imageUrl}, panel, layout, debug})。
|
|
18
|
+
* 副作用:**清理已下线字段**(一次写回),这样老 localStorage 里的残留会被自然淘汰,
|
|
19
|
+
* 不用用户手动清缓存。
|
|
20
|
+
*/
|
|
21
|
+
export function readViewSettings() {
|
|
22
|
+
let s;
|
|
23
|
+
try {
|
|
24
|
+
s = JSON.parse(localStorage.getItem(VIEW_SETTINGS_KEY) || '{}');
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
if (!s || typeof s !== 'object') return {};
|
|
29
|
+
const stale = DEPRECATED_VIEW_KEYS.filter((k) => k in s);
|
|
30
|
+
if (stale.length) {
|
|
31
|
+
for (const k of stale) delete s[k];
|
|
32
|
+
try { localStorage.setItem(VIEW_SETTINGS_KEY, JSON.stringify(s)); } catch { /* 写不了就只保留内存态 */ }
|
|
33
|
+
}
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 写视图设置:只覆盖自己管的键,其余字段原样保留 */
|
|
38
|
+
export function saveViewSettings(patch) {
|
|
39
|
+
try {
|
|
40
|
+
const cur = readViewSettings();
|
|
41
|
+
const next = { ...cur, ...patch };
|
|
42
|
+
if (patch.background) next.background = { ...(cur.background || {}), ...patch.background };
|
|
43
|
+
localStorage.setItem(VIEW_SETTINGS_KEY, JSON.stringify(next));
|
|
44
|
+
} catch { /* 隐私模式等写不了就忽略:持久化是增强 */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 主题:只改 CSS 变量所在的选择器属性,颜色全部集中在 panel.css 的主题色板 */
|
|
48
|
+
export function applyTheme(id) {
|
|
49
|
+
const theme = id || 'dark-cyan';
|
|
50
|
+
document.documentElement.dataset.uiTheme = theme;
|
|
51
|
+
state.viewTheme = theme;
|
|
52
|
+
saveViewSettings({ uiTheme: theme });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** 自定义背景图片上限(dataURL 存 localStorage,太大不安全) */
|
|
56
|
+
const MAX_BG_IMAGE_BYTES = 4 * 1024 * 1024;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 自定义背景:取一张本地图片作为舞台背景。
|
|
60
|
+
* 为什么用 dataURL 而不是 objectURL:要跟着视图设置一起持久化(localStorage),
|
|
61
|
+
* objectURL 只活在当前页面生命周期里,刷新即失效;文件过大时拒绝并提示,避免把存储写爆。
|
|
62
|
+
* @returns {Promise<boolean>} 是否落地成功
|
|
63
|
+
*/
|
|
64
|
+
export function pickBackgroundImage() {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
// 标准做法:<label> 包 <input type="file">,点 label 即触发选择框(不需要 JS 绑定 click)
|
|
67
|
+
const label = document.createElement('label');
|
|
68
|
+
label.classList.add('file-picker-hidden');
|
|
69
|
+
const input = document.createElement('input');
|
|
70
|
+
input.type = 'file';
|
|
71
|
+
input.accept = 'image/*';
|
|
72
|
+
label.append(input);
|
|
73
|
+
document.body.append(label);
|
|
74
|
+
// 仍需要 change 才能拿到文件内容(这是数据回调,不是"点击代理")
|
|
75
|
+
input.addEventListener('change', () => {
|
|
76
|
+
const file = input.files && input.files[0];
|
|
77
|
+
label.remove();
|
|
78
|
+
if (!file) { resolve(false); return; }
|
|
79
|
+
if (file.size > MAX_BG_IMAGE_BYTES) {
|
|
80
|
+
document.documentElement.dataset.bgError = `图片过大(${Math.round(file.size / 1024)}KB > ${MAX_BG_IMAGE_BYTES / 1024 / 1024}MB)`;
|
|
81
|
+
resolve(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const reader = new FileReader();
|
|
85
|
+
reader.addEventListener('load', () => {
|
|
86
|
+
const url = String(reader.result || '');
|
|
87
|
+
setCustomBackground(url);
|
|
88
|
+
resolve(!!url);
|
|
89
|
+
}, { once: true });
|
|
90
|
+
reader.addEventListener('error', () => { resolve(false); }, { once: true });
|
|
91
|
+
reader.readAsDataURL(file);
|
|
92
|
+
}, { once: true });
|
|
93
|
+
label.click(); // 点 label 等价于点 input(原生行为)
|
|
94
|
+
document.documentElement.dataset.bgPicker = 'open';
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 把图片落到舞台背景并持久化(data-bg=custom 时 CSS 不再画底色,由内联 backgroundImage 负责) */
|
|
99
|
+
export function setCustomBackground(url) {
|
|
100
|
+
const stageEl = document.getElementById('stage');
|
|
101
|
+
if (!stageEl) return false;
|
|
102
|
+
stageEl.dataset.bg = url ? 'custom' : 'checkerboard';
|
|
103
|
+
// 内联例外(契约《禁止内联CSS与JS》§3.1 动态计算值):
|
|
104
|
+
// 背景图 URL 属于运行时数据(从界面导入或粘贴产生),不可能预先写进 CSS;
|
|
105
|
+
// size/position 是跟随该 URL 的联动值。颜色/边框/字体等设计令牌一律走 CSS 变量。
|
|
106
|
+
stageEl.style.backgroundImage = url ? `url("${url}")` : '';
|
|
107
|
+
// 没有图就不要留 cover(否则空 url 也会把棋盘图案拉伸)
|
|
108
|
+
stageEl.style.backgroundSize = url ? 'cover' : '';
|
|
109
|
+
stageEl.style.backgroundPosition = url ? 'center' : '';
|
|
110
|
+
state.viewBg = 'custom';
|
|
111
|
+
saveViewSettings({ background: { mode: 'custom', color: defaultSolidColor(), imageUrl: url || '' } });
|
|
112
|
+
document.documentElement.dataset.bgImage = url ? 'set' : 'none';
|
|
113
|
+
return !!url;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 背景模式:写舞台容器的 data-bg,具体底色由 CSS 决定;custom 走文件选择器 */
|
|
117
|
+
export function applyBackground(mode) {
|
|
118
|
+
const stageEl = document.getElementById('stage');
|
|
119
|
+
if (!stageEl) return;
|
|
120
|
+
const m = mode || 'checkerboard';
|
|
121
|
+
if (m === 'custom') {
|
|
122
|
+
// 已经选过图就直接用回来,没有才让用户选(避免每次切档都弹框)
|
|
123
|
+
const saved = (readViewSettings().background || {}).imageUrl || '';
|
|
124
|
+
if (saved) { setCustomBackground(saved); return; }
|
|
125
|
+
pickBackgroundImage();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// 三个内联样式都要清:只清 backgroundImage 不够——自定义图设过 background-size:cover,
|
|
129
|
+
// 留着会把棋盘格/纯色依赖的 CSS 图案尺寸覆盖成 cover(整块拉伸成一个巨大格子,看着"背景不对")。
|
|
130
|
+
stageEl.style.backgroundImage = '';
|
|
131
|
+
stageEl.style.backgroundSize = '';
|
|
132
|
+
stageEl.style.backgroundPosition = '';
|
|
133
|
+
// 纯色模式:颜色值由 CSS 变量 --stage-solid 驱动(panel.css 里 #stage[data-bg="solid"] 读它)
|
|
134
|
+
// 颜色属于运行时数据 ⇒ 以变量下发,不写 style.background 字面量(契约 §3.1 动态值例外)
|
|
135
|
+
const savedColor = (readViewSettings().background || {}).color || defaultSolidColor();
|
|
136
|
+
if (m === 'solid') { try { stageEl.style.setProperty('--stage-solid', savedColor); } catch { /* 忽略 */ } }
|
|
137
|
+
// 背景唯一入口:交给 Surface(它会写 data-bg 并通知所有渲染器)
|
|
138
|
+
try {
|
|
139
|
+
if (state.surface && typeof state.surface.setBackground === 'function') state.surface.setBackground(m);
|
|
140
|
+
else stageEl.dataset.bg = m;
|
|
141
|
+
} catch { stageEl.dataset.bg = m; }
|
|
142
|
+
state.viewBg = m;
|
|
143
|
+
// 广播一次:右栏档位与底栏下拉都靠它同步(谁改的都能收敛到同一状态)
|
|
144
|
+
try { document.dispatchEvent(new CustomEvent('panel:bg-applied', { detail: { mode: m } })); } catch { /* 忽略 */ }
|
|
145
|
+
const cur = readViewSettings();
|
|
146
|
+
saveViewSettings({
|
|
147
|
+
background: {
|
|
148
|
+
mode: m,
|
|
149
|
+
color: (cur.background && cur.background.color) || defaultSolidColor(),
|
|
150
|
+
imageUrl: (cur.background && cur.background.imageUrl) || '',
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 应用一个纯色(并切到纯色模式)。
|
|
158
|
+
*
|
|
159
|
+
* 语义:**存的是颜色值本身,不是预设 id** —— 预设只是快捷方式,
|
|
160
|
+
* 因此预设被删/改名都不会影响已经选中的颜色(这也是它敢放在界面上随便改的前提)。
|
|
161
|
+
* @param {string} color '#rrggbb'(非法时回落到默认预设色)
|
|
162
|
+
* @returns {boolean} 是否成功应用
|
|
163
|
+
*/
|
|
164
|
+
export function setBackgroundColor(color) {
|
|
165
|
+
const stageEl = document.getElementById('stage');
|
|
166
|
+
if (!stageEl) return false;
|
|
167
|
+
const ok = /^#[0-9a-f]{6}$/i.test(String(color || ''));
|
|
168
|
+
const c = ok ? String(color).toLowerCase() : defaultSolidColor();
|
|
169
|
+
try { stageEl.style.setProperty('--stage-solid', c); } catch { return false; }
|
|
170
|
+
state.viewBg = 'solid';
|
|
171
|
+
try {
|
|
172
|
+
if (state.surface && typeof state.surface.setBackground === 'function') state.surface.setBackground('solid');
|
|
173
|
+
else stageEl.dataset.bg = 'solid';
|
|
174
|
+
} catch { stageEl.dataset.bg = 'solid'; }
|
|
175
|
+
// 与切模式同一条广播:右栏档位/底栏下拉都靠它收敛
|
|
176
|
+
try { document.dispatchEvent(new CustomEvent('panel:bg-applied', { detail: { mode: 'solid' } })); } catch { /* 忽略 */ }
|
|
177
|
+
const cur = readViewSettings();
|
|
178
|
+
saveViewSettings({
|
|
179
|
+
background: {
|
|
180
|
+
mode: 'solid',
|
|
181
|
+
color: c,
|
|
182
|
+
imageUrl: (cur.background && cur.background.imageUrl) || '',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return true;
|
|
186
|
+
}
|