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.
Files changed (281) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -2
  3. package/THIRD_PARTY_NOTICES.md +90 -0
  4. package/bin/modelstage.mjs +137 -0
  5. package/config/diag-upload.example.json +7 -0
  6. package/config/models/live2d.assetbundls.json +1 -0
  7. package/config/models/live2d.azurlane.json +1 -0
  8. package/config/models/live2d.azurlane_bundle.json +1 -0
  9. package/config/models/live2d.azurlane_spine.json +1 -0
  10. package/config/models/live2d.bd2.json +1 -0
  11. package/config/models/live2d.bluearchive.json +1 -0
  12. package/config/models/live2d.live2d.json +1 -0
  13. package/config/models/live2d.nikke.json +1 -0
  14. package/config/models/live2d.nikke_2.json +1 -0
  15. package/config/models/live2d.npc.json +1 -0
  16. package/config/models/spine.assetbundls.json +1 -0
  17. package/config/models/spine.azurlane.json +1 -0
  18. package/config/models/spine.azurlane_bundle.json +1 -0
  19. package/config/models/spine.azurlane_spine.json +1 -0
  20. package/config/models/spine.bd2.json +1 -0
  21. package/config/models/spine.bluearchive.json +1 -0
  22. package/config/models/spine.live2d.json +1 -0
  23. package/config/models/spine.nikke.json +1 -0
  24. package/config/models/spine.nikke_2.json +1 -0
  25. package/config/models/spine.npc.json +1 -0
  26. package/config/names/NIKKE.json +1058 -0
  27. package/config/names/README.md +47 -0
  28. package/config/names//345/244/251/345/221/275/344/271/213/345/255/220.json +42897 -0
  29. package/config/names//346/243/225/350/211/262/345/260/230/345/237/2032.json +4016 -0
  30. package/config/names//347/242/247/350/223/235/350/210/252/347/272/277.json +38342 -0
  31. package/config/plugins.json +13 -0
  32. package/config/resources.example.json +40 -0
  33. package/package.json +63 -4
  34. package/plugins/unity-live2d-bundle/README.md +98 -0
  35. package/plugins/unity-live2d-bundle/index.js +891 -0
  36. package/plugins/unity-live2d-bundle/lib/anim-to-motion.js +402 -0
  37. package/plugins/unity-live2d-bundle/lib/bundle-probe.js +244 -0
  38. package/plugins/unity-live2d-bundle/lib/bytes.js +189 -0
  39. package/plugins/unity-live2d-bundle/lib/common-strings.js +46 -0
  40. package/plugins/unity-live2d-bundle/lib/dxt.js +197 -0
  41. package/plugins/unity-live2d-bundle/lib/errors.js +71 -0
  42. package/plugins/unity-live2d-bundle/lib/etc2.js +371 -0
  43. package/plugins/unity-live2d-bundle/lib/extract.js +242 -0
  44. package/plugins/unity-live2d-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  45. package/plugins/unity-live2d-bundle/lib/lzma-d-min.js +34 -0
  46. package/plugins/unity-live2d-bundle/lib/model3.js +137 -0
  47. package/plugins/unity-live2d-bundle/lib/png.js +424 -0
  48. package/plugins/unity-live2d-bundle/lib/serialized.js +233 -0
  49. package/plugins/unity-live2d-bundle/lib/texdecode.js +190 -0
  50. package/plugins/unity-live2d-bundle/lib/typetree.js +215 -0
  51. package/plugins/unity-live2d-bundle/lib/unityfs.js +396 -0
  52. package/plugins/unity-live2d-bundle/plugin.json +81 -0
  53. package/plugins/unity-live2d-bundle/profiles/unityfs-live2d.json +26 -0
  54. package/plugins/unity-spine-bundle/index.js +535 -0
  55. package/plugins/unity-spine-bundle/lib/bytes.js +189 -0
  56. package/plugins/unity-spine-bundle/lib/common-strings.js +46 -0
  57. package/plugins/unity-spine-bundle/lib/dxt.js +197 -0
  58. package/plugins/unity-spine-bundle/lib/errors.js +79 -0
  59. package/plugins/unity-spine-bundle/lib/etc2.js +371 -0
  60. package/plugins/unity-spine-bundle/lib/extract.js +242 -0
  61. package/plugins/unity-spine-bundle/lib/lzma-d-min.LICENSE.txt +21 -0
  62. package/plugins/unity-spine-bundle/lib/lzma-d-min.js +34 -0
  63. package/plugins/unity-spine-bundle/lib/png.js +424 -0
  64. package/plugins/unity-spine-bundle/lib/serialized.js +233 -0
  65. package/plugins/unity-spine-bundle/lib/texdecode.js +190 -0
  66. package/plugins/unity-spine-bundle/lib/typetree.js +215 -0
  67. package/plugins/unity-spine-bundle/lib/unityfs.js +396 -0
  68. package/plugins/unity-spine-bundle/plugin.json +75 -0
  69. package/src/server/README.md +69 -0
  70. package/src/server/assets/browse.mjs +56 -0
  71. package/src/server/assets/bundle-probe.mjs +94 -0
  72. package/src/server/assets/probe.mjs +184 -0
  73. package/src/server/core/config.mjs +173 -0
  74. package/src/server/core/env-num.mjs +60 -0
  75. package/src/server/core/env.mjs +49 -0
  76. package/src/server/core/fs-portable.mjs +37 -0
  77. package/src/server/core/log.mjs +107 -0
  78. package/src/server/core/paths.mjs +44 -0
  79. package/src/server/data/catalog.mjs +1091 -0
  80. package/src/server/data/entry-states.mjs +499 -0
  81. package/src/server/data/live2d.mjs +272 -0
  82. package/src/server/data/manifest-cache.mjs +58 -0
  83. package/src/server/data/manifest-stamp.mjs +135 -0
  84. package/src/server/data/model-meta.mjs +114 -0
  85. package/src/server/data/names.mjs +106 -0
  86. package/src/server/data/shape-probe-worker.mjs +33 -0
  87. package/src/server/data/shape-probe.mjs +626 -0
  88. package/src/server/data/spine.mjs +336 -0
  89. package/src/server/data/tree.mjs +126 -0
  90. package/src/server/http/atomic-json.mjs +145 -0
  91. package/src/server/http/background-solids.mjs +270 -0
  92. package/src/server/http/backgrounds.mjs +388 -0
  93. package/src/server/http/diag-upload.mjs +264 -0
  94. package/src/server/http/diag.mjs +173 -0
  95. package/src/server/http/ready.mjs +167 -0
  96. package/src/server/http/static.mjs +312 -0
  97. package/src/server/http/write.mjs +654 -0
  98. package/src/server/index.mjs +114 -0
  99. package/src/server/plugins/builtin-providers.json +30 -0
  100. package/src/server/plugins/registry.mjs +650 -0
  101. package/src/server/plugins/slot-catalog.mjs +286 -0
  102. package/src/server/routes/background-solids.mjs +56 -0
  103. package/src/server/routes/backgrounds.mjs +77 -0
  104. package/src/server/routes/config.mjs +38 -0
  105. package/src/server/routes/data.mjs +313 -0
  106. package/src/server/routes/diag.mjs +37 -0
  107. package/src/server/routes/entries.mjs +170 -0
  108. package/src/server/routes/ready.mjs +122 -0
  109. package/src/server/serve.mjs +396 -0
  110. package/src/server/unpack/capacity.mjs +217 -0
  111. package/src/server/unpack/mem-probe.mjs +27 -0
  112. package/src/server/unpack/plugin-runner.mjs +529 -0
  113. package/src/server/unpack/plugin-store.mjs +736 -0
  114. package/src/server/unpack/probe-store.mjs +73 -0
  115. package/src/server/unpack/routes.mjs +359 -0
  116. package/src/server/unpack/runtime/exec.mjs +558 -0
  117. package/src/server/unpack/runtime/index.mjs +141 -0
  118. package/src/server/unpack/runtime/node-vm.mjs +111 -0
  119. package/src/server/unpack/unpack-worker.mjs +105 -0
  120. package/src/server/unpack/worker-client.mjs +184 -0
  121. package/src/server/unpack/write-result.mjs +84 -0
  122. package/src/shared/bundle-shape.mjs +83 -0
  123. package/src/shared/entry-reasons.mjs +119 -0
  124. package/src/shared/names.mjs +45 -0
  125. package/src/shared/type-labels.mjs +31 -0
  126. package/src/web/README.md +193 -0
  127. package/src/web/app/anim-list.js +222 -0
  128. package/src/web/app/background-solids.js +150 -0
  129. package/src/web/app/backgrounds.js +152 -0
  130. package/src/web/app/camera-log.js +28 -0
  131. package/src/web/app/context.js +72 -0
  132. package/src/web/app/dir-picker.js +242 -0
  133. package/src/web/app/entry-report.js +71 -0
  134. package/src/web/app/memory-bars.js +91 -0
  135. package/src/web/app/memory-budget.js +228 -0
  136. package/src/web/app/memory-guard.js +204 -0
  137. package/src/web/app/model-actions.js +682 -0
  138. package/src/web/app/renderer-setting.js +70 -0
  139. package/src/web/app/root-manager.js +254 -0
  140. package/src/web/app/status.js +138 -0
  141. package/src/web/app/topbar-actions.js +357 -0
  142. package/src/web/app/transport-actions.js +297 -0
  143. package/src/web/app/ui-state.js +559 -0
  144. package/src/web/app/url-params.js +171 -0
  145. package/src/web/app/view-settings.js +186 -0
  146. package/src/web/components.css +524 -0
  147. package/src/web/debug/diag-hud.js +95 -0
  148. package/src/web/debug/drag.js +369 -0
  149. package/src/web/debug/log.js +320 -0
  150. package/src/web/debug/model-cmp.js +292 -0
  151. package/src/web/debug/panel.css +474 -0
  152. package/src/web/debug/panel.js +712 -0
  153. package/src/web/debug/race-probe.js +82 -0
  154. package/src/web/debug/upload.js +130 -0
  155. package/src/web/engine/adapters/atlas.js +47 -0
  156. package/src/web/engine/adapters/index.js +22 -0
  157. package/src/web/engine/adapters/renderer.js +223 -0
  158. package/src/web/engine/adapters/skeleton.js +105 -0
  159. package/src/web/engine/adapters/texture.js +64 -0
  160. package/src/web/engine/animation.js +181 -0
  161. package/src/web/engine/appearance.js +136 -0
  162. package/src/web/engine/camera-adapter.js +212 -0
  163. package/src/web/engine/effect-chain.js +138 -0
  164. package/src/web/engine/engine-state.js +120 -0
  165. package/src/web/engine/filters.js +224 -0
  166. package/src/web/engine/fit.js +197 -0
  167. package/src/web/engine/frame-gate.js +33 -0
  168. package/src/web/engine/gl-driver.js +56 -0
  169. package/src/web/engine/gl.js +77 -0
  170. package/src/web/engine/index.js +20 -0
  171. package/src/web/engine/loader.js +55 -0
  172. package/src/web/engine/model-info.js +26 -0
  173. package/src/web/engine/pipeline.js +200 -0
  174. package/src/web/engine/player.js +427 -0
  175. package/src/web/engine/pma/blend.js +76 -0
  176. package/src/web/engine/pma/detect.js +118 -0
  177. package/src/web/engine/pma/index.js +8 -0
  178. package/src/web/engine/pma/resolve.js +112 -0
  179. package/src/web/engine/pma/upload.js +127 -0
  180. package/src/web/engine/pma.js +5 -0
  181. package/src/web/engine/render-frame.js +93 -0
  182. package/src/web/engine/runtime-capabilities.js +209 -0
  183. package/src/web/engine/runtime.js +37 -0
  184. package/src/web/engine/skins.js +165 -0
  185. package/src/web/engine/textures.js +174 -0
  186. package/src/web/index.html +117 -0
  187. package/src/web/infra/api.js +531 -0
  188. package/src/web/infra/diag.js +509 -0
  189. package/src/web/infra/document.js +269 -0
  190. package/src/web/infra/dom.js +31 -0
  191. package/src/web/infra/events.mjs +27 -0
  192. package/src/web/infra/log.js +153 -0
  193. package/src/web/infra/mem-stats.js +112 -0
  194. package/src/web/infra/plugin-host-registration.mjs +285 -0
  195. package/src/web/infra/plugin-host-report.mjs +32 -0
  196. package/src/web/infra/plugin-host-unload.mjs +79 -0
  197. package/src/web/infra/plugin-host-unpack.mjs +899 -0
  198. package/src/web/infra/plugin-host-warmup.mjs +180 -0
  199. package/src/web/infra/plugin-host.js +628 -0
  200. package/src/web/infra/ready.js +461 -0
  201. package/src/web/infra/schedule.js +51 -0
  202. package/src/web/infra/shortcuts.js +129 -0
  203. package/src/web/infra/slot-registry.js +402 -0
  204. package/src/web/infra/vfs.js +247 -0
  205. package/src/web/live2d/bridge.js +400 -0
  206. package/src/web/live2d/model.js +321 -0
  207. package/src/web/live2d/overlay.js +142 -0
  208. package/src/web/live2d/pixi-app.js +111 -0
  209. package/src/web/live2d/playback-probe.js +92 -0
  210. package/src/web/live2d/probe-geometry.js +329 -0
  211. package/src/web/live2d/probe.js +119 -0
  212. package/src/web/live2d/release.js +66 -0
  213. package/src/web/live2d/runtime-adapter.js +289 -0
  214. package/src/web/live2d/stage-ctx.js +152 -0
  215. package/src/web/live2d/stage.js +204 -0
  216. package/src/web/live2d/vfs-settings.js +174 -0
  217. package/src/web/live2d/view.js +89 -0
  218. package/src/web/main.js +629 -0
  219. package/src/web/panel/background-list.js +373 -0
  220. package/src/web/panel/bg-source.js +291 -0
  221. package/src/web/panel/color-picker.css +199 -0
  222. package/src/web/panel/color-picker.js +278 -0
  223. package/src/web/panel/dialog.css +116 -0
  224. package/src/web/panel/dialog.js +173 -0
  225. package/src/web/panel/dom.js +35 -0
  226. package/src/web/panel/icons.js +58 -0
  227. package/src/web/panel/memory-bar.js +195 -0
  228. package/src/web/panel/panel-settings.js +1263 -0
  229. package/src/web/panel/panel.css +1304 -0
  230. package/src/web/panel/render-choice.js +104 -0
  231. package/src/web/panel/schema.js +784 -0
  232. package/src/web/panel/settings-io.js +291 -0
  233. package/src/web/panel/shell-controls.js +274 -0
  234. package/src/web/panel/slider.js +210 -0
  235. package/src/web/panel/toolbar-schema.js +111 -0
  236. package/src/web/panel/toolbar.js +195 -0
  237. package/src/web/panel/transport-picker.js +375 -0
  238. package/src/web/panel/transport.js +321 -0
  239. package/src/web/panel/view.js +553 -0
  240. package/src/web/plugin/unpack-worker.js +185 -0
  241. package/src/web/render/camera/constants.js +60 -0
  242. package/src/web/render/camera/index.js +123 -0
  243. package/src/web/render/camera/spine.js +124 -0
  244. package/src/web/render/capture.js +30 -0
  245. package/src/web/render/interaction.js +527 -0
  246. package/src/web/render/live2d.js +627 -0
  247. package/src/web/render/playback.js +47 -0
  248. package/src/web/render/reset-view.js +29 -0
  249. package/src/web/render/spine.js +386 -0
  250. package/src/web/render/surface-switch.js +176 -0
  251. package/src/web/render/surface.js +571 -0
  252. package/src/web/render/surfaces.js +70 -0
  253. package/src/web/roster/filter.js +31 -0
  254. package/src/web/roster/list.js +276 -0
  255. package/src/web/roster/persist.js +92 -0
  256. package/src/web/roster/reveal.js +77 -0
  257. package/src/web/roster/rows.js +248 -0
  258. package/src/web/roster/snapshot.js +39 -0
  259. package/src/web/roster/type-filter.js +97 -0
  260. package/src/web/roster/view.js +761 -0
  261. package/src/web/spine/canvas-api-groups.js +267 -0
  262. package/src/web/spine/canvas-api.js +97 -0
  263. package/src/web/spine/canvas-document.js +80 -0
  264. package/src/web/spine/canvas-engine.js +193 -0
  265. package/src/web/spine/canvas-entry.js +28 -0
  266. package/src/web/spine/canvas-pixels.js +36 -0
  267. package/src/web/spine/canvas-runtime.js +127 -0
  268. package/src/web/spine/canvas-view.js +108 -0
  269. package/src/web/spine/canvas.js +7 -0
  270. package/src/web/spine/runtime-hint.js +73 -0
  271. package/src/web/spine/runtimes.js +212 -0
  272. package/src/web/style.css +493 -0
  273. package/tool/lib/bundle-probe-node.mjs +121 -0
  274. package/tool/lib/paths.mjs +20 -0
  275. package/tool/lib/runtime-guides.mjs +65 -0
  276. package/tool/lib/runtime-package.mjs +403 -0
  277. package/tool/lib/spine-meta.mjs +227 -0
  278. package/tool/lib/spine-motions.mjs +104 -0
  279. package/tool/parse-jsonc.mjs +86 -0
  280. package/tool/resolve-live2d-runtimes.mjs +276 -0
  281. package/tool/spine-motion-rules.json +101 -0
