手把手带你为流程图增加渐进连线能力😉

826 阅读6分钟

开始前在这里贴一下我们的项目地址和官网,欢迎大家访问和star⭐️~

项目地址:github.com/didi/LogicF…

官网地址:logicflow.cn/

为什么要加渐进连线🤔

被问起这个问题时,小编的第一反应是“当然是为了提升画图体验!”

试想一下,当我们使用流程图编辑工具绘制一个审批流程时,通常的操作会是 创建节点后,从一个节点的锚点拖出一条连线,再将其连接到另一个节点的锚点,如此重复,直到所有节点都完成连接。这种手动逐一连线的过程,虽然可行,但显得有些繁琐,尤其在绘制复杂流程图时,更容易让人感到疲劳。

Proximity-Connect-插件-LogicFlow-Examples-12-18-2024_03_50_PM.png录屏2024-12-18 15.51.22.gif

如果给流程图工具加入渐进连线功能,这一切都会变得更高效。 只需要移动节点或者将连线拖拽到一定距离,系统就能自动生成一条连线,大大减少了用户的操作量,让画图体验变得更加轻松和自然。

录屏2024-12-18 15.57.00.gif录屏2024-12-18 15.58.00.gif

本期,小编就带大家从 0 开始,手把手实现这个渐进连线功能!

功能设计

渐进连线的核心逻辑

渐进连线的核心功能是:

动态预览:当鼠标拖拽节点或锚点靠近另一个节点时,创建一条预连线以展示连接效果。

自动生成:在拖拽结束后,自动将预连线转换为实际连线。

为此,小编设计了以下两种交互模式:

模式节点拖拽模式锚点拖拽模式
流程节点渐进连线流程.png节点-锚点连线.png

为了确保渐进连线功能的灵活性和准确性,我们对实现进行了以下约束:

  1. 连线阈值可配置:用户可通过参数调整触发连线的最小距离(阈值),以适应不同画布场景需求。

  2. 预连线类型:动态生成的预连线为虚线,实际连线类型遵循画布的默认连线样式,保持一致性。

  3. 优先连接最近锚点:系统会优先连接距离最近节点上的可连接锚点,保证连线的合理性和准确性。

  4. 多节点情况下的规则:当多个节点与拖拽节点的距离都小于阈值,且距离相同时,系统会判断最接近的锚点,并以此创建预连线。

  5. 用户可控的连线方向:用户可以自由决定连线的指向:

    • 前置连接:以当前拖拽节点为起点,连接到目标节点。
    • 后置连接:以目标节点为起点,连接回当前拖拽节点。

技术方案

为了实现这个功能,我们需要满足以下条件:

  1. 参数支持:支持传入连线阈值(如触发预连线的最小距离)和连线方向(单向或双向)。
  2. 事件监听:实时监听节点和锚点的拖拽事件(如 drag 和 dragend),动态创建预连线,并在拖拽结束时生成真实连线。
  3. 去重逻辑:在生成连线时,需要判断当前锚点之间是否已经存在预连线或真实连线,避免重复创建。

整个功能框架大概会是这样:

渐进连线框架.png

预连线生成逻辑详解

在实际执行过程中,画布上的节点相当于多了一个感应区,等待着锚点进入感应区内。 节点在拖拽过程中就遍历所有节点和它们的锚点,找到最近的一组锚点。

allNodes.forEach((node) => {
  if (isEqual(node.id, id)) return
  const { anchors = [] } = node
  // 遍历所有节点,找离当前拖拽节点最近的可连接节点和锚点
  anchors.forEach((anchor) => {
    // 找距离最近的两个锚点
    draggingAnchors.forEach((draggingAnchor) => {
      // 判断拖拽点锚点和当前锚点是否可连线
      const anchorAllowConnect = this.anchorAllowConnect(
        node,
        anchor,
        draggingAnchor,
      )
      if (!anchorAllowConnect) return
      // 获取两个锚点之间的距离
      const curDistance = twoPointDistance(draggingAnchor, anchor)
      if (!distance || curDistance < distance) {
        // 如果是第一条数据,或者当前这对锚点距离更短,就替换数据
        distance = curDistance
        preConnectAnchor = draggingAnchor
        closestAnchor = anchor
        closestNode = node
      }
    })
  })
})
this.currentDistance = distance
this.currentAnchor = preConnectAnchor
this.closestAnchor = closestAnchor
this.closestNode = closestNode

