处理多折线
最近实现了一个比较有趣的小功能,其中有两个需要注意的小细节。
功能概述
多折线每一段都与相邻段正交,可以通过拖拽任一端点调整其所在段折线的长度/位置到目标位置。拖拽过程中始终保持每一段都与相邻段正交。
为了叙述方便,这里定义折线的数据部分形式为由每个端点的坐标组成的列表,如一条由六个点组成的多折线 L
的点列表示例:
scss
P0 (0, 0)
P1 (100, 0)
P2 (100, 100)
P3 (150, 100)
P4 (150, 0)
P5 (200, 0)
上述折线用代码表示如下:
C++
QList<QPoint> vertexList = {
QPoint(0, 0),
QPoint(100, 0),
QPoint(100, 100),
QPoint(150, 100),
QPoint(150, 0),
QPoint(200, 0)
}
细节一
功能需求
在调整折线时,若将点 P1 (100, 0)
拖拽到 (150, -50)
处, P1'
, P2'
, P3
以及 P4
将位于一条直线 (x = 150
) 上,若此时完成拖拽操作,新的折线 L'
应变为:
scss
P0' (50, -50)
P1' (150, -50)
P4' (150, 0)
P5 (200, 0)
即当同一段线段上若存在
≥ 3
个端点,则应仅保留这一段中端点索引最小和最大的两个端点
代码实现
用最简单粗暴的逻辑来实现这个方法,很容易想到的就是遍历:
其中 adjustedIndex
为当前拖拽的端点索引
C++
void removeRedundantVertex() {
// 记录 x 方向,即水平方向,共线点中的最小/最大索引
int minXIndex = adjustedIndex;
int maxXIndex = adjustedIndex;
// 记录 y 方向,即竖直方向,共线点中的最小/最大索引
int minYIndex = adjustedIndex;
int maxYIndex = adjustedIndex;
// 共线基准点,即当前调整点的最新位置
QPoint pivot = newAdjustedPos.toPoint();
// X/Y 方向上的共线点计数,基准点与自身共线,因此初始值为 1
int XCollinearCount = 1;
int YCollinearCount = 1;
for(int i = 0; i < vertexList.size(); i++)
{
if(i == adjustedIndex)
{
continue;
}
QPoint curr = vertexList.at(i);
if(curr.x() == pivot.x())
{
minYIndex = i < minYIndex ? i : minYIndex;
maxYIndex = i > maxYIndex ? i : maxYIndex;
XCollinearCount += 1;
}
if(curr.y() == pivot.y())
{
minXIndex = i < minXIndex ? i : minXIndex;
maxXIndex = i > maxXIndex ? i : maxXIndex;
YCollinearCount += 1;
}
}
// 若存在多点共线,则移除最小 index 和最大 index 之间的共线点
if(XCollinearCount > 2)
{
// pseudo code
// 移除索引范围为 [minYIndex + 1, maxYIndex) 的端点
vertexData->removeVertexRange(minYIndex + 1, maxYIndex);
}
if(YCollinearCount > 2)
{
// pseudo code
// 移除索引范围为 [minXIndex + 1, maxXIndex) 的端点
vertexData->removeVertexRange(minXIndex + 1, maxXIndex);
}
}
bug
但在实际拖拽时会发现,这个逻辑存在一定问题。若将示例的折线 L
中的端点 P1 (100, 0)
先拖拽到 (100, 1)
,此时未完成拖拽,继续拖拽回 (100, 0)
后结束拖拽,正确的结果应为折线 L
不变。但由函数 removeRedundantVertex()
逻辑得到的结果为折线 M
:
scss
P0' (0, 0)
P5 (200, 0)
bug 修复
问题在于不应遍历整个 vertexList
,这样可能会 "误伤" 原本就位于同一直线上,但与调整点并不相邻的其他 "无辜" 端点。因此,只应遍历被调整端点的相邻端点。修改后的 removeRedundantVertex()
如下:
C++
void SchemaGraphicsWire::removeRedundantPoint() {
int vertexCount = vertexList.size();
// 记录 x 方向,即水平方向,共线点中的最小/最大索引
int minXIndex = adjustedIndex;
int maxXIndex = adjustedIndex;
// 记录 y 方向,即竖直方向,共线点中的最小/最大索引
int minYIndex = adjustedIndex;
int maxYIndex = adjustedIndex;
// 共线基准点,即当前调整点的最新位置
QPoint pivot = newAdjustedPos.toPoint();
// X/Y 方向上的共线点计数,基准点与自身共线,因此初始值为 1
int XCollinearCount = 1;
int YCollinearCount = 1;
// 从 mAdjustedIndex 向前搜索
int i = adjustedIndex - 1;
// 从 mAdjustedIndex 向后搜索
int j = adjustedIndex + 1;
while(i >= 0 || j < vertexCount)
{
if(i >= 0)
{
QPoint curr = vertexList.at(i);
if(curr.x() == pivot.x())
{
minYIndex = i < minYIndex ? i : minYIndex;
maxYIndex = i > maxYIndex ? i : maxYIndex;
XCollinearCount += 1;
i--;
}
else if(curr.y() == pivot.y())
{
minXIndex = i < minXIndex ? i : minXIndex;
maxXIndex = i > maxXIndex ? i : maxXIndex;
YCollinearCount += 1;
i--;
}
else
{
break;
}
}
if(j < vertexCount)
{
QPoint curr = vertexList.at(j);
if(curr.x() == pivot.x())
{
minYIndex = j < minYIndex ? j : minYIndex;
maxYIndex = j > maxYIndex ? j : maxYIndex;
XCollinearCount += 1;
j++;
}
else if(curr.y() == pivot.y())
{
minXIndex = j < minXIndex ? j : minXIndex;
maxXIndex = j > maxXIndex ? j : maxXIndex;
YCollinearCount += 1;
j++;
}
else
{
break;
}
}
}
}
细节二
功能需求
我们仍以折线 L
为例,若将点 P1 (100, 0)
拖拽至 (-100, 0)
,拖拽过程中经过点 P0 (0, 0)
。
bug
此时我们会发现,当 P1'
经过 P0 (0, 0)
的一瞬间, P0
就会消失, P1
似乎永远无法越过 P0
。
bug 修复
这个现象非常好解决,是因为在调整过程中,修改的一直是原数组,即 vertexList
中的数据。(至于为什么一定要在操作过程中修改,是因为在 GUI 绘制中,拖拽过程中的折线的变化应实时绘制。)
我们只需要在操作前将 vertexList
copy 一份即可,调整过程中修改的始终是 vertexListCopy
。 并在调整结束时,如 mouseRelease
时,再用 vertexListCopy
替换原有的 vertexList
。