- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
ConstraintsControlRoot是 OpenPencil 的@open-pencil/vueSDK 中一个无头(headless)根级组件,用于向当前合法选区暴露水平与垂直方向的缩放约束状态,并提供支持撤销的修改动作。本文以 constraints-control-root.md 为骨架,结合其底层实现(useConstraints、isConstraintEligible、constraintPins等)与编辑器真实面板代码,讲解该原语的适用场景、插槽契约、五种约束取值语义以及如何基于它构建自定义属性面板。
什么是 ConstraintsControlRoot
ConstraintsControlRoot是面向"帧子节点约束"的通用控制原语。它本身不渲染任何 DOM,只通过默认插槽把当前选区的约束状态与操作动作交给调用方,由调用方决定用下拉框、钉选图还是其他任意 UI 形式来呈现。
从组件实现看,ConstraintsControlRoot的核心逻辑极简:内部调用useConstraints()组合式函数,再把状态与动作原样透传给插槽:
<script setup lang="ts"> import { useConstraints } from '#vue/controls/constraints/use' import type { ConstraintsControlActions, ConstraintsControlRootSlots } from '#vue/primitives/ConstraintsControl/types' const constraints = useConstraints() defineSlots<ConstraintsControlRootSlots>() const actions: ConstraintsControlActions = { setHorizontal: (value) => constraints.setAxis('horizontal', value), setVertical: (value) => constraints.setAxis('vertical', value), setCenter: (axis) => constraints.setAxis(axis, 'CENTER'), togglePin: constraints.togglePin } </script> <template> <slot :active="constraints.active.value" :is-multi="constraints.isMulti.value" :horizontal="constraints.horizontal.value" :vertical="constraints.vertical.value" :actions="actions" /> </template>完整源码见 ConstraintsControlRoot.vue,类型定义见 types.ts。
适用选区:哪些节点会激活约束
文档明确了该原语的生效范围:仅对"帧(frame)、组件(component)、组件集(component set)、实例(instance)"的子节点生效。以下两类节点被排除:
- 常规自动布局(auto-layout)子节点;
- 顶层页面图层(没有父节点容器)。
但绝对定位(absolutely positioned)的自动布局子节点仍然可用。
这一定义与底层isConstraintEligible的实现一一对应。在 model.ts 中,合法父类型集合与判定逻辑为:
const CONSTRAINT_PARENT_TYPES = new Set<SceneNode['type']>([ 'FRAME', 'COMPONENT', 'COMPONENT_SET', 'INSTANCE' ]) export function isConstraintEligible(graph: SceneGraph, node: SceneNode): boolean { if (node.type === 'GROUP' || !node.parentId) return false const parent = graph.getNode(node.parentId) if (!parent || !CONSTRAINT_PARENT_TYPES.has(parent.type)) return false return parent.layoutMode === 'NONE' || node.layoutPositioning === 'ABSOLUTE' }从源码可以推断其判定规则为:
GROUP节点(编组)或没有父节点的顶层图层直接判定为不可用;- 父节点必须是
FRAME/COMPONENT/COMPONENT_SET/INSTANCE之一; - 父节点处于非自动布局(
layoutMode === 'NONE'),或者子节点本身是绝对定位(layoutPositioning === 'ABSOLUTE')时可用。
插槽契约:状态与动作
ConstraintsControlRoot的默认插槽接收 4 个状态字段与 1 组动作,类型定义位于 types.ts:
export interface ConstraintsControlRootSlotProps { active: boolean // 当前选区是否全部为合法约束节点 isMulti: boolean // 是否为多选 horizontal: ConstraintValue // 水平约束值:MIN | CENTER | MAX | STRETCH | SCALE | MIXED vertical: ConstraintValue // 垂直约束值:同上 actions: ConstraintsControlActions } export interface ConstraintsControlActions { setHorizontal(value: ConstraintType): void setVertical(value: ConstraintType): void setCenter(axis: ConstraintAxis): void togglePin(axis: ConstraintAxis, edge: ConstraintEdge, additive: boolean): void }状态字段
| 字段 | 类型 | 含义 |
|---|---|---|
active | boolean | 选区中每个节点都满足isConstraintEligible时才为true,否则面板应隐藏或禁用 |
isMulti | boolean | 是否为多选,多选时修改动作会合并为一个撤销条目 |
horizontal | ConstraintValue | 水平约束值;多个节点取值不一致时为MIXED |
vertical | ConstraintValue | 垂直约束值;多节点不一致时为MIXED |
ConstraintValue的完整取值集合为ConstraintType | MIXED,其中ConstraintType定义在场景图包 types.ts 中:
export type ConstraintType = 'MIN' | 'CENTER' | 'MAX' | 'STRETCH' | 'SCALE'这与文档所述"约束值与 Figma Plugin API 完全一致"相吻合:MIN(靠起点)、CENTER(居中)、MAX(靠终点)、STRETCH(双向拉伸)、SCALE(等比缩放)。
动作字段
| 动作 | 签名 | 说明 |
|---|---|---|
setHorizontal | (value: ConstraintType) => void | 直接设置水平约束模式 |
setVertical | (value: ConstraintType) => void | 直接设置垂直约束模式 |
setCenter | (axis: ConstraintAxis) => void | 将指定轴设置为CENTER,适合"居中钉"一键点击 |
togglePin | (axis, edge, additive) => void | 切换起点/终点钉的开关,additive控制是否为"叠加"模式(如按住 Shift) |
其中ConstraintAxis为'horizontal' | 'vertical',ConstraintEdge为'leading' | 'trailing'。
五种约束值的行为语义(底层几何验证)
文档只列出了取值名称,约束的真正行为体现在场景图包的constrainedAxis实现中(resize.ts):
function constrainedAxis( position: number, size: number, parentBefore: number, parentAfter: number, constraint: ConstraintType ): { position: number; size: number } { const delta = parentAfter - parentBefore if (constraint === 'MAX') return { position: position + delta, size } if (constraint === 'CENTER') return { position: position + delta / 2, size } if (constraint === 'STRETCH') return { position, size: Math.max(1, size + delta) } if (constraint === 'SCALE' && parentBefore > 0) { const scale = parentAfter / parentBefore return { position: position * scale, size: Math.max(1, size * scale) } } return { position, size } }以父容器尺寸变化delta = 父容器新尺寸 - 父容器旧尺寸为前提:
| 约束值 | 位置行为 | 尺寸行为 | 直观效果 |
|---|---|---|---|
MIN | 位置不变(position) | 尺寸不变(size) | 钉在容器起点一侧 |
MAX | 位置随容器平移delta | 尺寸不变 | 钉在容器终点一侧 |
CENTER | 位置平移delta / 2 | 尺寸不变 | 保持居中 |
STRETCH | 位置不变 | 尺寸增加delta(最小为 1) | 随容器双向拉伸 |
SCALE | 位置乘以缩放比例 | 尺寸乘以缩放比例 | 等比缩放 |
constrainedChildRect会对横、纵两轴分别调用该函数并四舍五入取整;scaledChildRect则是两轴都使用SCALE的特例。这组几何语义是理解"钉选控件上每个钉代表什么"的关键依据。
实战示例:在属性面板中消费 ConstraintsControlRoot
最小用法:纯下拉框
文档强调默认插槽"提供具体或混合的轴值、以及用于设置模式或构建交互式钉选控件的撤销感知动作"。最直接的用法是把插槽状态渲染成两个下拉框:
<script setup lang="ts"> import { ConstraintsControlRoot } from '@open-pencil/vue' </script> <template> <ConstraintsControlRoot v-slot="{ active, horizontal, vertical, actions }"> <fieldset v-if="active"> <legend>Constraints</legend> <select :value="horizontal" @change="actions.setHorizontal(($event.target as HTMLSelectElement).value as never)"> <option value="MIN">Left</option> <option value="CENTER">Center</option> <option value="MAX">Right</option> <option value="STRETCH">Stretch</option> <option value="SCALE">Scale</option> </select> <select :value="vertical" @change="actions.setVertical(($event.target as HTMLSelectElement).value as never)"> <option value="MIN">Top</option> <option value="CENTER">Center</option> <option value="MAX">Bottom</option> <option value="STRETCH">Stretch</option> <option value="SCALE">Scale</option> </select> </fieldset> </ConstraintsControlRoot> </template>注意:active为false时应直接隐藏整个面板(OpenPencil 编辑器正是这样做的),而MIXED状态应单独呈现,不能作为可选项写入(下文介绍编辑器做法)。
编辑器真实面板:混合值处理 + 钉选图
OpenPencil 编辑器自身的约束面板 ConstraintsSection.vue 是官方参考实现,展示了两个关键实践:
MIXED只读呈现:当horizontal === MIXED时,在下拉框选项头部插入一个MIXED占位项,并在用户选择时拦截(不写入):
function optionsWithMixed(options, mixed) { return mixed ? [{ value: 'MIXED', label: panels.value.mixed }, ...options] : options }- 钉选图 + 下拉框并存:面板同时渲染
ConstraintsPinControl与两个AppSelect,二者共用同一组actions。
钉选控件 ConstraintsPinControl.vue 展示了如何用constraintPins把当前值映射为 6 个钉位(水平起点/终点/居中、垂直起点/终点/居中),并区分activate逻辑:
function activate(pin: PinItem, event: MouseEvent) { if (pin.edge === 'center') actions.setCenter(pin.axis) else actions.togglePin(pin.axis, pin.edge, event.shiftKey) // Shift 为叠加模式 }即:点击"居中"钉直接走setCenter;点击边缘钉走togglePin,按住 Shift 时为叠加切换(多钉组合),否则为单选替换。
底层原理:状态派生、MIXED 合并与撤销批处理
ConstraintsControlRoot的所有能力都来自 use.ts 中的useConstraints(),文档配套的组合式函数文档见 use-constraints.md。
派生状态:active / horizontal / vertical
const active = computed( () => nodes.value.length > 0 && nodes.value.every((node) => isConstraintEligible(store.graph, node)) ) function mergedConstraint(nodes, key): ConstraintValue { if (nodes.length === 0) return MIXED const first = nodes[0][key] return nodes.some((node) => node[key] !== first) ? MIXED : first }active要求每一个选中节点都满足资格条件("every"语义,与文档一致);horizontal/vertical通过比较首个节点与其他节点的horizontalConstraint/verticalConstraint属性生成;只要有一个不一致即为MIXED。
撤销感知:setAxis 与批量 undo
文档强调"多选变更会合并为一个撤销条目"。实现上setAxis对多选走store.undo.runBatch,单选走普通更新:
function setAxis(axis: ConstraintAxis, value: ConstraintType) { if (!active.value) return const key = axis === 'horizontal' ? 'horizontalConstraint' : 'verticalConstraint' const apply = () => { for (const node of nodes.value) { store.updateNodeWithUndo(node.id, { [key]: value }, `Change ${axis} constraint`) } } if (nodes.value.length > 1) store.undo.runBatch(`Change ${axis} constraint`, apply) else apply() }togglePin则读取当前轴的值,交给模型层toggleConstraintPin计算新模式后再走setAxis,因此钉选同样具备撤销感知与批量合并能力。
钉位映射与叠加切换逻辑
模型层 model.ts 提供两个可复用纯函数:
export function constraintPins(value: ConstraintValue) { return { leading: value === 'MIN' || value === 'STRETCH', trailing: value === 'MAX' || value === 'STRETCH', center: value === 'CENTER', scale: value === 'SCALE' } } export function toggleConstraintPin(value, edge, additive): ConstraintType { if (!additive) return edge === 'leading' ? 'MIN' : 'MAX' const pins = constraintPins(value) const leading = edge === 'leading' ? !pins.leading : pins.leading const trailing = edge === 'trailing' ? !pins.trailing : pins.trailing if (leading && trailing) return 'STRETCH' if (leading) return 'MIN' if (trailing) return 'MAX' return 'CENTER' }映射关系总结:
MIN激活 leading 钉;MAX激活 trailing 钉;CENTER激活居中钉;SCALE激活缩放徽标;STRETCH同时激活 leading 与 trailing 两个钉;- 非叠加切换(
additive=false):直接替换为MIN(leading)或MAX(trailing); - 叠加切换(
additive=true):翻转对应钉位,若两钉同时亮起得到STRETCH,两钉都熄灭落到CENTER。
use-constraints.md文档中建议:需要构建自定义无障碍钉选图时,直接复用constraintPins()与toggleConstraintPin(),避免重复实现模式映射——这正是编辑器ConstraintsPinControl的做法。
在 SDK 生态中的位置与关联 API
ConstraintsControlRoot属于@open-pencil/vue的"无头根原语"体系,与它平级、同类的还有:
- PositionControlsRoot:提供位置、尺寸、旋转、对齐、翻转等处理器;
- LayoutControlsRoot:提供自动布局与尺寸控制;
- 组合式函数版本 useConstraints:不想要插槽结构、直接拿状态与动作时使用;
- 属性面板总指南 property-panels.md:介绍了"控制类组合式函数优先、结构性列表用无头原语"的选择原则。
按该指南的取舍原则:如果面板只是需要"选区派生值 + 更新动作",可以直接用useConstraints()组合式函数;如果需要可复用的插槽结构(让多个面板共享同一套状态透传与动作包装),则选择ConstraintsControlRoot这类根原语。
快速上手模板
import { useConstraints } from '@open-pencil/vue' const constraints = useConstraints() constraints.setAxis('horizontal', 'STRETCH') constraints.togglePin('vertical', 'trailing', false)从 vue 包导出 与 ConstraintsControl 目录导出 可以确认:ConstraintsControlRoot及其相关类型(ConstraintsControlActions、ConstraintsControlRootSlotProps、ConstraintsControlRootSlots)均已作为公开 API 从@open-pencil/vue导出,可直接import { ConstraintsControlRoot } from '@open-pencil/vue'使用。
小结
ConstraintsControlRoot把"选区资格判定、MIXED 合并、撤销批处理、钉位模式映射"这些容易出错的编辑器接线逻辑全部封装在内部,对外只暴露一个干净的插槽契约。结合 ConstraintsSection.vue 与 ConstraintsPinControl.vue 的真实用法,开发者可以快速构建出与编辑器一致体验的约束面板:隐藏于active=false时、拦截MIXED写入、用constraintPins/togglePin实现支持 Shift 叠加的钉选交互,并获得默认的多选单步撤销支持。
- 前端
- 桌面应用
- AI 应用
- MCP 服务
【免费下载链接】open-pencil
AI-native design editor. Open-source Figma alternative.
相关推荐
OpenPencil SDK 无头组件 PropertyListItem 指南:为 fills/strokes/effects 构建自绘属性列表行
OpenPencil SDK 无头组件 PropertyListItem 指南:为 fills/strokes/effects 构建自绘属性列表行 Proper
前端桌面应用AI 应用MCP 服务OneUptime 事件状态与严重级别完全指南:从种子数据到状态机约束
OneUptime 事件状态与严重级别完全指南:从种子数据到状态机约束 本文围绕 OneUptime(开源可观测性与事件响应平台)的事件分类体系展开,深入讲解
可观测性后端运维前端云原生微服务AI AgentOpenPencil SDK 指南:SegmentedControl 分段控件 —— 单选、多选与动作模式的 Reka UI 无头实现
OpenPencil SDK 指南:SegmentedControl 分段控件 —— 单选、多选与动作模式的 Reka UI 无头实现 SegmentedCon
前端桌面应用AI 应用MCP 服务
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考