news 2026/10/21 17:20:23

CAD二次开发:用Polyline凸度(Bulge)绘制带圆弧多段线的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CAD二次开发:用Polyline凸度(Bulge)绘制带圆弧多段线的完整指南

最近在调一个路径规划小工具,需要在图纸里把“一段直线、一段圆弧、再接一段直线”的走线画出来。一开始图省事,直接往模型空间塞了一条直线加一条圆弧,后来要倒圆角、偏移、统计工程量,发现这种“散装”画法到处给自己制造麻烦。最后还是回到用CAD .NET托管API去画带圆弧的多段线(Polyline),把每一段圆弧的凸度(Bulge)算清楚,一次成型。

这篇文章就把整个过程记录下来,包括Bulge到底是怎么来的、怎么根据半径和起终点换算、代码怎么写、以及我实际画图时踩过的几个坑。适合正在做CAD二次开发、尤其被Polyline的Bulge概念绕晕的朋友。如果你只是偶尔用脚本批量生成图纸,这篇文章同样能让你少走弯路。

1. 为什么说“带圆弧的多段线”才是正确画法

1.1 图纸里少放一个实体,后面就少写一堆逻辑

很多人刚接触CAD二次开发时,看到“一段直线接一段圆弧”,第一反应是用Line加Arc两个实体拼出来。这样做不是不能画,但后续所有操作都会变重。

举个例子:一条输送路径由“直线-圆弧-直线”组成。用散装图元画,图纸里就有三个实体,选中路径时得把三个实体一起选,遍历图元时得判断哪条线跟哪条弧相连,统计总长度时得分别取Line.Length和Arc.Length再相加。改成一条带圆弧的多段线后,这些都是同一个实体,一次遍历、一次统计全部搞定。

实际工程里更麻烦的是后续修改。圆弧的一端挪动了,Arc实体的圆心和角度要重新算;而多段线只需要移动顶点,圆弧段会根据新顶点自动重算。底下的对照表是我自己项目里的真实体验:

对比项散装Line + Arc带圆弧的Polyline
图元数量每段至少1个,多段就是多个始终1个
顶点编辑圆弧端点移动后要重建弧拖动顶点后圆弧自动更新
面积统计需手动组合或构造面域可直接取Area属性
偏移和倒角三种实体要分开处理一个实体统一处理
二次开发遍历需判断线弧连接关系直接读顶点和凸度

1.2 一条曲线“转回来”的能力,才是最值钱的地方

做设计时经常要临时改方案。我遇到过方案调整,要求把路径某一段的圆弧半径从50改成80。散装图元的做法是删掉旧弧、算切点、再画新弧,稍不注意切点位置就对不上。多段线则简单得多:把对应顶点的凸度重新算一下,一行代码就改完。

另一个关键点是曲线转换。CAD里很多命令都只认多段线,比如倒角、圆角、生成面域、沿曲线阵列等。如果你的图元是散的,就得先花时间合并成一条多段线,而合并的前提又要求相邻图元端点严格重合。实际图纸里经常出现端点差0.001的尴尬局面,合并不上。直接用带圆弧的多段线,完全绕开了这个问题。

当然,多段线也不是万能的。它本质上是平面内的二维曲线,如果要画三维空间里的任意曲线,就应该用三维多段线或样条曲线。文章后面的例子默认画在WCS的XY平面上,这点先讲清楚。

2. Bulge不是弧度,也不是弦高,而是tan(圆心角/4)

2.1 多段线里的圆弧,是“顶点加凸度”二元组

在CAD的多段线数据模型里,每段线要么是直线,要么是圆弧。区分两者的关键字段就是凸度Bulge。

一个顶点上存一个Bulge值,它表示“从这个顶点到下一个顶点”这一段线的弯曲程度。Bulge为0表示直线;Bulge不为0表示圆弧,正负表示圆弧的走向。注意,这里说的“凸度”不是弧度,也不是弦高,而是一个比值。

这个比值用几何语言描述是:圆弧的弓高h除以半弦长a。也就是:

Bulge = 弓高 / 半弦长 = h / a

