1. 把分割动作搬到Web端先想清楚你要解决的是哪一环先说个真实场景。上周同事发来一个 80MB 的 CAD 总图里面密密麻麻排了两百多个图框从 A0 到 A4 混排甲方要求每个图框单独出一个 DXF再各出一张 PNG 预览。如果按老办法打开桌面软件逐个框选、逐个 WBLOCK 导出一天就搭进去了而且中间只要手抖一次坐标偏了或者漏了个标注返工成本极高。CAD 自动分割这件事本质上是三个动作串起来找到图框边界 → 判断哪些实体属于这个图框 → 把属于它的内容单独输出一份。听起来简单但每一步都有坑图框可能是块参照也可能是闭合多段线还可能画在图层上全靠命名约定实体可能整个落在框内也可能横跨两个框文字和标注的定位点在内、但笔画伸到外面你切还是不切。Web 端做这件事的价值不在于把桌面软件搬到浏览器而在于批量化、可排队、可复用。用户上传一次服务端跑一遍产出整包 ZIP图纸更新了重新跑一次就行不需要谁坐在电脑前点鼠标。本文面向的是已经有一定前端或后端基础、准备落地这套流程的开发者我会把格式解析、图框识别、实体裁剪、导出渲染、性能优化和踩过的坑按实际动手顺序讲一遍。1.1 桌面端分图的常规做法与它的天花板行业里分图的老套路无非几种。最常见的是用 WBLOCK 命令框选一片区域把它写成独立的 DWG 文件讲究一点的会写一段 AutoLISP 脚本遍历图框图层上的多段线算出每个框的角点然后循环调用 WBLOCK。再高级一些的会用 ObjectARX 或者 .NET API 做插件直接读数据库里的块表记录。这些方案在单机、单文件、偶尔用一次的场景下没问题问题出在三个地方。第一是环境依赖重。AutoLISP 要跑在 CAD 里ObjectARX 要匹配 CAD 的版本和位数客户电脑上装的是哪个版本你完全控制不了。第二是并发能力为零。一百个文件就得一百次人工操作任务量一大就只能排队。第三是结果不可追溯。谁在什么时候导了哪几张图出了错怎么定位全靠人记。Web 端方案恰好能补上这三块环境是容器镜像跑在服务端跟用户机器无关任务进队列横向扩几个 Worker 就能并发每一次分割都留日志分割了几张、哪张失败了、失败原因是什么全部可查。这也是为什么最近两年Web 端 CAD 处理这类需求在各行各业都冒出来了——不是因为技术多新而是因为它真的把重复劳动压下去了。1.2 Web 端方案绕不开的三个现实约束不过别把这事想得太顺。浏览器不是 CAD服务端也不是图形工作站做之前得先接受三个约束后面所有的技术选型都要围着它们转。约束一浏览器解析不了 DWG。DWG 是闭源二进制格式各版本结构还在变指望在前端用 JavaScript 完整解析它投入产出比极低。所以主流做法都是把 DWG 在服务端先转成 DXF前端只认 DXF。这一步转换质量直接决定后面所有环节的成败。约束二大图纸在前端渲染会卡死主线程。一个几十万实体的图纸光是遍历一遍计算包围盒就要好几秒再叠加上 Canvas 绘制页面直接白屏。解决方案只有两条路要么 Web Worker 隔离要么干脆把几何计算全放服务端前端只负责展示结果。约束三CAD 的图和 Web 的图不是一回事。CAD 里一个圆弧是数学定义的渲染到 PNG 上就变成了一串像素用户要的图片可能是 150 DPI 的预览图也可能是 600 DPI 要打印的图。DPI、线宽、颜色映射、字体替换每一个都会影响最终观感。把这三条认下来后面才不会走到一半发现方向错了。1.3 什么样的图纸适合一键分割什么样的不适合不是所有图纸都适合自动分割硬上只会得到一堆错图。我见过能跑得很顺的图纸通常满足这几个条件图框在模型空间里且是块参照或者统一图层的闭合多段线图框尺寸规范同一个项目里 A3 就是 420×297、A2 就是 594×420缩放比例一致图层命名有约定比如图框、TK、FRAME、A3-图框这类图纸里没有未绑定的外部参照也没有大量自定义对象。反过来以下几种情况建议先做预处理或者干脆提示用户手工处理图框是随手画的四条直线长短不一还带角度偏差图框在布局图纸空间里模型空间里是一整张图大量使用天正、浩辰等二次开发对象DXF 里变成代理实体一张图里混了好几个比例图框大小不一样但没规律。我的建议是先做能识别多少算多少的保守版本把识别结果可视化给用户确认人工勾掉误识别的框再执行导出。别一上来就追求全自动误识别的代价比多点两下鼠标大得多。2. DXF 与 DWGWeb 端解析必须做的格式取舍格式选型这一步做了决定之后基本就锁死了整个架构所以值得多花点时间。2.1 DXF 的组码结构其实比想象中好读DXF 是 Autodesk 公开的交换格式本质上是组码 值的成对结构。组码是整数表明这个值是什么含义值可能是字符串、整数或者浮点数。比如下面这一段0 LWPOLYLINE 8 图框 90 4 70 1 10 0.0 20 0.0 10 420.0 20 0.0 10 420.0 20 297.0 10 0.0 20 297.00后面跟LWPOLYLINE表示一个轻量多段线实体开始8后面是图层名图框90是顶点数量470是标志位1表示闭合后面的10/20成对出现就是各顶点的 X、Y 坐标。看懂这个结构你就能理解为什么 DXF 文件动辄上百兆——它是纯文本一个坐标要用两行表示。DXF 的整体结构分成若干个段HEADER存全局变量比如$INSUNITS图形单位、$EXTMIN/$EXTMAX图形范围TABLES存图层表、线型表、文字样式表、标注样式表BLOCKS存块定义ENTITIES存模型空间里的实体OBJECTS存字典和扩展数据。做分割时你主要关心HEADER、TABLES、BLOCKS和ENTITIES这四段。需要特别注意的是BLOCKS段。很多人第一次做 DXF 解析时会发现明明图上那么多东西ENTITIES段里只有几十个实体。原因就是绝大多数内容都在块定义里ENTITIES段里只有一堆INSERT引用。所以不做块展开你的包围盒计算一定是错的。2.2 DWG 转 DXF 放在服务端是成本最低的折中DWG 的解析方案我试过三种结论很清楚。第一种是纯开源库比如 LibreDWG。它在很多简单文件上能跑通但遇到高版本 DWG、或者带代理对象的文件成功率就掉得厉害而且它对中文文字样式的还原经常出问题。第二种是商业 SDK解析质量最好但授权费用不低而且一般要绑定硬件或按年付费对个人项目不友好。第三种是用格式转换工具先把 DWG 转成 DXF再走 DXF 流程。这是我最推荐的因为转换这一步是成熟工具在做你只需要保证转换参数设置对了输出 DXF 版本建议选R2000或R2004太新的版本会让下游解析器兼容性变差要勾选包含外部参照内容或者提前绑定外部参照如果能选尽量让转换器把字体和自定义对象做一次展开。工程上的做法是把转换工具封装成一个独立的服务输入 DWG 输出 DXF前面加个队列超时设成 60 秒左右。转换失败的图直接标记出来告诉用户这张图转换失败请检查是否有未绑定的外部参照而不是让整个任务卡死。2.3 解析器选型前端 dxf-parser 与服务端 ezdxf 的分工JavaScript 生态里dxf-parser是使用最广的一个parseSync一把梭返回的对象里有header、tables、blocks、entities四个主要字段。实体对象上会带type、layer多段线有vertices数组每个点带x、y、bulge圆有center和radius块引用有name、position、xScale、rotation。用来做快速预览和小图纸处理完全够用。但如果要做精确裁剪和 DXF 重写我建议把主战场放在服务端用 Python 的ezdxf。原因有三一是它能正确解析和写出完整的 DXF 结构包括表段和块段的资源依赖二是它自带bbox模块能算精确包围盒含圆弧、样条的真实外框不是简单取控制点三是它的Importer扩展能从源文档把实体连同图层、线型、文字样式一起导入到新文档这正好是导出子图需要的。一个典型的分工是这样import ezdxf from ezdxf import bbox doc ezdxf.readfile(input.dxf) msp doc.modelspace() for insert in msp.query(INSERT): name insert.dxf.name ip insert.dxf.insert rot insert.dxf.rotation print(name, ip.x, ip.y, rot) cache bbox.Cache() for e in msp: ext bbox.extents([e], cachecache) if ext.has_data: print(e.dxftype(), ext.extmin, ext.extmax)前端dxf-parser负责快速看一眼服务端ezdxf负责精确算和精确写两边各司其职比强行让前端干所有事靠谱得多。2.4 一次实测的性能基线为了让后面的优化有参照我先给一组我这边测出来的基线数据环境是 4 核 8G 的容器Python 3.11 ezdxf图纸规模文件大小解析耗时算包围盒耗时分割成 100 张出 100 张 PNG150DPI小型约 2 万实体6 MB0.8 s0.4 s1.2 s9 s中型约 15 万实体42 MB6 s4.5 s7 s38 s大型约 60 万实体180 MB26 s31 s25 s150 s可以看出两个规律解析和包围盒计算跟实体数基本线性相关而渲染出图的耗时增长更快因为它不仅跟实体数有关还跟输出分辨率有关。所以如果你的场景只需要 DXF 子图不需要图片整体可以控制在一分钟内如果要出图就必须考虑异步队列和进度反馈。3. 图框识别自动分割的成败全在这一步分割的准确率90% 取决于图框识别准不准。识别错了后面裁剪得再精细也是白搭。3.1 图纸里图框的三种存在形式我在实际项目里遇到过的图框基本可以归成三类。第一类是块参照。这是最理想的情况。设计院一般会有标准图框库A0 到 A4 各一个块块名可能是A3、GB-A3、图框-A3横、TK_A3之类块定义里包含了内外边框、标题栏、会签栏。在模型空间里每一个图就是这个块的一次 INSERT带插入点和旋转角。第二类是闭合多段线。有些单位不用块直接在图框或0图层上画一个 420×297 的闭合矩形偶尔还会用带凸度bulge的多段线做成圆角矩形。这种没有块名可以依赖只能靠几何特征识别。第三类是四条独立直线拼出来的矩形。这是最难处理的一类因为四条线之间没有任何关联你需要自己做矩形的拓扑重建找出所有水平和垂直线段两两配对看能不能组成一个封闭矩形。3.2 以块参照为锚点的主识别链路块参照这条路最稳我一般把它作为首选策略流程是这样的。第一步收集所有 INSERT 的块名集合统计每个块名出现的次数。出现次数多、且块定义里有矩形外框的大概率就是图框块。第二步计算每个 INSERT 的包围盒。这里有个坑不能只拿插入点当包围盒要真正展开块定义里的所有实体来算。ezdxf里对块引用调bbox.extents是可以拿到实际外框的但要确保块定义存在外部参照的块定义可能缺失。第三步用面积和长宽比做过滤。图框的包围盒面积通常在图形总面积的 1% 到 20% 之间长宽比接近标准图幅比例A3 是 1.414A4 是 1.414A2 也是 1.414A1 也是国内图纸标准图幅基本都是 √2 比例。如果算出来长宽比是 3.7那多半是把两个相邻的图框误当一个了或者是个长条形的表格块。第四步用块名做正则匹配加分。像图框、TK、FRAME、BORDER、A[0-4]、GB、标题栏这些关键词命中就给高分。我一般设个阈值得分高的自动确认得分居中的交给用户勾选。一个实用的正则大概是这样的import re FRAME_PATTERN re.compile( r(图框|图签|标题栏|边框|frame|border|tk[_\-]?|^a[0-4]|gb[_\-]?), re.IGNORECASE, ) def score_frame_block(name: str, width: float, height: float) - float: score 0.0 if FRAME_PATTERN.search(name): score 0.5 ratio max(width, height) / max(min(width, height), 1e-6) # 标准图幅是 1.414给个容差 if 1.30 ratio 1.55: score 0.3 if width * height 1000: score 0.2 return score这套打分逻辑看着土但在实际项目里的准确率比很多复杂算法都高因为图框这个东西本身就有很强的命名和尺寸规律。3.3 闭合多段线与图层命名的兜底方案块参照识别不出来的时候就走几何识别。核心思路是找出所有闭合的、近似矩形的多段线然后在里面筛。具体做法是遍历所有LWPOLYLINE和POLYLINE先看70标志位的闭合位有没有置位LWPOLYLINE是 bit 1POLYLINE也是 bit 1。闭合之后再数顶点刚好 4 个顶点的计算相邻边的夹角四个角都在 90 度 ±2 度范围内就认定是矩形。顶点多于 4 个的用最小外接矩形的思路处理看外接矩形面积和实际多边形面积的比值接近 1 才算矩形。图层命名可以作为一个强信号。如果图纸里有独立的图框图层那基本可以直接锁定如果没有但所有候选矩形都在0图层上那就只能靠几何特征。还有一类特殊情况需要处理图框是嵌套的。也就是说块 A 里包含块 B块 B 才是图框本体。这时候需要递归展开到最后一层或者直接算最外层 INSERT 的包围盒用最外层的结果当图框边界。经验上用最外层包围盒更符合人的直觉。3.4 识别结果去重、排序与人工确认识别出来的候选框通常会有重复和嵌套需要做一轮清洗。去重两个框如果 IoU交并比超过 0.85就认为是同一个框保留面积大的那个。IoU 的计算很简单交集面积除以并集面积。剔除嵌套如果框 A 完全被框 B 包住且 A 的面积小于 B 的 60%那就说明 A 是标题栏或者明细表这类内部框不是独立图框要剔掉。排序排序规则直接影响导出的文件序号好的排序能让用户一眼看懂。我的做法是先按行、再按列先把所有框按 Y 坐标框中心点分层Y 差值小于框高度的 50% 归为同一行每行内按 X 坐标升序。这样导出的顺序就是从左到右、从上到下跟人看图的顺序一致。人工确认这一步别省。把识别结果用 SVG 或者 Canvas 画出来让用户在页面上看到每个框的位置和编号能勾选、能删除、能手动补框。用户确认之后再进队列执行。我做过对比加了这一步之后因为识别错误导致的投诉下降了八成以上。4. 实体裁剪跨边界内容怎么切才不丢东西图框定下来之后接下来要决定每个实体归属哪个框以及跨框的实体怎么处理。4.1 只按坐标范围过滤是最常见的错误新手最容易写的代码是这样的def belongs_to_frame(entity, frame_bbox): ext bbox.extents([entity]) return frame_bbox.contains(ext.extmin) and frame_bbox.contains(ext.extmax)这段代码的逻辑是实体的包围盒完全落在图框内才要。它的问题在于过于保守一条从框内画到框外的引线整条就被丢掉了一段文字只要稍微超出边框一点也丢了。结果就是导出的子图缺胳膊少腿。另一个极端是只要沾边就要def belongs_to_frame(entity, frame_bbox): ext bbox.extents([entity]) return frame_bbox.intersects(ext)这个又太宽松一个横跨三个图框的长管线会被三个框各复制一份造成大量冗余而且子图看起来乱七八糟。正确的是分类处理简单几何按精确裁剪切复杂实体按锚点归属整块搬具体怎么分下面详细说。4.2 按实体类型分而治之的裁剪策略我把实体分成三类策略完全不同。第一类可精确裁剪的线段类。包括LINE、LWPOLYLINE、POLYLINE、ARC、CIRCLE、SPLINE、ELLIPSE。这类可以用几何算法切。LINE最简单用 Liang-Barsky 算法对矩形边界做裁剪保留框内那一段。如果是旋转的图框先把线段变换到图框局部坐标系裁剪完再变回来。LWPOLYLINE麻烦一些因为它可能带凸度bulge凸度不为零的段其实是圆弧。稳妥的做法是先把整条多段线离散成密集的线段序列裁剪完之后如果原来某段是圆弧且完整保留可以尝试还原成圆弧如果被切断了就保留成线段序列。我知道这会损失一点精度但在自动分割场景下保形比保数学精度更重要。CIRCLE和ARC的判断要分情况完全在框内的原样保留完全在框外的丢弃跟边界相交的算出圆与矩形四条边的交点把圆弧按交点拆成若干段圆弧只保留在框内的部分。这一步用参数角度来做比较省事把圆上所有交点换算成角度排序之后逐段判断中点是否在框内。SPLINE和ELLIPSE建议一律先转成多段线再裁剪因为保留原样去裁剪需要解方程投入太大。第二类整块搬运类。包括TEXT、MTEXT、DIMENSION、ATTRIB、INSERT、HATCH。这类不做几何裁剪按锚点归属整块保留或者整块丢弃。TEXT的锚点是插入点组码 10/20MTEXT也是INSERT是插入点DIMENSION用定义点组码 10/20或者文字中点组码 11/21都行。判断逻辑是锚点落在图框内可以给图框适当外扩一点比如外扩图框尺寸的 1%就保留否则丢弃。这么做的理由很实际文字被拦腰截断比稍微超出边框难看得多而且用户还得手动补字。标注被拆散更是灾难尺寸线断了、尺寸值跑了等于没标。HATCH的归属判断用边界路径的包围盒中心点落在框内就保留。填充一般不会横跨图框所以按中心点判断足够。第三类需要展开类。就是INSERT。这里要做一个选择是整块搬运还是展开成基本实体再裁剪整块搬运的前提是这个块引用完全落在一个图框内。如果块引用跨了两个图框整块搬运会导致两个子图里都有完整内容冗余。这时候应该调virtual_entities()把它展开成基本实体再走第一类的裁剪逻辑。我的默认策略是先算块引用的包围盒如果完全落在一个图框内整块搬如果跨框展开后再裁。这样既避免了无谓的展开开销又能正确处理跨框情况。当然展开会丢失参数化信息比如标注块的关联性所以要在结果里注明该块引用因跨图框被展开。4.3 文字、标注、填充、块参照的特殊照顾除了上面说的锚点归属还有几个细节要照顾。文字样式依赖。搬运文字的时候文字引用的STYLE必须一起搬走否则目标文件里找不到字体文字会变成默认字体宽度和高度全变。用ezdxf的Importer能自动处理这个依赖收集。标注样式依赖。DIMENSION引用的DIMSTYLE也要一起搬。标注样式缺失会导致标注的箭头、文字位置、公差全套错乱。填充的边界。HATCH的边界如果是关联边界associative会引用其他实体如果不是填充自己带着边界路径。导出时优先选非关联填充把边界路径直接烘焙进填充实体里避免引用丢失。块定义的去重。如果多个图框里都有同一个块引用的实例导出多个子图时每个子图都要带上这个块定义。别嫌文件大块定义丢了的代价比文件大得多。4.4 旋转图框的局部坐标系变换图框不一定都是水平放置的尤其是总图里的分图经常旋转 90 度甚至任意角度。这时候不能直接在全局坐标系里裁剪。思路是构造一个从图框局部坐标系到全局坐标系的变换矩阵然后所有裁剪都在局部坐标系里做。具体步骤求图框的变换平移量(tx, ty)是图框左下角在全局的坐标旋转角θ是图框相对水平方向的旋转角缩放s通常为 1。对每个待处理实体先用逆变换把它的坐标从全局映射到局部得到一个摆正的版本。在局部坐标系里图框就是一个从(0,0)到(W,H)的轴对齐矩形所有裁剪算法都可以简化成轴对齐矩形裁剪。裁剪完之后再用正向变换把保留的实体变换回全局坐标系。这一步的好处是能把任意四边形裁剪这个复杂问题降维成矩形裁剪这个简单问题。代价是多两次坐标变换但坐标变换是纯矩阵运算几乎不耗时。需要提醒的是变换要作用到实体上所有的坐标字段不只是主坐标。比如LINE有起点和终点两组坐标ARC除了圆心还有起止角旋转之后起止角要重新计算因为局部坐标系下圆弧可能变成椭圆弧。这也是为什么我在前面建议圆弧和样条统一离散成多段线——离散之后坐标变换就只是逐点变换不用考虑角度重算。5. 导出子图与图片矢量与像素的两条产线裁剪逻辑跑通之后输出环节有两种产物可编辑的 DXF 子图和用于预览或打印的图片。这两条产线的技术栈完全不同。5.1 导出 DXF 子图时必须一并带走的资源很多人第一次导出子图打开一看图形都在但全是白线图层没了线型也变成实线了。原因就是只搬了实体没搬资源。一个完整的 DXF 子图至少要包含这些东西资源类型对应表段缺失后果图层定义TABLES / LAYER实体颜色、线宽、开关状态全部丢失线型定义TABLES / LTYPE虚线变实线中心线看不出来文字样式TABLES / STYLE文字字体替换宽度位置全错标注样式TABLES / DIMSTYLE标注箭头、公差、文字位置错乱块定义BLOCKS块引用变成空框或者直接消失多线样式OBJECTS多线渲染异常单位设置HEADER / $INSUNITS打开时单位不对测量值差一千倍手工搬这些资源非常容易漏所以强烈建议用现成的导入工具。ezdxf的Importer就是干这个的import ezdxf from ezdxf.addons import Importer src ezdxf.readfile(input.dxf) tgt ezdxf.new(dxfversionsrc.dxfversion) # 把单位设置也带过去 tgt.header[$INSUNITS] src.header.get($INSUNITS, 4) importer Importer(src, tgt) importer.import_entities(entities_of_this_frame) importer.finalize() tgt.saveas(sub_001.dxf)finalize()这一步会自动补齐所有被引用到的图层、线型、样式和块定义。用这个接口之后我的子图打开正确率从六成提到了接近百分之百。另外一个小技巧导出前把$EXTMIN和$EXTMAX更新成当前子图的实际范围。否则用户打开文件后按缩放到范围会缩放到原图的范围看到一个很小很小的图形缩在角落。5.2 PNG/PDF 渲染的两种路线与 DPI 计算出图有两条路线各有取舍。服务端渲染在服务器上把 DXF 画成位图或 PDF。Python 生态里可以用ezdxf的绘图扩展配合 Matplotlib 后端或者用其他图形库自己画。优点是结果一致、不依赖用户设备、可以批量缺点是服务器要装字体、要处理并发而且渲染大图很吃内存。前端渲染把几何数据传到浏览器用 Canvas 或者 WebGL 画。可以用dxf-viewer这类基于 three.js 的库也可以自己写 Canvas 渲染器。优点是快、不需要服务端资源缺点是不同的浏览器字体渲染不一样导出高清图时可能糊。我的建议是预览用前端正式出图用服务端。用户上传后立刻在前端渲染一个低精度预览让他能确认识别结果确认之后由服务端按指定 DPI 出正式图。DPI 和像素尺寸的换算必须搞清楚否则用户要 A3 300 DPI你给出来的是 96 DPI 的糊图肯定被投诉。换算公式是像素宽 图纸宽(mm) / 25.4 * DPI 像素高 图纸高(mm) / 25.4 * DPI按这个公式算几个常用值图幅尺寸(mm)150 DPI300 DPI600 DPIA4 横297 × 2101754 × 12403508 × 24807016 × 4961A3 横420 × 2972480 × 17544961 × 35089921 × 7016A2 横594 × 4203508 × 24807016 × 496114031 × 9921A1 横841 × 5944967 × 35089933 × 701619866 × 14031可以看到 A1 的 600 DPI 已经是两亿像素级别单张图的内存占用超过 700MB按 RGBA 四通道算。所以出图这一环必须限制最大像素数超过阈值就自动降级到 300 DPI 或者 150 DPI并在结果里标注因尺寸过大已自动降级。线宽的换算也要注意。DXF 里线宽单位是 0.01 毫米画到图上时换算成像素是像素线宽 线宽(mm) * DPI / 25.4一条 0.25mm 的粗线在 300 DPI 下是 0.25 × 300 / 25.4 ≈ 2.95 像素得向上取整到 3 像素才不至于断线。小于 1 像素的线要强制画成 1 像素否则细线会在缩放时消失。5.3 批量打包、命名与结果自检导出多个文件之后命名和打包直接影响下游使用体验。命名规则我一般用序号 图号 图名的组合。图号和图名从标题栏里取这是关键技巧标题栏通常是个块引用里面的图号、图名是ATTRIB属性。可以这样提取def extract_title_info(insert): info {} for att in insert.attribs: tag att.dxf.tag.strip().upper() text att.dxf.text.strip() if tag in (图号, DWGNO, DRAWINGNO, 图纸编号): info[no] text elif tag in (图名, DWGNAME, TITLE, 图纸名称): info[name] text return info拿到图号之后文件名就是01_A-101_一层平面图.dxf这种格式用户拿到压缩包一看就知道哪张是哪张。如果标题栏里提取不到就退化成01_sub.dxf这种纯序号命名。打包用 ZIP里面分两个目录output.zip ├── dxf/ │ ├── 01_A-101_一层平面图.dxf │ └── 02_A-102_二层平面图.dxf ├── png/ │ ├── 01_A-101_一层平面图.png │ └── 02_A-102_二层平面图.png └── report.jsonreport.json里记录每个图框的编号、图号、图名、实体数量、是否识别到标题栏、导出是否成功、失败原因。这个文件在排查问题时非常有用。自检这一步很多人不做但强烈建议做。自检项包括每个子图里实体数是否大于零为零说明裁剪逻辑出问题了、子图包围盒是否在图框范围附近差太远说明裁剪没生效、标题栏块是否在子图里缺了说明块定义没带过去。任何一项不通过就在报告里标红提示用户复核。6. 大图纸的性能账从解析到渲染的耗时分布前面给过一份基线数据这一节讲讲怎么把它优化下来。6.1 Web Worker 与分片解析如果一定要在前端做解析务必放进 Web Worker。原因是dxf-parser的解析是 CPU 密集型的主线程被占住之后页面完全没法交互用户会以为程序死了。// worker.js import DxfParser from dxf-parser; self.onmessage (e) { const parser new DxfParser(); try { const dxf parser.parseSync(e.data.text); self.postMessage({ ok: true, dxf }); } catch (err) { self.postMessage({ ok: false, error: err.message }); } };但说实话超过 20MB 的 DXF即使放进 Worker解析时间也会到十几秒用户体验依然不好。所以我的实际做法是前端只上传文件解析和计算全部服务端做前端通过接口拿几何数据的精简版本做预览。预览数据可以只传图框位置和一层粗略的线框几万个点用二进制格式传输浏览器拿到就能画秒级响应。6.2 空间索引让实体归属判断从 O(n·m) 降下来假设图上有 20 万个实体和 200 个图框如果对每个图框都遍历一遍所有实体就是 4000 万次包围盒相交测试Python 里大概要跑几十秒。优化方法是建空间索引。最简单的做法是网格划分把整个图纸范围均分成网格每个实体按其包围盒覆盖的网格登记进去。查询某个图框时只取它覆盖到的网格里的实体集合再做精确判断。也可以用 R 树。Python 里可以用rtree或者shapely的STRtree把每个实体的包围盒插入索引查询时用图框的范围去查返回候选集。百万级实体的场景下这个优化能把耗时从几十秒压到一两秒。这里有个细节要注意一个实体可能跨越多个网格所以要在所有覆盖的网格里都登记查询时要对候选集去重别同一个实体处理两遍。6.3 服务端队列、超时与失败重试服务端的执行链路建议做成异步任务。用户上传之后立刻返回一个任务 ID前端轮询状态。任务状态机大概是排队中 → 解析中 → 识别图框中 → 待确认 → 分割中 → 出图中 → 打包中 → 完成任何一步出错就进失败带上错误码。超时设置要分层转换 60 秒解析 120 秒单张出图 30 秒整体任务 30 分钟。某一层超时就终止并报错别让任务永远挂着。失败重试只对转换和出图这两个步骤做最多重试两次且是退避重试。解析失败和识别失败一般不重试因为原因是数据本身的问题重试也是一样的结果直接告诉用户原因更有用。还有一个容易被忽略的点中间产物要及时清理。转换出的 DXF、中间 JSON、临时图片都要设过期时间否则磁盘很快就被撑爆。我一般设 24 小时任务完成之后就删同时保留report.json一段时间用于排查。7. 踩坑实录字体、外部参照与国产 CAD 自定义对象这一节讲几个我在实际项目里踩过的坑都是那种文档上不会写、但遇上了要折腾半天的。7.1 SHX 字体缺失导致的文字错位国内图纸大量使用 SHX 字体最常见的是txt.shx、simplex.shx、hztxt.shx、tssdeng.shx这几个。问题是这些字体文件通常装在用户的 CAD 里服务器上没有。缺失的后果不是文字看不见这么简单而是文字宽度计算错误导致位置偏移。因为 SHX 字体的字宽跟系统字体完全不同服务端渲染时如果找不到 SHX就会 fallback 到某个系统字体算出来的宽度可能差一倍标题栏里的文字就会溢出框外或者挤成一团。解决办法有几条按优先级排让用户在上传前用 CAD 自带的电子传递功能打包字体或者在设置里加上把 SHX 一起上传的入口在服务端预置一套常见的 SHX 字体集合覆盖大部分项目如果还是找不到就在渲染前做一次字体替换映射把找不到的 SHX 映射到形态接近的字体同时按原字体记录的宽度因子做缩放补偿在结果报告里明确列出以下字体未找到已使用替代字体位置可能有偏差。第 3 条的关键是宽度补偿。DXF 里文字实体的组码 41 是宽度因子替换字体后要重新计算需要的宽度因子让文字的实际宽度尽量接近原值。具体算法是先按替代字体的度量算出原始文字宽度再除以目标宽度得到补偿系数。7.2 外部参照与嵌套块的内容消失外部参照XREF是另一个高频坑。DXF 的BLOCKS段里只有本文件定义的块外部参照的块定义是空的只留个名字。你解析的时候会发现有一堆 INSERT 指向不存在的块渲染出来就是一片空白。自动检测的方法很简单遍历所有 INSERT看它的块名在blocks里有没有对应定义。没有的就是外部参照或者损坏的块。处理方式分两种。如果用户上传的是单个文件检测到外部参照就直接提示该图纸包含 N 个未绑定的外部参照请先在 CAD 中用 XREF 命令绑定后再上传。如果支持多文件上传就尝试把同名文件自动关联起来把外部参照的内容读进来当普通块处理。嵌套块的问题相对简单但也要注意块 A 引用块 B块 B 引用块 C。做包围盒计算时如果只展开一层算出来的范围会偏小。正确做法是递归展开到最底层或者直接用库提供的递归包围盒计算。同时要注意循环引用的保护虽然正常图纸不会出现但损坏的文件里见过递归不设深度上限会直接栈溢出。7.3 天正等自定义对象的处理姿势国内设计院大量使用二次开发软件这些软件创建的图形对象是自定义类型。导出成 DXF 之后它们会变成ACAD_PROXY_ENTITY代理实体或者在低版本 DXF 里被降级成匿名块名字类似*U12加上一堆代理图形。代理实体最大的问题是信息不完整。它可能只保留了外轮廓的近似线条也可能完全丢失取决于导出时有没有保留代理图形。这时候你算包围盒是算不准的分割出来的图也可能缺内容。最实用的解决办法是让用户在导出前做一次图形导出。主流二次开发软件基本都提供这个命令作用是把自定义对象分解成标准的基本实体墙变成多段线门窗变成块标注变成普通标注。这一步做完之后DXF 里就全是标准实体了后面所有流程都顺畅。如果没法要求用户操作那至少要在检测到代理实体时给出明确警告并在报告里统计代理实体的数量。别默默处理然后给用户一堆残图那才是最麻烦的。顺带提一个相关的坑匿名块爆炸。有些图纸里有成千上万个*U开头的匿名块每个都是几根线。这些块会让BLOCKS段膨胀好几倍解析时间和内存占用都上去了。可以在解析前做一次匿名块合并把同名的匿名块实例归并或者干脆展开成基本实体。7.4 单位、比例与标注样式最后讲一个最容易被忽略、但影响最大的问题单位和比例。DXF 的$INSUNITS变量定义图形单位值 0 是无单位1 是英寸4 是毫米6 是米。国内图纸绝大多数是 4毫米但确实遇到过填 0 或者填 1 的。如果按毫米去解读一张实际是英寸的图所有尺寸都差 25.4 倍。比例问题更微妙。建筑图纸常用 1:100 出图意思是模型空间里 1 个绘图单位对应实际 1 毫米但打印时缩小 100 倍。也就是说一个 A3 图框在模型空间里的实际尺寸是 42000 × 29700 个绘图单位。如果你按 420 × 297 去找图框什么都找不到。所以图框识别的判断标准不能写死成宽 420 高 297而应该是长宽比接近 1.414且尺寸在合理解析度范围内。判断图框尺寸是否合理可以看它跟图纸整体范围的比例一个图框的面积在总图形范围的千分之一到百分之一之间通常是合理的。出图的时候缩放比例要反着算。先算出图框在模型空间的宽度比如 42000再算输出图纸的物理宽度比如 A3 是 420mm缩放系数就是 420 / 42000 0.01。所有几何坐标乘上这个系数就映射到图纸坐标系了然后按 DPI 换算成像素。标注样式里的比例也要注意。标注的DIMSCALE变量控制标注元素的整体缩放如果这个值跟图框比例不匹配标注会显示得特别大或者特别小。导出子图时把这个变量一起带过去能避免大部分显示异常。我在实际项目里养成了一个习惯每个任务开始前先做一次图纸体检把单位、比例、图框尺寸、代理实体数量、外部参照数量这几个关键指标打出来输出到报告里。用户看到这份体检报告对后面能不能分割成功心里就有数了我这边排查问题时看报告就能定位到八九成的原因。后来再分享一个小技巧如果你不确定图框识别阈值设多少合适可以先跑一次不做过滤的识别把所有候选框的尺寸和长宽比打出来看看分布再回头定阈值这比拍脑袋定一个数字靠谱得多。