X6 连接桩布局(Port Layout)完全指南:内置算法、参数详解与自定义注册
2026/9/17 7:49:23 网站建设 项目流程

X6 连接桩布局(Port Layout)完全指南:内置算法、参数详解与自定义注册

【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6

连接桩(Port)是图编辑器节点上用于连接边的锚点,而连接桩布局算法决定了这些锚点在节点上的排布方式。本文基于 X6 官方文档 连接桩布局 展开,结合仓库内 port-layout 注册表源码、连接桩管理器 与单元测试,系统讲解 X6 内置布局算法的配置方法、参数含义,以及如何编写并注册自定义布局算法。读完本文,你将能够为任意节点配置整齐的等分边排布、椭圆环绕排布、绝对定位等布局,并实现属于自己的专属布局。

布局算法的本质:一个返回相对位置的函数

在 X6 中,连接桩布局算法本质上是一个函数,其签名为:

type Definition<T> = ( portsPositionArgs: T[], // 连接桩中指定的布局算法参数 elemBBox: Rectangle, // 节点的包围盒 groupPositionArgs: T, // group 中定义的默认布局算法参数 ) => Result[] interface Result { position: Point.PointLike // 相对于节点的位置 angle?: number // 旋转角度 }

函数接收三个参数,返回一组Result

  • portsPositionArgs:该分组下每个连接桩在items中声明的args参数数组(顺序与连接桩一一对应);
  • elemBBox:节点的包围盒(Rectangle),布局算法据此感知节点的尺寸与中心;
  • groupPositionArgsgroups分组中通过position.args声明的默认布局参数。

Result中的position相对于节点的位置,而非画布绝对坐标。例如,某节点在画布上的位置是{ x: 30, y: 40 },若布局返回的某个连接桩位置是{ x: 2, y: 4 },那么该连接桩渲染到画布后的实际位置是{ x: 32, y: 44 }angle则为可选参数,指定连接桩的旋转角度(渲染时由视图层应用,见下文"渲染链路")。

通过 groups 配置布局,通过 items 覆盖参数

配置连接桩布局时,只能通过ports.groups选项来指定布局算法,而在items中则可以为单个连接桩提供可选的args,用于覆盖分组内的默认参数:

graph.addNode( ..., ports: { // 连接桩分组 groups: { group1: { position: { name: 'xxx', // 布局算法名称 args: { }, // 布局算法的默认参数 }, }, }, // 连接桩定义 items: [ { group: 'group1', args: { }, // 覆盖 group1 中指定的默认参数 }, ], }, )

从实现上看,PortManager.getPortsLayoutByGroup 会先取出分组对应的布局函数,然后分别收集每个连接桩的position.args(作为portsPositionArgs)与分组的position.args(作为groupPositionArgs)一并传给布局函数,最终把返回的相对位置与连接桩 id、尺寸、attrs 等组装成LayoutResult供渲染使用。这就是"分组给默认值、单桩可覆盖"的底层机制。

内置布局算法

X6 内置了absoluteleftrighttopbottomlineellipseellipseSpread共 8 种布局算法,源码集中在 src/registry/port-layout 目录(main.ts 统一导出absoluteellipseline三个模块,其中 line.ts 同时导出left/right/top/bottom四种边布局)。

absolute:绝对定位

absolute布局通过args直接指定连接桩的坐标,适用于需要精确摆放锚点的场景:

interface AbsoluteArgs { x?: string | number y?: string | number angle?: number }
名称类型必选默认值描述
xstring | number0连接桩在 X 轴相对位置。
ystring | number0连接桩在 Y 轴相对位置。
anglenumber0连接桩旋转角度。

xy为百分比字符串(如'60%')或位于[0, 1]之间的数值时,表示相对节点宽度/高度的百分比偏移量;否则表示绝对偏移量。百分比换算的实现位于 src/common/number/number.ts 的 normalizePercentage:字符串以%结尾时先除以 100 再乘以参照尺寸(宽/高),普通数字则原样使用,未传值时视为0。这一点已被 absolute 单元测试 覆盖,例如节点包围盒为(0, 0, 200, 100)时,{ x: '50%', y: '60%' }会得到{ x: 100, y: 60 }

使用示例:

graph.addNode({ ports: { groups: { group1: { position: { name: 'absolute', args: { x: 0, y: 0 }, }, }, }, items: [ { group: 'group1', args: { x: '60%', y: 32, angle: 45, }, }, ], }, })

可运行示例见 站点演示源码 site/src/api/port-layout/absolute/index.tsx。

left / right / top / bottom:沿边线均匀排布

这四种布局让连接桩沿矩形节点指定的边线均匀分布,对矩形形状的节点非常友好,可通过args设置偏移量与旋转角度:

interface SideArgs { dx?: number dy?: number angle?: number x?: number y?: number }
名称类型必选默认值描述
strictbooleanfalse是否严格等分均匀分布。
dxnumber0沿 X 轴方向的偏移量。
dynumber0沿 Y 轴方向的偏移量。
anglenumber0连接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

实现上,left/right/top/bottom分别取包围盒的左/右/上/下两条对角顶点构造一条 Line,再复用统一的lineLayout内部函数(见 src/registry/port-layout/line.ts#L83-L104)在边上取点:

  • strict: false(默认)时,第index个桩的插值比例为(index + 0.5) / length,即首尾各留半个间距、连接桩居中排布;
  • strict: true时,比例为(index + 1) / (length + 1),连接桩严格等分整条边线(含端点等距)。

使用示例:

graph.addNode({ ports: { groups: { group1: { position: 'left', // 也支持对象写法 { name: 'left', args: {} } }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, })

可运行示例见 site/src/api/port-layout/side/index.tsx。

line:沿线段均匀分布

line布局允许自定义一条线段,让连接桩沿该线段均匀分布,适合需要把锚点排布在节点内部斜线等非边位置上的场景:

interface LineArgs { start?: Point.PointLike end?: Point.PointLike dx?: number dy?: number angle?: number x?: number y?: number }
名称类型必选默认值描述
startPoint.PointLike线段起点。
endPoint.PointLike线段终点。
strictbooleanfalse是否严格等分均匀分布。
dxnumber0沿 X 轴方向的偏移量。
dynumber0沿 Y 轴方向的偏移量。
anglenumber0连接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

startend未指定时,分别默认取包围盒的左上角(getOrigin())与右下角(getCorner()),此时行为等价于从左上到右下的对角线排布;strict的语义与边布局一致。注意start/end同样支持百分比写法(通过normalizePoint换算)。

使用示例:

graph.addNode({ ports: { groups: { group1: { position: { name: 'line', args: { start: { x: 10, y: 10 }, end: { x: 210, y: 10 }, }, }, }, }, items: [ { group: 'group1', args: { dx: 2, }, }, ], }, })

可运行示例见 site/src/api/port-layout/line/index.tsx。

ellipse:沿圆弧步进分布

ellipse布局让连接桩沿椭圆(或圆形)弧线分布,从start指定的角度开始,以step为步长均匀步进:

interface EllipseArgs { start?: number step?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number }
名称类型必选默认值描述
startnumber起始角度。
stepnumber20步长。
compensateRotatenumberfalse是否沿圆弧修正连接桩的旋转角度。
drnumber0沿半径方向的偏移量。
dxnumber0沿 X 轴方向的偏移量。
dynumber0沿 Y 轴方向的偏移量。
anglenumber0连接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

从 ellipse.ts 的实现 看:

  • start未指定时默认0(节点顶部),step默认20度;
  • index个桩的角度为start + (index + 0.5 - count / 2) * step,即以起始角为中心向两侧对称展开,适合节点已有若干桩后沿圆周逐步追加的场景;
  • compensateRotate: true时,会调用ellipse.tangentTheta(p)求出该点处的切线方向作为旋转角,使连接桩方向始终贴合圆弧;
  • dr表示沿半径方向朝圆心内外偏移(p.move(center, dr)),配合dx/dy平移可实现"环带"效果。

使用示例(动态追加 10 个连接桩):

const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipse', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 10 }).forEach((_, index) => { node.addPort({ id: `${index}`, group: 'group1', attrs: { text: { text: index } }, }) })

可运行示例见 site/src/api/port-layout/ellipse/index.tsx。

ellipseSpread:沿椭圆整周均匀分布

ellipseSpreadellipse类似,但把连接桩整周均匀铺满椭圆,适合雷达图、环形菜单等场景:

interface EllipseSpreadArgs { start?: number compensateRotate?: boolean dr?: number dx?: number dy?: number angle?: number x?: number y?: number }
名称类型必选默认值描述
startnumber起始角度。
compensateRotatenumberfalse是否沿圆弧修正连接桩的旋转角度。
drnumber0沿半径方向的偏移量。
dxnumber0沿 X 轴方向的偏移量。
dynumber0沿 Y 轴方向的偏移量。
anglenumber0连接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

ellipse的关键差异在步长计算:ellipseSpread未指定step时默认取360 / portsPositionArgs.length(见 ellipse.ts#L31-L42),因此无论连接桩数量多少,都能均匀覆盖整个椭圆一周;每个桩的角度为start + index * step

使用示例:

const node = graph.addNode({ ports: { groups: { group1: { position: { name: 'ellipseSpread', args: { start: 45, }, }, }, }, }, }) Array.from({ length: 36 }).forEach((_, index) => { node.addPort({ group: 'group1', id: `${index}`, attrs: { text: { text: index } }, }) })

可运行示例见 site/src/api/port-layout/ellipse-spread/index.tsx。

自定义连接桩布局

内置布局无法覆盖所有场景时,可以完全按照上文"布局算法是一个函数"的规则自定义算法,并注册到系统中。

第一步:实现布局函数

布局函数接收三个参数(连接桩参数数组、节点包围盒、分组默认参数),返回每个连接桩相对节点的位置与旋转角。例如,实现一个正弦分布的布局算法:

function sin(portsPositionArgs, elemBBox) { return portsPositionArgs.map((_, index) => { const step = -Math.PI / 8 const y = Math.sin(index * step) * 50 return { position: { x: index * 12, y: y + elemBBox.height, }, angle: 0, } }) }

该算法将连接桩沿 X 轴按12像素等距排列,Y 轴则按正弦曲线(振幅50)起伏,并以节点包围盒的高度为基线。

第二步:注册到系统

布局算法实现后必须注册到系统,之后即可像内置布局算法一样使用:

Graph.registerPortLayout('sin', sin)

Graph.registerPortLayout是 portLayoutRegistry.register 的静态快捷方式,底层由 Registry.create 创建的portLayoutRegistry承载。注册表还提供了对应的Graph.unregisterPortLayout(见 src/graph/graph.ts#L157)。需要注意的是,Registry.register 在注册同名算法且未开启force时会抛出Port layout with name 'xxx' already registered.错误;若确需覆盖,可调用Graph.registerPortLayout('sin', fn, true)(第三个参数force)。

第三步:像内置算法一样使用

注册以后,就可以在节点配置中直接引用自定义算法名称:

const rect = graph.addNode({ ports: { groups: { sin: { position: { name: 'sin', args: { start: 45, }, }, attrs: { rect: { fill: '#fe854f', width: 11, }, text: { fill: '#fe854f', }, circle: { fill: '#fe854f', r: 5, magnet: true, }, }, }, }, }, }) Array.from({ length: 24 }).forEach(() => { rect.addPort({ group: 'sin' }) })

可运行示例见 site/src/api/port-layout/sin/index.tsx。

布局的底层运行链路

理解布局算法的完整执行链路,有助于排查定位问题:

  1. 参数归并:PortManager.getPortsLayoutByGroup 收集分组内所有连接桩各自的args与分组的默认args,并查找注册表中的布局函数。若分组未显式配置position.name,则回退到portLayoutPresets.left(默认左侧排布,见 src/model/port.ts#L140);
  2. 名称校验:若注册表中不存在指定名称,会调用portLayoutRegistry.onNotFound抛出错误,并借助拼写建议机制给出提示(如Port layout with name 'sinx' does not exist. Did you mean 'sin'?,见 src/registry/registry.ts#L117-L141);
  3. 坐标换算:布局结果中的position经由 util.ts 的 normalizePoint / toResult 处理——normalizePoint使用NumberExt.normalizePercentage把百分比与数值统一换算为相对坐标,toResult将位置、角度与原始args合并为最终PortLayoutResult
  4. 渲染应用:视图层 src/view/node/index.ts#L464-L488 读取metric.portLayout,通过applyPortTransform将相对位置换算为画布坐标,并应用旋转-(portLayout.angle || 0),最终呈现到 SVG 中。

这一链路同样适用于自定义算法——你只需要关心"给定参数与包围盒,返回相对位置",其余坐标换算、渲染与交互均由框架完成。

内置布局速查

算法名适用场景关键参数
absolute精确摆放锚点xy(支持百分比)、angle
left/right/top/bottom矩形节点四边等分排布strictdxdyanglexy
line沿自定义线段排布startendstrictdxdy
ellipse沿圆弧以固定步长排布startstepcompensateRotatedr
ellipseSpread沿椭圆整周均匀排布startcompensateRotatedr
自定义任意数学分布由你的函数签名决定

无论是快速接入内置布局,还是为业务定制专属的锚点分布,X6 的连接桩布局注册表都提供了清晰的扩展点:写一个函数、注册一个名字、在groups.position.name中引用它,即可完成从"布局定义"到"节点渲染"的全流程。

【免费下载链接】X6🚀 JavaScript diagramming library that uses SVG and HTML for rendering.项目地址: https://gitcode.com/GitHub_Trending/x6/X6

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

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

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

立即咨询