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,285 @@
|
|
|
1
|
+
// src/web/infra/plugin-host-registration.mjs —— 注册与校验(从 plugin-host.js 抽出,B2)
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成模块:这是**插件唯一能碰的对外面**(window.SpineViewerPlugins / ModelStagePlugins.slots),
|
|
4
|
+
// 四条通道(provider / effect / 老接口控件 / 通知)+ 它们的拒绝原因,语义独立于一整套装载与解包;
|
|
5
|
+
// 搬出去后 plugin-host 只剩"组装 hostApi"。
|
|
6
|
+
//
|
|
7
|
+
// 依赖约定(**不传整块 ctx**,只传它真正读写的窄视图;工具 say/emit 也随参传入):
|
|
8
|
+
// · io = { say, emit } —— 日志与事件(依赖,不是状态)
|
|
9
|
+
// · tables = { providers, loadState, registeredEffects, getSlotRegistry }
|
|
10
|
+
// 前三张是稳定引用(Map);slotRegistry 是**惰性初始化**的,必须用 getter 现取,不能快照
|
|
11
|
+
// · decls = { manifestProvides, manifestEffects, registrationRejects, maxFragmentChars }
|
|
12
|
+
// 清单声明与拒绝记录(低频状态,宿主持有,这里按引用用)
|
|
13
|
+
// · probe = { getLoadingPluginId, getLastLoadedPluginId, dispatch }
|
|
14
|
+
// loadingPluginId 是**装载期窗口开关**:必须现读,快照会让窗口判定永远为假
|
|
15
|
+
// · sinks = { addEffect, publishSlotStats }
|
|
16
|
+
// 宿主的回收登记与统计发布(注册成功后要挂 disposer、要刷新 __SLOT_STATS__)
|
|
17
|
+
//
|
|
18
|
+
// ⚠️ 对外契约(逐字保留,改动即破坏兼容):错误码与文案(not-in-load-window / undeclared-slot /
|
|
19
|
+
// slot-not-open / unknown-slot / risk-mismatch / missing-setting / bad-group-id …)与校验**顺序**
|
|
20
|
+
// 都不许动;`window.ModelStagePlugins.slots.register` 的签名与返回值(disposer 或 null)也不许动。
|
|
21
|
+
|
|
22
|
+
import { EVENTS } from './events.mjs';
|
|
23
|
+
import { PANEL_GROUPS } from '../panel/schema.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {{io:object, tables:object, decls:object, probe:object, sinks:object}} deps
|
|
27
|
+
* @returns {{registerProvider:Function, registerEffect:Function, registerControls:Function,
|
|
28
|
+
* slots:object, notify:Function}}
|
|
29
|
+
*/
|
|
30
|
+
export function createRegistration({ io, tables, decls, probe, sinks }) {
|
|
31
|
+
const { say, emit } = io;
|
|
32
|
+
const { providers, loadState, registeredEffects, getSlotRegistry } = tables;
|
|
33
|
+
const { manifestProvides, manifestEffects, registrationRejects, maxFragmentChars } = decls;
|
|
34
|
+
const { getLoadingPluginId, getLastLoadedPluginId, dispatch } = probe;
|
|
35
|
+
const { addEffect, publishSlotStats } = sinks;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 注册被拒:显式记一条 + 可读原因(H2)。
|
|
39
|
+
*
|
|
40
|
+
* 与"静默丢弃"的区别:调用方能拿到 false,日志里能看出**哪个阶段**、**哪个插件**、
|
|
41
|
+
* **为什么**(异步注册 vs 缺字段),并且 `effectsReport().rejects` 可被测试断言。
|
|
42
|
+
* @param {string} kind register / registerEffect / registerControls
|
|
43
|
+
* @param {string} why 人话原因
|
|
44
|
+
* @param {object} [detail] 附加信息(id / group 等)
|
|
45
|
+
* @returns {false}
|
|
46
|
+
*/
|
|
47
|
+
function rejectRegistration(kind, why, detail) {
|
|
48
|
+
const rec = {
|
|
49
|
+
kind, why, at: Date.now(),
|
|
50
|
+
pluginId: getLoadingPluginId() || null,
|
|
51
|
+
likelyOwner: getLoadingPluginId() ? null : (getLastLoadedPluginId() || null),
|
|
52
|
+
...(detail || {}),
|
|
53
|
+
};
|
|
54
|
+
registrationRejects.push(rec);
|
|
55
|
+
if (registrationRejects.length > 50) registrationRejects.shift();
|
|
56
|
+
say.warn(`插件注册被拒(${kind}):${why}${rec.likelyOwner ? `(疑似来自最后装载的插件 ${rec.likelyOwner})` : ''}`);
|
|
57
|
+
emit('plugin.register-rejected', rec);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 内置分组的 title → id 映射(兼容层用)。
|
|
63
|
+
*
|
|
64
|
+
* 为什么从 PANEL_GROUPS 派生而不是写死一张表:老接口传的是**标题**(「设置」),
|
|
65
|
+
* 而插槽名要的是 groupId;写死映射会在加组/改标题时静默失配(这正是本轮要消灭的"两处表达")。
|
|
66
|
+
* 传进来的本来就是合法 id 时原样返回,方便调用方"标题或 id 都行"。
|
|
67
|
+
* @param {string} name
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
function groupIdOfTitle(name) {
|
|
71
|
+
const want = String(name || '').trim();
|
|
72
|
+
if (!want) return null;
|
|
73
|
+
if (PANEL_GROUPS.some((g) => String(g.id || '') === want)) return want;
|
|
74
|
+
const hit = PANEL_GROUPS.find((g) => String(g.title || '') === want);
|
|
75
|
+
return hit ? String(hit.id || '') : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 老控件声明 → 插槽 spec(兼容层用)。
|
|
80
|
+
*
|
|
81
|
+
* 老接口给的是"数据",插槽要的是"render 函数":这里把老形状包成一个返回老形状的 render,
|
|
82
|
+
* 之后统一由 mergeSlotControls → slotSpecToControl 翻译成渲染层 kind(一处翻译,两路共用)。
|
|
83
|
+
* @param {object} raw 老控件声明({id, kind, label, options, order, ...})
|
|
84
|
+
* @returns {object|null} 插槽 spec;缺 id 返回 null
|
|
85
|
+
*/
|
|
86
|
+
function legacyControlToSlotSpec(raw) {
|
|
87
|
+
const id = String((raw && raw.id) || '').trim();
|
|
88
|
+
if (!id) return null;
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
label: String((raw && raw.label) || id),
|
|
92
|
+
// 老接口的语义:控件 id 就是个效果 id —— 效果没注册成功时该控件要**置灰 + 给原因**(契约 §9.3)。
|
|
93
|
+
// 转插槽后由 mergeSlotControls 用 registeredEffects 判定,语义与迁移前一致。
|
|
94
|
+
effectId: id,
|
|
95
|
+
// 老接口没有 order 概念:缺省 1000(排在组内内置控件之后),显式给了就尊重
|
|
96
|
+
order: Number.isFinite(Number(raw && raw.order)) ? Number(raw.order) : 1000,
|
|
97
|
+
render: () => ({
|
|
98
|
+
kind: String((raw && raw.kind) || ''),
|
|
99
|
+
options: Array.isArray(raw && raw.options) ? raw.options.slice() : undefined,
|
|
100
|
+
label: String((raw && raw.label) || id),
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** 插件注册自己:只收有 id 的对象;缺 canHandle/resolve 时保留但标注(供排障) */
|
|
105
|
+
function registerProvider(plugin) {
|
|
106
|
+
if (!plugin || typeof plugin !== 'object') { say.warn('register 收到非对象,已忽略'); return false; }
|
|
107
|
+
const id = String(plugin.id || '').trim();
|
|
108
|
+
if (!id) { say.warn('register 缺少 id,已忽略'); return false; }
|
|
109
|
+
// 契约 PL2:id 重复必须**拒绝**且不覆盖已注册的那个。
|
|
110
|
+
// 静默 providers.set 覆盖会让"后加载的插件顶掉先加载的",且排障时看不出发生过覆盖。
|
|
111
|
+
// 保留先注册的实例(先到先得):它可能已经参与过解析,换掉会留下"半新半旧"的对象。
|
|
112
|
+
if (providers.has(id)) {
|
|
113
|
+
say.warn(`插件 ${id} 重复注册,已忽略(保留先注册的实例)`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// 角色以**清单**为准(契约 插件契约 §二):register 上来的对象不覆盖它
|
|
117
|
+
const declared = manifestProvides.get(id) || [];
|
|
118
|
+
const rec = { ...plugin, id, provides: declared.slice() };
|
|
119
|
+
providers.set(id, rec);
|
|
120
|
+
// 登记回收动作:卸载时把 provider 从表里摘掉(只摘自己这一份,别误删后来者)
|
|
121
|
+
addEffect(id, {
|
|
122
|
+
kind: 'provider',
|
|
123
|
+
label: id,
|
|
124
|
+
dispose: () => { if (providers.get(id) === rec) providers.delete(id); },
|
|
125
|
+
});
|
|
126
|
+
const st = loadState.get(id) || { files: 0, ms: 0 };
|
|
127
|
+
loadState.set(id, { ...st, loaded: true, error: null });
|
|
128
|
+
const caps = [];
|
|
129
|
+
if (typeof rec.canHandle === 'function') caps.push('canHandle');
|
|
130
|
+
if (typeof rec.resolve === 'function') caps.push('resolve');
|
|
131
|
+
say.info(`已注册提供者 ${id}(${rec.name || id}):${caps.join('/') || '无可调用成员'}`);
|
|
132
|
+
try { dispatch(EVENTS.PLUGIN_REGISTERED, { id }); } catch { /* 忽略 */ }
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 注册一个渲染效果(契约 §9.2):只收"作用于当前像素的表达式",不是完整着色器。
|
|
137
|
+
*
|
|
138
|
+
* 校验三条(任一不满足即注册失败,**不是静默忽略**):
|
|
139
|
+
* · id 必须在本插件清单的 effects[].id 里声明过 —— 否则能力键白名单不会认它,
|
|
140
|
+
* 面板能选、渲染层不认,就是"选了没反应"的静默失败;
|
|
141
|
+
* · fragment 必须是非空字符串;
|
|
142
|
+
* · id 全局唯一(跨插件也唯一,防两个插件抢同一个 uniform 命名空间)。
|
|
143
|
+
* @param {{id:string, fragment:string}} spec
|
|
144
|
+
* @returns {boolean} 是否注册成功
|
|
145
|
+
*/
|
|
146
|
+
function registerEffect(spec) {
|
|
147
|
+
const pluginId = getLoadingPluginId();
|
|
148
|
+
const id = String((spec && spec.id) || '').trim();
|
|
149
|
+
if (!pluginId) {
|
|
150
|
+
return rejectRegistration('registerEffect', '不在脚本装载期(异步注册不被支持:registerEffect 必须在脚本顶层同步调用)', { id });
|
|
151
|
+
}
|
|
152
|
+
if (!id) { say.warn(`registerEffect 缺少 id(插件 ${pluginId}),已忽略`); return false; }
|
|
153
|
+
const declared = (manifestEffects.get(pluginId) || []).some((e) => String(e.id) === id);
|
|
154
|
+
if (!declared) {
|
|
155
|
+
say.warn(`registerEffect(${id}) 未在插件 ${pluginId} 的 effects[] 里声明,已拒绝`);
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (typeof (spec && spec.fragment) !== 'string' || !spec.fragment.trim()) {
|
|
159
|
+
say.warn(`registerEffect(${id}) 的 fragment 为空,已拒绝`);
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
// 超长直接拒:fragment 会被拼进着色器并在挂载时编译,没有长度闸门就等于
|
|
163
|
+
// 允许插件往渲染链路里塞任意大段代码(编译失败还会连带拖慢首帧)
|
|
164
|
+
if (spec.fragment.length > maxFragmentChars) {
|
|
165
|
+
say.warn(`registerEffect(${id}) 的 fragment 过长(${spec.fragment.length} > ${maxFragmentChars} 字符),已拒绝`);
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
if (registeredEffects.has(id)) {
|
|
169
|
+
say.warn(`registerEffect(${id}) 重复注册(已被 ${registeredEffects.get(id).pluginId} 占用),已忽略`);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
// uniforms 是插件侧的"参数默认值兜底"(清单 params[].default 的补充口)。
|
|
173
|
+
// 实际每档用哪个值由 effect-chain.uniformsForOption 按档位算,这里只做缺省兜底。
|
|
174
|
+
const uniforms = (spec && spec.uniforms && typeof spec.uniforms === 'object') ? { ...spec.uniforms } : {};
|
|
175
|
+
registeredEffects.set(id, { pluginId, fragment: spec.fragment, uniforms });
|
|
176
|
+
addEffect(pluginId, {
|
|
177
|
+
kind: 'effect',
|
|
178
|
+
label: id,
|
|
179
|
+
dispose: () => registeredEffects.delete(id),
|
|
180
|
+
});
|
|
181
|
+
say.info(`已注册效果 ${id}(插件 ${pluginId})`);
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* **兼容层**:老接口 `registerControls({group, controls})` —— 内部转成插槽注册。
|
|
186
|
+
*
|
|
187
|
+
* 新插件请直接用 `slots.register('panel.group.<groupId>', { id, render })`;
|
|
188
|
+
* 这里保留是因为它写在契约里(公开面),不能因为"现网没人用"就悄悄删掉。
|
|
189
|
+
*
|
|
190
|
+
* 三条兼容语义(都是老插件能零改动继续工作的前提):
|
|
191
|
+
* ① 分组参数是**标题**(「设置」)⇒ 用内置表的 title→id 映射换成 groupId(不写死映射表)
|
|
192
|
+
* ② 老控件 id **不强制** plugin. 前缀(分组插槽本就不要求前缀)
|
|
193
|
+
* ③ **隐式声明**:老插件没有 plugin.json 的 slots 字段 ⇒ 视为已声明该分组插槽,
|
|
194
|
+
* 不报 undeclared-slot(否则老插件一升级就全部失效)
|
|
195
|
+
*
|
|
196
|
+
* 迁移后老接口与 slots.register 的控件走**同一条**合并/渲染/回收链路:
|
|
197
|
+
* 登记进同一个 slotRegistry ⇒ disposer 进同一个 effect 容器 ⇒ unloadPlugin 一并回收。
|
|
198
|
+
* @param {{group:string, controls:Array}} section
|
|
199
|
+
* @returns {boolean} 至少成功登记一个控件
|
|
200
|
+
*/
|
|
201
|
+
function registerControls(section) {
|
|
202
|
+
const pluginId = getLoadingPluginId();
|
|
203
|
+
if (!pluginId) {
|
|
204
|
+
return rejectRegistration('registerControls', '不在脚本装载期(异步注册不被支持:registerControls 必须在脚本顶层同步调用)', {
|
|
205
|
+
group: String((section && section.group) || ''),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const groupName = String((section && section.group) || '').trim();
|
|
209
|
+
const controls = Array.isArray(section && section.controls) ? section.controls : null;
|
|
210
|
+
if (!groupName || !controls || !controls.length) {
|
|
211
|
+
say.warn(`registerControls(插件 ${pluginId})缺少 group 或 controls,已忽略`);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (!getSlotRegistry()) {
|
|
215
|
+
say.warn(`registerControls(插件 ${pluginId})失败:插槽注册表尚未就绪`);
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
// ① 标题 → groupId:内置表派生;传进来的本来就是 id 时原样放过
|
|
219
|
+
const groupId = groupIdOfTitle(groupName) || groupName;
|
|
220
|
+
const slotName = `panel.group.${groupId}`;
|
|
221
|
+
let ok = 0;
|
|
222
|
+
const rejects = [];
|
|
223
|
+
for (const raw of controls) {
|
|
224
|
+
const spec = legacyControlToSlotSpec(raw);
|
|
225
|
+
if (!spec) { rejects.push({ id: (raw && raw.id) || '', reason: '控件缺 id' }); continue; }
|
|
226
|
+
// ②③ 前缀不强制、隐式声明:都靠 implicitDeclare 分支表达
|
|
227
|
+
const dispose = getSlotRegistry().register(pluginId, slotName, spec, { implicitDeclare: true });
|
|
228
|
+
if (typeof dispose === 'function') {
|
|
229
|
+
// 与 slots.register 完全同一条回收链:disposer 进同一个 effect 容器
|
|
230
|
+
// (不挂的话 unloadPlugin 只能靠 disposeAll 兜底,统计上会显示"disposed 少、slotsRemoved 多")
|
|
231
|
+
addEffect(pluginId, { kind: 'slot', label: slotName, dispose });
|
|
232
|
+
ok += 1;
|
|
233
|
+
} else {
|
|
234
|
+
rejects.push({ id: spec.id, reason: '插槽注册被拒(见 stats.rejects)' });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
say.info(`插件 ${pluginId} 提交控件节(兼容层 → 插槽):分组「${groupName}」→ ${slotName},成功 ${ok}/${controls.length}`
|
|
238
|
+
+ (rejects.length ? `,被拒 ${rejects.length}` : ''));
|
|
239
|
+
publishSlotStats();
|
|
240
|
+
return ok > 0;
|
|
241
|
+
}
|
|
242
|
+
/** 提示通道:插件不直接操作 DOM,走这里(同时派发事件给页面其它部分) */
|
|
243
|
+
function notify(kind, msg) {
|
|
244
|
+
const level = kind === 'error' ? 'error' : kind === 'warn' ? 'warn' : 'info';
|
|
245
|
+
say[level](`[插件] ${msg}`);
|
|
246
|
+
try { dispatch(EVENTS.NOTIFY, { kind, msg }); } catch { /* 忽略 */ }
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* 插槽注册(设计 §四):**只登记、不渲染**(渲染在 S2 接)。
|
|
250
|
+
*
|
|
251
|
+
* 插件侧写法(经典脚本,必须在脚本顶层**同步**调用):
|
|
252
|
+
* const dispose = window.ModelStagePlugins.slots.register('panel.group.settings', {
|
|
253
|
+
* id: 'plugin.<pluginId>.sharpness', // 必填,必须带插件前缀(设计 §8.2)
|
|
254
|
+
* order: 100, // 可选,列表插槽按它排序(小在前)
|
|
255
|
+
* label: '清晰度', // 必填,诊断与冲突提示都用它
|
|
256
|
+
* render(props) { return { kind: 'segmented', options: ['关', '轻', '强'] }; },
|
|
257
|
+
* onAction(action, props) { … },
|
|
258
|
+
* });
|
|
259
|
+
*
|
|
260
|
+
* 拒绝原因在 stats().rejects 里可读(unknown-slot / slot-not-open / undeclared-slot / risk-mismatch)。
|
|
261
|
+
* @param {string} name 插槽名(见 /api/plugins 的 slotCatalog)
|
|
262
|
+
* @param {object} spec { id, order?, label?, render?, onAction? }
|
|
263
|
+
* @returns {null | (() => void)} disposer(已登记进该插件的 effect 容器,卸载时自动回收)
|
|
264
|
+
*/
|
|
265
|
+
const slots = {
|
|
266
|
+
register(name, spec) {
|
|
267
|
+
if (!getSlotRegistry()) {
|
|
268
|
+
say.warn('插槽注册失败:注册表尚未就绪(插件清单还没拿到,通常是页面刚启动)');
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const dispose = getSlotRegistry().register(getLoadingPluginId(), name, spec);
|
|
272
|
+
if (typeof dispose === 'function') {
|
|
273
|
+
// 挂进 S1-b1 的 effect 容器 ⇒ unloadPlugin 会连插槽登记一起回收
|
|
274
|
+
addEffect(getLoadingPluginId(), { kind: 'slot', label: String(name), dispose });
|
|
275
|
+
}
|
|
276
|
+
publishSlotStats();
|
|
277
|
+
return dispose;
|
|
278
|
+
},
|
|
279
|
+
/** 只读快照(同时挂到 window.__SLOT_STATS__ 供测试断言) */
|
|
280
|
+
stats: () => (getSlotRegistry() ? getSlotRegistry().stats() : {
|
|
281
|
+
openStage: 'S1', catalogSize: 0, total: 0, byPlugin: {}, entries: [], rejects: [],
|
|
282
|
+
}),
|
|
283
|
+
};
|
|
284
|
+
return { registerProvider, registerEffect, registerControls, slots, notify };
|
|
285
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/web/infra/plugin-host-report.mjs —— 宿主报告(从 plugin-host.js 抽出,B1)
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成模块:它是"纯读"(只顺着 ctx 读状态,不写、不产生副作用),
|
|
4
|
+
// 是所有内部块里依赖最少的一块(只依赖 ctx),也是最安全的第一块搬迁。
|
|
5
|
+
//
|
|
6
|
+
// 依赖约定:
|
|
7
|
+
// · 只接收 ctx(9 个高频共享状态的容器);say/emit 也在 ctx 里,但本模块**不用**它们
|
|
8
|
+
// · 不做 IO、不派发事件、不改任何状态 —— 保证"报告"永远不会影响宿主行为
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {{ctx: object}} deps
|
|
12
|
+
* @returns {() => object} report():装载状态 + 已注册提供者的排障快照
|
|
13
|
+
*/
|
|
14
|
+
export function createReport({ ctx }) {
|
|
15
|
+
/** 排障快照:装载状态 + 已注册提供者 */
|
|
16
|
+
function report() {
|
|
17
|
+
return {
|
|
18
|
+
loaded: ctx.loaded,
|
|
19
|
+
discovered: ctx.discovered.map((p) => p.id),
|
|
20
|
+
providers: [...ctx.providers.values()].map((p) => ({
|
|
21
|
+
id: p.id,
|
|
22
|
+
provides: Array.isArray(p.provides) ? p.provides.slice() : [],
|
|
23
|
+
kind: p.kind || null,
|
|
24
|
+
fileKinds: Array.isArray(p.fileKinds) ? p.fileKinds.slice() : [],
|
|
25
|
+
hasCanHandle: typeof p.canHandle === 'function',
|
|
26
|
+
hasResolve: typeof p.resolve === 'function',
|
|
27
|
+
})),
|
|
28
|
+
loadState: [...ctx.loadState.entries()].map(([id, s]) => ({ id, ...s })),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return report;
|
|
32
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// src/web/infra/plugin-host-unload.mjs —— 卸载与回收(从 plugin-host.js 抽出,B1)
|
|
2
|
+
//
|
|
3
|
+
// 为什么单独成模块:卸载是"有明确边界的一组收缩动作"(逆序跑 disposer → 清四张表 → 派发事件),
|
|
4
|
+
// 它只**写** ctx 里的集合,不参与装载/解包逻辑;搬出去后 plugin-host 里只剩"谁调它"。
|
|
5
|
+
//
|
|
6
|
+
// 依赖约定(明确传参,不从 ctx 里取工具):
|
|
7
|
+
// · ctx —— 9 个高频共享状态容器(providers/pluginEffects/registeredEffects/loadState/slotRegistry)
|
|
8
|
+
// · manifestEffects —— 清单声明的效果表(低频状态,留在宿主,这里按引用注入)
|
|
9
|
+
// · workerPool —— providerId → worker(低频状态,同上)
|
|
10
|
+
// · publishSlotStats —— 插槽统计发布(宿主里的工具函数)
|
|
11
|
+
// · publishEffectStats —— 效果统计发布(同上)
|
|
12
|
+
// 日志与事件:say/emit 在 ctx 里,但调用口径与搬迁前完全一致(文案逐字未改)。
|
|
13
|
+
//
|
|
14
|
+
// ⚠️ 事件名见 `EVENTS.PLUGINS_LOADED`(infra/events.mjs),是**外部契约**(panel-settings 监听并重建面板),
|
|
15
|
+
// 改动它等于"卸载后界面不刷新"——本次搬迁逐字保留。
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {{ctx: object, manifestEffects: Map, workerPool: Map,
|
|
19
|
+
* publishSlotStats: () => void, publishEffectStats: () => void}} deps
|
|
20
|
+
* @returns {{unloadPlugin: (pluginId: string) => Promise<object>, unloadAll: () => Promise<object[]>}}
|
|
21
|
+
*/
|
|
22
|
+
import { EVENTS } from './events.mjs';
|
|
23
|
+
|
|
24
|
+
export function createUnload({ ctx, manifestEffects, workerPool, publishSlotStats, publishEffectStats }) {
|
|
25
|
+
/**
|
|
26
|
+
* 卸载一个插件:逆序跑它的 disposer,再从各张表里摘干净,最后派发事件让面板重建。
|
|
27
|
+
*
|
|
28
|
+
* 为什么必须派发事件:宿主表清掉、DOM 不会自己变;panel-settings 监听
|
|
29
|
+
* `EVENTS.PLUGINS_LOADED` 后重建,控件才真的从界面上消失(只摘宿主表不改 DOM 等于没卸载)。
|
|
30
|
+
* @param {string} pluginId
|
|
31
|
+
* @returns {{pluginId:string, disposed:number, failures:string[], removedEffects:number, slotsRemoved:number, hadProvider:boolean}}
|
|
32
|
+
*/
|
|
33
|
+
async function unloadPlugin(pluginId) {
|
|
34
|
+
const id = String(pluginId || '');
|
|
35
|
+
const list = ctx.pluginEffects.get(id) || [];
|
|
36
|
+
// 先记"卸载前有没有",再回收:disposer 会把 provider 删掉,之后再查就永远是 false(返回值会误导排障)
|
|
37
|
+
const hadProviderBefore = ctx.providers.has(id);
|
|
38
|
+
let disposed = 0;
|
|
39
|
+
const failures = [];
|
|
40
|
+
for (let i = list.length - 1; i >= 0; i -= 1) { // 逆序:后注册的先撤
|
|
41
|
+
try { list[i].dispose(); disposed += 1; }
|
|
42
|
+
catch (e) { failures.push(`${list[i].kind}:${list[i].label}: ${(e && e.message) || e}`); }
|
|
43
|
+
}
|
|
44
|
+
ctx.pluginEffects.delete(id);
|
|
45
|
+
|
|
46
|
+
let removedEffects = 0;
|
|
47
|
+
for (const [eid, e] of [...ctx.registeredEffects]) {
|
|
48
|
+
if (e && e.pluginId === id) { ctx.registeredEffects.delete(eid); removedEffects += 1; }
|
|
49
|
+
}
|
|
50
|
+
const hadProvider = ctx.providers.delete(id);
|
|
51
|
+
try { manifestEffects.delete(id); } catch { /* 老版本没有这张表 */ }
|
|
52
|
+
const wk = workerPool.get(id);
|
|
53
|
+
if (wk) {
|
|
54
|
+
try { if (wk.worker) wk.worker.terminate(); } catch { /* 已终止 */ }
|
|
55
|
+
workerPool.delete(id);
|
|
56
|
+
}
|
|
57
|
+
// 插槽登记也要回收:disposer 虽已挂在 effect 容器里,这里按 pluginId 再兜一次
|
|
58
|
+
// (历史数据/异常路径可能留下没登记 disposer 的项)
|
|
59
|
+
const slotsRemoved = ctx.slotRegistry ? ctx.slotRegistry.disposeAll(id) : 0;
|
|
60
|
+
publishSlotStats();
|
|
61
|
+
ctx.loadState.delete(id);
|
|
62
|
+
publishEffectStats();
|
|
63
|
+
ctx.say.info(`插件 ${id} 已卸载:执行 ${disposed} 个 disposer${failures.length ? `(${failures.length} 个失败)` : ''}`
|
|
64
|
+
+ `,清掉效果 ${removedEffects} 个、插槽 ${slotsRemoved} 个${hadProvider ? '、提供者 1 个' : ''}`);
|
|
65
|
+
// 让面板重建:只清宿主表不改 DOM 的话,控件会继续挂在界面上(等于没卸载)
|
|
66
|
+
try { window.dispatchEvent(new CustomEvent(EVENTS.PLUGINS_LOADED, { detail: { unloaded: id } })); } catch { /* 忽略 */ }
|
|
67
|
+
return { pluginId: id, disposed, failures, removedEffects, slotsRemoved, hadProvider: hadProviderBefore };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 卸载全部已装载插件(测试收尾 / 热重载前清理用) */
|
|
71
|
+
async function unloadAll() {
|
|
72
|
+
const ids = [...new Set([...ctx.pluginEffects.keys(), ...ctx.providers.keys(), ...ctx.loadState.keys()])];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const id of ids) out.push(await unloadPlugin(id)); // 串行:卸载会改宿主表,别并发
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return { unloadPlugin, unloadAll };
|
|
79
|
+
}
|