modelstage 0.0.0-stage → 0.5.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +267 -2
- package/THIRD_PARTY_NOTICES.md +90 -0
- package/bin/modelstage.mjs +101 -0
- package/config/diag-upload.example.json +7 -0
- package/config/models/live2d.assetbundls.json +1 -0
- package/config/models/live2d.azurlane.json +1 -0
- package/config/models/live2d.azurlane_bundle.json +1 -0
- package/config/models/live2d.azurlane_spine.json +1 -0
- package/config/models/live2d.bd2.json +1 -0
- package/config/models/live2d.bluearchive.json +1 -0
- package/config/models/live2d.live2d.json +1 -0
- package/config/models/live2d.nikke.json +1 -0
- package/config/models/live2d.nikke_2.json +1 -0
- package/config/models/live2d.npc.json +1 -0
- package/config/models/spine.assetbundls.json +1 -0
- package/config/models/spine.azurlane.json +1 -0
- package/config/models/spine.azurlane_bundle.json +1 -0
- package/config/models/spine.azurlane_spine.json +1 -0
- package/config/models/spine.bd2.json +1 -0
- package/config/models/spine.bluearchive.json +1 -0
- package/config/models/spine.live2d.json +1 -0
- package/config/models/spine.nikke.json +1 -0
- package/config/models/spine.nikke_2.json +1 -0
- package/config/models/spine.npc.json +1 -0
- package/config/names/NIKKE.json +1058 -0
- package/config/names/README.md +47 -0
- package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
- package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
- package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
- package/config/plugins.json +13 -0
- package/config/resources.example.json +40 -0
- package/package.json +58 -4
- package/plugins/unity-live2d-bundle/README.md +98 -0
- package/plugins/unity-live2d-bundle/index.js +891 -0
- package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
- package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
- package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
- package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
- package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
- package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
- package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
- package/plugins/unity-live2d-bundle/lib/png.js +424 -0
- package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
- package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
- package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-live2d-bundle/plugin.json +81 -0
- package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
- package/plugins/unity-spine-bundle/index.js +535 -0
- package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
- package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
- package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
- package/plugins/unity-spine-bundle/lib/errors.js +79 -0
- package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
- package/plugins/unity-spine-bundle/lib/extract.js +242 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
- package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
- package/plugins/unity-spine-bundle/lib/png.js +424 -0
- package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
- package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
- package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
- package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
- package/plugins/unity-spine-bundle/plugin.json +75 -0
- package/src/server/README.md +69 -0
- package/src/server/assets/browse.mjs +56 -0
- package/src/server/assets/bundle-probe.mjs +94 -0
- package/src/server/assets/probe.mjs +184 -0
- package/src/server/core/config.mjs +173 -0
- package/src/server/core/env-num.mjs +60 -0
- package/src/server/core/env.mjs +49 -0
- package/src/server/core/fs-portable.mjs +37 -0
- package/src/server/core/log.mjs +107 -0
- package/src/server/core/paths.mjs +44 -0
- package/src/server/data/catalog.mjs +1091 -0
- package/src/server/data/entry-states.mjs +499 -0
- package/src/server/data/live2d.mjs +272 -0
- package/src/server/data/manifest-cache.mjs +58 -0
- package/src/server/data/manifest-stamp.mjs +135 -0
- package/src/server/data/model-meta.mjs +114 -0
- package/src/server/data/names.mjs +106 -0
- package/src/server/data/shape-probe-worker.mjs +33 -0
- package/src/server/data/shape-probe.mjs +626 -0
- package/src/server/data/spine.mjs +336 -0
- package/src/server/data/tree.mjs +126 -0
- package/src/server/http/atomic-json.mjs +145 -0
- package/src/server/http/background-solids.mjs +270 -0
- package/src/server/http/backgrounds.mjs +388 -0
- package/src/server/http/diag-upload.mjs +264 -0
- package/src/server/http/diag.mjs +173 -0
- package/src/server/http/ready.mjs +167 -0
- package/src/server/http/static.mjs +312 -0
- package/src/server/http/write.mjs +647 -0
- package/src/server/index.mjs +114 -0
- package/src/server/plugins/builtin-providers.json +30 -0
- package/src/server/plugins/registry.mjs +650 -0
- package/src/server/plugins/slot-catalog.mjs +286 -0
- package/src/server/routes/background-solids.mjs +56 -0
- package/src/server/routes/backgrounds.mjs +77 -0
- package/src/server/routes/config.mjs +38 -0
- package/src/server/routes/data.mjs +313 -0
- package/src/server/routes/diag.mjs +37 -0
- package/src/server/routes/entries.mjs +170 -0
- package/src/server/routes/ready.mjs +122 -0
- package/src/server/serve.mjs +334 -0
- package/src/server/unpack/capacity.mjs +217 -0
- package/src/server/unpack/mem-probe.mjs +27 -0
- package/src/server/unpack/plugin-runner.mjs +529 -0
- package/src/server/unpack/plugin-store.mjs +736 -0
- package/src/server/unpack/probe-store.mjs +73 -0
- package/src/server/unpack/routes.mjs +359 -0
- package/src/server/unpack/runtime/exec.mjs +558 -0
- package/src/server/unpack/runtime/index.mjs +141 -0
- package/src/server/unpack/runtime/node-vm.mjs +111 -0
- package/src/server/unpack/unpack-worker.mjs +105 -0
- package/src/server/unpack/worker-client.mjs +184 -0
- package/src/server/unpack/write-result.mjs +84 -0
- package/src/shared/bundle-shape.mjs +83 -0
- package/src/shared/entry-reasons.mjs +119 -0
- package/src/shared/names.mjs +45 -0
- package/src/web/README.md +193 -0
- package/src/web/app/anim-list.js +222 -0
- package/src/web/app/background-solids.js +150 -0
- package/src/web/app/backgrounds.js +152 -0
- package/src/web/app/camera-log.js +28 -0
- package/src/web/app/context.js +72 -0
- package/src/web/app/dir-picker.js +242 -0
- package/src/web/app/entry-report.js +71 -0
- package/src/web/app/memory-bars.js +91 -0
- package/src/web/app/memory-budget.js +228 -0
- package/src/web/app/memory-guard.js +204 -0
- package/src/web/app/model-actions.js +682 -0
- package/src/web/app/renderer-setting.js +70 -0
- package/src/web/app/root-manager.js +254 -0
- package/src/web/app/status.js +138 -0
- package/src/web/app/topbar-actions.js +357 -0
- package/src/web/app/transport-actions.js +297 -0
- package/src/web/app/ui-state.js +559 -0
- package/src/web/app/url-params.js +171 -0
- package/src/web/app/view-settings.js +186 -0
- package/src/web/components.css +465 -0
- package/src/web/debug/diag-hud.js +95 -0
- package/src/web/debug/drag.js +369 -0
- package/src/web/debug/log.js +320 -0
- package/src/web/debug/model-cmp.js +292 -0
- package/src/web/debug/panel.css +474 -0
- package/src/web/debug/panel.js +712 -0
- package/src/web/debug/race-probe.js +82 -0
- package/src/web/debug/upload.js +130 -0
- package/src/web/engine/adapters/atlas.js +47 -0
- package/src/web/engine/adapters/index.js +22 -0
- package/src/web/engine/adapters/renderer.js +223 -0
- package/src/web/engine/adapters/skeleton.js +105 -0
- package/src/web/engine/adapters/texture.js +64 -0
- package/src/web/engine/animation.js +181 -0
- package/src/web/engine/appearance.js +136 -0
- package/src/web/engine/camera-adapter.js +212 -0
- package/src/web/engine/effect-chain.js +138 -0
- package/src/web/engine/engine-state.js +120 -0
- package/src/web/engine/filters.js +224 -0
- package/src/web/engine/fit.js +197 -0
- package/src/web/engine/frame-gate.js +33 -0
- package/src/web/engine/gl-driver.js +56 -0
- package/src/web/engine/gl.js +77 -0
- package/src/web/engine/index.js +20 -0
- package/src/web/engine/loader.js +55 -0
- package/src/web/engine/model-info.js +26 -0
- package/src/web/engine/pipeline.js +200 -0
- package/src/web/engine/player.js +427 -0
- package/src/web/engine/pma/blend.js +76 -0
- package/src/web/engine/pma/detect.js +118 -0
- package/src/web/engine/pma/index.js +8 -0
- package/src/web/engine/pma/resolve.js +112 -0
- package/src/web/engine/pma/upload.js +127 -0
- package/src/web/engine/pma.js +5 -0
- package/src/web/engine/render-frame.js +93 -0
- package/src/web/engine/runtime-capabilities.js +209 -0
- package/src/web/engine/runtime.js +37 -0
- package/src/web/engine/skins.js +165 -0
- package/src/web/engine/textures.js +174 -0
- package/src/web/index.html +115 -0
- package/src/web/infra/api.js +531 -0
- package/src/web/infra/diag.js +509 -0
- package/src/web/infra/document.js +269 -0
- package/src/web/infra/dom.js +31 -0
- package/src/web/infra/events.mjs +27 -0
- package/src/web/infra/log.js +153 -0
- package/src/web/infra/mem-stats.js +112 -0
- package/src/web/infra/plugin-host-registration.mjs +285 -0
- package/src/web/infra/plugin-host-report.mjs +32 -0
- package/src/web/infra/plugin-host-unload.mjs +79 -0
- package/src/web/infra/plugin-host-unpack.mjs +899 -0
- package/src/web/infra/plugin-host-warmup.mjs +180 -0
- package/src/web/infra/plugin-host.js +628 -0
- package/src/web/infra/ready.js +461 -0
- package/src/web/infra/schedule.js +51 -0
- package/src/web/infra/shortcuts.js +129 -0
- package/src/web/infra/slot-registry.js +402 -0
- package/src/web/infra/vfs.js +247 -0
- package/src/web/live2d/bridge.js +400 -0
- package/src/web/live2d/model.js +321 -0
- package/src/web/live2d/overlay.js +142 -0
- package/src/web/live2d/pixi-app.js +111 -0
- package/src/web/live2d/playback-probe.js +92 -0
- package/src/web/live2d/probe-geometry.js +329 -0
- package/src/web/live2d/probe.js +119 -0
- package/src/web/live2d/release.js +66 -0
- package/src/web/live2d/runtime-adapter.js +289 -0
- package/src/web/live2d/stage-ctx.js +152 -0
- package/src/web/live2d/stage.js +204 -0
- package/src/web/live2d/vfs-settings.js +174 -0
- package/src/web/live2d/view.js +89 -0
- package/src/web/main.js +629 -0
- package/src/web/panel/background-list.js +373 -0
- package/src/web/panel/bg-source.js +291 -0
- package/src/web/panel/color-picker.css +199 -0
- package/src/web/panel/color-picker.js +278 -0
- package/src/web/panel/dialog.css +116 -0
- package/src/web/panel/dialog.js +173 -0
- package/src/web/panel/dom.js +35 -0
- package/src/web/panel/icons.js +58 -0
- package/src/web/panel/memory-bar.js +195 -0
- package/src/web/panel/panel-settings.js +1263 -0
- package/src/web/panel/panel.css +1304 -0
- package/src/web/panel/render-choice.js +104 -0
- package/src/web/panel/schema.js +784 -0
- package/src/web/panel/settings-io.js +291 -0
- package/src/web/panel/shell-controls.js +274 -0
- package/src/web/panel/slider.js +210 -0
- package/src/web/panel/toolbar-schema.js +111 -0
- package/src/web/panel/toolbar.js +195 -0
- package/src/web/panel/transport-picker.js +375 -0
- package/src/web/panel/transport.js +321 -0
- package/src/web/panel/view.js +553 -0
- package/src/web/plugin/unpack-worker.js +185 -0
- package/src/web/render/camera/constants.js +60 -0
- package/src/web/render/camera/index.js +123 -0
- package/src/web/render/camera/spine.js +124 -0
- package/src/web/render/capture.js +30 -0
- package/src/web/render/interaction.js +527 -0
- package/src/web/render/live2d.js +627 -0
- package/src/web/render/playback.js +47 -0
- package/src/web/render/reset-view.js +29 -0
- package/src/web/render/spine.js +386 -0
- package/src/web/render/surface-switch.js +176 -0
- package/src/web/render/surface.js +571 -0
- package/src/web/render/surfaces.js +70 -0
- package/src/web/roster/filter.js +31 -0
- package/src/web/roster/list.js +276 -0
- package/src/web/roster/persist.js +52 -0
- package/src/web/roster/reveal.js +77 -0
- package/src/web/roster/rows.js +248 -0
- package/src/web/roster/snapshot.js +37 -0
- package/src/web/roster/view.js +670 -0
- package/src/web/spine/canvas-api-groups.js +267 -0
- package/src/web/spine/canvas-api.js +97 -0
- package/src/web/spine/canvas-document.js +80 -0
- package/src/web/spine/canvas-engine.js +193 -0
- package/src/web/spine/canvas-entry.js +28 -0
- package/src/web/spine/canvas-pixels.js +36 -0
- package/src/web/spine/canvas-runtime.js +127 -0
- package/src/web/spine/canvas-view.js +108 -0
- package/src/web/spine/canvas.js +7 -0
- package/src/web/spine/runtime-hint.js +73 -0
- package/src/web/spine/runtimes.js +212 -0
- package/src/web/style.css +493 -0
- package/tool/lib/bundle-probe-node.mjs +121 -0
- package/tool/lib/check-perms.mjs +114 -0
- package/tool/lib/contract-checks.mjs +582 -0
- package/tool/lib/cubism-core.mjs +122 -0
- package/tool/lib/export-bundle.mjs +328 -0
- package/tool/lib/paths.mjs +20 -0
- package/tool/lib/runtime-guides.mjs +65 -0
- package/tool/lib/runtime-package.mjs +403 -0
- package/tool/lib/spine-meta.mjs +227 -0
- package/tool/lib/spine-motions.mjs +104 -0
- package/tool/lib/spine-runtime.mjs +131 -0
- package/tool/lib/test-lock.mjs +178 -0
- package/tool/lib/test-report.mjs +133 -0
- package/tool/parse-jsonc.mjs +86 -0
- package/tool/resolve-live2d-runtimes.mjs +276 -0
- package/tool/spine-motion-rules.json +101 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// tool/lib/cubism-core.mjs —— 在 Node 的 vm 沙箱里跑 Cubism Core(浏览器构建)
|
|
3
|
+
//
|
|
4
|
+
// 为什么需要:生成 motion3 需要 moc3 的**真实参数 Id**(Core 的 parameters.ids)。
|
|
5
|
+
// 纯字符串扫描(moc3NameCandidates)得到的候选名不等于参数 Id:实测用它建 hashLookup
|
|
6
|
+
// 会让绑定哈希全部认不出。Core 是浏览器包(Emscripten),所以要垫一个最小浏览器环境。
|
|
7
|
+
//
|
|
8
|
+
// 沙箱契约(与 spine-runtime.mjs 同思路):
|
|
9
|
+
// · 提供 document.currentScript / location / atob / 可应答 data: URI 的 XMLHttpRequest
|
|
10
|
+
// —— Core 的 Emscripten 外壳用它们取内嵌 base64 的 WASM 初始化数据
|
|
11
|
+
// · 不提供 process/require:避免它走 Node 分支去 readFileSync 一个 data: URI
|
|
12
|
+
// · 初始化时机:脚本 eval 完即完成(Emscripten 的 run() 在 eval 末尾),随后自检版本
|
|
13
|
+
// · 释放:用完必须 model.release() → moc._release()(顺序固定),本模块在 finally 里保证
|
|
14
|
+
//
|
|
15
|
+
// 内存:Core 是 WASM 堆,泄漏会累积。getParameterIds 每次调用都成对释放;
|
|
16
|
+
// 调用方连续处理多个包时 RSS 应保持平稳(导出 CLI 的实测值见文档)。
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import vm from 'node:vm';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
26
|
+
|
|
27
|
+
// Core 的候选路径(项目自带优先;也可用 CUBISM_CORE 指到别处)
|
|
28
|
+
const CORE_CANDIDATES = [
|
|
29
|
+
path.join(ROOT, 'tool', 'vendor', 'live2dcubismcore.min.js'),
|
|
30
|
+
process.env.CUBISM_CORE || '',
|
|
31
|
+
].filter(Boolean);
|
|
32
|
+
|
|
33
|
+
let sandboxCache = null;
|
|
34
|
+
|
|
35
|
+
/** 最小浏览器垫片 + 加载 Core。只做一次(Core 初始化 ~26ms)。 */
|
|
36
|
+
function getCore() {
|
|
37
|
+
if (sandboxCache) return sandboxCache;
|
|
38
|
+
const file = CORE_CANDIDATES.find((p) => fs.existsSync(p));
|
|
39
|
+
if (!file) throw new Error(`找不到 Cubism Core(试过:${CORE_CANDIDATES.join('、')})`);
|
|
40
|
+
|
|
41
|
+
// Core 的 WASM 初始化数据内嵌为 data: URI,Emscripten 外壳用 XHR 取;这里给个只认 data: 的桩
|
|
42
|
+
class DataUriXHR {
|
|
43
|
+
open(_method, url) { this.url = url; }
|
|
44
|
+
send() {
|
|
45
|
+
const m = /^data:([^;]*);base64,(.*)$/s.exec(String(this.url));
|
|
46
|
+
if (!m) throw new Error(`Cubism Core 请求了非 data: URI(${String(this.url).slice(0, 60)}),沙箱不提供网络`);
|
|
47
|
+
const buf = Buffer.from(m[2], 'base64');
|
|
48
|
+
this.response = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
49
|
+
this.responseText = buf.toString('latin1');
|
|
50
|
+
this.status = 200;
|
|
51
|
+
this.readyState = 4;
|
|
52
|
+
if (this.onload) this.onload();
|
|
53
|
+
}
|
|
54
|
+
addEventListener() {}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const ctx = {
|
|
58
|
+
console: { log() {}, info() {}, warn() {}, error() {}, debug() {} },
|
|
59
|
+
WebAssembly, TextDecoder, TextEncoder, ArrayBuffer, Math, Date,
|
|
60
|
+
setTimeout, clearTimeout,
|
|
61
|
+
atob: (s) => Buffer.from(s, 'base64').toString('binary'),
|
|
62
|
+
document: { currentScript: { src: 'http://localhost/live2dcubismcore.min.js' }, addEventListener() {} },
|
|
63
|
+
XMLHttpRequest: DataUriXHR,
|
|
64
|
+
location: { href: 'http://localhost/live2dcubismcore.min.js' },
|
|
65
|
+
};
|
|
66
|
+
ctx.window = ctx;
|
|
67
|
+
ctx.self = ctx;
|
|
68
|
+
ctx.globalThis = ctx;
|
|
69
|
+
vm.createContext(ctx);
|
|
70
|
+
vm.runInContext(fs.readFileSync(file, 'utf8'), ctx, { filename: 'live2dcubismcore.min.js' });
|
|
71
|
+
|
|
72
|
+
const L = ctx.Live2DCubismCore;
|
|
73
|
+
if (!L || !L.Moc || !L.Model) throw new Error('Cubism Core 加载后没有 Moc/Model 导出');
|
|
74
|
+
// 自检:csmGetVersion 返回编码版本(如 67239938 = 4.2.0 的编码),拿不到就说明沙箱里没跑起来
|
|
75
|
+
const version = L.Version && L.Version.csmGetVersion ? L.Version.csmGetVersion() : null;
|
|
76
|
+
if (!version) throw new Error('Cubism Core 自检失败:Version.csmGetVersion() 无返回');
|
|
77
|
+
sandboxCache = { L, version, file, coreEvalMs: 0 };
|
|
78
|
+
return sandboxCache;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Core 版本信息(自检/报告用) */
|
|
82
|
+
export function coreInfo() {
|
|
83
|
+
const { version, file } = getCore();
|
|
84
|
+
return { version, file, latestMocVersion: sandboxCache.L.Version.csmGetLatestMocVersion() };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 从 moc3 字节取**真实参数 Id**(用完立即释放 model 与 moc)。
|
|
89
|
+
* @param {Uint8Array} moc3
|
|
90
|
+
* @returns {{ids:string[], canvas:{width:number,height:number,ppu:number}, count:number}}
|
|
91
|
+
*/
|
|
92
|
+
export function getParameterIds(moc3) {
|
|
93
|
+
const { L } = getCore();
|
|
94
|
+
if (!moc3 || !moc3.length) throw new Error('moc3 为空');
|
|
95
|
+
// Core 需要 ArrayBuffer(不是 Node Buffer 切片)
|
|
96
|
+
const ab = moc3.buffer.slice(moc3.byteOffset, moc3.byteOffset + moc3.byteLength);
|
|
97
|
+
let moc = null;
|
|
98
|
+
let model = null;
|
|
99
|
+
try {
|
|
100
|
+
moc = L.Moc.fromArrayBuffer(ab);
|
|
101
|
+
if (!moc) throw new Error('Moc.fromArrayBuffer 返回 null(moc3 损坏或版本过新)');
|
|
102
|
+
model = L.Model.fromMoc(moc);
|
|
103
|
+
if (!model) throw new Error('Model.fromMoc 返回 null');
|
|
104
|
+
const ids = Array.from(model.parameters.ids);
|
|
105
|
+
const info = model.canvasinfo;
|
|
106
|
+
return {
|
|
107
|
+
ids,
|
|
108
|
+
count: model.parameters.count,
|
|
109
|
+
canvas: {
|
|
110
|
+
width: info.CanvasWidth / info.PixelsPerUnit,
|
|
111
|
+
height: info.CanvasHeight / info.PixelsPerUnit,
|
|
112
|
+
ppu: info.PixelsPerUnit,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
} finally {
|
|
116
|
+
// 释放顺序固定:先 model 再 moc;任一为 null 就跳过,绝不让异常吃掉释放
|
|
117
|
+
try { if (model) model.release(); } catch { /* 释放失败不掩盖原错误 */ }
|
|
118
|
+
try { if (moc) moc._release(); } catch { /* 同上 */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const CORE_PATHS = { ROOT, CORE_CANDIDATES };
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// tool/lib/export-bundle.mjs —— 把 UnityFS 资源包导出成「标准 Live2D 模型目录」
|
|
3
|
+
//
|
|
4
|
+
// 产出目录(与 AzurLane-Live2D-Viewer 等第三方查看器认可的形态一致):
|
|
5
|
+
// <out>/<资源名>/
|
|
6
|
+
// <资源名>.moc3
|
|
7
|
+
// <资源名>.physics3.json (包里没有就跳过)
|
|
8
|
+
// <资源名>.model3.json (FileReferences 指向同目录文件)
|
|
9
|
+
// textures/texture_XX.png (ETC2/DXT 解码 → RGBA → PNG 真压缩)
|
|
10
|
+
// export-report.json (耗时/字节/跳过/警告,便于批量核对)
|
|
11
|
+
//
|
|
12
|
+
// 为什么这样组织:
|
|
13
|
+
// · 解析/解码/编码全部复用插件里的 lib(同一份实现,避免第二套解析)
|
|
14
|
+
// · PNG 必须用 encodePngAsync(真 deflate);encodePng 是 stored 不压缩,4096² 会写 67MB
|
|
15
|
+
// · 先写 .tmp-<name>-<pid>/ 再 rename,失败/断电不留半成品
|
|
16
|
+
// · 阶段 1 **不导出 motion3**:生成 motion3 需要 moc3 的真实参数 Id(Cubism Core),
|
|
17
|
+
// 纯 Node 侧拿不到(实测用名字候选建立的 hashLookup 会让 466 条曲线全部 skippedUnknownBinding)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import vm from 'node:vm';
|
|
23
|
+
import zlib from 'node:zlib';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { getParameterIds, coreInfo } from './cubism-core.mjs';
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const ROOT = path.resolve(__dirname, '..', '..');
|
|
29
|
+
const LIB_DIR = path.join(ROOT, 'plugins', 'unity-live2d-bundle', 'lib');
|
|
30
|
+
|
|
31
|
+
// 依赖顺序:errors 最先,model3 最后(组装 model3.json)
|
|
32
|
+
const LIB_ORDER = [
|
|
33
|
+
'errors.js', 'bytes.js', 'common-strings.js', 'unityfs.js', 'serialized.js', 'typetree.js',
|
|
34
|
+
'extract.js', 'etc2.js', 'dxt.js', 'texdecode.js', 'png.js', 'anim-to-motion.js', 'model3.js',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
let sandbox = null;
|
|
38
|
+
/** 建(或取缓存的)插件库沙箱;PNG 的 deflate/inflate 用 Node 的 zlib 注入 */
|
|
39
|
+
function getLibs() {
|
|
40
|
+
if (sandbox) return sandbox;
|
|
41
|
+
const ctx = { console: { log() {}, info() {}, warn() {}, error() {} }, TextDecoder, TextEncoder, setTimeout, clearTimeout };
|
|
42
|
+
ctx.globalThis = ctx; ctx.window = ctx; ctx.self = ctx;
|
|
43
|
+
vm.createContext(ctx);
|
|
44
|
+
for (const rel of LIB_ORDER) {
|
|
45
|
+
const abs = path.join(LIB_DIR, rel);
|
|
46
|
+
if (!fs.existsSync(abs)) throw new Error(`导出依赖缺失:${abs}`);
|
|
47
|
+
vm.runInContext(fs.readFileSync(abs, 'utf8'), ctx, { filename: `ulb-lib/${rel}` });
|
|
48
|
+
}
|
|
49
|
+
const NS = ctx.__ULB__;
|
|
50
|
+
if (NS.png && NS.png.setDeflate) NS.png.setDeflate((u8) => new Uint8Array(zlib.deflateSync(Buffer.from(u8))));
|
|
51
|
+
if (NS.png && NS.png.setInflate) NS.png.setInflate((u8) => new Uint8Array(zlib.inflateSync(Buffer.from(u8))));
|
|
52
|
+
sandbox = NS;
|
|
53
|
+
return sandbox;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** 递归汇总目录字节数(报告用) */
|
|
57
|
+
function dirBytes(dir) {
|
|
58
|
+
let total = 0;
|
|
59
|
+
const walk = (d) => {
|
|
60
|
+
let es = [];
|
|
61
|
+
try { es = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
62
|
+
for (const e of es) {
|
|
63
|
+
const p = path.join(d, e.name);
|
|
64
|
+
if (e.isDirectory()) walk(p);
|
|
65
|
+
else { try { total += fs.statSync(p).size; } catch { /* 忽略单个文件 */ } }
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
walk(dir);
|
|
69
|
+
return total;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 解析资源包并规划导出(不写盘)。dryRun 也走这一步,保证"将要做什么"与真做一致。
|
|
74
|
+
* @returns {{name, textures:[{name,w,h,format,formatName,supported}], moc3Bytes, physicsBytes, warnings, skippedTextures}}
|
|
75
|
+
*/
|
|
76
|
+
export function planExport(bundleBytes) {
|
|
77
|
+
const NS = getLibs();
|
|
78
|
+
const E = NS.errors;
|
|
79
|
+
if (!bundleBytes || !bundleBytes.length) throw E.fail(E.CODES.NO_FILE, '资源包为空或不存在');
|
|
80
|
+
let sig = '';
|
|
81
|
+
for (let i = 0; i < Math.min(7, bundleBytes.length); i++) sig += String.fromCharCode(bundleBytes[i]);
|
|
82
|
+
if (sig !== 'UnityFS') throw E.fail(E.CODES.NOT_UNITYFS, `不是 UnityFS 资源包(signature="${sig.replace(/[^\x20-\x7e]/g, '?')}")`);
|
|
83
|
+
|
|
84
|
+
let parsed, sf, got;
|
|
85
|
+
try {
|
|
86
|
+
parsed = NS.unityfs.parse(bundleBytes);
|
|
87
|
+
const cab = parsed.nodes.find((n) => /^CAB-/.test(n.path));
|
|
88
|
+
if (!cab) throw E.fail(E.CODES.NO_CAB, '资源包里没有 CAB-* 节点(不是资产包)');
|
|
89
|
+
const resS = parsed.nodes.find((n) => /\.resS$/.test(n.path));
|
|
90
|
+
const cabBytes = NS.unityfs.nodeBytes(parsed, cab.path);
|
|
91
|
+
const resSBytes = resS ? NS.unityfs.nodeBytes(parsed, resS.path) : null;
|
|
92
|
+
sf = NS.serialized.parse(cabBytes);
|
|
93
|
+
got = NS.extract.extractAll({ cabBytes, resSBytes, sf, resSBytesForTextures: resSBytes });
|
|
94
|
+
} catch (e) {
|
|
95
|
+
if (e && e.code) throw e;
|
|
96
|
+
throw E.fail(E.CODES.CONTAINER_PARSE, `容器解析失败:${e.message}`);
|
|
97
|
+
}
|
|
98
|
+
if (!got.moc3) throw E.fail(E.CODES.MODEL_BUILD, '包里没有 moc3(不是 Live2D 资源包?)');
|
|
99
|
+
|
|
100
|
+
const textures = (got.textures || []).map((t) => {
|
|
101
|
+
const f = NS.texdecode.FORMATS[t.format];
|
|
102
|
+
return {
|
|
103
|
+
name: t.name, width: t.width, height: t.height, format: t.format,
|
|
104
|
+
formatName: f ? f.name : `未知格式 ${t.format}`,
|
|
105
|
+
supported: !!f,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
parsed, sf, got, textures,
|
|
110
|
+
moc3Bytes: got.moc3,
|
|
111
|
+
physicsBytes: got.physics3 || null,
|
|
112
|
+
warnings: [],
|
|
113
|
+
skippedTextures: textures.filter((t) => !t.supported).map((t) => t.formatName),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 执行导出。
|
|
119
|
+
* @param {object} opts
|
|
120
|
+
* bundleBytes 资源包字节 | outDir 输出根 | name 资源名 | force 覆盖 | dryRun 只规划
|
|
121
|
+
* profile 可选(传给 buildModel3,用于 HitAreas 等)
|
|
122
|
+
* onProgress({stage, ...})
|
|
123
|
+
*/
|
|
124
|
+
|
|
125
|
+
/** 最坏情况产物估算:PNG 压缩后体积 ≤ 原始 RGBA,用它做磁盘检查上界 */
|
|
126
|
+
function estimateBytes(plan) {
|
|
127
|
+
let raw = 0;
|
|
128
|
+
for (const t of plan.textures) if (t.supported) raw += t.width * t.height * 4;
|
|
129
|
+
return raw + plan.moc3Bytes.length + (plan.physicsBytes ? plan.physicsBytes.length : 0);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 磁盘余量检查:剩余 < 预估×1.5 直接抛 ULB_ERR_EXPORT_NO_SPACE;
|
|
134
|
+
* 查不到剩余空间(如跨设备/权限)只记 warning,不阻断导出。
|
|
135
|
+
*/
|
|
136
|
+
function checkDisk(NS, outRoot, estimate, warnings) {
|
|
137
|
+
try {
|
|
138
|
+
let probeDir = outRoot;
|
|
139
|
+
while (!fs.existsSync(probeDir) && path.dirname(probeDir) !== probeDir) probeDir = path.dirname(probeDir);
|
|
140
|
+
const st = fs.statfsSync(probeDir);
|
|
141
|
+
const free = st.bavail * st.bsize;
|
|
142
|
+
if (free < estimate * 1.5) {
|
|
143
|
+
throw NS.errors.fail(NS.errors.CODES.EXPORT_NO_SPACE, `磁盘空间不足:需要约 ${(estimate * 1.5 / 1048576).toFixed(0)} MB,剩余 ${(free / 1048576).toFixed(0)} MB`);
|
|
144
|
+
}
|
|
145
|
+
} catch (e) {
|
|
146
|
+
if (e && e.code === NS.errors.CODES.EXPORT_NO_SPACE) throw e;
|
|
147
|
+
warnings.push(`磁盘剩余空间检查失败(继续导出):${e.message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** 纹理:解码 → PNG(真压缩)→ 写盘;单张失败只记 warning,不中断整包 */
|
|
152
|
+
async function writeTextures(NS, plan, tmpDir, progress, warnings, written) {
|
|
153
|
+
const rels = [];
|
|
154
|
+
fs.mkdirSync(path.join(tmpDir, 'textures'), { recursive: true });
|
|
155
|
+
const resSNode = plan.parsed.nodes.find((n) => /\.resS$/.test(n.path));
|
|
156
|
+
const resSBytes = resSNode ? NS.unityfs.nodeBytes(plan.parsed, resSNode.path) : null;
|
|
157
|
+
for (const t of plan.textures) {
|
|
158
|
+
if (!t.supported) continue;
|
|
159
|
+
progress({ stage: 'texture', name: t.name, width: t.width, height: t.height });
|
|
160
|
+
let rgba;
|
|
161
|
+
try {
|
|
162
|
+
const texObj = plan.got.textures.find((x) => x.name === t.name);
|
|
163
|
+
rgba = NS.texdecode.decode(texObj, NS.extract.textureCompressedBytes(texObj, resSBytes), { flipY: true });
|
|
164
|
+
} catch (e) {
|
|
165
|
+
warnings.push(`纹理 ${t.name} 解码失败,已跳过:${e.message}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const png = await NS.png.encodePngAsync(rgba, t.width, t.height);
|
|
169
|
+
const rel = `textures/${t.name}.png`;
|
|
170
|
+
fs.writeFileSync(path.join(tmpDir, rel), png);
|
|
171
|
+
written.push({ rel, bytes: png.length });
|
|
172
|
+
rels.push(rel);
|
|
173
|
+
}
|
|
174
|
+
return rels;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* motion3:优先直接落包内原生 motion3;否则用 Cubism Core 的真实参数 Id 建白名单后转换。
|
|
179
|
+
* 转换逻辑全部复用 lib/anim-to-motion.js(含单点曲线补两点),不另写一套。
|
|
180
|
+
* @returns {{defs:Array<{name,url,group}>, curves:{total,emitted,skipped}|null, dropped:number, source:'native'|'converted'}}
|
|
181
|
+
*/
|
|
182
|
+
function writeMotions(NS, arg) {
|
|
183
|
+
const { plan, profile, tmpDir, written, progress } = arg;
|
|
184
|
+
const native = (plan.got.nativeFiles || []).filter((f) => /motion3\.json$/i.test(f.name));
|
|
185
|
+
if (native.length) {
|
|
186
|
+
fs.mkdirSync(path.join(tmpDir, 'motions'), { recursive: true });
|
|
187
|
+
const defs = [];
|
|
188
|
+
for (const f of native) {
|
|
189
|
+
const rel = `motions/${f.name}`;
|
|
190
|
+
fs.writeFileSync(path.join(tmpDir, rel), f.bytes);
|
|
191
|
+
written.push({ rel, bytes: f.bytes.length });
|
|
192
|
+
const base = f.name.replace(/\.motion3\.json$/i, '');
|
|
193
|
+
defs.push({ name: base, url: rel, group: NS.model3.motionGroupOf(base, profile) });
|
|
194
|
+
}
|
|
195
|
+
return { defs, curves: null, dropped: 0, source: 'native' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
progress({ stage: 'motions' });
|
|
199
|
+
const { ids } = getParameterIds(plan.moc3Bytes);
|
|
200
|
+
const candidates = [...new Set([...NS.extract.moc3NameCandidates(plan.moc3Bytes), ...ids])];
|
|
201
|
+
const opts = { paramWhitelist: new Set(ids), isLoop: (n) => NS.model3.motionLoops(n, profile) };
|
|
202
|
+
if (profile && profile.motion) {
|
|
203
|
+
opts.fadeIn = profile.motion.fadeInTime;
|
|
204
|
+
opts.fadeOut = profile.motion.fadeOutTime;
|
|
205
|
+
}
|
|
206
|
+
const conv = NS.animToMotion.clipsToMotions(plan.sf, candidates, opts);
|
|
207
|
+
fs.mkdirSync(path.join(tmpDir, 'motions'), { recursive: true });
|
|
208
|
+
const defs = [];
|
|
209
|
+
const curves = { total: 0, emitted: 0, skipped: 0 };
|
|
210
|
+
for (const m of conv.motions) {
|
|
211
|
+
const text = JSON.stringify(m.json);
|
|
212
|
+
const rel = `motions/${m.name}.motion3.json`;
|
|
213
|
+
fs.writeFileSync(path.join(tmpDir, rel), text);
|
|
214
|
+
written.push({ rel, bytes: Buffer.byteLength(text) });
|
|
215
|
+
defs.push({ name: m.name, url: rel, group: NS.model3.motionGroupOf(m.name, profile) });
|
|
216
|
+
curves.total += (m.stats.curvesTotal || 0);
|
|
217
|
+
curves.emitted += (m.stats.emitted || 0);
|
|
218
|
+
curves.skipped += (m.stats.skippedUnknownBinding || 0);
|
|
219
|
+
}
|
|
220
|
+
return { defs, curves, dropped: (conv.dropped || []).length, source: 'converted' };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** model3.json:Motions 由 writeMotions 的结果填充;空 Motions 会被删掉(避免部分运行时报错) */
|
|
224
|
+
function writeModel3(NS, arg) {
|
|
225
|
+
const { name, textureRels, plan, profile, tmpDir, written, motions } = arg;
|
|
226
|
+
const model3 = NS.model3.buildModel3({
|
|
227
|
+
mocUrl: `${name}.moc3`,
|
|
228
|
+
textureUrls: textureRels,
|
|
229
|
+
physicsUrl: plan.physicsBytes ? `${name}.physics3.json` : null,
|
|
230
|
+
motions: motions || [],
|
|
231
|
+
paramNames: [],
|
|
232
|
+
profile: profile || null,
|
|
233
|
+
});
|
|
234
|
+
const refs = model3.FileReferences;
|
|
235
|
+
if (refs && refs.Motions && !Object.keys(refs.Motions).length) delete refs.Motions;
|
|
236
|
+
const text = JSON.stringify(model3, null, 2);
|
|
237
|
+
fs.writeFileSync(path.join(tmpDir, `${name}.model3.json`), text);
|
|
238
|
+
written.push({ rel: `${name}.model3.json`, bytes: Buffer.byteLength(text) });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** 报告落盘 + 原子落位(先 rename 到最终目录,保证读者永远看不到半成品) */
|
|
242
|
+
function finalize(arg) {
|
|
243
|
+
const { name, bundleBytes, t0, written, plan, warnings, tmpDir, finalDir, motions } = arg;
|
|
244
|
+
const report = {
|
|
245
|
+
name, bundleBytes, generatedAt: new Date().toISOString(), tookMs: Date.now() - t0,
|
|
246
|
+
files: written, totalBytes: written.reduce((a, f) => a + f.bytes, 0),
|
|
247
|
+
textures: plan.textures,
|
|
248
|
+
motions: {
|
|
249
|
+
count: (motions.defs || []).length,
|
|
250
|
+
source: motions.source,
|
|
251
|
+
dropped: motions.dropped || 0,
|
|
252
|
+
curves: motions.curves,
|
|
253
|
+
groups: [...new Set((motions.defs || []).map((d) => d.group))],
|
|
254
|
+
},
|
|
255
|
+
cubismCore: coreInfo(),
|
|
256
|
+
warnings,
|
|
257
|
+
};
|
|
258
|
+
const text = JSON.stringify(report, null, 2);
|
|
259
|
+
fs.writeFileSync(path.join(tmpDir, 'export-report.json'), text);
|
|
260
|
+
written.push({ rel: 'export-report.json', bytes: Buffer.byteLength(text) });
|
|
261
|
+
if (fs.existsSync(finalDir)) fs.rmSync(finalDir, { recursive: true, force: true }); // 仅 --force 会走到这
|
|
262
|
+
fs.renameSync(tmpDir, finalDir);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 执行导出(编排:规划 → 磁盘 → dry-run → 已存在 → 写临时目录 → 原子落位)。
|
|
267
|
+
* 具体写盘工作交给 writeTextures / writeMotions / writeModel3 / finalize。
|
|
268
|
+
*/
|
|
269
|
+
export async function exportBundle(opts) {
|
|
270
|
+
const NS = getLibs();
|
|
271
|
+
const o = opts || {};
|
|
272
|
+
const t0 = Date.now();
|
|
273
|
+
const progress = typeof o.onProgress === 'function' ? o.onProgress : () => {};
|
|
274
|
+
const name = String(o.name || '').trim();
|
|
275
|
+
if (!name) throw new Error('缺少资源名(name)');
|
|
276
|
+
const outRoot = path.resolve(o.outDir || path.join(ROOT, 'data', 'export'));
|
|
277
|
+
const finalDir = path.join(outRoot, name);
|
|
278
|
+
|
|
279
|
+
progress({ stage: 'plan' });
|
|
280
|
+
const plan = planExport(o.bundleBytes);
|
|
281
|
+
const warnings = [...plan.warnings];
|
|
282
|
+
if (plan.skippedTextures.length) {
|
|
283
|
+
warnings.push(`以下纹理格式暂不支持,已跳过(不整体失败):${[...new Set(plan.skippedTextures)].join('、')}`);
|
|
284
|
+
}
|
|
285
|
+
const estimate = estimateBytes(plan);
|
|
286
|
+
checkDisk(NS, outRoot, estimate, warnings);
|
|
287
|
+
const textures = plan.textures.map((t) => ({ name: t.name, width: t.width, height: t.height, formatName: t.formatName, supported: t.supported }));
|
|
288
|
+
|
|
289
|
+
if (o.dryRun) {
|
|
290
|
+
return {
|
|
291
|
+
ok: true, dryRun: true, name, outDir: finalDir, existed: fs.existsSync(finalDir),
|
|
292
|
+
wouldWrite: [`${name}.moc3`, ...(plan.physicsBytes ? [`${name}.physics3.json`] : []),
|
|
293
|
+
...plan.textures.filter((t) => t.supported).map((t) => `textures/${t.name}.png`),
|
|
294
|
+
'motions/<动作名>.motion3.json', `${name}.model3.json`, 'export-report.json'],
|
|
295
|
+
textures, estimateBytes: estimate, warnings, ms: Date.now() - t0,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (fs.existsSync(finalDir) && !o.force) {
|
|
299
|
+
return { ok: true, skipped: true, reason: '目标目录已存在(加 --force 覆盖)', name, outDir: finalDir, textures, warnings, ms: Date.now() - t0 };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fs.mkdirSync(outRoot, { recursive: true });
|
|
303
|
+
const tmpDir = path.join(outRoot, `.tmp-${name}-${process.pid}`);
|
|
304
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
305
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
306
|
+
const written = [];
|
|
307
|
+
try {
|
|
308
|
+
fs.writeFileSync(path.join(tmpDir, `${name}.moc3`), plan.moc3Bytes);
|
|
309
|
+
written.push({ rel: `${name}.moc3`, bytes: plan.moc3Bytes.length });
|
|
310
|
+
if (plan.physicsBytes) {
|
|
311
|
+
fs.writeFileSync(path.join(tmpDir, `${name}.physics3.json`), plan.physicsBytes);
|
|
312
|
+
written.push({ rel: `${name}.physics3.json`, bytes: plan.physicsBytes.length });
|
|
313
|
+
}
|
|
314
|
+
const rels = await writeTextures(NS, plan, tmpDir, progress, warnings, written);
|
|
315
|
+
const motions = writeMotions(NS, { plan, profile: o.profile, tmpDir, written, progress });
|
|
316
|
+
writeModel3(NS, { name, textureRels: rels, plan, profile: o.profile, tmpDir, written, motions: motions.defs });
|
|
317
|
+
finalize({ name, bundleBytes: o.bundleBytes.length, t0, written, plan, warnings, tmpDir, finalDir, motions });
|
|
318
|
+
return {
|
|
319
|
+
ok: true, name, outDir: finalDir, files: written.length, totalBytes: dirBytes(finalDir),
|
|
320
|
+
tookMs: Date.now() - t0, textures, motions: motions.defs.length, curves: motions.curves, warnings,
|
|
321
|
+
};
|
|
322
|
+
} catch (e) {
|
|
323
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* 清理失败不掩盖原错误 */ }
|
|
324
|
+
throw e;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export const EXPORT_PATHS = { ROOT, LIB_DIR, LIB_ORDER };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// tool/lib/paths.mjs —— 脚本里反复用到的"本文件位置"计算
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成一个模块:`path.dirname(fileURLToPath(import.meta.url))` 这一行在 6 个脚本里
|
|
4
|
+
// 各写了一遍(bin/modelstage.mjs 与 tool/ 下的 5 个)。它是纯机械代码,复制越多越容易出错 ——
|
|
5
|
+
// 常见的错法是漏掉 fileURLToPath,于是 HERE 变成 "file:///…" 字符串,
|
|
6
|
+
// 后面所有 path.join 都会拼出怪路径,而报错要等到读文件时才出现。
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 取「本文件所在目录」的绝对路径(ESM 没有 __dirname)。
|
|
12
|
+
*
|
|
13
|
+
* 注意它返回的是**调用方自己**的目录:bin/ 下调用得到 <root>/bin,tool/ 下得到 <root>/tool。
|
|
14
|
+
* 所以只统一写法,不统一返回值 —— 想把两者换算成项目根,各调用点自己 path.resolve(HERE, '..')。
|
|
15
|
+
* @param {string} metaUrl 调用方传 `import.meta.url`
|
|
16
|
+
* @returns {string} 目录绝对路径
|
|
17
|
+
*/
|
|
18
|
+
export function here(metaUrl) {
|
|
19
|
+
return path.dirname(fileURLToPath(metaUrl));
|
|
20
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// tool/lib/runtime-guides.mjs —— 缺运行时时的**引导文案**(3.x / Live2D 只引导,不自取)
|
|
3
|
+
//
|
|
4
|
+
// 为什么单独一个文件:这些是"给用户看的文本",与取包/校验逻辑无关;
|
|
5
|
+
// 同一段文案要同时被 CLI(终端)与 resolver(错误消息,会进调试日志)使用,
|
|
6
|
+
// 放独立文件后两边 import 同一份 —— 各写一套必然出现"终端说 A、日志说 B"。
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { MIN_VERSION, runtimesRoot } from './runtime-package.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Spine 3.x 的获取引导。
|
|
14
|
+
*
|
|
15
|
+
* 为什么不是"去 GitHub releases 下载":实测该 repo **releases 数为 0**,
|
|
16
|
+
* 用户按要求去只会看到空页面。可用的是 tag 下的 build 产物(实测 3.6/3.7/3.8 均 200)。
|
|
17
|
+
* @param {string} version
|
|
18
|
+
* @param {string} dataRoot
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function spine3Guide(version, dataRoot) {
|
|
22
|
+
const v = version || '3.8';
|
|
23
|
+
const dir = path.join(runtimesRoot(dataRoot), 'spine', v);
|
|
24
|
+
const url = `https://raw.githubusercontent.com/EsotericSoftware/spine-runtimes/${v}/spine-ts/build/spine-webgl.js`;
|
|
25
|
+
return [
|
|
26
|
+
`缺少 Spine ${v} 运行时。官方 npm 未发布 3.x(最早只有 ${MIN_VERSION}),只能自备:`,
|
|
27
|
+
` mkdir -p "${dir}"`,
|
|
28
|
+
` curl -L -o "${path.join(dir, `spine-webgl-${v}.js`)}" \\`,
|
|
29
|
+
` ${url}`,
|
|
30
|
+
' (该文件是"挂全局 spine"的构建,可以直接被引擎加载;上面已按目标名保存,无需再改名)',
|
|
31
|
+
` 也可以把已有的 spine-webgl.js 放到该目录并命名为 spine-webgl-${v}.js,或设 SPINE_RUNTIME_DIR`,
|
|
32
|
+
'提示:4.x 可以直接用本工具下载(node tool/fetch-runtimes.mjs --spine 4.1),3.x 官方没有 npm 包。',
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Live2D Cubism Core 的获取引导(**只引导,绝不代下**)。
|
|
38
|
+
* @param {string} dataRoot
|
|
39
|
+
* @param {string} [verDir] 目标版本目录名(如 v6-6.5.3)
|
|
40
|
+
* @returns {string}
|
|
41
|
+
*/
|
|
42
|
+
export function live2dGuide(dataRoot, verDir) {
|
|
43
|
+
const dir = path.join(runtimesRoot(dataRoot), 'live2d', verDir || '<版本目录>');
|
|
44
|
+
return [
|
|
45
|
+
'缺少 Live2D Cubism Core 运行时。请到官网下载:',
|
|
46
|
+
' https://www.live2d.com/en/sdk/download/web/',
|
|
47
|
+
`把下载到的主库文件放到:${dir}`,
|
|
48
|
+
' 需要 live2dcubismcore.js(以及 pixi.min.js / index.min.js,若使用 pixi-live2d-display)',
|
|
49
|
+
'本工具不会代下 Live2D:官方下载需要你本人同意专有软件许可,代替下载等于绕过该环节。',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 打印用的许可提示(每次下载前都要让用户看见)。
|
|
55
|
+
* @param {string} version
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
export function licenseNotice(version) {
|
|
59
|
+
return [
|
|
60
|
+
`Spine ${version} 运行时来自官方 npm 包 @esotericsoftware/spine-webgl。`,
|
|
61
|
+
'许可:Spine Runtimes License Agreement(Esoteric Software 自定义许可,**非 MIT**)。',
|
|
62
|
+
' · 使用即表示你同意该许可;商用需要 Spine 授权(详见落地目录里的 LICENSE)。',
|
|
63
|
+
' · 本工具只是替你从上游取,不托管、不再分发任何运行时二进制。',
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|