弓高是弧顶点到弦的垂直距离。这个定义很直观:弧越是“鼓起来”,弓高越大,凸度越大;弧越平,凸度越接近0。

2.2 从等腰三角形推导,这个公式其实很好记

为什么要用“弓高/半弦长”而不是直接存角度?因为CAD的早期数据格式需要用一个简单比值来存,同时在计算中还保留了角度信息。这个比值恰好等于tan(圆心角/4)。

推导并不复杂。设一段圆弧的起点为A,终点为B,圆心为O,弦AB的中点为M。因为三角形OAB是等腰三角形,OM垂直平分AB。设圆心角∠AOB为θ,那么∠AOM就是θ/2。

在直角三角形OAM里,有:

tan(θ/2) = AM / OM = 半弦长 / 弦心距

同时弓高h等于半径R减去弦心距OM。用三角函数的半角公式可以推出:

h / AM = tan(θ/4)

所以Bulge = tan(θ/4)。这里θ的单位是弧度,且是有符号的。从当前顶点到下一顶点,如果圆弧绕圆心逆时针走,θ为正;顺时针走,θ为负。

2.3 常用角度对应的Bulge值,建议直接背下来

实际开发中经常要快速手算凸度,我整理了一张常用表:

圆弧角度Bulge值
0°0
45°tan(11.25°) ≈ 0.199
90°tan(22.5°) ≈ 0.414
120°tan(30°) ≈ 0.577
180°tan(45°) = 1
270°tan(67.5°) ≈ 2.414

几个容易记的节点:

  • 90度圆弧,Bulge不是0.5,而是约0.414。
  • 半圆,Bulge是1,不是0.5。
  • 超过180度的圆弧,Bulge大于1,可以达到几甚至几十,都很正常。

3. 实际画图前,先把几何参数换成顶点坐标和凸度

3.1 从设计图到多段线,要经历两步换算

拿到一张带圆弧的路径图,第一步不是写代码,而是把设计参数翻译成多段线能理解的顶点坐标和凸度序列。

多段线的数据结构是:按顺序排列的Point2d顶点数组,以及和顶点一一对应的Bulge数组。第i个顶点的Bulge表示第i段弧,也就是从顶点i到顶点i+1这一段。最后一段如果闭合,则由最后一个顶点指向第一个顶点。

我自己的习惯是先把路径拆解成一张表:

段号起始点结束点类型圆心/半径/角度参数
0(0,0)(100,0)直线bulge=0
1(100,0)(150,50)圆弧圆心(100,50),逆时针90°
2(150,50)(150,150)直线bulge=0

把这表列完,代码就只是翻译工作了。

3.2 已知圆心角时,直接算凸度

最理想的情况是设计图纸上直接标了圆心角。比如一段顺时针90度圆弧,圆心角θ等于-π/2,那么Bulge就是:

double bulge = Math.Tan(-Math.PI / 4); // 即 -0.414

如果弧是优弧,比如从起点到终点绕了270度,那么θ等于-3π/2,Bulge就是Math.Tan(-3 * Math.PI / 8),大约-2.414。这个值足够大,但仍然合法。

3.3 已知半径和起终点时,先定位圆心再算角度

更常见的场景是:知道圆弧起点A、终点B和半径R,但没直接给圆心角。这时要先通过几何关系求出圆心。

设弦长为L,半弦长a = L/2。弦心距d为圆心到弦AB的垂直距离,由勾股定理可得:

d = sqrt(R² - a²)

圆心O在弦AB的中垂线上,距离中点为d。中垂线方向向量是弦方向向量逆时针旋转90度得到的法向量,记为n。于是两个可能的圆心分别是:

O1 = 中点 + n * d O2 = 中点 - n * d

选哪个圆心,取决于圆弧朝哪一侧凸。选定圆心后,用atan2计算圆心到A、B的方向角,再求差值。

我写了一个辅助函数,核心思路是先求带符号圆心角,再转凸度:

