- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
@open-pencil/vue是 OpenPencil 对外提供的 Vue 前端 SDK,它把框架无关的核心引擎@open-pencil/core适配为 Vue 注入上下文、响应式 Composables 与无样式(headless)结构原语,使开发者可以在自己的产品、内部工具或专用工作流编辑器中嵌入 OpenPencil 的画布与编辑能力。本文以官方 SDK 文档为主线,结合仓库源码,完整讲解 SDK 的定位、设计原则、两层级 API、安装配置、编辑器外壳/导航面板/属性面板的实战构建,以及 v0.14.0 迁移要点。
SDK 定位:OpenPencil 不只是一款独立设计应用
官方文档(德语版入口、英文版入口)开篇即点明:@open-pencil/vue存在的意义,是让 OpenPencil 超越"独立设计应用"这一定位,成为可以嵌入其他产品、内部工具和特定工作流编辑器的工具包(toolkit)。内置的 OpenPencil 应用只是该工具包的一种组合方式,而 SDK 就是用来构建"另一种组合"的途径。
SDK 提供的核心能力包括:
- 通过 Vue 依赖注入(Dependency Injection)提供的编辑器上下文;
- 基于CanvasKit的画布渲染;
- 覆盖**选择(selection)、命令(commands)、菜单(menu)、属性面板(property-panel)、变量(variables)**的 Composables;
- 无样式结构原语,如
PageListRoot、PropertyListRoot、ToolbarRoot; - 内置 i18n 原语,用于菜单、面板、对话框的本地化,以及自定义语言选择器。
不同产品和团队需要不同的编辑界面:有时是完整的图形设计编辑器,有时是嵌入在另一个应用中的紧凑画布,有时是内部工作流工具、模板编辑器,或者围绕窄场景构建的 AI 辅助编辑界面——SDK 正是支撑这些可能性的那一层。
设计原则:Headless First
SDK 的四个设计原则(来自 架构文档)决定了它的 API 形态:
- 无样式优先(Headless first):只提供逻辑与结构,不规定应用的视觉风格;
- Composable 优先于无意义的包装:当不存在需要协调的结构时,直接用 Composable 即可,不要包一层组件;
- 有意的公共 API:稳定导出集中定义在 packages/vue/src/index.ts,这是唯一需要关心的公共面;
- 框架感知(Framework-aware):在
@open-pencil/core之上做 Vue 集成,而不是重复实现编辑器内核。
在此基础上,还有一条职责边界的实用判断法则:如果一段逻辑能在另一个基于 OpenPencil 的应用中复用、且不携带当前应用的样式,它就属于@open-pencil/vue;反之,样式、布局外壳、路由、产品文件流、toast 与菜单等应用专属 UX 则属于你的应用。
两层级 API:Composables 与 Primitives
SDK 的公共 API 分为两个主要层级:
- Composables:提供编辑器状态与相关动作。只需要状态和动作时,从 Composables 开始;
- 组件/Primitives:定义有意义的 UI 结构。当你在开发可复用的编辑器界面构件时,从 Primitives 开始。
官方文档将 API 分为三个区域(详见 API 参考):
- 组件(Components):
CanvasRoot、CanvasSurface、ToolbarRoot、PageListRoot、PropertyListRoot、LayerTreeRoot、ColorPickerRoot、NumberFieldRoot、SegmentedControlRoot等结构原语; - Composables:
useEditor、useCanvas、useSelectionState、useEditorCommands、usePosition、useFillControls等; - 高级 API(Advanced):
useCanvasContext、useLayerTree、useVariables、useNumberField、useToolbar、usePropScrub、toolCursor、extractImageFilesFromClipboard等底层工具。
从 packages/vue/src/index.ts 的导出可以看到,公共面既包括类型导出(Editor、EditorState、EditorOptions、EditorEvents、Tool、EditorToolDef),也包括createEditor、EDITOR_TOOLS、TOOL_SHORTCUTS的转发,以及provideEditor/useEditor/EDITOR_KEY上下文三件套。这印证了文档强调的"稳定的公共 API 集中导出、内部模块不直接对外"的设计。
架构上还要避免"宽泛的 context-dump 插槽":优先使用聚焦的 slot props 或直接使用 Composable,而不是把巨大的v-slot="ctx"负载传给子组件。受控原语(如PropertyListRoot)只发出语义化事件,选择与撤销接线应放在 adapter 或控制型 Composable 中,而非原语内部。
三层心智模型与包结构
上手 SDK 前先建立三层心智模型(快速开始):
@open-pencil/core—— 框架无关的编辑器引擎(场景图、状态、撤销、渲染请求);@open-pencil/vue—— Vue Composables 与无样式原语;- 你的应用 —— 样式、路由、文件流程、产品专属 UI。
@open-pencil/vue本身不持有编辑器模型,它把核心编辑器适配为:Vue 注入、响应式 Composables、无样式结构原语、画布与输入接线四类能力。包内按领域组织(架构文档):
- 组件族:
Canvas/、ColorPicker/、FillPicker/、FontPicker/、GradientEditor/、LayerTree/、PageList/、PropertyList/、PropertySection/、SegmentedControl/、NumberField/、Toolbar/,内含结构/无样式原语与局部辅助; - Controls:
controls/存放属性面板与编辑器控制型 Composables,如usePosition、useLayout、useAppearance、useColorModel、useTypography、useExport、useFillControls、useStrokeControls、useEffectsControls、useNodeProps、usePropScrub、useEditorPropertyList; - Variables:
VariablesEditor/存放变量领域 Composables 与状态接线; - Selection:
selection/存放选择派生状态与能力; - Context:
context/存放编辑器注入辅助:EDITOR_KEY、provideEditor、useEditor; - Internal:
internal/存放跨领域工具,不视为首要的无样式原语。
快速开始:安装与最小可运行编辑器
安装依赖
bun add @open-pencil/core @open-pencil/scene-graph @open-pencil/vue canvaskit-wasmSDK 位于本仓库 monorepo 中,以@open-pencil/vue发布。当前开发版本要求 Vue^3.5.41;使用可选 CanvasKit peer 时要求canvaskit-wasm >=0.41.1。使用更早版本时请检查所安装包的 peer 依赖要求。
import { createEditor } from '@open-pencil/core/editor' import { provideEditor, useCanvas } from '@open-pencil/vue'第一步:创建编辑器实例
核心状态是框架中立的;传入reactive状态可以让 Vue 控件观察到编辑器变化:
import { reactive } from 'vue' import { createDefaultEditorState, createEditor } from '@open-pencil/core/editor' import { SceneGraph } from '@open-pencil/scene-graph' const graph = new SceneGraph() const page = graph.getPages()[0] if (!page) throw new Error('Expected an initial page') const editor = createEditor({ graph, state: reactive(createDefaultEditorState(page.id)), getViewportSize: () => ({ width: 1200, height: 800 }), })对照源码,createEditor的EditorOptions完整字段定义在 packages/core/src/editor/types.ts:
| 选项 | 类型 | 说明 |
|---|---|---|
graph | SceneGraph | 场景图实例,默认新建; |
state | EditorState | 编辑器状态,默认通过createDefaultEditorState生成; |
loadFont | (family, style, characters?, signal?) => Promise<ArrayBuffer \| null> | 自定义字体加载; |
resolveFigmaClipboardImages | FigmaClipboardImageResolver | Figma 剪贴板图片解析器; |
getViewportSize | () => { width: number; height: number } | 画布视口尺寸回调,用于可调整大小的编辑器; |
skipInitialGraphSetup | boolean | 是否跳过初始场景图设置(默认false)。 |
注意:width和height不是EditorOptions的属性——视口尺寸必须通过getViewportSize返回。若编辑器可缩放,应让该回调返回当前画布容器的实际尺寸(不要包含侧边栏与工具栏)。
第二步:把编辑器注入 Vue 子树
<script setup lang="ts"> import { provideEditor } from '@open-pencil/vue' import type { Editor } from '@open-pencil/core/editor' const props = defineProps<{ editor: Editor }>() provideEditor(props.editor) </script> <template> <slot /> </template>可以把这层看作编辑器组件树的 provider 层。文档推荐直接使用provideEditor(),因为它是当前真实 API 面。
第三步:挂载画布
<script setup lang="ts"> import { ref } from 'vue' import { useCanvas, useEditor } from '@open-pencil/vue' const canvasRef = ref<HTMLCanvasElement | null>(null) const editor = useEditor() useCanvas(canvasRef, editor) </script> <template> <canvas ref="canvasRef" class="size-full" /> </template>使用 Composables 读写状态
编辑器注入后,子组件即可读取选择并发出命令:
import { useEditorCommands, useSelectionState } from '@open-pencil/vue' const selection = useSelectionState() const commands = useEditorCommands()完整最小示例
<script setup lang="ts"> import { ref } from 'vue' import { useCanvas, useEditor, useSelectionState } from '@open-pencil/vue' const canvasRef = ref<HTMLCanvasElement | null>(null) const editor = useEditor() const { selectedCount } = useSelectionState() useCanvas(canvasRef, editor, { onReady: () => { console.log('Canvas ready') }, }) </script> <template> <div class="grid h-full grid-rows-[1fr_auto]"> <canvas ref="canvasRef" class="size-full" /> <div class="border-t px-3 py-2 text-xs text-muted"> Selected: {{ selectedCount }} </div> </div> </template>useCanvas支持onReady之类的回调选项,useSelectionState返回的selectedCount是响应式的,可直接渲染在状态栏。
实战一:构建自定义编辑器外壳
官方指南 Custom Editor Shell 给出了一个典型的三层编辑器外壳:@open-pencil/core创建编辑器、@open-pencil/vue适配为 Composables 与原语、你的应用负责外壳与产品 UX。推荐的组合方式是:
- 顶层用
provideEditor()提供编辑器; - 画布居中;
- 一侧放页面/图层导航;
- 另一侧放属性面板;
- 菜单与工具栏由 Composables 驱动。
完整示例:
<script setup lang="ts"> import { reactive } from 'vue' import { createDefaultEditorState, createEditor } from '@open-pencil/core/editor' import { SceneGraph } from '@open-pencil/scene-graph' import { provideEditor, CanvasRoot, CanvasSurface, ToolbarRoot, PageListRoot, } from '@open-pencil/vue' const graph = new SceneGraph() const page = graph.getPages()[0] if (!page) throw new Error('Expected an initial page') const editor = createEditor({ graph, state: reactive(createDefaultEditorState(page.id)), getViewportSize: () => ({ width: 1440, height: 900 }), }) provideEditor(editor) </script> <template> <div class="grid h-screen grid-cols-[240px_1fr_320px] grid-rows-[48px_1fr]"> <ToolbarRoot v-slot="{ tools, activeTool, actions }"> <header class="col-span-3 flex items-center gap-2 border-b px-3"> <button v-for="tool in tools" :key="tool.key" :data-active="activeTool === tool.key" @click="actions.setTool(tool.key)" > {{ tool.label }} </button> </header> </ToolbarRoot> <aside class="border-r"> <PageListRoot v-slot="{ pages, currentPageId, actions }"> <nav> <button v-for="page in pages" :key="page.id" :data-active="page.id === currentPageId" @click="actions.switch(page.id)" > {{ page.name }} </button> </nav> </PageListRoot> </aside> <main> <CanvasRoot> <CanvasSurface class="size-full" /> </CanvasRoot> </main> <aside class="border-l"> Properties panel here </aside> </div> </template>示例中的固定视口尺寸是为了保持代码简短。在可调整大小的外壳中,getViewportSize应返回画布容器本身的尺寸,不要计入侧边栏和工具栏。这一分工之所以成立,是因为:SDK 拥有编辑器集成与可复用无样式逻辑,你的应用拥有布局、样式与产品专属动作,而 Composables 可以在没有额外包装组件的情况下驱动菜单与面板。
实战二:导航面板(页面与图层)
OpenPencil 的侧边栏通常组合两个职责:页面导航与图层导航(Navigation Panels)。
页面导航
使用PageListRoot或usePageList():
<PageListRoot v-slot="{ pages, currentPageId, switchPage, addPage }"> <div> <button v-for="page in pages" :key="page.id" @click="switchPage(page.id)"> {{ page.name }} </button> <button @click="addPage()">New page</button> </div> </PageListRoot>图层导航
当你希望树结构由 SDK 管理、而展示由应用决定时,使用LayerTreeRoot:
<LayerTreeRoot v-slot="{ items, selectedIds, select, toggleExpand, getKey, getChildren }"> <TreeView :items="items" :selected-ids="selectedIds" :get-key="getKey" :get-children="getChildren" @select="select" @toggle-expand="toggleExpand" /> </LayerTreeRoot>常见的布局模式是:页面列表在侧边栏顶部,图层列表在下方,行内重命名等细节控件内嵌在你的行组件中。LayerTreeRoot还配套buildLayerTreeModel、visibleLayerRows、useLayerTree等导出(见 packages/vue/src/index.ts),用于构建与虚拟化图层树模型。
实战三:属性面板(Composable 优先)
属性面板在@open-pencil/vue中刻意设计为composable-first(Property Panels):如果面板主要是选择派生值与更新动作,优先使用 Composables;如果需要可复用的数组/列表结构,再使用PropertyListRoot之类的无样式原语。
常用控制 Composables
标准属性段从以下开始:
usePosition()useLayout()useAppearance()useTypography()useExport()
列表型面板使用:
useFillControls()useStrokeControls()useEffectsControls()
绑定感知字段(BindableValueRoot)
对于可能引用变量或外部设计令牌的字段,用BindableValueRoot包裹。该原语与展示无关,但绑定感知接口应保持"聚焦非破坏性":
- 字段闲置时展示变量身份,在 tooltip 等辅助 UI 中暴露解析后的值;
- 聚焦或打开 picker 不得解除绑定;
- 只有用户真正改动值时,才应用
detach-on-edit、readonly-when-bound或edit-variable; - 把显式的解绑动作放在 picker 中,而不是放在破坏性的一键字段图标上;
- 将绑定替换、编辑时解绑与多目标变更放在同一个 provider 批次内。
OpenPencil 自带应用在闲置时以紫色变量名 pill 展示绑定,并在 NumberField 进入编辑模式时揭示解析后的数值;自定义外壳可以用不同的方式呈现同一份无样式状态。绑定相关的底层 API(provideBindingProvider、useBindingProvider、useOpenPencilBindingProvider、useNumberBindingProvider、useColorBindingProvider及BoundEditPolicy等类型)同样导出自 packages/vue/src/index.ts。
位置面板示例
<script setup lang="ts"> import { usePosition } from '@open-pencil/vue' const { x, y, width, height, updateProp, commitProp } = usePosition() </script> <template> <div class="grid grid-cols-2 gap-2"> <input :value="x" @input="updateProp('x', Number(($event.target as HTMLInputElement).value))" /> <input :value="y" @input="updateProp('y', Number(($event.target as HTMLInputElement).value))" /> <input :value="width" @input="updateProp('width', Number(($event.target as HTMLInputElement).value))" /> <input :value="height" @input="updateProp('height', Number(($event.target as HTMLInputElement).value))" /> </div> </template>填充列表面板示例
<script setup lang="ts"> import { PropertyListRoot, useEditorPropertyList, useFillControls } from '@open-pencil/vue' const fillControls = useFillControls() const fills = useEditorPropertyList('fills') </script> <template> <PropertyListRoot prop-key="fills" :items="fills.items.value" :mixed="fills.isMixed.value" @add="fills.actions.add" @remove="fills.actions.remove" v-slot="{ items, actions }" > <div v-for="(fill, index) in items" :key="index"> {{ fill.type }} <button @click="actions.remove(index)">Remove</button> </div> <button @click="actions.add(fillControls.defaultFill)">Add fill</button> </PropertyListRoot> </template>经验法则:直接的控件逻辑用 Composables;重复的列表/树/插槽协调才是难点时,用结构原语。
从 v0.14.0 迁移的注意事项
快速开始文档 末尾列出了当前开发版本相对 v0.14.0 的破坏性变更,升级时需逐项处理:
- 场景图覆写(Scene Graph overrides):把
SceneNode.overrides记录替换为instanceOverrides,其self与descendants映射区分实例级与后代级覆写。应使用@open-pencil/scene-graph公开的覆写辅助函数,而不是当作简单的字段改名。 - 派生几何(Derived geometry):
figmaDerivedLayout改名为derivedLayout,figmaDerivedTextGlyphs改名为derivedTextGlyphs,导出类型FigmaDerivedTextGlyph改名为DerivedTextGlyph。 - 绑定提供者(Binding providers):实现
getBindingId()并处理unresolved。edit-variable用prepareEdit()取代setValue(),它捕获编辑键、值、setter 与恢复回调(参见 BindableValue)。 - 翻译(Translations):用对应的产品领域 Composables 与目录取代
useDialogMessages()和dialogMessages,如useSettingsMessages()或useRenameMessages()。目录键也已移动,不能只重命名导入(参见 useI18n)。 - CanvasKit:使用
PathBuilder进行可变构造;不可变Path操作要保留返回的路径,而不是期待原地修改。 - 自定义工具(Custom tools):使用原生 Valibot
input模式与执行元数据,取代params、ParamDef或paramToZod()。程序化 MCP 集成使用 MCP SDK v2 的 server/client 类型(参见 MCP)。
API 分区与下一步
SDK 的公共 API 按三个区域组织,均有对应参考文档可查:
- 组件 Components
- Composables
- 高级 API Advanced
继续深入推荐依次阅读:SDK 架构(architecture)、useEditor(use-editor)、useCanvas(use-canvas)与useI18n(use-i18n)。结合本仓库的 packages/vue/src 目录与 packages/core/src/editor 的createEditor/EditorOptions实现,你可以在阅读文档的同时直接对照源码验证每个 API 的真实行为。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
open-pencil @open-pencil/vue Composables:用 provideEditor、useEditor 与 useCanvas 构建自定义设计编辑器界面
open pencil @open pencil/vue Composables:用 provideEditor、useEditor 与 useCanvas 构
前端桌面应用AI 应用MCP 服务open-pencil @open-pencil/vue 无头组件体系:用无样式原语构建自定义设计编辑器界面
open pencil @open pencil/vue 无头组件体系:用无样式原语构建自定义设计编辑器界面 本篇基于 open pencil 仓库中 pack
前端桌面应用AI 应用MCP 服务OpenPencil Vue SDK Composables 全指南:用可复用状态与动作构建自定义设计编辑器界面
OpenPencil Vue SDK Composables 全指南:用可复用状态与动作构建自定义设计编辑器界面 导读 : @open pencil/vue 是
前端桌面应用AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考