ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

C# CAD二次开发:圆弧创建原理与常见坑位详解

C# CAD二次开发:圆弧创建原理与常见坑位详解

如果你做过一段时间的CAD二次开发,一定知道"画圆弧"这件事看着简单,实际坑不少。C#/CAD二次开发里常用图元很多,前面几篇我把直线、圆、多段线的创建都过了一遍,这篇单独把圆弧拉出来写,是因为它同时涉及圆心、半径、起始角度、终止角度、法向量五个维度,任何一个理解不到位,生成的图就会和你预期差十万八千里。这篇是2021年7月初整理的第4篇,内容聚焦在"创建圆弧"这件事本身,从API选型到角度系统,再到完整实操和排坑思路都会讲到,适合正在用C#做CAD平台二次开发、尤其是刚把直线和圆搞定的朋友参考。

1. 圆弧这个图元到底特殊在哪

1.1 一张图里圆弧的本质

圆只需要圆心加半径就能完全定义,圆弧不行。圆弧本质上是"带起止角度的圆",也就是说,它在圆的基础上多出了两个关键信息:从哪里开始,到哪里结束。这个额外信息的代价就是,代码里必须显式处理角度,而角度恰恰是CAD二次开发新手最容易翻车的地方。

我在实际项目中见过不少这样的情况:直线代码跑得溜,圆的代码也一次通过,结果一到圆弧,画出来的要么是半圆,要么角度明显偏了,要么方向反了。原因基本都不是"不会写代码",而是没有真正理解CAD底层对圆弧的定义方式。

这里先明确一个概念:在主流CAD平台的托管API里,圆弧就是通过圆心、法向量、半径、起始角度、终止角度这五个要素来描述的。你写的代码最终无非是给这几个赋值,只是不同的构造方式,赋值的来源和计算路径不一样。

1.2 三种常见创建方式与选型

根据场景,我一般把创建圆弧的方式分成三种:

创建方式核心思路适合场景
圆心+半径+起始角+终止角直接构造,参数最全已知圆心和角度,批量生成、参数化建模
起点+终点+圆弧上一点三点定弧,构造函数直接支持用户手动点选三个点创建圆弧
圆心+起点+包角先算起点角度,再加包角得到终点角已知圆心、一个端点和需要旋转的角度(扫角)

第一种最常用,也是这篇的重点。第二种优点是对用户非常友好,因为操作者脑子里想的就是"过这三个点画一条弧",不需要算任何角度。但要注意:三个点不能共线,否则圆弧退化,API会直接抛出异常,或者生成一个难以预料的结果。第三种是我个人非常喜欢用的一种,尤其在需要生成扇形、倒角、过渡线这类图形时特别省事,因为工程上很多时候给的就是"起点在哪、扫过多少度"。

2. 创建圆弧必懂的CAD角度系统

2.1 弧度制是第一个下马威

CAD平台.NET API里所有角度参数,用的都是弧度制。日常我们习惯说"30度""90度",但代码里必须传0.5236、1.5708这样的弧度值。我见过不少同事直接传30下去,结果圆弧只转了一个非常小的角度,然后就开始怀疑人生。

换算公式没什么神秘的:角度转弧度就是度数乘以Math.PI / 180,弧度转角度就是弧度乘以180 / Math.PI。我在项目里习惯在公共类里放两个静态方法,一个DegToRad,一个RadToDeg,所有涉及角度的地方都走这两个方法,避免每次手写公式导致不一致。

2.2 零度在X轴正方向,逆时针为正

这是圆弧创建里最容易被忽略、也最容易导致"角度看起来不对"的点。CAD平台的圆弧角度系统遵循数学上的单位圆约定:零度在X轴正方向,逆时针方向角度增大。也就是说,90度在Y轴正方向,180度在X轴负方向,270度在Y轴负方向。

有次我在项目里做一个旋转对称的零件轮廓,按照习惯从Y轴正方向算起点角度,结果每个圆弧都偏了90度。排查了老半天,最后发现就是参考方向理解错了。如果你也发现圆弧角度整体偏移了固定值,优先检查是不是把Y轴正方向当成了零度基准。

