做CAD二次开发这行最容易被新手高估的就是“画一条圆弧”。很多教程一上来就是new Arc(center, radius, startAngle, endAngle)看着清爽实际项目里一堆角度、走向、坐标系的坑等着你。这篇文章是“C#/CAD二次开发创建圆弧”系列的第四篇不聊Hello World聊实际写代码时躲不开的几种典型场景、背后原理和我的踩坑记录适合已经能写出基本命令、想往深水区走的开发者也适合第一次用Arc类但不想被角度绕晕的初学者。1. 先盘点创建一条圆弧到底有几种姿势1.1 最常规圆心 半径 起止角Arc类最常用的构造函数是这组Arc arc new Arc(centerPoint, radius, startAngle, endAngle);四个参数看起来一目了然但坑不在参数个数而在角度的意义。startAngle和endAngle不是“弧上某一点的切线与X轴的夹角”而是圆心指向该点的向量在WCS世界坐标系XY平面上的方位角。也就是说你传入的其实是弧的两个端点在极坐标下的角位置。举例来说如果我想画一个圆心在(0,0)、半径100、从12点钟方向到3点钟方向的四分之一圆弧直接写new Arc(Point3d.Origin, 100, Math.PI / 2, 0)是错的——因为弧终止角0对应的是3点钟方向起始角Math.PI/2对应12点钟方向这样确实是自己想要的四分之一弧。但如果你写new Arc(Point3d.Origin, 100, 0, Math.PI / 2)得到的是一条从3点钟到12点钟方向、逆时针走过的圆弧。二者弧长一致但走向和端点完全不一样。这个对初学者来说非常反直觉很多人在这里栽了一次换了个角度又栽一次。还有一个坑这个构造函数默认的Normal是(0,0,1)也就是说弧只能画在WCS的XY平面上。如果当前UCS被旋转过你直接把这个弧加进模型空间它依然老老实实呆在WCS的XY平面而不是UCS的平面。想让它跟着UCS走你得自己转换点坐标或者用带Normal参数的构造函数。1.2 三点画弧其实是最直观但最被忽略的Arc类里藏着另一个构造函数很多人用了几年都没注意Arc arc new Arc(startPoint, secondPoint, endPoint);这是最符合人类直觉的画法——给三个点让CAD自动算出过这三个点的圆弧。这个构造函数的内部实现是先判断三点是否共线共线则抛出异常再算出圆心和半径然后根据中间点的位置确定弧的走向保证弧从startPoint出发、经过secondPoint、到达endPoint。我在实际项目里用它处理“根据测量点画弧”的场景特别顺手。比如现场测绘给了三个控制点的坐标想在地图上画出经过这三点的规划弧线用这个构造函数一行搞定。但有一点要知道它内部对“共线”的判定有容差不是严格的数学共线。如果你给定的三个点非常接近一条直线它可能不报错而是给你一个半径大得离谱的弧肉眼看似平直。这种“假平直”弧在工程图上很容易骗过眼睛炸开后才发现是个半径几公里的圆弧所以用完一定要检查一下arc.Radius的合理性。1.3 起点 圆心 端点工程语境下的首选在机械制图和管线设计里最常用的其实不是三点而是“已知起点、圆心、终点”的输入方式。Arc类没有直接对应的构造函数但有等价写法Vector3d startVec startPoint - centerPoint; Vector3d endVec endPoint - centerPoint; double startAngle angleFromVector(startVec); double endAngle angleFromVector(endVec); Arc arc new Arc(centerPoint, radius, startAngle, endAngle);这里的angleFromVector就是Math.Atan2(vector.Y, vector.X)。为什么我偏爱这种方式因为工程图纸上常见的标注方式是“圆弧圆心在这里从这里到那里”而不是“起始角多少度”。你需要把人的直觉翻译成Arc类的参数翻译过程就靠Atan2。一个极其容易犯的错误你以为的“圆心”未必真的是圆弧的几何圆心。轴类零件上的R角、管线弯头的外弧标注的半径点往往是一个虚拟中心但它所在平面和两条相切边的关系才是关键。所以写代码时不要贪快把起点、圆心、终点这三个点先画成临时辅助线用眼睛确认一遍再进算法省得后面飞机稿。2. 角度换算问题为什么你的圆弧总是“神预测”2.1 弧度制的反直觉本质数学课教的是“弧度弧长/半径”但AutoCAD API里游戏规则不太一样所有角度都是弧度且都是从WCS的X轴正方向逆时针量取的。麻烦在于用户画图习惯说“30度”“120度”程序里却一律要转成弧度。我见过太多同行直接写Math.PI / 3然后被0.333风格的精度问题搞到头大。好消息是CAD的API内部对角度做了连续化处理startAngle和endAngle可以随便传负值或大于2π的值它会自动规范化。比如你想画一条从-45度到45度的弧写startAngle -Math.PI / 4, endAngle Math.PI / 4完全没问题。这给了我们很大的自由度绕开“起始角大于终止角时该怎么处理”的纠结。真正要命的是用户输入。CAD的Editor.GetAngle方法返回的是当前UCS下的角度值仍然以弧度计但这个角度是相对于当前UCS的X轴方向。如果你的UCS被旋转这个角度在WCS里就是错位的。不处理的后果就是在标准俯视图下画的弧一切正常一旦用户把UCS转了45度你再执行命令弧就跑到一个莫名其妙的位置。2.2 一套通吃的角度转换工具函数我自己长期维护一套角度工具每次新建项目都直接拷过去public static class AngleHelper { private const double Deg2Rad Math.PI / 180.0; private const double Rad2Deg 180.0 / Math.PI; // 度转弧度 public static double DegreeToRadian(double degree) degree * Deg2Rad; // 弧度转度 public static double RadianToDegree(double radian) radian * Rad2Deg; // 将任意角规范到 [0, 2π) public static double NormalizeAngle(double angle) { double twoPi 2 * Math.PI; angle angle % twoPi; if (angle 0) angle twoPi; return angle; } // 由向量求方位角弧度范围 (-π, π] public static double AngleFromVector(Point3d vector) { return Math.Atan2(vector.Y, vector.X); } }这套函数看着简单但每次都能救命。特别是NormalizeAngle处理“用户输入的结束角小于起始角”的场景时非常好用。举一个例子用户想画一条超过180度的弧从10度顺时针绕到-160度你的“起角”是10度“终角”原本是350度因为-160度规范化后等于200度不对-160度规范化后是200度。NormalizeAngle能让你确信无论输入多离谱最终拿到的都是可控的[0, 2π)范围。2.3 实战中常用的角度对照表我自己在随手查时经常用这张表贴出来供参考。这里我直接给出度数和对应的弧度值方便你往代码里填度数弧度精确到6位小数0°015°0.26179930°0.52359945°0.78539860°1.04719890°1.570796120°2.094395180°3.141593270°4.712389360°6.283185记忆口诀只有一句“180度就是π剩下按比例推。”真要在代码里用别靠记忆直接DegreeToRadian(60)既不容易错也方便后面统一改精度。3. 三点画弧的内在原理与兜底方案3.1 从三点到圆心半径的数学推导三点定圆是初中几何内容但工程代码里要做得严谨得把算法掰开揉碎。已知不共线三点P1(x1,y1)、P2(x2,y2)、P3(x3,y3)求圆心O(cx,cy)。思路是圆心到三点的距离相等所以圆心必然在P1P2和P2P3两条弦的中垂线上。先算P1P2中点M12 ((x1x2)/2, (y1y2)/2)P1P2的方向向量是(x2-x1, y2-y1)所以垂直于它的中垂线方向是(-(y2-y1), x2-x1)或者(y2-y1, -(x2-x1))。中垂线方程可以用点法式来表示(x - M12x) * (x2 - x1) (y - M12y) * (y2 - y1) 0同理对P2P3写第二条方程。解这个二元一次方程组就能得到cx和cy。求半径就简单了r sqrt((x1 - cx)^2 (y1 - cy)^2)起始角就是向量(P1-O)的方位角终止角是向量(P3-O)的方位角。如果想让弧经过P2而不是走背弓还需要判断一下方向计算(P2-O)的角位置是否落在起点角到终点角中间如果不在就交换起止角或者翻转Normal方向。3.2 C#实现从三点到Arc对象我不建议去调用那些“黑盒”的第三方几何库自己实现一版最稳妥也好调试。下面是我经常用的实现public static Arc CreateArcFromThreePoints(Point3d p1, Point3d p2, Point3d p3) { // 为了避免Z轴干扰这里强制投影到WCS的XY平面 Point2d a new Point2d(p1.X, p1.Y); Point2d b new Point2d(p2.X, p2.Y); Point2d c new Point2d(p3.X, p3.Y); double d 2 * (a.X * (b.Y - c.Y) b.X * (c.Y - a.Y) c.X * (a.Y - b.Y)); if (Math.Abs(d) 1e-9) throw new InvalidOperationException(三点接近共线无法创建圆弧); double a2 a.X * a.X a.Y * a.Y; double b2 b.X * b.X b.Y * b.Y; double c2 c.X * c.X c.Y * c.Y; double ux (a2 * (b.Y - c.Y) b2 * (c.Y - a.Y) c2 * (a.Y - b.Y)) / d; double uy (a2 * (c.X - b.X) b2 * (a.X - c.X) c2 * (b.X - a.X)) / d; Point3d center new Point3d(ux, uy, p1.Z); double radius center.DistanceTo(p1); double startAngle AngleHelper.AngleFromVector(p1 - center); double endAngle AngleHelper.AngleFromVector(p3 - center); // 判断中间点P2是否落在弧路径上不满足就修正方向 double midAngle AngleHelper.AngleFromVector(p2 - center); if (!IsAngleBetween(midAngle, startAngle, endAngle)) { // 翻转弧的走向把法向量反过来或采用补角方式处理 // 这里采用简单方式构造法向量反向的弧 Vector3d normal new Vector3d(0, 0, -1); return new Arc(center, normal, radius, startAngle, endAngle); } return new Arc(center, radius, startAngle, endAngle); } private static bool IsAngleBetween(double angle, double start, double end) { double span end - start; if (span 0) span 2 * Math.PI; double diff angle - start; if (diff 0) diff 2 * Math.PI; return diff span; }这段代码有两个关键点一是采用行列式直接求圆心避免了叉乘退化问题二是方向判断我故意翻转Normal而不是交换参数因为这样能保证弧从p1到p3经过p2而不是从p1反向绕到p3。实际上Arc类的三点构造函数内部也是这个逻辑但自己实现一遍排查问题时心里有底得多。3.3 兜底处理共线与重合的特殊场景三点画弧最典型的崩溃场景就是三点接近共线。工程图中从测绘点画弧时这种情况经常出现。我的兜底方案是先算三角形面积即上面代码里的d/2如果面积太小就改用两点的圆弧近似——比如取两端点之间用一条半径极大的弧拟合或者干脆提示用户换一个中间点。另外一个容易被忽略的分支三个点中任意两个重合。d可能不为0但生成的圆弧半径可能极小或者极大肉眼完全无法识别。我的习惯是在调用前检查两点之间的最小距离小于1e-6就直接return不给后面添堵。这类边界情况多写一行判断就能少一次凌晨加班的排查。4. 走向、法向量与坐标系高级用户才关心的方向问题4.1 逆时针的默认走向与判定技巧Arc对象默认沿逆时针方向从startAngle对应的点画到endAngle对应的点。这个“逆时针”不是数学教材里XY平面上的逆时针而是相对于Normal向量的“从法向量的正方向看过去逆时针方向”。也就是说如果在WCS下看Normal是(0,0,1)从俯视图Z轴正方向往下看弧是逆时针从起点走向终点。很多开发者在这里有个误解以为StartAngle和EndAngle是两个端点之间比较小的那段弧。其实当你传startAngle 0, endAngle 1.5 * Math.PI时得到的是270度的大弧而不是90度的小弧。推算方向时请一定要记住从startAngle按逆时针转到endAngle转过的角度就是弧的实际角度。如果想画顺时针弧有两个办法一是让终点角小于起点角二是不改变角度但把Normal从(0,0,1)改成(0,0,-1)。我强烈推荐第二个因为第一个把数值大小和方向混在一起逻辑上一不小心就乱了而法向量是显式表达“我就是要画个反向弧”。4.2 在UCS中画弧的正确姿势AutoCAD的用户坐标系UCS是另一个方向偏见的来源。如果你直接用Editor.GetPoint取点拿到的点已经经过坐标转换是在WCS下的坐标。但Editor.GetAngle返回的却是UCS下的角度这个不对称性非常坑。举个例子。UCS绕Z轴旋转了30度用户在屏幕上输入一个“0度”角度你的程序拿到的是UCS下的0弧度也就是真实WCS下的30度。如果你直接用这个角度去画弧弧会在WCS方向偏了30度。解法有两种用ed.CurrentUserCoordinateSystem取当前的UCS矩阵将该角度通过矩阵变换到WCS。干脆不用GetAngle让用户直接点两个点确定一个矢量然后用AngleFromVector算角度这样得到的角度天然在WCS下省去转换。实际项目中我基本放弃GetAngle统一用“选点定角”的方式可靠性高很多代码也少一个转换层。4.3 用Normal向量实现空间圆弧在三维空间焊管、钢结构节点这些场景里圆弧往往不在XY平面上。此时必须使用带Normal参数的构造函数Arc arc new Arc(center, normal, radius, startAngle, endAngle);这里center是圆心normal是圆弧所在平面的法向量startAngle和endAngle仍然是圆心指向端点的向量在平面内的极角。角度0方向被定义为“在圆弧平面上与某个参考方向所成的角”实际上这个参考方向是normal和世界Z轴叉乘得到的方向。理解这个规则最简单的方式是把normal想象成一块圆形木板的“法线”角度从木板的默认标记线量起逆时针为正。我自己在三维空间画弧时最稳的一条流程是先搭建一个以圆心为原点、以法向量为Z轴的局部坐标系然后把要画的起终点换算到局部坐标下再用局部坐标直接算角。这样一来不管后来鬼知道发生了什么旋转圆弧都能躺在该躺的平面上。写复杂三维程序时千万不能用“先画一个平面弧再旋转”的土办法一是累二是容易丢精度。5. 实操一个两线倒圆角命令的完整实现5.1 从需求到功能拆解今年我接到一个管线布局辅助工具的小需求用户框选两根相交的直线输入圆角半径程序自动在交点处生成一段圆弧平滑连接两根管线。这就是最经典的倒圆角问题。拆解下来需要四步让用户选择两条直线对象。计算两条直线延伸后的交点。依据半径求圆心和切点。生成圆弧并替换掉原来的尖角部分。不能直接用AutoCAD自带的Fillet命令因为这里管线不是普通直线还要关联一些自定义属性而且在多条管线批量处理时自带的Fillet驱动起来太慢用户体验也不行。自己写命令这些都好控制。5.2 核心代码找交点、算弧、落地直线对象在CAD API里是Line取端点和方向很简单。计算两条直线延伸交点的几何基础是二维直线的参数方程。下面是这个命令最核心的部分[CommandMethod(QuickFilletArc)] public void QuickFilletArc() { Document doc Application.DocumentManager.MdiActiveDocument; Database db doc.Database; Editor ed doc.Editor; // 选择两条直线 PromptEntityOptions opts new PromptEntityOptions(\n选择第一条直线: ); opts.SetRejectMessage(必须选择直线); opts.AddAllowedClass(typeof(Line), false); PromptEntityResult res1 ed.GetEntity(opts); if (res1.Status ! PromptStatus.OK) return; opts.Message \n选择第二条直线: ; PromptEntityResult res2 ed.GetEntity(opts); if (res2.Status ! PromptStatus.OK) return; PromptDoubleOptions radiusOpts new PromptDoubleOptions(\n指定圆角半径: ); radiusOpts.AllowNegative false; mean允许零?这里需要自己再判断一下是否大于0即可 PromptDoubleResult radiusRes ed.GetDouble(radiusOpts); if (radiusRes.Status ! PromptStatus.OK) return; double radius radiusRes.Value; if (radius 0) { ed.WriteMessage(\n半径必须大于0); return; } using (Transaction tr db.TransactionManager.StartTransaction()) { Line l1 tr.GetObject(res1.ObjectId, OpenMode.ForRead) as Line; Line l2 tr.GetObject(res2.ObjectId, OpenMode.ForRead) as Line; if (l1 null || l2 null) return; // 计算交点 Point3d pt1 l1.StartPoint, pt2 l1.EndPoint; Point3d pt3 l2.StartPoint, pt4 l2.EndPoint; // 方向向量 Vector3d v1 pt2 - pt1, v2 pt4 - pt3; v1 v1.GetNormal(); v2 v2.GetNormal(); // 求两线交点不考虑Z轴近似二维处理 double denom v1.X * v2.Y - v1.Y * v2.X; if (Math.Abs(denom) 1e-9) { ed.WriteMessage(\n两条线平行无法倒圆角); return; } // 解参数 t 和 s: pt1 t*v1 pt3 s*v2 double deltaX pt3.X - pt1.X; double deltaY pt3.Y - pt1.Y; double t (deltaX * v2.Y - deltaY * v2.X) / denom; double s (deltaX * v1.Y - deltaY * v1.X) / denom; Point3d intersection pt1 v1 * t; // 夹角计算用于确定圆心位置 double cosAngle v1.DotProduct(v2) / (v1.Length * v2.Length); cosAngle Math.Max(-1.0, Math.Min(1.0, cosAngle)); double angle Math.Acos(cosAngle); double halfAngle angle / 2; double centerDist radius / Math.Sin(halfAngle); Vector3d bisector (v1 v2).GetNormal(); Point3d center intersection bisector * centerDist; // 两个切点 Vector3d toTangent1 v1; Point3d tangent1 center (-toTangent1) * radius; Vector3d toTangent2 -v2; Point3d tangent2 center toTangent2 * radius; // 构造圆弧并添加 Arc arc new Arc(); arc.Center center; arc.Radius radius; arc.StartAngle AngleHelper.AngleFromVector(tangent1 - center); arc.EndAngle AngleHelper.AngleFromVector(tangent2 - center); arc.Normal new Vector3d(0, 0, 1); // 默认在XY平面 BlockTableRecord btr tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) as BlockTableRecord; btr.AppendEntity(arc); tr.AddNewlyCreatedDBObject(arc, true); tr.Commit(); } }注意代码里centerDist radius / Math.Sin(halfAngle)的推导两条直线夹角为angle圆心在角平分线上圆心到两条直线的垂直距离等于半径R所以圆心到交点的距离d R / sin(angle/2)。这个关系式是画“与两直线相切的圆弧”的数学基础请务必记牢。另外切点的位置我只做了简单化处理——假设圆弧和直线的切点落在从圆心向直线作垂线的方向上。在真正的工程倒圆角里还需要判断切点是否落在线段端点之间如果超出说明当前半径太大需要缩小或提示用户。生产代码里必须加上这个判断否则会生成“悬空”的圆弧。5.3 事务与出错回滚的细节处理上面的代码里我用using (Transaction tr db.TransactionManager.StartTransaction())包裹了所有操作。有两个细节是实战总结出来的一是打开对象要指定正确的OpenMode。读取两点、计算交点时不需要写权限OpenMode.ForRead足够不要一上来就ForWrite否则在多文档环境里容易出现“对象已被另一个事务锁定”的报错。二是tr.Commit()之前如果发生异常一定要让事务正常释放回滚。using结构可以保证这一点但前提是不要在事务块内部到处catch然后继续往下走。我的习惯是事务内部只做获取对象和添加实体所有计算都放到事务外面做完减少事务占用时间也避免锁定冲突。还有一个小经验ed.GetEntity选择了两条线之后最好在事务里重新打开一次不要直接拿res1.ObjectId到事务外去GetObject。AutoCAD的事务机制要求所有对象打开操作都在同一个事务上下文中进行跨事务使用对象引用大概率会碰到eObjectErased或eLockNotSet之类的问题。很多新人贴报错出来其实根源就是打开位置不对。6. 常见问题排查与避坑记录6.1 常见问题速查表这里把我这几年在圆弧开发上遇到的典型问题整理成一张表随手可查现象根本原因解决思路圆弧方向反了Normal方向不对或起止角顺序反了确认Normal为(0,0,1)若期望顺时针将Normal设为(0,0,-1)圆弧大小和预期不符半径传错或角度单位混用检查半径单位全部用弧度运算打桩输出Radius值圆弧看起来像直线半径过大或三点近似共线检查半径是否超过图纸量级用new Arc(p1,p2,p3)前判断三点面积命令执行后没看到圆弧忘记AppendEntity或AddNewlyCreatedDBObject在事务中两者必须配套调用且事务要Commit在UCS下画的位置不对直接用GetAngle获取UCS角度改用点矢量计算角度或用CurrentUserCoordinateSystem转换圆弧添加成功但对象无法操作对象被事务回滚或数据库关闭检查tr.Commit()是否被条件分支跳过批处理多条线段时频繁报错每段都开新事务且未及时提交单个事务内处理完所有对象再一次性Commit圆弧与直线接不上切点计算未处理线段边界增加最小/最大切点校验控制半径范围最烦人的其实是“圆弧方向和位置都对但浮动变化”。这种情况多半是某个角度经过多次计算后落到了另一种象限导致最终弧长忽大忽小。解决方案只有一个在添加实体前把弧的StartAngle、EndAngle、TotalAngle全部用ed.WriteMessage打印出来人工核对。6.2 调试技巧与性能建议我的调试三板斧先把圆心平移到原点看看角度是否逼近预期再用arc.GetPointAtDist(0)取起始点坐标和用户输入的起点比对最后创建一个临时图层把辅助的圆心、切点、交点全部绘制成小圆点或短线一眼就能看出几何关系哪里不对。这三个动作做下来90%的问题都能当场定位。性能方面的经验集中在批量场景。如果你要在几千个节点上倒圆角千万别循环里每次新建Transaction。正确做法是开启一个事务把所有对象读进来、计算完、统一添加实体最后提交一次。这样不仅速度快还能保证生成失败时整体回滚数据库保持干净。再提一个很多人忽略的地方Line.StartPoint和Line.EndPoint返回的不是数组而是Point3d结构体直接用v1 pt2 - pt1得到的是Vector3d。很多新手把它当成几何向量来运算报错后才发现混用了点与向量的减法。其实CAD API在这两种类型之间的运算规则非常严格不确定时先把Type打出来看一眼别硬编译。7. 最后再分享一个我最近在用的技巧最近在做批量管廊建模时我发现用Arc类做圆角有一个很顺手的扩展直接利用GetPointAtDist反向计算圆弧上的任意点。比如我要在倒角圆弧上每隔50mm布置一个法兰标记可以这样double totalDist arc.GetDistanceAtParameter(arc.EndParam) - arc.GetDistanceAtParameter(arc.StartParam); int count (int)(totalDist / 50); for (int i 0; i count; i) { double dist i * 50; Point3d ptOnArc arc.GetPointAtDist(dist); // 在这里插入标记对象 }这个方法比起自己按角度插值算点要稳得多因为GetPointAtDist完全遵循圆弧的几何定义避开了角度规范化带来的零点跳变。我最早是自己写角度插值结果在跨越0度分界线的弧上翻过两次车改用这个API之后世界清净了。如果你也在为圆弧上等距布点发愁直接抄这个思路就好。圆弧这个对象看着简单真正要把它用稳、用好得把数学原理、坐标系习惯和API行为三者缝在一起。希望这篇能帮你少走几步弯路。有新的坑我后续再在系列里继续填。