首页 / 资讯中心 / 文章详情

C# WinForms+GDI+ 实现 DXF 图纸解析与轻量查看器源码

C# WinForms+GDI+ 实现 DXF 图纸解析与轻量查看器源码 ★ FEATURED ARTICLE
简介面向具备一定C#基础、希望掌握CAD图形数据解析与显示的开发者这份源码合集围绕C# 2010环境下DXF文件的读取与渲染展开覆盖从文件解析到界面呈现的完整链路。包内共180个文件以90个cs源文件为主体配有28个dxf示例图纸用于验证解析效果另有dll、exe、csproj、sln、xaml等工程与界面文件及少量缓存、日志类附属文件压缩包约2.78MB可直接编译运行调试。内容涉及DXF分段结构识别HEADER、TABLES、ENTITIES等、点线圆与多段线等实体的对象模型封装、坐标数据转换、基于GDI或WPF的图形绘制以及OpenFileDialog选文件、异常处理、大文件流式读取等工程细节。已有2610人学习适合用来对照理解解析流程、复用绘图代码或作为课程设计、小型CAD查看器的起点。1. 车间那台只装得上 .NET 4.0 的工控机,要打开一张 DXF 图纸上个月有人问我,客户只给.dxf文件,不给 CAD 图纸文件,现场那台工控机系统是 Win7,装不上任何现代版本的绘图软件,只想在自家 WinForms 程序里把图打开、放大、点两个位置量个尺寸。找现成的库要么收费,要么依赖新框架;翻开源代码时,一套用 C# 读取 DXF 并由 WinForms GDI 显示图纸的源码重新被翻了出来,工程文件是 VS2010 的,目标框架 .NET Framework 4.0,不用装任何第三方组件。它做的事很朴素:自己按 DXF 的组码解析ENTITIES段,把 LINE、CIRCLE、ARC、LWPOLYLINE、TEXT 这些图元还原成对象,再用 GDI 画到控件上,带缩放和平移。适合两类人:一类是被现场环境限制住、必须离线跑在旧机器上的工控或设备类开发者;另一类是想弄明白「DXF 到底怎么读」,准备自己写一个轻量查看器、并且能看懂 DXF 源码结构的开发者。2. 项目骨架:C# DXF 源码里那几个文件各管什么2.1 分层目录与各层职责这份源码的目录不像网上很多示例那样所有代码都挤在一个 Form 里,它至少有解析、渲染、界面三层。我拿到的版本大致是这样的结构,你手上的如果文件名对不上,按职责对号入座即可:DxfViewer/ ├─ Dxf/ 解析层不引用 System.Windows.Forms │ ├─ DxfReader.cs 两行一组的组码读取器 │ ├─ DxfParser.cs 段落扫描 实体分派 │ ├─ DxfDocument.cs 解析结果容器实体表、图层表、包围盒 │ ├─ Entities/ │ │ ├─ DxfEntity.cs 所有图元的基类ReadProperty / Draw 虚方法 │ │ ├─ DxfLine.cs DxfCircle.cs DxfArc.cs │ │ ├─ DxfLwPolyline.cs DxfText.cs DxfInsert.cs │ │ └─ DxfUnknown.cs 兜底吞掉不认识的图元 │ └─ Tables/DxfLayer.cs 图层名、颜色、开关状态 ├─ Render/ │ ├─ ViewerControl.cs 自绘控件OnPaint 里出图 │ └─ PenCache.cs 画笔复用避免每次重绘 new Pen ├─ UI/ │ ├─ MainForm.cs 打开文件、状态栏、缩放按钮 │ └─ LayerPanel.cs 图层列表勾选控制显隐 └─ Program.cs这个分层最值钱的地方是解析层不碰Graphics。DxfEntity里只存几何数据和一个Draw(Graphics, DxfDocument)的抽象方法,渲染层拿到的是纯数据。以后要把显示换成 WPF 的DrawingVisual,或者要把图元导出成 SVG、PDF,只需要再写一个渲染器,解析代码一行都不用动。反过来说,如果你接手的是把所有绘制逻辑都塞进OnPaint的那种写法,先做这一步拆分,后面所有改造都会轻松。2.2 DXF 的组码段落:TABLES、BLOCKS、ENTITIES 各管什么DXF 是纯文本格式,最核心的概念就一个:两行一组,第一行是组码(整数),第二行是值。组码告诉你「这个值是什么含义」,同一个组码在不同图元里含义不同。比如10/20/30一般是主点的 X/Y/Z,40在 CIRCLE 里是半径、在 TEXT 里是字高。文件被切成若干段,每段用0/SECTION开头、2/段名声明名字、0/ENDSEC结尾:段名内容这套源码用不用HEADER全局变量,$ACADVER版本、$EXTMIN/$EXTMAX图形范围用,取版本定编码、取范围定初始缩放TABLES图层表 LAYER、线型表 LTYPE、文字样式表 STYLE用,建图层字典BLOCKS块定义,每块内部又是一堆图元用,INSERT 要靠它展开ENTITIES图纸上的实体,画图的主体核心OBJECTS字典、扩展数据等非图形对象跳过有个细节需要留意:$EXTMIN/$EXTMAX只是「作者建议的视图范围」,不少导出工具写的是0,0,0,甚至是错的。完全依赖它做初始缩放的实现,打开某些图纸会是一片空白或者缩成一个点。这套源码的处理方式是:先看$EXTMIN/$EXTMAX是否有效,无效就在解析完实体后遍历一遍算真实包围盒。这个兜底逻辑看着不起眼,却是「为什么有的图打开是空白」这个问题的第一嫌疑人。2.3 目标框架 .NET 4.0 带来的语法约束工程是 C# 4 时代的产物,这决定了你能抄多少网上的新代码。字符串内插${x}、空条件运算符?.、nameof、元组、async/await,在这里全都编不过——async/await要 C# 5 和 VS2012 起。拼接字符串老老实实string.Format或者,判空老老实实if (x ! null)。LINQ、var、lambda 是有的,这些放心用。提示:直接把项目属性里的目标框架改成 4.5 以上,语法能放开,但会破坏「在老机器上免安装运行」这个前提。现场机器只有 .NET 4.0 的话不要动它,新建一个类库放新代码反而更稳妥。选 WinForms GDI 而不是 WPF,理由也在这:老的集成显卡驱动上 GDI 的兼容性更可控,启动也快,几万个图元以内完全够用。3. 解析核心:从双行组码流到图元对象的完整链路3.1 组码读取器:两行一组,值行千万别 Trim解析的起点是最枯燥的一步:把文件读成一组一组的(code, value)。这里有两个容易犯错的地方——一是组码行可以有前导空格,必须Trim才能int.Parse;二是值行不能 Trim,TEXT 实体的文字内容前后空格是有意义的,把 备注 改成 备注 属于数据损坏。所以读取器里只 Trim 组码行。// DxfReader.cs —— 最小可用的组码读取器C# 4 语法 public sealed class DxfPair { public int Code; // 组码整数 public string Value; // 组码对应的值原样保留不做 Trim } public sealed class DxfReader : IDisposable { private readonly StreamReader _sr; public DxfReader(string path, Encoding enc) { // enc 由调用方根据 $ACADVER 决定不要在这里写死 UTF-8 _sr new StreamReader(path, enc, false, 1 16); // 64KB 缓冲大文件明显更快 } public DxfPair ReadPair() { string codeLine _sr.ReadLine(); if (codeLine null) return null; // 文件到底 string valueLine _sr.ReadLine(); if (valueLine null) throw new DxfException(组码 codeLine 后面缺少值行); int code; if (!int.TryParse(codeLine.Trim(), out code)) throw new DxfException(非法组码: [ codeLine ]); return new DxfPair { Code code, Value valueLine }; } public void Dispose() { _sr.Dispose(); } }参数上唯一需要斟酌的是StreamReader的缓冲区大小。默认 1KB 对于几十万行的 DXF 来说系统调用次数太多,换成 64KB 后,一个 8MB 的图纸解析时间能从两三秒降到一秒出头。detectEncodingFromByteOrderMarks传false,因为 DXF 不带 BOM,让它去猜反而会猜错。3.2 实体分派:用组码 0 切分实体边界组码0在 DXF 里是「结构分界符」——后面跟的值是实体类型名(LINE、CIRCLE)或者段标记(SECTION、ENDSEC、EOF)。所以解析主循环的骨架非常统一:遇到0就结束上一个实体、开始下一个;其他组码全部交给当前实体自己消化。// DxfParser.cs —— 只关心 ENTITIES 段的主循环 public ListDxfEntity ParseEntities(DxfReader reader, DxfDocument doc) { var list new ListDxfEntity(); DxfPair p; // 1) 快进到 ENTITIES 段其他段一律跳过 while ((p reader.ReadPair()) ! null) { if (p.Code 0 p.Value SECTION) { DxfPair name reader.ReadPair(); // 组码 2 段名 if (name.Code 2 name.Value.Trim() ENTITIES) break; } } // 2) 组码 0 就是实体边界 DxfEntity cur null; while ((p reader.ReadPair()) ! null) { if (p.Code 0) { if (p.Value ENDSEC) { if (cur ! null) list.Add(cur); break; } if (cur ! null) list.Add(cur); cur EntityFactory.Create(p.Value, doc); // 未知类型返回 DxfUnknown continue; } if (cur ! null) cur.ReadProperty(p.Code, p.Value); // 交给实体自己认领 } return list; }这里的关键设计是把「识别组码含义」的责任下放到每个实体类。DxfLine.ReadProperty只认10/20/11/21/8/62,DxfCircle只认10/20/40,DxfUnknown.ReadProperty是空实现——这样遇到 HATCH、SPLINE、DIMENSION 这些没实现的图元时,解析器不会抛异常,而是安静地建一个占位对象吞掉它的所有属性,后面的图元照常解析。能跳过,比能解析更重要,否则一张图里有个三维实体,整张图就打不开了。EntityFactory.Create就是一段switch,匹配不到就new DxfUnknown()。加一个新图元类型的成本基本等于「写一个类 加一行 case」。3.3 LWPOLYLINE 的凸度:一条多段线里藏着圆弧LWPOLYLINE(轻量多段线)是 DXF 里最容易写错的一个实体,因为它不是「一个点一组组码」,而是多个组码 10 交替出现,每个 10 开启一个新顶点,紧跟的 20 给出 Y,可选的 42 给这一段的凸度(bulge)。如果用Dictionaryint, string存属性,后出现的 10 会把前面的覆盖掉,最后你只会得到一个点。// DxfLwPolyline.cs —— 按出现顺序收集顶点而不是塞进字典 public sealed class DxfLwPolyline : DxfEntity { public readonly ListPoint2D Vertices new ListPoint2D(); public readonly Listdouble Bulges new Listdouble(); public bool IsClosed; private double _x, _y; // 暂存当前顶点的 X / Y public override void ReadProperty(int code, string value) { switch (code) { case 10: // 新顶点开始只记 X _x ToDouble(value); break; case 20: // Y 到了顶点才算完整 _y ToDouble(value); Vertices.Add(new Point2D(_x, _y)); Bulges.Add(0.0); // 先占位后面遇到 42 再回填 break; case 42: // 凸度属于「上一段」 if (Bulges.Count 0) Bulges[Bulges.Count - 1] ToDouble(value); break; case 70: IsClosed (ToInt(value) 1) 1; // 第 0 位 闭合标志 break; case 8: LayerName value; break; } } }凸度b的定义是b tan(θ/4),θ是这段圆弧的有符号圆心角,逆时针为正。b 0表示直线段,|b| 1表示半圆。由两个端点和凸度反算圆弧参数:// 凸度转圆弧参数返回圆心、半径、起始角、扫掠角 private static bool BulgeToArc(Point2D p1, Point2D p2, double b, out Point2D center, out double radius, out double sweep) { center Point2D.Zero; radius 0; sweep 0; double dx p2.X - p1.X, dy p2.Y - p1.Y; double chord Math.Sqrt(dx * dx dy * dy); if (Math.Abs(b) 1e-9 || chord 1e-9) return false; // 凸度为 0 直线段 double theta 4.0 * Math.Atan(b); // 有符号圆心角 radius chord / (2.0 * Math.Sin(theta / 2.0)); double h radius * Math.Cos(theta / 2.0); // 圆心到弦中点的有符号距离 double mx (p1.X p2.X) / 2.0, my (p1.Y p2.Y) / 2.0; double nx -dy / chord, ny dx / chord; // 弦的左法线 center new Point2D(mx nx * h, my ny * h); sweep theta; return true; }画的时候我不建议用GraphicsPath.AddArc。原因在 4.1 会说:视图矩阵里 Y 轴取了负,AddArc的起止角是按屏幕坐标顺时针算的,套上去之后圆弧的走向会和 DXF 里正好相反,直观看就是「应该凸出去的弧凹了进去」。稳妥做法是在世界坐标里把圆弧离散成 32 段折线,再AddLines——精度够,方向不会错,还省掉一堆角度换算。半径大于 1e5 的大圆弧段数可以按弧长自适应,这属于后面调优的事。3.4 OCS 任意轴算法:为什么有的圆画歪了DXF 里除了世界坐标,还有一套对象坐标系(OCS),由组码210/220/230给出的挤出方向 N 决定。绝大多数二维图纸的 N 就是(0,0,1),可以直接当世界坐标用;但只要 N 不是 Z 轴,点的 X/Y 就得先转换才能画。判别方法是看 N 是否等于(0,0,1),不相等就套任意轴算法:// OcsToWcs —— 把 OCS 下的 (x, y, z) 转成世界坐标 private static void OcsToWcs(ref double x, ref double y, double z, double nx, double ny, double nz) { // 快路绝大多数二维图纸走这里省掉所有叉乘 if (Math.Abs(nx) 1e-12 Math.Abs(ny) 1e-12 nz 0) return; // 1) 归一化挤出方向 double len Math.Sqrt(nx * nx ny * ny nz * nz); nx / len; ny / len; nz / len; // 2) 选参考轴|Nx|、|Ny| 都小于 1/64 用世界 Y否则用世界 Z double ax, ay, az; if (Math.Abs(nx) 1.0 / 64 Math.Abs(ny) 1.0 / 64) { ax nz; ay 0; az -nx; } // (0,1,0) × N else { ax -ny; ay nx; az 0; } // (0,0,1) × N len Math.Sqrt(ax * ax ay * ay az * az); ax / len; ay / len; az / len; // Ax 轴 // 3) Ay N × Ax double bx ny * az - nz * ay; double by nz * ax - nx * az; double bz nx * ay - ny * ax; // 4) 组合成世界坐标 double wx x * ax y * bx z * nx; double wy x * ay y * by z * ny; x wx; y wy; }那个1/64的阈值不是凭感觉定的,是这套算法的原始定义,用来避开 N 接近 Z 轴时叉乘结果退化成零向量的问题。写成别的值(比如1e-6)在大多数图上也能跑,但在 N 恰好接近(0, 0.01, 0.9999)这类值时,叉乘出来的基向量长度会小到接近浮点精度极限,归一化之后方向就飘了,画出来的圆会变成一个歪掉的椭圆。这类问题属于典型的「一万张图里有一张炸」,不写测试很难发现。4. 显示层:GDI 绘制管线、视口变换与交互4.1 世界坐标到屏幕坐标:一个 Matrix 全搞定图纸坐标是数学坐标(Y 向上),屏幕坐标是 Y 向下,这中间必然要有一次翻转。这套源码的做法是把Graphics.Transform设成一个 3×2 的仿射矩阵,之后所有图元都用世界坐标直接画,不用在图元里各自算屏幕坐标。// ViewerControl.cs —— 视图矩阵只由 zoom 和 offset 决定 private float _zoom 1f, _offsetX 0f, _offsetY 0f; private void UpdateTransform(Graphics g) { // m [ zoom, 0, 0, -zoom, offsetX, offsetY ] // 屏幕X 世界X * zoom offsetX // 屏幕Y -世界Y * zoom offsetY ← 负号完成 Y 翻转 g.Transform new Matrix(_zoom, 0f, 0f, -_zoom, _offsetX, _offsetY); }用Matrix的六参数构造函数一次写清楚,而不是连着调Translate、Scale。原因是Matrix.Translate和Scale都是前插(prepend),连续调用时最终生效的顺序和你读代码的顺序是反的,g.TranslateTransform(a); g.ScaleTransform(b);与反过来写的结果完全不同。这个问题调起来很费劲,因为图形不是报错,只是位置不对。直接给矩阵元素赋值,一眼就能看出每个格子干什么。对应的反变换也很简单,鼠标位置换算成世界坐标就靠它:private Point2D ScreenToWorld(Point p) { double wx (p.X - _offsetX) / _zoom; double wy (_offsetY - p.Y) / _zoom; // 注意分子顺序反了 return new Point2D(wx, wy); }初始视图的缩放,拿$EXTMIN/$EXTMAX或实测包围盒的宽高,除以控件客户区宽高,取两个方向里较小的那个,再留 5% 边距;然后把这个包围盒的中心平移到控件中心。这一步算错的表现就是「图是打开了,但在屏幕外」或者「缩成一个点」。4.2 鼠标滚轮缩放与中键平移缩放要围绕鼠标位置做,不然滚两下图纸就跑没影了。思路是:先把鼠标点换算成世界坐标,改完zoom之后再反解offset,让同一个世界点仍然落在鼠标下面。protected override void OnMouseWheel(MouseEventArgs e) { Point2D before ScreenToWorld(e.Location); // 缩放前鼠标下的世界点 double factor e.Delta 0 ? 1.1 : 1.0 / 1.1; _zoom (float)Math.Max(1e-4, Math.Min(1e4, _zoom * factor)); // 反解 offset让 before 仍然落在 e.Location _offsetX (float)(e.X - before.X * _zoom); _offsetY (float)(e.Y before.Y * _zoom); Invalidate(); // 只重绘不重解析 } // 中键平移直接把鼠标位移累加到 offset 上 protected override void OnMouseMove(MouseEventArgs e) { if (_panning) { _offsetX e.X - _lastPt.X; _offsetY e.Y - _lastPt.Y; _lastPt e.Location; Invalidate(); } base.OnMouseMove(e); }_zoom上下限是必须加的。我在一个没做限制的版本上试过,滚轮连滚几十下以后float精度耗尽,整个图形开始抖动、变成一团线——这就是典型的没事先加限位的结果。1e-4 到 1e4 这个区间对工程图纸基本够用。还有一点:Invalidate()就好,别在这两个方法里调Refresh()。Refresh是同步强制重绘,滚轮连续触发时会把消息队列堵塞,手感会变得一顿一顿的。用Invalidate交给系统合并重绘请求,流畅度差别很明显。4.3 双缓冲与画笔复用:大图纸不卡的关键OnPaint里最容易被写坏的两件事:每帧 new 一堆 Pen/Brush,以及每帧重新解析文件。GDI 的Pen、Brush都是非托管句柄的包装,靠终结器回收,高频创建会导致句柄数飙升、GC 压力剧增,表现就是拖动画布时越拖越卡,拖久了直接卡死。控件构造函数里打开双缓冲,这是所有自绘控件的标配:public ViewerControl() { SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint // 忽略 WM_ERASEBKGND减少闪烁 | ControlStyles.OptimizedDoubleBuffer // 双缓冲 | ControlStyles.ResizeRedraw, true); UpdateStyles(); }画笔按「颜色 线宽」做缓存,整个生命周期内复用:// PenCache.cs —— 简单哈希缓存够用 private readonly Dictionaryint, Pen _cache new Dictionaryint, Pen(); public Pen Get(Color color, float width) { int key color.ToArgb() ^ ((int)(width * 100) 1); Pen pen; if (!_cache.TryGetValue(key, out pen)) { pen new Pen(color, width); _cache[key] pen; } return pen; }这里有个 GDI 的冷知识值得记一下:new Pen(color, 0)创建的是宽度为 1 像素的 cosmetic pen,它不受Graphics.Transform缩放影响。这意味着不论你把图纸放大到 100 倍还是缩到 0.01 倍,线始终是 1 像素粗,不会变成一根粗黑条或者细到看不见。整套源码里所有图元都用宽度 0 的画笔,只在选中高亮时才用固定宽度,这个选择很省事。至于「每帧重新解析」,把DxfDocument缓存在控件字段里,只在打开新文件时重建;图层显隐切换只改DxfLayer.Visible,重绘时跳过不可见图层即可。5. 避坑与排查:图纸读不出来时先看这五处5.1 读出乱码或直接抛异常:编码与版本的坑现象一:中文图层名变成一串方块或者问号,英文正常。原因:DXF 的编码跟版本绑定。AC1021(R2007)及以后是 UTF-8,更早的版本用的是本地代码页,简体中文环境就是 GB2312/936。StreamReader默认按 UTF-8 解码,喂给它一份 2004 年的中文图纸,中文全变\uFFFD。解决:先探$ACADVER再决定编码,探不到就按 936 兜底。// 只在文件头部找 $ACADVER再决定用哪个编码打开整份文件 private static Encoding DetectEncoding(string path) { using (var sr new StreamReader(path, Encoding.ASCII)) { for (int i 0; i 400 !sr.EndOfStream; i) // HEADER 就在文件最前面 { string code sr.ReadLine(); string val sr.ReadLine(); if (code ! null code.Trim() 9 val ! null val.Trim() $ACADVER) { sr.ReadLine(); // 组码 1 string ver sr.ReadLine(); // AC1009 / AC1015 / AC1021 ... if (ver ! null string.Compare(ver.Trim(), AC1021, StringComparison.Ordinal) 0) return new UTF8Encoding(false); return Encoding.GetEncoding(936); // 简体中文老图纸 } } } return Encoding.GetEncoding(936); }现象二:解析到某个图元时抛「非法组码」,但用记事本打开看着一切正常。原因:大多数是文件末尾被截断,只剩一个组码行没有值行;或者是导出工具在EOF之后又追加了二进制垃圾。解决:ReadPair里对valueLine null直接返回null结束解析,不要抛异常;同时把EOF当成正常终止条件而不是异常。5.2 能读出来但画错了:坐标与几何的坑现象三:整张图上下颠倒,或者圆心位置偏了一倍半径。原因有两个。一是漏了 Y 翻转,m22写成了正的zoom。二是CIRCLE的圆心在 OCS 里,而挤出方向是(0,0,-1),没走任意轴算法就直接当世界坐标用了,结果圆心被镜像到了另一侧。解决:矩阵里m22 -zoom;所有带210/220/230的图元,在ReadProperty收尾时统一走一遍OcsToWcs。判断标准很简单——先用 AutoCAD 或者任意查看器打开同一份文件,截个图,和你的渲染结果叠一起对比,镜像和偏移一眼就能看出来。现象四:多段线只画出一个点,或者整条线成了一条折线,该凸的地方是平的。原因:前者是顶点被字典覆盖(见 3.3),后者是组码 42 被忽略了。解决:顶点用List按出现顺序追加;42 只在收到 20 之后才回填,顺序不能乱。现象五:圆弧画出来是「补弧」,该凸的凹进去了。原因:在 Y 翻转过的Graphics.Transform下,GraphicsPath.AddArc的起始角和扫掠角是按屏幕坐标的顺时针方向解释的,正好和 DXF 的逆时针相反。解决:世界坐标里离散成折线,别用AddArc。5.3 打开就卡死或者越来越慢:渲染与资源的坑现象六:几十 KB 的小图没问题,几 MB 的图一打开界面就无响应。原因:解析在主线程同步执行,加上OnPaint里每帧 new 画笔。解决:解析放到BackgroundWorker里做,进度回报给状态栏;画笔走缓存;再加上双缓冲。三步做完,同样一份 5MB 的图纸从「打开要等十几秒」变成一秒多出图。现象七:图形边缘有锯齿、缩放时线条忽粗忽细。原因:SmoothingMode没开,或者用了非 0 宽度的画笔。解决:在OnPaint开头设g.SmoothingMode SmoothingMode.AntiAlias,图元画笔统一宽度 0。6. 二次开发:从能看图纸到能干活这套源码最容易上手的三类改造,按投入产出比排:改造点落点文件改动量风险图层过滤 / 按颜色高亮DxfLayer.csOnPaint小,二三十行低新增图元类型(ELLIPSE、SPLINE)Entities/加一个类 EntityFactory一行 case中中,样条要处理控制点和节点向量点选图元、量距离ViewerControl加命中测试中中,命中测试要在世界坐标里做导出 SVG / PDF新写一个渲染器,复用解析层大低,解析层不用动图层过滤最省事,DxfDocument里已经建好了图层字典,OnPaint遍历实体时加一句if (!doc.Layers[e.LayerName].Visible) continue;就行,图层面板勾选后调Invalidate()。新增图元建议先做ELLIPSE(组码10/20中心、11/21长轴端点、40长短轴比、41/42起止参数),比 SPLINE 简单得多,而且能在现有渲染管线里直接离散成折线,和 3.3 的圆弧处理方式一致。SPLINE 要处理40节点向量和10/20控制点、70的次数,有 NURBS 基础再动。命中测试的思路是反着来:别在屏幕坐标里算,把鼠标点用ScreenToWorld换算到世界坐标,再和平移后的图元包围盒比对;精度要求高的话对线段做点到线段距离,阈值取3 / _zoom,这样不管缩放多少,点选的容差在屏幕上始终是 3 像素左右。验证方法我一般用三条:一是拿同一份 DXF 在成熟查看器里和自绘结果各截一张图,叠一起看偏差;二是对已知图元写断言,比如某个 CIRCLE 的圆心和半径读出来必须和文本里10/20/40的值一致,防止后续改动把解析改坏;三是准备一个「脏样本集」——中文图层名、空$EXTMAX、含 HATCH、含(0,0,-1)挤出方向的各一份,每次改完跑一遍,能挡掉绝大部分回归。这套代码结构不复杂,但坑基本都藏在 DXF 的细节里。从那以后我每次接这类老图纸的活,都先把$ACADVER和ENTITIES段头几十行打出来看一眼再动手,省下来的调试时间远比这几分钟多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站