@shangchien/mol-editor
v0.1.0
Published
RDKit-powered molecular editor kernel for Vue 3 applications.
Readme
Mol Editor
Mol Editor 是一个基于 RDKit、Vue 3、Vite 和 VRender 的分子画板内核。它的职责是提供稳定的分子编辑、文档模型、交互编排和插件 runtime;业务域能力由宿主与下游插件实现,而不是继续堆进 core。
npm package 使用
当前包面向 Vue 3 应用发布,根入口提供编辑器组件、插件 SDK、文档快照类型和 RDKit runtime 配置;mol-editor/utils 子入口提供浏览器优先的分子解析、校验和 SVG/PNG 生成函数。
pnpm add mol-editorimport { createApp, ref } from 'vue'
import { MolEditor, configureRDKit, type DocumentSnapshot } from 'mol-editor'
import 'mol-editor/style.css'
configureRDKit({ wasmUrl: new URL('mol-editor/RDKit_minimal.wasm', import.meta.url).href })
const documentSnapshot = ref<DocumentSnapshot | null>(null)<template>
<MolEditor
v-model:document-snapshot="documentSnapshot"
:options="{ width: 960, height: 640, readonly: false }"
/>
</template>如果不需要受控模式,也可以只监听 document-change,并通过组件实例调用 getDocumentSnapshot() / restoreDocumentSnapshot() 完成保存与恢复。受控模式和现有命令式 API 会并存,方便宿主逐步迁移。
工具函数
import {
validateSmiles,
smilesToRdkitJson,
smilesToSvg,
smilesToPngBlob,
} from 'mol-editor/utils'
const validation = await validateSmiles('CCO')
if (validation.ok) {
const svg = await smilesToSvg('CCO')
const png = await smilesToPngBlob('CCO', { scale: 2 })
}公开工具函数统一返回 { ok, data } 或 { ok, error }。SVG 返回字符串;PNG 返回 Blob,依赖浏览器栅格化环境,不承诺 Node/SSR 直接可用。
构建与发布
pnpm run build:lib:构建 npm package 产物与.d.tspnpm run build:demo:构建 GitHub Pages demopnpm run pack:smoke:发布前检查 npm tarball 内容- GitHub Pages demo 默认按根路径
/构建,适配自定义域;如需仓库子路径部署,可在仓库变量PAGES_BASE_URL中覆盖为/mol-editor/。RDKit wasm 默认通过 ViteBASE_URL定位;npm consumer 可用configureRDKit({ wasmUrl })接管。
当前仓库的对外主入口已经是 plugin families:
- object:定义对象类型与 live runtime
- ui:定义工具栏、菜单、选区栏、快捷键与命令
- workflow:定义上传、布局、持久化、分享、剪贴板、导入导出等服务
这描述的是 public surface,也已经对应到当前 kernel 主路径。当前 package 入口与 kernel 主装载器都以 plugin families 作为正式插件契约;legacy EditorPlugin / PluginContext 不再作为 public API 暴露,但内核内部仍通过 registryBridge 把 kernel 状态适配为 PluginActivateContext。
当前判断
项目已经从“分子编辑器原型”进入“RDKit 分子画板内核 + 插件平台”阶段:
core / builtin / optional三层装载已进入 live path- direct
ObjectTypePlugin已可通过createRuntime(ctx)接入当前 object controller - direct
UIContributionPlugin已可接入当前 toolbar / context menu / selection bar / shortcut 壳层,并可通过uiComponents渲染 leftDock / slots - direct
WorkflowServicePlugin已可进入 workflow registry;layout / persistence / share / clipboard / upload 已由 kernel 主路径直接消费,import / export 也可通过 registry 提供扩展实现 - public surface 与 kernel 主装载路径都已以 direct activation + plugin families 为主线;legacy
EditorPlugin/PluginContext已退到内部适配层,plugin families 已成为唯一 public 插件契约
当前 live 矩阵
| 装载层 \ 能力族 | object | ui | workflow |
|---|---|---|---|
| core | molecule、connector | 默认壳层与选区工具 | 当前无 |
| builtin | 默认 text、image | 当前不建议重建整套 builtin shell | 默认 workflow 服务:import / export / clipboard / layout / persistence / share |
| optional | 宿主自定义对象 | 宿主自定义按钮、菜单、快捷键与命令 | 宿主覆盖 workflow 服务,如 upload、layout、持久化、分享、导入导出 |
私有项目格式
Mol Editor 的私有项目文件统一使用 .mpz 后缀。内部结构为 gzip(JSON(DocumentSnapshot)),其中 DocumentSnapshot 是当前 normalized 文档快照格式。浏览器分享链接使用同一份快照数据,并通过 ?mpz=<url-safe-base64-gzip> query 字段承载;演示页从 ?smi= 或 ?sdf= 加载后也会升级为 ?mpz= 快照链接。
运行时架构
flowchart TB
Host["Host / Downstream App"] --> MolEditor["MolEditor.vue\nShell Entry"]
MolEditor --> UseEditor["useEditor.ts\nKernel Orchestrator"]
subgraph Kernel["Kernel"]
Services["Kernel Services\nselection / gesture / commands / overlay"]
Stores["DocumentStore + molDataStore\nsnapshot / transaction / history"]
Scene["useScene / useMoleculeScene\nVRender stage + 5 layers"]
Events["Editor Events"]
end
subgraph Plugins["Plugin Runtime"]
Adapter["registryBridge\nPluginActivateContext adapter"]
Registry["PluginRegistryRuntime\nobject / ui / workflow"]
Aggregator["usePluginRegistry\nactivation + UI/object aggregation"]
end
subgraph Controllers["Object Controllers"]
MolCtrl["molecule"]
ConnCtrl["connector"]
TextCtrl["text"]
ImageCtrl["image"]
end
subgraph Shell["Editor Shell"]
Toolbar["Toolbar / Context Menu"]
SelectionBar["SelectionActionBar / overlays"]
DockSlots["LeftDock / Slots / HUD"]
end
UseEditor --> Services
UseEditor --> Stores
UseEditor --> Scene
UseEditor --> Events
UseEditor --> Adapter
Adapter --> Registry
UseEditor --> Aggregator
Aggregator --> Registry
Aggregator --> Controllers
Registry --> Controllers
Registry --> Toolbar
Registry --> SelectionBar
Registry --> DockSlots
Controllers --> Scene
UseEditor --> Toolbar
UseEditor --> SelectionBar
MolEditor --> Toolbar
MolEditor --> SelectionBar
MolEditor --> DockSlots架构评审结论
- 对外契约已经清晰收口到 plugin families,宿主扩展入口明确,
core / builtin / optional三层装载也已经进入当前 live path。 - 内核内部仍处于 controller-first 迁移阶段:
useEditor.ts负责 kernel、registry、scene、interaction 与 public API 的总装配,usePluginRegistry.ts负责 direct runtime 激活与 UI/object 聚合,二者共同构成当前主路径。 - 文档模型已经部分统一到
DocumentStore,但 molecule 仍委托给molDataStore。对于一个主打分子编辑能力、且深度依赖 RDKit 的画板来说,这个特例应保留在 chemistry / molecule store / scene build 边界;后续要收口的是它在 selection / history / workflow / shell 条件判断中的外溢,而不是强行把 molecule 压平成与所有对象完全一致的内部实现。 - workflow registry 现在更准确的定位是“宿主覆盖与扩展边界”:layout / persistence / share / clipboard / upload 由 kernel 直接消费,importer / exporter 则由 shell 或 workflow helper 查询调用。
- scene sync 已经开始向 object controller 下放,但当前驱动与兜底协调仍在
useScene/useEditor一侧,尚未完全变成 controller 自治。
快速开始
不传 plugins 时,编辑器会保留默认 builtin 对象层。宿主通常只需要通过 optionalPlugins 追加 plugin families。
import {
EDITOR_COMMANDS,
MolEditor,
type UIContributionPlugin,
type WorkflowServicePlugin,
} from 'mol-editor'
const hostUiPlugin: UIContributionPlugin = {
manifest: {
id: 'host:ui.viewport',
name: 'Host Viewport Actions',
version: '1.0.0',
},
toolbar: [
{
id: 'host:ui.viewport.autofit',
slot: 'top-utility',
label: '重置视图',
commandId: EDITOR_COMMANDS.viewAutofit,
},
],
}
const hostWorkflowPlugin: WorkflowServicePlugin = {
manifest: {
id: 'host:workflow.upload',
name: 'Host Upload Workflow',
version: '1.0.0',
},
upload: {
id: 'host:upload',
accept: ['image/*'],
async upload(file) {
const formData = new FormData()
formData.append('file', file, 'image.png')
const res = await fetch('/api/upload', { method: 'POST', body: formData })
const data = await res.json()
return { url: data.url }
},
},
}<template>
<MolEditor
:optional-plugins="[hostUiPlugin, hostWorkflowPlugin]"
:options="{ width: 960, height: 640, readonly: false }"
/>
</template>Plugin 使用案例教程
下面用一个宿主场景把最常见的 plugin 接法串起来。假设宿主想做四件事:
- 在 toolbar 放一个“重置视图”按钮
- 在 leftDock 挂一个宿主 inspector,在 HUD 放状态组件
- 接入 upload、share 和 exporter 这类 workflow 能力
- 试验一个自定义 object plugin
1. 先准备公共常量与组件映射
import { ref } from 'vue'
import {
EDITOR_COMMANDS,
type DescriptorCommand,
type ObjectControllerRuntime,
type ObjectDescriptor,
type ObjectTypePlugin,
type UIContributionPlugin,
type WorkflowServicePlugin,
} from 'mol-editor'
import HostInspectorPanel from './HostInspectorPanel.vue'
import HostStatusHud from './HostStatusHud.vue'
const INSPECTOR_ICON = '<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 5.5C4 4.67 4.67 4 5.5 4H18.5C19.33 4 20 4.67 20 5.5V18.5C20 19.33 19.33 20 18.5 20H5.5C4.67 20 4 19.33 4 18.5V5.5Z" stroke="currentColor" stroke-width="1.8"/><path d="M8 9H16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8 12H16" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/><path d="M8 15H13" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>'
const uiComponents = {
'host:inspector-panel': HostInspectorPanel,
'host:status-hud': HostStatusHud,
}当前壳层会直接消费 SVG 字符串,因此 iconKey 建议直接传 SVG。leftDock.panelComponent 与 slots.component 则通过 uiComponents 做字符串到 Vue 组件的映射。
2. 案例一:UI 插件,把按钮、右键菜单、选区栏和面板挂进去
const hostCommands: DescriptorCommand[] = [
{
id: 'host:command.autofit',
title: '重置视图',
async run(ctx) {
await ctx.services.commands.execute(EDITOR_COMMANDS.viewAutofit)
ctx.events.emit('host:ui.command', { id: 'autofit' })
},
},
{
id: 'host:command.openInspector',
title: '打开宿主检查面板',
run(ctx) {
ctx.events.emit('host:ui.command', { id: 'open-inspector' })
},
},
]
export const hostUiPlugin: UIContributionPlugin = {
manifest: {
id: 'host:ui.workspace',
name: 'Host Workspace UI',
version: '1.0.0',
},
commands: hostCommands,
toolbar: [
{
id: 'host:toolbar.autofit',
slot: 'top-utility',
label: '重置视图',
commandId: 'host:command.autofit',
},
],
contextMenu: [
{
id: 'host:context.inspect',
label: '查看宿主面板',
iconKey: INSPECTOR_ICON,
commandId: 'host:command.openInspector',
},
],
selectionBar: [
{
id: 'host:selection.inspect',
label: '检查选区',
iconKey: INSPECTOR_ICON,
visibility: ({ selectionCount, readonly }) => ({
visible: selectionCount > 0,
enabled: !readonly,
}),
commandId: 'host:command.openInspector',
},
],
leftDock: [
{
id: 'host:leftDock.inspector',
label: 'Inspector',
iconKey: INSPECTOR_ICON,
panelComponent: 'host:inspector-panel',
},
],
slots: [
{
id: 'host:hud.status',
slot: 'hud',
component: 'host:status-hud',
},
],
}这类 UI plugin 适合做三件事:
- 用
commands连接宿主动作和内建命令 - 用
visibility读取共享选区上下文,而不是自己维护一套 UI 状态 - 用
leftDock和slots把宿主 Vue 组件挂进编辑器壳层
3. 案例二:Workflow 插件,把 upload、share 和 exporter 接到宿主侧
export const hostWorkflowPlugin: WorkflowServicePlugin = {
manifest: {
id: 'host:workflow.io',
name: 'Host Workflow IO',
version: '1.0.0',
},
upload: {
id: 'host:upload',
accept: ['image/*'],
async upload(file) {
const formData = new FormData()
formData.append('file', file, 'upload.bin')
const res = await fetch('/api/upload', { method: 'POST', body: formData })
const data = await res.json()
return { url: data.url }
},
},
share: {
id: 'host:share',
async share(_ctx, nodeIds) {
const params = new URLSearchParams()
if (nodeIds.length > 0) {
params.set('selection', nodeIds.map(String).join(','))
}
return {
url: `https://host.example.com/mol-editor/share?${params.toString()}`,
}
},
},
exporters: [
{
id: 'host:export.selection.json',
mime: 'application/vnd.host.mol-editor.selection+json',
async serialize(nodes) {
return JSON.stringify(nodes, null, 2)
},
},
],
}当前 workflow 边界建议这样理解:
upload、share、layout、persistence、clipboard会被 kernel 主路径直接读取importers、exporters通过 registry 注册,再由 shell 或 workflow helper 在需要时查询- 如果宿主只需要上传,可以继续传
uploadService便捷 prop;但长期建议仍是正式WorkflowServicePlugin
4. 案例三:Object 插件,先做一个最小运行时骨架
type HostNotePayload = {
kind: 'host:note'
text: string
}
const hostNoteDescriptor: ObjectDescriptor<HostNotePayload> = {
id: 'host:note',
kind: 'host:note',
family: 'leaf',
title: 'Host Note',
schemaVersion: 1,
}
export const hostNotePlugin: ObjectTypePlugin = {
manifest: {
id: 'host:note.object',
name: 'Host Note Object',
version: '1.0.0',
},
descriptors: [hostNoteDescriptor],
createRuntime(ctx) {
const selectedIds = ref(new Set<string>())
const controller: ObjectControllerRuntime = {
key: 'host:note',
descriptor: hostNoteDescriptor,
actions: {},
selectedIds,
hitObject: () => null,
isSelected: id => selectedIds.value.has(id),
selectObject: id => {
selectedIds.value = id ? new Set([id]) : new Set()
},
clearSelection: () => {
selectedIds.value = new Set()
},
getSelectedObject: () => null,
beginDrag: () => false,
updateDrag: () => {},
endDrag: () => {},
cancelDrag: () => {},
}
ctx.events.emit('host:object.runtime-ready', {
key: controller.key,
moleculeCount: ctx.molecule.groupList.value.length,
})
return { controllers: [controller] }
},
}这个骨架说明了三件事:
descriptors负责 kind 身份与生命周期契约createRuntime(ctx)负责把 controller 接进当前 object plane- 如果插件需要感知分子平面,请走稳定的
ctx.molecule边界,而不是直接依赖内部molDataStore或 RDKit 运行时对象
5. 案例四:在宿主里把这些插件装起来
const optionalPlugins = [
hostUiPlugin,
hostWorkflowPlugin,
hostNotePlugin,
]<template>
<MolEditor
:optional-plugins="optionalPlugins"
:ui-components="uiComponents"
:options="{ width: 960, height: 640, readonly: false }"
@document-change="onDocumentChange"
/>
</template>装载时可以按下面这条规则判断:
optionalPlugins用于在 core + builtin 之后追加宿主能力,这是最常见的接法plugins用于替换 builtin 层,只有当你想完全接管默认 text / image / workflow 时才需要uiComponents只负责把字符串组件 ID 解析成真实 Vue 组件,不参与 plugin 注册
三类插件分别负责什么
ObjectTypePlugin
用于新增或接管一种文档对象类型。当前 public 形态由两部分组成:
descriptors:定义 kind、schema 和对象级生命周期createRuntime(ctx):返回 object controllers,通过标准 runtime 接口把对象接入当前命中、选区、拖拽、工具和 scene sync 主路径
UIContributionPlugin
用于扩展当前编辑器壳层:
toolbarcontextMenuselectionBarleftDockshortcutscommandsslots
当前 leftDock.panelComponent 与 slots.component 会通过 MolEditor 的 uiComponents 映射解析为真实 Vue 组件;toolbar 六个位置也已直接按 toolbar layout model 渲染,legacy toolbar slot 名只在 compatibility boundary 归一化。
WorkflowServicePlugin
用于连接宿主与外部世界:
uploadlayoutpersistenceshareclipboardimportersexporters
workflow registry 负责提供宿主覆盖与扩展入口。当前 layout / persistence / share / clipboard / upload 已由 kernel 主路径直接消费;import / export 也可通过 registry 提供 importer / exporter,并在 shell 与 workflow helper 中被查询调用。
当前缺口
useEditor.ts仍承担较大的交互装配职责,后续仍需继续压缩 orchestration 与 interaction wiring 的耦合面- molecule 会继续保留
molDataStore+ RDKit 的特化路径,但 selection snapshot、kernel services 与 shell 判断仍需减少对这层特化的外溢依赖 - scene helper 与 workflow helper 仍有内部协调边界,需要继续向 registry / controller-first 主路径收口
- 仍需补齐更高层组件级与集成测试基线
文档导航
- PRD.md:产品定位、能力矩阵与阶段目标
- ARCHITECTURE.md:内核结构、registry runtime 与剩余架构缺口
- docs/PLUGIN_ARCHITECTURE.md:plugin families 设计与 live 支持边界
- docs/HOST_INTEGRATION.md:宿主接入、能力裁剪与命令扩展
- src/editor/plugins/README.md:插件作者快速上手
开发命令
pnpm install
pnpm run dev
pnpm run typecheck
pnpm run test:run下一阶段重点
- 继续压缩 workflow / scene helper 的内部耦合,进一步降低宿主与插件作者对内核实现细节的感知
