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,82 @@
|
|
|
1
|
+
// race-probe.js —— 竞态验收探针(跨 kind 连点,last-wins 后终态必须非空白)
|
|
2
|
+
//
|
|
3
|
+
// 判空白的方式(重要):
|
|
4
|
+
// · 终态 spine 面 → stage.samplePixels().ratio(读的是 Spine WebGL 画布)
|
|
5
|
+
// · 终态 Live2D 面 → 不能用 samplePixels(那是 Spine 画布,此时被隐藏),
|
|
6
|
+
// 改用 live2d.collectCmp() 的 extract.opaqueRatio(PIXI extract 出模型实际画出来的像素占比)
|
|
7
|
+
(async () => {
|
|
8
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
9
|
+
const q = (s) => document.querySelector(s);
|
|
10
|
+
const qa = (s) => [...document.querySelectorAll(s)];
|
|
11
|
+
const w = window.__WEB__;
|
|
12
|
+
const stageEl = q('#stage');
|
|
13
|
+
for (let i = 0; i < 80 && qa('[data-game-key]').length === 0; i += 1) await sleep(150);
|
|
14
|
+
|
|
15
|
+
const trajectory = [];
|
|
16
|
+
let last = '__init__';
|
|
17
|
+
const watch = setInterval(() => {
|
|
18
|
+
const v = stageEl.getAttribute('data-stage-mode');
|
|
19
|
+
if (v !== last) { last = v; trajectory.push(v); }
|
|
20
|
+
}, 30);
|
|
21
|
+
|
|
22
|
+
const click = async (kw, root) => {
|
|
23
|
+
const input = q('#roster-search');
|
|
24
|
+
input.value = kw;
|
|
25
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
26
|
+
for (let i = 0; i < 140 && qa('[data-model-id]').length === 0; i += 1) await sleep(150);
|
|
27
|
+
await sleep(250);
|
|
28
|
+
const row = qa('[data-model-id]').find((n) => n.dataset.rootId === root);
|
|
29
|
+
if (!row) return false;
|
|
30
|
+
row.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/** 终态"画面上有没有东西"的量化:按当前面选口径 */
|
|
35
|
+
const contentRatio = async () => {
|
|
36
|
+
if (stageEl.getAttribute('data-stage-mode') === 'live2d') {
|
|
37
|
+
const rec = w.live2d.collectCmp('race-probe');
|
|
38
|
+
const ex = rec && rec.extract;
|
|
39
|
+
return ex && typeof ex.opaqueRatio === 'number' ? +ex.opaqueRatio.toFixed(4) : null;
|
|
40
|
+
}
|
|
41
|
+
return +w.stage.samplePixels().ratio.toFixed(4);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const L = ['abeikelongbi', 'azurlane'];
|
|
45
|
+
const S = ['aijier', 'azurlane_spine'];
|
|
46
|
+
const rounds = [];
|
|
47
|
+
for (const gap of [60, 150, 300]) {
|
|
48
|
+
for (const pair of [[L, S, 'spine'], [S, L, 'live2d']]) {
|
|
49
|
+
const [first, second, wantMode] = pair;
|
|
50
|
+
trajectory.length = 0;
|
|
51
|
+
const a = await click(first[0], first[1]);
|
|
52
|
+
await sleep(gap);
|
|
53
|
+
const b = await click(second[0], second[1]);
|
|
54
|
+
// 等到切换真正落定(首次装载 Live2D 要按需加载运行时库,固定 sleep 会误判成"还在 loading")
|
|
55
|
+
for (let i = 0; i < 120 && stageEl.getAttribute('data-stage-mode') === 'loading'; i += 1) await sleep(150);
|
|
56
|
+
await sleep(1200);
|
|
57
|
+
const s = w.stage.stats();
|
|
58
|
+
const ratio = await contentRatio();
|
|
59
|
+
rounds.push({
|
|
60
|
+
gapMs: gap,
|
|
61
|
+
lastClick: second[1],
|
|
62
|
+
clicked: [a, b],
|
|
63
|
+
mode: stageEl.getAttribute('data-stage-mode'),
|
|
64
|
+
wantMode,
|
|
65
|
+
modeOk: stageEl.getAttribute('data-stage-mode') === wantMode,
|
|
66
|
+
live2dActive: !!(w.live2d && w.live2d.isActive()),
|
|
67
|
+
contentRatio: ratio,
|
|
68
|
+
nonBlank: ratio !== null && ratio > 0.05,
|
|
69
|
+
lastError: s.lastError,
|
|
70
|
+
stageReady: document.documentElement.dataset.stageReady,
|
|
71
|
+
hadLoading: trajectory.includes('loading'),
|
|
72
|
+
visibleFacesAtEnd: qa('#stage canvas').filter((c) => getComputedStyle(c).visibility !== 'hidden'
|
|
73
|
+
&& getComputedStyle(c.parentElement || c).display !== 'none').length,
|
|
74
|
+
trajectory: trajectory.slice(0, 10).join('>'),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
clearInterval(watch);
|
|
79
|
+
const logs = (w.stageLog || []).slice(-10).map((x) => `${x.level}:${x.line}`.slice(0, 88));
|
|
80
|
+
const failed = rounds.filter((r) => !r.modeOk || !r.nonBlank || r.lastError || !r.hadLoading).length;
|
|
81
|
+
return { failed, rounds, logs };
|
|
82
|
+
})()
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// debug/upload.js —— 把页面内存里的调试日志上传到服务端「调试日志上传」按钮的行为
|
|
2
|
+
//
|
|
3
|
+
// 背景:调试日志只在内存(刷新即丢),用户描述问题时说不清、AI 又读不到浏览器。这个按钮把
|
|
4
|
+
// 当前日志一次性交给服务端落盘(data/logs/),之后 AI 用 GET /api/diag/uploads 就能自己查到文件。
|
|
5
|
+
//
|
|
6
|
+
// 数据源**只从统一工厂取**(infra/log.js 的 recent):那里的内容已经同时包含
|
|
7
|
+
// stageStatus 状态行(tag='stage')与 diag 事件(tag='diag')——这正是"走工厂化"的验证点,
|
|
8
|
+
// 也避免出现"面板有、上传缺"的半套日志。
|
|
9
|
+
//
|
|
10
|
+
// 三态必须可见(不许静默):
|
|
11
|
+
// 上传中 → 按钮禁用 + 文案「上传中…」
|
|
12
|
+
// 成功 → 「已上传:diag-….json(N 条 / X KB)」
|
|
13
|
+
// 失败 → 「上传失败:<原因>(点按钮重试)」
|
|
14
|
+
// 同时把状态写进 <html> 的 dataset(logUpload / logUploadMsg / logUploadCount),供自动化断言。
|
|
15
|
+
|
|
16
|
+
import { postJson } from '../infra/api.js';
|
|
17
|
+
import { recent } from '../infra/log.js';
|
|
18
|
+
|
|
19
|
+
/** 一次最多捞多少条:与后端默认 maxCount 对齐(后端还会按配置再截一次) */
|
|
20
|
+
const MAX_UPLOAD_COUNT = 5000;
|
|
21
|
+
|
|
22
|
+
/** 字节数转人话(KB/MB 保留一位小数) */
|
|
23
|
+
function humanBytes(n) {
|
|
24
|
+
const bytes = Number(n) || 0;
|
|
25
|
+
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
26
|
+
if (bytes >= 1024) return `${(bytes / 1024).toFixed(0)}KB`;
|
|
27
|
+
return `${bytes}B`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 收集要上传的日志与上下文。
|
|
32
|
+
* meta 是为了"离线也能定位":哪一版、哪个页面、什么模型/渲染器、什么浏览器。
|
|
33
|
+
* @param {() => object} readState 取应用状态的函数(避免本模块直接依赖 context.js)
|
|
34
|
+
* @returns {{logs: object[], meta: object, truncated: boolean}}
|
|
35
|
+
*/
|
|
36
|
+
export function collectPayload(readState) {
|
|
37
|
+
const all = recent(); // 工厂里的全部(受 LOG_LIMIT 限制)
|
|
38
|
+
const truncated = all.length > MAX_UPLOAD_COUNT;
|
|
39
|
+
const logs = truncated ? all.slice(-MAX_UPLOAD_COUNT) : all;
|
|
40
|
+
let app = {};
|
|
41
|
+
try { app = (typeof readState === 'function' ? readState() : null) || {}; } catch { app = {}; }
|
|
42
|
+
const model = app.selectedModel || null;
|
|
43
|
+
const meta = {
|
|
44
|
+
version: app.version || '',
|
|
45
|
+
url: typeof location !== 'undefined' ? location.href : '',
|
|
46
|
+
ua: typeof navigator !== 'undefined' ? navigator.userAgent : '',
|
|
47
|
+
at: Date.now(),
|
|
48
|
+
model: model ? (model.id || model.name || null) : null,
|
|
49
|
+
renderer: app.renderer || null,
|
|
50
|
+
count: logs.length,
|
|
51
|
+
clientTruncated: truncated,
|
|
52
|
+
};
|
|
53
|
+
return { logs, meta, truncated };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 建一个上传器:把按钮与状态文本接起来。
|
|
58
|
+
* @param {{
|
|
59
|
+
* button: HTMLButtonElement,
|
|
60
|
+
* statusEl?: HTMLElement,
|
|
61
|
+
* readState?: () => object,
|
|
62
|
+
* post?: (path: string, body: object) => Promise<any>,
|
|
63
|
+
* }} opts
|
|
64
|
+
* @returns {{run: () => Promise<object|null>, state: () => string, destroy: () => void}}
|
|
65
|
+
*/
|
|
66
|
+
export function createUploader(opts) {
|
|
67
|
+
const { button, statusEl } = opts;
|
|
68
|
+
const readState = opts.readState;
|
|
69
|
+
const post = typeof opts.post === 'function' ? opts.post : postJson;
|
|
70
|
+
let current = 'idle';
|
|
71
|
+
let lastResult = null;
|
|
72
|
+
|
|
73
|
+
/** 统一写状态:按钮文案/禁用、状态文本、<html> dataset 三处同源 */
|
|
74
|
+
function paint(next, text) {
|
|
75
|
+
current = next;
|
|
76
|
+
if (button) {
|
|
77
|
+
button.disabled = next === 'uploading';
|
|
78
|
+
// 文案用独立 span(避免把图标冲掉):没有就退回纯文本
|
|
79
|
+
const label = button.querySelector('[data-upload-label]');
|
|
80
|
+
const word = next === 'uploading' ? '上传中…' : '上传日志';
|
|
81
|
+
if (label) label.textContent = word;
|
|
82
|
+
else button.textContent = word;
|
|
83
|
+
button.setAttribute('aria-busy', String(next === 'uploading'));
|
|
84
|
+
}
|
|
85
|
+
if (statusEl) {
|
|
86
|
+
statusEl.textContent = text || '';
|
|
87
|
+
statusEl.hidden = !text;
|
|
88
|
+
}
|
|
89
|
+
if (typeof document !== 'undefined' && document.documentElement) {
|
|
90
|
+
const ds = document.documentElement.dataset;
|
|
91
|
+
ds.logUpload = next;
|
|
92
|
+
ds.logUploadMsg = text || '';
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 执行一次上传(幂等:上传中再点无效) */
|
|
97
|
+
async function run() {
|
|
98
|
+
if (current === 'uploading') return null;
|
|
99
|
+
const { logs, meta } = collectPayload(readState);
|
|
100
|
+
if (!logs.length) {
|
|
101
|
+
paint('fail', '上传失败:当前还没有日志可上传');
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
paint('uploading', `上传中…(${logs.length} 条)`);
|
|
105
|
+
try {
|
|
106
|
+
const res = await post('/api/diag/upload', { logs, meta });
|
|
107
|
+
if (!res || res.ok !== true) throw new Error((res && res.error) || '服务端未确认');
|
|
108
|
+
lastResult = res;
|
|
109
|
+
const extra = res.truncated ? '(服务端按上限截断)' : '';
|
|
110
|
+
paint('ok', `已上传:${res.file}(${res.count} 条 / ${humanBytes(res.bytes)})${extra}`);
|
|
111
|
+
if (typeof document !== 'undefined' && document.documentElement) {
|
|
112
|
+
document.documentElement.dataset.logUploadCount = String(res.count);
|
|
113
|
+
document.documentElement.dataset.logUploadFile = String(res.file || '');
|
|
114
|
+
}
|
|
115
|
+
return res;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
const reason = err && err.message ? err.message : String(err);
|
|
118
|
+
paint('fail', `上传失败:${reason}(点按钮重试)`);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
run,
|
|
125
|
+
state: () => current,
|
|
126
|
+
lastResult: () => lastResult,
|
|
127
|
+
/** 复位到空闲(重新打开面板/清空日志时用) */
|
|
128
|
+
reset: () => paint('idle', ''),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// adapters/atlas.js —— 图集适配(构造签名差异 + 页纹理挂载)
|
|
2
|
+
//
|
|
3
|
+
// 3.x:`new TextureAtlas(text, imageResolver)` —— resolver 的返回值就是页纹理;
|
|
4
|
+
// 4.x:`new TextureAtlas(text)` —— 不吃 resolver,必须构造后逐页补挂。
|
|
5
|
+
// 这里用能力矩阵判断,不读版本号。
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {{caps: object, texture: object, onWarn?: Function}} ctx
|
|
9
|
+
*/
|
|
10
|
+
export function createAtlasAdapter({ caps, texture, onWarn }) {
|
|
11
|
+
const Ctor = caps.atlas.ctor;
|
|
12
|
+
if (!Ctor) throw new Error('运行时缺少 TextureAtlas');
|
|
13
|
+
|
|
14
|
+
/** 给尚未挂纹理的页补挂(4.x 路径;也兜住 3.x 中 resolver 未覆盖的页) */
|
|
15
|
+
function attachPages(atlas, resolveImage, premultiplied) {
|
|
16
|
+
for (const page of (atlas && atlas.pages) || []) {
|
|
17
|
+
if (page.texture) continue;
|
|
18
|
+
const img = resolveImage(page.name);
|
|
19
|
+
const tex = img ? texture.create(img, premultiplied) : texture.placeholder();
|
|
20
|
+
if (typeof page.setTexture === 'function') page.setTexture(tex);
|
|
21
|
+
else page.texture = tex;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 建图集。
|
|
27
|
+
* @param {string} text atlas 文本
|
|
28
|
+
* @param {(pageName: string) => any|null} resolveImage 页名 → 图片源
|
|
29
|
+
* @param {boolean} premultiplied
|
|
30
|
+
* @returns {object} atlas
|
|
31
|
+
*/
|
|
32
|
+
function create(text, resolveImage, premultiplied) {
|
|
33
|
+
if (caps.atlas.acceptsResolver) {
|
|
34
|
+
// resolver 的返回值会被直接当作页纹理 ⇒ 必须在这里创建纹理对象(不能返回裸图片)
|
|
35
|
+
return new Ctor(text, (pageName) => {
|
|
36
|
+
const img = resolveImage(pageName);
|
|
37
|
+
return img ? texture.create(img, premultiplied) : texture.placeholder();
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
onWarn && onWarn(`atlas:该运行时 TextureAtlas 只接受 ${caps.atlas.arity} 个参数,改用"构造后补挂纹理"路径`);
|
|
41
|
+
const atlas = new Ctor(text);
|
|
42
|
+
attachPages(atlas, resolveImage, premultiplied);
|
|
43
|
+
return atlas;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { create };
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// adapters/index.js —— 适配器装配:能力矩阵 → 各组适配器
|
|
2
|
+
//
|
|
3
|
+
// 业务层(player / loader / render-frame)只拿这里的对象,不直接摸 spine 库 API;
|
|
4
|
+
// 于是"换运行时版本"只影响本目录,业务代码不含任何版本判断。
|
|
5
|
+
|
|
6
|
+
import { createTextureAdapter } from './texture.js';
|
|
7
|
+
import { createAtlasAdapter } from './atlas.js';
|
|
8
|
+
import { createSkeletonAdapter } from './skeleton.js';
|
|
9
|
+
import { createRendererAdapter } from './renderer.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {{spine: object, caps: object, gl: WebGLRenderingContext, canvas: HTMLCanvasElement,
|
|
13
|
+
* size: {w: number, h: number}, onWarn?: Function}} ctx
|
|
14
|
+
* @returns {{texture: object, atlas: object, skeleton: object, renderer: object, caps: object}}
|
|
15
|
+
*/
|
|
16
|
+
export function createAdapters({ spine, caps, gl, canvas, size, onWarn }) {
|
|
17
|
+
const texture = createTextureAdapter({ caps, gl, onWarn });
|
|
18
|
+
const atlas = createAtlasAdapter({ caps, texture, onWarn });
|
|
19
|
+
const skeleton = createSkeletonAdapter({ caps, spine, onWarn });
|
|
20
|
+
const renderer = createRendererAdapter({ caps, canvas, gl, size, onWarn });
|
|
21
|
+
return { texture, atlas, skeleton, renderer, caps };
|
|
22
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// adapters/renderer.js —— 渲染器适配(SceneRenderer / SkeletonRenderer + 相机 + 绘制)
|
|
2
|
+
//
|
|
3
|
+
// 差异点:
|
|
4
|
+
// · 3.x:`new SceneRenderer(canvas, gl)`,自带 camera,绘制走 drawSkeleton(skeleton, premultiplied)
|
|
5
|
+
// · 4.x:可能只有 `SkeletonRenderer(gl)`,相机要自己 `new OrthoCamera(w, h)`,绘制走 draw(skeleton)
|
|
6
|
+
// 本适配器用能力矩阵选择,并在运行时校验方法引用(避免"方法存在但签名不同")。
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {{caps: object, canvas: HTMLCanvasElement, gl: WebGLRenderingContext,
|
|
10
|
+
* size: {w: number, h: number}, onWarn?: Function}} ctx
|
|
11
|
+
*/
|
|
12
|
+
export function createRendererAdapter({ caps, canvas, gl, size, onWarn }) {
|
|
13
|
+
const R = caps.renderer;
|
|
14
|
+
|
|
15
|
+
/** 建渲染器(同时给出相机) */
|
|
16
|
+
function create() {
|
|
17
|
+
if (R.kind === 'scene') return createScene();
|
|
18
|
+
if (R.kind === 'skeleton') return createSkeleton();
|
|
19
|
+
throw new Error('运行时既没有 SceneRenderer 也没有 SkeletonRenderer');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createScene() {
|
|
23
|
+
// 3.x 需要 canvas;个别版本只要 gl —— 用构造参数个数探测,不看版本号
|
|
24
|
+
const renderer = R.sceneArity >= 2 ? new R.sceneCtor(canvas, gl) : new R.sceneCtor(gl);
|
|
25
|
+
const camera = renderer.camera || (R.orthoCtor ? new R.orthoCtor(size.w, size.h) : null);
|
|
26
|
+
if (!renderer.camera && camera) onWarn && onWarn('renderer:SceneRenderer 未自带相机,已改用 OrthoCamera');
|
|
27
|
+
return { renderer, camera, kind: 'scene' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSkeleton() {
|
|
31
|
+
const renderer = new R.skeletonCtor(gl);
|
|
32
|
+
const camera = R.orthoCtor ? new R.orthoCtor(Math.max(1, size.w), Math.max(1, size.h)) : null;
|
|
33
|
+
if (!camera) onWarn && onWarn('renderer:没有 OrthoCamera,绘制可能不可见');
|
|
34
|
+
return { renderer, camera, kind: 'skeleton' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 画一帧。
|
|
39
|
+
* @param {{renderer: object, kind: string}} target create() 的返回值
|
|
40
|
+
* @param {object} skeleton
|
|
41
|
+
* @param {boolean} premultiplied
|
|
42
|
+
*/
|
|
43
|
+
function draw(target, skeleton, premultiplied) {
|
|
44
|
+
const r = target && target.renderer;
|
|
45
|
+
if (!r || !skeleton) return false;
|
|
46
|
+
try {
|
|
47
|
+
if (typeof r.begin === 'function') r.begin();
|
|
48
|
+
try {
|
|
49
|
+
if (typeof r.drawSkeleton === 'function') {
|
|
50
|
+
// 不要用 drawSkeleton.length 判断"接不接受第二个参数"——那是错的判据:
|
|
51
|
+
// 4.0 的签名是 `drawSkeleton(skeleton, premultipliedAlpha = false, slotRangeStart = -1, …)`,
|
|
52
|
+
// 第 2 个参数**带默认值** ⇒ Function.length 只数默认值之前的形参 ⇒ 结果是 1,
|
|
53
|
+
// `length >= 2` 恒为假 ⇒ 永远走单参分支 ⇒ 从来不传 premultiplied。
|
|
54
|
+
// 后果在 spine-webgl-4.0 的 SceneRenderer.drawSkeleton 内部:
|
|
55
|
+
// this.skeletonRenderer.premultipliedAlpha = premultipliedAlpha ← 一直停在默认 false
|
|
56
|
+
// 而 `_SkeletonRenderer` 里这个字段同时决定两件事:
|
|
57
|
+
// :10544 srcFunc = premultipliedAlpha ? gl.ONE : gl.SRC_ALPHA ← blend 的 src 因子
|
|
58
|
+
// :10809 if (premultipliedAlpha) { finalColor.r *= finalColor.a; … } ← 顶点色是否乘 alpha
|
|
59
|
+
// 于是 atlas 声明 pma:true 的模型(像素已预乘)变成"预乘纹理 + 非预乘公式",
|
|
60
|
+
// 柔边被二次乘 alpha ⇒ 眼周/嘴周发暗。
|
|
61
|
+
// 老版本只有 1 个形参时,多传的那个参数会被 JS 忽略,所以统一传两个是安全的。
|
|
62
|
+
r.drawSkeleton(skeleton, premultiplied === true);
|
|
63
|
+
} else if (typeof r.draw === 'function') {
|
|
64
|
+
r.draw(skeleton);
|
|
65
|
+
} else {
|
|
66
|
+
onWarn && onWarn('renderer:既没有 drawSkeleton 也没有 draw,无法绘制');
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
} finally {
|
|
70
|
+
if (typeof r.end === 'function') r.end();
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
onWarn && onWarn(`renderer.draw 失败:${e.message}`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 取(或首次创建)调试渲染器。
|
|
81
|
+
* 优先用渲染器自带的 skeletonDebugRenderer;否则用能力矩阵探测到的构造器自己建一个。
|
|
82
|
+
* 为什么要缓存:调试渲染器持有 GPU 资源,每帧新建会持续泄漏。
|
|
83
|
+
* @param {{renderer: object, camera?: object}} target create() 的返回值
|
|
84
|
+
*/
|
|
85
|
+
function ensureDebugRenderer(target) {
|
|
86
|
+
const r = target && target.renderer;
|
|
87
|
+
if (!r) return null;
|
|
88
|
+
if (r.skeletonDebugRenderer) return r.skeletonDebugRenderer;
|
|
89
|
+
if (target.debugRenderer !== undefined && target.debugRenderer !== null) return target.debugRenderer;
|
|
90
|
+
const Ctor = R.debugCtor;
|
|
91
|
+
if (!Ctor) return null;
|
|
92
|
+
try {
|
|
93
|
+
target.debugRenderer = new Ctor(gl);
|
|
94
|
+
return target.debugRenderer;
|
|
95
|
+
} catch (e) {
|
|
96
|
+
onWarn && onWarn(`调试渲染器创建失败:${e.message}`);
|
|
97
|
+
target.debugRenderer = null;
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 八类开关 → SkeletonDebugRenderer 的 draw* 属性。
|
|
104
|
+
* 属性名按运行时实际存在才写(不同版本命名有差异),映射口径:
|
|
105
|
+
* bones→drawBones regions→drawRegionAttachments boundingBoxes→drawBoundingBoxes
|
|
106
|
+
* hulls→drawMeshHull meshes→drawMeshTriangles paths→drawPaths
|
|
107
|
+
* points→drawPoints clipping→drawClipping
|
|
108
|
+
* @param {object} dbg 调试渲染器
|
|
109
|
+
* @param {object} flags { bones, meshes, ... }
|
|
110
|
+
*/
|
|
111
|
+
function applyDebugFlags(dbg, flags) {
|
|
112
|
+
if (!dbg) return [];
|
|
113
|
+
const f = flags || {};
|
|
114
|
+
const map = {
|
|
115
|
+
drawBones: f.bones,
|
|
116
|
+
drawRegionAttachments: f.regions,
|
|
117
|
+
drawBoundingBoxes: f.boundingBoxes,
|
|
118
|
+
drawMeshHull: f.hulls,
|
|
119
|
+
drawMeshTriangles: f.meshes,
|
|
120
|
+
drawPaths: f.paths,
|
|
121
|
+
drawPoints: f.points,
|
|
122
|
+
drawClipping: f.clipping,
|
|
123
|
+
};
|
|
124
|
+
const missing = [];
|
|
125
|
+
for (const [prop, on] of Object.entries(map)) {
|
|
126
|
+
// 属性不存在 = 该运行时的调试渲染器不支持这一类(例如 3.8/4.1 都没有 drawPoints),
|
|
127
|
+
// 记下来交给上层如实上报,而不是静默吞掉让"点了没反应"
|
|
128
|
+
if (prop in dbg) dbg[prop] = !!on;
|
|
129
|
+
else if (on) missing.push(prop);
|
|
130
|
+
}
|
|
131
|
+
return missing;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 调试绘制能力探测(确定性,不依赖"是否已经画过一帧")。
|
|
136
|
+
* 为什么要它:面板要如实显示哪几类画不出来,若靠绘制时记录,暂停/未出帧时就会误报"都支持"。
|
|
137
|
+
* @param {object} target create() 的返回值
|
|
138
|
+
* @returns {{available: boolean, missing: string[]}} missing 为运行时没有对应属性的项
|
|
139
|
+
*/
|
|
140
|
+
function debugSupport(target) {
|
|
141
|
+
const dbg = ensureDebugRenderer(target);
|
|
142
|
+
if (!dbg) return { available: false, missing: [] };
|
|
143
|
+
const props = ['drawBones', 'drawRegionAttachments', 'drawBoundingBoxes',
|
|
144
|
+
'drawMeshHull', 'drawMeshTriangles', 'drawPaths', 'drawPoints', 'drawClipping'];
|
|
145
|
+
return { available: true, missing: props.filter((p) => !(p in dbg)) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 画一帧调试层。全关时不触碰 GL(避免"开关关了还在画"的假象)。
|
|
150
|
+
* @param {object} target create() 的返回值
|
|
151
|
+
* @param {object} skeleton
|
|
152
|
+
* @param {object} flags 八类开关
|
|
153
|
+
* @param {boolean} premultiplied 与主绘制同口径
|
|
154
|
+
* @returns {boolean} 是否真的画了
|
|
155
|
+
*/
|
|
156
|
+
function drawDebug(target, skeleton, flags, premultiplied) {
|
|
157
|
+
const any = !!flags && Object.keys(flags).some((k) => flags[k]);
|
|
158
|
+
if (!any || !skeleton) return false;
|
|
159
|
+
const r = target && target.renderer;
|
|
160
|
+
if (!r) return false;
|
|
161
|
+
const dbg = ensureDebugRenderer(target);
|
|
162
|
+
if (!dbg) return false;
|
|
163
|
+
const missing = applyDebugFlags(dbg, flags);
|
|
164
|
+
if (missing.length) {
|
|
165
|
+
// 同一个缺失项只告警一次(每帧都刷会把日志冲爆)
|
|
166
|
+
target.debugMissing = missing;
|
|
167
|
+
if (!target.debugMissingWarned) {
|
|
168
|
+
target.debugMissingWarned = {};
|
|
169
|
+
}
|
|
170
|
+
for (const prop of missing) {
|
|
171
|
+
if (target.debugMissingWarned[prop]) continue;
|
|
172
|
+
target.debugMissingWarned[prop] = true;
|
|
173
|
+
onWarn && onWarn(`调试渲染器没有 ${prop}:该运行时画不出这一类调试绘制`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
if (typeof r.begin === 'function') r.begin();
|
|
178
|
+
try {
|
|
179
|
+
if (typeof r.drawSkeletonDebug === 'function') {
|
|
180
|
+
// 渲染器自带调试绘制入口(3.x/4.x 都是这个名),二参是是否预乘
|
|
181
|
+
if (r.drawSkeletonDebug.length >= 2) r.drawSkeletonDebug(skeleton, premultiplied === true);
|
|
182
|
+
else r.drawSkeletonDebug(skeleton);
|
|
183
|
+
} else if (typeof dbg.draw === 'function') {
|
|
184
|
+
// 兜底:自己调调试渲染器的 draw,需要把相机的投影矩阵交给它
|
|
185
|
+
const cam = target.camera;
|
|
186
|
+
if (cam && cam.projection && 'projection' in dbg) dbg.projection = cam.projection;
|
|
187
|
+
dbg.draw(skeleton);
|
|
188
|
+
} else {
|
|
189
|
+
onWarn && onWarn('调试渲染器既无 drawSkeletonDebug 也无 draw,调试绘制不可用');
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
} finally {
|
|
193
|
+
if (typeof r.end === 'function') r.end();
|
|
194
|
+
}
|
|
195
|
+
return true;
|
|
196
|
+
} catch (e) {
|
|
197
|
+
onWarn && onWarn(`renderer.drawDebug 失败:${e.message}`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 释放渲染器(有的版本没有 dispose) */
|
|
203
|
+
function dispose(target) {
|
|
204
|
+
const r = target && target.renderer;
|
|
205
|
+
const own = target && target.debugRenderer;
|
|
206
|
+
// 只释放自己建的(渲染器自带的 skeletonDebugRenderer 由渲染器自己回收)
|
|
207
|
+
if (own && own !== r && typeof own.dispose === 'function') {
|
|
208
|
+
try { own.dispose(); } catch { /* 忽略 */ }
|
|
209
|
+
}
|
|
210
|
+
if (target) target.debugRenderer = null;
|
|
211
|
+
if (r && typeof r.dispose === 'function') {
|
|
212
|
+
try { r.dispose(); } catch { /* 忽略 */ }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** 相机视口跟随(缺 setViewport 时静默跳过) */
|
|
217
|
+
function syncViewport(camera, w, h) {
|
|
218
|
+
if (!camera || typeof camera.setViewport !== 'function') return false;
|
|
219
|
+
try { camera.setViewport(w, h); return true; } catch { return false; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { create, draw, drawDebug, debugSupport, dispose, syncViewport };
|
|
223
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// adapters/skeleton.js —— 骨架数据与骨架操作适配
|
|
2
|
+
//
|
|
3
|
+
// 差异点:JSON/Binary 读取器、AttachmentLoader、以及 skeleton 的世界变换/包围盒方法;
|
|
4
|
+
// 缺失的能力通过能力矩阵降级并 warn(不静默)。
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {{caps: object, spine: object, onWarn?: Function}} ctx
|
|
8
|
+
*/
|
|
9
|
+
export function createSkeletonAdapter({ caps, spine, onWarn }) {
|
|
10
|
+
/** 附件装载器(把图集区域绑到附件上) */
|
|
11
|
+
function attachmentLoader(atlas) {
|
|
12
|
+
const Ctor = caps.skeleton.attachmentLoaderCtor;
|
|
13
|
+
if (!Ctor) throw new Error('运行时缺少 AtlasAttachmentLoader');
|
|
14
|
+
return new Ctor(atlas);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** 读二进制骨架 */
|
|
18
|
+
function readBinary(loader, bytes) {
|
|
19
|
+
const Ctor = caps.skeleton.binaryCtor;
|
|
20
|
+
if (!Ctor) throw new Error('运行时缺少 SkeletonBinary');
|
|
21
|
+
return new Ctor(loader).readSkeletonData(bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 读 JSON 骨架 */
|
|
25
|
+
function readJson(loader, json) {
|
|
26
|
+
const Ctor = caps.skeleton.jsonCtor;
|
|
27
|
+
if (!Ctor) throw new Error('运行时缺少 SkeletonJson');
|
|
28
|
+
return new Ctor(loader).readSkeletonData(json);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 建骨架 + 动画状态机 */
|
|
32
|
+
function createRuntime(data) {
|
|
33
|
+
const skeleton = new spine.Skeleton(data);
|
|
34
|
+
const stateData = new spine.AnimationStateData(data);
|
|
35
|
+
const state = new spine.AnimationState(stateData);
|
|
36
|
+
return { skeleton, stateData, state };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 摆到 setup pose(fit 取包围盒前必须做) */
|
|
40
|
+
function setupPose(skeleton) {
|
|
41
|
+
try { if (caps.skeletonOps.setToSetupPose) skeleton.setToSetupPose(); } catch { /* 忽略 */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 更新世界变换(2 = 物理模式;缺能力时退回无参调用) */
|
|
45
|
+
function updateWorldTransform(skeleton) {
|
|
46
|
+
if (!skeleton || !caps.skeletonOps.updateWorldTransform) return;
|
|
47
|
+
try {
|
|
48
|
+
if (skeleton.updateWorldTransform.length >= 1) skeleton.updateWorldTransform(2);
|
|
49
|
+
else skeleton.updateWorldTransform();
|
|
50
|
+
} catch { /* 忽略 */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 取骨架包围盒(用运行时的 Vector2;缺能力返回 null) */
|
|
54
|
+
function bounds(skeleton) {
|
|
55
|
+
const V = caps.skeleton.vector2;
|
|
56
|
+
if (!skeleton || !caps.skeletonOps.getBounds || !V) return null;
|
|
57
|
+
try {
|
|
58
|
+
const offset = new V();
|
|
59
|
+
const size = new V();
|
|
60
|
+
skeleton.getBounds(offset, size, []);
|
|
61
|
+
const w = Number(size.x);
|
|
62
|
+
const h = Number(size.y);
|
|
63
|
+
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return null;
|
|
64
|
+
return { x: Number(offset.x) || 0, y: Number(offset.y) || 0, w, h };
|
|
65
|
+
} catch (e) {
|
|
66
|
+
onWarn && onWarn(`skeleton.bounds 失败:${e.message}`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 在"全部皮肤合并"的形态下取一次值,然后还原原皮肤与 setup pose。
|
|
73
|
+
* 为什么要合并:算 fit 包围盒前会把所有皮肤合成一个临时 skin,
|
|
74
|
+
* 否则只按默认皮肤测量,多皮肤模型的包围盒会偏小(实测 7 动作模型 reset 后 zoom 差 0.18%)。
|
|
75
|
+
* @param {object} ctx {skeleton, skeletonData}
|
|
76
|
+
* @param {Function} fn 测量回调
|
|
77
|
+
*/
|
|
78
|
+
function withAllSkins({ skeleton, skeletonData }, fn) {
|
|
79
|
+
const Skin = caps.skinCtor;
|
|
80
|
+
const skins = (skeletonData && skeletonData.skins) || [];
|
|
81
|
+
if (!Skin || !skeleton || skins.length <= 1) return fn();
|
|
82
|
+
let prev = null;
|
|
83
|
+
try {
|
|
84
|
+
prev = skeleton.skin;
|
|
85
|
+
const merged = new Skin('_all_skins_bounds');
|
|
86
|
+
for (const sk of skins) {
|
|
87
|
+
try { merged.addSkin(sk); } catch { /* 单个皮肤合并不了不影响其它 */ }
|
|
88
|
+
}
|
|
89
|
+
skeleton.setSkin(merged);
|
|
90
|
+
setupPose(skeleton);
|
|
91
|
+
updateWorldTransform(skeleton);
|
|
92
|
+
return fn();
|
|
93
|
+
} catch {
|
|
94
|
+
return fn();
|
|
95
|
+
} finally {
|
|
96
|
+
try {
|
|
97
|
+
skeleton.setSkin(prev || null);
|
|
98
|
+
setupPose(skeleton);
|
|
99
|
+
updateWorldTransform(skeleton);
|
|
100
|
+
} catch { /* 还原失败不影响结果 */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { attachmentLoader, readBinary, readJson, createRuntime, setupPose, updateWorldTransform, bounds, withAllSkins };
|
|
105
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// adapters/texture.js —— 纹理适配(创建 / 占位 / 释放 / 上传绑定)
|
|
2
|
+
//
|
|
3
|
+
// 业务层只调这里,不直接 `new GLTexture(...)`:不同版本可能是 GLTexture 或 Texture,
|
|
4
|
+
// 且有 bind()/setFilters() 与否的差异,全部收敛在本文件。
|
|
5
|
+
|
|
6
|
+
/** 1×1 透明画布(占位纹理的像素源) */
|
|
7
|
+
function blankCanvas() {
|
|
8
|
+
const c = document.createElement('canvas');
|
|
9
|
+
c.width = 1;
|
|
10
|
+
c.height = 1;
|
|
11
|
+
const ctx = c.getContext('2d');
|
|
12
|
+
if (ctx) ctx.clearRect(0, 0, 1, 1);
|
|
13
|
+
return c;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{caps: object, gl: WebGLRenderingContext, onWarn?: Function}} ctx
|
|
18
|
+
*/
|
|
19
|
+
export function createTextureAdapter({ caps, gl, onWarn }) {
|
|
20
|
+
const Ctor = caps.texture.glTextureCtor || caps.texture.textureCtor;
|
|
21
|
+
if (!Ctor) onWarn && onWarn('texture:运行时没有 GLTexture/Texture,纹理将无法创建');
|
|
22
|
+
|
|
23
|
+
/** 由图片源创建纹理 */
|
|
24
|
+
function create(image, premultiplied) {
|
|
25
|
+
if (!Ctor) return null;
|
|
26
|
+
return new Ctor(gl, image, false, premultiplied === true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 占位纹理(必须是与运行时同源的纹理对象:Spine 批处理器会对它调 bind) */
|
|
30
|
+
function placeholder() {
|
|
31
|
+
if (!Ctor) return null;
|
|
32
|
+
return new Ctor(gl, blankCanvas(), false, false);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 释放纹理(有 dispose 用 dispose,否则直接删 GL 纹理) */
|
|
36
|
+
function dispose(tex) {
|
|
37
|
+
if (!tex) return;
|
|
38
|
+
try {
|
|
39
|
+
if (caps.texture.hasDispose && typeof tex.dispose === 'function') { tex.dispose(); return; }
|
|
40
|
+
const handle = tex.glTexture || tex.texture;
|
|
41
|
+
if (handle && gl) gl.deleteTexture(handle);
|
|
42
|
+
} catch { /* 释放失败不影响后续 */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 显式绑定(批处理器内部也会绑;这里给"需要手动绑定"的路径用) */
|
|
46
|
+
function bind(tex) {
|
|
47
|
+
if (!tex || !gl) return false;
|
|
48
|
+
try {
|
|
49
|
+
if (caps.texture.hasBind && typeof tex.bind === 'function') { tex.bind(); return true; }
|
|
50
|
+
const handle = tex.glTexture || tex.texture;
|
|
51
|
+
if (!handle) {
|
|
52
|
+
onWarn && onWarn('texture.bind:纹理既没有 bind() 也没有 GL 句柄,已跳过绑定');
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
gl.bindTexture(gl.TEXTURE_2D, handle);
|
|
56
|
+
return true;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
onWarn && onWarn(`texture.bind 失败:${e.message}`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { create, placeholder, dispose, bind, caps: caps.texture };
|
|
64
|
+
}
|