2.3 跨零度边界时的扫角计算

圆弧有个特性:总是从起始角逆时针走向终止角。如果终止角小于起始角,它会跨过零度继续走,实际扫角等于终止角减起始角再加一个完整圆周长(2π)。

举例来说,如果起始角是350度,终止角是10度,按"终止角大于起始角才是扫角"的直觉来判断,会以为这是一个从350度逆时针扫过340度的大圆弧。但实际上,因为终止角小于起始角,CAD会从350度逆时针跨过0度走到10度,扫过的是20度的小弧。反过来,如果起始角是10度、终止角是350度,才会得到那个340度的大弧。

这个规则很容易把人绕晕。我的建议是:不要依赖直觉去传起始角和终止角,而是明确先算出起点角度,再根据需要的扫角算出终点角度。也就是上面说的"圆心+起点+包角"方案,包角本身是明确的,不存在跨边界歧义。

2.4 法向量的作用与影响

法向量决定了圆弧所在平面以及"哪一面朝上"。默认情况下,法向量是(0,0,1),也就是WCS的Z轴方向,圆弧落在XY平面上。如果法向量变成(0,0,-1),圆弧虽然还在同一个平面,但看起来走向会变成顺时针,视觉上完全反转。

在三维建模场景里,法向量必须垂直于圆弧所在平面,且必须是单位向量。如果你创建的是平面上的一般圆弧,保持默认法向量就好,不要随便改。只有需要把圆弧放到任意空间平面时,才需要显式设置法向量,并配合右手定则确认旋转方向。

3. 实操:做一个带圆弧轮廓的零件图

3.1 明确需求与整体流程

这次实操做一个小零件轮廓:一个扇形垫片,圆心在指定坐标,半径为给定值,圆弧从30度到120度,再加上两条半径直线,形成一个封闭的扇形区域。这个场景很典型,既用圆弧,又用直线,而且能比较好地检验你对角度和坐标计算的理解。

整体流程分三步:先算圆弧起点和终点的坐标,再创建圆弧和两条直线,最后把它们批量写入模型空间并提交事务。代码逻辑不复杂,但每一步都有需要注意的细节。

3.2 完整代码与参数计算过程

以某主流CAD平台的托管API为例,代码如下:

// 请根据你实际使用的CAD平台替换命名空间 using CAD.DatabaseServices; using CAD.Geometry; using CAD.Runtime; using CAD.ApplicationServices; public static void CreateFanArc() { Document doc = Application.DocumentManager.MdiActiveDocument; Database db = doc.Database; Editor ed = doc.Editor; using (doc.LockDocument()) using (Transaction tr = db.TransactionManager.StartTransaction()) { BlockTable bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); BlockTableRecord btr = (BlockTableRecord)tr.GetObject( bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite); Point3d center = new Point3d(100, 100, 0); double radius = 80; double startDeg = 30; double endDeg = 120; // 角度转弧度 double startAngle = startDeg * Math.PI / 180; double endAngle = endDeg * Math.PI / 180; // 计算圆弧起点和终点坐标 Point3d startPt = new Point3d( center.X + radius * Math.Cos(startAngle), center.Y + radius * Math.Sin(startAngle), 0); Point3d endPt = new Point3d( center.X + radius * Math.Cos(endAngle), center.Y + radius * Math.Sin(endAngle), 0); // 创建圆弧和两条半径直线 Arc arc = new Arc(center, Vector3d.ZAxis, radius, startAngle, endAngle); Line line1 = new Line(center, startPt); Line line2 = new Line(center, endPt); // 写入模型空间并提交 btr.AppendEntity(arc); tr.AddNewlyCreatedDBObject(arc, true); btr.AppendEntity(line1); tr.AddNewlyCreatedDBObject(line1, true); btr.AppendEntity(line2); tr.AddNewlyCreatedDBObject(line2, true); tr.Commit(); ed.Regen(); } }

