
做CAD插件的时候遇到一个特别基础的问题用户圈了一段圆弧程序要自动捕捉这段弧的中点坐标。我一开始以为这题白送结果第一版代码就被测试打回来了——用户给过来的弧是优弧我的“中点”落在了劣弧上。后来真正把圆上任意一段弧的中点算明白了才发现这里面的门道比想象中多。如果你也是写图形程序、做机器人圆弧轨迹规划或者还在上学正在被解析几何折磨这篇内容应该能帮你省下不少调试时间。这里不写教科书式的理论推导只讲我实际开发中验证过的几种算法、它们各自的坑以及不同场景下到底该选哪一套。1. 先把问题定义清楚弧中点到底是什么1.1 几何定义弧的中点就是“等分圆心角”先说一个最容易踩的坑很多人拿到题目第一反应是把弧的两个端点坐标做算术平均觉得这样就算出中点了。这个思路对线段成立但对圆弧完全不成立。为什么因为圆弧是弯曲的它在平面上的坐标分布并不均匀。圆上的点由圆心角唯一决定一段弧的弧长s和它对应的圆心角Δθ满足s r·Δθ这个关系。也就是说弧上的点随角度线性变化而不是随坐标线性变化。所谓“弧的中点”本质上是把这段弧对应的圆心角等分成两份中点在角平分线上而不是在两端点连线的中点。举个例子就很直观四分之一圆上取A(1,0)、B(0,1)这段弧在第一象限。如果直接坐标平均得到(0.5,0.5)这个点显然不在圆x²y²1上。真正的弧中点对应的角度是45°坐标是(√2/2, √2/2)约等于(0.7071, 0.7071)。两个点的误差接近30%在图形程序里这种误差会直接导致标注错位、刀轨偏出公差。所以弧中点的严格定义是在该弧上满足两端点到该点的弧长相等、且该点对应的圆心角为该弧圆心角一半的那个点。写程序时所有算法都必须围绕圆心角来做文章绕开这一步必然会出问题。1.2 这题背后真正要解决的是什么从纯数学角度这个题目很单纯给一个圆给一段弧求中点。但放到实际工程里“给出弧”的方式千奇百怪这才是问题真正复杂的地方。我在实际开发中遇到的输入形式主要有三种第一种最常见用户直接给圆心坐标、半径、弧的起始角度和终止角度这是CAD软件内部最自然的存储方式第二种是给圆心坐标、半径、弧上两个端点的坐标并不直接提供角度第三种最麻烦可能还带着一个方向标志——顺时针还是逆时针甚至弧本身是优弧还是劣弧都不说明。不同输入形式决定了该用哪套算法。比如输入是端点坐标时如果你强行先转换成角度再用角度平均就会引入atan2和坐标转换这一步边界情况随之而来反过来如果你用向量法根本不需要把角度算出来就能直接拿到方向代码会简洁很多。这也是为什么我强烈建议你先把输入形式想清楚再选方案。另外还要区分一个概念纯几何的“劣弧中点”和工程里“指定弧段的中点”。前者只有一种答案后者取决于你选的弧段是大于半圆的那半还是小于半圆的那半。同一个圆上从A到B的劣弧中点和优弧中点是两个点它们关于圆心对称。这个问题我在第4节专门展开。2. 三种主流算法从原理到代码2.1 角度取平均参数方程法这是最直接、最符合几何直觉的做法。既然弧长与圆心角线性相关弧中点对应的角度就是起始角和终止角的算术平均。已知圆心O(x0, y0)、半径r、起始角α、终止角β圆上任意角度θ的坐标由圆的参数方程给出 x x0 r·cosθ y y0 r·sinθ那么弧中点坐标就是 θ_mid (α β) / 2 midX x0 r·cos(θ_mid) midY y0 r·sin(θ_mid)看上去简单到不需要解释但我实际写代码时发现一个隐藏问题如果α和β跨过0°边界比如从350°到10°直接取平均得到180°而实际的中点应该是在0°附近。这个坑我第4节会专门讲典型解法是把角度先归一化再取平均或者用向量相加的方式来规避。这类算法的定位非常明确适用于你已经明确知道起止角度、且能保证角度顺序的场景。比如数控机床的圆弧插补指令通常直接给出起止点对应的角度用这个方法最直接。优点是计算量极小一次cos、一次sin就完事缺点是把角度边界问题甩给了你自己你需要额外处理角度跨越和优弧劣弧判断否则会静默出错——不是崩溃而是给你一个看似合理但完全错误的坐标。2.2 向量法不需要算角度绕开所有边界问题我在实际项目里最终长期用的是这一套说实话它比角度平均更“结实”也更优雅。已知圆心O和两个端点A、B思路是从圆心指向端点的两个向量方向就是弧的两个端点方向。我们想找的是角平分线方向而两个单位向量相加的结果恰好指向角平分线方向——这是向量加法最基本的平行四边形法则。具体步骤如下求向量OA A - O向量OB B - O分别归一化得到单位向量u_a OA / |OA|u_b OB / |OB|求和u_sum u_a u_b把u_sum归一化得到单位方向向量u_mid弧中点 O r · u_mid为什么这个解法能成立因为u_a和u_b是单位向量长度都是1它们的和向量的方向正好位于两个原向量的角平分线方向。这就像两个人从同一起点出发各走1米后连线的方向正好位于两人方向的中间。无论夹角是30°还是170°这个结论都成立而且它根本不需要你调用atan2自然也就没有角度跨零、负角转换这类麻烦。但这个方法有一个致命边界当A、B恰好在直径两端时u_a和u_b方向相反相加得到零向量无法归一化。这种情况对应半圆弧中点有两个上半圆弧和下半圆弧的中点需要额外信息才能确定选哪个。所以我在代码里通常会判断u_sum的模长如果接近0就回退到角度法并提示输入不完整。2.3 垂直平分线法解析几何思路适合手算和教学前两种都是偏“程序思维”的做法这一套更接近高中解析几何的思路适合手算时用。考虑弧中点C它有个天然性质C到弧的两个端点A、B的距离相等。为什么因为C是弧的中点弧AC和弧CB相等都等于整段弧的一半等弧对等弦所以弦AC和CB长度相等。这说明C在AB的垂直平分线上。于是解法变成写出AB垂直平分线方程。设A(x1, y1)B(x2, y2)中点为M((x1x2)/2, (y1y2)/2)垂直平分线方向向量是(A-B)的法向量方程为(x - Mx)(x2-x1) (y - My)(y2-y1) 0联立这个直线方程与圆的方程(x-x0)² (y-y0)² r²解出两个交点这两个点分别是整圆被AB分成的两条弧劣弧和优弧的中点根据你需要的是哪一段弧选出正确的一个点第4步的判断需要额外的逻辑如果你要的是劣弧中点就选圆心和这个点连线的方向与OA、OB的夹角都在90°以内的那个交点如果要优弧中点就选另一个。实际判断时用点积即可设p为候选交点若向量OP与OA的点积以及OP与OB的点积都大于0说明OP方向位于OA和OB的夹角范围劣弧一侧内它就是劣弧中点。这个方法在工程代码里不常用因为要解二次方程涉及求根和判断分支效率低而且同样有优弧劣弧的判断问题。但如果你是拿纸笔在算题这个方法最见功夫也能加深对“等弧对等弦”这一几何性质的理解。3. 从公式到程序完整实现与验证3.1 Python实现两个核心函数我实际开发时会把两个方案都封装成函数方便不同输入形式直接调用。先给Python版本。第一个函数处理“已知起止角度”的情形import math def arc_midpoint_by_angles(cx, cy, r, start_angle, end_angle): 参数方程法已知圆心、半径、弧起止角弧度制 计算弧中点坐标 mid_angle (start_angle end_angle) / 2.0 mid_x cx r * math.cos(mid_angle) mid_y cy r * math.sin(mid_angle) return (mid_x, mid_y)这段代码逻辑是最短的但如果直接放进实际项目里会在角度跨零时出错所以更稳的写法得先把两个角度调整到合适范围。比如def arc_midpoint_by_angles_safe(cx, cy, r, start_angle, end_angle): # 确保 end_angle 在 [start_angle, start_angle 2pi) 范围内 delta end_angle - start_angle delta math.fmod(delta, 2 * math.pi) if delta 0: delta 2 * math.pi mid_angle start_angle delta / 2.0 mid_x cx r * math.cos(mid_angle) mid_y cy r * math.sin(mid_angle) return (mid_x, mid_y)这里的思路是不直接对两个角度取算术平均而是先算出从start到end的有向角度差delta归一到[0, 2π)区间然后从起点角度往前走一半delta。这样即使start6.11弧度约350°、end0.17弧度约10°delta归一化后是0.35弧度约20°中点就是start0.17弧度得到的坐标正好指向0°方向附近而不是错到π附近。这个写法处理跨零问题非常干净。第二个函数处理“已知端点坐标”的情形用向量法def arc_midpoint_by_points(cx, cy, r, ax, ay, bx, by): 向量法已知圆心、半径、弧的两个端点坐标 返回劣弧中点若为半圆则返回 None # 从圆心指向两端点的向量 ox_a ax - cx oy_a ay - cy ox_b bx - cx oy_b by - cy # 归一化 len_a math.hypot(ox_a, oy_a) len_b math.hypot(ox_b, oy_b) if len_a 0 or len_b 0: raise ValueError(端点不能与圆心重合) ua_x ox_a / len_a ua_y oy_a / len_a ub_x ox_b / len_b ub_y oy_b / len_b # 单位向量相加 - 角平分线方向 sum_x ua_x ub_x sum_y ua_y ub_y len_sum math.hypot(sum_x, sum_y) # 半圆情况向量相反和为零向量 if len_sum 1e-12: return None mid_x cx r * (sum_x / len_sum) mid_y cy r * (sum_y / len_sum) return (mid_x, mid_y)这个函数返回的是劣弧中点。你可能注意到我并没有显式地传“这段弧是哪个弧”默认处理的是两点之间小于半圆的那段。如果需要求优弧中点只需要把结果坐标关于圆心对称一下即可优弧中点 (2cx - mid_x, 2cy - mid_y)。这就是同一个方程两个根的几何意义。3.2 JavaScript实现前端和游戏开发版本Web端和游戏开发里经常遇到同样的需求我也提供JavaScript版本。和Python版本思路完全一致只是语法不同。function arcMidpointByAngles(cx, cy, r, startAngle, endAngle) { // 角度差归一到 [0, 2PI) let delta endAngle - startAngle; delta delta % (2 * Math.PI); if (delta 0) delta 2 * Math.PI; const midAngle startAngle delta / 2; return { x: cx r * Math.cos(midAngle), y: cy r * Math.sin(midAngle) }; } function arcMidpointByPoints(cx, cy, r, ax, ay, bx, by) { let uax (ax - cx) / r, uay (ay - cy) / r; let ubx (bx - cx) / r, uby (by - cy) / r; let sx uax ubx, sy uay uby; let len Math.hypot(sx, sy); if (len 1e-12) return null; return { x: cx r * (sx / len), y: cy r * (sy / len) }; }在使用向量法时如果你能确保A、B两点确实都在圆上到圆心距离等于r那么第一版的归一化可以直接除以r省去hypot计算。但实际工程中因为浮点数误差端点未必严格在圆上所以稳妥起见还是算出实际长度再归一化动一次hypot的成本几乎可以忽略。有个容易忽略的细节屏幕坐标系里y轴是向下的所以angle从0到π/2描出的弧不是“左上”而是“左下”。这在数学坐标系里没问题但如果你把数学坐标系的角度直接塞给Canvas API会得到镜像的弧。解决方法是明确你的坐标系约定Canvas中角度方向反转绘制时需要对y取反或对角度取负。代码本身不关心坐标系但验证取点时一定要拿着坐标在画布上打点检查否则方向反了你都发现不了。3.3 怎么验证算出来的点真的是弧中点我在开发中吃过亏所以每次实现完算法都会做一个验证步骤。用回第一象限四分之一圆的例子# 圆心(0,0)半径1A(1,0)B(0,1) mid arc_midpoint_by_points(0, 0, 1, 1, 0, 0, 1) print(mid) # (0.7071067811865476, 0.7071067811865475)预期结果是(√2/2, √2/2)计算值和预期一致。如果你算出来的点和预期不符先检查角度的弧度/度数有没有混用。弧度制下180°是π≈3.14159cos(90°)应该传math.pi/2而不是90——这个错误我见过太多人犯包括当年的我自己。还有一个自检公式弧中点C到两端点A、B的距离应当相等而且这个距离等于2r·sin(Δθ/4)。你可以把它写进断言里作为自动化测试的一部分。如果验证失败说明算法或输入数据有问题早点暴露比上线后炸要好得多。4. 常见问题与排查技巧实录4.1 优弧和劣弧同一个角度公式的两副面孔这个问题值得重点说。题目里写“圆上某一段弧”往往没有告诉你这段弧是优弧还是劣弧。而你用角度平均法算出来的那个中点数学上默认是劣弧的中点。看个具体例子。圆心(0,0)半径1A(1,0)B(0,1)。这两点之间的圆弧有两段第一象限的90°劣弧中点是(0.707, 0.707)剩下那段270°优弧中点是(-0.707, -0.707)正好在圆心的另一侧。优弧中点的求法很简单把劣弧中点坐标关于圆心对称。数学上就是加π θ_优 θ_劣 π因为优弧对应圆心角大于π它的中点必然位于劣弧中点的对径位置。这个关系在向量法里同样成立得到劣弧中点向量v后优弧中点向量就是-v。实际工程里最稳妥的方式不要把“优弧/劣弧”作为隐含假设一定要从输入数据结构里找方向信息。大多数图形库比如SVG的arc指令、CAD的Arc对象都会明确告诉你是顺时针还是逆时针、是大弧还是小弧。把方向标志传给你的函数再配合一个参数选优弧还是劣弧才能保证不会出错。4.2 半圆情况向量法直接失效圆上取两点把这两点连起来通过圆心意味着这段弧正好是半圆。这时候A和B是直径的两个端点单位向量u_a和u_b互为相反数相加等于零向量向量法没法算出角平分线方向。这个在数学上也说得通——直径端点的角平分线有两条恰好对应上半圆和下半圆不附带额外条件根本没法选。我的实际处理策略是在向量法里检测到模长小于某个阈值比如1e-12时返回一个特殊标志让上层逻辑去决定取哪个点。如果需要自动选择可以约定取“逆时针方向从A到B经过的那半弧的中点”做法是计算有向角度并把弧中点定为旋转90°方向上的点——从OA方向逆时针转90°或顺时针转90°根据弧的方向选一个。这个约定在CAD里通常是默认的但在通用程序里最好显式暴露给调用者。4.3 角度跨零为什么平均180°是错的前面提到过角度平均法最大的坑是跨零边界。再具体一点假设圆心(0,0)半径1起始角为350°6.11弧度终止角为10°0.17弧度这段弧只有20°跨度非常小。如果直接平均 (6.11 0.17) / 2 3.14约180°这个结果指向了圆上完全相反的位置。正确的中点角度应该是0°方向。这就是为什么我在3.1节提供的安全版函数不直接做平均而是先计算有向角度差并归一化。数学上你永远应该问自己“描述这段弧的有向角度差是多少”而不是“两个角度数的平均值是多少”——Angular Difference和Arithmetic Mean是两回事。顺手提醒一个和atan2相关的坑atan2(y, x)返回值的范围是(-π, π]当你从atan2得到-2.94约-168°又用atan2得到1.74约100°你可能会以为这两角差268°但实际上差值是92°。要正确计算角度差应该让delta atan2(sin(β-α), cos(β-α))这能天然保证结果落在(-π, π]彻底避开跨±π边界的烦恼。4.4 浮点数精度能不能用怎么验证图形程序里有个常见的初级错误端点坐标是经过多少次旋转、平移之后得到的实际不在理想的圆上。比如A点到圆心的距离是0.9999999B点是1.0000001用半径r直接算归一化向量时方向会带一点误差但通常不影响最终结果。真正要注意的是比较坐标时不能用精确相等要用容差。我在测试代码里用的比较方式是这样的def nearly_equal(a, b, eps1e-9): return abs(a - b) eps另外向量加法在这种场景下的数值稳定性优于三角函数法。原因很简单三角函数在参数接近0或π时可能出现较大舍入误差而向量加法和归一化主要用到乘法和开方没有周期性边界误差更可控。尤其是在嵌入式设备或者游戏引擎中内存和CPU有限能少调一次sin/cos就少调一次。如果数据是单精度浮点数误差会更大判断半圆和端点重合时阈值需要适当放宽比如1e-7。5. 三种算法横向对比与选型建议5.1 一张表看穿三种方法的适用边界实现过一轮之后我把三种算法的适用条件、优缺点整理成了一张表在团队内部讨论方案时会直接拿出来对照算法适用输入核心操作边界风险程序健壮性手算友好度参数方程法圆心半径起止角角度平均cos/sin角度跨零、优弧劣弧需要额外防护中等向量法圆心半径端点坐标归一化向量相加半圆失效高天然避开角度边界较难垂直平分线法圆心半径端点坐标解直线与圆的方程要先判断弧度一般需解二次方程最直观参数方程法的优势是代码最短、执行最快但防御逻辑都写在外围向量法代码稍微多两行却把最难缠的角度边界问题直接消灭掉了解析法手算最友好编程最不友好——因为要解二次方程并筛选根麻烦且意义不大。5.2 实际项目里我到底怎么选做CAD插件时我拿到的是端点坐标和半径最自然的选择是向量法。做数控机床的圆弧插补解析器时指令格式里明确有起止角度那就用安全版参数方程法配合方向标志决定取优弧还是劣弧。做Web前端图表的时候数据源通常给的是三个点坐标圆心、起始点、终止点我依然用向量法因为它在代码库里结构最统一出错概率最低。学生解题或者面试手撕这类算法的时候推荐优先掌握垂直平分线法因为最能体现你对几何本质的理解弧中点到两端点距离相等、在垂直平分线上。答题的时候把参数方程法作为验证手段两者结合基本不会失分。还有个工程细节如果API需要返回多个候选值比如同时返回劣弧中点和优弧中点我建议接口设计成返回两个坐标或返回一个坐标加一个标志位而不是隐含默认选劣弧。很多问题都是“差一个flag”的事但就是差了这个flag后续接手的同事才会在一堆看似莫名的bug里挣扎。最后分享一个我自己的体会最早实现这个功能时我用的是角度平均法结果总被各种角度跨零、方向反转的问题折腾。后来重构成向量法代码短了三分之一边界问题几乎全部消失。现在如果有人来问我我会说只要输入给的是点位坐标优先用向量法只有在输入本身就明确给了角度、且你能保证角度范围约定的前提下才直接用参数方程法。如果你未来要处理复杂的弧段解析比如CAD中的带方向圆弧、路径规划中的多弧段拼接也建议从向量法的思路出发去扩展——它才是那个真正抗折腾的骨架。