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,267 @@
|
|
|
1
|
+
// spine/canvas-api-groups.js —— 各能力组的具体挂载
|
|
2
|
+
//
|
|
3
|
+
// 四个组:设置(fps/dpr/纹理过滤/PMA/调试绘制)→ 资产(皮肤/混合模式/部件筛除)→
|
|
4
|
+
// 视图(相机与取景)→ 渲染(动作/播放/抗锯齿重建)。
|
|
5
|
+
// 红线:组内 setDebug 会被渲染组覆盖,两组必须在同一模块内且按此顺序装配。
|
|
6
|
+
|
|
7
|
+
import { buildDocument } from '../infra/document.js';
|
|
8
|
+
import { createStageCamera } from '../render/camera/spine.js';
|
|
9
|
+
import { applySettings, ensureStageEngine, ensureStageEngineWith, loadIntoStage } from './canvas-engine.js';
|
|
10
|
+
import { pickEngineCtor } from './canvas-runtime.js';
|
|
11
|
+
import { captureViewBase, viewOf } from './canvas-view.js';
|
|
12
|
+
|
|
13
|
+
/** 通用 setter(fit/纹理过滤/速度/循环/调试/帧率/DPR)与 PMA 四档 */
|
|
14
|
+
export function attachSettingSetters(api, st) {
|
|
15
|
+
|
|
16
|
+
// PMA 不走通用 setter:切它必须重传贴图,单列一个方法(见下)
|
|
17
|
+
for (const key of ['fit', 'textureFilter', 'speed', 'loop', 'debug', 'fpsLimit', 'dprMode']) {
|
|
18
|
+
api[`set${key[0].toUpperCase()}${key.slice(1)}`] = (v) => applySettings(
|
|
19
|
+
st, { [key]: key === 'fpsLimit' ? Number(v) : v },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 切换 PMA 模式:true=预乘、false=直通。
|
|
24
|
+
* 为什么单列:引擎没有 setPmaMode,切预乘必须重建贴图纹理(rebuildTextures)。
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* 切换 PMA 模式(auto / premultiplied / straight / upload 四档)。
|
|
28
|
+
* 四档都会重传贴图:auto 需要重新采样像素判断,其余三档要按新口径重上传。
|
|
29
|
+
* @param {string} mode
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* 组装 Document(Assets 层入口,测试/工具用)。
|
|
33
|
+
* 产品路径由 app/model-actions 组装;舞台只消费 ready 的 Document,绝不自己取资源。
|
|
34
|
+
* @param {object} entry catalog 条目(含 kind / rootId / entry.url 等)
|
|
35
|
+
* @returns {Promise<object>} Document(state 可能是 'error',调用方自行判断)
|
|
36
|
+
*/
|
|
37
|
+
api.buildDocument = (entry) => buildDocument(entry || {});
|
|
38
|
+
|
|
39
|
+
api.setPma = async (mode) => {
|
|
40
|
+
const m = ['auto', 'premultiplied', 'straight', 'upload'].includes(mode) ? mode : 'auto';
|
|
41
|
+
const prev = (st.settings && st.settings.pma) || 'auto';
|
|
42
|
+
applySettings(st, { pma: m });
|
|
43
|
+
const eng = st.engine;
|
|
44
|
+
if (!eng || typeof eng.rebuildTextures !== 'function') return m;
|
|
45
|
+
// 必须 await:rebuildTextures 是异步的(要重新解包贴图 + 重建骨架),
|
|
46
|
+
// 不 await 会让"同步 try/catch"抓不到失败,UI 以为切换成功而实际没生效。
|
|
47
|
+
let ok = false;
|
|
48
|
+
try {
|
|
49
|
+
ok = await eng.rebuildTextures(m);
|
|
50
|
+
} catch (e) {
|
|
51
|
+
try { eng.log('error', `PMA 切换异常: ${e && e.message ? e.message : e}`, 'pma'); } catch { /* 忽略 */ }
|
|
52
|
+
ok = false;
|
|
53
|
+
}
|
|
54
|
+
if (ok) return m;
|
|
55
|
+
// 失败如实反映:把档位回滚为上一档,面板/持久化读到的就是"实际生效值"
|
|
56
|
+
applySettings(st, { pma: prev });
|
|
57
|
+
try { eng.log('warn', `PMA 切换失败(${m}),已回滚为 ${prev}`, 'pma'); } catch { /* 忽略 */ }
|
|
58
|
+
return prev;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 调试绘制开关(8 个独立项,可同时开)。
|
|
63
|
+
* 每次只传变化的键,引擎内部会合并;状态同步记到 settings.debug 便于断言。
|
|
64
|
+
* @param {object} patch 形如 { bones: true, meshes: false }
|
|
65
|
+
*/
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 资产相关:皮肤列表与切换、混合模式、部件筛除 */
|
|
69
|
+
export function attachAssetApi(api, st) {
|
|
70
|
+
/** 当前模型的皮肤名列表(无皮肤时返回 [default]) */
|
|
71
|
+
api.getSkins = () => {
|
|
72
|
+
if (!st.engine || typeof st.engine.getSkins !== 'function') return ['default'];
|
|
73
|
+
try {
|
|
74
|
+
const list = st.engine.getSkins() || [];
|
|
75
|
+
return list.length ? list : ['default'];
|
|
76
|
+
} catch {
|
|
77
|
+
return ['default'];
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 当前**实际生效**的皮肤名(引擎侧记账,装载时自动选中的那个也在内)。
|
|
83
|
+
*
|
|
84
|
+
* 为什么需要它:面板刷新时若拿"皮肤列表里有没有 default"去猜当前项,会把
|
|
85
|
+
* "自动选中了具名皮肤"的资源显示成 default —— 设置里写 A、画面是 B,排查时会被这条假信息带偏。
|
|
86
|
+
* 返回 null 表示引擎还没设过皮肤(调用方自行决定回落成什么)。
|
|
87
|
+
* @returns {string|null}
|
|
88
|
+
*/
|
|
89
|
+
api.getActiveSkin = () => {
|
|
90
|
+
if (!st.engine) return null;
|
|
91
|
+
const list = st.engine.activeSkins;
|
|
92
|
+
return Array.isArray(list) && list.length ? list[0] : null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 切换皮肤(引擎按名称匹配;找不到时保持原皮肤)。
|
|
97
|
+
* 成功与否必须取引擎返回值:否则面板会记下一个并不存在的皮肤名,
|
|
98
|
+
* 表现为"设置里显示 skin_X、画面还是 default",且下次重建贴图时又按错名字去设一遍。
|
|
99
|
+
* @param {string} name
|
|
100
|
+
* @returns {boolean} 是否真的切过去了
|
|
101
|
+
*/
|
|
102
|
+
api.setSkin = (name) => {
|
|
103
|
+
if (!st.engine || typeof st.engine.setSkins !== 'function') return false;
|
|
104
|
+
try {
|
|
105
|
+
const ok = st.engine.setSkins([name]) !== false;
|
|
106
|
+
if (ok) st.settings.skin = name;
|
|
107
|
+
return ok;
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 动作清单(名称 + 时长):时长来自 skeletonData.animations 的 duration。
|
|
115
|
+
* 面板与底栏都读这一份,避免"两处各取一处"导致条数/时长不一致。
|
|
116
|
+
* @returns {Array<{name: string, duration: number|null}>}
|
|
117
|
+
*/
|
|
118
|
+
api.animationDetails = () => {
|
|
119
|
+
try {
|
|
120
|
+
const e = st.engine; // 与同一 api 里其它方法一致:引擎挂在 st 上
|
|
121
|
+
const anims = (e && e.skeletonData && e.skeletonData.animations) || [];
|
|
122
|
+
return anims.map((a) => {
|
|
123
|
+
const d = Number(a && a.duration);
|
|
124
|
+
return { name: String(a && a.name || ''), duration: Number.isFinite(d) && d > 0 ? d : null };
|
|
125
|
+
}).filter((a) => a.name);
|
|
126
|
+
} catch {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 动画切换的交叉淡化时长(秒):混合模式开 = 用它,关 = 0(硬切)。
|
|
133
|
+
* 提成命名常量:这个值同时被「混合模式开关」与「换肤临时套用」用到,
|
|
134
|
+
* 散落的裸 0.2 一旦改一处忘另一处,动作过渡与换肤过渡就会不一致。
|
|
135
|
+
*/
|
|
136
|
+
const MIX_TIME_CROSSFADE_SEC = 0.2;
|
|
137
|
+
|
|
138
|
+
api.setMixMode = (on) => api.setMixTime(on ? MIX_TIME_CROSSFADE_SEC : 0);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 混合时长(秒):0 表示**不混合**(硬切),>0 表示动作/皮肤切换时的交叉淡化秒数。
|
|
142
|
+
* 面板的「混合时长」滑块走这里;原来的「混合模式」开关等价于 setMixMode(0|默认值)。
|
|
143
|
+
* @param {number} t 0~1 秒
|
|
144
|
+
* @returns {number} 生效值
|
|
145
|
+
*/
|
|
146
|
+
api.setMixTime = (t) => {
|
|
147
|
+
// 范围夹到 0~1:与面板滑块一致,避免外部传入巨大值把过渡拖成几秒
|
|
148
|
+
const v = Math.max(0, Math.min(1, Number(t) || 0));
|
|
149
|
+
st.settings.mixTime = v;
|
|
150
|
+
if (st.engine && typeof st.engine.setMixTime === 'function') {
|
|
151
|
+
try { st.engine.setMixTime(v); } catch { /* 版本不支持则只记状态 */ }
|
|
152
|
+
}
|
|
153
|
+
return v;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 相机视图入口:只读读数 + 薄转发(不换算) */
|
|
160
|
+
export function attachViewApi(api, st) {
|
|
161
|
+
/** 相机统一入口(全部委托引擎原生方法,本文件不做任何坐标换算) */
|
|
162
|
+
st.camera = createStageCamera({ getEngine: () => st.engine });
|
|
163
|
+
api.camera = st.camera;
|
|
164
|
+
|
|
165
|
+
/** 只读视图读数:zoom 取引擎 userScale 与基准的比值,pan 用入口累计的屏幕位移 */
|
|
166
|
+
api.getView = () => viewOf(st);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 兼容入口(交互层仍调用 setView)。这里**只转发**,不换算:
|
|
170
|
+
* · zoom → camera.zoomTo(基准 × 倍数)(倍乘即「相对 fit 的倍数」,clamp 由引擎负责)
|
|
171
|
+
* · panX/panY → camera.panBy(增量),单位是屏幕像素,
|
|
172
|
+
*/
|
|
173
|
+
api.setView = (patch = {}) => {
|
|
174
|
+
const camera = st.camera;
|
|
175
|
+
if (!camera) return viewOf(st);
|
|
176
|
+
if (typeof patch.zoom === 'number' && Number.isFinite(patch.zoom)) {
|
|
177
|
+
const base = st.viewBaseScale || 1;
|
|
178
|
+
camera.zoomTo(base * patch.zoom);
|
|
179
|
+
}
|
|
180
|
+
if (typeof patch.panX === 'number' || typeof patch.panY === 'number') {
|
|
181
|
+
const cur = st.viewPan || { x: 0, y: 0 };
|
|
182
|
+
const nx = typeof patch.panX === 'number' && Number.isFinite(patch.panX) ? patch.panX : cur.x;
|
|
183
|
+
const ny = typeof patch.panY === 'number' && Number.isFinite(patch.panY) ? patch.panY : cur.y;
|
|
184
|
+
if (nx !== cur.x || ny !== cur.y) {
|
|
185
|
+
camera.panBy(nx - cur.x, ny - cur.y);
|
|
186
|
+
st.viewPan = { x: nx, y: ny };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return viewOf(st);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** 视图复位:交给引擎 resetCamera 重新 fit,然后重取基准 */
|
|
193
|
+
/**
|
|
194
|
+
* 幂等入口:确保引擎实例已创建(只建上下文与相机,不加载模型)。
|
|
195
|
+
* 谁需要:Live2D 分支要接管引擎的 Live2D 态(l2dActive 依赖 l2dModelId + 引擎实例),
|
|
196
|
+
* 而引擎是懒创建的 —— 用户先点 Live2D 时还没有实例。
|
|
197
|
+
* @returns {Promise<object|null>} 引擎实例
|
|
198
|
+
*/
|
|
199
|
+
api.ensureEngineReady = async () => {
|
|
200
|
+
if (st.engine) return st.engine;
|
|
201
|
+
try {
|
|
202
|
+
const Engine = await pickEngineCtor();
|
|
203
|
+
if (!ensureStageEngine(st, Engine, st.canvas)) return null;
|
|
204
|
+
return st.engine;
|
|
205
|
+
} catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
api.resetView = () => {
|
|
211
|
+
if (st.camera) st.camera.reset();
|
|
212
|
+
captureViewBase(st);
|
|
213
|
+
return viewOf(st);
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 渲染相关:调试绘制、滤镜预设、MSAA 重建 */
|
|
218
|
+
export function attachRenderApi(api, st) {
|
|
219
|
+
api.setDebug = (patch) => {
|
|
220
|
+
const next = { ...(st.settings.debug || {}), ...(patch || {}) };
|
|
221
|
+
st.settings.debug = next;
|
|
222
|
+
if (st.engine && typeof st.engine.setDebugDraw === 'function') {
|
|
223
|
+
try { st.engine.setDebugDraw(patch || {}); } catch { /* 版本不支持则只记状态 */ }
|
|
224
|
+
}
|
|
225
|
+
return next;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
/** 切换滤镜预设(id 见引擎预设表;未知 id 由引擎回落原画) */
|
|
229
|
+
api.setFilter = (id) => {
|
|
230
|
+
if (!st.engine || typeof st.engine.setFilter !== 'function') {
|
|
231
|
+
// 与 applySettings 同一约定:引擎还没建时也要把设置记进快照,
|
|
232
|
+
// 装载时由引擎按 settings.filter 应用 —— 否则启动阶段下发会静默丢失
|
|
233
|
+
st.settings.filter = id;
|
|
234
|
+
return id;
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
st.engine.setFilter(id);
|
|
238
|
+
st.settings.filter = st.engine.getFilter ? st.engine.getFilter() : id;
|
|
239
|
+
return st.settings.filter;
|
|
240
|
+
} catch { return null; }
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 切换 MSAA:GL 的 antialias 只在建上下文时生效,必须换 canvas 节点重建。
|
|
245
|
+
* 流程:换节点 → dispose → init(antialias) → 重新加载最近一次的模型。
|
|
246
|
+
* @param {boolean} on
|
|
247
|
+
* @returns {Promise<{antialias:boolean, restoreMs:number, restored:boolean}>}
|
|
248
|
+
*/
|
|
249
|
+
api.rebuildWithAntialias = async (on) => {
|
|
250
|
+
const arg = st.lastArg;
|
|
251
|
+
const old = st.canvas;
|
|
252
|
+
const next = old.cloneNode(false);
|
|
253
|
+
old.replaceWith(next);
|
|
254
|
+
st.canvas = next;
|
|
255
|
+
try { if (st.engine && st.engine.dispose) st.engine.dispose(); } catch { /* 释放失败继续重建 */ }
|
|
256
|
+
st.engine = null;
|
|
257
|
+
st.model = null;
|
|
258
|
+
st.disposes += 1; // 重建也计入释放次数,便于用 loads/disposes 判泄漏
|
|
259
|
+
st.settings.msaa = on ? 'auto' : 'off';
|
|
260
|
+
const t0 = performance.now();
|
|
261
|
+
const Engine = await pickEngineCtor();
|
|
262
|
+
const ok = ensureStageEngineWith(st, Engine, on);
|
|
263
|
+
if (!ok) return { antialias: !!on, restoreMs: 0, restored: false };
|
|
264
|
+
if (arg) await loadIntoStage(st, arg);
|
|
265
|
+
return { antialias: !!on, restoreMs: Math.round(performance.now() - t0), restored: !!arg };
|
|
266
|
+
};
|
|
267
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// spine/canvas-api.js —— 对外 31 键契约的装配入口
|
|
2
|
+
//
|
|
3
|
+
// 职责:buildStageApi 组装核心键(load/unload/stats/samplePixels/...),
|
|
4
|
+
// attachExtraApi 按固定顺序挂上四组能力(设置 → 资产 → 视图 → 渲染)。
|
|
5
|
+
// 红线:挂载顺序即语义 —— 设置组与渲染组各定义一次 setDebug,后挂的覆盖前者;
|
|
6
|
+
// 调换顺序会让调试绘制静默变样(渲染器契约 §2.1)。
|
|
7
|
+
|
|
8
|
+
import { attachAssetApi, attachRenderApi, attachSettingSetters, attachViewApi } from './canvas-api-groups.js';
|
|
9
|
+
import { abortedError } from './canvas-document.js';
|
|
10
|
+
import { applySettings, loadIntoStage, unloadModel , startFrameTick, stopFrameTick, repaintOnce } from './canvas-engine.js';
|
|
11
|
+
import { samplePixels } from './canvas-pixels.js';
|
|
12
|
+
import { statsOf } from './canvas-view.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 创建舞台(对外契约见 docs/参考/渲染与纹理.md §14)。
|
|
16
|
+
* @param {{canvas: HTMLCanvasElement, onStatus?: (s:object)=>void}} opts
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* 组装舞台对外 API 的"基础方法"(加载/释放/统计/采样/播放控制)。
|
|
20
|
+
* 拆出来是为了让 createSpineStage 只做编排(单函数过长会被审计拦截)。
|
|
21
|
+
* @param {object} st 舞台状态
|
|
22
|
+
* @param {{status: Function, fail: Function}} ctx 状态回调与失败包装
|
|
23
|
+
*/
|
|
24
|
+
export function buildStageApi(st, ctx) {
|
|
25
|
+
const { status, fail } = ctx;
|
|
26
|
+
const api = {
|
|
27
|
+
async load(arg) {
|
|
28
|
+
try {
|
|
29
|
+
const info = await loadIntoStage(st, arg);
|
|
30
|
+
// 被取代时 loadIntoStage 返回 null:必须显式抛 aborted,不能"静默成功"让调用方读到空结果
|
|
31
|
+
if (!info) throw abortedError();
|
|
32
|
+
status({ phase: 'ready', ...info });
|
|
33
|
+
return info;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err && err.business) {
|
|
36
|
+
// 业务错误也要落到状态里(页面提示与遥测都靠 lastError)
|
|
37
|
+
st.lastError = String(err.message || err);
|
|
38
|
+
status({ phase: 'error', error: st.lastError });
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
if (err && err.name === 'AbortError') { st.lastError = '加载已被新的选择取代'; throw abortedError(st.lastError, err); }
|
|
42
|
+
throw fail(`加载失败:${err && err.message ? err.message : err}`, err);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
unload() { unloadModel(st); status({ phase: 'unload' }); },
|
|
46
|
+
stats: () => statsOf(st),
|
|
47
|
+
samplePixels: () => samplePixels(st),
|
|
48
|
+
applySettings: (patch) => applySettings(st, patch),
|
|
49
|
+
play() {
|
|
50
|
+
if (st.engine && st.engine.start) { try { st.engine.start(); } catch { /* 已在播放 */ } }
|
|
51
|
+
st.paused = false; // 恢复:重启帧计数循环(单实例由 startFrameTick 保证)
|
|
52
|
+
startFrameTick(st);
|
|
53
|
+
},
|
|
54
|
+
pause() {
|
|
55
|
+
if (st.engine && st.engine.stop) { try { st.engine.stop(); } catch { /* 已暂停 */ } }
|
|
56
|
+
st.paused = true; // 暂停:停掉帧计数循环,避免空转重绘同一张静止画面
|
|
57
|
+
stopFrameTick(st);
|
|
58
|
+
repaintOnce(st); // 停循环前补一帧,保证"按下瞬间"的画面是最新的
|
|
59
|
+
},
|
|
60
|
+
repaint: () => repaintOnce(st),
|
|
61
|
+
seek(t) { if (st.engine && st.engine.seek) { try { st.engine.seek(t); } catch { /* 越界忽略 */ } } },
|
|
62
|
+
setAnimation(name, loop) {
|
|
63
|
+
if (!st.engine || !name) return false;
|
|
64
|
+
try { st.engine.setAnimation(name, typeof loop === 'boolean' ? loop : st.settings.loop); return true; } catch { return false; }
|
|
65
|
+
},
|
|
66
|
+
resize() {
|
|
67
|
+
if (!st.engine) return;
|
|
68
|
+
try { if (st.engine.refreshResolution) st.engine.refreshResolution(); } catch { /* 忽略 */ }
|
|
69
|
+
try { if (st.engine.resetCamera) st.engine.resetCamera(); } catch { /* 忽略 */ }
|
|
70
|
+
},
|
|
71
|
+
dispose() {
|
|
72
|
+
const e = st.engine;
|
|
73
|
+
st.engine = null; st.model = null; st.seq++;
|
|
74
|
+
if (!e) return;
|
|
75
|
+
try { if (e.dispose) e.dispose(); } catch { /* 忽略 */ }
|
|
76
|
+
st.disposes++;
|
|
77
|
+
},
|
|
78
|
+
// debug 同时给出统计字段(验收脚本从 debug() 读 frames 等)
|
|
79
|
+
debug: () => ({ ...statsOf(st), engine: st.engine, tickCalls: st.tickCalls || 0, paused: !!st.paused }),
|
|
80
|
+
};
|
|
81
|
+
return api;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 给舞台 API 挂"可选能力":PMA 四档、滤镜、皮肤、混合模式、调试绘制、MSAA 重建。
|
|
86
|
+
* 这些方法依赖模块级工具(applySettings / loadIntoStage…),与工厂解耦。
|
|
87
|
+
*/
|
|
88
|
+
/**
|
|
89
|
+
* 给舞台 API 挂"可选能力"。按职责拆成四段,便于维护与审计(单函数长度)。
|
|
90
|
+
* 依赖模块级工具(applySettings / loadIntoStage…),与工厂解耦。
|
|
91
|
+
*/
|
|
92
|
+
export function attachExtraApi(api, st) {
|
|
93
|
+
attachSettingSetters(api, st);
|
|
94
|
+
attachAssetApi(api, st);
|
|
95
|
+
attachViewApi(api, st);
|
|
96
|
+
attachRenderApi(api, st);
|
|
97
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// spine/canvas-document.js —— Document → 舞台描述符
|
|
2
|
+
//
|
|
3
|
+
// 职责:把(已就绪的)Document 翻成引擎能直接吃的描述符,并区分两类失败:
|
|
4
|
+
// aborted(用户切换导致的中止,属正常)与 business(真实失败,要显示给用户)。
|
|
5
|
+
// 契约:Document 是渲染层唯一输入,这里不做资源抓取,只读已经解析好的地址。
|
|
6
|
+
|
|
7
|
+
import { buildDocument } from '../infra/document.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 被取代的装载错误:业务错误 + aborted 标记。
|
|
11
|
+
* 为什么单列:调用方必须能区分"装载失败"与"被更新的选择取代"——后者是正常竞态结果,
|
|
12
|
+
* 不该显示成红色故障,也不该被当成"成功"继续读结果字段(曾经因此抛 TypeError 留下空白画面)。
|
|
13
|
+
*/
|
|
14
|
+
export function abortedError(msg = '加载已被新的选择取代', cause) {
|
|
15
|
+
const e = businessError(msg, cause);
|
|
16
|
+
e.aborted = true;
|
|
17
|
+
return e;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 业务错误:调用方只更新提示,不抛控制台 */
|
|
21
|
+
export function businessError(msg, cause) {
|
|
22
|
+
const e = new Error(msg);
|
|
23
|
+
e.business = true;
|
|
24
|
+
if (cause) e.cause = cause;
|
|
25
|
+
return e;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 说明:模型资源(骨架 / atlas / 贴图)的**取用已上移到 infra/document.js**(Assets 层),
|
|
29
|
+
// 本文件只消费 Document 给出的地址与字节 —— 渲染器里不再有 fetch / XHR 取模型资源,
|
|
30
|
+
// 因此也不会有"取资源失败"分支(契约 渲染器契约 §五-6、Document 契约 D4)。
|
|
31
|
+
// 运行时库(/spine-runtimes/*)仍在这里加载:那是**库**不是模型资产,属已批准的豁免项。
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 取出 ready 的 Document(舞台入参的唯一形态)。
|
|
35
|
+
* 只认 { document }:非 Document 直接抛,ready 之外(error)也抛并留痕。
|
|
36
|
+
* 组装是调用方的事(api.buildDocument 或 app/model-actions),舞台不代劳。
|
|
37
|
+
* @param {object} st 舞台状态
|
|
38
|
+
* @param {object} arg 只接受 { document }
|
|
39
|
+
* @returns {object} ready 的 Document
|
|
40
|
+
*/
|
|
41
|
+
export function resolveDocument(st, arg) {
|
|
42
|
+
// **只接受 Document**,不做旧形状(catalog entry)的兜底组装:
|
|
43
|
+
// 兜底留着,调用方忘了组装就会静默走它,而"组装失败"这条路径永远没人覆盖。
|
|
44
|
+
// 组装入口是 api.buildDocument(Assets 层 infra/document.js),调用方显式两步走。
|
|
45
|
+
const doc = arg && arg.document;
|
|
46
|
+
if (!doc || typeof doc.state !== 'string') {
|
|
47
|
+
// 旧形状({ root, model })要点名,否则调用方只看到 undefined 不知错在哪
|
|
48
|
+
const got = arg && arg.model
|
|
49
|
+
? `旧形状 { root, model }(kind=${(arg.model.kind) || '?'})`
|
|
50
|
+
: (doc && typeof doc === 'object' ? `普通对象(kind=${doc.kind || '?'})` : String(doc));
|
|
51
|
+
throw businessError(`stage.load 只接受 Document(请先 buildDocument 组装),收到:${got}`);
|
|
52
|
+
}
|
|
53
|
+
if (doc.state !== 'ready') {
|
|
54
|
+
const why = (doc.warnings || []).join(';') || '未提供原因';
|
|
55
|
+
st.lastError = why; // 组装失败要留痕:下一次成功装载会清掉它
|
|
56
|
+
throw businessError(`Document 不是 ready 状态(${doc.state}):${why}`);
|
|
57
|
+
}
|
|
58
|
+
return doc;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* engine 的模型描述符(字段名是 engine 的契约,见文档 §14)。
|
|
63
|
+
* 数据全部来自 **Document.assets**(地址已由 Assets 层经 VirtualFS 解析、字节也由那一层取好)。
|
|
64
|
+
* @param {object} doc ready 的 Document
|
|
65
|
+
* @param {string} ver 运行时版本
|
|
66
|
+
*/
|
|
67
|
+
export function buildDescriptor(doc, ver) {
|
|
68
|
+
const assets = (doc && doc.assets) || {};
|
|
69
|
+
const entry = assets.entry || {};
|
|
70
|
+
const atlas = assets.atlas || {};
|
|
71
|
+
const textures = Array.isArray(assets.textures) ? assets.textures : [];
|
|
72
|
+
return {
|
|
73
|
+
name: (doc && (doc.name || doc.id)) || '',
|
|
74
|
+
version: ver,
|
|
75
|
+
skeletonFile: { name: entry.name || '', content: entry.bytes },
|
|
76
|
+
atlasFile: { name: atlas.name || '', content: atlas.bytes },
|
|
77
|
+
textureFiles: textures.map((t) => ({ name: t.name || '', content: t.bytes })),
|
|
78
|
+
atlasPma: doc && doc.meta && doc.meta.extras ? doc.meta.extras.atlasPma : null,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// spine/canvas-engine.js —— 引擎生命周期与装载流程
|
|
2
|
+
//
|
|
3
|
+
// 职责:建引擎(含抗锯齿重建)、装载骨架、启动/停止播放循环、卸载模型。
|
|
4
|
+
// 为什么单独成模块:这里是唯一持有/销毁 WebGL 上下文的地方(契约 §五-7 的落点),
|
|
5
|
+
// 卸载语义(unload 保留引擎 / dispose 销毁引擎)必须能在一屏内读完。
|
|
6
|
+
|
|
7
|
+
import { installRuntimeManager } from './runtimes.js';
|
|
8
|
+
import { abortedError, buildDescriptor, businessError, resolveDocument } from './canvas-document.js';
|
|
9
|
+
import { availableVersions, ensureRuntimeSource, pickEngineCtor, pickRuntimeVersion, takeForcedRuntime } from './canvas-runtime.js';
|
|
10
|
+
// 缺运行时时的可读提示(含服务端给的落地目录绝对路径与可复制命令)
|
|
11
|
+
import { loadRuntimeHint, spineMissingHint } from './runtime-hint.js';
|
|
12
|
+
import { captureViewBase } from './canvas-view.js';
|
|
13
|
+
|
|
14
|
+
/** 应用设置(全部走 engine 的 setter,缺哪个就跳过哪个) */
|
|
15
|
+
export function applySettings(st, patch) {
|
|
16
|
+
if (patch && typeof patch === 'object') Object.assign(st.settings, patch);
|
|
17
|
+
const e = st.engine;
|
|
18
|
+
if (!e) return;
|
|
19
|
+
const s = st.settings;
|
|
20
|
+
const call = (fn, ...args) => { try { if (typeof fn === 'function') fn.apply(e, args); } catch { /* 版本不支持则跳过 */ } };
|
|
21
|
+
if (s.fit) call(e.setFitMode, s.fit);
|
|
22
|
+
if (s.textureFilter) call(e.setTextureFilter, s.textureFilter);
|
|
23
|
+
if (typeof s.fpsLimit === 'number') call(e.setFpsLimit, s.fpsLimit);
|
|
24
|
+
if (s.dprMode !== undefined && s.dprMode !== null) call(e.setDprMode, s.dprMode);
|
|
25
|
+
if (typeof s.speed === 'number') call(e.setSpeed, s.speed);
|
|
26
|
+
if (typeof s.loop === 'boolean') call(e.setLoop, s.loop);
|
|
27
|
+
if (s.debug) call(e.setDebugDraw, s.debug);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 释放当前模型(保留 engine 实例) */
|
|
31
|
+
export function unloadModel(st) {
|
|
32
|
+
const e = st.engine;
|
|
33
|
+
st.model = null;
|
|
34
|
+
st.seq++; // 让在途加载失效
|
|
35
|
+
if (!e) return;
|
|
36
|
+
try { e.stop(); } catch { /* 已停止 */ }
|
|
37
|
+
try { if (e.clearSkeleton) e.clearSkeleton(); } catch { /* 清理失败不阻塞 */ }
|
|
38
|
+
st.disposes++;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 首次进入时创建 engine;canvas 尺寸变化由 engine 的 refreshResolution 处理 */
|
|
42
|
+
export function ensureStageEngine(st, Engine, canvas) {
|
|
43
|
+
if (st.engine) return true;
|
|
44
|
+
// 不写死 antialias:交给 ensureStageEngineWith 按 settings.msaa 决定(默认自动=开)
|
|
45
|
+
return ensureStageEngineWith(st, Engine, undefined, canvas);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 建引擎;antialias 只在此时能生效(换画布重建时传 true) */
|
|
49
|
+
export function ensureStageEngineWith(st, Engine, antialias, canvasOverride) {
|
|
50
|
+
const canvas = canvasOverride || st.canvas;
|
|
51
|
+
// 未显式指定时按设置走:'auto' = 开抗锯齿(与面板默认一致)
|
|
52
|
+
const wantAA = typeof antialias === 'boolean' ? antialias : st.settings.msaa === 'auto';
|
|
53
|
+
const engine = new Engine({
|
|
54
|
+
log: (level, msg, scope) => { if (level === 'error') st.lastError = `${scope || 'engine'}: ${msg}`; },
|
|
55
|
+
onLoadComplete: (info) => { st.model = info; },
|
|
56
|
+
onError: (msg) => { st.lastError = String(msg); },
|
|
57
|
+
});
|
|
58
|
+
if (!engine.init(canvas, { antialias: wantAA, allowParseOnly: false })) return false;
|
|
59
|
+
st.engine = engine;
|
|
60
|
+
applySettings(st);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 帧计数循环(验收口径:stats().frames 必须真实反映"画面在推进")。
|
|
66
|
+
*
|
|
67
|
+
* 为什么可暂停:暂停后仍每帧跑一遍这条 rAF 纯烧 CPU(部署机无 GPU、纯软件 WebGL),
|
|
68
|
+
* 而它只是计数循环,与引擎是否绘制无关。暂停时**停掉循环本身**,恢复时重启。
|
|
69
|
+
* 单实例保证:用 st.frameRaf 记录句柄,已在跑就直接返回,避免重复排帧。
|
|
70
|
+
* @param {object} st 舞台内部状态
|
|
71
|
+
*/
|
|
72
|
+
export function startFrameTick(st) {
|
|
73
|
+
if (st.frameRaf) return; // 已在跑:绝不重复启动
|
|
74
|
+
const tick = () => {
|
|
75
|
+
st.frameRaf = 0;
|
|
76
|
+
if (!st.model || st.model !== st.frameInfo) return; // 模型已换/卸载:自然结束
|
|
77
|
+
st.tickCalls = (st.tickCalls || 0) + 1; // 打点:验收直接看"循环被调了几次"
|
|
78
|
+
if (st.paused) return; // 暂停:不再排帧(resume 时重启)
|
|
79
|
+
st.frames++;
|
|
80
|
+
st.frameRaf = requestAnimationFrame(tick);
|
|
81
|
+
};
|
|
82
|
+
st.frameRaf = requestAnimationFrame(tick);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 单帧重绘(暂停中按需渲染的唯一入口)。
|
|
87
|
+
* 引擎在 stop() 之后不会自己画,而交互/改设置/切动作都改了状态 ⇒ 必须显式补一帧,
|
|
88
|
+
* 否则"暂停"会变成"点了没反应"。
|
|
89
|
+
* @param {object} st 舞台内部状态
|
|
90
|
+
* @returns {boolean} 是否成功请求到一次绘制
|
|
91
|
+
*/
|
|
92
|
+
export function repaintOnce(st) {
|
|
93
|
+
const e = st && st.engine;
|
|
94
|
+
if (!e) return false;
|
|
95
|
+
try {
|
|
96
|
+
// 按需渲染也算一帧:frames 的语义是"真的画了一次",暂停期间的交互/改设置都体现在这里
|
|
97
|
+
if (typeof e.requestRender === 'function') { e.requestRender(); st.frames += 1; return true; }
|
|
98
|
+
if (typeof e.presentFirstFrame === 'function') { e.presentFirstFrame(); st.frames += 1; return true; }
|
|
99
|
+
} catch { /* 引擎不支持就放弃 */ }
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 停掉帧计数循环(暂停/卸载时调用) */
|
|
104
|
+
export function stopFrameTick(st) {
|
|
105
|
+
if (st.frameRaf) { try { cancelAnimationFrame(st.frameRaf); } catch { /* 忽略 */ } }
|
|
106
|
+
st.frameRaf = 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 播第一个动作 + 启动渲染循环 + 帧计数(验收用) */
|
|
110
|
+
function startPlayback(st, info, loop) {
|
|
111
|
+
const e = st.engine;
|
|
112
|
+
const anims = e.getAnimations ? e.getAnimations() : [];
|
|
113
|
+
const motion = anims.length ? anims[0] : null;
|
|
114
|
+
if (motion) { try { e.setAnimation(motion, loop); } catch { /* 播不了就停首帧 */ } }
|
|
115
|
+
try { e.start(); } catch { /* 循环已在跑 */ }
|
|
116
|
+
try { if (e.presentFirstFrame) e.presentFirstFrame(); } catch { /* 留给下一帧 */ }
|
|
117
|
+
st.frames = 0;
|
|
118
|
+
st.frameInfo = info;
|
|
119
|
+
startFrameTick(st);
|
|
120
|
+
return { animations: anims, motion };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 加载主流程(编排:校验 Document → 选版本 → 注入源码 → 建 engine → 加载 → 起播) */
|
|
124
|
+
export async function loadIntoStage(st, arg) {
|
|
125
|
+
// 入参只有一种形状:{ document }(ready)。资源地址与字节都在组装阶段备好,本层不取。
|
|
126
|
+
const doc = resolveDocument(st, arg);
|
|
127
|
+
const model = doc;
|
|
128
|
+
const seq = ++st.seq;
|
|
129
|
+
// 每次装载开始都清掉上一次的错误:lastError 语义是"最近一次装载的结果状态",
|
|
130
|
+
// 成功后若不清,后面每次成功采样都会带着旧的失败原因(实测会污染对照表的每一格)
|
|
131
|
+
st.lastError = null;
|
|
132
|
+
// 切模型:中止上一个在途加载的请求(被中止属预期,不产生控制台报错)
|
|
133
|
+
if (st.abort) st.abort.abort();
|
|
134
|
+
const ctl = new AbortController();
|
|
135
|
+
st.abort = ctl;
|
|
136
|
+
// 上一个模型由 engine.loadModel 内部 stop+disposeTexturesAndAtlas 负责释放,这里只记账
|
|
137
|
+
if (st.model) { st.model = null; st.disposes++; }
|
|
138
|
+
// 先装运行时管理器:引擎取某个版本的运行时源码时要经它(多版本共存的单一入口)
|
|
139
|
+
installRuntimeManager();
|
|
140
|
+
const Engine = await pickEngineCtor();
|
|
141
|
+
const avail = await availableVersions();
|
|
142
|
+
// 版本来自 Document 的 meta(Document 形状里没有顶层 version):
|
|
143
|
+
// 读错会挑到不匹配的运行时 —— 表现为骨架解析报 "Bone name must not be null" 这类怪错
|
|
144
|
+
const skeletonVer = (doc.meta && doc.meta.version) || doc.version;
|
|
145
|
+
// 一次性覆盖(URL 的 ?runtime=,由 app/url-params.js 设置):取走即清空,天然只生效一次。
|
|
146
|
+
// 这是**唯一**的"手动指定运行时"入口,最终都落到下面这段选版逻辑上 —— 不另写优选规则。
|
|
147
|
+
const forced = takeForcedRuntime();
|
|
148
|
+
let ver;
|
|
149
|
+
if (forced) {
|
|
150
|
+
if (!avail.includes(forced)) {
|
|
151
|
+
// 这里才是"全新机器上缺运行时"最常撞到的地方:清单里根本没有这个版本,
|
|
152
|
+
// 请求压根不会发出去(也就走不到 sourceOf 的 404 分支)⇒ hint 必须在这里拼。
|
|
153
|
+
const hint = await loadRuntimeHint();
|
|
154
|
+
throw businessError(`指定的运行时 ${forced} 不可用(可用:${avail.join(' / ') || '运行时清单不可用'})\n`
|
|
155
|
+
+ spineMissingHint(forced, hint, null));
|
|
156
|
+
}
|
|
157
|
+
ver = forced; // 给了就用,**绝不**回落到其它版本 —— 自动降级会让排查得出错误结论
|
|
158
|
+
} else {
|
|
159
|
+
ver = pickRuntimeVersion(skeletonVer, avail); // 现状行为,一行没改
|
|
160
|
+
}
|
|
161
|
+
if (!ver) {
|
|
162
|
+
// 同理:清单全空(一个运行时都没装)时也不会发请求 ⇒ 在这里给补齐方案
|
|
163
|
+
const hint = await loadRuntimeHint();
|
|
164
|
+
throw businessError('无法确定该模型的 Spine 版本:元数据缺少 version,且运行时清单不可用\n'
|
|
165
|
+
+ spineMissingHint(skeletonVer || '4.x', hint, null));
|
|
166
|
+
}
|
|
167
|
+
await ensureRuntimeSource(ver);
|
|
168
|
+
if (seq !== st.seq) throw abortedError();
|
|
169
|
+
if (seq !== st.seq) throw abortedError();
|
|
170
|
+
if (!ensureStageEngine(st, Engine, st.canvas)) throw businessError('当前环境没有可用的 WebGL,无法渲染 Spine');
|
|
171
|
+
if (!st.runtimeKeys.includes(ver)) st.runtimeKeys.push(ver);
|
|
172
|
+
st.loads++;
|
|
173
|
+
let info;
|
|
174
|
+
try {
|
|
175
|
+
info = await st.engine.loadModel(buildDescriptor(doc, ver));
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// 手动指定时补一行"骨架版本 vs 所选版本"的对照 —— 这是手动覆盖与自动保证唯一实质分歧点:
|
|
178
|
+
// 自动在同 major 内会退到最大可用 minor,手动**不退**。不写清用户会以为两者选的是同一个东西。
|
|
179
|
+
if (forced) {
|
|
180
|
+
const autoVer = pickRuntimeVersion(skeletonVer, avail);
|
|
181
|
+
throw businessError(`模型加载失败:骨架版本 ${skeletonVer || '未知'},当前指定的是运行时 ${ver}`
|
|
182
|
+
+ `(去掉 ?runtime= 让自动选择,当前会自动用 ${autoVer || '未知'};可用:${avail.join(' / ') || '清单不可用'})`
|
|
183
|
+
+ `。原始错误:${(e && e.message) || e}`);
|
|
184
|
+
}
|
|
185
|
+
throw e;
|
|
186
|
+
}
|
|
187
|
+
if (seq !== st.seq) throw abortedError();
|
|
188
|
+
if (!info) throw businessError('模型加载失败(引擎未返回结果)');
|
|
189
|
+
st.lastArg = arg; // 供 MSAA 重建上下文后恢复同一模型
|
|
190
|
+
captureViewBase(st); // 新模型 fit 完成:把当前取景记为视图基准
|
|
191
|
+
const playing = startPlayback(st, info, st.settings.loop);
|
|
192
|
+
return { version: ver, animations: playing.animations, motion: playing.motion };
|
|
193
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// spine/canvas-entry.js —— createSpineStage 入口
|
|
2
|
+
//
|
|
3
|
+
// 构造期只接受 opts.canvas / opts.onStatus;其余设置一律经 applySettings 或各 setter 下发
|
|
4
|
+
// (构造期塞设置会让"面板设置"与"构造参数"出现两套真相)。
|
|
5
|
+
|
|
6
|
+
import { attachExtraApi, buildStageApi } from './canvas-api.js';
|
|
7
|
+
import { businessError } from './canvas-document.js';
|
|
8
|
+
import { DEFAULT_SETTINGS, statsOf } from './canvas-view.js';
|
|
9
|
+
|
|
10
|
+
export function createSpineStage(opts) {
|
|
11
|
+
const o = opts || {};
|
|
12
|
+
if (!o.canvas) throw new Error('createSpineStage 需要 canvas');
|
|
13
|
+
const onStatus = typeof o.onStatus === 'function' ? o.onStatus : () => {};
|
|
14
|
+
const st = {
|
|
15
|
+
canvas: o.canvas, lastArg: null,
|
|
16
|
+
engine: null, seq: 0, loads: 0, disposes: 0, lastError: null,
|
|
17
|
+
runtimeKeys: [], model: null, frames: 0, settings: { ...DEFAULT_SETTINGS },
|
|
18
|
+
};
|
|
19
|
+
const status = (patch) => { try { onStatus({ ...statsOf(st), ...patch }); } catch { /* 回调不影响渲染 */ } };
|
|
20
|
+
const fail = (msg, cause) => {
|
|
21
|
+
st.lastError = String(msg);
|
|
22
|
+
status({ phase: 'error', error: st.lastError });
|
|
23
|
+
return businessError(st.lastError, cause);
|
|
24
|
+
};
|
|
25
|
+
const api = buildStageApi(st, { status, fail });
|
|
26
|
+
attachExtraApi(api, st);
|
|
27
|
+
return api;
|
|
28
|
+
}
|