插件用于扩展踩点编辑器:导入/导出新格式、数据生成与批量编辑、侧栏浮动静默面板、 自定义快捷键、独立预览窗口,以及针对特定游戏新增“类型化轨道”(如翻转事件), 它们与内置踩点共用同一条节拍时间轴。
完整的接口清单(命名空间 / 方法 / 类型 / 示例)见 插件 API 参考。
插件是一个文件夹,内含:
my-plugin/
├─ manifest.json # 元信息(必需)
├─ main.js # 可选:主进程入口(全 Node + Electron 权限)
├─ renderer.js # 可选:编辑器页面内执行的脚本(注册 UI 贡献)
└─ … # 插件自带的 HTML/静态资源
扫描目录:
- 用户目录
<userData>/plugins(设置页可一键打开); - 开发模式下同时扫描项目根目录的
plugins/(示例插件放这里)。 - manifest 解析失败的插件会列在设置中并显示错误,不影响其余插件。
{
"id": "dev.bdg.example-basic",
"version": "0.1.0",
"name": { "zh": "示例插件", "en": "Example Plugin" },
"description": { "zh": "…", "en": "…" },
"main": "main.js",
"renderer": "renderer.js"
}name/description 可为字符串或 { zh, en }。id 建议反向域名风格且不得含冒号 :。
renderer.js 是普通脚本,通过全局注册函数声明入口。类型提示:
/// <reference path="plugin-api.d.ts" />
window.__bdgPluginRegister(function activate(api) {
// 在此注册所有贡献(见 PluginApi 类型)
return function dispose() {
// 卸载清理(取消订阅等)
};
});可以注册的贡献:
api.ui.registerAction({ label, run })→ 出现在顶部「插件」菜单;api.ui.registerPanel({ id, title, mount, defaultWidth?, defaultHeight?, minWidth?, minHeight? })→ 浮动静默窗口(可拖动、右下角可缩放),mount(hostEl)里用 DOM 自由渲染,返回可选清理函数;返回PanelHandle可open()/toggle()等。窗口的位置与尺寸会按插件+面板 id 自动记忆并在重启后恢复,defaultWidth/Height只作为首次打开的初始尺寸,minWidth/Height为缩放下限;api.ui.registerShortcut({ id, label, combo, run })→combo形如Alt+1、Ctrl+Shift+F;api.ui.registerImporter({ label, run })→ 出现在「文件 → 导入…」;api.ui.registerExporter({ label, run })→ 出现在「导出」菜单的“插件导出”分组;api.ui.registerTimelineOverlay({ id, mount, z? })→ 在时间轴视口内叠加一层,用于可视化/拖拽(见下);api.ui.registerBarButton({ id, title, icon?, toggle?, active?, order?, run })→ 在工程栏快捷行(pb-quick)加一个小按钮(见下);api.trackTypes.register({ id, trackName, pointName, color?, fields })→ 新增类型化轨道,侧栏+可创建,点在属性卡里编辑字段。
本系统不提供“替换核心 UI”的能力(插件无法接管/替换 Timeline、Transport 等宿主组件)。插件对界面的扩展方式只有浮窗面板与时间轴叠加层,宿主内部实现重构不会破坏插件。
菜单入口只保留一个:注册面板(
registerPanel)已自动在「插件」菜单生成一个带开合状态的菜单项, 因此不要再用registerAction重复开关同一个面板,否则会出现两个功能重复的入口。
api.ui.registerTimelineOverlay 在时间轴视口内挂载一层 position:absolute; inset:0; pointer-events:none 的容器,mount(container) 里用 DOM 自由绘制,返回可选清理函数。
用来把“区间选区、手柄、参考线”等贴到节拍时间轴上。
- 坐标换算:用
api.timeline.timeToScreenX(ms)/api.timeline.screenToTime(x)/api.timeline.timeToX(ms)在时间与像素间换算;api.timeline.viewport()给出当前 滚动与缩放(x/y/vw/vh/pxPerSec);api.timeline.durationMs()为当前音频时长。 - 跟随缩放/滚动:订阅
api.events.on("view", cb)(滚动、缩放、尺寸变化时触发, 已按帧节流)重绘,否则叠加层不会随视口变化。 - 指针仲裁(重要):容器是
pointer-events:none,只有需要交互的子元素自行设pointer-events:auto;且这些元素在pointerdown/move/up里必须调用stopPropagation()(并用setPointerCapture),否则事件会穿透到下方画布,触发 编辑器的放置/拖拽。
不要在插件里用 localStorage(会被清缓存/E2E 环境清掉,且与其他插件混在一起,受同源限制)。
用 api.storage —— 一个按插件隔离、由主进程写到磁盘(<userData>/plugin-data.json)的键值库:
api.storage.get(key, def?)/set(key, value)/remove(key)/all()/clear();- 同步读写(像 localStorage),写入会被合并后稍后落盘;插件卸载时自动 flush;
- 数据按插件 id 命名空间隔离,插件之间互不可见。
var snap = api.storage.get("snapDiv", 4); // 读取,缺省 4
api.storage.set("snapDiv", 8); // 写入(自动持久化)只适合存放配置/偏好等小数据,不要塞入大块内容(音频/工程数据请走文件)。
api.ui.registerBarButton 在工程栏的快捷操作行(pb-quick)注册一个小按钮,适合
“一键开/关某功能”。icon 是插件自带的内联 SVG 字符串;toggle: true 时宿主在
点击后翻转按下态并把新值传给 run(active),按钮随之高亮。返回的句柄可
setActive(v) / isActive() 编程控制与 dispose() 注销。
var handle = api.ui.registerBarButton({
id: "toggle-x",
title: { zh: "功能 X", en: "Feature X" },
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor"><path d="..."/></svg>',
toggle: true,
active: false,
order: 100,
run: function (active) { console.log("enabled:", active); },
});
// later: handle.setActive(true)最小片段:
var el = null;
api.events.on("view", function () {
if (!el) return;
el.style.left = api.timeline.timeToScreenX(2000) + "px"; // 时间轴上的 2s 处
});
api.ui.registerTimelineOverlay({
id: "my-overlay",
mount: function (host) {
el = document.createElement("div");
el.style.cssText = "position:absolute;top:0;bottom:0;width:2px;background:#f00";
host.appendChild(el);
return function () { el = null; host.textContent = ""; };
},
});在主进程加载,拥有完整 Node / Electron:
module.exports = function activate(ctx) {
ctx.log("loaded", ctx.dir);
ctx.registerHandler("ping", () => "pong");
ctx.onDispose(() => {});
};渲染进程用 api.callMain(method, ...args) 调本插件注册的处理器。activate 需保持同步。
- 快照:
api.project.snapshot()一次给出节拍视角与timeMs时间视角(见字段)。 - 编辑一律走
api.project.edit.*,自动计入撤销栈;多步编辑用edit.batch(fn)合并为一次撤销。 - 循环组:主踩点可用
edit.setMarkerLoop(id, { interval, count, exclude? })按「间隔拍数 × 个数」 批量生成子点;第 k 个子点落在主点拍位 + k × interval,与同轨道已有踩点重合的位置不生成。count上限 256(超出按 256 处理),exclude按 1 起编号跳过指定子点(如[2, 5]跳过第 2、5 个)。传null清除循环并删除其子点;id不是主踩点或所在轨道被锁定时 该调用无效果。调用计入撤销栈,子点在快照里带parentId与loop字段。 - 类型化轨道:轨道带
type: "<pluginId>:<localId>";点带attrs,字段默认值在放置/粘贴时自动补齐,或由插件用setMarkerAttrs修改。 - 内置导出(.txt / EDL / 踩点指示灯)不包含类型化轨道;插件导出自行读取。
.bdg直接存type+attrs,字段全可选、向后兼容。若保存的工程含某插件类型而该插件未安装:轨道与数据照常显示(点属性只读),属性卡提示需要安装对应插件。
编辑器渲染层是沙箱(sandbox:true + contextIsolation)。插件拿到的是受限桥接:
- 数据/编辑/播放/选区/事件、
api.timeline.*(只读时间轴换算)、api.system.pickFile/saveFile/readText/writeText/readBytes/writeBytes、openWindow(加载任意页面)、api.system.audioPath()(当前加载音频的绝对路径,便于 main.js 用 Node 读取并打包)、callMain; - 音频能力:
api.audio.getBuffer()(当前解码后的AudioBuffer)、api.audio.encodeWav(buffer, startMs?, endMs?)(转 16-bit PCM WAV 字节)、api.audio.replaceFromBytes(bytes, { filePath, name?, autoApply? })(解码并替换当前音频,刷新波形/重链/MD5/分析;autoApply默认false)。注意替换音频不进入撤销栈。 - 需要任意 Node 能力时让插件自带
main.js处理。本系统不弹权限确认,安装插件即视为信任。
plugins/example-basic 覆盖:面板、快捷键、导入/导出、main 往返调用、类型化轨道注册、
api.storage 持久化(并按上文约定只保留一个菜单入口)。
音频裁剪插件(独立仓库 beat-data-generator/bdg_plugin_audiotrim)
演示时间轴叠加层 + 拖拽手柄 + api.timeline 换算、api.audio 编码/替换与 system.writeBytes。
开发时把工程目录当扫描根即可(见上),发布则把插件放入 <userData>/plugins(或从应用内插件市场安装)。
设置 → 插件 → 插件市场 提供应用内的一键安装/更新/卸载:
- 数据来自官方注册表
beat-data-generator/registry的registry.json(可用环境变量BDG_MARKET_REGISTRY覆盖地址)。 - 每个版本都带 SHA-256 校验和,安装前强制校验;产物为 HTTPS 下载的 ZIP,解压时 会拦截路径穿越与超大文件。
- 市场安装的插件落在
<userData>/plugins/<id>,并写入.installed.json回执; 只有带回执的插件才能从市场卸载,开发/手动放入的插件不受影响。 - 插件可获得完整 Node 权限,安装前请确认来源可信(界面会给出提示)。
- 缓存:索引缓存在
<userData>/market-cache.json。进入市场优先读缓存,超过 有效期(市场页可选 1 天 / 3 天 / 一周 / 一个月)才重新拉取;手动“刷新市场” 始终重新拉取。网络失败时会回退到缓存。 - 网络设置(设置 → 网络):代理来源可选 系统代理 / 读取环境变量
(
HTTP_PROXY、HTTPS_PROXY、ALL_PROXY) / 不使用;还可开启 GitHub 加速, 从预设 gh-proxy 镜像中选择或自定义地址,并可一键测试各镜像延迟。 - 本地安装:市场页“从 ZIP 安装”可选择一个本地
.zip(包根含manifest.json), 走与市场一致的解压/路径校验流程;来源记为file:<path>并写入安装回执。
市场的分类芯片来自注册表条目的 categories 字段(字符串数组,小写 slug),
用于市场页顶部筛选与详情展示。约定使用以下固定品类(顺序即展示顺序):
| slug | 中文 | 说明 |
|---|---|---|
export |
导出 | 新增导出格式 / 导出目标 |
import |
导入 | 导入新格式的数据 |
integration |
联动 | 与外部工具/进程联动、桥接 |
visual |
界面 | 界面外观、主题、可视化增强 |
utility |
工具 | 通用辅助工具、批量编辑 |
analysis |
分析 | 音频/节拍分析等数据洞察 |
- 一个插件可同时属于多个品类;未知 slug 仍会被列出并做首字母美化,但不会本地化。
新增品类需同时更新
src/shared/market.ts的MARKET_CATEGORIES与settings.plugins.cats的 i18n 文案。 - slug 会被宿主统一转为小写、去空格并去重,因此注册表无需关心大小写与重复。
发布流程:插件仓库打 vX.Y.Z tag(与 manifest.json 的 version 一致)→
组织级工作流打包 plugin.zip 并创建 Release → registry 定时任务读取 Release
资产的 SHA-256 写入索引 → 应用内即可安装。详见
bdg_plugin_template
的 README。
npm run typecheck # 改动编辑器代码后跑类型检查
npm run build- 你编写的插件属于你自己的作品(版权归你),可自行选择开源协议。
- 宿主编辑器 Beat Data Generator 以 GNU GPL v3 发布(作者 BUGJI)。插件由宿主加载器装载运行,分发插件时建议注明与宿主的关联。
- 官方插件模板/脚手架见 https://github.com/beat-data-generator/bdg_plugin_template。