几何求交(一):直线和直线的交点

1. 向量复习

因为本节需要向量来计算交点,如果大家对向量不是很熟悉,请见:向量复习(一)。已经很熟悉的童鞋,可以跳过这部分内容。

2. 思路解析

用向量求解两直线a和b的交点主要思路为:

  1. 用to left测试判断两直线是否有交点;
  2. 求解a两端点在b上的垂线长度,这个长度可以用向量组成的平行四边形来求解;
  3. 用相似三角形求解已知点到交点的向量;
  4. 已知点 + 向量 = 交点;

文字描述太难理解了,我们用一个图例来说明哦。现在我们给定两条直线和他们的两个端点,AB和CD,以及交点E:

在这里插入图片描述
如果我们要求E,运用向量的特性,我们把线段看成向量,那么有一个非常简单的思路:任意一个已知端点 + 该端点到交点的向量。比如选择C点,那么C + CE = E(加粗线段表示向量),因为我们在向量复习(一)中提到,向量的求解可以用两个坐标相减,那么已知一个向量和其中一个点,我们可以 点 + 向量 = 另一个点,如下图所示:

在这里插入图片描述
而且我们知道,CECD是有比例关系的,那么接下来我们需要思考如何求解这个比例1 / t,即CD : CE = 1 / t(后面我们会解释为什么会设成这样,而不是更一般的 CD : CE = t )。如果我们过C和D做AB的垂线,大家知道我们能得到什么信息呢:

在这里插入图片描述

没错你可能已经发现了,两条垂线所在的两个三角形为相似三角形:三角形DED’~ 三角形ECC’。相似的理由为:

  1. ∠DED’ = ∠C’EC;
  2. ∠DD’E = ∠EC’C;

即两个角相等的三角形相似。通过这里,我们知道只要我们求解d1和d2的比例,即可求得CECD的比例 1 / t。那么,现在问题转换成求解d1和d2的比例。怎么做呢?我们主要到,DD’和CC’为垂线,也就是说如果我们能找到以DD’和CC为’垂线的平行四边形,而且两个四边形的底都是AB,那么我们可以通过面积和底边来求解高,即垂线的长度,如下图所示:

在这里插入图片描述

到现在,我们构建了两个我们需要的平行四边形。还差最后一步:如何求得两个平行四边形的面积呢?这里我们就需要向量来帮忙了,不知道大家还记得向量的叉积么?向量叉积的模表示两个向量组成的平行四边形的面积。那么,我们可以引入两个向量:ADAC,然后分别求AD x ABAC x AB就可以求得两个平行四边形的面积,即平行四边形ABFD和平行四边形ACGB的面积。接下来就简单了,我们根据求得的d1和d2,去求解 1 / t;

3. 代码解析

讲解完思路,我们接着讲解一下代码,帮助大家梳理一下代码实现的思路。首先,我们先来看看整体的求交代码:


/**
 * get line intersection point with vector, if exists
 *
 * Reference resource:
 * https://blog.csdn.net/qq_40998706/article/details/87482435
 */

public static
Vector linesIntersect( Line line1, Line line2 ) {
    // have intersection?
    if ( !ifLinesIntersect( line1, line2 ) ) return null;
    // yes, but the intersection point is one of the endpoints.
    if ( isOverlapButHavingCommonEndPoint( line1, line2 ) )
        return getOnlyCommonEndPoint( line1, line2 );

    // yes, normal intersection point.
    Vector base = line2.getVector();
    double d1 = Math.abs( Vector.cross(
            base, line1.startPoint.subtract( line2.startPoint ) ) );
    double d2 = Math.abs( Vector.cross(
            base, line1.endPoint.subtract( line2.startPoint ) ) );
    assert !MyMath.equalZero( d1 + d2 );
    double t = d1 / ( d1 + d2 );
    Vector intersection = line1.getVector().multiply( t );

    // the following commented-out code is correct as well,
    // but with less computational accuracy because of ( 1 / t ),
    // one additional division compared to the method above.
//        double t = ( d1 + d2 ) / d1;
//        Vector intersection = line1.endPoint.subtract(
//        line1.startPoint ).multiply( 1 / t );

    return line1.startPoint.add( intersection );
}

代码基本就是思路的直接实现,但这里需要注意两点:

1)判断是否有交点的方法

/**
 * get the common endPoint, intersection as well,
 * if two lines intersect at one of the endpoints
 */

private static
Vector getOnlyCommonEndPoint( Line line1, Line line2 ) {
    if ( line1.startPoint.equalsXAndY( line2.endPoint ) ) return line1.startPoint;

    return line1.endPoint;
}

/**
 * if the two lines Overlap But Have Common EndPoint,
 * but note that may only have one common endPoint.
 */

private static
boolean isOverlapButHavingCommonEndPoint( Line line1, Line line2 ) {
    return Vector.sortByX( line2.startPoint, line2.endPoint ) > 0 &&
            line1.startPoint.equalsXAndY( line2.endPoint ) ||
            line1.endPoint.equalsXAndY( line2.startPoint );
}

/**
 * toLeft test to check if two lines intersect
 */

