- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
LayerTreeRoot是 Open Pencil(开源的 AI 原生设计编辑器)Vue SDK(@open-pencil/vue)中面向图层树(layer tree)的无样式(headless)结构原语。它把图层树的树形结构、选择状态、展开/折叠分支、可见性/锁定切换、重命名与拖拽排序等交互逻辑全部封装在内,而把 DOM 标记(markup)与样式(styling)完全交给应用自身。本文将以 LayerTreeRoot 官方文档 为主线,结合 源码实现 与配套 API,讲解它的职责边界、上下文模型、生命周期同步机制,并给出可直接复制的实战用法。
1. 组件定位:结构由 SDK 管,外观由应用管
LayerTreeRoot的设计目标可以概括为一句话:提供可复用的树结构与交互接线,而不内置任何表现层(without built-in presentation)。它不像传统组件库那样输出一套固定的带样式 UI,而是通过作用域插槽(scoped slot)暴露数据与动作,由你在插槽内自由组装行内容、缩进样式、选中高亮与拖拽指示器。
这意味着在实际使用中你需要:
- 用
LayerTreeRoot包住整个面板区域,让 SDK 负责从编辑器场景图(scene graph)构建树模型、维护展开状态、计算可见行、处理选择与拖拽; - 在插槽内用自有组件(例如你自己的
TreeView)渲染items,并通过插槽暴露的select、toggleExpand等动作把点击事件接回 SDK; - 每一行既可以使用配套的
LayerTreeItem(它提供选择、展开、可见性、锁定、直接重命名等行级状态与动作),也可以完全自写行组件,仅从useLayerTree()取所需上下文。
2. 核心概念:树模型、可见行与选择状态
要正确使用LayerTreeRoot,先理解它向下游暴露的三个核心概念。它们都定义在 context.ts 中:
| 概念 | 类型 | 含义 |
|---|---|---|
items | Ref<LayerNode[]> | 当前页面下的图层树根级节点数组,LayerNode含id、name、type、layoutMode、visible、locked与嵌套的children |
visibleRows | ComputedRef<LayerRow[]> | 按当前展开状态拍平后的可见行列表,每行含node、level(层级,从 1 起)与hasChildren |
selectedIds | ComputedRef<Set<string>> | 当前编辑器中选中的节点 id 集合,与画布选区保持同步 |
树模型的构建:buildLayerTreeModel
树模型由 model.ts 中的buildLayerTreeModel(graph, parentId)从场景图构建:它从当前页开始,深度优先遍历parent.childIds,跳过internalOnly的内部节点,把每个SceneNode映射为轻量LayerNode,并同时返回byId索引用于后续的补丁式更新(patch)。
visibleLayerRows(items, expandedIds)则把树按展开集合拍平成带level的行序列:某行有子节点且其 id 在展开集合中,才会继续展开下一层。也就是说,展开状态完全由expanded数组驱动,LayerTreeRoot会替你维护这个数组,并保证删除节点后残留的展开 id 会被自动过滤(见下文的 rebuild 逻辑)。
选择模式:layerSelectionForTarget
选择逻辑集中在layerSelectionForTarget:根据目标 id、锚点 id(selectionAnchorId)与LayerSelectionMode(additive、range两个布尔位)计算新选区:
- 非 range:单击(additive=false)直接替换选区;additive 则在当前选区上增/删目标;
- range:结合锚点与目标在可见行中的索引,取二者之间的连续区间;配合 additive 可把区间并入现有选区。
这正是实现按住 Ctrl/Shift 多选、框选连选等交互的底层依据,Open Pencil 编辑器的选择行为与画布是一致的。
3. 插槽协议:组件暴露的数据与动作
LayerTreeRoot的模板基于 reka-ui 的TreeRoot构建,并通过单个默认插槽把全部能力下放(见 LayerTreeRoot.vue)。插槽参数完整列表如下:
| 插槽参数 | 说明 |
|---|---|
items | 树根级LayerNode[] |
flattenItems | reka-ui 计算出的拍平节点列表 |
visibleRows | 按展开状态拍平的LayerRow[](含 level/hasChildren) |
expanded | 当前展开的 id 数组 |
treeVersion | 树版本号,每次重建模型 +1,可用来强制行组件刷新 |
selectedIds | Set<string>当前选区 |
focused | 面板是否获得焦点(用于键盘导航与视觉聚焦态) |
draggingId | 正在拖拽的行 id(无拖拽时为 null) |
instruction | 拖拽放置指令:reorder-above/reorder-below/make-child |
instructionTargetId | 拖拽指令对应的目标行 id |
actions | 动作集合:select(id, mode)、toggleExpand(id)、setFocused(bool)、setVirtualizer(v) |
下面是根据 导航面板指南 给出的最小可运行示例——SDK 管理结构,应用管理标记与样式:
<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>注:示例中的
getKey/getChildren由内部TreeRoot提供,分别返回节点 id 与子节点数组;如果你完全自写行渲染,可改用visibleRows+ 插槽内的actions.select/actions.toggleExpand。
4. 行级渲染:LayerTreeItem 与 useLayerTree
LayerTreeRoot官方文档特别指出:可以搭配LayerTreeItem使用,也可以用自己的插槽渲染行。LayerTreeItem是默认的行组件,其默认插槽会收到以下行级状态与动作(见 LayerTreeItem.vue):
- 状态:
node、level、hasChildren、isSelected、isDragging、instruction、instructionTargetId、focused、padLeft(按(level-1) * indentPerLevel计算的左缩进值); - 动作:
actions.select(additive)、actions.toggleExpand()、actions.toggleVisibility()、actions.toggleLock()、actions.rename(name)。
行级动作会先emit到LayerTreeRoot(供外部监听),再落到编辑器:toggleVisibility调用editor.toggleNodeVisibility(id)、toggleLock调用editor.toggleNodeLock(id)、rename调用editor.renameNode(id, name),因此这些操作默认就是带撤销(undo)语义的编辑器动作。
如果你在自写行组件中只想读取上下文而无需外层插槽,可以使用配套的组合式函数:
import { useLayerTree } from '@open-pencil/vue' // 在 LayerTreeRoot 的任何后代组件中 const ctx = useLayerTree() console.log(ctx.visibleRows.value) // 当前可见行 ctx.select(nodeId, { additive: true, range: false }) ctx.toggleExpand(nodeId)useLayerTree()通过注入键LAYER_TREE_KEY(Symbol('layer-tree'))返回最近的LayerTreeRoot上下文;如果在LayerTreeRoot之外调用,会直接抛出错误提示,这能帮助你在开发期尽早发现结构错误(见 context.ts)。
上下文完整字段
LayerTreeContext除了上述状态外,还包含供内部协作的成员:editor(当前编辑器实例)、treeVersion、setupDrag、setVirtualizer、setRowRef等。多数情况下你只需关注插槽参数与select/toggleExpand/toggleVisibility/toggleLock/rename这套动作。所有公开类型(LayerNode、LayerRow、LayerSelectionMode、LayerDragInstruction、LayerTreeContext、LayerTreeVirtualizer)均从 index.ts 导出。
5. 与编辑器生命周期的同步机制
LayerTreeRoot不是一次性渲染的静态树,而是与编辑器场景图保持实时同步的“活”结构。从 LayerTreeRoot.vue 可以看到它订阅了一系列编辑器事件:
| 事件 | 处理策略 |
|---|---|
graph:replaced/page:changed | 立即重建整棵树(rebuildTree) |
node:created/node:deleted/node:reparented/node:reordered | 合并到微任务队列中调度一次重建(scheduleTreeRebuild),避免高频操作反复重建 |
node:updated | 只做补丁更新(patchTreeNode):仅当改动涉及name、type、layoutMode、visible、locked时才就地同步对应行,否则忽略 |
selection:changed | 自动展开选中节点的祖先分支(expandSelectionAncestors),必要时滚动到选中行 |
两个细节值得注意:
- 增量更新而非全量重建:普通属性变化走
patchLayerNode原地更新LayerNode,只有结构变化(增删/换父/重排)才触发整树重建,这是它在大文档中保持流畅的关键设计; - 选区联动:当选区变化时,组件会自动把选中节点的祖先 id 加入
expanded,确保“选中即展开可见”,这与主流设计工具的行为一致。组件在onScopeDispose中统一退订所有事件,避免内存泄漏。
6. 拖拽重排与换容器:useLayerDrag
LayerTreeRoot内部内置了基于@atlaskit/pragmatic-drag-and-drop的拖拽能力(useLayerDrag,见 useLayerDrag.ts),支持三种放置指令:
reorder-above:把源行插入目标行之前;reorder-below:把源行插入目标行之后;make-child:把源行放入目标容器末尾(成为子节点)。
实现要点:
- 每行通过
setupItem注册为可拖拽源(draggable)与放置目标(dropTargetForElements),并结合命中框计算(attachInstruction)与缩进量indentPerLevel推导指令; - 容器行(
editor.graph.isContainer)允许被放入子节点(block 列表不含make-child),普通叶子节点则禁止; - 放置校验包含
source.id !== data.id(不能放到自己身上)与isDescendant(targetId, sourceId)(不能放进自己的后代里); - 放置成功后调用
editor.reorderChildWithUndo(sourceId, parentId, index)执行带撤销的重排,make-child场景还会把目标容器自动展开,保证移动后节点立即可见。
拖拽过程中的draggingId、instruction、instructionTargetId会随插槽参数实时下发,因此你可以据此绘制“插入线/放入高亮”等视觉反馈——这也是为什么组件本身不内置样式、但交互体验却能完全自定义的原因。
7. 在真实面板中的位置与配套 API
在 Open Pencil 中,图层树是左侧导航面板的核心区域。官方 导航面板指南 推荐的布局是:上方为页面列表(PageListRoot),下方为图层树(LayerTreeRoot),行组件内部承载属性详情与直接重命名。与之配套的 API 还包括:
- LayerTreeItem:单行组件,提供选择、展开、可见性、锁定与重命名;
- useLayerTree:在任意后代组件中获取最近
LayerTreeRoot的上下文; - useLayerDrag:拖拽重排/换容器状态与动作的组合式函数;
- useSelectionState:与选择状态相关的组合式 API;
- PageListRoot:页面列表根组件。
这些组件同属@open-pencil/vue的无样式组件体系,均可在 组件索引 中查阅。
8. 完整实战:组装一个自定义图层树面板
综合以上内容,一个典型的“自带标记与样式”的面板可以这样组织:
<script setup lang="ts"> import { LayerTreeRoot } from '@open-pencil/vue' </script> <template> <LayerTreeRoot :indent-per-level="20" v-slot="{ items, visibleRows, expanded, selectedIds, toggleExpand, actions }" > <!-- 行渲染完全自定义,样式由应用决定 --> <ul class="layer-panel"> <template v-for="row in visibleRows" :key="row.node.id"> <li :class="{ selected: selectedIds.has(row.node.id), dragging: row.node.id === $attrs.draggingId }" :style="{ paddingLeft: (row.level - 1) * 20 + 'px' }" @click="actions.select(row.node.id, false)" @dblclick="row.hasChildren && actions.toggleExpand(row.node.id)" > {{ row.node.name }} </li> </template> </ul> </LayerTreeRoot> </template>要点回顾:
visibleRows已经替你算好每个节点的level与hasChildren,无需自己递归;- 缩进可以用
indentPerLevel属性(默认 16)统一控制,也可以在行内自行计算; actions中的动作直接作用于编辑器,操作自动进入撤销栈;- 如需更细粒度的行级能力(可见性/锁定/重命名/拖拽),改用
LayerTreeItem或直接调用useLayerTree()。
9. 总结
LayerTreeRoot是 Open Pencil Vue SDK 中“结构即服务、样式归应用”理念的典型代表:它把图层树的模型构建、展开状态、可见行计算、选区同步、事件订阅与拖拽重排全部封装,并通过插槽与useLayerTree()把状态和动作完整暴露给应用层。掌握它之后,你可以在不触碰渲染细节的前提下,快速搭建出与 Open Pencil 编辑器深度联动、交互行为一致的自定义图层树面板。进一步可阅读 LayerTreeItem、useLayerTree 与 useLayerDrag 继续深入。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
open-pencil LayerTreeItem 组件详解:基于 @open-pencil/vue 的无头图层树行原语
open pencil LayerTreeItem 组件详解:基于 @open pencil/vue 的无头图层树行原语 LayerTreeItem 是 ope
前端桌面应用AI 应用MCP 服务open-pencil SDK LayerTreeItem 详解:面向应用定制的头less图层树行原语
open pencil SDK LayerTreeItem 详解:面向应用定制的头less图层树行原语 LayerTreeItem 是 open pencil
前端桌面应用AI 应用MCP 服务open-pencil @open-pencil/vue 无头组件体系:用无样式原语构建自定义设计编辑器界面
open pencil @open pencil/vue 无头组件体系:用无样式原语构建自定义设计编辑器界面 本篇基于 open pencil 仓库中 pack
前端桌面应用AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考