☰
基于 @open-pencil/vue 打造自定义设计编辑器:OpenPencil Vue SDK 开发指南
2026/9/27 7:57:01 网站建设 项目流程
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

AI-native design editor. Open-source Figma alternative.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载

@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 分为两个主要层级:

  1. Composables:提供编辑器状态与相关动作。只需要状态和动作时,从 Composables 开始;
  2. 组件/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 前先建立三层心智模型(快速开始):

  1. @open-pencil/core—— 框架无关的编辑器引擎(场景图、状态、撤销、渲染请求);
  2. @open-pencil/vue—— Vue Composables 与无样式原语;
  3. 你的应用 —— 样式、路由、文件流程、产品专属 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-wasm

SDK 位于本仓库 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:

选项类型说明
graphSceneGraph场景图实例,默认新建;
stateEditorState编辑器状态,默认通过createDefaultEditorState生成;
loadFont(family, style, characters?, signal?) => Promise<ArrayBuffer \| null>自定义字体加载;
resolveFigmaClipboardImagesFigmaClipboardImageResolverFigma 剪贴板图片解析器;
getViewportSize() => { width: number; height: number }画布视口尺寸回调,用于可调整大小的编辑器;
skipInitialGraphSetupboolean是否跳过初始场景图设置(默认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):使用原生 Valibotinput模式与执行元数据,取代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.

项目地址:https://gitcode.com/gh_mirrors/op/open-pencil
点击查看免费下载

相关推荐

上一篇:3行代码实现Excel数据透视表:EasyExcel合并策略实战指南
下一篇:解决ComfyUI-Manager模块属性缺失的终极方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询