编辑器 API 与交互
以下都是 XenolithEditor.init(...) 返回的 editor 对象上的方法。
构建图
// 添加你自己构造的节点。editor.addNode(node, { category: 'logic', title: 'Source', collapsed: false })
// 连接输出引脚到输入引脚(节点 `pins` 数组的索引)。editor.connect(source, 0, sink, 1, { sourceType: 'float' })不过大多数应用不会手动构造节点——它们加载一个文档。
加载与保存(xenolith.v1 格式)
编辑器使用一种规范的 JSON 格式。loadJSON 替换整个图、选区和视口;toJSON 再序列化回去。
editor.loadJSON(graphDoc) // 从 xenolith.v1 文档替换场景const doc = editor.toJSON() // 序列化节点 + 连线 + 渲染选项 + 视口文档为 { version: 'xenolith.v1', nodes: [...], edges: [...], viewport? }。引脚、尺寸、折叠状态、category/title 以及每条连线的 sourceType 都会序列化。推荐用这种方式初始化图——把它写成数据,注册对应 schema,然后加载:
for (const schema of schemas) editor.registry.register(schema) // 让 Tab 能生成它们editor.loadJSON(graphDoc)editor.fitView()自适应视图
fitView 计算所有节点的包围盒并在画布中框住它——无论图多大,都能可靠地展示刚加载的图。
editor.fitView() // 64px 边距,不会放大超过 1×editor.fitView({ padding: 80, maxZoom: 1 })editor.resetView() // 回到单位视图| 选项 | 默认值 | 含义 |
|---|---|---|
padding | 64 | 四周保留的屏幕边距 |
maxZoom | 1 | 不放大超过此值(避免小图占满屏幕) |
minZoom | 编辑器的缩放下限 | 不缩小超过此值 |
插入节点与 reroute
// 在世界坐标生成已注册的 schema(可撤销,并选中)。editor.insertNode('Transform', { x: 320, y: 200 })editor.insertNode('Transform', midpoint, { center: true }) // 以该点为中心
// 在世界坐标处用内联 reroute 切分连线。editor.insertRerouteOnEdge(edgeId, worldPos)
// 仅删除一条连线;悬空的内联 reroute 会被自动清理。editor.deleteEdge(edgeId)两种 reroute
- 内联节点(
$reroute)——通过切分连线创建。不可拉拽的圆点,传递连线并取连线的类型颜色。它无法独立存在:删除最后一条连接就会移除它。 - Reroute 节点(
Reroute)——可移动的矩形中继,可以从中拉出新连线。它是内置 schema,始终出现在插入面板中。
开箱即用的交互
| 手势 | 操作 |
|---|---|
Tab / 双击空白画布 | 打开插入面板(模糊搜索;内置节点排在前面) |
| 右键连线中点圆点 | 上下文菜单:Add Node(仅兼容类型)· Add Reroute · Delete |
| 从引脚拖拽 | 实时幽灵连线,带类型兼容校验 |
Alt+拖拽已连接引脚 | 断开连线并重连 |
| 拖拽节点 / 框选 | 移动(8px 吸附,Alt 关闭)/ 框选 |
Delete · ⌘Z / ⌘⇧Z · ⌘C/⌘V · ⌘D · ⌘A | 删除 · 撤销/重做 · 复制/粘贴 · 复制副本 · 全选 |
加载遮罩
繁重的首次渲染(大图)可以包裹在可主题化的模糊 + 加载圈遮罩中。它先绘制,在模糊背后执行你的工作,然后淡出——大图平滑显现,而不是先卡住再弹出。由当前主题的 paletteStyle 控制样式。
await editor.withOverlay('Rendering…', () => { editor.loadJSON(bigGraph) editor.fitView()})
// 或手动控制:editor.showOverlay('Loading…')editor.hideOverlay()控件(Widgets)
节点可携带内联控件。控件是节点(或其 schema)上的声明式数据;值存于 node.state[key],因此随图序列化,且每次修改都可撤销。
widgets: [ { id: 'steps', type: 'number', label: 'Steps', key: 'steps', min: 1, max: 150, step: 1 }, { id: 'cfg', type: 'slider', label: 'CFG', key: 'cfg', min: 0, max: 20, step: 0.1 }, { id: 'sampler',type: 'combo', label: 'Sampler', key: 'sampler', values: ['euler', 'dpm++', 'ddim'] }, { id: 'prompt', type: 'text', label: 'Prompt', key: 'prompt', multiline: true }, { id: 'hires', type: 'toggle', label: 'Hi-res', key: 'hires' }, { id: 'tint', type: 'color', label: 'Tint', key: 'tint' }, { id: 'run', type: 'button', label: 'Run', action: 'run' },]内置类型:number(拖拽 + 点击输入)、slider、combo、text(multiline)、toggle、color(弹出取色器)、button。
editor.getWidgetValue(nodeId, 'steps')editor.setWidgetValue(nodeId, 'steps', 30) // 钳制、可撤销editor.on('widget:action', ({ nodeId, action }) => { /* 按钮被按下 */ })每个控件可通过 color.widget / geometry.widget 令牌全局主题化,也可通过 style 逐实例覆盖:
{ id: 'gain', type: 'slider', label: 'Gain', key: 'gain', min: 0, max: 1, style: { fill: '#FF3366', radius: 10, borderFocused: '#FF3366' } }自定义控件
注册一个控制器,并由 custom 控件的 renderer 引用。两种控制器:
// canvas-draw(快——绘制到 WebGL 纹理,无 DOM):editor.registerWidget('curve', { draw(ctx, { value, width, height }) { /* 2D-canvas 绘制 */ }, onPointer(phase, x, y, ctx) { return newValue },})
// dom-mount(任意 HTML——React/Vue/Svelte 适配器封装的契约):editor.registerWidget('chart', { mount(el, { value, setValue }) { /* 把组件挂载到 el */; return () => {} }, update({ value }) {},})canvas-draw 是高性能默认;DOM 控件由编辑器在平移/缩放/拖拽时保持贴合节点的屏幕矩形。
ComfyUI 导入
importComfyWorkflow 按值类型将每个节点的位置型 widgets_values 映射为类型化控件(数字→number、布尔→toggle、字符串→text)。名称/下拉/范围需要服务器的 object_info,属于后续升级。
插件
editor.use(plugin) 安装一个插件。插件形如 { name, install(ctx) };install 仅执行一次,可返回一个销毁函数,会在 editor.destroy() 时触发。插件让编辑器保持可扩展,而无需把功能写死——节点包、运行时、类型系统、自定义控件和校验器都通过这个入口接入。
editor.use({ name: 'my-plugin', install(ctx) { ctx.registry.register({ type: 'Custom', title: 'Custom', pins: [/* ... */] }) ctx.types.register({ id: 'vec3', color: '#a0f', shape: 'diamond', compatibleWith: ['vec2'] }) ctx.icons.register('star', '<svg>...</svg>') ctx.setIsValidConnection((from, to) => from.node.type !== 'Frozen') ctx.registerWidget('myKnob', { draw(/* ... */) {}, onPointer(/* ... */) {} }) const off = ctx.on('node:added', e => console.log('added', e.id)) return () => off() },})PluginContext 提供:
| 字段 | 用途 |
|---|---|
registry | 节点 schema 注册表(ctx.registry.register(schema)) |
types | 自定义引脚类型的 TypeRegistry(id/color/shape/compatibleWith) |
icons | 图标注册表——内置 Feather 集 + register(name, svg) |
commandBus | 将多次修改合并为一个事务(谨慎使用) |
graph | 图的只读视图 |
app | PIXI Application——仅作为自定义渲染的逃生口;在此修改场景图不在支持范围内 |
registerWidget | 注册 canvas-draw 或 DOM-mount 自定义控件控制器 |
setIsValidConnection | 安装全局连接校验器(在内置类型兼容检查之后运行) |
on | 完整事件总线访问(见 事件) |
运行时委托(用于执行类插件——编辑器本身不附带任何执行):
| 方法 | 用途 |
|---|---|
onTick(cb) | 订阅每帧 tick |
startLoop() / stopLoop() / step() | 启停 tick 循环,或单步一帧 |
setWidgetValue(nodeId, key, value, { ephemeral: true }) | 用于仿真的不可撤销控件写入 |
setNodePins(nodeId, pins) | 可变引脚(Sequence / MakeArray) |
setEdgeAnimated(edgeId, on) | 开关 marching-ants 流动动画 |
expandTemplateInstance(nodeId) | 只读展平模板实例 |
graphSnapshot({ expandMacros, flattenReroutes }) | 用于执行遍历的快照 |
自定义类型
TypeRegistry 定义引脚类型——颜色、形状、以及可自动转换的相关类型。类型决定连线颜色、引脚填充和连接合法性。
editor.types.register({ id: 'float', color: '#36f', shape: 'circle', compatibleWith: ['int'] })editor.types.register({ id: 'exec', color: '#fff', shape: 'arrow' })editor.types.register({ id: 'struct:Vec3', color: '#a0f', shape: 'diamond' })引脚形状:circle(data)、arrow(exec)、diamond(struct/object)。compatibleWith 启用相关类型间的自动转换(如 int → float)。
自定义校验
setIsValidConnection 安装一个谓词,在内置类型兼容检查之后运行。返回 false 即拒绝连接。
editor.setIsValidConnection((from, to) => { return !(from.node.type === 'A' && to.node.type === 'B')})表头图标
图标是节点表头标题旁的小 SVG 图形。内置 Feather 集已预加载;其余按名称注册。
editor.icons.register('zap', '<svg viewBox="0 0 24 24"><path d="..."/></svg>')
editor.registry.register({ type: 'Trigger', title: 'Trigger', glyph: { icon: 'zap', side: 'left' }, pins: [{ kind: 'exec', direction: 'out' }],})
// 节点级覆盖(不修改 schema):editor.setNodeGlyph(nodeId, { icon: 'star', side: 'right' })editor.setNodeGlyph(nodeId, null) // 清除覆盖节点级图标覆盖会随 xenolith.v1 往返保存。
事件
editor.on(event, handler) 返回 Unsubscribe。事件桥接自 command bus,因此撤销/重做也会触发——绑定到事件的 UI 无需在每个修改调用周围加额外逻辑就能保持同步。
const off = editor.on('node:added', ({ id, node }) => {})editor.on('node:removed', ({ id }) => {})editor.on('node:moved', ({ id, position }) => {})editor.on('edge:added', ({ id, edge }) => {})editor.on('edge:removed', ({ id }) => {})editor.on('selection:changed', ({ ids }) => {})editor.on('widget:changed', ({ nodeId, key, value }) => {})editor.on('viewport:changed', ({ x, y, scale }) => {})editor.on('dive:changed', ({ defId, depth, path }) => {}) // 模板进入/退出off()动画连线
沿连线开启流动的 marching-ants 脉冲。随 xenolith.v1 往返保存。
editor.setEdgeAnimated(edgeId, true)editor.setEdgeAnimated(edgeId, false)谨慎使用——只要动画连线仍在屏幕上,就会破坏 render-on-demand。
可变引脚
setNodePins 替换节点的引脚列表。用于引脚数量随状态变化的节点——Sequence(exec out 1..N)、MakeArray(data in 1..N)等。插件通常在 widget:changed 处理器中调用它。
editor.setNodePins(nodeId, [ { kind: 'exec', direction: 'in', label: 'in' }, { kind: 'exec', direction: 'out', label: 'Then 1' }, { kind: 'exec', direction: 'out', label: 'Then 2' },])连接到已移除引脚的连线会被自动清理。
注释
注释是节点后方带标签的矩形——空间分组,而非数据包含。可移动、从角落改尺寸,双击表头重命名。
const id = editor.createComment({ x: 100, y: 100, width: 400, height: 300, title: 'IO', color: '#36c' })editor.setCommentTitle(id, 'Inputs')editor.setCommentColor(id, '#f93')editor.editComment(id) // 开始内联编辑(覆盖文本输入)editor.removeComment(id)快捷键:Tab → Comment,或双击空白区域并在面板中选择 Comment。
宏(分组)
宏将 N 个选中节点打包到一个可折叠容器中,边界上有代理引脚。内联——没有共享定义,没有传播。
const macroId = editor.createMacroFromSelection()const macroId = editor.createMacroFromSelection([id1, id2, id3], 'Backup')editor.collapseMacro(macroId)editor.expandMacro(macroId)editor.ungroupMacro(macroId) // 拆散;成员回到根图快捷键:Cmd+G 将当前选区分组。
Live 模板
模板是带有存储定义的子图。可以从一个定义生成多个实例;通过 dive-in 编辑定义会传播到所有实例。定义内的边界节点 $templateInput / $templateOutput 构成引脚接口——每个持有一个引脚,重命名即设置引脚标签。
const instanceId = editor.createTemplateFromSelection()const instanceId = editor.createTemplateFromSelection([id1, id2], 'My Template')
editor.diveInto(instanceId) // 进入定义(面包屑跟踪路径)editor.diveOut() // 返回父图editor.renameTemplate(defId, 'New name')
// 断开共享链接,得到可编辑的宏:editor.unpackTemplateInstance(instanceId)
// 双向转换:editor.convertTemplateInstanceToMacro(instanceId)editor.convertMacroToTemplate(macroId) // 嵌套宏一并并入定义面板将每个定义列为可插入节点。定义不能插入自身或 dive 栈中任何祖先(递归保护)。
快捷键:Cmd+Shift+G 从选区生成模板;双击实例 dive-in。
虚拟化与 LOD
编辑器会虚拟化视口外的节点,并自动将远处节点降级为低保真表示。默认值针对 ~30k+ 节点调优;只在超小图或自定义渲染器时才需要覆盖。
editor.setVirtualization({ enabled: true, threshold: 300, overscan: 200 })LOD 随缩放下降切换:full → sprite-baked → flat-batch。
Headless 运行时
执行被委托给插件——编辑器自身不附带执行。运行时(如 @xenolithengine/graph-plugin-runtime、Blueprint VM,正在开发中)作为插件安装,使用上面的运行时委托 API。
import { runtime } from '@xenolithengine/graph-plugin-runtime' // 未来包editor.use(runtime({ /* 选项 */ }))editor.startLoop() // 开始 tick——运行时决定执行什么editor.step() // 单帧