然后判断这组锚点的距离是否小于阈值,是的话创建预连线。

if (this.currentDistance < this.thresholdDistance) {
  this.addVirtualEdge()
}
// ...
addVirtualEdge() {...}

当鼠标释放时,判断当前是否有预连线,如果有则创建真实连线

handleDrop() {
  if (isNil(this.virtualEdge)) return
  const {
    type,
    sourceNodeId,
    targetNodeId,
    sourceAnchorId,
    targetAnchorId,
    startPoint,
    endPoint,
    pointsList,
  } = this.virtualEdge
  this.lf.addEdge({
    type,
    sourceNodeId,
    targetNodeId,
    sourceAnchorId,
    targetAnchorId,
    startPoint,
    endPoint,
    pointsList,
  })
  this.lf.deleteEdge(this.virtualEdge.id)
}

完整过程示意图:

边框.png拖入边框.png连虚线.png真实连线.png

过程中我们可能会遇到锚点之间已经存在连线或预连线的情况,因此需要增加去重逻辑,以确保画布不会出现重复连线。

handleDrag() {
  /**
   * 主要做几件事情
   * 判断当前是否有虚拟连线,有的话判断两点距离是否超过阈值,超过的话删除连线
   * 遍历画布上的所有节点,找到距离最近的节点,获取其所有锚点数据
   * 判断每个锚点与当前选中节点的所有锚点之间的距离,找到路路径最短的两个点时,把当前节点、当前锚点当前最短记录记录下来,作为当前最近数据
   * 判断当前最短距离是否小于阈值
   * 如果是 就创建虚拟边
   */
  const { nodes } = this.lf.graphModel


  if (!isNil(this.virtualEdge)) {
    const { startPoint, endPoint, id } = this.virtualEdge
    const curDistance = twoPointDistance(startPoint, endPoint)
    if (curDistance > this.thresholdDistance) {
      this.lf.deleteEdge(id)
      this.virtualEdge = undefined
    }
  }
  //...创建预连线相关逻辑
}
// 增加虚拟边
addVirtualEdge() {
  const { edges } = this.lf.graphModel
  // 判断当前是否已存在一条同样配置的真实边
  const actualEdgeIsExist = reduce(
    edges,
    (result, edge) => {
      if (edge.virtual) return result
      return result || this.sameEdgeIsExist(edge)
    },
    false,
  )
  // 如果有真实边就不重复创建边了
  if (actualEdgeIsExist) return


  // 判断当前是否有虚拟边
  // 如果当前已有虚拟边,判断当前的节点和锚点信息与虚拟边的信息是否一致
  if (!isNil(this.virtualEdge)) {
    const {
      virtualEdge: { id: edgeId },
    } = this
    // 信息一致不做处理
    if (this.sameEdgeIsExist(this.virtualEdge)) return
    // 不一致就删除老边
    this.lf.deleteEdge(edgeId)
  }
  // ... 创建虚拟边相关逻辑
}

至此渐进连线功能就实现完成啦~

最终效果展示.gif

如果你对实现细节感兴趣,可以访问完整源码:github.com/didi/LogicF…

结语

渐进连线功能的加入,不仅提升了用户的画图体验,也让流程图工具更加智能和易用。希望本文的设计和实现思路能为你带来启发,也欢迎大家尝试实现属于自己的渐进连线功能!

如果这篇文章对你有帮助,欢迎关注我们的账号,我们会持续输出干货文章。也希望您能为我们的项目点上 Star,这对我们非常重要,感恩的心~

项目地址传送门:github.com/didi/LogicF…