PortLayout

6 min read

链接桩布局算法是一个函数具有如下签名的函数,返回链接桩相对于节点的相对位置。例如,某节点在画布的位置是 { x: 30, y: 40 },如果返回的某个链接桩的位置是 { x: 2, y: 4 },那么该链接桩渲染到画布后的位置是 { x: 32, y: 44 }

type Definition<T> = (
  portsPositionArgs: T[],  // 链接桩中指定的布局算法参数
  elemBBox: Rectangle,     // 节点的包围盒
  groupPositionArgs: T,    // group 中定义的默认布局算法参数
) => Result[]

interface Result {
  position: Point.PointLike // 相对于节点的位置
  angle?: number            // 旋转角度
}

需要注意的是,配置链接桩 ports 时,我们只能通过 groups 选项来配置布局算法,而在 items 中可以提供可选的布局算法参数 args 来影响布局结果。

graph.addNode(
  ...,
  ports: {
    // 链接桩分组
    groups: {
      group1: {
        position: {
          name: 'xxx', // 布局算法名称
          args: { },   // 布局算法的默认参数
        },
      },
    },

    // 链接桩定义
    items: [
      {
        groups: 'group1',
        args: { }, // 覆盖 group1 中指定的默认参数
      },
    ],
  },
)

下面我们一起来看看如何使用内置的链接桩布局算法,以及如何自定并注册自定义布局算法。

presets

Registry.PortLayout.presets 命名空间下提供了以下几个内置的布局算法。

absolute

绝对定位,通过 args 指定链接桩的位置。

interface AbsoluteArgs {
  x?: string | number
  y?: string | number
  angle?: number
}

参数

名称类型必选默认值描述
xstring | number0链接桩在 X 轴相对位置。
ystring | number0链接桩在 Y 轴相对位置。
anglenumber0链接桩旋转角度。

xy 为百分比字符串或位于 [0, 1] 之间时,表示在宽度和高度方向的百分比偏移量,否则表示绝对偏移量。

用法

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

left, right, top, bottom

链接桩沿矩形指定边线均匀布局,leftrighttopbottom 四个布局对矩形形状的节点非常友好,可以通过 args 来设置偏移量和旋转角度。

interface SideArgs {
  dx?: number
  dy?: number
  angle?: number
  x?: number
  y?: number
}

参数

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

用法

graph.addNode({
  ports: {
    groups: {
      group1: {
        position: 'left',
      },
    },
    items: [
      {
        group: 'group1',
        args: {
          dx: 2,
        },
      },
    ]
  },
})

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沿 X 轴方向的偏移量。
anglenumber0链接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

用法

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,
        },
      },
    ]
  },
})

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沿 X 轴方向的偏移量。
anglenumber0链接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

用法

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 } },
  })
})

ellipseSpread

沿椭圆均匀分布的链接桩,从 start 指定的角度开始均匀分布。

interface EllipseSpreadArgs {
  start?: number
  compensateRotate?: boolean
  dr?: number
  dx?: number
  dy?: number
  angle?: number
  x?: number
  y?: number
}

参数

名称类型必选默认值描述
startnumber起始角度。
compensateRotatenumberfalse是否沿圆弧修正链接桩的旋转角度。
drnumber0沿半径方向的偏移量。
dxnumber0沿 X 轴方向的偏移量。
dynumber0沿 X 轴方向的偏移量。
anglenumber0链接桩的旋转角度。
xnumber-用指定的 X 坐标覆盖计算结果中的 X 坐标。
ynumber-用指定的 Y 坐标覆盖计算结果中的 Y 坐标。

用法

const node = graph.addNode({
  ports: {
    groups: {
      group1: {
        position: {
          name: 'ellipseSpread',
          args: {
            start: 45,
          },
        },
      },
    },
  },
})

Array.from({ length: 36 }).forEach(function (_, index) {
  ellipse.addPort({
    group: 'group1',
    id: `${index}`,
    attrs: { text: { text: index } },
  })
})

registry

链接桩布局算法是一个函数具有如下签名的函数,返回每个链接桩相对于节点的相对位置。例如,某节点在画布的位置是 { x: 30, y: 40 },如果返回的某个链接桩的位置是 { x: 2, y: 4 },那么该链接桩渲染到画布后的位置是 { x: 32, y: 44 }

type Definition<T> = (
  portsPositionArgs: T[],  // 链接桩中指定的布局算法参数
  elemBBox: Rectangle,     // 节点的包围盒
  groupPositionArgs: T,    // group 中定义的默认布局算法参数
) => Result[]

interface Result {
  position: Point.PointLike // 相对于节点的位置
  angle?: number            // 旋转角度
}

所以我们可以按照上面规则来创建自定义布局算法,例如,实现一个正弦分布的布局算法:

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,
    }
  })
}

布局算法实现后,需要注册到系统,注册后就可以像内置布局算法那样来使用。

register

register(entities: { [name: string]: Definition }, force?: boolean): void
register(name: string, entity: Definition, force?: boolean): Definition

注册自定义布局算法。

unregister

unregister(name: string): Definition | null

删除注册的自定义布局算法。

实际上,我们将 registryregisterunregister 方法分别挂载为 Graph 的两个静态方法 Graph.registerPortLayoutGraph.unregisterPortLayout,所以我们定义的正弦布局可以像下面这样注册到系统:

Graph.registerPortLayout('sin', sin)

或者:

Graph.registerPortLayout('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,
    }
  })
})

注册以后,我们就可以像内置布局算法那样来使用:

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' })
})