// 已知起点、终点、圆心,求该段圆弧对应的Bulge // majorArc为true表示要画优弧(大于180度) static double BulgeFromCenter(Point2d start, Point2d center, Point2d end, bool majorArc) { double a0 = Math.Atan2(start.Y - center.Y, start.X - center.X); double a1 = Math.Atan2(end.Y - center.Y, end.X - center.X); double delta = a1 - a0; // 归一化到 (-π, π],得到短弧对应的有向圆心角 while (delta > Math.PI) delta -= 2 * Math.PI; while (delta < -Math.PI) delta += 2 * Math.PI; // 如果实际走优弧,补到另一个方向 if (majorArc) delta = (delta >= 0) ? delta - 2 * Math.PI : delta + 2 * Math.PI; return Math.Tan(delta / 4.0); }

这里我一直强调“带符号”。因为Bulge的正负必须和前进方向一致:从当前顶点走向下一个顶点,如果圆弧绕圆心逆时针为正,顺时针为负。稍不注意就会画反,第5章我会详细讲这个坑。

4. 用CAD .NET托管API画一条完整的闭合多段线

4.1 完整示例:直线、右上角圆弧、直线、左上角圆弧、闭合

下面这段代码可以在模型空间画出一个闭合轮廓:底边直线、右上角90度逆时针圆弧、顶边直线、左上角90度顺时针圆弧、最后直线闭合。为了简洁,省略了命名空间引用,对应的命名空间在CAD托管API里叫DatabaseServices和Geometry。

// 省略命名空间引用,对应CAD托管API的DatabaseServices和Geometry Database db = HostApplicationServices.WorkingDatabase; 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); Polyline pl = new Polyline(); // 第一步:先把所有顶点按顺序加入 pl.AddVertexAt(0, new Point2d(0, 0), 0, 0, 0); pl.AddVertexAt(1, new Point2d(100, 0), 0, 0, 0); pl.AddVertexAt(2, new Point2d(150, 50), 0, 0, 0); pl.AddVertexAt(3, new Point2d(150, 150), 0, 0, 0); pl.AddVertexAt(4, new Point2d(100, 200), 0, 0, 0); pl.AddVertexAt(5, new Point2d(0, 200), 0, 0, 0); // 第二步:再逐段设置凸度 pl.SetBulgeAt(0, 0); // 第0段:直线 pl.SetBulgeAt(1, Math.Tan(Math.PI / 8)); // 第1段:90度逆时针弧,约0.414 pl.SetBulgeAt(2, 0); // 第2段:直线 pl.SetBulgeAt(3, -Math.Tan(Math.PI / 8)); // 第3段:90度顺时针弧,约-0.414 pl.SetBulgeAt(4, 0); // 第4段:直线 pl.SetBulgeAt(5, 0); // 第5段:闭合段直线 pl.Closed = true; btr.AppendEntity(pl); tr.AddNewlyCreatedDBObject(pl, true); tr.Commit(); }

这段代码执行后,模型空间里会多出一条闭合多段线。用鼠标点选时只有一个对象,用LIST命令查看时能看到顶点和凸度信息。

4.2 为什么我坚持“先加顶点,后设凸度”

很多老示例代码直接在AddVertexAt里传非0的凸度参数,比如:

pl.AddVertexAt(1, new Point2d(100, 0), 0.5, 0, 0);

这个写法不是不行,但容易让人误解。AddVertexAt的第三个参数是“新顶点的凸度”,指的是新顶点到下一个顶点的凸度。如果你在末尾加顶点,后面没有“下一个顶点”,这个凸度就被丢掉了。

踩过这个坑之后,我给自己定了一个规矩:第一步只加顶点,凸度全部传0;第二步再用SetBulgeAt逐个设置。这样代码的语义一目了然,也不会因为顶点顺序调整导致凸度错位。

另外要注意,对非闭合多段线,最后一个顶点的凸度实际上不会生效,因为不存在“最后顶点到下一顶点”的线段。只有把Closed设为true后,最后一个顶点到第一个顶点的闭合段才会用到最后一个顶点的凸度。

4.3 画完如何验证:读回顶点和凸度

画完之后最好立刻验证一下,不要等到打印图纸才发现方向反了。我用得最简单的验证方法是遍历多段线,把顶点和凸度打印到命令行:

