- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
useNodeProps()是 OpenPencil 中属性面板(Property Panel)家族的底层助手组合式函数(composable),useAppearance、useLayout、useTypography等高层组合式函数均建立在它之上。本文围绕它要解决的四个核心问题展开:混合值(mixed-value)检测、多选节点批量更新、数组型属性(fills / strokes / effects)的条目编辑、以及支持撤销(undo)的"预览 + 提交"分离语义,并结合@open-pencil/vue源码剖析其底层实现。读完本文,你将能够使用useNodeProps为自定义属性面板接入多选感知的状态与提交逻辑。
为什么需要 useNodeProps
在设计工具中,属性面板总是要面对同一个复杂场景:用户选中了一个节点,或者同时选中了多个节点,面板需要:
- 检测混合值——多个选中节点上同一属性取值不一致时,面板应显示"混合"(MIXED)占位,而不是某个节点的具体值;
- 批量更新——对多个选中节点同时应用同一个属性变更;
- 编辑数组属性——修改
fills(填充)、strokes(描边)、effects(效果)数组中某一项,或删除、切换某项的可见性; - 撤销感知的提交——拖动数值时先实时预览(不产生撤销记录),松手时再统一提交为一条可撤销的历史条目。
这些逻辑如果由每个面板重复实现,会带来大量重复代码和一致性问题。OpenPencil 将它们集中到了packages/vue/src/controls/node-props/use.ts的useNodeProps()与packages/vue/src/controls/node-props/helpers.ts中,并通过packages/vue/src/index.ts(第 32 行)对外导出useNodeProps与MIXED。
使用场景判定
原文档明确指出,当你的自定义面板需要以下能力时,应该使用useNodeProps():
- 检测混合值(mixed-value detection);
- 多选更新(multi-selection updates);
- 数组条目编辑(array-item editing);
- 撤销感知的属性提交(undo-aware property commits)。
这些能力覆盖了几乎所有属性面板的底层需求。useNodeProps不关心具体是哪个属性域(外观、布局还是排版),它只负责"选区状态 + 属性读写 + 撤销提交"这一通用底座。
API 速览
useNodeProps()返回一组由选区派生的响应式状态与动作。以下是完整返回值列表(对应 use.ts 的返回对象):
| 返回值 | 类型/含义 | 说明 |
|---|---|---|
store | Editor | 编辑器实例,来自useEditor()注入 |
node | Ref<SceneNode \| null> | 当前激活节点(单选时即选中节点) |
nodes | Ref<SceneNode[]> | 当前选中的全部节点 |
isMulti | ComputedRef<boolean> | 是否多选(nodes.value.length > 1) |
active | ComputedRef<boolean> | 是否存在可编辑选区(有节点或多选) |
activeNode | ComputedRef<SceneNode \| null> | 单选的节点,或多选时的第一个节点 |
targetNodes | () => SceneNode[] | 数组动作实际作用的节点列表 |
prop(key) | ComputedRef<MixedValue<T>> | 某属性的合并(merged)值,多选不一致时返回MIXED |
merged(key) | MixedValue<T> | 非响应式的合并值读取函数 |
updateAllWithUndo(patch, label) | 函数 | 对全部选中节点批量打补丁并记入撤销 |
updateArrayItem(key, index, patch, label) | 函数 | 更新数组属性中某一项 |
removeArrayItem(key, index, label) | 函数 | 移除数组属性中某一项 |
toggleArrayVisibility(key, index) | 函数 | 切换数组条目visible标志 |
isArrayMixed(key) | boolean | 数组属性在多选中是否混合 |
updateProp(key, value) | 函数 | 实时更新(scrub)单属性,不产生撤销记录 |
commitProp(key, value, previous) | 函数 | 提交属性变更,写入撤销历史 |
另外还导出了两个类型级能力:
MIXED:哨兵符号,表示"多个节点取值不一致"(定义于 helpers.ts);MixedValue<T>:T | typeof MIXED的联合类型。
混合值检测:MIXED 哨兵与 merged
混合值检测由createNodePropSelectionState实现。核心逻辑在 helpers.ts:
function merged<K extends keyof SceneNode>(key: K): MixedValue<SceneNode[K]> { const all = nodes.value if (all.length === 0) return MIXED // 无选区 → 混合占位 const first = all[0][key] for (let i = 1; i < all.length; i++) { if (all[i][key] !== first) return MIXED // 任一节点不一致 → 混合占位 } return first }实现要点:
- 无选区时直接返回
MIXED,面板据此渲染占位状态; - 多选时逐一比对第一个节点之后的每个节点,只要有一处不一致就整体返回
MIXED; prop(key)在此基础上包了一层computed(),得到响应式的混合值引用,可直接绑定到模板。
数组属性的混合检测
对于fills、strokes、effects这类数组属性,普通的!==比较无法捕获"长度相同但内容不同"的情况。isNodeArrayMixed(helpers.ts)做了深度比较:
- 长度不同的数组直接判定混合;
- 长度相同的数组逐项调用
areArrayItemsEqual递归比较,支持嵌套对象与嵌套数组; - 非数组属性则退回逐节点
!==比较。
useNodeProps()将其暴露为isArrayMixed(key),面板可以据此决定是否禁用某项编辑或显示混合占位。
多选批量更新:updateAllWithUndo
updateAllWithUndo对nodes.value中每一个节点调用store.updateNodeWithUndo(n.id, patch, label)(helpers.ts)。
底层实现位于 packages/core/src/editor/nodes.ts。updateNodeWithUndo的核心流程是:
- 通过
styleDetachmentChanges、textAutoResizeChanges、pathTextEditChanges对补丁做二次处理(例如路径文本的重排字形优先级); pick出变更前每个被改动属性的旧值previous;ctx.graph.updateNode(id, nextChanges)应用新值并runLayoutForNode重排布局;ctx.undo.push写入一条带label、包含forward/inverse的撤销记录;ctx.requestRender()触发重绘。
因此,面板里"一键把所有选中节点的不透明度设为 50%"这类操作,会自然产生一条可撤销的单一历史记录,且撤销后布局会正确重排。
数组条目编辑:更新、删除与可见性切换
createNodePropArrayActions(helpers.ts)封装了三个数组操作,全部作用于targetNodes()(多选时为全部选中节点,单选时为激活节点):
updateArrayItem:更新第 index 项
function updateArrayItem(key, index, patch, label) { for (const n of targetNodes()) { const arr = [...n[key]] arr[index] = { ...arr[index], ...patch } store.updateNodeWithUndo(n.id, { [key]: arr }, label) } }它通过不可变更新(先拷贝数组再替换目标项)保证不触发意外的响应式副作用,例如修改填充颜色或描边线宽时,可传入{ color: ... }这类局部补丁。
removeArrayItem:删除第 index 项
用filter生成去掉目标项的新数组后提交,同样经过updateNodeWithUndo,因此删除操作可撤销。
toggleArrayVisibility:切换条目可见性
对数组第index项翻转visible标志,撤销标签固定为`Toggle ${key} visibility`。
updateArrayItem与removeArrayItem的第三个/第二参数label会作为撤销记录的标签,建议使用有语义的中文或英文描述,例如'Change fill',方便用户在历史面板中识别。
预览与提交分离:updateProp / commitProp
数值型控件(拖动 scrub)通常需要"拖动过程中实时预览、松手时一次性提交为一条撤销记录"。createNodePropScrubActions(helpers.ts)实现了这套语义:
updateProp(key, value):只调用store.updateNode(不带撤销记录),用于拖动过程的实时预览;多选时先用previousValuesMap 记录每个节点修改前的原始值,避免后续提交时丢失各节点各自的起点;commitProp(key, value, previous):调用store.commitNodeUpdate写入撤销历史;多选时逐节点用各自记录的previous作为撤销逆操作的还原值,提交后清空缓存。
底层commitNodeUpdate定义于 packages/core/src/editor/undo.ts:它会重新pick当前值,若与还原值相等则跳过(避免空撤销记录),否则 push 一条forward/inverse成对的记录。注意undo.ts的commitNodeUpdate同时被 bridges/undo.ts 以undoActions.commitNodeUpdate的形式暴露给编辑器桥接层。
因此,一个典型的数值输入框可以这样接线:
const { updateProp, commitProp, prop } = useNodeProps() // 拖动过程中:实时预览,不写撤销 updateProp('opacity', 0.6) // 松手时:提交为一条可撤销记录 commitProp('opacity', 0.6, 0.5)多选场景下这套"先记录旧值、再统一提交"的设计,保证每个节点都能正确回到各自的初始值,而不是退回到某一个节点的旧值。
在高层组合式函数中的应用
useNodeProps是高层组合式函数的底座。例如useAppearance(packages/vue/src/controls/appearance/use.ts)直接解构了nodes、node、active、isMulti、merged、updateProp、commitProp,再叠加外观领域专属的createAppearanceState/createAppearanceActions:
export function useAppearance() { const editor = useEditor() const { nodes, node, active, isMulti, merged, updateProp, commitProp } = useNodeProps() const appearanceState = createAppearanceState({ node, nodes, isMulti, merged, ... }) const appearanceActions = createAppearanceActions({ editor, ... }) return { editor, nodes, node, active, isMulti, ...appearanceState, updateProp, commitProp, ...appearanceActions } }useLayout(packages/vue/src/controls/layout/use.ts)同样构建在选区状态之上,只是它更多使用createLayoutSelectionState等布局专属 helper,并在内部共享updateProp/commitProp的命名约定。从 SDK 架构文档 可以看出,controls/目录中usePosition、useLayout、useAppearance、useTypography、useNodeProps、usePropScrub等组合式函数共同组成了属性面板的控制层;@open-pencil/vue只负责编辑器集成与无样式可复用的逻辑,不持有编辑器模型本身。
这种"底层通用底座 + 领域专属动作"的分层,使得新增一个属性面板时只需关注领域逻辑,多选、混合值、撤销等横切关注点全部由useNodeProps兜底。
相关 API
- useAppearance——外观(可见性、不透明度、圆角)领域的面板组合式函数;
- useLayout——布局(flex/grid、尺寸、内边距、对齐、轨道)领域的面板组合式函数;
- useTypography——排版领域的面板组合式函数;
- usePropScrub——更底层的拖动 scrub 提交辅助;
- PropertyListRoot——属性列表的结构化根组件。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
open-pencil SDK 深度解析:useNodeProps 属性面板 Composable 的混合值检测、多选更新与撤销感知提交
open pencil SDK 深度解析:useNodeProps 属性面板 Composable 的混合值检测、多选更新与撤销感知提交 useNodeProp
前端桌面应用AI 应用MCP 服务PaddleSpeech s2t frontend utility 模块全解:词典、Manifest、CMVN 与音频数值处理的底层工具箱
PaddleSpeech s2t frontend utility 模块全解:词典、Manifest、CMVN 与音频数值处理的底层工具箱 导读 PaddleS
人工智能语音音频CADmium底部面板:属性编辑与历史记录
CADmium底部面板:属性编辑与历史记录 概述 CADmium作为一款现代化的浏览器端CAD(Computer Aided Design,计算机辅助设计)软件
3D建模前端图形学WebAssembly桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考