做结构设计的朋友大概率都遇到过这种场面一个总平面图里塞了几十甚至上百个标准图框甲方要的是每个图框单独一张图还得是能直接发出去的 PNG 或者 PDF。手工在 CAD 里一个个框选、复制、粘贴、另存一百张图能耗掉一整个下午眼睛都看花。这套Web端把 CAD 图自动分割成多张图纸并导出子图或图片的东西就是冲着这个场景来的——浏览器里打开、上传、点一下、批量下载。它解决的核心问题不是怎么画图而是怎么把一张大图按图框边界拆干净并且保证拆出来的每张图坐标不乱、比例不变、线宽正常。适合做工程制图、市政道路、建筑电气、机械装配这些经常要交付图纸套件的人也适合想在自己系统里集成图纸处理能力的前端或全栈开发者。1. 需求拆解与技术方案选型1.1 先搞清楚自动分割到底分的是什么很多人一上来就问能不能按图层拆其实真正干活的时候图纸分割的第一判据几乎永远是图框而不是图层。原因是设计交付是有硬性规范的每张正式图纸必须有一个图框图框里有标题栏、图号、比例、签字栏。你要交付 N 张图本质上就是交付 N 个图框以及图框内属于这张图的全部图元。图层拆分只能算辅助手段因为一张图里必然共用大量图层比如标注中心线轮廓这些图层会在所有图框里都出现按图层拆只会把图拆得乱七八糟。所以这里要先把分割这个词翻译成可执行的规则遍历整张图的所有图元对每个图元判断它的空间位置落在哪个图框矩形范围内落在谁里面就归谁。图元完全在框内的直接归入跨框的比如一条路径线横穿好几个图框就要做特殊处理要么整体归到主图框要么按框裁切。这套逻辑听起来简单但实际工程图纸里各种奇葩情况很多后面第 3 章会挨个拆。还有一个容易被忽略的点图框本身也是一个图元集合它是用直线画的、还是用多段线画的、还是做成块BLOCK反复引用的这直接决定识别算法的写法。我在实际项目里统计过用闭合 LWPOLYLINE轻量多段线画的图框占六成以上用块引用INSERT的占三成剩下的一成是用四条独立 LINE 拼出来的——最后这一种最麻烦因为四条线之间没有任何它们是一伙的的显式关联。1.2 为什么优先啃 DXF 而不是 DWG这是绕不开的技术选型我踩过坑所以讲得直白一点。DWG 是闭源二进制格式官方 SDK 是 C 的想在浏览器里直接解析要么后端起一个转换服务重量级、并发差、有授权成本要么用一些逆向出来的解析库版本兼容性一言难尽R2018 之后的格式经常读一半就崩。DXF 则是文本格式虽然是 AutoLISP 风格的组码堆砌但结构清晰、可读、可裁切、可 grep浏览器里用 JavaScript 逐行解析完全可行。实操上更稳的做法是上传 DWG 时后端先用开源转换工具或 CAD 内核转成 DXFASCII 版不要二进制 DXF前端只负责解析 DXF。这样前端逻辑干净后端也只需要处理格式转换这一件事。如果你只想做个纯前端的小工具那就引导用户自己另存为 DXF体验上多一步但工程上最省事。提示转 DXF 时一定要选 ASCII 而不是 Binary二进制 DXF 的组码是紧凑排布的用文本方式读会直接乱码而且文件头信息也不一样。1.3 前端渲染Canvas 2D、SVG 还是 WebGL我三个方案都写过 demo结论是万级图元用 Canvas 2D十万级以上用 WebGLSVG 只适合预览小图。Canvas 2D 的优势是绘制路径快、toDataURL和toBlob直接就能导出图片整个链路最短。缺点是图元一多每帧重绘会很吃力而且它不保留图元对象想改成点一下选中某条线这种交互就得自己维护命中检测。SVG 的优势是元素即对象、CSS 可样式化、导出矢量天然友好缺点是 DOM 节点数量一上去浏览器直接卡死一万个path就能让滚动发涩。WebGL或者基于 Three.js 的方案适合海量图元但它本质是画像素导出高清图要靠离屏渲染加大画布内存占用陡增而且填充图案、线型虚线点划线的处理比 Canvas 2D 麻烦不少。我的实际选择是解析和分割用纯 JS 数据结构渲染预览用 Canvas 2D导出高清图时临时抬高 canvas 尺寸重绘一次。这套组合在普通办公电脑上跑五万图元的图纸很轻松。方案适合图元量导出图片交互成本线型/填充支持Canvas 2D1万~10万极简toBlob需自建命中检测好SVG1千以内矢量友好原生事件好WebGL10万以上需离屏渲染需自建拾取一般1.4 整体架构分层我把整个系统拆成四层每一层职责单一方便单独调试解析层读 DXF 文本产出标准化的图元对象数组{type, layer, coords, ...}。索引层计算整图包围盒识别所有图框矩形建立图元 → 图框的归属表。渲染层给定一个图框算出视图变换矩阵把该框内的图元画到指定尺寸的画布上。导出层把画布转成 Blob收集所有子图 Blob打包成 zip 一次性下载。这么分层最大的好处是分割算法出问题时你只需要看索引层渲染变形时只需要看渲染层的坐标映射排查范围一下子就窄了。2. CAD图元解析从文本堆到可操作对象2.1 DXF文件结构速览DXF 文件的组织方式是组码 值成对出现每行一个组码下一行是它的值交替往下排。比如0 LWPOLYLINE 8 图框 90 4 70 1 10 0.0 20 0.0 10 420.0 20 0.0 ...这段的意思是实体类型是 LWPOLYLINE所在图层是图框顶点数 4闭合标志 1闭合后面跟着四组顶点坐标。整个文件按段SECTION划分常见的有 HEADER全局变量含$EXTMIN、$EXTMAX整图范围、TABLES图层表、线型表、字体样式表、BLOCKS块定义、ENTITIES实体数据最关键、OBJECTS。我们真正要处理的是 ENTITIES 段但 BLOCKS 段也不能跳过——因为 INSERT 引用的块内容都定义在那里处理 INSERT 时必须回头去 BLOCKS 里取出子图元套上缩放和平移再做一遍坐标变换。2.2 必须吃透的几个核心图元不用支持 DXF 全部实体抓住下面这几个就能覆盖 95% 的工程图LINE组码 10/20 起点11/21 终点。LWPOLYLINE组码 90 是顶点数70 的 bit1 表示是否闭合10/20 成对出现顶点坐标可选 42 是凸度圆弧段。CIRCLE / ARC10/20 圆心40 半径ARC 额外有 50 起始角、51 终止角。TEXT / MTEXT10/20 插入点40 字高1 是文本内容MTEXT 的内容可能跨多个 3 组码需要拼接并处理转义。INSERT2 是块名10/20 插入点41/42/43 是三轴缩放50 是旋转角。HATCH填充边界由多个 path 组成处理起来最烦可以先支持基本填充复杂图案后续再说。解析的时候我建议自己手写一个状态机别指望通用 JSON 库。核心就一个循环读一行组码再把下一行读成值塞进当前实体的属性表遇到组码 0 就说明上一个实体结束了落盘、开新实体。这套写法代码不到两百行但可控性远好于一堆第三方库混用。2.3 用 Web Worker 把解析任务请出主线程我拿真实文件测过一个 28MB 的 ASCII DXF实体数量大约 11 万在主线程里跑解析页面会完全冻结 9 到 14 秒用户以为卡死了开始狂点甚至直接关页面。放到 Web Worker 里之后解析时间基本没变大约 7 到 12 秒但主线程全程保持流畅你可以正常显示进度条。Worker 里传数据要注意传输开销。把解析结果当普通对象postMessage传回去时浏览器会做结构化克隆几十万个对象拷一遍也要一两秒。更省事的是把结果打成Float32Array这种类型化数组用 transferable 转移所有权近乎零拷贝// 主线程 const worker new Worker(./dxf-parser.worker.js); worker.postMessage({ fileText }); worker.onmessage (e) { const { positions, meta } e.data; // positions 是 Float32Array转移过来的 buildEntities(positions, meta); }; // worker 内部 const positions new Float32Array(totalNums); // ... 填充数据 self.postMessage({ positions, meta }, [positions.buffer]);提示大文件别一次性await file.text()再传给 Worker那样字符串会在主线程里先驻留一份内存直接翻倍。用file.slice()分片每片转成 ArrayBuffer 转移过去Worker 里再用TextDecoder解码拼接峰值内存能降三四成。2.4 坐标系的坑从解析阶段就要埋好CAD 世界坐标是 Y 轴向上的屏幕画布是 Y 轴向下的这两个坐标系如果一开始不统一后面渲染、分割、导出会一路错到底。我的做法是在解析完成后立刻做一次归一化算出整图包围盒[minX, minY, maxX, maxY]把所有图元坐标平移成以包围盒左下角为原点同时保留一个全局的变换参数供导出时反算。还有 Z 值的问题。三维图纸里图元有标高同一个 x/y 位置可能对应多个 z。二维交付场景下我会按 z 值分层只取数量最多的那一层作为主平面其他层的图元在导图时按用户选项决定是否投影叠加——这一步不做的话你可能会看到图框里莫名其妙多出一些重影线。3. 图纸自动分割的四种策略与实现3.1 图框识别把矩形从一堆线里挑出来图框识别的准确率直接决定整套工具的可用性我前后改过三版算法最后稳定下来的是多路召回 打分排序的思路。第一路召回扫描所有闭合的 LWPOLYLINE条件是顶点数为 4 且构成轴对齐矩形也就是四个顶点里 x 只有两个不同值、y 也只有两个不同值。满足就直接判定为候选图框。第二路召回扫描所有 INSERT 实体看它引用的块名里是否含图框frameborder这类关键词或者把块展开后看它的包围盒是不是矩形且尺寸落在 A0 到 A4 的标准范围内。标准图纸尺寸单位毫米大概是A4 是 210×297A3 是 297×420A2 是 420×594A1 是 594×841A0 是 841×1189。放大 100 倍画图很常见所以判定时要把常见缩放系数1、100、0.01都试一遍。第三路召回把四条独立 LINE 组队。做法是把所有水平线和竖直线按端点坐标聚类找出四条能首尾相接成矩形的组合。这一路开销大只在前面两路召回数量为 0 时才启用。召回完还要打分过滤。打分维度包括矩形面积是否在合理范围太小的是标注框、太大的是外边界、长宽比是否接近标准图纸、框内是否包含文字标题栏一般有文字、矩形是否被其他图框包含嵌套的取外层。这些维度加权重排后低于阈值的直接丢掉基本能压掉误判。3.2 点包含判定图元到底属于哪个框拿到一堆矩形之后就是判断每个图元落在哪个框里。这里有个性能陷阱如果对每个图元都遍历所有图框做一次射线法ray casting图元 10 万、图框 100 个那就是一千万次判定浏览器直接跪。正确做法是先做空间索引。我一般用简单的均匀网格把整图切成 50×50 的格子每个图元根据包围盒落到一组格子里每个图框也落到一组格子里。判断归属时只比较跟图框共享格子的那些图元。实测下来这一步能把归属判定的耗时从秒级压到百毫秒级。图元归属的规则要定清楚我的默认策略是这样的图元包围盒完全落在某个图框内100% 归该框。图元包围盒与多个图框相交算相交面积占比归给占比最大的那个框同时在日志里记一笔方便用户人工复核。图元包围盒与所有图框都不相交判定为图外内容通常是大样引线、图例说明默认丢弃但给用户开关选择是否附加到某张指定图。对文本实体的处理要特别小心。TEXT 的插入点是它的定位基点但文字是有宽高的如果只用插入点判断一个贴在边界上的标注可能会被错误归到隔壁框。正确姿势是用文字包围盒插入点 字高 × 字符数估算宽度来判断。3.3 没有图框怎么办空间聚类兜底实际工作中会碰到一些野图客户发来的就是个草图没有规范图框。这种时候前面的图框识别会颗粒无收需要有兜底方案我一般用基于网格的空间聚类。思路是把整图按一个可调的网格切成小格统计每格里的图元密度密度高的区域做连通合并合并出来的块如果面积超过阈值就算一张虚拟图纸。这个阈值不能写死我是从图幅统计里推出来的把所有图元的平均线长算出来取平均线长的 40 到 60 倍作为聚类半径效果比较稳。聚类法的缺陷也很明显图纸里两块内容挨得很近时会被错误合并。所以我在界面上留了参数让用户手动调聚类半径同时提供框选补充——自动分的结果永远可以人工微调这才是能落地的产品形态。3.4 跨框元素裁还是整搬横穿多个图框的元素是自动分割里最折磨人的地方。典型场景是一条市政道路的中心线贯穿十几个图框每一个框里都有一段。你如果整体归给某一个框其他框里就缺线你如果按框裁切就得实现几何裁剪。我的处理是这样的对直线和多段线实现矩形裁剪对其他类型图元整体归属。直线对矩形的裁剪是经典的 Liang-Barsky 或者 Cohen-Sutherland 算法几十行代码搞定。多段线裁剪稍微麻烦点但可以拆成逐段直线处理裁剪完再把结果重新串成多段线。圆弧和圆遇到跨框的情况很少真遇到了就整体归属加日志提示。// Liang-Barsky 直线裁剪返回落在矩形内的线段或 null function clipLine(x1, y1, x2, y2, rect) { const dx x2 - x1, dy y2 - y1; const p [-dx, dx, -dy, dy]; const q [x1 - rect.minX, rect.maxX - x1, y1 - rect.minY, rect.maxY - y1]; let u1 0, u2 1; for (let i 0; i 4; i) { if (p[i] 0) { if (q[i] 0) return null; // 平行且在界外 } else { const r q[i] / p[i]; if (p[i] 0) u1 Math.max(u1, r); else u2 Math.min(u2, r); if (u1 u2) return null; } } return { x1: x1 u1 * dx, y1: y1 u1 * dy, x2: x1 u2 * dx, y2: y1 u2 * dy }; }注意裁剪会破坏图元的线型相位虚线裁剪后起点相位会重置视觉上可能跟原图对不上。对线型精度要求高的场景建议关闭裁剪改成整体归属 相邻框提示把判断权交回给用户。4. 子图渲染与导出实现4.1 视图变换把图框坐标映射到画布这一步是整个导出链路的数学核心写错了图形就会拉伸变形。给定图框的包围盒[fMinX, fMinY, fMaxX, fMaxY]以及目标画布的像素尺寸W × H加上一个边距pad可用宽 W - 2 * pad 可用高 H - 2 * pad scale min(可用宽 / (fMaxX - fMinX), 可用高 / (fMaxY - fMinY)) // 居中偏移 offsetX (W - (fMaxX - fMinX) * scale) / 2 offsetY (H - (fMaxY - fMinY) * scale) / 2然后对每个点px (x - fMinX) * scale offsetX py H - ((y - fMinY) * scale offsetY) // Y 轴翻转这里加min是为了取较小缩放系数保证图框完整装进画布不出血。边距pad一般取画布短边的 3% 到 5%太小的边距会让图框线贴着画布边缘导出后裁剪很不舒服。关于导出分辨率我建议按目标 DPI × 图框物理尺寸来算。假设用户要 300 DPI 的 A3 图297×420 毫米那么像素尺寸就是宽 297 / 25.4 * 300 ≈ 3508 px 高 420 / 25.4 * 300 ≈ 4961 px三千五乘五千等于约 1740 万像素单张 PNG 画布内存大约 70MB这在浏览器里是能扛住的但如果用户要一次导出 50 张就不能同时驻留必须导完一张释放一张。4.2 线宽的换算别忽略CAD 里的线宽单位是毫米映射到像素时要乘scale再乘一个 DPI 系数。举例0.35 毫米线宽scale是 0.5 px/单位假设图纸单位就是毫米那么画出来就是 0.175 像素几乎看不见。所以渲染时我会给线宽设一个下限比如最细 1 像素同时提供按线宽渲染 / 按图层颜色渲染两种模式让用户选。工程图很多时候靠颜色区分线宽按颜色渲染反而更符合阅读习惯。4.3 PNG、JPG、PDF、SVG 该选哪个导出的格式选择要看下游用途PNG无损、透明背景可选适合发微信、贴报告单张体积中等。JPG有损但体积极小适合图量大、只作预览的场景需要注意白底否则透明区域变黑。PDF矢量格式放大不糊适合正式交付或者打印。前端生成 PDF 可以用 pdf-lib 或 jsPDF把画布内容当成图片嵌进去是最省事的但那样还是位图要做真矢量得把图元直接转成 PDF 的绘图指令工作量大好几倍。SVG矢量、文本可搜索适合后续还要在别的工具里编辑的场景。把图元转成path和line直接序列化就行。我的默认推荐是 PNG 可选 PDF。原因很实际大多数交付场景要的是能看清楚、能发出去、能贴文档PNG 完全够用PDF 只是个加分项。4.4 批量导出怎么不卡死也不炸内存一次导 50 张 300 DPI 的 A3 图如果同步跑浏览器会假死好几分钟。必须做异步批次 进度反馈async function exportAll(frames, options, onProgress) { const zip new JSZip(); for (let i 0; i frames.length; i) { const blob await renderFrameToBlob(frames[i], options); zip.file(${sanitize(frames[i].title || 图纸)}_${i 1}.png, blob); forms[i] null; // 主动断开引用帮 GC 回收 onProgress((i 1) / frames.length); await new Promise(r setTimeout(r, 0)); // 让出主线程 } return zip.generateAsync({ type: blob }); }关键点是三个每张图渲染完立即释放画布引用、每次循环让出主线程、用requestAnimationFrame或setTimeout(0)驱动进度条更新。否则用户看不到任何反馈会觉得工具死了。提示JSZip 生成最终压缩包时是 CPU 密集操作几十张高清图压缩也会卡。调用generateAsync时传compression: STORE不压缩、只打包速度能快好几倍图片本来就已经是压缩格式了再压没多少收益。4.5 文件名怎么取才实用自动分割出来的子图如果全叫output_1.png、output_2.png用户拿回去还得一个个重命名体验极差。我的做法是从图框内的标题栏区域提取文字找出图号和图名拼成文件名。图的标题栏一般在图框的右下角或底部取该区域内的所有 TEXT 实体按位置排好找出符合图号规则的那一串通常是类似JS-01、建施-03的格式。识别不准的时候退回到用图框的坐标或序号命名保证不出现重名。文件名里要过滤掉系统不允许的字符\ / : * ? |这个细节不做Windows 用户解压时会看到一堆乱码文件名。5. 常见问题与排查速查5.1 导出的图形是歪的、被拉伸了这类问题九成出在坐标映射公式上排查顺序是这样先检查宽高比是否锁定取scale时如果对 X 和 Y 用了不同的缩放系数图形必然变形——正确做法是min取一个统一scale。再检查 Y 轴翻转有没有漏做漏了的话图形会上下颠倒。最后检查图框包围盒是否包含了线宽和文字外扩如果图框自身是用 0.7 毫米粗线画的包围盒要往外扩半个线宽否则框线会被裁掉半边。还有一种隐蔽情况图元坐标里混了 Z 值巨大的点比如误画的远端参照导致整图包围盒被拉得极大所有图挤成一个小点。这种情况要加坐标异常值过滤超过中位数 1000 倍的坐标点直接剔除并记日志。5.2 图框识别不出来或者识别出一堆假框排查路径按顺序走现象可能原因处理办法一个框都没识别到图框是四条独立 LINE启用第三路四线组队召回识别到几十个假框图纸里的表格、图例框被当图框引入面积阈值 含文字判定只识别到部分图框部分图框用块、部分用多段线多路召回结果合并去重图框大小差十倍图纸单位不是毫米检查 HEADER 段$INSUNITS统一换算去重这一步很关键。用矩形中心距离小于半个框宽就视为同一个的规则合并可以消掉大部分重复。5.3 大文件导出内存爆掉浏览器单标签页的内存上限大概在 2 到 4GB视设备而定导出大批量高清图很容易触顶。几个缓解手段降低导出 DPI150 DPI 对屏幕阅读完全够、把压缩方式改成 STORE、把导出任务改成串行且每张之间强制 GC 间隔、实在不行就分批导出一次 20 张分批下载。还有一个常被忽略的canvas.toBlob是异步的如果你在它回调之前就把画布尺寸改小了导出的图会变成空白。必须用await正确处理function canvasToBlob(canvas, type image/png, quality 0.92) { return new Promise((resolve, reject) { canvas.toBlob(blob { blob ? resolve(blob) : reject(new Error(导出失败)); }, type, quality); }); }5.4 中文变成方块或问号这是 Web 端处理 CAD 图纸排名第一的坑根源是 SHX 字体。CAD 里的中文标注很多用的是hztxt.shx、gbcbig.shx这类专用形字体浏览器根本没有对应的字体文件只能显示成方框。目前没有完美方案可行的是这几条一是把 SHX 映射到系统字体。解析 TABLES 段的 STYLE 表把字体名映射到SimSun、Microsoft YaHei这类系统字体简单粗暴但有效。二是对关键标注用 CAD 导出时自带的文字替换让用户出图前把 SHX 换成 TTFTrueType字体。三是提供字体上传功能让用户传.ttf文件前端用 FontFace API 动态加载这个体验最好但需要用户配合。提示改图形字体映射时别忘了同时校正字宽比例。SHX 字体的宽高比通常接近 0.7而 TrueType 中文接近 1.0不校正的话文字会比原来的更长可能超出标注框。5.5 分割后相邻图框内容串联表现是 A 图框里出现了 B 图框的内容通常是归属判定把边界图元分错了。检查两件事图元的判定依据是插入点还是包围盒只说插入点的话靠近边界的文字很容易串框。图框之间有没有重叠如果两个图框矩形有像素级重叠共享区域的图元归属就会随机。处理办法是先把重叠的图框矩形做一次微收缩各自向内缩 0.5 个线宽消除重叠后再判定。6. 把工具塞进现有系统的几个建议如果你的目标不是做独立工具而是把这套能力集成到自己的设计管理平台里有几个接口设计上的经验可以参考。第一分割结果要落库而不是只返 Blob。分割完成后把每个子图的图框坐标、图号、图名、图元数量存成一条记录图片文件存到对象存储。这样用户可以后续修改归属、重新导出而不是每次都要重跑一遍分割。第二留人工干预入口。自动算法再准也有翻车的时候界面上至少要支持手动补一个图框把某图元移到另一个框合并两个框这三个操作。我见过太多纯自动、没有干预入口的工具有人用一次就弃了。第三导出参数要能存成模板。DPI、格式、边距、是否透明背景这些参数每次导出都调一遍很烦做成模板一键套用体验会好很多。我们内部现在固定了三套模板屏幕预览用 96 DPI JPG、正式交付用 300 DPI PNG、打印用 300 DPI PDF。第四给用户一个预估时间。导出前根据图框数量和 DPI 算一个预计耗时显示出来比如预计 45 秒用户的等待耐心会显著提高。7. 说点我在实际项目里踩过的坑最后分享几个具体的、文档里不会写的东西。第一是别信用户说的图纸单位是毫米我遇到过用米做单位的图纸一算scale差了一千倍导出来只有一个小点。稳妥做法是从 HEADER 段读$INSUNITS读不到就靠图框尺寸反推A3 图框宽度是 420 毫米或 0.42 米一比就知道。第二是顶面线和底面线重叠的问题。装配图里同一位置可能有上下两个零件轮廓导出成图片后完全重合看不出层级。我在渲染时按图元 Z 值做了微小的视觉偏移或者给不同 Z 层的线条加一点点颜色差异实测能明显改善可读性。第三是进度条要做分段式而不是平滑式。因为解析、分割、渲染三个阶段耗时差异很大平滑进度条会在某个阶段卡住不动让用户以为死了。改成显示当前阶段名 阶段内进度体验立刻好很多。第四真要落地一个稳定可用的工具图元类型支持不用一上来就求全。我第一版只支持 LINE、LWPOLYLINE、CIRCLE、ARC、TEXT、INSERT 六种覆盖了绝大部分场景其他类型SPLINE、ELLIPSE、HATCH遇到就跳过并提示该图元类型暂不支持导出已在预览中忽略用户反而能接受。一上来什么都想支持结果样样都不精项目周期会拖到没法交付。
阅读完成 · 觉得有帮助?