text-to-cad实战:从自然语言到STEP与URDF的Python工具链
1. 从一句话到三维模型text-to-cad 到底在解决什么问题第一次听到 “text-to-cad” 这个词很多人会下意识觉得它离自己很远——毕竟 CAD 在大多数人的印象里是工程师坐在电脑前用鼠标一个点一个点画出来的。但如果你最近在关注 AI 辅助设计这个方向就会发现一个很明显的趋势用自然语言直接生成可用的三维模型文件正在从论文里的 demo 变成能落地的工具链。text-to-cad 就是这条链路上最直观的一环它的核心目标很朴素——你输入一段文字描述它输出一个标准的 CAD 文件比如 STEP或者一个用于机器人仿真的 URDF。这件事为什么值得单独拿出来讲因为传统 CAD 建模的门槛其实卡在“翻译”这一步。你脑子里有一个零件的形状但你得先把它翻译成拉伸、旋转、布尔运算这些操作再翻译成软件里的点击序列。这个翻译过程对熟手来说是肌肉记忆对新手来说就是一道墙。text-to-cad 想做的是把这道墙拆掉让“描述”直接变成“几何”。它适合谁适合做机械设计的工程师用来快速出概念模型适合做机器人仿真的同学用来批量生成 URDF也适合做 Python 工具链开发的程序员用来把建模能力嵌进自己的脚本里。我自己的使用场景比较杂一边要出一些结构件的初步方案一边又要给仿真环境准备模型。以前这两件事是割裂的CAD 里画完导出 STEP再手动写 URDF 的关节和连杆改一次尺寸就要重复一遍。text-to-cad 这类工具出现之后最直接的变化是参数化描述可以复用同一段文字改几个数字就能重新生成STEP 和 URDF 都能从同一个源头出来。这篇文章就把我踩过的坑、验证过的流程、以及那些文档里不会写的细节完整地摊开讲一遍。2. text-to-cad 的整体设计与技术选型思路2.1 为什么是“文本到几何”而不是“文本到网格”要理解 text-to-cad 的设计先要搞清楚一个关键分叉生成的结果到底是网格mesh还是边界表示BRep。很多 AI 生成 3D 模型的项目输出的是 STL 或者 OBJ 这类三角网格文件看起来也能用但一旦你要做精确的工程操作——比如倒角、打孔、装配配合——网格就露馅了。网格没有“面”和“边”的拓扑概念你没法在它上面标注一个直径 8 毫米的孔也没法让两个零件按轴线对齐。text-to-cad 走的是另一条路它输出的是STEP 这类基于 BRep 的格式。STEP 里保存的是精确的几何定义圆柱面、平面、圆锥面以及它们之间的拓扑关系。这意味着生成出来的模型是可以继续在 CAD 软件里编辑的可以测量、可以改参数、可以做布尔运算。这个选择直接决定了整个技术栈的走向——你不能用一个纯视觉的扩散模型去生成 STEP因为扩散模型擅长的是像素或点云不是精确的数学曲面。所以 text-to-cad 的典型架构是大语言模型负责理解文本并生成结构化的建模指令几何内核负责把这些指令执行成真实的 BRep 实体。这里的几何内核通常是 OpenCASCADEOCCT它是目前开源领域最成熟的 BRep 内核FreeCAD、KiCad 的 3D 查看器背后都是它。Python 这边通过cadquery或者build123d这类库来调用 OCCT把代码写成链式调用的建模脚本。大语言模型要做的就是把“一个边长 50 毫米、中心有直径 10 毫米通孔的立方体”翻译成一段 CadQuery 代码。2.2 为什么 URDF 是另一个必须支持的出口如果 text-to-cad 只输出 STEP那它服务的主要是机械设计。但热词里出现了 URDF说明还有一大波用户是冲着机器人仿真来的。URDF 是机器人描述格式它描述的不是一个静态零件而是一个由连杆link和关节joint组成的运动学树。一个机械臂的 URDF 里每个连杆有自己的几何形状和惯性参数每个关节有类型旋转、平移、固定、轴线方向、运动范围。text-to-cad 支持 URDF 出口意味着它要能理解“这是一个两连杆的机械臂第一关节绕 Z 轴旋转第二关节绕 Y 轴旋转”这类描述并且把几何体和运动学结构一起生成出来。这件事的难点在于URDF 里的几何通常比 STEP 简单但运动学关系必须准确。如果关节轴线搞错了导入 CoppeliaSim 之后机械臂会往奇怪的方向动。所以 text-to-cad 在处理 URDF 时往往需要把“几何生成”和“运动学装配”分成两步先用文本生成各个连杆的几何再根据文本里的关节描述把它们组装成树结构。2.3 Python 在整个链路里的角色热词里 Python 出现的频率极高这不是偶然。text-to-cad 的整个工具链几乎都长在 Python 生态里。原因有三第一调用大语言模型的 API 用 Python 最顺手第二CadQuery、build123d、trimesh 这些几何库都是 Python 优先第三URDF 的解析和生成有urdfpy、yourdfpy这类现成的库。所以如果你想自己搭一套 text-to-cad 的流程Python 是绕不开的。我自己的做法是把它拆成三层最上层是文本解析层负责调用模型把自然语言转成结构化的 JSON 或者直接转成建模代码中间是几何执行层用 CadQuery 执行建模代码导出 STEP最下层是格式转换层把几何体转成 URDF 需要的 mesh 和惯性参数。这三层之间用明确的接口隔开好处是任何一层出问题都能单独替换。比如文本解析层今天用这个模型明天想换一个只要输出格式不变下面两层完全不用动。3. 核心细节解析从文本到 STEP 的关键环节3.1 文本描述的结构化拆解大语言模型直接生成建模代码最大的风险是“看起来对但跑不通”。我试过让模型直接写 CadQuery 代码十次里有三次会因为 API 用错或者参数类型不对而报错。后来我改了一个策略不让模型直接写代码而是让它先输出一个结构化的中间表示再由一个固定的代码生成器把中间表示翻译成 CadQuery 代码。这个中间表示通常是一个 JSON里面包含零件的基本体、尺寸、位置、布尔操作。举个例子输入“一个 60x40x20 毫米的底板四角有直径 4 毫米的安装孔孔中心距边缘 5 毫米”模型输出的 JSON 大概是这样{ base: {type: box, length: 60, width: 40, height: 20}, features: [ { type: hole, diameter: 4, positions: [ {x: 5, y: 5}, {x: 55, y: 5}, {x: 5, y: 35}, {x: 55, y: 35} ], through: true } ] }这个 JSON 的好处是可校验。孔的位置是不是在板子范围内直径是不是小于板子厚度这些检查可以在生成代码之前做掉避免跑出一堆报错。而且这个中间表示可以缓存、可以版本管理改一个尺寸只需要改 JSON 里的数字不用重新让模型生成一遍。3.2 几何内核的参数计算与单位陷阱CadQuery 和 OCCT 默认使用的单位是毫米这一点和大多数 CAD 软件一致。但坑在于有些模型在生成尺寸时会“自作主张”地换算单位。我遇到过一次输入里写的是“2 厘米厚的板”模型输出 JSON 时把 2 厘米转成了 0.2它以为是米。结果生成出来的板子薄得像纸。后来我在提示词里强制要求“所有尺寸统一用毫米表示不要做任何单位换算”这个问题才消失。另一个参数陷阱是孔的定位方式。文本里说“四角有孔”模型可能理解为孔在四个角点上也可能理解为孔距离边缘有一段距离。这两种理解生成的模型完全不同。我的做法是在中间表示里强制要求孔的位置用绝对坐标表示并且在提示词里明确“孔中心距边缘的距离”这个参数。如果文本里没提就默认一个合理值比如板厚的 1.5 倍并且在输出里标注这是默认值让用户知道可以改。3.3 URDF 生成时的惯性参数处理URDF 里每个连杆都需要惯性矩阵这个矩阵决定了仿真时的动力学行为。如果惯性参数不对机械臂在 CoppeliaSim 里会抖得像得了帕金森。text-to-cad 生成 URDF 时惯性参数通常是从几何体推算的假设材料密度均匀根据几何体的体积和形状计算质量和转动惯量。对于简单的长方体、圆柱体有现成的公式对于复杂形状可以用 trimesh 或者 OCCT 的质量属性计算功能。这里有个经验不要用默认密度。URDF 里如果不指定密度很多工具会默认 1000 kg/m³也就是水的密度。对于铝合金零件实际密度是 2700 kg/m³ 左右对于钢是 7800 kg/m³。密度差 2.7 倍惯性矩阵就差 2.7 倍仿真结果完全不一样。我的做法是在中间表示里加一个material字段默认是aluminum对应密度 2700用户可以在文本里指定“钢制”或者“塑料”来切换。3.4 STEP 与 URDF 的几何精度取舍STEP 追求的是精确几何URDF 里的视觉几何和碰撞几何通常用 mesh 表示。这里有一个取舍mesh 的精度越高文件越大仿真加载越慢精度越低碰撞检测越不准。我的经验是视觉几何用中等精度碰撞几何用低精度。比如一个圆柱体视觉几何用 32 个面的棱柱近似碰撞几何用 16 个面就够了。在 URDF 里可以分别指定visual和collision的 mesh 文件这样既好看又跑得快。4. 实操过程搭一套可复现的 text-to-cad 流程4.1 环境准备与依赖安装先把 Python 环境弄干净。我推荐用 conda 建一个独立环境避免和系统里的其他包打架。Python 版本选 3.10 或 3.11太新的版本有些几何库还没跟上。conda create -n text2cad python3.11 conda activate text2cad pip install cadquery build123d trimesh yourdfpy numpyCadQuery 的安装在某些平台上需要额外步骤如果 pip 装不上可以去它的官方文档看 conda 安装方式。build123d 是另一个建模库API 比 CadQuery 更 Pythonic我两个都用看哪个顺手。trimesh 用来做 mesh 的转换和简化yourdfpy 用来读写 URDF。注意CadQuery 依赖 OCCT安装包比较大第一次装可能要几分钟。如果网络慢可以配一个国内镜像源。4.2 文本解析层的实现文本解析层的核心是一个函数输入是自然语言字符串输出是前面说的那个 JSON 中间表示。我用的方式是通过 API 调用大语言模型提示词里把 JSON 的 schema 写清楚并且给两个例子。提示词的关键部分是“只输出 JSON不要输出任何解释文字”这样解析起来最省事。import json import openai def parse_text_to_json(text): prompt f 你是一个 CAD 建模助手。把下面的描述转成 JSON。 JSON 格式 {{ base: {{type: box, length: 数字, width: 数字, height: 数字}}, features: [ {{type: hole, diameter: 数字, positions: [{{x: 数字, y: 数字}}], through: true}} ] }} 所有尺寸用毫米。不要做单位换算。只输出 JSON。 描述{text} response openai.ChatCompletion.create( modelgpt-4, messages[{role: user, content: prompt}] ) return json.loads(response.choices[0].message.content)这段代码里openai的调用方式可能随版本变化实际用的时候按你装的版本调整。关键是提示词里的 schema 要写死不要让模型自由发挥。4.3 几何执行层从 JSON 到 STEP拿到 JSON 之后用 CadQuery 执行建模。下面是一个简化的例子处理底板加孔的情况import cadquery as cq def build_from_json(data): base data[base] result cq.Workplane(XY).box(base[length], base[width], base[height]) for feature in data.get(features, []): if feature[type] hole: for pos in feature[positions]: result ( result.faces(Z).workplane() .center(pos[x] - base[length]/2, pos[y] - base[width]/2) .hole(feature[diameter]) ) cq.exporters.export(result, output.step) return result这里有个细节CadQuery 的box是以原点为中心的所以孔的坐标要减去板子尺寸的一半才能把文本里的“距边缘 5 毫米”正确映射过去。这个偏移量很容易搞错我建议在 JSON 里就统一用“距边缘距离”而不是绝对坐标然后在代码里做转换这样文本和代码的语义更一致。4.4 URDF 生成与 CoppeliaSim 导入验证URDF 的生成比 STEP 多一步要把几何体导出成 mesh然后写 XML。下面是一个两连杆机械臂的 URDF 生成片段import yourdfpy from yourdfpy import Link, Joint, URDF def build_urdf(link_geometries, joints): links [] for name, geom in link_geometries.items(): mesh_path f{name}.stl geom.export(mesh_path) links.append(Link(namename, visual_meshes[mesh_path], collision_meshes[mesh_path])) joint_objs [] for j in joints: joint_objs.append(Joint( namej[name], typej[type], parentj[parent], childj[child], axisj[axis], originj[origin] )) urdf URDF(links, joint_objs) urdf.write(robot.urdf)生成之后导入 CoppeliaSim 验证。CoppeliaSim 对 URDF 的容错性一般如果关节轴线方向不对机械臂会以奇怪的方式运动。我的验证方法是导入之后先手动拖动每个关节看运动方向是否符合预期。如果反了就把 axis 取反。这个步骤看起来笨但比在仿真里调试快得多。4.5 批量生成与参数化复用text-to-cad 最大的优势是批量。我经常需要生成一系列尺寸不同的底板比如长度从 40 到 100 毫米步长 10 毫米。用传统 CAD 要画十次用这套流程只需要改 JSON 里的数字循环执行就行for length in range(40, 101, 10): data { base: {type: box, length: length, width: 40, height: 20}, features: [ {type: hole, diameter: 4, positions: [ {x: 5, y: 5}, {x: length-5, y: 5}, {x: 5, y: 35}, {x: length-5, y: 35} ], through: True} ] } build_from_json(data)这段代码跑完你会得到十个 STEP 文件每个的孔位都自动跟着长度调整。这种参数化复用是文本驱动建模最实在的价值。5. 常见问题与排查技巧实录5.1 模型生成的几何体是空心的或者破面的这是最常见的问题通常是因为布尔运算的顺序不对。比如先打孔再倒角倒角可能会把孔的边缘切掉导致面不完整。我的经验是先做大的布尔运算再做小的特征。具体顺序是基本体 → 合并/切除大特征 → 打孔 → 倒角/圆角。如果倒角之后还要打孔就把倒角放到最后。另一个原因是 OCCT 的容差设置。有些模型在布尔运算时因为容差太小而失败可以在 CadQuery 里调整tolerance参数默认是 1e-6改成 1e-4 有时候能救回来。5.2 URDF 导入 CoppeliaSim 后关节不动先检查关节类型。URDF 里的关节类型有revolute、prismatic、fixed、continuous等。如果写成了fixed关节当然不动。如果类型对但不动检查axis字段。CoppeliaSim 里关节的旋转轴是相对于关节坐标系定义的如果 axis 写成了[0, 0, 0]那就没有旋转轴。正常的旋转关节 axis 应该是[0, 0, 1]或者[1, 0, 0]这样的单位向量。还有一个隐蔽的问题URDF 里的 link 和 joint 名字不能有空格和特殊字符。我有一次用中文名字命名 link导入直接失败。后来统一改成英文加下划线问题消失。5.3 STEP 文件在别的 CAD 软件里打不开STEP 有不同的协议版本AP203、AP214、AP242。CadQuery 默认导出的是 AP214大多数软件都能打开。如果对方用的是很老的 CAD 软件可能需要导出 AP203。在 CadQuery 里可以指定cq.exporters.export(result, output.step, exportTypeSTEP, opt{write_pcurves: False})如果文件能打开但显示不全可能是单位问题。有些软件默认单位是英寸打开毫米的 STEP 会缩小 25.4 倍。在导入时手动指定单位就行。5.4 常见问题速查表问题现象可能原因排查方法解决方式几何体空心/破面布尔运算顺序错误检查特征顺序先大后小倒角放最后URDF 关节不动关节类型或轴线错误检查 type 和 axis改为 revoluteaxis 设为单位向量STEP 打不开协议版本不兼容换软件测试导出 AP203 或 AP214尺寸差 25.4 倍单位混淆测量已知尺寸导入时指定毫米模型生成报错API 调用错误看报错行检查 CadQuery 版本和参数类型仿真抖动惯性参数错误检查密度设置按材料设置密度铝合金 27005.5 几个文档里不会写的避坑技巧第一个技巧在提示词里加一句“如果描述有歧义选择最保守的解释”。比如“打一个孔”没说直径模型可能猜 5 毫米也可能猜 10 毫米。加了这句话之后模型倾向于选一个中间值并且标注出来方便你改。第二个技巧生成的 STEP 文件先做一次几何修复。OCCT 有一个ShapeFix工具可以自动修复小的破面和缝隙。CadQuery 里可以通过cq.occ_impl.shapes调用。修复之后再导出文件在别的软件里打开成功率会高很多。第三个技巧URDF 的 mesh 文件用相对路径。CoppeliaSim 导入 URDF 时如果 mesh 路径是绝对路径换一台机器就找不到文件。在 URDF 里写package://或者相对路径然后把 mesh 文件和 URDF 放在同一个目录下可移植性最好。6. 工具链选型与扩展方向6.1 CadQuery 与 build123d 的取舍CadQuery 和 build123d 都是基于 OCCT 的 Python 建模库但风格不同。CadQuery 是链式调用适合写“选择面 → 工作平面 → 打孔”这种操作序列build123d 更接近面向对象适合构建复杂的装配体。我自己的用法是简单零件用 CadQuery多零件装配用 build123d。text-to-cad 生成的中间表示如果只描述单个零件CadQuery 就够了如果要描述装配关系build123d 的Assembly类会更方便。6.2 从 STEP 到 URDF 的自动化转换如果已经有 STEP 文件想转成 URDF可以用 trimesh 做中间转换。流程是STEP → OCCT 读取 → 导出 STL → trimesh 简化 → 写 URDF。这个流程的难点在于 STEP 里的装配层级信息在转 STL 时会丢失所以关节关系需要手动补。我的做法是在 STEP 里用命名约定比如link1、link2这样的零件名转换时根据名字推断层级。6.3 后续可以扩展的方向这套流程目前处理的是规则几何体对于自由曲面比如汽车外壳还力不从心。一个可能的扩展方向是结合点云生成或者隐式曲面表示让 text-to-cad 能处理更复杂的形状。另一个方向是加入约束求解让生成的模型满足装配约束比如“这个孔必须和那个轴同心”。这些方向目前还在探索阶段但已经有一些开源项目在做了。我个人在实际操作中的体会是text-to-cad 目前最适合的场景是快速出概念模型和批量生成参数化零件而不是替代精细的 CAD 设计。把它当成一个“草图生成器”和“参数化脚本生成器”期望值放对了用起来就很顺手。最后再分享一个小技巧把常用的零件描述存成一个文本模板库每次改几个数字就能生成新模型比从头写描述快得多。