Editor ed = Application.DocumentManager.MdiActiveDocument.Editor; for (int i = 0; i < pl.NumberOfVertices; i++) { Point2d pt = pl.GetPoint2dAt(i); double bulge = pl.GetBulgeAt(i); ed.WriteMessage($"\n顶点{i}: ({pt.X:F1}, {pt.Y:F1}), Bulge={bulge:F4}"); }

这段代码能帮我快速定位哪一段的凸度算错了。特别是圆弧方向反了时,正负号一眼就能看出来。

5. 画完之后的实测与踩坑记录

5.1 方向陷阱:圆心在弦上侧,凸度不一定是正

这是新人最容易翻车的点。以起点(0,0)、终点(100,0)为例,如果圆心在弦上方比如(50,50),画出来的圆弧是向下凸的劣弧。从起点沿弧走到终点,绕圆心的方向是逆时针,所以凸度应为正。

很多人会凭直觉认为“圆心在上方,弧在上方”,然后画出反向弧。正确判断方法是:始终站在圆心上,观察从起点向量转到终点向量的方向。逆时针为正,顺时针为负。

我在代码里遇到方向不对时,第一反应就是检查atan2求出的圆心角差值有没有做归一化。差值大于π或小于-π时,说明取的不是同一圈上的角。

5.2 半圆凸度是1,270度凸度是2.414

半圆对应的Bulge是1,这个数太容易记成0.5了。如果用0.5去画半圆,实际画出来的弧只有大约53度,不是半圆。

我后来把常用角度的凸度做成常量写在工具类里,避免每次手算:

public const double Bulge90 = 0.4142135623730951; public const double Bulge180 = 1.0; public const double Bulge270 = 2.414213562373095;

超过180度的圆弧完全可以用一个凸度表达,不需要拆段。比如270度弧的凸度约2.414,合法且稳定。千万不要看到凸度大于1就以为写错了。

5.3 闭合轮廓的最后一段圆弧,很容易漏

画闭合多段线时,最后一段回到起点的圆弧常常被忽略。假设一个矩形轮廓,想让右边那条边变成向内凹的半圆,实际上需要修改的是“最后一个顶点到第一个顶点”这一段。

具体做法是:先确保pl.Closed = true,然后对最后一个顶点调用SetBulgeAt(pl.NumberOfVertices - 1, 1.0)。很多人在闭合前把最后一个顶点的凸度设为0,闭合后才发现那条边是直线。

5.4 实体没显示,不一定是没画上去

有那么几次,执行完代码后屏幕上什么都没有,第一反应是实体没写进数据库。排查到最后发现,实体已经写进去了,只是视图没有刷新。

最简单的处理是提交事务后调用一次视图更新命令,或者手动在命令行执行REGEN。当然,更多时候是块表记录写错了:把实体加到了当前空间以外的地方。检查一下你取到的BlockTableRecord是不是模型空间的记录,这是新手高频错误。

5.5 多段线终归是二维的,别拿去画三维弧

Polyline本质上是在某个平面坐标系内表达的二维曲线,即使不设置标高,默认也在WCS的XY平面内。如果项目里有大量三维管道走向,需要的是三维多段线或者其他三维曲线类型。

我犯过的错误是试图用二维多段线去模拟空间折弯,结果画出来的全部被压平在一个平面上。后来规规矩矩按三维曲线处理,反而简单了。

6. 从“能画”到“好用”:几个可以直接扩展的方向

6.1 把已有圆弧转成带圆弧的多段线

有时候用户给的图纸里已经有大量Arc实体,需要统一转成多段线。读取Arc的起点角度、终点角度、半径和圆心后,可以计算圆心角θ,再生成顶点和凸度。

要注意的是,如果圆弧本身就是个整圆,圆心角是2π,直接算凸度会无穷大,需要拆成两条半圆多段线。其他弧段无论优弧劣弧,都可以用一个凸度表达。

6.2 给折线自动倒圆角

