资讯动态

圆弧直线求交的通用公式:从参数方程到工程级实现

发布时间:2026/10/13 9:00:10 来源:尧图企业网站定制
圆弧直线求交这个几何问题从高中解析几何就开始学了但等到我真正在工程项目里写“通用公式”时才发现教科书那套和工程实战完全是两码事。这个系列一直想讲的就是怎么把课堂上学过的数学变成工作里能直接用的工具。这篇是第24篇专门聊圆弧和直线求交的通用公式——不是纸上谈兵地摆一个表达式而是能直接写进代码、扛得住上万次调用、边界条件都想清楚的那种版本。先说这公式到底解决什么问题。读图软件里要修剪一段圆弧、数控加工要检测刀具路径和零件轮廓有没有干涉、PCB布线要把圆弧走线转成直线段输出、游戏里要判断子弹轨迹有没有擦到圆角障碍物这些场景背后全是同一个几何操作给一条直线或线段、一段圆弧求它们的交点坐标。学生时代解题只需要代入公式算一次工程里写通用公式面对的是成千上万次调用要求数值稳定、边界清晰、代码高效。这篇文章把两头的需求都照顾到先把推导讲透再给可直接复现的实现最后把那些容易翻车的边界条件逐一拆开看。1. 为什么老生常谈的求交问题还要专门写一篇1.1 从一道课堂练习题到一沓工程Bug清单大部分人第一次接触这个问题的路径是这样的圆的标准方程 ((x-a)^2 (y-b)^2 r^2)直线的一般式 (Ax By C 0)代入消元得到一个一元二次方程解出 (x) 或 (y)完事。课堂练习到这里就结束了但工程实现刚开始。第一层坎是表达方式。直线一般式确实能把水平线、垂直线统一表达但往代码里一落用斜率 (k) 表达就会遇到竖直直线的无穷大问题用一般式又要维护 A、B、C 三个系数跟线段端点之间的换算总绕一道弯。第二层坎是“直线”不等于“线段”。很多场景下我们拿到的是两个端点 P0、P1要判断交点是不是落在这两个点之间直接套用无限直线的公式就得多写一堆区间判断。第三层坎是圆弧本身有方向、有起始角、有终止角交点就算在整圆上也未必落在这段弧里。这三层坎叠起来就把一个“课堂送分题”变成了“生产环境送命题”。我参与过的某个CAD图元编辑模块早期就吃过亏用无限直线求交交点到线段外了也没过滤用户拖着鼠标修剪图形时弧线莫名其妙被切掉一截。后来换成参数方程路线一条函数能同时处理无限直线、线段、射线三种输入所有区间判定都收敛到同一个参数 t 上这才算消停。1.2 应用场景盘点从CAD/CNC到游戏碰撞圆弧直线求交听上去很底层但应用面广得惊人。我随手列一下这些年实际接触到或调研过的场景CAD 图元编辑软件里倒圆角后的轮廓线常由直线段和圆弧段拼成执行修剪、延伸、打断操作时第一步就是算交点。CNC 刀具路径规划里要检查刀具走直线段时是否切进某个圆弧型避让区机床面板上显示走刀轨迹时也要把直线和圆弧求交后的结果做动态刷新。PCB 布线工具中布线经常走 45 度直线加圆弧拐角DRC 检查时要把线段之间、线段与圆弧焊盘之间的距离算清楚求交是距离判定的前置步骤。激光切割路径优化中切割头要沿着“直线–圆弧”连续运动切割头是否会在接刀点偏移、是否会在圆弧端点处过烧都要先精确求出接刀点位置。二维游戏物理引擎里圆形碰撞体与射线检测相交时本质就是射线一条直线和圆求交再加上“只取圆上某一段弧”的限制条件而已。这些场景听起来五花八门数学内核却完全相同。正因为同一份数学可以反复用才值得提炼一个通用公式而不是每个项目都重新推导一遍。2. 通用公式怎么推参数方程比代数消元干净得多2.1 两种直线表达方式的对比求直线与圆弧交点核心是选好直线的表达方式。常见选项有两类。第一类是显式/一般式表达比如 (y kx b) 或 (Ax By C 0)。这类表达在纸面推导时顺手但代码里处理竖直直线、水平直线、点斜退化都要特判。更麻烦的是解出的 x、y 坐标要拿去判断“是否在线段范围内”还要再做一次投影距离比较。第二类是参数式表达直线上任意一点 (P(t) P_0 t \cdot \vec{d})其中 (P_0) 是直线上某个参考点(\vec{d}) 是方向向量(t) 是一个实数。当 (t \in [0, 1]) 时P(t) 正好落在以 P0 和 P1 为端点的线段上(t 0) 或 (t 1) 则对应延长线。我在工程代码里几乎只用参数式理由是它天然统一了无限直线、线段、射线三种对象区间判断就是比较 t 的范围方向向量 (\vec{d}) 即使某个分量是 0竖直线、水平线也不会产生无穷大斜率代入圆方程后所有计算直接沿着向量运算走语义清楚出错了也好排查。2.2 代入与展开一元二次方程的三次分支假设圆弧所在圆的圆心是 (C)半径是 (r)直线 (P(t) P_0 t \cdot \vec{d})。圆上的点满足 (|P - C|^2 r^2)。把参数式代进去[ |P_0 t\vec{d} - C|^2 r^2 ]为了方便令 (\vec{f} P_0 - C)也就是从圆心指向直线起始点的向量。展开向量的平方[ |\vec{d}|^2 t^2 2 (\vec{d} \cdot \vec{f}) t |\vec{f}|^2 - r^2 0 ]设[ a \vec{d} \cdot \vec{d} |\vec{d}|^2 ] [ b 2 (\vec{d} \cdot \vec{f}) ] [ c \vec{f} \cdot \vec{f} - r^2 ]就得到一个标准的一元二次方程 (a t^2 b t c 0)。解为[ t \frac{-b \pm \sqrt{b^2 - 4ac}}{2a} ]这里有必要把三种情况掰开讲清楚因为它们对应着不同的几何含义(b^2 - 4ac 0)直线与圆有两个交点一个“进入”圆内一个“离开”圆。(b^2 - 4ac 0)直线与圆相切只有一个交点。(b^2 - 4ac 0)直线与圆无交点直线整体在圆外或圆与直线错开。工程代码里我会用判别式的值与一个极小阈值比如 (10^{-9}) 量级比较而不是和 0 做精确比较。因为浮点计算里本应相切的情形算出来经常是 (-10^{-16}) 这种负数。这一点后面在代码部分再说。2.3 从直线交点筛选到圆弧区间解出 t 之后交点坐标 (P(t) P_0 t\vec{d}) 是“无限直线与整圆”的交点。要得到“线段与圆弧”的交点需要过两道关卡。第一道关判断 t 是否落在线段范围内即 (t \in [0, 1])。这是参数式带来的天然便利——你根本不需要用坐标去比较 x、y 的范围直接比较一个标量 t 就完成了。第二道关判断交点是否落在圆弧区间内。圆弧不像整圆它有个起始角 (\alpha) 和终止角 (\beta)还可能有方向。把交点坐标换成极坐标角度 (\theta \text{atan2}(y - C_y, x - C_x))然后判断 (\theta) 是否在从 (\alpha) 到 (\beta) 的扫掠范围内。这道关卡最值得小心因为角度的“范围”天然带周期性和方向性。比如一段从 (-170^\circ) 到 (170^\circ) 的逆时针弧它跨越了 (180^\circ) 这条分界线而一段从 (350^\circ) 到 (10^\circ) 的弧又跨越了 0 度线。如果代码里简单写成 (\alpha \le \theta \le \beta)大概率会在跨 0 度线或跨 180 度线的弧上翻车。工程上更稳的做法不是直接比较每个角度的大小而是计算“从起始角到交点角度的有向旋转量”再跟“从起始角到终止角的有向旋转量”做比较。只要旋转方向一致都按逆时针或都按顺时针差值落在 ([0, 总张角]) 区间内交点就在弧上。这个方法完全绕开了跨 0 度线的特判。3. 把公式写成能上生产的代码3.1 函数签名与基本数据结构我自己写这类几何库时习惯先把数据结构定清楚。圆弧的基本描述是圆心坐标、半径、起始角、终止角、方向标志。方向尤其关键默认约定逆时针为正但很多外部格式会传顺时针所以函数里必须留一个方向参数不能写死。下面这个 Python 函数实现了完整的圆弧直线求交。代码以可读性优先工程上可以直接套用或者改写成 C、TypeScript 等语言。import math def intersect_line_arc( cx, cy, radius, start_angle, end_angle, p0, p1, ccwTrue, eps1e-9 ): 求线段 p0-p1 与圆弧的交点。 参数含义 cx, cy圆心坐标 radius圆弧半径 start_angle / end_angle弧的起始角 / 终止角弧度制 p0, p1线段两个端点 ccwTrue 表示逆时针画弧False 表示顺时针 eps数值容差 返回交点坐标列表每个元素为 (x, y) # 方向向量 vx p1[0] - p0[0] vy p1[1] - p0[1] # 从圆心到直线起点的向量 fx p0[0] - cx fy p0[1] - cy a vx * vx vy * vy if a eps: # 线段退化为一个点只要这个点在圆弧上就返回它 if abs(math.hypot(fx, fy) - radius) eps: return [(p0[0], p0[1])] return [] b 2.0 * (vx * fx vy * fy) c fx * fx fy * fy - radius * radius disc b * b - 4.0 * a * c if disc -eps: return [] if disc 0: disc 0.0 sqrt_disc math.sqrt(disc) t1 (-b - sqrt_disc) / (2.0 * a) t2 (-b sqrt_disc) / (2.0 * a) # 方向因子逆时针为 1顺时针为 -1 direction 1.0 if ccw else -1.0 span ((end_angle - start_angle) * direction) % (2.0 * math.pi) def in_arc(x, y): ang math.atan2(y - cy, x - cx) rel ((ang - start_angle) * direction) % (2.0 * math.pi) return rel span eps result [] for t in (t1, t2): if -eps t 1.0 eps: x p0[0] t * vx y p0[1] t * vy if in_arc(x, y): result.append((x, y)) # 去重避免相切时两个根几乎相同导致重复交点 dedup [] for pt in result: if not any(math.hypot(pt[0] - q[0], pt[1] - q[1]) eps for q in dedup): dedup.append(pt) return dedup这个函数有几个设计细节对应前面提到的坑。第一(t) 的区间判断用了 (-\text{eps}) 和 (1\text{eps})而不是精确的 0 和 1就是为了容忍交点恰好在端点时浮点误差带来的偏移。第二角度判断用“相对旋转量”而不是直接比较角度大小跨零度线、跨 180 度线的弧都能正确处理。第三最后去重了一下相切情况下两个根会靠得极近直接返回两个几乎一模一样的坐标会让调用方在做交点计数时出错。3.2 角度归属判断的正确姿势很多人在角度归属判断上摔过跤。如果写成 (\theta) 在 start 和 end 之间那圆弧从 350 度到 10 度逆时针这种合法弧段就会被误判成只有 20 度而不是 350 度交点明明在弧上也给丢了。我用的是“相对旋转量”方案。核心思想是与其问 (\theta) 这个绝对角度在哪里不如问从起始角出发按圆弧方向旋转了多少角度才到达交点。这个旋转量天然落在 ([0, 2\pi)) 区间内跟圆弧的总张角 (\text{span}) 比较逻辑干净得多。Python 的取模运算符对负数返回正余数这正好符合我的需求。但如果语言换成 C 或 Java取模对负数的处理不是这样的你得手动把负角度加回 (2\pi)。这属于跨语言移植时必须注意的差异点。3.3 数值容差与去重策略写几何算法数值容差是躲不开的话题。判别式算出来是负的但很小本应相切交点算出来距离圆心差 (10^{-8}) 但明明应该在圆上线段端点恰在圆弧端点上算出来的 t 是 (0.9999999999)。这些都是浮点计算无法避免的。我通常的做法是设一个相对容差。绝对 eps 设为 (10^{-9}) 只能应对坐标在 1 到 100 量级的情况如果坐标是几千、几万的大坐标这个绝对容差就太紧了。更合理的做法是根据半径或线段长度设置比例容差比如eps max(1e-9, radius * 1e-12)。这篇文章给的代码为了可读性用固定 eps放到工程里时建议改成动态容差。去重策略也很实际。相切情况下判别式等于 0两个根相等用上面的代码会得到两个几乎相同的点。对调用方来说这是“一个切点”还是“两个重合点”会产生歧义。所以我干脆在函数末尾加了一道按距离去重距离小于 eps 的点只保留一个。这样返回的点数就精确表达了几何拓扑关系0 个、1 个或 2 个交点。4. 工程中的边界条件与隐蔽的坑4.1 圆弧方向翻车实录某个模拟项目里我写过一个针对 DXF 文件解析的轮廓检查工具。DXF 标准里 ARC 实体默认是逆时针方向但总有上游 CAD 软件导出的 DXF 里方向标志不标准或者用户手工改过起始角。结果就是算法验算刀具路径是否越过圆弧轮廓时明明有交点却全被过滤掉了因为我的角度判断默认按逆时针而实际弧段是顺时针。排查过程大约花了一下午。先怀疑数学公式把参数方程代入、手算好几组数据发现公式没问题又怀疑浮点容差太小调大之后依旧漏检最后把单个交点坐标打出来才意识到方向标志反了。从那以后我写圆弧类算法第一个问题就是“方向从哪来”。4.2 相切、端点和整圆的特殊约定相切和端点看起来是小概率事件在批量处理场景里却一点都不罕见。特别是设计图纸里的相切连接比如直线与圆弧光滑过渡处几乎每个圆角都会出现。这时候公式给出的两个根会非常接近处理不当会引出两个问题一是重复交点导致后续“交点数量”判断错误二是 t 值在 0 或 1 边缘抖动导致线段端点归属不稳定。端点情况同理。当交点恰好落在线段端点或者圆弧端点上不同业务系统对“算不算相交”的定义不一样。CAD 里可能算作打断点路径规划里可能算作路径连续点游戏碰撞里可能不算碰撞。所以通用公式只负责算出“几何上存在的交点”业务层还必须自己决定“哪些临界点要被接受”。我在写代码注释时会把端点命中的行为写清楚因为这个约定直接影响调用方逻辑。还有一种约定当起始角等于终止角时很多系统把它解释为“整圆”而不是“零长度圆弧”。我的通用函数里如果不处理这种情况整圆会被当作零长度弧所有交点都会被过滤掉。实际工程做法是提前判断整圆条件然后在 in_arc 函数里直接返回 True跳过角度判断。4.3 一组高危边界测试样例写过几何算法的人都知道单元测试的难点不是正常用例而是边界用例。我给自己列了一个固定清单每次改完求交代码都要跑一遍用例期望结果意义水平直线穿过圆心两个交点最基本情形竖直直线穿过圆心两个交点验证无斜率表达直线与圆相切一个交点验证判别式接近 0线段从圆外到圆弧端点终止一个交点t1验证端点容差交点落在圆弧之外整圆上有交点但弧段上无空结果验证弧段筛选圆弧跨越 0 度线直线与弧相交按实际几何验证角度判断圆弧顺时针方向按实际几何验证方向参数交点为两个其中一个恰好被去重一个交点验证去重策略这套用例不复杂但把最容易出的问题全覆盖了。我强烈建议任何要把求交代码用于生产环境的人都维护一份类似的用例清单。5. 常见问题与排查技巧实录5.1 典型问题速查表根据我自己的踩坑记录和帮别人看代码的经验圆弧直线求交的问题集中在下面几类。问题现象可能原因排查方法交点数量总是比预期多没有过滤线段范围或没去重相切点打印 t 值检查 t 是否落在 [0,1]交点全丢但几何上明明有交点角度判断直接比较了绝对角度跨零度线漏判改用相对旋转量方案顺时针圆弧交点全丢方向参数写死为逆时针在函数签名显式传入方向标志圆与直线相切时返回两个点判别式略小于 0 被强制成 0两个根仍然有微小差异判别式接近 0 时只保留一个根再按距离去重交点坐标出现在线段延长线上t 的边界判断条件太宽松收窄容差或改成相对容差部分交点坐标略微偏离圆弧浮点累计误差、坐标系缩放过大调整 eps 为相对容差整圆被当成长度为 0 的弧未约定 start end 表示整圆增加整圆分支如果对照这张表还定位不了问题我建议先把输入数据可视化。把圆心、半径、起始角、终止角、线段都画到画布上然后分步打印中间变量先看 t 解出来是多少再看角度旋转量 rel 是多少最后看 span 是多少。绝大多数问题在这个过程里会原形毕露。5.2 排错经验先用可视化调参再谈精度有一次某跨平台系统集成时同事反馈圆弧求交偶尔会漏掉其中一个交点。我拿到现场数据后没有立刻看代码而是把发生问题的几何对象画出来结果发现两个交点中的一个正好落在线段端点上而且这个端点同时是圆弧的终止点。业务逻辑规定“端点不重复计为交点”因此需要的是特殊处理而非通用公式修正。这类经验多了之后我养成一个习惯拿到几何 bug先质疑输入数据再质疑中间计算最后才质疑公式本身。因为公式一旦推对并经过边界用例检验出错概率远低于数据姿势不规范。还有调试这种问题时我强烈建议输出带足够精度的文本中间量不要只看简略坐标。差值在 (10^{-7}) 量级时控制台默认的几位小数根本不够用。5.3 聊聊“通用公式”里的“通用”到底指什么文章标题里的“通用公式”实际上不是指一个公式打天下而是指一套参数化、可配置、能覆盖所有常见约束的处理流程。它必然包含三件事参数化表达消除无穷大斜率问题、判别式统一分类三种相交关系、有向角度判断统一处理圆弧方向与跨线问题。这三件事缺一件代码遇到真实输入就会“偶尔失灵”。后面如果继续写这个系列我大概率会顺着交点向外延伸——比如线段与圆弧在交点的切线方向怎么算、切线方向连续性如何影响路径规划、圆弧端点处曲率跳变在微积分里意味着什么。毕竟求交只是起点交点之后的分析才是工程里真正有深度的部分。最后分享一个个人经验我现在写任何几何求交代码第一步永远是列边界条件清单第二步才推公式。清单包括线段退化、整圆、相切、端点重合、跨零度线、方向反转这六项。等清单里的每一条都有了明确处理策略写核心代码其实也就是组装一下的事。这个方法帮我少踩了无数坑你也试试。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