本文核心内容由我们画图团队的小姐姐 @huanhuanwa 执笔,介绍最近给流程图支持的魔法功能:正交线支持自定义拐点,下面是正文。
前言
我们流程图业务的连线目前支持三种形态,分别是直线、曲线和正交线,这其中最复杂的要数正交线了,关于正交线的实现之前的文章已经详细介绍过了(传送门: pubuzhixing:画图技术之流程图正交连线算法 ),而今天要分享的,是在此基础上支持自定义拐点。
什么是自定义拐点呢?大家都知道两点确定一条直线,观察下面这条正交线可以看出,它是由四个点构成的:
这条连线是根据点 source,target 加上 A* 算法绘制出来的,也就是点 a,b 是计算得出的点,并不存储在数据中,那么如果我想通过拖动连线把 source 和 target 之间的连线可以转换成其它形态,就需要在这条线的基础上加减或者修改点来完成了。
如下图,当向上拖动 [source, a] 这条连线时,增加了点 d,a',这些不在默认正交连线上的数据点就称为自定义拐点。
技术实现
实现的过程可谓是非常曲折,前后总共修改了四个版本才达到现在效果。
1.0 版本
还是以这条连线举例,因为默认正交连线的绘制是根据 source 和 target 计算来的,也就是点 a、b 其实并不存储在数据点中,这就导致无法直接根据已有的数据点来新增/修改点,所以 1.0 版本的方案是:
- 当检测到拖拽时,获取当前拖拽线段在整个正交连线线段中的 index
- 获取到正交连线上的所有拐点
- 根据上述获取到的点和 index 查找到要修改的拐点,根据拖拽距离修改拐点
- 把修改后的数据点连同连线上的其它拐点一起存储到数据上
因为所有点都存入了数据中,每一次拖拽都是基于上一次拖拽的结果来渲染,这在一定程度上是方便计算的,但核心问题也是这个,因为所有点都存入了数据中,就是无法根据数据中的区分该点是否已经拖拽过,有几个场景就无法实现了:
- 无法在 UI 上区分:拖动过的线段和未拖动过的线段
- 如果拖动连线的 source 和 target 节点,默认的正交连线会重新计算,但此时当前的连线上的拐点已经全部记录到数据中,无法做一些自动的调整(连线可能会没法看)
所以在经过讨论后,确认了实现的一个大方向: 拖拽时只存储正交连线中已经拖拽过的点,并且在拖拽 source 和 target 时,不修改已存储的数据点 。
2.0 版本
要增加点有两个前提,第一是确定在什么位置插入,第二要确定怎么插入,所以这个版本的处理思路如下:
- 当检测到拖拽时,获取当前拖拽线段的 index
- 根据正交连线实际渲染的拐点(renderKeyPoints)和数据中的点(dataPoints),确认以下几种修改情况(3 种情况),获取对应的的 deleteCount 和 index
- 根据 index 和 deleteCount 修改 dataPoints 中的数据
① 增加两个点: dataPoints 中的数据在 renderKeyPoints 中不存在,deleteCount = 0, index = 1
下图中红色点表示 dataPoints 中已经存在的点,紫色代表修改点,蓝色代表新增点
向右移动虚线 A,在 dataPoints 中 index= 1 的位置新增两个点 [a, b]
② 增加一个点,修改一个点 :dataPoints 中的数据在 renderKeyPoints 中存在一个,deleteCount = 1, index = 1)
向下移动虚线 A,在 dataPoints 中找到 index = 1 的位置,删除点 a,新增点 a' b
③ 修改两个点 :dataPoints 中的数据在 renderKeyPoints 中存在两个,deleteCount = 2, index = 1
向右移动虚线 A,在 dataPoints 中找到 index = 1 的位置,删除点 a, b,新增 a',b'
这个版本其实已经确定了大致的处理思路,但是最开始计算 deleteCount 时考虑的场景过于简单了,这些判断条件在不拖动连线的 source 和 target 时没有问题,一旦拖动过,dataPoints 中的数据在 renderKeyPoints 中就无法对应了。
如下图,当向上移动 source 元素后,dataPoints 中的点 a 已经不在实际渲染的连线上了(实线),这个时候单纯的通过判断 dataPoints 中在 renderKeyPoints 中已经得不到正确的结果了,这时再向右拖动 [a',b] 时,按原来的算法得到的是「增加一个点,修改一个点」,但其实这里应该是「修改两个点」。
情况说明:
图示虚线代表拖动前的位置,实线代表拖动后的位置(拖动后会造成数据点无法和真实路径完全匹配,产生了脏的路径点)
dataPoints 指数据结构上存储的自定义拐点,是需要持久化的数据
renderKeyPoints 是当前实际渲染的正交连线拐点,这个是需要动态计算的
向右拖动 [a',b],在 dataPoints 中修改点 a 为 a',点 b 为 b'
3.0 版本
这个版本主要针对获取 deleteCount 和 index 的错误进行了处理,这种错误出现在拖动连线的 source 和 target 后,我们无法根据实际渲染的连线 renderKeyPoints 在已有的数据点 dataPoints 中找到对应的点了,也就无法确定要修改数据的的 index 和 deleteCount ,针对这种情况我们采用了 n 点共线的思路来解决。
比如上图情况下,点 a 虽然不在连线上,但点 a,a',b 在一条直线上,当拖动线段 [a',b] 时,通过三点共线查找到实际要修改的点为 [a, b],从而得到正确的 index 和 deleteCount
再比如上图这种情况下,点 a, b 都不在连线上,但点 a,b,a',b' 在一条直线上,所以在拖动线段 [a',b'] 时,通过四点共线找到要修改的点 [a,b], 从而得到正确的 index 和 deleteCount
就在我们以为终于处理完了的时候,发现这种计算方式并不能覆盖所有的场景
如上图所示,当先在 dataPoints 中生成两个点 [a,b] 时,拖动 source,然后拖动线段 A,此时线段 A 两端的 [c, d] 跟原数据 [a,b] 没有任何联系,也就无法确定要插入数据的 index 。
4.0 版本
这个版本除了解决 3.0 版本的问题外,还解决了最重要的数据渲染问题。
在 1.0 版本我们确认了,当拖拽 source 和 target 时,不会修改数据,那么问题就来了,如下图,当先在 dataPoints 中生成两个点 [b, c] 时,再拖动 source 和 target,此时数据中存储的点为 [a,b,c,d],但是如果按这个顺序渲染连线,肯定不是我们希望的,那么怎么根据数据点渲染正确的连线呢?
这个问题可能更基础,因为这种情况下只有连线是符合预期的,后面的拖拽调整拐点才可能是正确的,上图的实心连线应该是符合预期的正交线,可以看出实际渲染的连线经过拖拽形成的自定义拐点 [b, c] 所在的直线,但是不经过 [b, c] 点,这里有一个规律:正交连线关联的元素位置变化后,实际渲染的拐点需要根据关联元素的位置进行适当的纠正,但是要保证经过自定义拐点所在的直线。
这块的实现依赖两个比较关键的函数 getMidKeyPoints 和 getMirrorDataPoints,可以先看下面表格的解释:
函数
说明
midKeyPoints
两个数据点之间的标准正交连线的点,如果两个数据点之间的连线是不连通的(两点构成的直线不是水平或者垂直的),那么可能存在两种情况,一种是前面所说的因为关联元素位置发生变化数据点需要进行纠正,另外一种情况就是两个数据点之间存在标准的正交连线点,这种情况一把发生在(midKeyPoints 示意图1、midKeyPoints 示意图2):
-
起点 -> 自定义拐点
-
自定义拐点 -> 终点
-
自定义拐点 -> 自定义拐点
mirrorDataPoints
mirrorDataPoints 是 dataPoints 映射到实际渲染的点连线上的点:
-
mirrorDataPoints 的长度和 dataPoints 的长度相同
-
它们每一个点也是一一对应,但每一个位置的值有可能不同
-
节点经过拖动后会导致 dataPoints 上的点和实际渲染的连线上的点不一致,而 mirrorDataPoints 上面的点 和 dataPoints 上面点的一一对应关系可以根据实际渲染点有效定位数据点。
getMirrorDataPoints
获取上面介绍的 mirrorDataPoints 的函数,计算思路如下(就是根据自定义拐点和关联元素位置计算纠正后的实际点为的过程):
-
基于平行线纠正
-
基于前后点纠正
midKeyPoints 示意图1:自定义拐点和终点之间存在两个 keyPoints 点:
midKeyPoints 示意图2:自定义拐点和自定义拐点之间存在两个 keyPoints 点
下面介绍下 getMirrorDataPoints 的两种纠正:
一、平行线纠正(核心):
- 虚线 source 是移动前的状态、实线 source 是移动后的状态
- 红色的 a 点和 b 点是经过拖拽后新增的自定义拐点
- 红色的虚线是标准的正交连线路径(不存在自定义拐点的情况下)
- 这个时候连接点 p1 和 第一个自定义拐点 a 无法直接形成水平或者垂直的连线,这时就需要平行线纠正
平行线纠正参考的标准其实是标准的正交连线,就是上图的红色虚线,所谓平行线纠正就是查找平行线,上图可以看出红色的 a 点和 b 点和标准正交线上的 key1 点 和 key2 点平行,如此就可以构成一个矩形,如下图蓝色框所示:
此时可能还有一些约束条件就是,构建的这个矩形不可以穿越 source 和 target 节点,上图满足条件,如此根据一些条件判断就可以得到下图的 a' 映射点:
二、前后点纠正:
平行线纠正不起作用(找不到平行线,或者一些限制条件无法满足)时,就需要一个兜底逻辑,就是基于前后点纠正。
前后点纠正就不再考虑正交连线的合理性了,只要保证连线时水平或者垂直的正交线就可以了,这种情况下可能会出现连线穿越图形的情况。
- 自定义的两个拐点是红色的 a 点 和 b 点。
- 红色虚线是标准的正交线连线
- 平行纠正不起作用
- 基于前后点 p1 和 p2 纠正到 a' 和 b'
基于这个方案也可以解决方案 3.0 遇到的问题:
- 通过 getMirrorDataPoints 方法找到数据 mirrorDataPoints([a, b] -> [a, b])
- 拖拽线段 A 时,需要确定新插入的 [c, d] 的位置,那么需要找到 b的位置,通过 mirrorDataPoints 可以获得 b 和 b 的对应关系,得到 b` 的索引(定义为 previousIndex)
- previousIndex + 1 就是我们需要正确插入的位置,这里为 3
除了上述功能外,我们还支持了合并自定义拐点、以及连线吸附等功能。
总结
本文主要介绍了流程图正交线支持自定义拐点的技术思路以及我们的整个踩坑过程,希望给感兴趣或者有类似需求的同学一些参考。
其实刚开始做这个功能的时候没想到会这么复杂,主要是要处理的场景太多了,每次觉得处理好了一测试又发现还有很多新的场景没有覆盖到,直到最后提取出来 mirrorDataPoints 的概念,确立了那些经过偏移的自定义拐点和实际渲染的拐点的对应关系才逐步将问题收敛,虽然过程比较波折的,中间走了一些弯路,但是也不得不说这个需求比较难,走弯路可能也在情理之中。
Plait 框架介绍
一款开源、现代化的绘图框架,用于构建一体化的白板工具产品,比如:思维导图、流程图、自由画笔等等。
Plait 采用插件机制支持具体的业务功能开发,目前已经支持了基础的思维导图插件和流程图插件,基础功能演示传送到:摊牌了,我们在做画图框架!!!_哔哩哔哩_bilibili
欢迎 Star 支持,大家有任何技术上或者交互上问题欢迎提 Issue。