我将为任何可以分成直线段的形状提供通用解决方案。
因此,正如您可能已经猜到的那样,我首先将您的“形状”视为完成循环的段列表。或者简单地放置一个代表循环的点的循环列表,例如,您的正方形将是以下点列表:
0, 0
0, 2
2, 2
2, 0
请注意,我们认为每个点到下一个点都有线段,并且最后一个点连接到第一个点。另外,我们要求连续的点都不相等,第一个点和最后一个点也不相等。如果有的话,必须在继续之前将其删除。
现在,对于每个片段,我们可以确定边界框。例如给定这个片段:
a = (0, 2)
b = (2, 2)
那么 x 中的值范围是 [0, 2],y 中的值范围是 [2, 2],这就是该段的边界框。
接下来您需要的是线段线的导向向量。为此,首先计算线段的长度:
length = sqrt((a.x - b.x)*(a.x - b.x) + (a.y - b.y)*(a.y - b.y))
进而:
director.x = (a.x - b.x)/length
director.y = (a.y - b.y)/length
注 1:当长度为 0 时,则表示段无效。这就是为什么我们不想要重复的点。
注2:使用指向矢而不是直线方程会使事情变得更容易。
现在,给定一个点 p,您可以确定该点是否在一个线段中(如果它是列表中的点之一)。对于其余情况,我们首先查看它是否位于轴对齐边界框内。只需检查范围即可完成此操作:
if
(
(p.x >= box.left && p.x <= box.right) &&
(p.y >= box.top && p.y <= box.bottom) // with origin at the top-left corner
)
{
//It is inside of the bounding box
}
如果是,那么我们计算从点到线的距离,如果是
0然后就上线了。现在,由于浮点运算,您可以测试距离是否小于或等于 epsilon,其中 epsilon 是一个非常小的数字。
我们使用这个公式:
distance vector = (a - p) - ((a - p) · director) * director
distance = the norm of distance vector
其中“·”表示点积,“*”表示标量积。
剩下的就是迭代这些线段,为每个线段计算距离,如果任何一个线段的距离小于 epsilon,则该点位于“形状上”。
好吧,但是“形状”呢?
好吧,借助拓扑学的一点帮助,我们可以确定一个点是否在内部。这与 Windows 用于填充多边形或多段线的算法相同(例如在 Microsoft Paint 中徒手确定选定区域内的内容)。
事情是这样的:
计算从外面到达该点需要穿过的路段数。如果数字是对,那么它在外面,如果它是奇数,那么它在里面。
您可以选择从哪个方向到达该点。我选择左边。
您将再次迭代这些段。对于每一个,我们需要确定它是否在垂直范围内。为此,请使用边界框:
if ((p.y >= box.top && p.y <= box.bottom))
{
//In the vertical range
}
现在,确定该段是在左侧还是右侧:
if (p.x < box.left)
{
//The segment is at the left
}
else if (p.x > box.right)
{
//The segment is at the right
}
else
{
//The segment is close, need further calculation
}
如果该线段很近,我们需要计算到该线段的矢量距离并检查其方向。
向量距离?好吧,我们已经有了它,我们正在用它的范数来确定距离。现在,不采用范数,而是验证 x 坐标的符号。小于0则为右,大于0则为左。如果为0...则表示该段是水平的(因为距离向量始终垂直于该段),可以跳过该段*。
*:事实上,如果该线段是水平的并且在垂直范围内,则表示它在该线段处。片段是否“正常”?
现在,您需要计算左侧线段的数量,如果是奇数,则该点位于形状内部。否则就出局了。这也可以通过上方、右侧或下方的段来完成。我刚刚选了左边。
对于迭代所有段的成本较高的大型形状,您可以将段存储在某些空间分区数据结构中。这超出了本文的范围。