@@ -0,0 +1,36 @@
1
+ // spine/canvas-pixels.js —— 像素采样与画面签名
2
+ //
3
+ // 职责:从画布读回像素、算画面签名(自检/回归判据)。
4
+ // 注意:preserveDrawingBuffer=false 时直接 readPixels 常读到全 0,判据用 ratio 而不是绝对值;
5
+ // 另外「像素非空」不能证明画面可见(存活在游离画布上时照样有像素),可见性请用 DOM 关系判。
6
+
7
+ const ALPHA_THRESHOLD = 8;
8
+
9
+ /** 像素签名:对读回的缓冲做一次轻量哈希(验收用"两帧不同"判定,不做加密) */
10
+ function pixelSignature(buf) {
11
+ let h = 2166136261;
12
+ for (let i = 0; i < buf.length; i += 97) { // 稀疏采样:覆盖全画面且够快
13
+ h ^= buf[i];
14
+ h = Math.imul(h, 16777619) >>> 0;
15
+ }
16
+ return h.toString(16);
17
+ }
18
+
19
+ /** 同帧采样:非透明像素占比(先渲染一帧再读,避免读到来不及呈现的缓冲) */
20
+ export function samplePixels(st) {
21
+ const e = st.engine;
22
+ const gl = e && e.gl;
23
+ if (!gl) return { ratio: 0, w: 0, h: 0, reason: 'no-gl' };
24
+ try {
25
+ if (e.presentFirstFrame) e.presentFirstFrame();
26
+ const w = gl.drawingBufferWidth;
27
+ const h = gl.drawingBufferHeight;
28
+ const buf = new Uint8Array(w * h * 4);
29
+ gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, buf);
30
+ let n = 0;
31
+ for (let i = 3; i < buf.length; i += 4) if (buf[i] > ALPHA_THRESHOLD) n++;
32
+ return { ratio: +(n / (w * h)).toFixed(4), w, h, signature: pixelSignature(buf) };
33
+ } catch (err) {
34
+ return { ratio: 0, w: 0, h: 0, reason: String(err && err.message ? err.message : err) };
35
+ }
36
+ }
@@ -0,0 +1,127 @@
1
+ // spine/canvas-runtime.js —— 运行时源码的获取与版本选择
2
+ //
3
+ // 职责:向服务端要运行时清单、按语义化版本挑一个、把源码文本缓存起来喂给引擎。
4
+ // 为什么单独成模块:这段是唯一做网络请求与版本比较的地方,与舞台装配无关,
5
+ // 混在 canvas.js 里时最长的那几个 try/catch 都在这里,出错排查面很窄。
6
+
7
+ import { SpineEngine as NextEngine } from '../engine/index.js';
8
+ // 清单读取走统一 HTTP 层(重试 + 统一超时/错误归类)
9
+ import { getJson } from '../infra/api.js';
10
+ // 缺运行时时的可读提示(含服务端给的落地目录绝对路径与可复制命令)
11
+ import { loadRuntimeHint, spineMissingHint } from './runtime-hint.js';
12
+
13
+ const RUNTIME_BASE = '/spine-runtimes/spine-webgl-';
14
+
15
+ const RUNTIME_MANIFEST = '/spine-runtimes/index.json';
16
+
17
+ const runtimeSourceCache = new Map(); // ver -> 源码文本
18
+
19
+ let manifestPromise = null;
20
+
21
+ let availableCache = null;
22
+
23
+ /** '3.8.99' -> '3.8' */
24
+ function majorMinor(v) {
25
+ const m = String(v || '').trim().match(/^(\d+)\.(\d+)/);
26
+ return m ? `${m[1]}.${m[2]}` : String(v || '');
27
+ }
28
+
29
+ /** 运行时目录清单(dev-server 扫目录生成;客户端不写死版本列表) */
30
+ function fetchManifest() {
31
+ if (manifestPromise) return manifestPromise;
32
+ // 幂等 GET 的小 JSON ⇒ 走统一层(重试 + 统一超时/错误归类)。no-store 保留:新版本要立刻可见。
33
+ const p = getJson(RUNTIME_MANIFEST, { cache: 'no-store' })
34
+ .then((j) => (j && Array.isArray(j.versions) ? j.versions : []))
35
+ .catch(() => []);
36
+ manifestPromise = p;
37
+ p.then((list) => {
38
+ // 空结果多为"服务刚起/网络抖动",缓存它会让后续加载永久拿不到版本 ⇒ 清掉待下次重试
39
+ if (!list.length && manifestPromise === p) manifestPromise = null;
40
+ });
41
+ return p;
42
+ }
43
+
44
+ /** 本机可用版本:直接采用 dev-server 扫目录生成的清单(不再逐版发 HEAD 探测,
45
+ * 避免并发探测请求在切换/卸载时被中断成 ERR_ABORTED)。 */
46
+ export async function availableVersions() {
47
+ if (availableCache && availableCache.length) return availableCache;
48
+ let list = await fetchManifest();
49
+ // 服务刚起/抖动时清单可能一时为空:同一次加载内立即重试一次,避免把"瞬时失败"变成"这次一定失败"
50
+ if (!list.length) {
51
+ await new Promise((r) => setTimeout(r, 150));
52
+ list = await fetchManifest();
53
+ }
54
+ if (list.length) availableCache = list; // 只缓存非空结果
55
+ return list;
56
+ }
57
+
58
+ /**
59
+ * 本次装载的**运行时覆盖值**(来自 URL 的 ?runtime=)。
60
+ *
61
+ * 语义严格限定为"只生效一次":
62
+ * · 不落 localStorage / uiState —— 绝不记忆。自动选版本是**正确性保证**
63
+ * (骨架 4.1.20 只能用 4.1 运行时),一旦记住人工覆盖,
64
+ * 下次打开不兼容的模型会莫名失败,且用户看不出是自己上次设的。
65
+ * · 由 canvas-engine 在选版时 takeForcedRuntime() 取走并**立刻清空**,
66
+ * 所以"带参数装载成功后去掉参数刷新 ⇒ 回到自动"天然成立。
67
+ * @type {string|null}
68
+ */
69
+ let forcedRuntime = null;
70
+
71
+ /** 设置一次性覆盖值(传 null/'' 表示清除) */
72
+ export function setForcedRuntime(version) {
73
+ forcedRuntime = version ? String(version) : null;
74
+ }
75
+
76
+ /** 取走覆盖值(取一次即清空,天然只生效一次) */
77
+ export function takeForcedRuntime() {
78
+ const v = forcedRuntime;
79
+ forcedRuntime = null;
80
+ return v;
81
+ }
82
+
83
+ /** 精确命中优先,否则同 major 的最大 minor,最后退清单里最大的 */
84
+ export function pickRuntimeVersion(version, available) {
85
+ const want = majorMinor(version);
86
+ if (!available.length) return want || null; // 清单不可用:有版本用版本,没版本交给调用方报错
87
+ if (!want) return available[available.length - 1]; // 模型缺版本号:用清单里最新的一档
88
+ if (available.includes(want)) return want;
89
+ const major = want.split('.')[0];
90
+ const same = available.filter((v) => v.split('.')[0] === major);
91
+ if (same.length) return same[same.length - 1];
92
+ return available[available.length - 1];
93
+ }
94
+
95
+ /** 取运行时源码文本并塞进 engine 的懒加载钩子(隔离执行由 engine 负责) */
96
+ export async function ensureRuntimeSource(ver) {
97
+ window.__SPINE_SINGLE_FILE__ = true;
98
+ window.__SPINE_RUNTIME_SOURCES__ = window.__SPINE_RUNTIME_SOURCES__ || {};
99
+ if (window.__SPINE_RUNTIME_SOURCES__[ver]) return;
100
+ let text = runtimeSourceCache.get(ver);
101
+ if (!text) {
102
+ // 20s:运行时脚本实测 439KB(spine-webgl 4.1.0),CIFS/NAS 上读大文件可能数秒。
103
+ // 不给超时会让"装载模型"永久卡在"加载运行时"(用户看不出是卡住还是慢)。
104
+ let r;
105
+ try {
106
+ r = await fetch(`${RUNTIME_BASE}${ver}.js`, { cache: 'no-store', signal: AbortSignal.timeout(20000) });
107
+ } catch (e) {
108
+ // 超时/中断必须翻译成人话:裸的 "The operation was aborted." 分不清是
109
+ // "这台机器没装这个版本"(要补文件)还是"服务端/磁盘慢"(等或换机器)—— 两者的处置完全不同。
110
+ throw new Error(`运行时 ${ver} 加载超时或中断(上限 20s):${(e && e.message) || e}`);
111
+ }
112
+ if (!r.ok) {
113
+ // 404 最常见的原因是"这台机器上没放这个版本" ⇒ 附上服务端给的可复制补齐方案,
114
+ // 否则用户只看到一句 HTTP 404,不知道要下什么、放哪里(这条日志会进调试面板)。
115
+ const hint = await loadRuntimeHint();
116
+ throw new Error(spineMissingHint(ver, hint, r.status));
117
+ }
118
+ text = await r.text();
119
+ runtimeSourceCache.set(ver, text);
120
+ }
121
+ window.__SPINE_RUNTIME_SOURCES__[ver] = text;
122
+ }
123
+
124
+ /** 取引擎构造器:只有新引擎(可读实现)。旧脚本注入路径已随引擎重写收尾删除。 */
125
+ export async function pickEngineCtor() {
126
+ return NextEngine;
127
+ }
@@ -0,0 +1,108 @@
1
+ // spine/canvas-view.js —— 视口/相机/设置快照
2
+ //
3
+ // 职责:相机缩放与取景、设置默认值与回读、stats 快照的组装。
4
+ // 为什么单独成模块:这些是「纯状态换算」,不碰引擎生命周期;拆开后 canvas.js 只留装配。
5
+ // 注意:settings 快照是设置类 setter 的"回显权威"(getSetting 读它判成功),字段名不可改。
6
+
7
+ /**
8
+ * 相机视图约定(交互层按此实现,勿改):
9
+ * · pan 单位 = 屏幕像素,原点 = 画布中心(即 fit 视图的中心),向右/向下为正;
10
+ * · zoom = 相对 fit 的倍数,reset 后为 1.0,取值 0.2~8;
11
+ * · 世界单位 ↔ 屏幕像素的换算只在本文件做:屏幕 = 世界差值 × base.zoom × zoom。
12
+ */
13
+
14
+ /** 相机视图:缩放范围(相对 fit 的倍数)与取景基准缓存 */
15
+ const VIEW_ZOOM_MIN = 0.2;
16
+
17
+ const VIEW_ZOOM_MAX = 8;
18
+
19
+ export const DEFAULT_SETTINGS = {
20
+ pma: 'auto', fit: 'subject', textureFilter: 'linear', speed: 1, loop: true, debug: null,
21
+ fpsLimit: 60, dprMode: 'auto', msaa: 'auto', filter: 'original',
22
+ };
23
+
24
+ /** 当前相机对象(部分运行时用 scale 而非 zoom) */
25
+ function cameraOf(st) {
26
+ return st.engine && st.engine.camera ? st.engine.camera : null;
27
+ }
28
+
29
+ /** 读相机缩放(统一成"越大越放大"的 zoom 语义) */
30
+ function cameraZoom(cam) {
31
+ if (!cam) return 1;
32
+ if (cam.zoom !== undefined) return cam.zoom;
33
+ if (cam.scale) return 1 / cam.scale;
34
+ return 1;
35
+ }
36
+
37
+ /** 写相机缩放(与 cameraZoom 的两种字段对应) */
38
+ function setCameraZoom(cam, zoom) {
39
+ if (!cam) return;
40
+ if (cam.zoom !== undefined) cam.zoom = zoom;
41
+ else if (cam.scale !== undefined) cam.scale = 1 / zoom;
42
+ }
43
+
44
+ /**
45
+ * 取"fit 后的缩放"作为视图基准。
46
+ * 引擎的 baseZoom 就是 fit 结果;只有在拿不到时才退回当前相机 zoom。
47
+ * (曾经用"调用时的相机 zoom"当基准,导致 reset 之后基准被上一次的缩放污染)
48
+ */
49
+
50
+ /**
51
+ * 记录视图基准:缩放基准取引擎的 userScale(引擎语义「越大越放大」),平移累计归零。
52
+ * 不在本文件推导相机字段——引擎的 camera.zoom 是 userScale 的倒数,自己算必错。
53
+ */
54
+ export function captureViewBase(st) {
55
+ const engine = st.engine;
56
+ // 基准 = 当前 userScale(引擎的 getCameraZoom 是唯一真相;resetCamera 之后取一次即可)
57
+ st.viewBaseScale = engine && typeof engine.getCameraZoom === 'function' ? engine.getCameraZoom() : 1;
58
+ st.viewPan = { x: 0, y: 0 };
59
+ }
60
+
61
+ /**
62
+ * 标定「平移输入 1 像素 → 画面位移多少像素」。
63
+ * 为什么要标定:引擎 applyPan 内部有 `×2` 与 flip 系数,直接原样传会让拖动比手指快一倍多。
64
+ * 做法:applyPan(±1) 探测相机世界位移,再乘「每世界单位对应多少屏幕像素」,探测后立即复原(净位移 0)。
65
+ */
66
+
67
+ /**
68
+ * 视图快照(对外单位约定)。
69
+ * zoom = 相对 fit 的倍数:engine.getCameraZoom() / 基准 userScale;reset 后为 1;
70
+ * panX/panY = **屏幕像素**,原点画布中心,向右/向下为正(累计值)。
71
+ */
72
+ export function viewOf(st) {
73
+ const engine = st.engine;
74
+ const base = st.viewBaseScale || 1;
75
+ const cur = engine && typeof engine.getCameraZoom === 'function' ? engine.getCameraZoom() : base;
76
+ const pan = st.viewPan || { x: 0, y: 0 };
77
+ return {
78
+ zoom: +(cur / base).toFixed(4),
79
+ panX: +pan.x.toFixed(2),
80
+ panY: +pan.y.toFixed(2),
81
+ userScale: +cur.toFixed(6),
82
+ baseUserScale: +base.toFixed(6),
83
+ available: !!engine,
84
+ };
85
+ }
86
+
87
+ /** 只读统计(不暴露 engine 内部对象) */
88
+ export function statsOf(st) {
89
+ const e = st.engine;
90
+ return {
91
+ model: st.model ? { name: st.model.name, version: st.model.version } : null,
92
+ runtimeVersion: e && e.getRuntimeVersion ? e.getRuntimeVersion() : null,
93
+ runtimeKeys: [...st.runtimeKeys],
94
+ contexts: e && e.gl ? 1 : 0,
95
+ frames: st.frames,
96
+ // 暂停状态(给外部读快照用:看门狗要判断"这段时间本来就不该有帧")。
97
+ // 放这里而不是让调用方改用 debug():stats() 每秒被看门狗调一次,
98
+ // 而 debug() 内部是 `{...statsOf(st), engine, tickCalls, paused}` —— 每秒多造一层对象纯属浪费。
99
+ paused: !!st.paused,
100
+ fps: e && e.getCurrentFps ? e.getCurrentFps() : null,
101
+ loads: st.loads,
102
+ disposes: st.disposes,
103
+ lastError: st.lastError,
104
+ animations: e && e.getAnimations ? e.getAnimations() : [],
105
+ settings: { ...st.settings },
106
+ view: viewOf(st),
107
+ };
108
+ }
@@ -0,0 +1,7 @@
1
+ // spine/canvas.js —— 舞台的**唯一出口**(拆分后只做再导出)。
2
+ //
3
+ // 保留这个文件名是因为导入路径属于既有契约:调用方一律 `import { createSpineStage } from './spine/canvas.js'`。
4
+ // 实现分在同目录 canvas-*.js(runtime / document / view / pixels / engine / api / api-groups / entry)。
5
+ // 契约(渲染器契约 §2.1):对外键一个不能少;setDebug 在设置组与渲染组各定义一次,
6
+ // 后挂载者生效 —— 挂载顺序由 attachExtraApi 固定,勿调整。
7
+ export { createSpineStage } from './canvas-entry.js';
@@ -0,0 +1,73 @@
1
+ // spine/runtime-hint.js —— 把「缺运行时怎么补」拼成一句可读提示
2
+ //
3
+ // 为什么 hint 要从服务端拿:提示里最有用的那条是**落地目录的绝对路径**,
4
+ // 而数据根(dataRoot)只有服务端知道 —— 前端自己拼只能写"某个目录",用户还得猜。
5
+ // 服务端在 GET /spine-runtimes/index.json 的 runtimeHint 字段里给出模板,前端只做版本号替换。
6
+ //
7
+ // 为什么单独一个模块:canvas-runtime.js 与 runtimes.js 两条加载路径都要用它,
8
+ // 复制两份必然漂移(同一句话在两个文件里各说各话,是本项目反复栽过的坑)。
9
+ import { getJson } from '../infra/api.js';
10
+
11
+ /** hint 缓存(60s):只在"加载失败"时才需要它,但失败可能连续发生,别每次都问服务端 */
12
+ const CACHE_TTL_MS = 60000;
13
+ let cache = null; // { at:number, hint:object|null }
14
+
15
+ /**
16
+ * 取服务端给的运行时提示模板(失败返回 null,绝不因此抛错)。
17
+ * @returns {Promise<object|null>}
18
+ */
19
+ export async function loadRuntimeHint() {
20
+ if (cache && Date.now() - cache.at < CACHE_TTL_MS) return cache.hint;
21
+ let hint = null;
22
+ try {
23
+ const j = await getJson('/spine-runtimes/index.json', { cache: 'no-store' });
24
+ hint = (j && j.runtimeHint) || null;
25
+ } catch {
26
+ hint = null; // 拿不到就退回"没有 hint"的降级文案
27
+ }
28
+ cache = { at: Date.now(), hint };
29
+ return hint;
30
+ }
31
+
32
+ /** 模板占位替换:服务端给的模板里用 {ver} 表示版本号 */
33
+ function fill(tpl, ver) {
34
+ return String(tpl || '').split('{ver}').join(String(ver));
35
+ }
36
+
37
+ /** Spine 3.x 判断(3.x 官方没有 npm 包,只能给出 curl 自备的引导) */
38
+ function isSpine3(ver) {
39
+ return String(ver || '').trim().startsWith('3.');
40
+ }
41
+
42
+ /**
43
+ * Spine 运行时缺失的可读提示。
44
+ * @param {string} ver 版本号(如 '4.1')
45
+ * @param {object|null} hint 服务端给的模板
46
+ * @param {number|null} status HTTP 状态(拿得到就带上,方便判断是"没这个版本"还是"服务端没起来")
47
+ * @returns {string}
48
+ */
49
+ export function spineMissingHint(ver, hint, status) {
50
+ const h = hint || {};
51
+ const head = `运行时 ${ver} 加载失败${status ? `(HTTP ${status})` : ''}。`;
52
+ if (isSpine3(ver)) {
53
+ const lines = [head, `缺少 Spine ${ver} 运行时。官方 npm 未发布 3.x(最早 4.0.1),只能自备:`];
54
+ if (h.spine3Curl) lines.push(` ${fill(h.spine3Curl, ver)}`);
55
+ if (h.spine3Note) lines.push(` (${h.spine3Note})`);
56
+ return lines.join('\n');
57
+ }
58
+ const lines = [head, `缺少 Spine ${ver} 运行时。可执行:${fill(h.spineFetch || 'node tool/fetch-runtimes.mjs --spine {ver}', ver)}`];
59
+ if (h.spineTarget) lines.push(` (或把 spine-webgl-${ver}.js 放到 ${fill(h.spineTarget, ver)})`);
60
+ return lines.join('\n');
61
+ }
62
+
63
+ /**
64
+ * Live2D 运行时缺失的可读提示。
65
+ * `serverError` 是服务端 503/404 响应体里的 error 文本(已包含官网 URL 与目标目录)。
66
+ * @param {string} url 加载失败的脚本地址
67
+ * @param {string} [serverError]
68
+ * @returns {string}
69
+ */
70
+ export function live2dMissingHint(url, serverError) {
71
+ const head = `Live2D 运行时加载失败:${url}`;
72
+ return serverError ? `${head}\n${serverError}` : head;
73
+ }
@@ -0,0 +1,212 @@
1
+ // ============================================================================
2
+ // src/web/spine/runtimes.js —— Spine 运行时管理器(提供 `Is` 能力)
3
+ //
4
+ // 作用:把"按版本取运行时源码 → 隔离执行 → 交出该版本的 spine 命名空间"收敛到一处,
5
+ // 引擎侧只需问它要某个版本,不关心源码从哪来、怎么执行。
6
+ //
7
+ // 为什么需要它:多版本共存要求每个版本的 `var spine` 不互相覆盖(详见下方"必须隔离执行")。
8
+ //
9
+ // 关键点:**必须隔离执行**。每个 `spine-webgl-<ver>.js` 都是顶层 `var spine = …`;
10
+ // 若用 `<script src>` 加载,后加载的会覆盖 `window.spine`,导致先加载版本的类在运行期
11
+ // 解析到新版基类而崩(典型:`Class constructor Texture cannot be invoked without 'new'`)。
12
+ // 用 `Function` 构造器执行源码时 `var spine` 成了**函数局部绑定**,
13
+ // 各版本天然隔离(旧 app 的 `evalRuntimeIsolated` 也是这个原理)。
14
+ // ============================================================================
15
+
16
+ const RUNTIME_BASE = '/spine-runtimes/spine-webgl-';
17
+
18
+ // 清单读取走统一 HTTP 层(重试 + 统一超时/错误归类)。api.js 顶层不引用 window/document,
19
+ // 所以这个 import 在 node 侧被间接加载时也安全。
20
+ import { getJson } from '../infra/api.js';
21
+ // 缺运行时提示与 canvas-runtime 共用同一份(含服务端给的落地目录与可复制命令)
22
+ import { loadRuntimeHint, spineMissingHint } from './runtime-hint.js';
23
+
24
+ /** 运行时必须导出的类(缺任一即视为该版本不可用) */
25
+ const REQUIRED_CLASSES = ['Skeleton', 'AnimationState', 'TextureAtlas', 'AtlasAttachmentLoader'];
26
+ const REQUIRED_PARSERS = ['SkeletonJson', 'SkeletonBinary'];
27
+
28
+ function majorMinor(v) {
29
+ const m = String(v || '').trim().match(/^(\d+)\.(\d+)/);
30
+ return m ? `${m[1]}.${m[2]}` : String(v || '');
31
+ }
32
+
33
+ /** 某个 spine 命名空间里取类:优先 webgl 子命名空间,其次根(两代布局不同) */
34
+ function rtClass(rt, name) {
35
+ if (!rt) return null;
36
+ return (rt.webgl && rt.webgl[name]) || rt[name] || null;
37
+ }
38
+
39
+ /** 检查运行时是否完整(缺什么就报出来,便于定位) */
40
+ function missingClasses(rt) {
41
+ const missing = [];
42
+ for (const n of REQUIRED_CLASSES) if (!rtClass(rt, n)) missing.push(n);
43
+ if (!REQUIRED_PARSERS.some((n) => rtClass(rt, n))) missing.push('SkeletonJson/SkeletonBinary');
44
+ if (!rtClass(rt, 'SceneRenderer') && !rtClass(rt, 'SkeletonRenderer')) missing.push('SceneRenderer/SkeletonRenderer');
45
+ if (!rtClass(rt, 'GLTexture')) missing.push('GLTexture');
46
+ if (!rtClass(rt, 'OrthoCamera') && !rtClass(rt, 'SceneRenderer')) missing.push('OrthoCamera');
47
+ return missing;
48
+ }
49
+
50
+ /** 运行时是否支持 straight alpha(4.1+ 的 SkeletonRenderer 有 premultipliedAlpha 字段) */
51
+ function detectStraightAlpha(rt, version) {
52
+ // **判据:按运行时版本号 >= 4.1 判定**(判据原文 `parseFloat(T.version||"0")>=4.1`)。
53
+ // 为什么不用能力探测:spine 4.1 的 `SkeletonRenderer.premultipliedAlpha` 是**类字段(实例属性)**,
54
+ // 不在 prototype 上 ⇒ `'premultipliedAlpha' in SR.prototype` 恒为 false ⇒ 4.1 被误判"不支持直通"
55
+ // ⇒ 面板显示"直通支持: 不支持"、`straight` 档被降级成 upload(同为 4.1 时本实现的判据更严格,会报不支持)。
56
+ // 实测:4.1 类定义 `SkeletonRenderer = class { premultipliedAlpha = false; … }`;3.8 段内无此字段。
57
+ const ver = Number(version);
58
+ if (Number.isFinite(ver)) return ver >= 4.1;
59
+ // 版本号拿不到时退回能力探测(旧行为)
60
+ const SR = rtClass(rt, 'SkeletonRenderer');
61
+ if (!SR || !SR.prototype) return false;
62
+ return 'premultipliedAlpha' in SR.prototype;
63
+ }
64
+
65
+
66
+ /**
67
+ * 在隔离作用域执行运行时源码,返回它自己的 spine 对象。
68
+ * 注意:这里不碰 window.spine —— 每个版本的 `var spine` 都在此函数体内,互不影响。
69
+ */
70
+ function evalIsolated(source, version) {
71
+ let spineObj = null;
72
+ try {
73
+ spineObj = new Function(`${source}\n;return (typeof spine !== "undefined") ? spine : null;`)(); // dsh-skip-sensitive 运行时隔离执行本地可信源码(src/spine-runtimes/*.js),不接收任何外部输入
74
+ } catch (err) {
75
+ throw new Error(`Spine ${version} 运行时执行失败:${err && err.message ? err.message : err}`);
76
+ }
77
+ if (!spineObj || typeof spineObj !== 'object') {
78
+ throw new Error(`Spine ${version} 运行时执行后没有导出 spine 对象(源码形态不符)`);
79
+ }
80
+ const missing = missingClasses(spineObj);
81
+ if (missing.length) throw new Error(`Spine ${version} 运行时缺少核心类:${missing.join('、')}`);
82
+ return spineObj;
83
+ }
84
+
85
+ /** 运行时管理器(提供 getInstance / setLogCallback / getRuntimeWithFallback 三项能力) */
86
+ export class SpineRuntimeManager {
87
+ static getInstance() {
88
+ if (!SpineRuntimeManager._inst) SpineRuntimeManager._inst = new SpineRuntimeManager();
89
+ return SpineRuntimeManager._inst;
90
+ }
91
+
92
+ constructor() {
93
+ this.cache = new Map(); // ver -> { version, spine, supportsStraightAlpha }
94
+ this.sources = new Map(); // ver -> 源码文本
95
+ this.loading = new Map();
96
+ this.logCallback = null;
97
+ }
98
+
99
+ setLogCallback(cb) { this.logCallback = typeof cb === 'function' ? cb : null; }
100
+
101
+ log(level, msg, scope) {
102
+ try { if (this.logCallback) this.logCallback(level, msg, scope || 'runtime'); } catch { /* 日志回调不得影响渲染 */ }
103
+ }
104
+
105
+ /** 版本规整:'3.8.99' -> '3.8' */
106
+ normalizeVersion(v) { return majorMinor(v); }
107
+
108
+ isLoaded(version) { return this.cache.has(this.normalizeVersion(version)); }
109
+
110
+ getCachedVersions() { return [...this.cache.keys()]; }
111
+
112
+ /** 可用版本:优先用页面注入的源码表,否则按 URL 探测 */
113
+ async listVersions() {
114
+ const fromSources = Object.keys(window.__SPINE_RUNTIME_SOURCES__ || {});
115
+ if (fromSources.length) return fromSources.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
116
+ try {
117
+ // 幂等 GET 的小 JSON 清单 ⇒ 走统一层(重试 + 统一超时 + 错误归类)。
118
+ // no-store 必须保留:换了运行时版本要立刻看到新清单。
119
+ const j = await getJson('/spine-runtimes/index.json', { cache: 'no-store' });
120
+ if (j && Array.isArray(j.versions) && j.versions.length) return j.versions;
121
+ } catch { /* 清单取不到就走空表 */ }
122
+ return [];
123
+ }
124
+
125
+ /** 取某版源码文本(页面已注入则直接用;否则 fetch 一次并缓存) */
126
+ async sourceOf(version) {
127
+ const injected = (window.__SPINE_RUNTIME_SOURCES__ || {})[version];
128
+ if (injected) return injected;
129
+ if (this.sources.has(version)) return this.sources.get(version);
130
+ // 20s:与 canvas-runtime 同口径(运行时脚本实测 439KB),别让"加载运行时"无限挂着
131
+ let r;
132
+ try {
133
+ r = await fetch(`${RUNTIME_BASE}${version}.js`, { cache: 'no-store', signal: AbortSignal.timeout(20000) }); // dsh-skip-quality 本地同源运行时脚本读取,既有代码
134
+ } catch (e) {
135
+ // 同 canvas-runtime:把裸的 abort 翻成"超时/中断",否则与"没装这个版本"混为一谈
136
+ throw new Error(`运行时 ${version} 加载超时或中断(上限 20s):${(e && e.message) || e}`);
137
+ }
138
+ if (!r.ok) {
139
+ // 与 canvas-runtime 同一条提示口径(共用一个模块,别各写一份):
140
+ // 404 时把"下哪个版本 / 放到哪个绝对路径 / 可复制的命令"一起给出来
141
+ const hint = await loadRuntimeHint();
142
+ throw new Error(spineMissingHint(version, hint, r.status));
143
+ }
144
+ const text = await r.text();
145
+ this.sources.set(version, text);
146
+ return text;
147
+ }
148
+
149
+ /** 取运行时(缓存;并发去重) */
150
+ async getRuntime(version) {
151
+ const ver = this.normalizeVersion(version);
152
+ if (this.cache.has(ver)) return this.cache.get(ver);
153
+ if (this.loading.has(ver)) return this.loading.get(ver);
154
+ const p = (async () => {
155
+ const src = await this.sourceOf(ver);
156
+ const spine = evalIsolated(src, ver);
157
+ const entry = { version: ver, spine, supportsStraightAlpha: detectStraightAlpha(spine, ver) };
158
+ this.cache.set(ver, entry);
159
+ this.loading.delete(ver);
160
+ this.log('info', `Spine ${ver} 运行时已就绪`, 'runtime');
161
+ return entry;
162
+ })().catch((err) => {
163
+ this.loading.delete(ver);
164
+ this.log('error', `加载 Spine ${ver} 运行时失败:${err.message}`, 'runtime');
165
+ throw err;
166
+ });
167
+ this.loading.set(ver, p);
168
+ return p;
169
+ }
170
+
171
+ /** 取运行时,失败时按"同 major 相邻 → 清单最大"降级 */
172
+ async getRuntimeWithFallback(version) {
173
+ const want = this.normalizeVersion(version);
174
+ try { return await this.getRuntime(want); } catch { /* 继续降级 */ }
175
+ const list = await this.listVersions();
176
+ const major = want.split('.')[0];
177
+ const same = list.filter((v) => v.split('.')[0] === major && v !== want);
178
+ const candidates = [...same, ...list.filter((v) => v !== want && v.split('.')[0] !== major).reverse()];
179
+ for (const v of candidates) {
180
+ try {
181
+ const entry = await this.getRuntime(v);
182
+ this.log('warn', `Spine ${want} 不可用,已降级到 ${v}`, 'runtime');
183
+ return entry;
184
+ } catch { /* 试下一个 */ }
185
+ }
186
+ throw new Error(`无法加载 Spine ${want} 运行时(清单:${list.join('、') || '空'})`);
187
+ }
188
+ }
189
+
190
+ /**
191
+ * 打包器的 class-field 助手(engine 切片里 88 处调用它,但切片没带这个定义)。
192
+ * 语义:把 value 定义到 obj[key] 上并返回 value;键已存在时用 defineProperty 覆盖,
193
+ * 避免继承来的只读属性导致赋值失败(esbuild/tsc 的等价实现)。
194
+ */
195
+ function defineField(obj, key, value) {
196
+ if (key in obj) {
197
+ Object.defineProperty(obj, key, { value, enumerable: true, configurable: true, writable: true });
198
+ } else {
199
+ obj[key] = value;
200
+ }
201
+ return value;
202
+ }
203
+ /**
204
+ * 安装到全局:引擎取某个版本的运行时源码时经它取实例,
205
+ * 所以必须在装载模型**之前**调用。
206
+ */
207
+ export function installRuntimeManager() {
208
+ if (!window.de) window.de = defineField; // engine 切片依赖的 class-field 助手
209
+ if (window.Is) return window.Is;
210
+ window.Is = SpineRuntimeManager;
211
+ return window.Is;
212
+ }