开始前在这里贴一下我们的项目地址和官网,欢迎大家访问和star⭐️~
官网地址:logicflow.cn/
为什么要加渐进连线🤔
被问起这个问题时,小编的第一反应是“当然是为了提升画图体验!”
试想一下,当我们使用流程图编辑工具绘制一个审批流程时,通常的操作会是 创建节点后,从一个节点的锚点拖出一条连线,再将其连接到另一个节点的锚点,如此重复,直到所有节点都完成连接。这种手动逐一连线的过程,虽然可行,但显得有些繁琐,尤其在绘制复杂流程图时,更容易让人感到疲劳。
如果给流程图工具加入渐进连线功能,这一切都会变得更高效。 只需要移动节点或者将连线拖拽到一定距离,系统就能自动生成一条连线,大大减少了用户的操作量,让画图体验变得更加轻松和自然。
本期,小编就带大家从 0 开始,手把手实现这个渐进连线功能!
功能设计
渐进连线的核心逻辑
渐进连线的核心功能是:
• 动态预览:当鼠标拖拽节点或锚点靠近另一个节点时,创建一条预连线以展示连接效果。
• 自动生成:在拖拽结束后,自动将预连线转换为实际连线。
为此,小编设计了以下两种交互模式:
| 模式 | 节点拖拽模式 | 锚点拖拽模式 |
|---|---|---|
| 流程 |
为了确保渐进连线功能的灵活性和准确性,我们对实现进行了以下约束:
-
连线阈值可配置:用户可通过参数调整触发连线的最小距离(阈值),以适应不同画布场景需求。
-
预连线类型:动态生成的预连线为虚线,实际连线类型遵循画布的默认连线样式,保持一致性。
-
优先连接最近锚点:系统会优先连接距离最近节点上的可连接锚点,保证连线的合理性和准确性。
-
多节点情况下的规则:当多个节点与拖拽节点的距离都小于阈值,且距离相同时,系统会判断最接近的锚点,并以此创建预连线。
-
用户可控的连线方向:用户可以自由决定连线的指向:
- 前置连接:以当前拖拽节点为起点,连接到目标节点。
- 后置连接:以目标节点为起点,连接回当前拖拽节点。
技术方案
为了实现这个功能,我们需要满足以下条件:
- 参数支持:支持传入连线阈值(如触发预连线的最小距离)和连线方向(单向或双向)。
- 事件监听:实时监听节点和锚点的拖拽事件(如 drag 和 dragend),动态创建预连线,并在拖拽结束时生成真实连线。
- 去重逻辑:在生成连线时,需要判断当前锚点之间是否已经存在预连线或真实连线,避免重复创建。
整个功能框架大概会是这样:
预连线生成逻辑详解
在实际执行过程中,画布上的节点相当于多了一个感应区,等待着锚点进入感应区内。 节点在拖拽过程中就遍历所有节点和它们的锚点,找到最近的一组锚点。
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)
}
完整过程示意图:
过程中我们可能会遇到锚点之间已经存在连线或预连线的情况,因此需要增加去重逻辑,以确保画布不会出现重复连线。
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)
}
// ... 创建虚拟边相关逻辑
}
至此渐进连线功能就实现完成啦~
如果你对实现细节感兴趣,可以访问完整源码:github.com/didi/LogicF…
结语
渐进连线功能的加入,不仅提升了用户的画图体验,也让流程图工具更加智能和易用。希望本文的设计和实现思路能为你带来启发,也欢迎大家尝试实现属于自己的渐进连线功能!
如果这篇文章对你有帮助,欢迎关注我们的账号,我们会持续输出干货文章。也希望您能为我们的项目点上 Star,这对我们非常重要,感恩的心~
项目地址传送门:github.com/didi/LogicF…