上一篇:
Blazor入门-简单svg绘制+导出图像_blazor 画图-CSDN博客
https://blog.csdn.net/pxy7896/article/details/139003443
注意:本文只给出思路和框架,对于具体的计算细节,考虑到日后会写入软件著作权和专利文书,因此不会展示。
目录
- 问题
- 解决思路
- 处理起终位置重叠的元件
- 处理效果
- 处理相隔很近但不重叠的元件
- 碰撞检测
- 坐标调整
- 涉及的svg技术
- 如何将文字写到弧线上
- 文字/label的书写方向
- 矩形绘制中的坐标转换
问题
上一篇的结尾提到Label重叠问题。事实上,这个问题很严重。
比如我手边这个文件,按照上一篇的方法绘制之后,会变成:(箭头是新画法,也是简单的三角函数,不展开)
这种重叠不仅让画面变得不美观、杂乱,更是影响了用户的阅读体验。
有的文件直接无法识别:
这是因为:
- 某些元件的起终位置有重叠
- 在某些区域中,元件彼此相隔太近,因此计算出的坐标彼此间距极小,显示到前台就会重叠
解决思路
处理起终位置重叠的元件
首先是对元件做排序,然后识别出有重叠的元件。
目前我采取一种傻瓜式的解决方案,流程是:
- 对元件做排序(可以按先起点后终点或者按中点升序排序),获取排序后的列表
L1
- 将可以放置元件的区域分成 N 层(实际是 N 层同心圆环),对于
L1
中每个特征,使用下面的分层方法:
2.1 将第一个特征 f 0 f_0 f0 放入第一层;
2.2 对于后续特征 f i f_i fi(起始数值分别为 f i s f_{is} fis和 f i e f_{ie} fie):遍历当前层的空闲位置,如果对于某个空闲位置 p i p_i pi(起始数值分别为 p i s p_{is} pis和 p i e p_{ie} pie ),那么满足 f i s > p i s , f i e < p i e f_{is} > p_{is}, f_{ie} < p_{ie} fis>pis,fie<pie ,即可放入当前层,修改当前层的对象列表;
2.3 如果当前层无法放置 f i f_i fi,那么新建一个层,将 f i f_i fi 直接放入;
2.4 循环直到遍历结束
在循环中,可以为每个元件记录所属层编号,层数直接与计算坐标时的半径关联。
代码只是简单的三角函数,不会给出。
处理效果
处理相隔很近但不重叠的元件
这个主要包含两块:
- 碰撞检测:即判断两个label是否有重合
- 调整label的位置
碰撞检测
如果有很多对象,需要进行比较高效的碰撞检测,可以考虑 Quadtree (四叉树) 算法。这个算法将空间分为4个象限,可以快速检测碰撞。这里当然可以使用,不过考虑到我的元素没那么多,就使用最简单的碰撞检测函数:
bool isLabelColliding(Rectangle r1, Rectangle r2) {double x1 = r1.X;double y1 = r1.Y;double w1 = r1.Width;double x2 = r2.X;double y2 = r2.Y; double w2 = r2.Width;return x1 < x2 + w2 && x1 + w1 > x2 &&y1 < y2 + Constants.charHeight &&y1 + Constants.charHeight > y2;
}
其中Rectangle
是我定义的一个类,包含四个double类型的变量 X Y Width Height,分别表示矩形左下角顶点的坐标、宽度和高度。其中高度其实是固定的,因为我假设label都只有一行。
另外,因为我对行高留了一点缓冲量,同时可以忽略框本身轻微的重叠,所以我另外写了一个放松版检测函数:
bool isLabelCollidingSlack(Rectangle r1, Rectangle r2, double xo, double yo)
{double x1 = r1.X;double y1 = r1.Y;double w1 = r1.Width;double x2 = r2.X;double y2 = r2.Y;double w2 = r2.Width;return x1 + xo < x2 + w2 &&x1 + w1 > x2 + xo &&y1 + yo < y2 + Constants.charHeight &&y1 + Constants.charHeight > y2 + yo;
}
其中 xo 和 yo 表示 x 和 y 方向上放松的程度,可以是字的宽度或高度乘一个合适的系数。这个系数就来自实际数据中获得的经验了。
坐标调整
这个其实是最麻烦的一部分,因为盲目的调整可能引入新的碰撞。比如如果 A 和 B 碰撞,则将 B 向下移动一段距离,那么可能原本不重叠的 A 和 C 碰撞了。如果采用调整-碰撞检测-再调整,则需要考虑终止条件,以免超时。
我试过几种调整方式,比如:(当然最后都放弃了,因为调整效果不佳)
- 设置一个基础值 b ,然后对于每个label,给一个随机数作为 offset ,计算label位置时,半径是 r = b + o f f s e t r = b + offset r=b+offset,角度仍然使用元件中点( ( s t a r t + e n d ) / 2 (start + end) / 2 (start+end)/2)在圆上偏移的角度;
- 1中偏移的角度也可以再加偏移量;
- 在1的基础上,进行二次碰撞检测,对于仍然碰撞的,在 x 和 y 上增加一点偏移;
- 在3的基础上,偏移采用正负随机;
- 在3的基础上,对于偏移量的符号,应该根据label所在圆上的位置进行判断:将圆像坐标轴一样十字分割成四等分,向上需要-y,向下需要+y,向左-x,向右+x;
- 使用一个数组记录每个label与其他label碰撞的次数,碰撞次数为0则不需要处理,碰撞次数越多越需要远离圆心。因此不使用随机数和偏移量,而是固定半径,通过碰撞次数的多少调整 x 和 y 上的偏移量;
- 偏移量可以采用爬坡的方式,因为svg图是有边界的,超出边界后label是看不到的。爬坡的意思是偏移量先递增,到了边界再递减或者直接归到下边界值。
还有别的方法没记录。以上的方法也可以组合。但是效果仍然是不好的,因为太随机了,反而使得label的位置无法控制,而且调整后仍然可能再次引入碰撞。
最终的方法我不能写哈哈哈哈,是真的在保密。
我只能说用到了上述某两种方法的组合,而且做了两次碰撞检测(有用到放松版碰撞检测函数)。基本在第二次碰撞检测的时候就只剩很少很少 label 了(比如1~2个),然后我用了新的放置方式:将label以弧形的状态放置在圆环内侧,比如下图这样。(当然,其实也考虑了弧形放置的碰撞问题,那个就更复杂一点)
具体的过程是:
- 将元件按照中点升序排序
- 计算元件 label 内容的长度,与箭头中心弧线的长度做比较,如果能放下,说明 label 可以放在元件内部,那后续也不需要考虑碰撞问题;
- 如果不能放在内部,先根据中点和原始的坐标计算方法,计算label的坐标和宽度
- 将上述计算后的 label 进行碰撞检测,经过一些处理方法调整坐标,再进行第二轮碰撞检测
- 如果还有碰撞,将产生碰撞的 label 以弧线的形态放入圆内部
涉及的svg技术
如何将文字写到弧线上
思路:将内容绑定到一条Path上。需要计算 path 的 d 属性,同时需要给该 path 指明 id 。对于blazor,只要在循环里绑定即可。
上图对应的html代码如下所示。其中 d 的计算与前篇文章中内弧和外弧的计算思路一致,先计算各种数据,然后再拼接。
<text font-size="16" fill="black" ><path id="p1" d="M 763.9369985579203 297.9295177580384 A 245 245 36.01448997965174 0 1 838.7345961997676 424.9473653915278 z" /><textPath href="#p1">Hello everyone!</textPath>
</text>
文字/label的书写方向
<text>
中有一个属性是direction
,值为"rtl"
表示从右向左书写,"ltr"
则相反。
矩形绘制中的坐标转换
在前面进行碰撞检测时,为了直观,我给所有label加了一些防撞框(或者说边界框),如下图所示:
需要注意的是,前面我的碰撞检测函数,是以左下角为顶点,向右延长width,向上延长height这样的方式构建边界框,再进行检测的。但是在svg绘制rect时,它是向下延长height构建的,所以需要给原本的坐标减去字高(假设只有一行),否则构建的框如下图所示:
同时,考虑到左右半圆,label中文字的书写方式是不同的,因为处理 rect 的 x 坐标也需要减去相应的宽度。
最后,rect的一种写法是:
<rect x="xx" y="xx" width="xx" height="xx" fill="none" stroke="red" stroke-width="1" />