modelstage 0.0.0-stage → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +101 -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 +58 -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 +647 -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 +334 -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/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 +465 -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 +115 -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 +52 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +37 -0
- package/src/web/roster/view.js +670 -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/check-perms.mjs +114 -0
- package/tool/lib/contract-checks.mjs +582 -0
- package/tool/lib/cubism-core.mjs +122 -0
- package/tool/lib/export-bundle.mjs +328 -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/lib/spine-runtime.mjs +131 -0
- package/tool/lib/test-lock.mjs +178 -0
- package/tool/lib/test-report.mjs +133 -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,571 @@
|
|
|
1
|
+
// render/surface.js —— 渲染面(StageSurface):画布与帧时钟的唯一所有者
|
|
2
|
+
//
|
|
3
|
+
// 职责边界:
|
|
4
|
+
// · Surface **唯一拥有**:#stage-canvas(Spine 面)、覆盖层容器与它的 canvas(Live2D 面)、
|
|
5
|
+
// 尺寸计算、DPR、z 序、可见性、`data-stage-mode`、帧调度入口;
|
|
6
|
+
// · Renderer 只拿到 **handle**(canvas / overlay / size / dpr / requestRender / onResize),
|
|
7
|
+
// 不许自己建 canvas、不许读 DOM 尺寸、不许自己起 rAF 主循环;
|
|
8
|
+
// · 切换只有一个入口 `activate(document)`:loading → 统一卸载 → 装载 → 切面,串行保护;
|
|
9
|
+
// · 卸载只有 `disposeActive()`:幂等、不关心当前是哪一面。
|
|
10
|
+
//
|
|
11
|
+
// 为什么这么分:新增渲染面(3D)时只加一个 register(),不必再改画布/尺寸/可见性逻辑。
|
|
12
|
+
|
|
13
|
+
import { createSwitcher } from './surface-switch.js';
|
|
14
|
+
// 面元数据表:kind/id → 面属性(画布、覆盖层、stageMode)的唯一真相
|
|
15
|
+
import { byIdOrKind, byKind } from './surfaces.js';
|
|
16
|
+
import { buildDocument } from '../infra/document.js';
|
|
17
|
+
|
|
18
|
+
/** 覆盖层容器 id(Live2D 面用) */
|
|
19
|
+
|
|
20
|
+
const OVERLAY_ID = 'live2d-overlay';
|
|
21
|
+
/** 覆盖层内 canvas id */
|
|
22
|
+
const OVERLAY_CANVAS_ID = 'live2d-overlay-canvas';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 创建渲染面。
|
|
26
|
+
* @param {{
|
|
27
|
+
* root?: HTMLElement | (() => HTMLElement|null),
|
|
28
|
+
* log?: (message: string, level?: 'info'|'warn'|'error') => void,
|
|
29
|
+
* }} [options] root 缺省用 #stage
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* 按 mode 选出要调用的卸载方法(纯函数,契约 §二 / §五-3 的落地判据)。
|
|
33
|
+
* · mode='unmount' → 优先 unmount(释放内容、保留引擎)
|
|
34
|
+
* · 其它(含缺省)→ dispose(彻底拆)
|
|
35
|
+
* · 渲染器**没实现** unmount 时回落 dispose —— 宁可彻底拆,也不能什么都不做(否则就是资源泄漏)
|
|
36
|
+
* @param {object|null} renderer
|
|
37
|
+
* @param {'unmount'|'dispose'} mode
|
|
38
|
+
* @returns {Function|null} 无参调用函数;渲染器为空或两种方法都没有时返回 null
|
|
39
|
+
*/
|
|
40
|
+
export function pickDisposeCall(renderer, mode) {
|
|
41
|
+
if (!renderer) return null;
|
|
42
|
+
const want = mode === 'unmount' ? 'unmount' : 'dispose';
|
|
43
|
+
if (typeof renderer[want] === 'function') return () => renderer[want]();
|
|
44
|
+
if (typeof renderer.dispose === 'function') return () => renderer.dispose();
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createStageSurface({ root, log } = {}) {
|
|
49
|
+
/** 根容器(#stage) */
|
|
50
|
+
function rootEl() {
|
|
51
|
+
if (typeof root === 'function') return root();
|
|
52
|
+
return root || document.getElementById('stage');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const renderers = new Map();
|
|
56
|
+
const resizeCbs = new Set();
|
|
57
|
+
/** 切换完成后要通知的订阅者(面板据此重算三态) */
|
|
58
|
+
const switchCbs = new Set();
|
|
59
|
+
|
|
60
|
+
const st = {
|
|
61
|
+
/** 当前渲染器 id(null = 未激活) */
|
|
62
|
+
activeId: null,
|
|
63
|
+
/** 当前已装载的 document(便于排查) */
|
|
64
|
+
document: null,
|
|
65
|
+
/** 切换串行锁 */
|
|
66
|
+
swapping: false,
|
|
67
|
+
pending: null, // 切换进行中收到的最新请求(last-wins,完成后立即消费)
|
|
68
|
+
/** 尺寸与 DPR */
|
|
69
|
+
size: { width: 0, height: 0 },
|
|
70
|
+
backgroundMode: 'checkerboard', // 背景唯一权威(写 data-bg 的唯一入口)
|
|
71
|
+
bgCbs: new Set(),
|
|
72
|
+
dpr: (typeof window !== 'undefined' && window.devicePixelRatio) || 1,
|
|
73
|
+
/** 帧统计 */
|
|
74
|
+
frames: 0,
|
|
75
|
+
fps: 0,
|
|
76
|
+
fpsAt: 0,
|
|
77
|
+
/** rAF 句柄(Surface 的帧调度入口) */
|
|
78
|
+
rafId: 0,
|
|
79
|
+
/** 待渲染标记 */
|
|
80
|
+
dirty: false,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** 统一日志出口 */
|
|
84
|
+
function note(message, level = 'info') {
|
|
85
|
+
if (typeof log === 'function') {
|
|
86
|
+
try { log(message, level); } catch { /* 忽略 */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------- 画布所有权
|
|
91
|
+
/** 覆盖层容器(Surface 建、Surface 管) */
|
|
92
|
+
let overlayEl = null;
|
|
93
|
+
|
|
94
|
+
/** 确保覆盖层存在(父级 = #stage,绝对定位铺满,不接收指针事件) */
|
|
95
|
+
function ensureOverlay() {
|
|
96
|
+
if (overlayEl && overlayEl.isConnected) return overlayEl;
|
|
97
|
+
let el = document.getElementById(OVERLAY_ID);
|
|
98
|
+
if (!el) {
|
|
99
|
+
el = document.createElement('div');
|
|
100
|
+
el.id = OVERLAY_ID;
|
|
101
|
+
// 定位/尺寸/pointer-events 全在 style.css 的 #live2d-overlay(契约:不在 JS 里写设计值)
|
|
102
|
+
const cv = document.createElement('canvas');
|
|
103
|
+
cv.id = OVERLAY_CANVAS_ID;
|
|
104
|
+
el.appendChild(cv);
|
|
105
|
+
(rootEl() || document.body).appendChild(el);
|
|
106
|
+
}
|
|
107
|
+
overlayEl = el;
|
|
108
|
+
return el;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 取覆盖层内的 canvas(Live2D 渲染目标) */
|
|
112
|
+
function overlayCanvas() {
|
|
113
|
+
ensureOverlay();
|
|
114
|
+
return document.getElementById(OVERLAY_CANVAS_ID);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 取 Spine 面 canvas(舞台里已有的那块,Surface 负责尺寸/DPR) */
|
|
118
|
+
function spineCanvas() {
|
|
119
|
+
return document.getElementById('stage-canvas');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------- 尺寸 / DPR
|
|
123
|
+
/**
|
|
124
|
+
* 量测根容器的 CSS 尺寸,并把「CSS 尺寸 + 期望 DPR」交给当前渲染器。
|
|
125
|
+
* 为什么这里不写 canvas.width:像素尺寸(backing store)只由渲染器/引擎拥有,
|
|
126
|
+
* Surface 一旦也写,就会与引擎的 getEffectiveDpr 口径打架 ——
|
|
127
|
+
* 像素网格变了而 gl.viewport / camera.viewportWidth 没变 ⇒ 画面整体缩放偏移(BUG-2)。
|
|
128
|
+
*/
|
|
129
|
+
function measure() {
|
|
130
|
+
const el = rootEl();
|
|
131
|
+
if (!el) return st.size;
|
|
132
|
+
const rect = el.getBoundingClientRect();
|
|
133
|
+
let width = Math.max(1, Math.round(rect.width || el.clientWidth || 0));
|
|
134
|
+
let height = Math.max(1, Math.round(rect.height || el.clientHeight || 0));
|
|
135
|
+
// 移动端旋转后 layout viewport 可能滞后:根容器是 position:fixed + inset:0,
|
|
136
|
+
// 实测横屏时它的 rect 仍是竖屏尺寸(777x1035),与真实视口方向相反 ——
|
|
137
|
+
// 这会让画布与模型按旧方向布局,表现为"模型只在某个矩形内可见、拖动超出不显示"。
|
|
138
|
+
// 此时以 innerWidth/innerHeight 为准(它跟随真实视口更新)。
|
|
139
|
+
// 只在「方向相反」时替换:同方向下的细微差异仍以容器为准(容器可能带内边距等)。
|
|
140
|
+
if (typeof window !== 'undefined') {
|
|
141
|
+
const iw = Math.round(window.innerWidth || 0);
|
|
142
|
+
const ih = Math.round(window.innerHeight || 0);
|
|
143
|
+
if (iw > 0 && ih > 0 && (width > height) !== (iw > ih)) {
|
|
144
|
+
width = iw;
|
|
145
|
+
height = ih;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const changed = width !== st.size.width || height !== st.size.height;
|
|
149
|
+
st.size = { width, height };
|
|
150
|
+
const box = { cssW: width, cssH: height, dpr: st.dpr };
|
|
151
|
+
if (changed) {
|
|
152
|
+
notifyResize(box);
|
|
153
|
+
for (const cb of resizeCbs) {
|
|
154
|
+
try { cb({ ...box }); } catch { /* 回调异常不影响其它订阅者 */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return st.size;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 读取 URL 上的 ?res=N 强制分辨率(对照实验用;缺省/非法返回 null)。
|
|
162
|
+
* 放在 Surface 里:分辨率也是「几何」的一部分,不允许各渲染面自己解析一遍。
|
|
163
|
+
* @returns {number|null}
|
|
164
|
+
*/
|
|
165
|
+
function forcedResOverride() {
|
|
166
|
+
try {
|
|
167
|
+
const raw = new URLSearchParams(window.location.search || '').get('res');
|
|
168
|
+
if (!raw) return null;
|
|
169
|
+
const n = Number(raw);
|
|
170
|
+
return Number.isFinite(n) && n >= 1 && n <= 4 ? n : null;
|
|
171
|
+
} catch { return null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 尺寸唯一来源:渲染器只从这里取几何,不得自己读 DOM 或 devicePixelRatio。
|
|
176
|
+
* 语义:cssW/cssH = 舞台 CSS 像素;dpr = 设备比;res = 期望分辨率。
|
|
177
|
+
* 最终生效的分辨率由各渲染器按自身规则决定(Spine 引擎会再按软件渲染 / dprMode 收口)。
|
|
178
|
+
* 为什么集中在 Surface:同一件事只能有一个负责人,否则两套渲染面会各自算出不同尺寸。
|
|
179
|
+
* @returns {{cssW:number, cssH:number, dpr:number, res:number}}
|
|
180
|
+
*/
|
|
181
|
+
function geometry() {
|
|
182
|
+
let width = st.size.width;
|
|
183
|
+
let height = st.size.height;
|
|
184
|
+
// 首次调用可能还没 measure 过:只补一次只读测量,不触发 notifyResize
|
|
185
|
+
if (!width || !height) {
|
|
186
|
+
const el = rootEl();
|
|
187
|
+
if (el) {
|
|
188
|
+
const rect = el.getBoundingClientRect();
|
|
189
|
+
width = Math.max(1, Math.round(rect.width || el.clientWidth || 0));
|
|
190
|
+
height = Math.max(1, Math.round(rect.height || el.clientHeight || 0));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const dpr = st.dpr > 0 ? st.dpr : 1;
|
|
194
|
+
const forced = forcedResOverride();
|
|
195
|
+
return { cssW: width || 1, cssH: height || 1, dpr, res: forced || dpr };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* 把尺寸变化交给当前渲染器(渲染器负责决定 backing store:引擎侧按 getEffectiveDpr 算)。
|
|
199
|
+
* @param {{cssW:number, cssH:number, dpr:number}} box
|
|
200
|
+
*/
|
|
201
|
+
function notifyResize(box) {
|
|
202
|
+
const entry = renderers.get(st.activeId);
|
|
203
|
+
if (entry && typeof entry.resize === 'function') {
|
|
204
|
+
try { entry.resize(box); } catch (err) { note(`渲染器 resize 失败:${err && err.message}`, 'warn'); }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** 尺寸变化订阅(渲染器在这里拿到最新尺寸,自己不读 DOM) */
|
|
209
|
+
function onResize(cb) {
|
|
210
|
+
if (typeof cb !== 'function') return () => {};
|
|
211
|
+
resizeCbs.add(cb);
|
|
212
|
+
return () => resizeCbs.delete(cb);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** ResizeObserver:root 尺寸变化即重算(避免轮询) */
|
|
216
|
+
let observer = null;
|
|
217
|
+
function watchResize() {
|
|
218
|
+
if (observer || typeof ResizeObserver !== 'function') return;
|
|
219
|
+
const el = rootEl();
|
|
220
|
+
if (!el) return;
|
|
221
|
+
observer = new ResizeObserver(() => { measure(); });
|
|
222
|
+
observer.observe(el);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------- 常驻断言
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------- 帧时钟
|
|
228
|
+
/** 调度一次渲染(Surface 的帧入口:渲染器只"请求渲染",不自己起循环) */
|
|
229
|
+
function requestRender() {
|
|
230
|
+
st.dirty = true;
|
|
231
|
+
if (st.rafId) return;
|
|
232
|
+
st.rafId = requestAnimationFrame(() => {
|
|
233
|
+
st.rafId = 0;
|
|
234
|
+
if (!st.dirty) return;
|
|
235
|
+
st.dirty = false;
|
|
236
|
+
st.frames += 1;
|
|
237
|
+
const now = performance.now();
|
|
238
|
+
if (!st.fpsAt) st.fpsAt = now;
|
|
239
|
+
if (now - st.fpsAt >= 1000) {
|
|
240
|
+
st.fps = Math.round((st.frames * 1000) / (now - st.fpsAt));
|
|
241
|
+
st.frames = 0;
|
|
242
|
+
st.fpsAt = now;
|
|
243
|
+
}
|
|
244
|
+
const entry = renderers.get(st.activeId);
|
|
245
|
+
if (entry && typeof entry.render === 'function') {
|
|
246
|
+
try { entry.render({ width: st.size.width, height: st.size.height, dpr: st.dpr }); } catch { /* 忽略 */ }
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---------------------------------------------------------------- 切面(唯一写入点)
|
|
252
|
+
/**
|
|
253
|
+
* 写 data-stage-mode(**本文件是唯一写入点**)。
|
|
254
|
+
* @param {'spine'|'live2d'|'loading'} mode 渲染面的 id/kind,或过渡值 'loading'
|
|
255
|
+
*/
|
|
256
|
+
function setMode(mode) {
|
|
257
|
+
const el = rootEl();
|
|
258
|
+
// 查表得面属性:'loading' 是过渡遮罩(不属于任何面),其余一律要求命中表项。
|
|
259
|
+
// 原先 `mode === 'live2d' ? 'live2d' : 'spine'` 会把未知面**静默写成 spine** ——
|
|
260
|
+
// 于是 CSS 走 2D 布局、覆盖层显隐也按 spine 判,加新面时整个舞台错乱且不报错。
|
|
261
|
+
const meta = mode === 'loading' ? null : byIdOrKind(mode);
|
|
262
|
+
const next = mode === 'loading' ? 'loading' : (meta ? meta.stageMode : null);
|
|
263
|
+
if (!next) throw new Error(`setMode 收到未知渲染面「${mode || '?'}」:未在 render/surfaces.js 注册`);
|
|
264
|
+
if (el) el.dataset.stageMode = next;
|
|
265
|
+
// 覆盖层显隐由表字段 overlayVisible 决定(只有声明用覆盖层的面才可见)
|
|
266
|
+
const ov = document.getElementById(OVERLAY_ID);
|
|
267
|
+
// 显隐用类切换(样式在 style.css),避免 JS 写 display
|
|
268
|
+
if (ov) ov.classList.toggle('is-visible', !!(meta && meta.overlayVisible));
|
|
269
|
+
return next;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** 读当前面 */
|
|
273
|
+
function stageMode() {
|
|
274
|
+
const el = rootEl();
|
|
275
|
+
return el ? el.dataset.stageMode || null : null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** 切换断言日志(只读三项状态 + 画布尺寸) */
|
|
279
|
+
function noteSwitch(tag) {
|
|
280
|
+
const cv = spineCanvas();
|
|
281
|
+
const ov = document.getElementById(OVERLAY_ID);
|
|
282
|
+
const entry = renderers.get(st.activeId);
|
|
283
|
+
let active = null;
|
|
284
|
+
try { active = entry && typeof entry.isActive === 'function' ? !!entry.isActive() : null; } catch { active = 'err'; }
|
|
285
|
+
const line = `${tag} → stageMode=${stageMode()} active=${st.activeId} activeState=${active}`
|
|
286
|
+
+ ` overlay=${ov ? getComputedStyle(ov).display : 'none'} spineCanvas=${cv ? getComputedStyle(cv).visibility : null}`
|
|
287
|
+
+ ` width=${cv ? cv.width : null} size=${st.size.width}x${st.size.height}@${st.dpr}`;
|
|
288
|
+
note(line);
|
|
289
|
+
return line;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ---------------------------------------------------------------- 注册与句柄
|
|
293
|
+
/**
|
|
294
|
+
* 注册一个渲染器。
|
|
295
|
+
*
|
|
296
|
+
* 这里做**启动期断言**:渲染器必须声明 kind,且该 kind 必须在面元数据表里、id 与表项一致。
|
|
297
|
+
* 目的:让"注册了渲染器却忘了加表项"在启动时就炸,而不是等用户点开那个面才现形
|
|
298
|
+
* (那种错表现为静默误判,最难排)。也顺带挡住 register('spine-v2') 这类 id 漂移 ——
|
|
299
|
+
* 表的 id 是 setMode/handle/卸载模式的推导依据,漂了就会查不到。
|
|
300
|
+
* @param {{id: string, kind: string, capabilities: object, mount: Function, dispose?: Function, isActive?: Function, camera?: object, render?: Function}} renderer
|
|
301
|
+
*/
|
|
302
|
+
function register(renderer) {
|
|
303
|
+
if (!renderer || !renderer.id) throw new Error('stageSurface.register: 渲染器必须有 id');
|
|
304
|
+
if (!renderer.kind) {
|
|
305
|
+
throw new Error(`stageSurface.register: 渲染器「${renderer.id}」必须声明 kind(用于面元数据表校验)`);
|
|
306
|
+
}
|
|
307
|
+
const meta = byKind(renderer.kind);
|
|
308
|
+
if (!meta) {
|
|
309
|
+
throw new Error(`stageSurface.register: 渲染面「${renderer.kind}」未在 render/surfaces.js 注册`);
|
|
310
|
+
}
|
|
311
|
+
if (meta.id !== renderer.id) {
|
|
312
|
+
throw new Error(`stageSurface.register: 渲染器 id「${renderer.id}」与表项 id「${meta.id}」不一致(kind=${renderer.kind})`);
|
|
313
|
+
}
|
|
314
|
+
renderers.set(renderer.id, renderer);
|
|
315
|
+
note(`注册渲染器:${renderer.id}(kind=${renderer.kind})`);
|
|
316
|
+
return renderer;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 取渲染资源句柄(渲染器只能通过它碰画布/尺寸)。
|
|
321
|
+
* @param {string} rendererId
|
|
322
|
+
*/
|
|
323
|
+
function handle(rendererId) {
|
|
324
|
+
// 用哪个画布由表字段 overlay 决定:'canvas' ⇒ 该面独占覆盖层 canvas;null ⇒ 用舞台画布。
|
|
325
|
+
// 原先 `const isLive2D = rendererId === 'live2d'` 让未知面拿到舞台画布 + overlay=null,
|
|
326
|
+
// 等于静默降级成 spine 的形态(新面会在错误的画布上画)。
|
|
327
|
+
const meta = byIdOrKind(rendererId);
|
|
328
|
+
if (!meta) throw new Error(`surface.handle 收到未知渲染面「${rendererId || '?'}」:未在 render/surfaces.js 注册`);
|
|
329
|
+
const useOverlay = meta.overlay === 'canvas';
|
|
330
|
+
return {
|
|
331
|
+
/** 该渲染面独占的 canvas(表里 overlay=null 的面用舞台画布,overlay='canvas' 的用覆盖层) */
|
|
332
|
+
canvas: useOverlay ? overlayCanvas() : spineCanvas(),
|
|
333
|
+
/** 覆盖层容器(声明用覆盖层的面才有;其余为 null) */
|
|
334
|
+
overlay: useOverlay ? ensureOverlay() : null,
|
|
335
|
+
/** 逻辑尺寸(CSS 像素) */
|
|
336
|
+
get size() { return { ...st.size }; },
|
|
337
|
+
/** 设备像素比 */
|
|
338
|
+
get dpr() { return st.dpr; },
|
|
339
|
+
/** 尺寸唯一来源(cssW/cssH/dpr/res) */
|
|
340
|
+
geometry,
|
|
341
|
+
/** 请求渲染(Surface 调度,渲染器不自己起 rAF) */
|
|
342
|
+
requestRender,
|
|
343
|
+
/** 订阅尺寸变化 */
|
|
344
|
+
onResize,
|
|
345
|
+
/** 背景模式(唯一权威在 Surface;渲染器只读) */
|
|
346
|
+
getBackgroundMode,
|
|
347
|
+
/** 订阅背景变化(mount 时订阅、卸载时退订) */
|
|
348
|
+
onBackgroundChange,
|
|
349
|
+
/** 背景配色(从 CSS 变量集中读取) */
|
|
350
|
+
palette,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ---------------------------------------------------------------- 激活状态机
|
|
355
|
+
/**
|
|
356
|
+
* 唯一卸载入口:幂等,不关心当前是哪一面。
|
|
357
|
+
* @param {'unmount'|'dispose'} [mode] 切换渲染面用 unmount(保留引擎);Surface 销毁用 dispose(彻底拆)
|
|
358
|
+
*/
|
|
359
|
+
async function disposeActive(mode = 'dispose') {
|
|
360
|
+
const entry = renderers.get(st.activeId);
|
|
361
|
+
const call = pickDisposeCall(entry, mode);
|
|
362
|
+
if (call) {
|
|
363
|
+
try { await call(); } catch { /* 卸载失败也要继续切面 */ }
|
|
364
|
+
}
|
|
365
|
+
st.activeId = null;
|
|
366
|
+
st.document = null;
|
|
367
|
+
setMode('loading');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 唯一切换入口。
|
|
372
|
+
* @param {{kind?: 'spine'|'live2d', id?: string, rendererId?: string}} document
|
|
373
|
+
* @returns {Promise<any>} 装载结果(并发被丢弃时返回 null)
|
|
374
|
+
*/
|
|
375
|
+
// 切换状态机(last-wins 排队 + 失败回滚)拆到 surface-switch.js,本文件只提供回调
|
|
376
|
+
const switcher = createSwitcher({
|
|
377
|
+
renderers,
|
|
378
|
+
state: st,
|
|
379
|
+
setMode,
|
|
380
|
+
disposeActive,
|
|
381
|
+
handle,
|
|
382
|
+
measure,
|
|
383
|
+
requestRender,
|
|
384
|
+
note,
|
|
385
|
+
noteSwitch,
|
|
386
|
+
onSwitched: (activeId) => { for (const cb of switchCbs) { try { cb(activeId, stageMode()); } catch { /* 订阅者异常不影响切换 */ } } },
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
/** 切换到一份文档(唯一入口,语义见 surface-switch.js) */
|
|
390
|
+
function activate(document) {
|
|
391
|
+
return switcher.activate(document);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/** 当前渲染器的相机(交互层通过它拿到 camera.*,不关心是哪一面) */
|
|
395
|
+
function activeCamera() {
|
|
396
|
+
const entry = renderers.get(st.activeId);
|
|
397
|
+
return entry && entry.camera ? entry.camera : null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/** 只读统计 */
|
|
401
|
+
function stats() {
|
|
402
|
+
return {
|
|
403
|
+
active: st.activeId,
|
|
404
|
+
mode: stageMode(),
|
|
405
|
+
size: { ...st.size },
|
|
406
|
+
dpr: st.dpr,
|
|
407
|
+
fps: st.fps,
|
|
408
|
+
swapping: st.swapping,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 设置期望 DPR:只记录并下发给渲染器,**不直接写画布**。
|
|
414
|
+
* 真实生效值由引擎 getEffectiveDpr 决定(它知道软件渲染、dprMode、封顶 2 这些规则)。
|
|
415
|
+
* @param {number|string} value 像素比,或 'auto'
|
|
416
|
+
*/
|
|
417
|
+
function setDpr(value) {
|
|
418
|
+
const v = value === 'auto' || value === undefined ? null : Number(value);
|
|
419
|
+
st.dprRequest = v && v > 0 ? v : null;
|
|
420
|
+
st.dpr = st.dprRequest || (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
|
|
421
|
+
const entry = renderers.get(st.activeId);
|
|
422
|
+
if (entry && typeof entry.setDpr === 'function') {
|
|
423
|
+
try { entry.setDpr(value); } catch (err) { note(`渲染器 setDpr 失败:${err && err.message}`, 'warn'); }
|
|
424
|
+
}
|
|
425
|
+
measure();
|
|
426
|
+
requestRender();
|
|
427
|
+
return st.dpr;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* 读当前背景模式(唯一权威在 Surface,渲染器不得自己读 DOM 属性)。
|
|
432
|
+
* @returns {'checkerboard'|'solid'|'transparent'}
|
|
433
|
+
*/
|
|
434
|
+
function getBackgroundMode() {
|
|
435
|
+
return st.backgroundMode;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* 读背景配色(从 CSS 变量集中读取,渲染器不自己 getComputedStyle)。
|
|
440
|
+
* 为什么集中:棋盘/纯色的具体色值属于"表面"的视觉约定,分散到各渲染器会两套真相。
|
|
441
|
+
* @returns {{solid:string, checkerDark:string, checkerLight:string}}
|
|
442
|
+
*/
|
|
443
|
+
function palette() {
|
|
444
|
+
const el = rootEl();
|
|
445
|
+
const read = (name, fallback) => {
|
|
446
|
+
try {
|
|
447
|
+
const v = el ? getComputedStyle(el).getPropertyValue(name).trim() : '';
|
|
448
|
+
return v || fallback;
|
|
449
|
+
} catch { return fallback; }
|
|
450
|
+
};
|
|
451
|
+
return {
|
|
452
|
+
solid: read('--stage-solid', '#2a2a2a'),
|
|
453
|
+
checkerDark: read('--stage-checker-dark', '#2a2a2a'),
|
|
454
|
+
checkerLight: read('--stage-checker-light', '#3a3f45'),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* 设置背景模式(**写 data-bg 的唯一入口**)。
|
|
460
|
+
* 副作用顺序:更新权威状态 → 写 data-bg(CSS 兜底)→ 通知所有渲染器与订阅者。
|
|
461
|
+
* @param {'checkerboard'|'solid'|'transparent'} mode
|
|
462
|
+
* @returns {string} 规范化后的模式
|
|
463
|
+
*/
|
|
464
|
+
function setBackground(mode) {
|
|
465
|
+
const el = rootEl();
|
|
466
|
+
const next = mode === 'solid' || mode === 'transparent' ? mode : 'checkerboard';
|
|
467
|
+
const changed = next !== st.backgroundMode;
|
|
468
|
+
st.backgroundMode = next;
|
|
469
|
+
if (el) el.dataset.bg = next;
|
|
470
|
+
const pal = palette();
|
|
471
|
+
// 通知所有已注册渲染器(不论当前是否激活:切面后状态必须一致)
|
|
472
|
+
for (const [, entry] of renderers) {
|
|
473
|
+
if (entry && typeof entry.setBackground === 'function') {
|
|
474
|
+
try { entry.setBackground(next, pal); } catch (err) { note(`渲染器 setBackground 失败:${err && err.message}`, 'warn'); }
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (changed) {
|
|
478
|
+
for (const cb of st.bgCbs) {
|
|
479
|
+
try { cb(next, pal); } catch { /* 订阅者异常不影响其他订阅者 */ }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return next;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* 订阅背景变化(渲染器在 mount 时订阅、卸载时退订)。
|
|
487
|
+
* 回调签名:(mode, palette) => void
|
|
488
|
+
* @param {(mode: string, pal: object) => void} cb
|
|
489
|
+
* @returns {() => void} 退订函数
|
|
490
|
+
*/
|
|
491
|
+
function onBackgroundChange(cb) {
|
|
492
|
+
if (typeof cb !== 'function') return () => {};
|
|
493
|
+
st.bgCbs.add(cb);
|
|
494
|
+
return () => st.bgCbs.delete(cb);
|
|
495
|
+
}
|
|
496
|
+
/** 释放整块表面 */
|
|
497
|
+
function dispose() {
|
|
498
|
+
try { disposeActive(); } catch { /* 忽略 */ }
|
|
499
|
+
if (observer) { try { observer.disconnect(); } catch { /* 忽略 */ } observer = null; }
|
|
500
|
+
if (st.rafId) { try { cancelAnimationFrame(st.rafId); } catch { /* 忽略 */ } st.rafId = 0; }
|
|
501
|
+
resizeCbs.clear();
|
|
502
|
+
renderers.clear();
|
|
503
|
+
note('渲染面已释放');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// 初始化:建覆盖层、量尺寸、开始观察
|
|
507
|
+
try {
|
|
508
|
+
ensureOverlay();
|
|
509
|
+
// 以 DOM 现值作为初始权威(样式表可能已预设 data-bg)
|
|
510
|
+
try {
|
|
511
|
+
const el0 = rootEl();
|
|
512
|
+
st.backgroundMode = (el0 && el0.dataset && el0.dataset.bg) || 'checkerboard';
|
|
513
|
+
} catch { /* 忽略 */ }
|
|
514
|
+
measure();
|
|
515
|
+
watchResize();
|
|
516
|
+
} catch (err) {
|
|
517
|
+
note(`渲染面初始化失败:${err && err.message}`, 'warn');
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
register,
|
|
522
|
+
/** Assets 层入口(测试/工具用):catalog entry → ready Document */
|
|
523
|
+
buildDocument,
|
|
524
|
+
handle,
|
|
525
|
+
activate,
|
|
526
|
+
dispose,
|
|
527
|
+
disposeActive,
|
|
528
|
+
setDpr,
|
|
529
|
+
stats,
|
|
530
|
+
stageMode,
|
|
531
|
+
setMode,
|
|
532
|
+
activeCamera,
|
|
533
|
+
requestRender,
|
|
534
|
+
onResize,
|
|
535
|
+
measure,
|
|
536
|
+
geometry,
|
|
537
|
+
getBackgroundMode,
|
|
538
|
+
setBackground,
|
|
539
|
+
onBackgroundChange,
|
|
540
|
+
palette,
|
|
541
|
+
/**
|
|
542
|
+
* 交互目标:整个舞台区域(不是某一块画布)。
|
|
543
|
+
* 为什么要暴露它:画布可见性会随渲染面切换而变化(Live2D 面下 Spine 画布
|
|
544
|
+
* `visibility:hidden` 不参与命中测试),而交互属于"舞台区域"、不属于某块画布;
|
|
545
|
+
* 由 Surface 给出恒定目标,交互层绑定一次即可 —— 切面后无需重绑,也不会双监听。
|
|
546
|
+
*/
|
|
547
|
+
get interactionTarget() { return rootEl(); },
|
|
548
|
+
renderers: () => [...renderers.keys()],
|
|
549
|
+
/** 订阅渲染面切换(回调收到新的激活 id 与 stageMode) */
|
|
550
|
+
onRendererChange: (cb) => (typeof cb === 'function' ? (switchCbs.add(cb), () => switchCbs.delete(cb)) : () => {}),
|
|
551
|
+
/**
|
|
552
|
+
* 当前生效的能力集合。
|
|
553
|
+
* 取"激活中的渲染器";还没装载任何内容时用**首个注册者**(默认面),
|
|
554
|
+
* 因为能力是静态声明,与是否已装载模型无关;一个渲染器都没有才返回 null(面板走回落)。
|
|
555
|
+
*/
|
|
556
|
+
capabilities() {
|
|
557
|
+
const entry = renderers.get(st.activeId) || renderers.values().next().value;
|
|
558
|
+
return entry && entry.capabilities ? entry.capabilities : null;
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* 当前激活渲染器(供外壳把设置下发给它,不自己持有舞台引用)。
|
|
562
|
+
* 还没装载任何东西时退回**首个注册者**(默认面):设置快照要在启动阶段就落到引擎,
|
|
563
|
+
* 等装载时再补会出现"面板显示 A、引擎在 B"的漂移(与上面 capabilities() 同一口径)。
|
|
564
|
+
*/
|
|
565
|
+
activeRenderer() {
|
|
566
|
+
return renderers.get(st.activeId) || renderers.values().next().value || null;
|
|
567
|
+
},
|
|
568
|
+
/** 按 id 取渲染器 */
|
|
569
|
+
renderer: (id) => renderers.get(id) || null,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// render/surfaces.js —— 渲染面元数据表:**「有哪些面、每个面长什么样」的唯一真相**。
|
|
2
|
+
//
|
|
3
|
+
// 为什么要有这张表:这个事实原先被十处 `x === 'live2d' ? : 'spine'` 分别表达。
|
|
4
|
+
// 只有两个面时那种写法成立("不是 live2d 就是 spine"),加第三个面时每一处都会
|
|
5
|
+
// **静默误判成 spine** —— 表现为功能错乱而非报错,是最难排的一类缺陷。
|
|
6
|
+
//
|
|
7
|
+
// 加新面时:① 在这里加一行 ② 写 render/<kind>.js ③ main.js 里 register() 一次。
|
|
8
|
+
// 消费方一律查表取值,不再自己写二分判断。
|
|
9
|
+
//
|
|
10
|
+
// 核心约束:**未知 kind 一律返回 null,绝不"回落到 spine"**。
|
|
11
|
+
// 返回 null 会逼调用方显式处理(报错或跳过),比"猜一个默认值"安全得多。
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 每个字段都对应一处原先的硬编码(加 3D 时按需填,不必再找第 11 个 if):
|
|
15
|
+
* id 渲染器 id(= document.rendererId 的缺省值,surface.register 的键)
|
|
16
|
+
* kind document.kind 的取值(catalog/document 层的内容类型)
|
|
17
|
+
* label 名册里显示的类型名(原 roster/list.js 的三元表达式)
|
|
18
|
+
* badge 名册徽标的 CSS 修饰名(原 roster/rows.js 的 kindClass)
|
|
19
|
+
* stageMode 写到 data-stage-mode 的值,CSS 据此选布局(原 surface.js setMode)
|
|
20
|
+
* overlay 'canvas' = 该面用**覆盖层** canvas;null = 用舞台画布(原 isLive2D 判断)
|
|
21
|
+
* overlayVisible 覆盖层容器是否可见(原 setMode 里的 is-visible 类切换)
|
|
22
|
+
* cameraKind 相机常量覆盖表的键(原 camera/constants.js limitsFor)
|
|
23
|
+
* deepLink 深链(?model= 不带 runtime 时)是否接受这个 kind(原 app/url-params.js)
|
|
24
|
+
*/
|
|
25
|
+
export const SURFACES = [
|
|
26
|
+
{ id: 'spine', kind: 'spine', label: 'Spine', badge: 'spine', stageMode: 'spine', overlay: null, overlayVisible: false, cameraKind: 'spine', deepLink: true },
|
|
27
|
+
{ id: 'live2d', kind: 'live2d', label: 'Live2D', badge: 'live2d', stageMode: 'live2d', overlay: 'canvas', overlayVisible: true, cameraKind: 'live2d', deepLink: true },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** 按 kind 查(未知 → null) */
|
|
31
|
+
export function byKind(kind) {
|
|
32
|
+
if (!kind) return null;
|
|
33
|
+
return SURFACES.find((s) => s.kind === kind) || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 按渲染器 id 查(未知 → null) */
|
|
37
|
+
export function byId(id) {
|
|
38
|
+
if (!id) return null;
|
|
39
|
+
return SURFACES.find((s) => s.id === id) || null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 已知 kind 列表(document.detectKind 用它判"是不是受支持的内容类型") */
|
|
43
|
+
export function knownKinds() {
|
|
44
|
+
return SURFACES.map((s) => s.kind);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 是否受支持的 kind */
|
|
48
|
+
export function isKnownKind(kind) {
|
|
49
|
+
return byKind(kind) !== null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* kind → data-stage-mode 的值。**未知 kind 返回 null**,调用方必须显式处理。
|
|
54
|
+
* @param {string} kind
|
|
55
|
+
* @returns {string|null}
|
|
56
|
+
*/
|
|
57
|
+
export function stageModeOf(kind) {
|
|
58
|
+
const s = byKind(kind);
|
|
59
|
+
return s ? s.stageMode : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 给"手里可能是 rendererId、也可能是 kind"的旧调用点一个统一入口
|
|
64
|
+
* (两个面的 id 与 kind 恰好同名,历史上被混用过;这里一次收敛,不改既有调用语义)。
|
|
65
|
+
* @param {string} value
|
|
66
|
+
* @returns {object|null} 元数据
|
|
67
|
+
*/
|
|
68
|
+
export function byIdOrKind(value) {
|
|
69
|
+
return byId(value) || byKind(value);
|
|
70
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// roster/filter.js —— 名册搜索过滤(纯函数,不碰 DOM)
|
|
2
|
+
//
|
|
3
|
+
// 搜索口径:
|
|
4
|
+
// · 空关键词 ⇒ 全显示
|
|
5
|
+
// · 角色整组判定:角色名命中,或该角色任意变体命中(变体命中显示整组,避免"只剩一个变体"的割裂感)
|
|
6
|
+
// · 游戏名 / 资源根 id 命中也算,直接显示该游戏全部角色
|
|
7
|
+
|
|
8
|
+
/** 变体参与搜索的文本(模型名 + 目录 + 中文名/原名/服装;统一小写) */
|
|
9
|
+
export function variantSearchText(v) {
|
|
10
|
+
const m = v.model;
|
|
11
|
+
const nm = v.nm;
|
|
12
|
+
return `${m.name} ${m.srcDir || ''} ${nm ? `${nm.name} ${nm.costume}` : ''}`.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 角色是否命中(按角色整组判定:搜角色名时显示该角色全部变体) */
|
|
16
|
+
export function charMatches(c, kw) {
|
|
17
|
+
if (!kw) return true;
|
|
18
|
+
if (String(c.title).toLowerCase().includes(kw)) return true;
|
|
19
|
+
return c.variants.some((v) => variantSearchText(v).includes(kw));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 某游戏在搜索下可见的角色(未加载的游戏不参与过滤) */
|
|
23
|
+
export function visibleChars(game, state) {
|
|
24
|
+
const entry = state.gameChars.get(game.key);
|
|
25
|
+
if (!entry) return [];
|
|
26
|
+
const kw = state.keyword;
|
|
27
|
+
if (!kw) return entry.chars;
|
|
28
|
+
const gameHit = String(game.name).toLowerCase().includes(kw)
|
|
29
|
+
|| game.roots.some((r) => String(r.id).toLowerCase().includes(kw));
|
|
30
|
+
return gameHit ? entry.chars : entry.chars.filter((c) => charMatches(c, kw));
|
|
31
|
+
}
|