text-to-cad 实战:从文本描述到 STEP 与 URDF 的完整链路
1. 从一段文字到三维实体text-to-cad 到底在解决什么问题第一次听到 text-to-cad 这个词很多人脑子里浮现的画面大概是对着电脑敲一句给我画一个法兰盘然后屏幕上就自动长出一个三维模型。这个想象不算离谱但真正落地的时候它解决的问题比自动画图要具体得多也琐碎得多。我接触这个方向是从一个很实际的需求开始的手头有一批参数化的机械零件尺寸规格经常变每次改动都要在 CAD 软件里手动调草图、改约束、重新拉伸改一个零件十分钟改五十个零件就是大半天。更麻烦的是这些零件还要拿去做运动仿真需要导出成 URDF 给仿真环境用手动导出的过程又是一堆重复劳动。于是我开始琢磨能不能用一段结构化的文本描述直接把模型生成出来顺带把 STEP 和 URDF 一起吐出来。text-to-cad 的核心就是把自然语言或结构化文本翻译成CAD 能理解的几何与拓扑描述再驱动几何内核生成实体最后导出成通用格式。它不是一个单一工具而是一条链路文本解析 → 参数提取 → 几何构建 → 格式导出。关键词里出现的 CAD、STEP、URDF、Python正好对应了这条链路的四个关键环节——CAD 是目标领域STEP 是通用的三维交换格式URDF 是机器人仿真用的描述格式Python 则是把这一切串起来的胶水语言。这篇文章适合谁看如果你是会一点 Python、又经常和 CAD 打交道的人比如做机械设计、机器人仿真、参数化建模、批量出图的工程师那这篇内容基本就是为你写的。如果你完全没碰过 CAD但想理解文本生成三维模型这件事的技术轮廓也能看懂大半。我会把每一步为什么这么做、坑在哪里、怎么绕过去都讲清楚尽量让你看完能自己动手跑一遍。需要先说明一点text-to-cad 目前没有哪个方案能做到随便说句话就出完美模型。自然语言的歧义太大一个圆角到底是 R2 还是 R5机器猜不出来。所以真正可用的方案几乎都是结构化文本 参数的路子而不是纯自由文本。这一点想通了后面的技术选型就顺了。2. 文本描述怎么变成几何解析层的设计取舍2.1 为什么纯自然语言解析在工程上不靠谱先说一个反直觉的结论在 text-to-cad 这条链路上最不该花大力气的地方恰恰是自然语言理解。我一开始也想着搞个模型来解析画一个直径 50、厚 10 的圆盘后来发现纯属给自己找麻烦。原因很直接。工程模型对尺寸是零容错的直径 50 和直径 50.0 在语义上一样但在几何内核里必须是一个确定的值。自然语言里大概五十、五十左右这种表述人一看就懂机器一解析就抓瞎。更别说倒个角这种话连人都不确定是哪个边、多大半径。与其让模型去猜不如把输入约束成结构化格式让解析变成确定性的字符串处理。所以我的做法是定义一个轻量的文本 DSL领域特定语言用键值对加层级缩进来描述模型。比如一个带孔的法兰盘输入长这样part flange type: cylinder diameter: 120 height: 15 hole: count: 6 diameter: 10 pitch_diameter: 90 fillet: radius: 2 edges: top_outer这种格式的好处是解析逻辑极其简单用 Python 的正则或者configparser就能搞定不需要任何模型推理速度快、结果稳定、可复现。你可能会问那这不就是配置文件吗跟text-to-cad有什么关系关系在于这个文本是可以由上层再生成的——你可以让一个语言模型把用户的自由描述转成这个 DSL但 DSL 到几何这一段必须是确定性的。把不确定的部分和确定的部分切开是整个系统稳定的关键。2.2 参数提取阶段最容易忽略的单位与坐标系问题解析出参数只是第一步真正让新手翻车的是单位和坐标系。CAD 内核里长度单位可以是毫米、厘米、米不同内核默认值还不一样。我踩过的坑是文本里写diameter: 120心里想的是毫米结果导出 STEP 之后在另一个软件里打开变成了 120 米模型大得看不见。解决办法是在 DSL 里强制声明单位并且在解析层做统一换算所有内部计算一律用毫米导出时再按目标格式转换。别小看这一条STEP 文件本身对单位是有约定的URDF 则默认用米这两个格式之间来回倒单位不统一必然出错。坐标系的问题更隐蔽。CAD 建模通常以 Z 轴为拉伸方向而 URDF 里机器人关节的旋转轴、连杆的朝向有自己的一套约定。如果你生成的模型要直接进仿真坐标原点放在哪里、Z 轴朝哪必须提前想清楚。我的经验是建模阶段统一用底面中心为原点、Z 轴向上的约定导出 URDF 时再统一做一次坐标变换这样建模逻辑最干净。2.3 用 Python 做解析层的具体实现思路解析层用 Python 写是最省事的标准库就够。核心就是一个递归下降的小解析器把缩进层级转成嵌套字典。下面是我常用的一个简化版本import re def parse_dsl(text): lines [l for l in text.splitlines() if l.strip() and not l.strip().startswith(#)] root {} stack [(-1, root)] for line in lines: indent len(line) - len(line.lstrip()) key, _, value line.strip().partition(:) key key.strip() value value.strip() while stack and stack[-1][0] indent: stack.pop() parent stack[-1][1] if value: parent[key] coerce(value) else: parent[key] {} stack.append((indent, parent[key])) return root def coerce(v): try: return int(v) except ValueError: pass try: return float(v) except ValueError: return v这段代码不长但覆盖了绝大多数参数化模型的描述需求。coerce函数负责把字符串转成数字转不了就保留字符串这样type: cylinder和diameter: 120能自动区分。实测下来几百行的模型描述解析耗时在毫秒级完全不是瓶颈。提示解析层一定要写单元测试。参数解析错了后面几何全错而且错误往往在导出后才暴露排查成本极高。我一般会针对每个字段写一个最小用例改解析逻辑时先跑测试。3. 几何构建选对内核比写对代码更重要3.1 为什么我最终选了 CadQuery 而不是直接调 OpenCASCADE几何构建这一步本质是调用一个几何内核把参数变成实体。可选的路子有几条直接调 OpenCASCADE 的 Python 绑定、用 FreeCAD 的脚本接口、用 CadQuery或者用 build123d。我前后试过三种最后稳定在 CadQuery 上。直接调 OpenCASCADE 最灵活但 API 极其底层画一个带孔的圆柱要写几十行还得自己管理拓扑对象稍不注意就内存泄漏或者产生无效实体。FreeCAD 的脚本接口功能全但它的 Python 环境依赖整个 FreeCAD部署起来重而且版本之间 API 变动不小。CadQuery 的好处是它在 OpenCASCADE 之上做了一层声明式的封装写起来接近描述几何而不是操作内核同时它导出的 STEP 质量很稳。用 CadQuery 画上面那个法兰盘代码大概是这样import cadquery as cq result ( cq.Workplane(XY) .circle(60) .extrude(15) .faces(Z) .workplane() .polarArray(45, 0, 360, 6) .hole(10) .edges(|Z) .fillet(2) ) cq.exporters.export(result, flange.step)十几行就把模型建出来并导出 STEP 了。polarArray做环形阵列hole直接打孔fillet倒角这些都是声明式的读起来就是几何本身。这就是我选它的核心理由代码即模型描述改起来直观出错概率低。3.2 参数化建模里特征顺序为什么不能随便调用 CadQuery 这类工具一个很容易被忽视的点是特征的操作顺序。上面那段代码里先拉伸、再打孔、最后倒角顺序换一下结果可能完全不同。如果先倒角再打孔孔可能会切掉倒角的一部分或者倒角在孔的位置产生自交几何内核直接报错。这不是 CadQuery 的问题而是所有基于边界表示BRep的建模都有的特性。BRep 记录的是实体的面、边、顶点以及它们之间的拓扑关系每一步操作都会重建这套关系。顺序不对中间结果的拓扑就不对后续操作自然出问题。我的经验是遵循一个固定的特征顺序基础形体 → 布尔运算加/减→ 细节特征倒角、圆角→ 阵列。阵列放最后是因为阵列会复制特征如果被复制的特征本身有问题阵列会放大问题。另外倒角和圆角尽量放在布尔运算之后因为布尔运算产生的边往往更干净倒角成功率高。3.3 从参数到实体的容错设计无效几何怎么提前拦住几何内核最让人头疼的地方是它不会告诉你这个参数不合理它只会在某个操作上直接抛异常而且异常信息经常看不懂。比如孔径大于外径打孔的时候可能不报错但生成的是一个空实体或者自交实体导出 STEP 之后在别的软件里打开才发现是坏的。我的做法是在几何构建之前加一层参数校验把能提前判断的约束都检查掉。比如校验项判断逻辑处理方式孔径小于外径hole.diameter outer.diameter直接报错提示参数冲突阵列不重叠pitch_diameter * π / count hole.diameter警告可能产生干涉倒角半径合理fillet.radius min(壁厚) / 2报错倒角会切穿高度为正height 0报错这层校验用纯 Python 写不依赖几何内核跑得飞快。别嫌麻烦它能帮你挡掉八成以上的低级错误而且报错信息是你自己写的比内核的异常友好得多。注意参数校验只能挡住逻辑上明显不合理的情况像自交、退化面这类几何层面的问题还是得靠内核。所以构建完之后最好做一次有效性检查CadQuery 里可以用result.val().isValid()来判断。4. 导出 STEP 与 URDF两种格式两套心思4.1 STEP 导出看着简单单位和小数位是重灾区STEP 是三维模型的通用交换格式几乎所有 CAD 软件都认。CadQuery 导出 STEP 就一行cq.exporters.export(result, xxx.step)看起来毫无技术含量但坑都在细节里。第一个坑是单位。前面提过STEP 文件内部有单位声明CadQuery 默认按毫米导出但如果你从别的来源导入模型再导出单位可能就乱了。我遇到过导入一个英寸制的模型导出后尺寸差了 25.4 倍。解决办法是导出前显式确认模型的单位必要时做缩放。第二个坑是小数位。STEP 是文本格式坐标值的小数位直接影响文件大小和精度。默认精度通常够用但如果你做的是精密零件需要确认导出精度满足要求。反过来如果模型很大很复杂适当降低精度能显著减小文件体积。第三个坑是坐标系朝向。不同 CAD 软件对哪个轴朝上的约定不一样有的 Z 朝上有的 Y 朝上。导出时如果不做处理模型在对方软件里可能是躺着的。这个没有通用解只能针对目标软件调整。4.2 URDF 不是模型格式别把它当 STEP 用很多人第一次接触 URDF 会误以为它是另一种三维模型格式其实不是。URDF 描述的是机器人的运动学结构有哪些连杆link、连杆之间怎么连接joint、每个连杆的惯性参数、碰撞体、视觉模型长什么样。它本身不包含几何几何是通过引用外部网格文件通常是 STL 或 DAE来提供的。所以从 CAD 到 URDF实际要做两件事一是把模型导出成网格文件STL/DAE二是在 URDF 里引用这个网格并定义好连杆和关节。关键词里出现的urdf 导入 coppeliasim说的就是把这个 URDF 连同网格一起导入仿真环境。一个最小的 URDF 片段长这样robot nameflange_assembly link namebase_link visual geometry mesh filenameflange.stl scale0.001 0.001 0.001/ /geometry /visual collision geometry mesh filenameflange.stl scale0.001 0.001 0.001/ /geometry /collision inertial mass value0.5/ inertia ixx0.001 ixy0 ixz0 iyy0.001 iyz0 izz0.001/ /inertial /link /robot注意那个scale0.001 0.001 0.001。STL 通常按毫米导出而 URDF 和大多数仿真环境按米计算所以这里要缩小一千倍。这个 scale 是新手最容易漏的地方漏了之后模型在仿真里会大得离谱或者小得看不见。4.3 惯性参数最容易被糊弄、也最容易出问题的地方URDF 里的惯性参数质量和转动惯量是很多人直接填 1 或者随便估一个的地方。如果你的仿真只是看看运动学问题不大但一旦涉及动力学惯性参数错了仿真结果就是错的而且错得很隐蔽——模型能动但力矩、加速度全不对。正确的做法是从 CAD 模型里直接算。CadQuery 和 OpenCASCADE 都能计算实体的体积和质心配合材料密度就能得到质量和转动惯量。下面是一个计算思路solid result.val() volume solid.Volume() density 7.85e-6 # 钢单位 kg/mm^3 mass volume * density center solid.Center() # 转动惯量需要绕质心计算OpenCASCADE 提供相关接口这里单位要特别小心。体积是立方毫米密度用 kg/mm³算出来才是千克。转动惯量的单位是 kg·mm²而 URDF 要求 kg·m²所以还要乘 1e-6。我见过太多人在这里单位搞错导致仿真里零件轻得像纸或者重得像铅。提示如果模型形状复杂手算转动惯量不现实一定要用内核的计算接口。CadQuery 底层是 OpenCASCADE可以通过BRepGProp相关方法拿到惯性张量虽然 API 稍微绕一点但结果可靠。5. 把整条链路串起来一个可复现的实操流程5.1 环境准备Python 版本和依赖的坑先说环境。CadQuery 对 Python 版本有要求目前稳定支持 3.8 到 3.11太新的版本可能装不上。关键词里python 3.8、python 安装这些搜索词说明不少人在这一步卡住。我的建议是直接用 conda 建一个独立环境别在系统 Python 里折腾conda create -n text2cad python3.10 conda activate text2cad conda install -c conda-forge cadquery用 conda 而不是 pip 装 CadQuery是因为它依赖 OpenCASCADE 的原生库conda 能自动处理这些二进制依赖pip 装经常在编译环节失败。这一步省下来的时间够你写好几个模型了。装完之后验证一下import cadquery as cq print(cq.__version__) box cq.Workplane(XY).box(10, 10, 10) print(box.val().Volume()) # 应该是 1000体积是 1000 就说明环境没问题。如果报错找不到 OCCT 相关的库八成是 conda 环境没激活或者装错了 channel。5.2 完整流程从文本文件到 STEP 和 URDF把前面几节串起来一个完整的流程是这样的读取文本描述文件用解析器转成参数字典参数校验拦截明显不合理的输入根据type字段分发到对应的建模函数用 CadQuery 构建实体导出 STEP 文件导出 STL 网格同时生成 URDF 文件填好惯性参数和 scale可选把 URDF 和网格打包导入仿真环境验证第 3 步的分发逻辑很关键它决定了你的系统能支持多少种模型。我的做法是用一个字典把类型名映射到建模函数BUILDERS { cylinder: build_cylinder, box: build_box, flange: build_flange, } def build(params): ptype params.get(type) if ptype not in BUILDERS: raise ValueError(f不支持的模型类型: {ptype}) return BUILDERS[ptype](params)这样加新模型类型只需要写一个函数并注册进去不用改主流程。实测下来支持十几种常见零件类型代码量也就几百行。5.3 批量生成时怎么避免内存和性能问题单个模型生成很快但如果你要批量生成几百个模型问题就来了。CadQuery 的实体对象占用内存不小如果全部留在内存里最后一起导出很容易爆。我踩过一次坑生成到两百多个的时候进程直接被系统杀掉。解决办法是生成一个导出一个及时释放for params in all_params: result build(params) cq.exporters.export(result, f{params[name]}.step) del result # 及时释放del之后最好再手动触发一次垃圾回收尤其是模型复杂的时候。另外如果批量任务很大可以考虑用多进程并行但要注意 CadQuery 的线程安全性多线程容易出问题多进程更稳。注意并行生成时每个进程要独立初始化几何内核环境不要在主进程建好模型再传给子进程那样会序列化失败。让子进程自己读参数、自己建模、自己导出主进程只负责调度。6. 实操中踩过的坑和几条硬经验6.1 模型能生成但导不进仿真问题往往在网格质量有一次我生成的模型在 CAD 里看着好好的导出 STL 之后导入仿真环境碰撞检测各种穿透零件像幽灵一样互相穿过。排查了半天问题出在 STL 的网格精度上。STL 是三角网格曲面被离散成三角形如果离散精度太低圆柱看起来像多边形棱柱碰撞体自然不准。CadQuery 导出 STL 时可以指定精度参数cq.exporters.export(result, part.stl, tolerance0.01, angularTolerance0.1)tolerance控制线性偏差angularTolerance控制角度偏差值越小网格越细、文件越大。我的经验是做碰撞检测的模型tolerance给到 0.01 毫米级别比较稳视觉模型可以放宽到 0.1。别一味追求精细网格太密会让仿真变慢得不偿失。6.2 中文路径和文件名引发的玄学问题这个坑很隐蔽但确实存在。几何内核和某些导出库对非 ASCII 路径的处理不完善模型文件放在中文目录下导出时可能报错或者生成空文件。我遇到过导出 STEP 成功但文件是 0 字节的情况换成英文路径就好了。所以我的建议是整个工作目录用纯英文命名文件名也用英文加数字别用中文和空格。如果非要保留中文名至少在导出时用临时英文路径导出完再重命名。这个习惯能帮你省掉很多莫名其妙的排查时间。6.3 版本升级导致的 API 变动怎么应对CadQuery 从 1.x 到 2.x 有一些 API 变动比如导出接口从exporters.export的某些参数调整过。如果你在网上抄的代码跑不通先检查版本。我的做法是在项目里锁定依赖版本用requirements.txt或者 conda 的environment.yml固定住别让环境自己升级。cadquery2.4.0锁定版本之后升级变成一个有意识的行为而不是某天突然发现代码跑不了了。升级前先在测试环境跑一遍全部用例确认没问题再切生产。6.4 关于免费源码和插件的一点提醒搜索热词里有一堆免费 python 源码大全、盘扣 cad 插件免费版之类的词我理解大家想省事的心情但在这条链路上抄来的代码往往水土不服。text-to-cad 的难点不在某一段代码而在整条链路的衔接和参数约定。别人的代码可能用的是英寸、Y 轴朝上、不同的内核版本直接拿来用光对齐这些约定就够你喝一壶。我的建议是核心的解析、建模、导出三段自己写参考别人的思路可以但别整段复制。这三段加起来也就几百行自己写一遍后面改起来心里有底。真正值得复用的是几何内核和 CadQuery 这类成熟库而不是别人拼凑的业务代码。7. 这套方案还能往哪些方向延伸把基础链路跑通之后能扩展的方向其实不少。一个自然的延伸是接一个大语言模型做前端让用户用自然语言描述模型负责转成前面说的 DSL再由确定性链路生成模型。这样既保留了自然语言的便利又避免了直接让模型生成几何的不稳定。关键在于 DSL 这一层要设计得足够表达力强能覆盖你目标领域的常见零件。另一个方向是和参数化设计结合。很多零件族比如一系列不同规格的法兰、齿轮、支架有共同的拓扑结构只是尺寸不同。用 text-to-cad 的思路可以把整个零件族用一套模板加参数表描述批量生成所有规格顺带把 STEP、URDF、图纸一起出。这在做产品系列或者仿真参数扫描的时候特别有用。再往深了走可以接入优化循环。给定目标性能比如重量最轻、刚度最大让优化算法调整参数每轮调用一次建模和仿真自动迭代。这时候 text-to-cad 就成了整个优化流程里的一个确定性模块输入参数、输出模型稳定可靠这正是它最大的价值所在。我个人在实际操作中的体会是text-to-cad 这件事技术门槛没有想象中高难的是把工程细节抠干净——单位、坐标系、特征顺序、网格精度、惯性参数每一个都是小问题但每一个都能让整条链路崩掉。把这些细节一个个踩平之后你会发现从一段文本到一个能进仿真的模型真的可以做到几秒钟完成而且结果可复现。这种确定性带来的效率提升比任何花哨的自动化都实在。