这里最核心的坐标计算就是center.X + radius * Math.Cos(startAngle)和center.Y + radius * Math.Sin(startAngle),这其实就是极坐标到直角坐标的转换。很多圆弧角度错乱的问题,追到根上就是这里用错了三角函数或者角度没转弧度。

3.3 指定图层和颜色

如果想把生成的圆弧和直线放到指定图层,有一个很简单的方法:在把实体加入数据库之后、事务提交之前,直接设置实体的Layer属性。但前提是这个图层必须已经存在,否则CAD会抛异常。

我习惯写一个确保图层存在的辅助方法,这样调用时就不用担心图层不存在:

private static void EnsureLayer(Database db, Transaction tr, string layerName) { LayerTable lt = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead); if (!lt.Has(layerName)) { using (LayerTableRecord ltr = new LayerTableRecord()) { ltr.Name = layerName; lt.UpgradeOpen(); lt.Add(ltr); tr.AddNewlyCreatedDBObject(ltr, true); } } }

然后这样使用:

EnsureLayer(db, tr, "轮廓层"); arc.Layer = "轮廓层"; line1.Layer = "轮廓层"; line2.Layer = "轮廓层"; arc.ColorIndex = 1;

颜色方面,ColorIndex = 1表示红色,2是黄色,3是绿色。如果只想让实体跟随图层颜色,可以不设置ColorIndex,保持默认的ByLayer。

3.4 通过事务批量修改已生成的实体

有些场景下,实体已经加入数据库,但你还想统一修改它们的属性,比如把一批圆弧和直线全部换成另一个图层。这时不需要重新创建实体,可以在同一个事务里通过对象ID打开它们进行修改。

ObjectIdCollection ids = new ObjectIdCollection(); ids.Add(arc.ObjectId); ids.Add(line1.ObjectId); ids.Add(line2.ObjectId); foreach (ObjectId id in ids) { Entity ent = (Entity)tr.GetObject(id, OpenMode.ForWrite); ent.Layer = "新图层"; ent.ColorIndex = 3; }

这里务必注意:如果这些实体是在同一个事务里创建的,打开方式要用OpenMode.ForWrite,因为需要写操作。如果只是读取属性,用OpenMode.ForRead就行。

3.5 三点圆弧与包角圆弧的代码写法

三点圆弧在API里有直接对应的构造函数,传起点、终点和弧上一点即可:

Point3d startPoint = new Point3d(0, 0, 0); Point3d endPoint = new Point3d(100, 0, 0); Point3d pointOnArc = new Point3d(50, 50, 0); Arc arc3P = new Arc(startPoint, endPoint, pointOnArc);

这段代码的效果和你手动执行CAD里"三点画弧"命令是一样的。需要留意的是:如果三个点近似共线,或者计算精度导致它们共线,API会抛异常,所以实际使用前最好做一次三点共线判断。

包角圆弧是我推荐的方式。已知圆心、半径、起点坐标以及需要扫过的角度时,先用Atan2算出起点相对圆心的角度,再累加包角作为终止角:

double sweep = 90 * Math.PI / 180; Point3d center = new Point3d(50, 50, 0); Point3d startPoint = new Point3d(100, 50, 0); double radius = startPoint.DistanceTo(center); double startAngle = Math.Atan2( startPoint.Y - center.Y, startPoint.X - center.X); Arc arcBySweep = new Arc( center, Vector3d.ZAxis, radius, startAngle, startAngle + sweep);

Atan2的返回值范围是-π到π,可能得到负角度,但这个负角度作为起始角传给CAD平台不会有问题,CAD会按照"从起始角逆时针到终止角"的规则自动处理。

4. 常见问题排查与调试实录

4.1 圆弧创建成功,但屏幕上什么都没有

这个问题我遇到太多次了,排查顺序基本固定:先看图层是否关闭或冻结,再看实体颜色是否和背景色一致,最后看视角是否在实体所在范围外。

图层问题最隐蔽,尤其是当实体被自动放到了当前图层,而当前图层恰好是关闭状态。解决方法是把实体的Layer强制指定到一个可见图层。颜色问题也很常见,实体是白色,背景也是白色,自然看不见。视角问题则可以用一行代码解决:

