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,83 @@
|
|
|
1
|
+
// shared/bundle-shape.mjs —— 资源包"形态"的**唯一判据**(前端 plugin-host 与 node 侧 probe 共用这一份)
|
|
2
|
+
//
|
|
3
|
+
// 回答一个问题:这个 UnityFS 包里到底有没有"模型定义",还是只有美术资源?
|
|
4
|
+
// · 有模型定义类(TextAsset / SkeletonDataAsset / AtlasAsset / CubismModel / CubismMoc)
|
|
5
|
+
// ⇒ 是模型包,交给对应插件去解
|
|
6
|
+
// · 只有纹理/材质/网格/动画片段、一个模型定义类都没有
|
|
7
|
+
// ⇒ 是**美术依赖包**:它给同批的模型包提供共享资源,本身不可单独装载
|
|
8
|
+
//
|
|
9
|
+
// 为什么必须共用一份:同一类包在"页面点开"与"直接调 /api/unpack/resolve"两条路径上
|
|
10
|
+
// 必须给同一个答案;判据或文案复制两份,早晚会漂。
|
|
11
|
+
//
|
|
12
|
+
// 铁律:**只看包内 className 构成**,不读文件名、不读目录名、不读角色名 ——
|
|
13
|
+
// 按名字判断等于给某几个具体资源开后门,换个游戏就失效。
|
|
14
|
+
|
|
15
|
+
/** 能"定义模型"的类:出现任一即视为模型包 */
|
|
16
|
+
export const MODEL_DEFINING_CLASSES = [
|
|
17
|
+
'TextAsset', // spine 的 .skel.txt / .atlas.txt;Cubism 的 model3.json / physics3.json
|
|
18
|
+
'SkeletonDataAsset', // spine-unity 的资产类
|
|
19
|
+
'AtlasAsset', // spine-unity 的资产类
|
|
20
|
+
'CubismModel', // Live2D 的资产类
|
|
21
|
+
'CubismMoc', // Live2D 的资产类
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/** 纯美术资源类:这些有、上面一个都没有 ⇒ 依赖包 */
|
|
25
|
+
export const PURE_ART_CLASSES = [
|
|
26
|
+
'Texture2D', 'Sprite', 'Material', 'Mesh', 'AnimationClip', 'MeshFilter', 'MeshRenderer', 'Shader',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 从(serialized.parse 的)对象表算 className 直方图。
|
|
31
|
+
* 两侧注入各自"怎么拿到对象表"的方式,但**统计规则就是这一份**。
|
|
32
|
+
* @param {{objects?: object[], types?: object[]}} sf serialized 解析结果
|
|
33
|
+
* @returns {Record<string, number>} className → 数量
|
|
34
|
+
*/
|
|
35
|
+
export function histogramFromObjects(sf) {
|
|
36
|
+
const objects = (sf && sf.objects) || [];
|
|
37
|
+
const types = (sf && sf.types) || [];
|
|
38
|
+
const byId = new Map();
|
|
39
|
+
for (const t of types) byId.set(t.classID, t.className || t.name);
|
|
40
|
+
const hist = {};
|
|
41
|
+
for (const obj of objects) {
|
|
42
|
+
const name = byId.get(obj.classID) || `ClassID ${obj.classID}`;
|
|
43
|
+
hist[name] = (hist[name] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
return hist;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 这个直方图是否描述"美术依赖包"。
|
|
50
|
+
* @param {Record<string, number>} hist
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
export function isAssetDependencyHistogram(hist) {
|
|
54
|
+
if (!hist) return false;
|
|
55
|
+
if (MODEL_DEFINING_CLASSES.some((k) => (hist[k] || 0) > 0)) return false;
|
|
56
|
+
return PURE_ART_CLASSES.some((k) => (hist[k] || 0) > 0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 直方图压成一句可读的类型构成(只报数量最多的前几类,避免文案过长)。
|
|
61
|
+
* 过滤掉 `ClassID 199` 这类占位项:类名没解析出来时对用户没有解释力,
|
|
62
|
+
* 把它们列进文案只会让人以为"这东西有名字但没显示"。
|
|
63
|
+
*/
|
|
64
|
+
export function describeHistogram(hist) {
|
|
65
|
+
return Object.entries(hist || {})
|
|
66
|
+
.filter(([k]) => !/^ClassID \d+$/.test(k))
|
|
67
|
+
.sort((a, b) => b[1] - a[1])
|
|
68
|
+
.slice(0, 5)
|
|
69
|
+
.map(([k, v]) => `${k}×${v}`)
|
|
70
|
+
.join('、');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 依赖包的统一文案(前端 toast / 日志、服务端 JSON 都引用它,保证逐字一致)。
|
|
75
|
+
* 要求:说清"为什么打不开" + "下一步怎么做"。
|
|
76
|
+
* @param {Record<string, number>} hist
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function dependencyReason(hist) {
|
|
80
|
+
return `这是美术依赖包,不能单独装载:包内只有 ${describeHistogram(hist)},`
|
|
81
|
+
+ '没有任何骨架或模型定义(spine 的 .skel/.atlas、Live2D 的 model3.json 都不在里面)。'
|
|
82
|
+
+ '它提供给同批的模型包共用;请改选同一目录下带骨架的那个包。';
|
|
83
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// shared/entry-reasons.mjs —— 装载结果 reason 的**唯一码表**(服务端存储、接口、前端渲染都从这里取)
|
|
2
|
+
//
|
|
3
|
+
// 为什么必须收口:同一个失败在四条路上有四套叫法 ——
|
|
4
|
+
// 前端 plugin-host 叫 no-provider、服务端 plugin-runner 叫 not-handled、
|
|
5
|
+
// 插件 lib 抛 ULB_ERR_TEX_DECODE、解包层还可能冒出 insufficient-memory。
|
|
6
|
+
// 契约《条目状态与展示归属》§二 要求 reason 是**结构化码**("不稳定文案不许进这里"),
|
|
7
|
+
// 所以这里定一份枚举 + 一份边界映射,禁止各处再写字面量。
|
|
8
|
+
//
|
|
9
|
+
// 插件 lib 不改:ULB_ERR_* 是它的内部码,只在边界用 fromUlbError() 映射过来。
|
|
10
|
+
|
|
11
|
+
/** 契约码枚举(键名给代码用,值是与接口/存储里出现的字符串) */
|
|
12
|
+
export const REASONS = Object.freeze({
|
|
13
|
+
// ── 认领层:没人愿意/不能接这个文件
|
|
14
|
+
NO_PROVIDER: 'no-provider',
|
|
15
|
+
ASSET_DEPENDENCY: 'asset-dependency',
|
|
16
|
+
// ── 资产层:目录型散文件缺件
|
|
17
|
+
MISSING_TEXTURES: 'missing-textures',
|
|
18
|
+
ASSET_MISSING: 'asset-missing',
|
|
19
|
+
VERSION_UNREADABLE: 'version-unreadable',
|
|
20
|
+
// ── 容器层
|
|
21
|
+
CONTAINER_PARSE: 'container-parse',
|
|
22
|
+
NOT_UNITYFS: 'not-unityfs',
|
|
23
|
+
NO_CAB: 'no-cab',
|
|
24
|
+
// ── 纹理解码层
|
|
25
|
+
TEX_DECODE: 'tex-decode',
|
|
26
|
+
TEX_FORMAT_UNSUPPORTED: 'tex-format-unsupported',
|
|
27
|
+
TEX_DATA_SHORT: 'tex-data-short',
|
|
28
|
+
// ── 资源/环境层
|
|
29
|
+
INSUFFICIENT_MEMORY: 'insufficient-memory',
|
|
30
|
+
BAD_ARGS: 'bad-args',
|
|
31
|
+
NOT_FOUND: 'not-found',
|
|
32
|
+
UNPACK_FAILED: 'unpack-failed',
|
|
33
|
+
// ── 渲染层(前端上报用)
|
|
34
|
+
RENDER_FAILED: 'render-failed',
|
|
35
|
+
// ── 兜底:宁可说"不知道",也不要编一个像样的原因
|
|
36
|
+
UNKNOWN: 'unknown',
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const ALL = new Set(Object.values(REASONS));
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 契约码 → 中文兜底文案。
|
|
43
|
+
* 注意:这只是**兜底**(调用方没给 message 时用)。真正的可读原因应尽量由判定方给出
|
|
44
|
+
* (如依赖包用 shared/bundle-shape.mjs 的 dependencyReason()),否则会退化成"只知道码、不知道话"。
|
|
45
|
+
*/
|
|
46
|
+
export const REASON_TEXT = Object.freeze({
|
|
47
|
+
[REASONS.NO_PROVIDER]: '没有插件能处理这个资源包',
|
|
48
|
+
[REASONS.ASSET_DEPENDENCY]: '这是美术依赖包,不能单独装载',
|
|
49
|
+
[REASONS.MISSING_TEXTURES]: '缺少贴图文件,渲染必然失败',
|
|
50
|
+
[REASONS.ASSET_MISSING]: '资源文件缺失',
|
|
51
|
+
[REASONS.VERSION_UNREADABLE]: '读不出骨架/模型版本',
|
|
52
|
+
[REASONS.CONTAINER_PARSE]: '资源包结构解析失败',
|
|
53
|
+
[REASONS.NOT_UNITYFS]: '不是 UnityFS 资源包',
|
|
54
|
+
[REASONS.NO_CAB]: '资源包里没有资产节点',
|
|
55
|
+
[REASONS.TEX_DECODE]: '纹理解码失败',
|
|
56
|
+
[REASONS.TEX_FORMAT_UNSUPPORTED]: '不支持这种纹理格式',
|
|
57
|
+
[REASONS.TEX_DATA_SHORT]: '纹理数据不完整',
|
|
58
|
+
[REASONS.INSUFFICIENT_MEMORY]: '可用内存不足,已拒绝解包',
|
|
59
|
+
[REASONS.BAD_ARGS]: '请求参数不合法',
|
|
60
|
+
[REASONS.NOT_FOUND]: '路径不存在',
|
|
61
|
+
[REASONS.UNPACK_FAILED]: '解包失败',
|
|
62
|
+
[REASONS.RENDER_FAILED]: '渲染失败',
|
|
63
|
+
[REASONS.UNKNOWN]: '原因未知',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** 插件内部码 → 契约码。插件 lib 的 ULB_ERR_* 不改,只在这里翻译。 */
|
|
67
|
+
const ULB_MAP = Object.freeze({
|
|
68
|
+
ULB_ERR_TEX_DECODE: REASONS.TEX_DECODE,
|
|
69
|
+
ULB_ERR_TEX_FORMAT_UNSUPPORTED: REASONS.TEX_FORMAT_UNSUPPORTED,
|
|
70
|
+
ULB_ERR_TEX_DATA_SHORT: REASONS.TEX_DATA_SHORT,
|
|
71
|
+
ULB_ERR_NOT_UNITYFS: REASONS.NOT_UNITYFS,
|
|
72
|
+
ULB_ERR_NO_CAB: REASONS.NO_CAB,
|
|
73
|
+
ULB_ERR_CONTAINER_PARSE: REASONS.CONTAINER_PARSE,
|
|
74
|
+
ULB_ERR_NO_FILE: REASONS.ASSET_MISSING,
|
|
75
|
+
ULB_ERR_BAD_ARGS: REASONS.BAD_ARGS,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把任意来源的 reason 归一到契约码。
|
|
80
|
+
*
|
|
81
|
+
* 为什么需要:服务端 plugin-runner 用 `not-handled`,前端 plugin-host 用 `no-provider`,
|
|
82
|
+
* 两者是同一件事("所有提供者都说不处理");不归一会让同一条失败在存储里有两个键,
|
|
83
|
+
* 前端按码查文案时会漏。
|
|
84
|
+
* @param {unknown} raw
|
|
85
|
+
* @returns {string} 契约码(永远落在 REASONS 的值里)
|
|
86
|
+
*/
|
|
87
|
+
export function normalizeReason(raw) {
|
|
88
|
+
if (raw === null || raw === undefined || raw === '') return REASONS.UNKNOWN;
|
|
89
|
+
const s = String(raw).trim();
|
|
90
|
+
if (ALL.has(s)) return s;
|
|
91
|
+
// 同义合并:服务端 runner 的 not-handled ≡ 前端 host 的 no-provider
|
|
92
|
+
if (s === 'not-handled') return REASONS.NO_PROVIDER;
|
|
93
|
+
// 已经带着 ULB_ 前缀的也认(有些路径会把插件内部码直接透上来)
|
|
94
|
+
if (s.startsWith('ULB_ERR_')) return fromUlbError(s);
|
|
95
|
+
return REASONS.UNKNOWN;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 插件内部错误码 → 契约码。
|
|
100
|
+
* @param {string|null|undefined} code 形如 ULB_ERR_TEX_DECODE
|
|
101
|
+
* @returns {string} 契约码;未登记的 ULB_ERR_* 一律 unpack-failed(不是 unknown:它确实是解包环节失败的)
|
|
102
|
+
*/
|
|
103
|
+
export function fromUlbError(code) {
|
|
104
|
+
if (!code) return REASONS.UNKNOWN;
|
|
105
|
+
const s = String(code).trim();
|
|
106
|
+
if (ULB_MAP[s]) return ULB_MAP[s];
|
|
107
|
+
if (s.startsWith('ULB_ERR_')) return REASONS.UNPACK_FAILED;
|
|
108
|
+
return normalizeReason(s);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 取某个契约码的兜底文案。
|
|
113
|
+
* @param {string} reason 契约码(内部会先归一)
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
export function reasonText(reason) {
|
|
117
|
+
const code = normalizeReason(reason);
|
|
118
|
+
return REASON_TEXT[code] || REASON_TEXT[REASONS.UNKNOWN];
|
|
119
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// names.mjs —— 名表取值(前端与服务端共用的纯函数,无平台依赖)
|
|
2
|
+
//
|
|
3
|
+
// 名表结构见 docs/设计/名表.md:
|
|
4
|
+
// characters[id] = { id, name: [{lang,text}], costume: [{lang,text}], icon }
|
|
5
|
+
// byAsset[资源文件名] = 角色 id
|
|
6
|
+
// _meta.mergeBy 声明用条目的哪个字段做归并键
|
|
7
|
+
//
|
|
8
|
+
// 为什么放在 shared:同一套取值规则前端要用(显示)、工具要用(校验)。
|
|
9
|
+
// 各写一份就会出现"页面显示的和校验认得的不是同一套规则"。
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 从多语言名数组里取一个显示名。
|
|
13
|
+
*
|
|
14
|
+
* 当前只维护中文 ⇒ 调用方一律用默认 lang('zh'),**不接受外部传语言**:
|
|
15
|
+
* 数据里只有中文时,做出"可选语言"只会让用户切过去还是看到中文,是假能力。
|
|
16
|
+
* 将来接用户语言时,只改调用处传的 lang,本函数不动。
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<{lang?: string, text?: string}>} names
|
|
19
|
+
* @param {string} lang 目标语言(默认 'zh')
|
|
20
|
+
* @returns {string} 取不到返回空串,调用方自行回落(例如显示资源名)
|
|
21
|
+
*/
|
|
22
|
+
export function pickName(names, lang = 'zh') {
|
|
23
|
+
if (!Array.isArray(names) || !names.length) return '';
|
|
24
|
+
const ok = (n) => n && typeof n.text === 'string' && n.text.trim();
|
|
25
|
+
const hit = names.find((n) => ok(n) && n.lang === lang);
|
|
26
|
+
if (hit) return hit.text.trim();
|
|
27
|
+
const zh = names.find((n) => ok(n) && n.lang === 'zh');
|
|
28
|
+
if (zh) return zh.text.trim();
|
|
29
|
+
const first = names.find(ok);
|
|
30
|
+
return first ? first.text.trim() : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 取一个名表条目的归并键。
|
|
35
|
+
* 归并字段名由 `_meta.mergeBy` 声明(不硬编码字段名),取不到时回落到条目键。
|
|
36
|
+
*
|
|
37
|
+
* @param {object} entry 名表条目
|
|
38
|
+
* @param {string} key 该条目在 characters 里的键
|
|
39
|
+
* @param {string} mergeBy 声明的归并字段名
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function mergeKeyOf(entry, key, mergeBy = 'id') {
|
|
43
|
+
const v = entry && entry[mergeBy];
|
|
44
|
+
return typeof v === 'string' && v ? v : key;
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// shared/type-labels.mjs —— 内容类型(kind)的显示名映射(**单一来源**)
|
|
2
|
+
//
|
|
3
|
+
// 谁用:前端名册的类型筛选胶囊、以及将来可能的插件/其它视图。
|
|
4
|
+
// 为什么放 shared 而不是各自写一份:显示名是"同一事实两处表达"的高危点——
|
|
5
|
+
// 前端写一套、插件再写一套,很快就会出现"同一个类型两处叫法不同"。
|
|
6
|
+
//
|
|
7
|
+
// 规则(设计口径):
|
|
8
|
+
// · 已知类型 → 可读名(下表)
|
|
9
|
+
// · 未知类型 → **原样返回 key**,不做大小写美化(避免"看起来认识其实不认识")
|
|
10
|
+
|
|
11
|
+
/** kind 为 null/空时的归并键(未解包容器条目走这一档,见设计稿 §5.1) */
|
|
12
|
+
export const UNKNOWN_TYPE_KEY = 'unknown';
|
|
13
|
+
|
|
14
|
+
/** 内置显示名:键是条目上的 kind 值(null 一律先归成 UNKNOWN_TYPE_KEY) */
|
|
15
|
+
export const TYPE_LABELS = Object.freeze({
|
|
16
|
+
spine: 'Spine',
|
|
17
|
+
live2d: 'Live2D',
|
|
18
|
+
[UNKNOWN_TYPE_KEY]: '待解包',
|
|
19
|
+
bundle: '资源包',
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 取显示名:已知返回中文/品牌名,未知**原值返回**。
|
|
24
|
+
* @param {string} typeKey 类型键(如 'spine' / 'unknown' / 将来插件的自定义值)
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function typeLabel(typeKey) {
|
|
28
|
+
const key = String(typeKey == null ? '' : typeKey);
|
|
29
|
+
if (!key) return String(UNKNOWN_TYPE_KEY);
|
|
30
|
+
return Object.prototype.hasOwnProperty.call(TYPE_LABELS, key) ? TYPE_LABELS[key] : key;
|
|
31
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# ModelStage · 前端(`src/web/`)
|
|
2
|
+
|
|
3
|
+
纯 HTML + 原生 ES 模块 + 手写 CSS,无构建步骤。页面与接口由同一个服务提供
|
|
4
|
+
(`src/server/serve.mjs`:静态文件 + `/api/*` + `/files/*` 模型资源),同源、无反向代理。
|
|
5
|
+
|
|
6
|
+
## 一、怎么起
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
bash web.sh start|stop|restart|status # 默认 8804
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
打开 `http://<本机IP>:8804/`。
|
|
13
|
+
|
|
14
|
+
| 变量 | 默认 | 作用 |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `PORT` | `8804` | 监听端口 |
|
|
17
|
+
| `MODELSTAGE_AVATAR_ROOT` | `<项目根>/avatars` | 头像根(也可用 `--avatar-root` 传)|
|
|
18
|
+
|
|
19
|
+
启停一律走 `web.sh`(PID 与日志由脚本统一管理,不要用 nohup)。
|
|
20
|
+
|
|
21
|
+
## 二、目录结构
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
src/web/
|
|
25
|
+
├── index.html 五区域骨架:顶栏 / 左栏(名册)/ 舞台 / 右栏 / 底栏
|
|
26
|
+
├── style.css 变量 + 基础重置 + 舞台 + 五区域容器几何
|
|
27
|
+
├── components.css 控件外观:品牌、按钮、搜索框、三级树行、分组行、底栏、错误横幅
|
|
28
|
+
├── main.js 入口:只做装配(品牌、面板开关、资源根、名册挂载、渲染器、错误兜底、__WEB__)
|
|
29
|
+
├── transport.js 底部播放条渲染
|
|
30
|
+
│
|
|
31
|
+
├── infra/ 与业务无关的基础设施
|
|
32
|
+
│ ├── api.js 同源 JSON 请求封装(统一错误口径)
|
|
33
|
+
│ ├── vfs.js 内存资源视图(登记表 + 字节缓存)
|
|
34
|
+
│ ├── diag.js 诊断探针 __DIAG__(错误/警告/关键事件 + 落盘)
|
|
35
|
+
│ └── plugin-host.js 插件宿主适配
|
|
36
|
+
│
|
|
37
|
+
├── roster/ 左栏名册
|
|
38
|
+
│ ├── list.js 数据层:按根取清单 + 名表归并 + 角色聚合
|
|
39
|
+
│ ├── filter.js 搜索过滤
|
|
40
|
+
│ ├── view.js 视图层:状态、事件、懒加载、选中态、持久化
|
|
41
|
+
│ └── rows.js 三级树行渲染(纯 DOM 工厂,无状态)
|
|
42
|
+
│
|
|
43
|
+
├── panel/ 右栏控制面板
|
|
44
|
+
│ ├── schema.js 控件声明(9 组 + 播放条)
|
|
45
|
+
│ ├── view.js 手风琴渲染(能力不支持的不构建)
|
|
46
|
+
│ ├── shell-controls.js / dom.js / memory-bar.js
|
|
47
|
+
│ └── panel.css 右栏/播放条内部样式
|
|
48
|
+
│
|
|
49
|
+
├── render/ 舞台与渲染面
|
|
50
|
+
│ ├── surface.js 渲染面:几何唯一来源、背景模式、渲染器切换与生命周期
|
|
51
|
+
│ ├── spine.js Spine 渲染器
|
|
52
|
+
│ ├── live2d.js Live2D 渲染器
|
|
53
|
+
│ ├── interaction.js 交互层:滚轮/拖动/双指/重置
|
|
54
|
+
│ └── camera/ 统一相机(范围、步进、钳制)
|
|
55
|
+
│
|
|
56
|
+
├── spine/ Spine 运行时(引擎封装、多版本运行时选择)
|
|
57
|
+
├── live2d/ Live2D 运行时(库加载、模型挂载、缩放平移桥)
|
|
58
|
+
└── debug/ 调试(环形日志、日志面板、模型差异采集)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**目录约定**
|
|
62
|
+
|
|
63
|
+
| 目录 | 放什么 | 不放什么 |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `infra/` | 通用能力(请求、存储、诊断) | 模型/渲染逻辑 |
|
|
66
|
+
| `roster/` `panel/` | 左右栏的视图与数据 | 渲染与相机 |
|
|
67
|
+
| `render/` | 渲染面、渲染器、相机、交互 | 具体模型的加载(在 `spine/`、`live2d/`) |
|
|
68
|
+
| `spine/` `live2d/` | 各自运行时的加载与能力封装 | 跨渲染器公共逻辑 |
|
|
69
|
+
| `debug/` | 日志与排障 | 生产路径依赖 |
|
|
70
|
+
|
|
71
|
+
规则:单文件 ≤ 400 行;文件名不带模块前缀(目录已表达归属);`main.js` 只装配。
|
|
72
|
+
|
|
73
|
+
## 三、尺寸与相机的唯一来源
|
|
74
|
+
|
|
75
|
+
| 事 | 唯一负责人 | 约束 |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| 舞台几何(CSS 像素) | `render/surface.js` 的 `geometry()` | 渲染器只读取,不得自行测 DOM |
|
|
78
|
+
| 背板像素尺寸 | 由 `geometry()` 推导后交给渲染器 | 不允许第二处 DPR 计算 |
|
|
79
|
+
| 相机(范围/步进/锚点/复位) | `render/camera/index.js` | 唯一钳制点,Spine 与 Live2D 语义一致 |
|
|
80
|
+
| 交互(滚轮/拖动/双指) | `render/interaction.js` | 绑 `surface.interactionTarget`,不绑某块画布 |
|
|
81
|
+
| 背景(棋盘/纯色/透明) | `surface.setBackground()` | 写 `data-bg` 的唯一入口,渲染器不画背景 |
|
|
82
|
+
|
|
83
|
+
调参只改 `render/camera/constants.js`(`MIN 0.125 / MAX 16 / STEP 0.1 / FIT_MARGIN 0.05`),逻辑里不出现同样的裸数字。
|
|
84
|
+
|
|
85
|
+
## 四、品牌与标题
|
|
86
|
+
|
|
87
|
+
品牌不写死在前端:启动时 `GET /api/brand` 取 `title` / `icon` / `iconKind`,同时应用到顶栏与 `document.title`;
|
|
88
|
+
配置为空时回落默认名 `ModelStage`。
|
|
89
|
+
|
|
90
|
+
- `iconKind === 'text'`:按文字/emoji 渲染
|
|
91
|
+
- `iconKind === 'image'`:按 `<img>` 渲染(站内绝对路径 / http(s) / data:)
|
|
92
|
+
- 均空:保留 `index.html` 里的内联 SVG 图标
|
|
93
|
+
|
|
94
|
+
改配置后刷新即生效,无需重启。
|
|
95
|
+
|
|
96
|
+
## 五、诊断(`window.__DIAG__`)
|
|
97
|
+
|
|
98
|
+
`main.js` 在模块体第一件事就安装它,保证早期错误也能抓到。
|
|
99
|
+
|
|
100
|
+
采什么(只收错误/警告/关键事件,不做每帧采样):
|
|
101
|
+
|
|
102
|
+
- `window.onerror`(含 img/script/link/video/audio 资源加载失败,capture 阶段)
|
|
103
|
+
- `unhandledrejection`
|
|
104
|
+
- 包装 `console.error / warn / info`(幂等;内部日志不自激)
|
|
105
|
+
- 业务主动上报:`__DIAG__.event('stage.load', { … })`
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
__DIAG__.__installed // 已安装
|
|
109
|
+
__DIAG__.errors() // error 级条目
|
|
110
|
+
__DIAG__.warnings()
|
|
111
|
+
__DIAG__.timeline() // 一行一条时间线(便于贴出来排查)
|
|
112
|
+
__DIAG__.tail(10) / dump() / size() / clear() / flushNow()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
落盘:批量 `POST /api/diag` → 追加写 `data/diag.jsonl`;失败静默降级为仅内存,不影响页面。
|
|
116
|
+
|
|
117
|
+
## 六、自定义背景图存储(`data/backgrounds/`)
|
|
118
|
+
|
|
119
|
+
- 索引 `index.json`:原子写(tmp → fsync → rename),进程内串行排队,避免并发上传互相覆盖
|
|
120
|
+
- 文件名由服务端生成(`bg-<时间戳>-<随机>.<ext>`),不用用户提供的名字
|
|
121
|
+
- 删除:文件移入 `data/backgrounds/.trash/`(可恢复),索引同步移除
|
|
122
|
+
- 上限 5MB;只收 png / jpeg / webp(按文件头判定,不信 Content-Type 与扩展名);
|
|
123
|
+
刻意不收 gif —— 动画背景在软渲染机上持续重绘,与「暂停不空转」冲突
|
|
124
|
+
|
|
125
|
+
| 方法 | 路径 | 说明 | 失败码 |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| GET | `/api/backgrounds` | 列表(新的在前) | — |
|
|
128
|
+
| POST | `/api/backgrounds` | 上传(原始二进制 + `x-filename` 显示名) | `too-large`(413) / `bad-type`(415) / `empty`(400) / `disk-error`(500) |
|
|
129
|
+
| POST | `/api/backgrounds/rename` | 只改显示名,重名自动加 `(2)` | `bad-name`(400) / `not-found`(404) |
|
|
130
|
+
| POST | `/api/backgrounds/delete` | 删除(文件进 `.trash/`) | `not-found`(404) |
|
|
131
|
+
| GET | `/api/backgrounds/file/<id>` | 出图(只查索引,不拼路径) | `not-found`(404) |
|
|
132
|
+
|
|
133
|
+
`data/backgrounds/` 是部署侧数据:换机或备份要一起带走(已 gitignore)。
|
|
134
|
+
|
|
135
|
+
## 七、自测方式
|
|
136
|
+
|
|
137
|
+
自动门禁只跑 node-test(13 套,约 100s);浏览器注入脚本在 `test/browser/`,手动单跑。
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
node tool/test-all.mjs # 自动门禁(13 套 node-test)
|
|
141
|
+
node tool/test-all.mjs --only f3-reveal # 单跑一个浏览器脚本
|
|
142
|
+
node tool/test-all.mjs --browser # 把浏览器脚本也纳入
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
三道静态/数据门禁,失败即拦:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
node tool/check-wiring.mjs # 控件声明 ↔ 接线
|
|
149
|
+
node tool/check-index.mjs # 名表结构校验
|
|
150
|
+
node tool/check-single-source.mjs
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
零错误自证(四类信号:console.error / 未捕获异常 / 浏览器 error 日志 / 4xx 与请求失败):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
node tool/check-console.mjs http://127.0.0.1:8804/ # 有错退出码 2
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 浏览器脚本的硬规矩
|
|
160
|
+
|
|
161
|
+
1. **前置条件由脚本自己建立或自己断言**,不依赖「上次跑时环境正好合适」
|
|
162
|
+
2. **不出现硬编码绝对路径**:需要路径的一律由调用方传参或从项目相对位置派生
|
|
163
|
+
3. **缺前置 = skip,不是 fail**:文案以 `skipped:` 开头,报告里单列计数;该失败的仍然 fail
|
|
164
|
+
|
|
165
|
+
| 脚本 | 前置 | 传参 |
|
|
166
|
+
|---|---|---|
|
|
167
|
+
| `f1-controls` | 页面已启动;至少一个能扫出模型的资源根 | 无(自动发现) |
|
|
168
|
+
| `f2-topbar` | 同上(截图要有画面可截) | 无 |
|
|
169
|
+
| `f3-reveal` | 名册有足够长的列表 | 无(自己挑变体最多的游戏) |
|
|
170
|
+
| `f4-roots` | 服务端存在探针目录 | **要**:`?f4probe=<服务端已存在目录>` |
|
|
171
|
+
| `spine-actions` | 至少一个能扫出模型的根;WebGL 可用 | 无 |
|
|
172
|
+
| `panel-actions` / `roster-actions` / `anim-list` / `debug-drag` | 页面已启动 | 无 |
|
|
173
|
+
|
|
174
|
+
F4 的探针目录别用点开头的隐藏目录(`/api/browse` 不列隐藏项,会被预检判成不存在)。
|
|
175
|
+
|
|
176
|
+
### 页面状态快照
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
window.__WEB__.rosterSnapshot()
|
|
180
|
+
// → { games:[{key,name,roots,status,sources,chars,variants,miss}],
|
|
181
|
+
// expandedGames, expandedChars, keyword, selected, domRows }
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
自动化断言也读这个出口。
|
|
185
|
+
|
|
186
|
+
## 八、决策记录
|
|
187
|
+
|
|
188
|
+
1. **一级节点按 name 合并**:一个游戏一个节点,角色**跨根归并**(同一角色的 Live2D 与 Spine 聚成一个)
|
|
189
|
+
2. **搜索补加载全部游戏**:首次输入关键词时并行拉齐各游戏(服务端有清单缓存,实测 <1s)。
|
|
190
|
+
取舍:宁可多一次请求,也要搜得到 —— 只搜已展开的游戏会让人以为列表缺内容
|
|
191
|
+
3. **不做区域几何 diff 工具**:视觉已降级,改用截图比对或人工核对
|
|
192
|
+
4. **改左栏行为后先跑 `roster-actions`**(穷举实操 + 断言)
|
|
193
|
+
5. **名字与头像来自名表**,不从资源目录名现场推断:目录名只作匹配线索,名字以名表为准
|