text-to-cad实战:从自然语言到STEP与URDF的Python自动化链路
1. 项目缘起与核心思路拆解
第一次听到 text-to-cad 这个词,我的直觉是:终于有人把大语言模型和传统 CAD 工作流之间的那堵墙凿开了一个口子。过去几年,我们做机械设计、机器人建模或者产品结构验证,绕不开的一个环节就是手动在 CAD 软件里拉草图、约束尺寸、拉伸切除、装配配合。一个中等复杂度的零件,从需求到能用的三维模型,熟练工也得花上小半天。而 text-to-cad 想做的事情非常直接:你用自然语言描述一个零件或结构,系统自动生成可用的 CAD 文件,通常是 STEP 格式,有时还会输出 URDF 用于机器人仿真。
这个项目的核心价值不在于“替代设计师”,而在于把重复性建模、概念验证、参数化变体生成这些环节的耗时压缩到分钟级。我实测过几个类似的流程,一个带孔法兰盘,从写描述到拿到 STEP 文件,大概两分钟。如果手动建模,算上打开软件、建草图、标注、拉伸、打孔、导出,十五分钟起步。对于需要快速迭代几十个变体的场景,这个效率差距是数量级的。
适合谁来参考这篇文章?三类人:一是做机器人仿真的工程师,经常需要根据文字描述快速生成 URDF 和配套的 STEP 模型;二是产品设计前期做概念验证的人,需要快速把想法变成可视化的三维文件;三是对 Python 和 CAD 自动化感兴趣的技术爱好者,想理解从文本到几何体这条链路到底是怎么打通的。不需要你是 CAD 专家,但最好对 Python 有基本了解,知道什么是虚拟环境、怎么装库。
整个 text-to-cad 的技术链路,我把它拆成四层:语义解析层、几何参数化层、CAD 内核调用层、格式导出层。语义解析层负责把自然语言变成结构化的几何参数,比如“一个长 100mm、宽 50mm、厚 10mm 的板,四角各有一个直径 5mm 的孔”。几何参数化层把这些参数映射成 CAD 内核能理解的草图、特征和约束。CAD 内核调用层通常用 Python 的 cadquery 或者 OpenCASCADE 绑定来执行建模操作。格式导出层负责把模型写成 STEP、STL 或 URDF。每一层都有坑,后面我会逐个拆开讲。
为什么选 Python 作为主力语言?因为 CAD 领域的开源生态里,Python 的绑定最成熟。cadquery 基于 OpenCASCADE,API 设计得比较符合直觉,而且能直接导出 STEP。URDF 那边有 urdfpy 和你的机器人描述需求对接。再加上 Python 在数据处理和 AI 集成上的天然优势,整条链路用 Python 串起来是最顺的。如果你用 C++ 直接调 OpenCASCADE,性能会好一些,但开发效率会下降很多,对于快速验证的场景不划算。
注意:text-to-cad 不是要取代传统 CAD 软件。它擅长的是参数化、规则明确的零件。对于自由曲面、复杂装配体、工程图标注这些任务,目前还远远不够。把它当成一个“快速草稿生成器”来用,心态会好很多。
2. 核心细节解析与实操要点
2.1 语义解析:从一句话到几何参数表
这一步是整个流程里最“AI”的部分,但也是最容易出问题的部分。大语言模型很擅长理解“一个圆柱体,直径 30mm,高 50mm”这种描述,但一旦描述里出现隐含约束,比如“一个 L 形支架,竖板高 80mm,横板长 60mm,厚度都是 8mm,转角处倒圆角 R5”,模型就需要推断出竖板和横板的相对位置、圆角作用在哪个边。我的经验是,不要指望一次解析就完美,要在提示词里明确要求输出 JSON 格式的参数表,并且给出字段定义。
比如你可以这样设计提示词模板:
prompt_template = """ 你是一个 CAD 参数解析器。请把下面的自然语言描述转换成 JSON 格式的几何参数表。 字段定义: - shape_type: 基础形状类型,可选值有 box, cylinder, plate_with_holes, l_bracket, flange - dimensions: 一个对象,包含该形状所需的尺寸参数,单位统一为毫米 - features: 一个数组,每个元素描述一个特征,如孔、倒角、圆角 - material: 材料名称,如果描述中没有提到,填 "unknown" 用户描述:{user_input} 只输出 JSON,不要输出其他内容。 """这样做的好处是,后续的几何参数化层可以直接消费这个 JSON,不需要再做复杂的文本解析。我试过让模型直接输出 cadquery 代码,虽然也能跑,但出错率明显更高,因为模型对 cadquery 的 API 细节掌握得并不牢靠,经常写出不存在的函数名或者参数顺序错误。先转 JSON,再由确定性代码生成 cadquery 脚本,稳定性会好很多。
还有一个坑是单位。用户可能说“长 10 厘米”,也可能说“长 100”,默认单位是毫米还是米?我的做法是在提示词里强制要求模型把所有尺寸转换成毫米,并且在输出 JSON 里加一个unit字段,值固定为"mm"。这样后续处理就不用猜了。
2.2 几何参数化:把 JSON 映射成 CAD 操作序列
拿到 JSON 之后,下一步是把它翻译成 cadquery 的操作序列。cadquery 的编程模型是链式调用,比如:
import cadquery as cq result = ( cq.Workplane("XY") .box(100, 50, 10) .faces(">Z") .workplane() .rect(80, 30) .cutThruAll() )这段代码生成一个 100x50x10 的板,然后在顶面切一个 80x30 的通槽。对于 text-to-cad 来说,你需要写一个映射函数,根据shape_type和features动态生成这样的链式调用。我的做法是给每种形状写一个独立的构建函数,然后用一个调度器根据 JSON 里的类型字段调用对应的函数。
以plate_with_holes为例,构建函数大概长这样:
def build_plate_with_holes(params): length = params["dimensions"]["length"] width = params["dimensions"]["width"] thickness = params["dimensions"]["thickness"] holes = params["features"] result = cq.Workplane("XY").box(length, width, thickness) for hole in holes: if hole["type"] == "hole": diameter = hole["diameter"] positions = hole["positions"] # 列表,每个元素是 (x, y) for pos in positions: result = ( result.faces(">Z") .workplane() .center(pos[0], pos[1]) .hole(diameter) ) return result这里有个细节:hole()方法默认是打穿整个零件的,但如果你在错误的工作平面上操作,孔的方向会不对。我踩过的坑是,在faces(">Z")之后没有调用workplane(),导致center()的坐标系还是全局坐标系,孔的位置偏了。正确的做法是每次选择面之后都显式调用workplane(),把工作平面重置到该面上。
另一个坑是孔的位置坐标。用户描述“四角各有一个孔”时,模型可能输出(0, 0), (length, 0), (0, width), (length, width),但实际加工时孔中心通常要离边缘留一定距离,比如 5mm。我的处理方式是在提示词里加一条规则:“如果用户没有指定孔到边缘的距离,默认取孔径的 1.5 倍,且不小于 3mm。”这样模型会自动计算偏移后的坐标,减少后续手动调整。
2.3 CAD 内核调用:cadquery 的稳定性技巧
cadquery 底层是 OpenCASCADE,功能很强,但有些操作在特定版本下会出问题。我目前用的是 cadquery 2.4 版本,配合 Python 3.10,实测比较稳。安装的时候建议用 conda 而不是 pip,因为 OpenCASCADE 的二进制依赖在 pip 下偶尔会有兼容性问题。命令是:
conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge cadquery如果你只能用 pip,那就:
pip install cadquery但要注意,pip 安装的 cadquery 在某些 Linux 发行版上需要手动装 libGL 和 libGLU,否则导入会报错。Ubuntu 下的命令是:
sudo apt-get install libgl1-mesa-glx libglu1-mesa还有一个常见问题是导出 STEP 时文件名包含中文或特殊字符,会导致导出失败。我的习惯是统一用英文和数字命名,比如flange_001.step。导出代码很简单:
cq.exporters.export(result, "flange_001.step")如果你需要同时导出 STL 用于 3D 打印预览,可以:
cq.exporters.export(result, "flange_001.stl")提示:cadquery 的
export函数会根据文件扩展名自动选择导出格式,所以扩展名一定要写对。.step和.stp都支持,但建议统一用.step。
2.4 URDF 导出:从 CAD 到机器人仿真
URDF 是机器人描述格式,用来定义机器人的连杆、关节和运动学结构。text-to-cad 生成的 URDF 通常用于 CoppeliaSim 这类仿真环境。这里的关键是,URDF 需要的是网格文件(通常是 STL 或 DAE)加上关节的坐标系变换,而不是 STEP 文件。所以流程是:先用 cadquery 生成 STEP,再转成 STL,然后写 URDF 的 XML。
转 STL 的代码前面已经给了。写 URDF 的时候,最容易出错的是关节的原点和轴向。比如一个旋转关节,它的原点应该定义在旋转轴上,轴向用axis标签指定。我一般会用一个 Python 脚本根据 JSON 里的关节信息自动生成 URDF:
def generate_urdf(link_name, mesh_file, joint_name, joint_type, origin_xyz, axis_xyz): urdf = f"""<?xml version="1.0"?> <robot name="{link_name}_robot"> <link name="{link_name}"> <visual> <geometry> <mesh filename="{mesh_file}" scale="1 1 1"/> </geometry> </visual> <collision> <geometry> <mesh filename="{mesh_file}" scale="1 1 1"/> </geometry> </collision> </link> <joint name="{joint_name}" type="{joint_type}"> <parent link="base_link"/> <child link="{link_name}"/> <origin xyz="{origin_xyz}" rpy="0 0 0"/> <axis xyz="{axis_xyz}"/> </joint> </robot>""" return urdf导入 CoppeliaSim 的时候,注意 URDF 文件里的 mesh 路径要写相对路径或者绝对路径,CoppeliaSim 对路径比较敏感。我一般把 STL 和 URDF 放在同一个目录下,mesh 文件名只写文件名,不写路径,这样 CoppeliaSim 会自动在同目录下找。
3. 实操过程与核心环节实现
3.1 环境搭建:从零到能跑通第一个模型
我建议用 conda 来管理环境,因为 cadquery 的依赖比较多,conda 能自动处理 OpenCASCADE 的二进制包。步骤如下:
- 安装 Miniconda 或 Anaconda。如果已经装了 Python,也可以直接用 venv,但 cadquery 在 venv 下安装有时会缺系统库。
- 创建虚拟环境:
conda create -n text2cad python=3.10 - 激活环境:
conda activate text2cad - 安装 cadquery:
conda install -c conda-forge cadquery - 安装其他依赖:
pip install openai(如果你用 OpenAI 的 API 做语义解析)、pip install urdfpy(用于 URDF 校验) - 验证安装:
python -c "import cadquery; print(cadquery.__version__)"
如果这一步报错说找不到OCP模块,说明 OpenCASCADE 的 Python 绑定没装好。解决办法是重新用 conda 安装,不要混用 pip 和 conda。
3.2 完整流程:从一句话到一个 STEP 文件
我拿一个实际例子来走一遍。用户输入:“生成一个法兰盘,外径 120mm,内径 60mm,厚度 15mm,沿圆周均匀分布 6 个直径 10mm 的螺栓孔,螺栓孔中心圆直径 90mm。”
第一步,语义解析。把这句话发给大语言模型,要求输出 JSON。我用的提示词前面已经给了,这里直接看输出:
{ "shape_type": "flange", "dimensions": { "outer_diameter": 120, "inner_diameter": 60, "thickness": 15 }, "features": [ { "type": "bolt_holes", "count": 6, "diameter": 10, "pitch_circle_diameter": 90 } ], "material": "unknown", "unit": "mm" }第二步,几何参数化。写一个build_flange函数:
import cadquery as cq import math def build_flange(params): od = params["dimensions"]["outer_diameter"] id_ = params["dimensions"]["inner_diameter"] thickness = params["dimensions"]["thickness"] bolt_holes = params["features"][0] count = bolt_holes["count"] hole_dia = bolt_holes["diameter"] pcd = bolt_holes["pitch_circle_diameter"] # 创建外圆柱 result = cq.Workplane("XY").circle(od / 2).extrude(thickness) # 切内孔 result = result.faces(">Z").workplane().hole(id_) # 切螺栓孔 for i in range(count): angle = 2 * math.pi * i / count x = (pcd / 2) * math.cos(angle) y = (pcd / 2) * math.sin(angle) result = ( result.faces(">Z") .workplane() .center(x, y) .hole(hole_dia) ) return result第三步,调用并导出:
flange = build_flange(json_params) cq.exporters.export(flange, "flange_001.step")实测下来,从输入文字到生成 STEP 文件,整个过程在普通笔记本上大约 3 到 5 秒,其中大部分时间花在调用大语言模型 API 上。如果本地部署一个小模型做语义解析,速度会更快,但解析准确率会下降。我的建议是,对于精度要求高的场景,用大模型 API;对于批量生成、精度要求不高的场景,可以用本地小模型加规则校验。
3.3 参数计算:螺栓孔位置和倒角尺寸的确定
螺栓孔的位置计算看起来简单,但有几个细节容易忽略。第一,角度步长是2π / count,但如果 count 是奇数,起始角度最好偏移半个步长,这样孔不会正好落在水平轴上,视觉上更对称。第二,孔中心到边缘的距离要大于孔径的一半加上最小壁厚。比如外径 120mm,螺栓孔中心圆直径 90mm,那么孔中心到外边缘的距离是(120 - 90) / 2 = 15mm,孔径 10mm,半径 5mm,剩余壁厚 10mm,足够。如果这个值小于 3mm,就要提醒用户调整参数。
倒角尺寸的确定也有讲究。对于法兰盘,外边缘通常倒角 C1 或 C2,内孔边缘倒角 C0.5 或 C1。这些在用户描述里往往不会提到,但实际加工时需要。我的做法是在提示词里加一条:“如果用户没有指定倒角,默认外边缘倒角 C1,内孔边缘倒角 C0.5。”然后在几何参数化层自动加上倒角操作:
result = result.edges("|Z").chamfer(1.0)edges("|Z")选择所有平行于 Z 轴的边,也就是外圆柱面和内孔面的边。chamfer(1.0)表示倒角 1mm。注意,如果零件上有其他平行于 Z 轴的边,比如螺栓孔的内壁,也会被选中。这时候需要更精确的选择器,比如edges("%CIRCLE")选择圆形边,再根据半径过滤。
3.4 URDF 导入 CoppeliaSim 的实操记录
生成 URDF 之后,导入 CoppeliaSim 的步骤是:打开 CoppeliaSim,菜单栏File->Import->URDF,选择 URDF 文件。如果一切正常,你会看到模型出现在场景里。但有几个常见问题:
- 模型位置不对:URDF 里的
origin标签定义了关节的原点,如果原点设成了(0, 0, 0),模型可能会沉到地面以下。解决办法是调整origin的 z 值,让模型底部刚好接触地面。 - 网格不显示:检查 STL 文件路径是否正确,CoppeliaSim 的控制台会输出错误信息,告诉你哪个文件找不到。
- 关节不能动:URDF 里的关节类型要写对,
revolute是旋转关节,prismatic是滑动关节。如果写成了fixed,关节就不会动。
我一般会在 CoppeliaSim 里手动调整一下模型的初始位置,然后保存成.ttt文件,这样下次直接打开就行,不用重新导入。
4. 常见问题与排查技巧实录
4.1 语义解析不准:模型理解偏差的修正方法
大语言模型有时候会把“直径”理解成“半径”,或者把“厚度”理解成“高度”。我的应对策略是在提示词里加几个示例,也就是 few-shot prompting。比如:
示例1: 用户描述:一个圆柱,直径 20mm,高 30mm 输出:{"shape_type": "cylinder", "dimensions": {"diameter": 20, "height": 30}, ...} 示例2: 用户描述:一个方块,长 50,宽 30,高 20 输出:{"shape_type": "box", "dimensions": {"length": 50, "width": 30, "height": 20}, ...}加了示例之后,解析准确率明显提升。另外,可以在输出 JSON 之后加一个校验步骤,检查尺寸是否在合理范围内。比如直径不能为负数,厚度不能大于外径的一半,等等。如果校验失败,就重新调用模型,并在提示词里加上错误信息,让模型修正。
4.2 cadquery 报错:常见异常与解决
cadquery 的报错信息有时候不太直观。我整理了几个常见的:
| 报错信息 | 可能原因 | 解决办法 |
|---|---|---|
ValueError: Null TopoDS_Shape object | 操作产生了空形状,比如切孔时孔的位置在零件外部 | 检查孔坐标是否在零件范围内 |
Standard_Failure: BRep_API: command not done | 倒角或圆角尺寸过大,超过了边的长度 | 减小倒角尺寸,或检查边的选择器 |
ImportError: cannot import name 'OCP' | OpenCASCADE 绑定未正确安装 | 用 conda 重新安装 cadquery |
RuntimeError: Export failed | 导出路径不存在或文件名包含非法字符 | 检查路径,用英文命名 |
还有一个坑是,cadquery 的hole()方法默认是打穿整个零件,但如果你在非平面的工作平面上调用,可能会失败。解决办法是确保工作平面是平面,或者用cutThruAll()代替。
4.3 URDF 导入失败:路径和坐标系问题
URDF 导入 CoppeliaSim 失败,最常见的原因是 mesh 文件路径不对。URDF 里的<mesh filename="..."/>如果写的是绝对路径,换一台机器就失效了。建议写相对路径,并且把 STL 文件和 URDF 文件放在同一个目录下。另外,CoppeliaSim 对 STL 的单位很敏感,默认按米处理。如果你的 STL 是按毫米生成的,导入后会变成巨大或极小的模型。解决办法是在导出 STL 时指定单位,或者在 CoppeliaSim 里手动缩放。
坐标系问题也很常见。URDF 的坐标系是右手系,Z 轴向上。如果你的 CAD 模型是 Y 轴向上,导入后模型会躺倒。解决办法是在导出 STL 之前,用 cadquery 旋转模型:
result = result.rotate((0, 0, 0), (1, 0, 0), -90)这样就把 Y 轴向上的模型转成了 Z 轴向上。
4.4 批量生成时的性能优化
如果你需要批量生成几十个模型,每次调用大语言模型 API 会很慢。我的优化方案是:先用大模型生成一批 JSON 参数表,缓存到本地文件;然后写一个循环,逐个读取 JSON,调用 cadquery 生成 STEP。这样 API 调用只发生一次,后续的几何生成都是本地操作,速度快很多。另外,cadquery 的建模操作可以并行化,用 Python 的multiprocessing模块开多个进程,每个进程处理一个 JSON。实测下来,8 核机器上并行生成 50 个模型,总时间从 3 分钟降到 30 秒左右。
注意:并行化的时候,每个进程要独立导入 cadquery,不要共享同一个 Workplane 对象,否则会出现线程安全问题。
4.5 独家避坑技巧汇总
- 单位统一:所有尺寸在进入几何参数化层之前,强制转换成毫米。我见过太多因为单位混乱导致模型大 1000 倍的案例。
- 参数校验:在生成模型之前,加一个校验函数,检查尺寸是否为正数、孔是否在零件内部、倒角是否小于最小边长的三分之一。这些检查能拦截 80% 的明显错误。
- 版本锁定:cadquery 的 API 在不同版本之间有细微变化,建议在项目里锁定版本号,比如
cadquery==2.4。升级之前先在测试环境跑一遍回归。 - 日志记录:每次生成模型时,把输入的 JSON、生成的 Python 代码、导出的文件路径都记到日志里。出问题的时候,翻日志比重新跑一遍快得多。
- 人工复核:对于关键零件,生成 STEP 之后一定要在 CAD 软件里打开看一眼。自动生成的模型偶尔会有微小的几何缺陷,比如面不闭合、边不连续,这些在导出时不一定报错,但会影响后续加工。
5. 扩展方向与个人体会
text-to-cad 这条链路跑通之后,我陆续试了几个扩展方向。一个是加装配体支持,让用户描述“一个减速箱,包含箱体、输入轴、输出轴、齿轮”,系统自动生成多个零件并装配。这个难度比单零件大很多,因为涉及配合关系和运动约束。另一个方向是加工程图输出,生成 STEP 之后自动投影三视图,标注尺寸。这个用 cadquery 的exporters.export配合 SVG 导出可以做到,但标注的自动布局还需要不少规则。
我个人在实际操作中的体会是,text-to-cad 目前最适合的场景是“参数化变体生成”。比如你有一组标准零件,需要根据客户需求调整尺寸,手动改模型很繁琐,用 text-to-cad 写一个模板,输入不同参数就能批量出图。这种场景下,语义解析的准确率要求不高,因为参数是结构化的,可以直接从表格读取,不需要经过大语言模型。真正需要大语言模型的场景是“从模糊描述到初步模型”,这时候不要追求一次完美,先出一个粗略版本,再手动微调,效率反而更高。
最后分享一个小技巧:如果你经常需要生成类似的零件,可以建一个“零件模板库”,每个模板对应一个 Python 函数和一段提示词。用户输入描述后,先用一个分类模型判断属于哪个模板,再调用对应的函数。这样比让大语言模型从零生成代码要稳定得多,而且生成速度也快。模板库可以随着使用不断扩充,慢慢覆盖你工作中 80% 的常见零件。