跳转到内容

编辑器 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() // 回到单位视图
选项默认值含义
padding64四周保留的屏幕边距
maxZoom1不放大超过此值(避免小图占满屏幕)
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(拖拽 + 点击输入)、slidercombotextmultiline)、togglecolor(弹出取色器)、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图的只读视图
appPIXI 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 启用相关类型间的自动转换(如 intfloat)。

自定义校验

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)

快捷键:TabComment,或双击空白区域并在面板中选择 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() // 单帧