这套思路还能用来做折线圆角。给定三个顶点P0、P1、P2,要在转角处插入一段圆弧。先根据圆角半径求出圆弧与两条直线的切点T0、T1,两个切点连起来就是多段线的新顶点,中间那段圆弧的凸度由转角算出。

我实践中发现这个功能在道路中心线、管线转弯处理里非常实用,批量处理比手动倒圆角快很多。

6.3 批量坐标文件导入

如果有一批坐标数据要生成带圆弧路径,可以约定CSV格式,每行是一列x、y和凸度,然后循环调用AddVertexAt和SetBulgeAt。这样原来的手工作业就变成了批量批处理,出错率也大幅下降。

我自己固定使用这种工作流:先在Excel里把路径拆成顶点表和凸度表,再让工具读取生成,最后人工抽查几个关键点坐标。这样既能保证效率,又不会盲目相信脚本结果。

几次踩坑之后,我现在画带圆弧的多段线已经形成固定习惯:先在纸上列出每段起始点、终止点、圆心角,再用常用角度凸度表做一次心算校验,最后才写代码。坐标可以在模型空间里用临时辅助线检查,确认无误后再批量生成正式图元。这样虽然多花几分钟,但基本不会返工。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 17:19:22

高校图书馆管理员工作总结写作指南:从流水账到服务闭环

简介&#xff1a;本资源是一份面向高校图书馆管理岗位新人及图情专业实习人员的实务型工作总结与心得体会文档&#xff0c;聚焦图书分类编目、规范服务流程、破损修复实操及读者需求响应等核心能力提升。全文系统梳理了基于《中国图书馆分类法》的图书整理上架实践、外借阅览制…

作者头像 李华
网站建设 2026/10/21 17:18:06

AI辅助回复审稿意见的三阶工作法:理解-构建-表达

1. 这不是“AI代写”&#xff0c;而是科研沟通力的智能增强工具 “如何用AI辅助回复审稿意见&#xff1f;”——这句话最近在好几个学术交流群里被反复刷屏。我第一次看到它&#xff0c;是在帮某高校青年教师修改一篇被拒稿后重投的论文时。他把审稿人那句“实验设计缺乏对照组…

作者头像 李华
网站建设 2026/10/21 17:18:01

朱雀AI检测原理与人类化写作12个实操动作

1. 这不是“查重软件”&#xff0c;而是一把AI内容健康诊断尺“朱雀AI查重”这五个字最近在内容创作者圈里频繁刷屏&#xff0c;但很多人点开界面后第一反应是&#xff1a;它和知网、万方那些标着“重复率XX%”的系统根本不是一回事。我接触过几十个用它做初筛的写作者——有高…

作者头像 李华
网站建设 2026/10/21 17:17:50

Go语言核心三板斧:函数、结构体与接口的工程实践

1. 从一次代码评审聊起&#xff1a;为什么这三个概念是Go的命根子 大概两年前&#xff0c;我在某公司做内部代码评审&#xff0c;看到一个刚转Go的同事写了这么一段逻辑&#xff1a;他用一个结构体存用户信息&#xff0c;又单独写了一大堆函数去操作这个结构体&#xff0c;函数…

作者头像 李华
网站建设 2026/10/21 17:17:45

AI辅助编码实战:终端Agent的API报错与光标Bug排查,沉淀感应模式理论

最近两周&#xff0c;我把晚上的时间基本都投在了一个有点“自找麻烦”的项目上&#xff1a;用 AI 辅助编码&#xff0c;写一个跑在终端里的 AI Agent。目标很朴素——在命令行里用自然语言下达指令&#xff0c;让它自动拆解任务、调用外部 API、再把结果整理好回显给我。听起来…

作者头像 李华
网站建设 2026/10/21 17:14:59

Notepad++中精准指定GCC版本的可复现工作流

1. 项目概述&#xff1a;为什么在 Notepad 里“指定 GCC 版本”不是个功能&#xff0c;而是一套完整的工作流&#xff1f; 你点开 Notepad 写完一个 hello.c &#xff0c;按 F5 想直接编译运行&#xff0c;结果弹窗报错&#xff1a;“gcc 不是内部或外部命令”&#xff0c;或…

作者头像 李华