private static
boolean ifLinesIntersect( Line line1, Line line2,
                          double res1, double res2 ) {
    // parallel cases:
    // case 1: overlap or on the same line.
    if ( MyMath.equalZero( res1 ) &&
            MyMath.equalZero( res2 ) )
        return isOverlapButHavingCommonEndPoint( line1, line2 );
    // case 2: parallel on the right side.
    if ( res1 < 0 && res2 < 0 )
        return false;
    // case 3: parallel on the left side.
    if ( res1 > 0 && res2 > 0 )
        return false;

    // intersecting cases: either intersect at
    // a common point other than endpoints,
    // or at one of the endpoints.
    return true;
}

/**
 * line1 and line2 intersects?
 */

public static
boolean ifLinesIntersect( Line line1, Line line2 ) {
    if ( line1 == null || line2 == null ) return false;

    // to left test based on line1.
    double res1 = Triangle.areaTwo( line1.endPoint, line1.startPoint, line2.endPoint );
    double res2 = Triangle.areaTwo( line1.endPoint, line1.startPoint, line2.startPoint );
    // to left test based on line2.
    double res3 = Triangle.areaTwo( line2.endPoint, line2.startPoint, line1.endPoint );
    double res4 = Triangle.areaTwo( line2.endPoint, line2.startPoint, line1.startPoint );

    // have intersection if and only if
    // two endpoints of one line are
    // at the opposite side of the other line.
    boolean finalRes1 = ifLinesIntersect( line1, line2, res1, res2 );
    boolean finalRes2 = ifLinesIntersect( line1, line2, res3, res4 );
    return finalRes1 && finalRes2;
}

这里判断两直线是否有直线的方法为To Left测试,即判断另一条直线的两端点位于基准直线的哪一侧,如果两个端点都是异侧,则有交点,或交点为端点;其余情况都没有交点,即两线平行或重合(我们认为有无穷个交点的情况,不算有交点),但注意一种特殊情况:两线共线但有一个公共的端点(这个很特殊,很容易忽略!);

To Left测试会在计算几何中详细介绍,因为如果这里不检查是否有交点,直接用向量去算的话,如果两线平行(非重合),则会算出不存在的交点。大家在代码中的测试案例可以看到这样的情况。

// public static void testLinesIntersection()
System.out.println( linesIntersect( line7, line6 ) ); // null

如果觉得判断交点的代码太复杂的童鞋,可以跳过额,说实话,情况太多了,这里就不细说了,但可以参考一下注释哦。

2)为什么要设CD : CE = 1 / t,而不是CD : CE = t

以上图为例,我们先用这种方法算一下CD : CE,我们假设|CE| = m,|DE| = z,|CD| = l,如下图:

在这里插入图片描述
那么,我们可以得到下面的等式:

  1. l : m = 1 / t;
  2. z + m = l;
  3. d1 : d2 = m / z;

然后我们可以解得 t = d1 / d1 + d2,和代码里面是一样的。那么现在反过来,我们假设CD : CE = t,我们再用上面的等式解出t,t = ( d1 + d2 ) / d1,刚好和上面是反的。好像两者都可以用,但是我们注意一下这里求解交点向量的代码(公式):

Vector intersection = line1.getVector().multiply( t );

如果我们用的是CD : CE = t,那么这里就不是乘t,而是 1 / t 了,因为原来CE = t * CD,现在变成CE = CD / t。从数学的角度来看,两者都是没有什么问题的,但是从计算精度来看,这里多算了一次除法,会导致精度丢失,所以原来的假设更优。看到这里,不得不佩服原作者的设计思路哦,这一点很小,但很精妙。

最后我用代码中用的变量来标记上面的图例,方便大家理解。假设AB为line2,CD为line1:

在这里插入图片描述
最后需要提醒一点:严格来说,上面的代码应该算是线段和线段的交点,而不是直线和直线的交点,但是可以通过一点点的修改将原来的代码处理成直线和直线的交点,或者将直线处理成线段,然后求交,这些问题就留给大家“课后思考题”啦。如果有机会,我将会在计算几何 - BO算法中详细介绍哦~

接下来,我们将讲解另一种几何求交的情况:直线和圆的交点。


上一节:向量复习(一):定义、求解、四则运算、点积和叉积

下一节:几何求交(二):直线和圆的交点

4. 附录:项目代码

1.1.3 Geometric Intersection

DescriptionEntry method\File
Line and lineVector lineIntersect( Line l )
Segment and segmentVector segmentIntersect( Line l )
Segment and CircleVector[] segmentCircle( Segment s, Circle c )
Line and CircleLine lineCircleIntersect( Line line, Circle circle )
Brute ForceList<Vector> bruteForce( List<E> S )
Bentley Ottmann’s algrithom( Intersection Of segment, ray, line and Circle )List<EventPoint2D> findIntersection( List<IntersectionShape> S )
Program ( including visualization )CG2017 PA1-2 Crossroad

5. 参考资料

  1. 计算几何学 | 线段的交点 | Cross Point | C/C++实现

6. 免责声明

※ 本文之中如有错误和不准确的地方,欢迎大家指正哒~
※ 此项目仅用于学习交流,请不要用于任何形式的商用用途,谢谢呢;


在这里插入图片描述

  • 7
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值