news 2026/9/26 10:21:18

useNodeProps:OpenPencil 属性面板的底层多选与混合值工具箱

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
useNodeProps:OpenPencil 属性面板的底层多选与混合值工具箱
  • 前端
  • 桌面应用
  • AI 应用
  • MCP 服务

【免费下载链接】open-pencil

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

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

useNodeProps()是 OpenPencil 中属性面板(Property Panel)家族的底层助手组合式函数(composable),useAppearance、useLayout、useTypography等高层组合式函数均建立在它之上。本文围绕它要解决的四个核心问题展开:混合值(mixed-value)检测、多选节点批量更新、数组型属性(fills / strokes / effects)的条目编辑、以及支持撤销(undo)的"预览 + 提交"分离语义,并结合@open-pencil/vue源码剖析其底层实现。读完本文,你将能够使用useNodeProps为自定义属性面板接入多选感知的状态与提交逻辑。

为什么需要 useNodeProps

在设计工具中,属性面板总是要面对同一个复杂场景:用户选中了一个节点,或者同时选中了多个节点,面板需要:

  1. 检测混合值——多个选中节点上同一属性取值不一致时,面板应显示"混合"(MIXED)占位,而不是某个节点的具体值;
  2. 批量更新——对多个选中节点同时应用同一个属性变更;
  3. 编辑数组属性——修改fills(填充)、strokes(描边)、effects(效果)数组中某一项,或删除、切换某项的可见性;
  4. 撤销感知的提交——拖动数值时先实时预览(不产生撤销记录),松手时再统一提交为一条可撤销的历史条目。

这些逻辑如果由每个面板重复实现,会带来大量重复代码和一致性问题。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 的返回对象):

返回值类型/含义说明
storeEditor编辑器实例,来自useEditor()注入
nodeRef<SceneNode \| null>当前激活节点(单选时即选中节点)
nodesRef<SceneNode[]>当前选中的全部节点
isMultiComputedRef<boolean>是否多选(nodes.value.length > 1)
activeComputedRef<boolean>是否存在可编辑选区(有节点或多选)
activeNodeComputedRef<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的核心流程是:

  1. 通过styleDetachmentChanges、textAutoResizeChanges、pathTextEditChanges对补丁做二次处理(例如路径文本的重排字形优先级);
  2. pick出变更前每个被改动属性的旧值previous;
  3. ctx.graph.updateNode(id, nextChanges)应用新值并runLayoutForNode重排布局;
  4. ctx.undo.push写入一条带label、包含forward/inverse的撤销记录;
  5. 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.

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

相关推荐

上一篇:Windows 11终极瘦身指南:免费开源工具让你的系统性能飙升51%
下一篇:在Mac桌面打造沉浸式音乐体验:LyricsX 2.0深度解析与实战指南

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 10:20:35

CTF夺旗赛新手入门指南:从零到独立解题的完整路径

CTF 夺旗赛这几年在国内的热度肉眼可见地涨&#xff0c;尤其是高校和刚入行的安全新人&#xff0c;几乎把 CTF 当成入门安全的第一块敲门砖。但真上手之后你会发现&#xff0c;网上教程要么是零散的 WriteUp&#xff0c;要么是直接甩一堆工具让你自己悟&#xff0c;中间那条&qu…

作者头像 李华
网站建设 2026/9/26 10:20:24

Vue DevTools 源码跳转失效?用 TaoToken 统一 Key 打通 Trae 编辑器配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 10:18:24

Python的函数的参数

在定义函数的时候, 我们将参数的名称和对应的位置先给确定好, 这样函数的接口定义就算完成了。对于那个要调用函数的对象来说呢, 它只需要知道应该怎样去传递正确的参数, 以及这个函数最后会返回一个什么样的值就可以了。至于函数内部那些复杂得很的逻辑, 全都给它封装了起来, …

作者头像 李华
网站建设 2026/9/26 10:17:48

SLAM入门学习路线:从定位建图原理到视觉激光与3DGS实战

1. 从零开始搭建SLAM学习路线&#xff1a;为什么我劝你先搞懂“定位与建图”这对双胞胎SLAM这个词&#xff0c;全称是Simultaneous Localization and Mapping&#xff0c;翻译过来就是“同时定位与建图”。我第一次接触这个概念的时候&#xff0c;脑子里冒出的第一个问题是&…

作者头像 李华