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,400 @@
|
|
|
1
|
+
// live2d/bridge.js —— 相机 backend 与播放控制的落地接口
|
|
2
|
+
//
|
|
3
|
+
// 这是 Live2D 渲染器对外的"能力面":引擎与面板只通过它操作 Live2D,
|
|
4
|
+
// 不直接碰 PIXI。相机动作(resetView / setZoomRatio / pan)最终都落到 view.applyZoom(),
|
|
5
|
+
// 与 Spine 共用同一套相机语义(render/camera)。
|
|
6
|
+
// 成员语义见各方法注释;成员集合属于对外契约,增删需同步渲染器与面板。
|
|
7
|
+
import { ZOOM_MIN, ZOOM_MAX, isAbsoluteRef } from './stage-ctx.js';
|
|
8
|
+
import { createViewProbe } from './playback-probe.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 创建桥对象。
|
|
12
|
+
* @param {object} ctx 运行时上下文
|
|
13
|
+
* @returns {object} bridge(12 个成员)
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* 动作相关成员(清单 / 时长 / 播放):单独成工厂,避免 createBridge 过长。
|
|
17
|
+
* 时长为什么要预取:运行时 getDuration 只有把动作播过一次才有值,
|
|
18
|
+
* 照那样做,列表里的时长会"点过才出现";改为直接读动作文件的 Meta.Duration。
|
|
19
|
+
* @param {object} ctx 运行时上下文(含 st / note / resolveAsset)
|
|
20
|
+
*/
|
|
21
|
+
function createMotionApi(ctx) {
|
|
22
|
+
const { st, note } = ctx;
|
|
23
|
+
/** label → 秒数(null = 确实取不到) */
|
|
24
|
+
const durationCache = new Map();
|
|
25
|
+
|
|
26
|
+
/** 动作在 model3.json 里的原始 File 值(如 motions/idle.motion3.json) */
|
|
27
|
+
function motionFileOf(group, index) {
|
|
28
|
+
const meta = st.meta || {};
|
|
29
|
+
const mo = ((meta.motions && meta.motions[group]) || [])[index] || null;
|
|
30
|
+
return (mo && (mo.File || mo.Name)) || null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** 动作文件 URL:model3.json 里的 File 相对模型目录 */
|
|
34
|
+
function motionFileUrl(group, index) {
|
|
35
|
+
const meta = st.meta || {};
|
|
36
|
+
const dir = String(meta.modelUrl || '').replace(/[^/]*$/, '');
|
|
37
|
+
const file = motionFileOf(group, index);
|
|
38
|
+
if (!file) return null;
|
|
39
|
+
// 绝对地址(任意 scheme 或 data:)不拼目录:模型定义里允许写绝对 URL,
|
|
40
|
+
// 拼上去会变成 /files/…/http://… 这种无效地址(曾经 14 次 miss 的来源);
|
|
41
|
+
// 根绝对(/xxx)也原样保留。
|
|
42
|
+
const joined = isAbsoluteRef(file) || file.startsWith('/') ? file : `${dir}${file}`;
|
|
43
|
+
try { return ctx.resolveAsset(joined); } catch { return joined; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 标签解析:先按规范标签精确匹配;不中再兼容旧格式 `group / name`(历史持久化里存过这种值)。
|
|
48
|
+
* 都查不到返回 null(调用方按"未知标签"处理)。
|
|
49
|
+
* @param {string} label
|
|
50
|
+
* @returns {{label:string, group:string, index:number}|null}
|
|
51
|
+
*/
|
|
52
|
+
function resolveLabel(label) {
|
|
53
|
+
const map = st.motionMap || new Map();
|
|
54
|
+
const key = String(label || '');
|
|
55
|
+
if (map.has(key)) {
|
|
56
|
+
const hit = map.get(key);
|
|
57
|
+
return { label: key, group: hit.group, index: hit.index };
|
|
58
|
+
}
|
|
59
|
+
// 旧格式兜底:`group / motionName` → 用 motionName 段匹配(同名多条取第一条)
|
|
60
|
+
const legacyName = key.includes(' / ') ? key.split(' / ').pop() : '';
|
|
61
|
+
if (legacyName && map.has(legacyName)) {
|
|
62
|
+
const hit = map.get(legacyName);
|
|
63
|
+
return { label: legacyName, group: hit.group, index: hit.index };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
labels: () => ctx.api.model.motionLabels(),
|
|
70
|
+
/** 当前播放的动作标签(未播过为 null);渲染器用它回显 animation 的生效值 */
|
|
71
|
+
currentLabel: () => st.currentLabel || null,
|
|
72
|
+
/** 动作清单(只读):标签 + group/index + 时长(取不到给 null) */
|
|
73
|
+
motionItems() {
|
|
74
|
+
const items = [];
|
|
75
|
+
for (const [label, hit] of (st.motionMap || new Map())) {
|
|
76
|
+
// 时长优先级:① Document 预取表(Assets 层,全量动作都有)
|
|
77
|
+
// ② durationCache(本进程补取过的)
|
|
78
|
+
// ③ 已加载动作的 motionManager(只有被播过的那条有,作最后兜底)
|
|
79
|
+
const prefetched = ((st.meta && st.meta.motionDurations) || {})[motionFileOf(hit.group, hit.index)];
|
|
80
|
+
let duration = Number.isFinite(prefetched) && prefetched > 0
|
|
81
|
+
? prefetched
|
|
82
|
+
: (durationCache.has(label) ? durationCache.get(label) : null);
|
|
83
|
+
if (duration === null) {
|
|
84
|
+
try {
|
|
85
|
+
const mm = st.model && st.model.internalModel && st.model.internalModel.motionManager;
|
|
86
|
+
const mo = mm && mm.motionGroups && mm.motionGroups[hit.group] && mm.motionGroups[hit.group][hit.index];
|
|
87
|
+
const d = mo && typeof mo.getDuration === 'function' ? mo.getDuration() : null;
|
|
88
|
+
if (typeof d === 'number' && isFinite(d) && d > 0) duration = d;
|
|
89
|
+
} catch { /* 取不到就不给 */ }
|
|
90
|
+
}
|
|
91
|
+
items.push({ label, group: hit.group, index: hit.index, duration });
|
|
92
|
+
}
|
|
93
|
+
return items;
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* 预热动作时长(**本版不实现**)。
|
|
97
|
+
*
|
|
98
|
+
* 为什么保留空实现:时长要读动作文件(motion3.json)里的 Meta.Duration,
|
|
99
|
+
* 而"取模型资源"已按契约收到 Assets 层(infra/document.js)——渲染器里不允许再 fetch 动作文件。
|
|
100
|
+
* 恢复正解:让 Document 多带一份 meta.motionDurations(Assets 层算好),这里只读地取用。
|
|
101
|
+
* 在那之前动作时长显示为空,属已知能力缺口,不是 bug。
|
|
102
|
+
* @returns {Promise<number>} 本次补齐条数(恒 0)
|
|
103
|
+
*/
|
|
104
|
+
async ensureMotionDurations() {
|
|
105
|
+
return 0;
|
|
106
|
+
},
|
|
107
|
+
/** 动作切换:标签 → (group, index) */
|
|
108
|
+
playLabel(label) {
|
|
109
|
+
const hit = resolveLabel(label);
|
|
110
|
+
if (!hit || !st.model) {
|
|
111
|
+
note(`Live2D 未知动作标签:${label}`, 'warn');
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
// Cubism 的动作队列是**顺序播放**语义:直接 motion() 只会把新动作排到队尾,
|
|
116
|
+
// 正在播的那条不结束就不会切过去(引擎侧表现为队列实例不变、时间轴一直推进在旧动作上)。
|
|
117
|
+
// 因此先 stopAllMotions() 清空队列再起新的 —— 这才是"切换即打断"。
|
|
118
|
+
const mm = st.model.internalModel && st.model.internalModel.motionManager;
|
|
119
|
+
if (mm && typeof mm.stopAllMotions === 'function') mm.stopAllMotions();
|
|
120
|
+
st.model.motion(hit.group, hit.index);
|
|
121
|
+
// 回显用解析后的**规范标签**(旧格式 "group / name" 也能落到同一条)
|
|
122
|
+
st.currentLabel = hit.label || label;
|
|
123
|
+
note(`Live2D 播动作:${st.currentLabel}`);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (err) {
|
|
126
|
+
note(`Live2D 播动作失败:${err && err.message}`, 'warn');
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 状态组:是否激活 / 当前模型 id。
|
|
135
|
+
* 拆成小组的理由:单个 createBridge 曾涨到 119 行(审计 max-function-length),
|
|
136
|
+
* 而它本质只是"把若干能力拼起来"。按能力分组后每块都能单独读、单独测。
|
|
137
|
+
*/
|
|
138
|
+
function createStateGroup(st) {
|
|
139
|
+
return {
|
|
140
|
+
isActive: () => st.active,
|
|
141
|
+
currentModelId: () => st.modelId,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 播放组:引擎侧探针 + 「动作播完一次」订阅。
|
|
147
|
+
* 注意事件源是 **motionManager**,不是 model(实测 model._events 只有 'destroy')。
|
|
148
|
+
*/
|
|
149
|
+
function createPlaybackGroup(st, probe) {
|
|
150
|
+
return {
|
|
151
|
+
/** 引擎侧播放探针(验收用:读引擎真实状态,不拿我们自己的 currentLabel 自证) */
|
|
152
|
+
playbackProbe: probe.playbackProbe,
|
|
153
|
+
/**
|
|
154
|
+
* 订阅「当前动作播完一次」(Live2D 侧)。
|
|
155
|
+
* 循环动作永远不触发(库内 `isFinished()` 恒为 false),与 Spine 的 complete 语义一致。
|
|
156
|
+
* @param {(e: {name: string, source: string}) => void} cb
|
|
157
|
+
* @returns {() => void} 取消订阅
|
|
158
|
+
*/
|
|
159
|
+
onMotionComplete(cb) {
|
|
160
|
+
const model = st.model;
|
|
161
|
+
const mm = model && model.internalModel && model.internalModel.motionManager;
|
|
162
|
+
const target = (mm && typeof mm.on === 'function') ? mm : model;
|
|
163
|
+
if (!target || typeof target.on !== 'function') return () => {};
|
|
164
|
+
const handler = () => {
|
|
165
|
+
let name = '';
|
|
166
|
+
// 事件不带名字(库内 emit 无参)⇒ 从 playback probe 读当前动作。
|
|
167
|
+
// ⚠ probe.currentMotion() 返回**对象**(label/group/index),直接 String() 会得到
|
|
168
|
+
// "[object Object]"(实测踩过)⇒ 按 label → name → String 顺序取值。
|
|
169
|
+
try {
|
|
170
|
+
const cur = probe.currentMotion && probe.currentMotion();
|
|
171
|
+
if (cur && typeof cur === 'object') name = String(cur.label || cur.name || '');
|
|
172
|
+
else if (cur) name = String(cur);
|
|
173
|
+
if (!name) name = String(st.currentLabel || '');
|
|
174
|
+
} catch { name = ''; }
|
|
175
|
+
try { cb({ name, trackIndex: 0, source: 'live2d' }); } catch { /* 忽略 */ }
|
|
176
|
+
};
|
|
177
|
+
try { target.on('motionFinish', handler); } catch { return () => {}; }
|
|
178
|
+
return () => { try { target.off('motionFinish', handler); } catch { /* 忽略 */ } };
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** 视图组:显隐 / 复位 / 缩放 / 平移 / 只读快照 */
|
|
184
|
+
function createViewGroup(ctx, st, probe) {
|
|
185
|
+
return {
|
|
186
|
+
hide: () => ctx.api.overlay.hide(),
|
|
187
|
+
/** 相机复位 */
|
|
188
|
+
resetView() {
|
|
189
|
+
st.zoomRatio = 1;
|
|
190
|
+
st.panX = 0;
|
|
191
|
+
st.panY = 0;
|
|
192
|
+
ctx.api.view.applyZoom();
|
|
193
|
+
ctx.note('Live2D 视图复位');
|
|
194
|
+
},
|
|
195
|
+
/** 相机缩放:值越大模型越大 */
|
|
196
|
+
setZoomRatio(ratio) {
|
|
197
|
+
const v = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Number(ratio) || 1));
|
|
198
|
+
st.zoomRatio = v;
|
|
199
|
+
ctx.api.view.applyZoom();
|
|
200
|
+
return v;
|
|
201
|
+
},
|
|
202
|
+
/** 相机平移:屏幕像素增量 */
|
|
203
|
+
pan(dx, dy) {
|
|
204
|
+
st.panX += Number(dx) || 0;
|
|
205
|
+
st.panY += Number(dy) || 0;
|
|
206
|
+
ctx.api.view.applyZoom();
|
|
207
|
+
},
|
|
208
|
+
/** 只读诊断快照 */
|
|
209
|
+
readState() {
|
|
210
|
+
const motion = probe.currentMotion();
|
|
211
|
+
return { active: st.active, modelId: st.modelId, zoom: st.zoomRatio, pan: { x: st.panX, y: st.panY }, motion };
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 装载与表情组:装载入口(复用或加载)+ 表情列表/切换 */
|
|
217
|
+
function createLoadGroup(ctx, st, note) {
|
|
218
|
+
return {
|
|
219
|
+
/**
|
|
220
|
+
* 装载入口:复用或加载,返回与 Spine 同形的摘要。
|
|
221
|
+
* @param {object} model
|
|
222
|
+
* @returns {Promise<{animations: string[], skins: string[], durations: object, version: string}>}
|
|
223
|
+
*/
|
|
224
|
+
async loadForEngine(model) {
|
|
225
|
+
const reusable = st.active && st.modelId === model.id && st.model;
|
|
226
|
+
if (!reusable) {
|
|
227
|
+
note(`Live2D 装载:${model.name}`);
|
|
228
|
+
await ctx.api.model.loadModel(model);
|
|
229
|
+
// 换模型 = 换内容:清掉上一个模型的"当前动作"标签。
|
|
230
|
+
// 为什么必须清:loadForEngine 的返回值会作为 info.motion 回传给应用层,
|
|
231
|
+
// 而 fillTransportOptions 会把它当成"新模型要播的动作"。旧 label 若恰好同名
|
|
232
|
+
// (碧蓝航线 live2d 的动作全叫 idle/home/…)⇒ 新模型真的会去播上一个模型选的那条。
|
|
233
|
+
st.currentLabel = '';
|
|
234
|
+
} else {
|
|
235
|
+
note(`Live2D 复用已加载模型:${model.name}`);
|
|
236
|
+
}
|
|
237
|
+
// 装载完成即补一次速度:新模型实例没有补丁,且"先设速度再换模型"要能延续
|
|
238
|
+
applySpeedToModel(st);
|
|
239
|
+
return {
|
|
240
|
+
animations: ctx.api.model.motionLabels(),
|
|
241
|
+
skins: ((st.meta && st.meta.expressions) || []).map((e) => e.name),
|
|
242
|
+
durations: {},
|
|
243
|
+
version: `Live2D Cubism ${(model && model.cubism) || 3}`,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
expressions: () => ((st.meta && st.meta.expressions) || []).map((e) => e.name),
|
|
247
|
+
/** 表情切换:表情名 → expression 下标 */
|
|
248
|
+
setExpressions(names) {
|
|
249
|
+
if (!st.model) return false;
|
|
250
|
+
const list = (names || []).filter(Boolean);
|
|
251
|
+
try {
|
|
252
|
+
if (!list.length) { st.model.expression(); return true; }
|
|
253
|
+
const exprs = (st.meta && st.meta.expressions) || [];
|
|
254
|
+
const idx = exprs.findIndex((e) => e.name === list[0]);
|
|
255
|
+
if (idx >= 0) st.model.expression(idx);
|
|
256
|
+
note(`Live2D 切表情:${list[0]}${idx >= 0 ? '' : '(未找到,保持原表情)'}`, idx >= 0 ? 'info' : 'warn');
|
|
257
|
+
return idx >= 0;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
note(`Live2D 切表情失败:${err && err.message}`, 'warn');
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* 只读诊断:动作完成事件到底挂在哪个对象上(排障用,不参与逻辑)。
|
|
268
|
+
* 为什么需要它:库内 `emit("motionFinish")` 的目标对象与文档描述不一定一致,
|
|
269
|
+
* 只能实测确认(本次就是靠它发现事件在 motionManager 而不是 model 上)。
|
|
270
|
+
*/
|
|
271
|
+
function debugMotionOf(st) {
|
|
272
|
+
const model = st.model;
|
|
273
|
+
const mm = model && model.internalModel && model.internalModel.motionManager;
|
|
274
|
+
const state = mm && mm.state;
|
|
275
|
+
const keysOf = (o) => (o ? Object.keys(o).slice(0, 12) : null);
|
|
276
|
+
return {
|
|
277
|
+
hasModel: !!model,
|
|
278
|
+
modelOn: !!(model && typeof model.on === 'function'),
|
|
279
|
+
modelEvents: model && model._events ? Object.keys(model._events) : null,
|
|
280
|
+
hasManager: !!mm,
|
|
281
|
+
managerOn: !!(mm && typeof mm.on === 'function'),
|
|
282
|
+
managerKeys: keysOf(mm),
|
|
283
|
+
managerStateOn: !!(state && typeof state.on === 'function'),
|
|
284
|
+
managerStateKeys: keysOf(state),
|
|
285
|
+
currentLabel: st.currentLabel || null,
|
|
286
|
+
// 动作定义里的 Loop 标记(验收"循环动作是否上报"要看它)
|
|
287
|
+
definitions: mm && mm.definitions
|
|
288
|
+
? Object.keys(mm.definitions).reduce((acc, g) => {
|
|
289
|
+
acc[g] = (mm.definitions[g] || []).map((d) => ({ File: d.File, Loop: d.Loop }));
|
|
290
|
+
return acc;
|
|
291
|
+
}, {})
|
|
292
|
+
: null,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 组装 Live2D bridge(供 app 层调用)。
|
|
298
|
+
*
|
|
299
|
+
* **导出键集合与顺序必须与原实现逐字一致**(调用方按 key 取能力):
|
|
300
|
+
* 拆分组时用显式键序拼装,而不是简单 spread 各组 —— 否则 Object.keys 顺序变了,
|
|
301
|
+
* 依赖顺序的断言/快照就会假红。
|
|
302
|
+
*
|
|
303
|
+
* @param {object} ctx {{st, note, api}}
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 给 Live2D 模型实例打"速度补丁"(**幂等**:重复调用不会套多层)。
|
|
308
|
+
*
|
|
309
|
+
* 为什么这么做:pixi-live2d-display 与 Cubism 核心都**没有** expose 速度接口
|
|
310
|
+
* (三个库 grep timeScale/setSpeed/speed 全为 0),唯一可控的是喂给 `model.update(dt)` 的 dt:
|
|
311
|
+
* 源码链路 `onTickerUpdate → update(deltaMS) → _render 里 internalModel.update(deltaTime, elapsedTime)`,
|
|
312
|
+
* 而 `internalModel.update` 会把 dt 传给 动作 + 物理 + 呼吸/眨眼 + 焦点 + pose。
|
|
313
|
+
* ⇒ 缩放 dt = 整帧时间缩放(物理与呼吸会一起变快,这是预期行为,要在控件 title 里写明)。
|
|
314
|
+
*
|
|
315
|
+
* 为什么不改 `PIXI.Ticker.shared.speed`:那是全局的,会影响所有订阅者;实例级补丁作用面最小。
|
|
316
|
+
*
|
|
317
|
+
* @param {object} model Live2DModel 实例
|
|
318
|
+
* @returns {boolean} 是否已可用(无 update 方法时返回 false)
|
|
319
|
+
*/
|
|
320
|
+
function patchModelSpeed(model) {
|
|
321
|
+
if (!model || typeof model.update !== 'function') return false;
|
|
322
|
+
if (model.__speedPatched) return true;
|
|
323
|
+
const orig = model.update.bind(model);
|
|
324
|
+
model.__speedPatched = { orig };
|
|
325
|
+
model.__speed = model.__speed || 1;
|
|
326
|
+
model.update = (dt) => orig(dt * (model.__speed || 1));
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 播放速度组:统一入口的 Live2D 侧实现(与 spine 的 AnimationState.timeScale 对齐为线性倍率)。
|
|
332
|
+
* @param {object} st live2d 舞台状态
|
|
333
|
+
*/
|
|
334
|
+
function createSpeedGroup(st) {
|
|
335
|
+
return {
|
|
336
|
+
/**
|
|
337
|
+
* 设置播放速度(夹 0.1–3)。
|
|
338
|
+
* 模型还没装载时先记住(st.speed),装载后由 applySpeedToModel 补上 —— 否则"先设速度再换模型"会丢。
|
|
339
|
+
* @param {number} v 倍率
|
|
340
|
+
* @returns {number} 生效值
|
|
341
|
+
*/
|
|
342
|
+
setSpeed(v) {
|
|
343
|
+
const speed = Math.max(0.1, Math.min(3, Number(v) || 1));
|
|
344
|
+
st.speed = speed;
|
|
345
|
+
const model = st.model;
|
|
346
|
+
if (model && patchModelSpeed(model)) model.__speed = speed;
|
|
347
|
+
return speed;
|
|
348
|
+
},
|
|
349
|
+
/** @returns {number} 当前速度倍率(未设置过为 1) */
|
|
350
|
+
getSpeed() {
|
|
351
|
+
return typeof st.speed === 'number' ? st.speed : 1;
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 把已记住的速度应用到当前模型(换模型 / 装载后必须调一次,新实例没有补丁)。
|
|
358
|
+
* @param {object} st
|
|
359
|
+
*/
|
|
360
|
+
function applySpeedToModel(st) {
|
|
361
|
+
const model = st.model;
|
|
362
|
+
if (!model || !patchModelSpeed(model)) return;
|
|
363
|
+
model.__speed = typeof st.speed === 'number' ? st.speed : 1;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function createBridge(ctx) {
|
|
367
|
+
const { st, note } = ctx;
|
|
368
|
+
const probe = createViewProbe(st);
|
|
369
|
+
const stateGroup = { ...createStateGroup(st), debugMotion: () => debugMotionOf(st) };
|
|
370
|
+
const playbackGroup = createPlaybackGroup(st, probe);
|
|
371
|
+
const viewGroup = createViewGroup(ctx, st, probe);
|
|
372
|
+
const loadGroup = createLoadGroup(ctx, st, note);
|
|
373
|
+
const speedGroup = createSpeedGroup(st);
|
|
374
|
+
|
|
375
|
+
const bridge = {
|
|
376
|
+
isActive: stateGroup.isActive,
|
|
377
|
+
playbackProbe: playbackGroup.playbackProbe,
|
|
378
|
+
hide: viewGroup.hide,
|
|
379
|
+
currentModelId: stateGroup.currentModelId,
|
|
380
|
+
...createMotionApi(ctx),
|
|
381
|
+
expressions: loadGroup.expressions,
|
|
382
|
+
debugMotion: stateGroup.debugMotion,
|
|
383
|
+
onMotionComplete: playbackGroup.onMotionComplete,
|
|
384
|
+
loadForEngine: loadGroup.loadForEngine,
|
|
385
|
+
setExpressions: loadGroup.setExpressions,
|
|
386
|
+
resetView: viewGroup.resetView,
|
|
387
|
+
setZoomRatio: viewGroup.setZoomRatio,
|
|
388
|
+
pan: viewGroup.pan,
|
|
389
|
+
readState: viewGroup.readState,
|
|
390
|
+
// 播放速度(统一入口的 Live2D 侧;与 spine 同名能力键 `speed`)
|
|
391
|
+
...speedGroup,
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// 验收探针挂到页面上(与 lead 共用同一套引擎侧判据)
|
|
395
|
+
try {
|
|
396
|
+
if (typeof globalThis !== 'undefined' && globalThis.__WEB__) globalThis.__WEB__.playbackProbe = () => bridge.playbackProbe();
|
|
397
|
+
} catch { /* 忽略 */ }
|
|
398
|
+
|
|
399
|
+
return bridge;
|
|
400
|
+
}
|