doc.Editor.ZoomExtents();

如果没有调Regen,在某些视图状态下也会出现"数据里有、显示上没有"的情况。手动调用一次doc.Editor.Regen()就能强制重生成图形。

4.2 圆弧角度整体偏移了90度

这是零度参考方向搞错了。CAD平台的角度零度在X轴正方向,不是你习惯的任何方向。如果你是按"Y轴正方向为0度"来算的,所有角度都会偏90度。

排查方法很简单:把已知角度代入圆弧的起点坐标计算,看算出来的点坐标是否符合预期。如果起点坐标偏了,角度参考系统肯定有问题;如果起点坐标对,但圆弧形状不对,那就要看法向量和角度方向。

4.3 圆弧方向总是反的

默认法向量是(0,0,1),圆弧逆时针从起始角走向终止角。如果你看到圆弧是顺时针走的,优先检查是否手动设置了法向量,尤其是设成了(0,0,-1)。有些老代码里会把法向量传给构造函数,一不留神就导致方向反转。

还有一种情况是起始角和终止角传反了,比如想把90度到180度的弧画出来,却传了180和90,结果CAD走了另一个方向形成一段优弧。解决这类问题,我建议在代码里显式注释出"起点角度、终点角度"对应的坐标,一眼就能看出传参是否合理。

4.4 圆弧看起来不圆,像多边形

这个不是创建逻辑错了,而是显示精度的问题。CAD的圆弧在屏幕上是用一系列短直线段近似显示的,视图放大到一定程度就能看到折线感。这种情况不影响数据正确性,图纸打印或者导出时也不会有问题。如果想在屏幕上看到更平滑的圆弧,可以调整CAD平台的圆弧显示精度变量,但不推荐在二次开发代码里随便改这个参数,容易影响整个绘图环境的性能。

4.5 事务没提交,修改全部丢失

新手最容易犯的错误是只写了StartTransaction,却忘了tr.Commit()。结果代码跑完,模型空间里一切照旧,也没有报错。这个问题在调试时非常迷惑,因为断点显示实体确实创建了,但就是不写入数据库。

我的经验是一开始就把事务模板背下来:开启事务、获取块表、获取模型空间、创建实体、追加实体、提交事务。每一步都不要省。如果你是在一个长循环里创建大量实体,也记得最后统一提交一次,不要每创建一个实体就提交一次,性能会差很多。

4.6 常见问题速查表

现象最可能原因快速解决办法
画完看不到图层关闭或颜色与背景一致检查Layer和ColorIndex,再ZoomExtents
角度偏90度零度参考方向错误确认按X轴正方向为0度计算
方向反了法向量为(0,0,-1)或起止角反了检查法向量和起止角传参顺序
圆弧不圆显示精度不足调整显示相关变量或忽略
修改丢失事务未Commit补上tr.Commit()
三点画弧报错三点共线计算共线判断或换用其他构造方式

5. 一些写在后面的经验

圆弧这个东西,代码量不大,但每个参数背后都是一个几何知识点。把这几个点串起来,其实能帮助你建立对整个CAD几何系统的理解。

我最想说的是一个实践建议:所有涉及圆弧的地方,不要直接在业务代码里写角度换算和坐标计算,抽一个工具类统一处理。这个工具类至少提供三个方法:角度转弧度、弧度转角度、根据圆心半径和角度求坐标。我在几个项目里都是这么做的,后期维护起来特别省心。

另外,如果只是做图形预计算、碰撞检测,不一定要把圆弧立刻加入数据库。可以先使用几何类在内存里把圆弧对象构建出来做各种判断,确认没问题之后再创建数据库实体。这样既避免了事务回滚的麻烦,也让计算逻辑更清晰。

这个系列后面我打算再写一篇关于多段线里圆弧段的内容。工程图纸里很多圆弧并不是独立的圆弧图元,而是作为多段线的一部分存在的,那个凸度参数又是另一套逻辑。先把独立圆弧吃透,再看多段线弧段会轻松很多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进