几何算法:判断两条线段是否相交

2023年 8月 30日 50.7k 0

大家好,我是前端西瓜哥。

如何判断两条线段(注意不是直线)是否有交点?

传统几何算法的局限

上过一点学的西瓜哥我,只用高中学过的知识,还是可以解这个问题的。

一条线段两个点,可以列出一个两点式(x - x1) / (x2 - x1) = (y - y1) / (y2 - y1)),两条线段是两个两点式,这样就是 二元一次方程组 了 ,就能求出两条直线的交点。

然后判断这个点是否在其中一条线段上。如果在,说明两线段相交,否则不相交。

看起来不错,但这里要考虑直线垂直或水平于坐标轴的特殊情况,还有两条直线平行导致没有唯一解的情况,除数不能为 0 的情况。

特殊情况实在是太多了,能用是能用,但不好用。

那么,有其他的更好的解法吗?

有的,叉乘。

叉乘是什么?

叉乘(cross product)是线性代数的一个概念,也叫外积、叉积、向量积,是在三维空间中两个向量的二元运算的结果,该结果为一个向量。

但那是严格意义上的。实际也可以用在二维空间的二维向量中,不过此时它们的叉乘结果变成了标量。

假设向量 A 为 (x1, y1),向量 B 为 (x2, y2),则叉乘 AxB 的结果为 x1 * y2 - x2 * y1。

(注意叉乘不满足交换律)

在几何意义上,这个叉乘结果的绝对值对应两个向量组成的平行四边形的面积。

此外可通过符号判断向量 A 变成向量 B 的旋转方向。

如果叉乘为正数,说明 A 变成 B 需要逆时针旋转(旋转角度小于 180 度);

如果为负数,说明 A 到 B 需要顺时针旋转;

如果为 0,说明两个向量平行(或重合)。

叉乘解法的原理

回到题目本身。

假设线段 1 的端点为 A 和 B,线段 2 的端点为 C 和 D。

我们可以换另一个角度去解,即判断线段 1 的两个端点是否在线段 2 的两边,然后再反过来比线段 2 的两点是否线段 1 的两边。

这里我们可以利用上面 叉乘的正负代表旋转方向的特性。

以上图为例, AB 向量到 AD 向量位置需要逆时针旋转,AB 向量到 AC 向量则需要顺时针,代表 C 和 D 在 AB 的两侧,对应就是两个叉乘相乘为负数。

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

const [a, b] = seg1;
const [c, d] = seg2;

// d1 的符号表示 AB 旋转到 AC 的旋转方向
const d1 = crossProduct(a, b, c);

只是判断了 C 和 D 在 AB 线段的两侧还不行,因为可能还有下面这种情况。

所以我们还要再判断一下,A 和 B 是否在 CD 线的的两侧。计算过程同上,这里不赘述。

一般实现

type Point = [number, number];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
  const x1 = p2[0] - p1[0];
  const y1 = p2[1] - p1[1];
  const x2 = p3[0] - p1[0];
  const y2 = p3[1] - p1[1];
  return x1 * y2 - x2 * y1;
}

function isSegmentIntersect(
  seg1: [Point, Point],
  seg2: [Point, Point],
): boolean {
  const [a, b] = seg1;
  const [c, d] = seg2;

  const d1 = crossProduct(a, b, c);
  const d2 = crossProduct(a, b, d);
  const d3 = crossProduct(c, d, a);
  const d4 = crossProduct(c, d, b);

  return d1 * d2 < 0 && d3 * d4 < 0;
}

// 测试
const seg1: [Point, Point] = [
  [0, 0],
  [1, 1],
];
const seg2: [Point, Point] = [
  [0, 1],
  [1, 0],
];

console.log(isSegmentIntersect(seg1, seg2)); // true

注意,这个算法认为线段的端点刚好在另一条线段上的情况,不属于相交。

考虑点在线段上或重合

如果你需要考虑线段的端点刚好在另一条线段上的情况,需要额外在叉乘为 0 的情况下,再判断一下线段 1 的端点是否在另一个线段的 x  和 y 范围内。

对应的算法实现:

type Point = [number, number];

function crossProduct(p1: Point, p2: Point, p3: Point): number {
const x1 = p2[0] - p1[0];
const y1 = p2[1] - p1[1];
const x2 = p3[0] - p1[0];
const y2 = p3[1] - p1[1];
return x1 * y2 - x2 * y1;
}

function onSegment(p: Point, seg: [Point, Point]): boolean {
const [a, b] = seg;
const [x, y] = p;
return (
x >= Math.min(a[0], b[0]) &&
x = Math.min(a[1], b[1]) &&
y

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论