1. 从一段文字到三维实体:text-to-cad 到底在解决什么问题
如果你做过机械设计、建筑建模或者机器人仿真,一定经历过这样的场景:脑子里已经想清楚了一个零件的形状,但打开 CAD 软件之后,光是拉伸、倒角、打孔、装配这些操作就要花掉大半个小时。更别提参数改一版,整个特征树跟着重算,有时候一个圆角报错能让人抓狂半天。text-to-cad这个方向要干的事情,就是让你用一段自然语言描述,直接生成可用的 CAD 模型文件,跳过手工建模的重复劳动。
我最早接触这个思路是在做机器人仿真的时候。当时需要批量生成一批标准件——法兰盘、支架、连接件,每个零件的差别无非是孔径、孔距、厚度这几个参数。手工在 CAD 里改参数再导出 STEP,一天下来也就能搞定二三十个。后来我尝试用 Python 脚本调用 CAD 的 API 来批量生成,效率提升明显,但脚本本身还是要人写,参数变了脚本也得跟着改。text-to-cad的价值就在于,它把"写脚本"这一步也省掉了,你只需要用自然语言描述需求,系统自动解析意图、生成几何、输出标准格式文件。
这个方向适合什么人呢?我梳理了一下,大概有三类:一是做机器人仿真的工程师,需要快速生成 URDF 模型和对应的可视化网格;二是机械设计岗位的从业者,经常要出标准件或者简单非标件的三维模型;三是对 CAD 二次开发感兴趣的 Python 开发者,想找一个能落地的项目来练手。不管你是哪一类,理解text-to-cad的完整链路——从文本解析到几何生成再到格式导出——都是绕不开的基本功。
提示:
text-to-cad目前并不是要替代专业 CAD 软件做复杂曲面造型,它的强项在于规则几何体、标准件、简单装配体的快速生成。把它的能力边界搞清楚,用起来才不会失望。
2. 文本到几何的翻译层:意图解析与参数提取
2.1 为什么不能直接让大模型输出 CAD 命令
很多人第一反应是:既然有大语言模型,直接让它输出 CAD 软件的脚本命令不就行了?我试过这条路,踩了不少坑。大语言模型对自然语言的理解确实强,但它对几何运算的精确性把控很差。你让它生成一个"直径 50mm、厚度 10mm 的法兰盘,中心孔 20mm,均布 6 个直径 8mm 的螺栓孔,孔距半径 35mm",它可能会给你一段看起来像模像样的脚本,但里面的坐标计算、阵列角度、布尔运算顺序经常出错。更麻烦的是,CAD 脚本的 API 在不同软件之间差异巨大,AutoCAD 的 AutoLISP、SolidWorks 的 VBA、FreeCAD 的 Python API,语法完全不一样,模型很难通用。
所以text-to-cad的正确做法是分层处理:第一层用自然语言处理技术把文本解析成结构化的参数描述,第二层用确定性的几何内核来生成模型。这样既发挥了大模型理解语言的优势,又保证了几何生成的准确性。参数提取这一层,核心要解决的是"实体识别"和"关系抽取"两个问题。
实体识别就是找出文本里提到的几何要素。比如"法兰盘"是零件类型,"直径 50mm"是外径参数,"厚度 10mm"是高度参数,"中心孔 20mm"是内孔特征,"6 个直径 8mm 的螺栓孔"是孔阵列特征。关系抽取则是理清这些要素之间的从属关系和空间关系——螺栓孔是均布在法兰盘上的,孔距半径 35mm 决定了它们的位置,中心孔和外圆是同心的。
2.2 参数提取的实操方案与常见坑
我在实际项目里用过两种方案来做参数提取。第一种是基于规则模板的,针对常见零件类型预先定义好参数槽位,用正则表达式加关键词匹配来填充。这种方案的好处是稳定、可控,坏处是扩展性差,遇到没见过的描述方式就抓瞎。第二种是用大语言模型做结构化输出,给它一个 JSON Schema,让它把文本里的参数按格式填进去。这种方案灵活性强,但需要做好校验,防止模型胡编参数。
下面是我常用的一个参数提取提示词模板,用 Python 调用大语言模型接口来实现:
import json EXTRACTION_PROMPT = """ 你是一个CAD参数提取助手。请从下面的文本描述中提取几何参数,输出JSON格式。 如果某个参数没有提到,值设为null。 文本:{user_input} 输出格式: {{ "part_type": "零件类型", "outer_diameter": 外径数值或null, "thickness": 厚度数值或null, "center_hole_diameter": 中心孔直径或null, "bolt_hole_count": 螺栓孔数量或null, "bolt_hole_diameter": 螺栓孔直径或null, "bolt_circle_radius": 螺栓孔分布圆半径或null, "unit": "单位,默认mm" }} """ def extract_params(text): prompt = EXTRACTION_PROMPT.format(user_input=text) response = call_llm(prompt) params = json.loads(response) # 校验必填参数 if params["outer_diameter"] is None or params["thickness"] is None: raise ValueError("缺少关键尺寸参数,无法生成模型") return params这里有几个坑我要特别提醒。第一个坑是单位问题。用户可能说"直径 5 厘米",也可能说"直径 50",默认单位是毫米还是厘米需要明确。我的做法是在提示词里强制要求输出单位字段,并且在后续几何生成时统一换算成毫米。第二个坑是数值范围校验。用户可能输入"直径 5000mm 的法兰盘",这在几何上能生成,但实际加工根本做不出来。我一般会加一个合理范围检查,超出范围就提示用户确认。第三个坑是歧义消解。"6 个孔"到底是指 6 个螺栓孔还是包含中心孔一共 6 个?这种歧义在提示词里要明确约定,我通常约定"孔数量"专指阵列孔,中心孔单独计数。
2.3 从参数到几何:选择什么内核来生成模型
参数提取出来之后,下一步就是生成三维几何。这里的选择直接决定了整个项目的技术栈。我对比过几种方案,各有优劣。
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| FreeCAD Python API | 开源免费,功能完整,支持 STEP 导出 | 文档偏少,API 学习曲线陡 | 个人项目、快速原型 |
| CadQuery | 语法简洁,基于 OCCT 内核,代码即模型 | 复杂曲面支持有限 | 规则几何体批量生成 |
| OpenSCAD | 脚本化建模,社区活跃 | 渲染式建模,非精确 B-rep | 简单模型、3D 打印 |
| 商业 CAD API | 功能最强,精度最高 | 授权费用高,依赖特定软件 | 企业级生产环境 |
我个人的选择是 CadQuery。它的设计哲学就是"用代码描述几何",和text-to-cad的思路天然契合。比如生成一个法兰盘,CadQuery 的代码读起来几乎就是自然语言的直接翻译:
import cadquery as cq def make_flange(outer_d, thickness, center_hole_d, bolt_count, bolt_d, bolt_circle_r): # 创建基础圆柱体 flange = cq.Workplane("XY").circle(outer_d / 2).extrude(thickness) # 打中心孔 flange = flange.faces(">Z").workplane().hole(center_hole_d) # 打螺栓孔阵列 flange = ( flange.faces(">Z").workplane() .polarArray(bolt_circle_r, 0, 360, bolt_count) .hole(bolt_d) ) return flange这段代码的逻辑非常清晰:先画外圆拉伸成圆柱,再在顶面打中心孔,最后用极坐标阵列打一圈螺栓孔。每一步对应一个几何操作,参数直接来自上一步提取的结果。实测下来,生成一个法兰盘的时间在 0.5 秒以内,导出 STEP 文件也就一两秒,比手工建模快了两个数量级。
注意:CadQuery 依赖 OCCT 几何内核,安装时建议用 conda 而不是 pip,pip 安装在某些系统上会出现动态库找不到的问题。我踩过这个坑,折腾了半天才发现是 OCCT 的二进制依赖没装全。
3. 输出格式的抉择:STEP、URDF 与网格文件的适用边界
3.1 STEP 文件:精确几何交换的通用语言
STEP 是 CAD 领域最通用的三维模型交换格式,全称是 Standard for the Exchange of Product model data。它的核心优势是保存的是精确的 B-rep(边界表示)几何,而不是三角网格近似。这意味着你把 STEP 文件导入任何主流 CAD 软件,看到的都是精确的圆柱面、平面、倒角,而不是一堆小三角面片拼出来的近似形状。
在text-to-cad的流程里,STEP 导出是标配。CadQuery 直接提供了导出接口:
cq.exporters.export(flange, "flange.step")一行代码搞定。但这里有个细节要注意:STEP 文件的单位。CadQuery 默认输出单位是毫米,但有些 CAD 软件导入时会把单位识别成米或者英寸,导致模型尺寸差了几个数量级。我的做法是在导出前显式设置单位,并且在文件名里标注尺寸,比如flange_od50_th10.step,这样即使单位识别错了,也能通过文件名快速判断。
另一个坑是坐标系。CadQuery 默认以 XY 平面为基准面,Z 轴向上。但有些 CAD 软件默认 Z 轴向前或者 Y 轴向上。如果生成的模型要导入特定软件做后续装配,最好在导出前调整好坐标系朝向,省得导入之后还要手动旋转。
3.2 URDF:机器人仿真场景下的模型描述
URDF 全称是 Unified Robot Description Format,是机器人领域描述机器人模型的 XML 格式。它本身不包含几何数据,而是通过引用外部网格文件(通常是 STL 或 DAE 格式)来定义机器人的外观,同时定义关节、连杆、惯性矩阵等物理属性。
为什么text-to-cad要支持 URDF 输出?因为机器人仿真工程师的典型工作流是:设计机器人本体结构,导出 URDF,导入仿真环境(比如 CoppeliaSim、Gazebo)做运动学和动力学仿真。如果text-to-cad能直接生成 URDF,就省去了从 CAD 导出网格再手工编写 URDF 的繁琐过程。
一个典型的 URDF 文件结构是这样的:
<robot name="simple_arm"> <link name="base_link"> <visual> <geometry> <mesh filename="base_link.stl" scale="0.001 0.001 0.001"/> </geometry> </visual> <collision> <geometry> <mesh filename="base_link.stl" scale="0.001 0.001 0.001"/> </geometry> </collision> <inertial> <mass value="1.5"/> <inertia ixx="0.01" ixy="0" ixz="0" iyy="0.01" iyz="0" izz="0.01"/> </inertial> </link> <joint name="joint1" type="revolute"> <parent link="base_link"/> <child link="arm_link"/> <axis xyz="0 0 1"/> <limit lower="-3.14" upper="3.14" effort="10" velocity="1.0"/> </joint> </robot>这里的关键点是scale属性。STL 文件通常以毫米为单位导出,但 URDF 和大多数仿真环境默认使用米。如果不加scale="0.001 0.001 0.001",导入仿真环境后模型会大一千倍,看起来就像一座山。我第一次做 URDF 导入的时候就被这个坑了,在 CoppeliaSim 里找了半天才发现是单位问题。
3.3 网格文件:STL 与 DAE 的取舍
STL 是最简单的三维网格格式,只包含三角面片的顶点坐标和法向量,不包含颜色、材质、纹理信息。它的优点是通用性极强,几乎所有三维软件和仿真环境都支持。缺点是文件体积大,精度受三角面片数量影响。
DAE(Collada)格式则支持颜色、材质、纹理,适合需要视觉区分的场景。比如一个机器人有多个连杆,每个连杆用不同颜色区分,用 DAE 就比 STL 方便。但 DAE 的解析复杂度更高,有些仿真环境对 DAE 的支持不如 STL 稳定。
我的经验是:如果只是做碰撞检测和运动学仿真,STL 足够了,文件小、加载快。如果需要做视觉展示或者论文配图,用 DAE 更好看。CadQuery 导出 STL 很简单:
cq.exporters.export(flange, "flange.stl", tolerance=0.01)tolerance参数控制三角面片的精度,值越小面片越密、文件越大。对于一般仿真用途,0.01mm 的容差已经足够,文件大小也在可接受范围内。
4. 用 Python 把整条链路串起来:从文本输入到文件输出
4.1 项目结构设计与依赖管理
把前面几层串起来,一个完整的text-to-cad项目大概需要这些模块:文本解析模块、参数校验模块、几何生成模块、格式导出模块。我习惯用这样的目录结构:
text-to-cad/ ├── main.py # 入口,接收文本输入 ├── parser/ │ ├── __init__.py │ └── extractor.py # 参数提取 ├── geometry/ │ ├── __init__.py │ ├── flange.py # 法兰盘生成 │ ├── bracket.py # 支架生成 │ └── base.py # 几何基类 ├── exporter/ │ ├── __init__.py │ ├── step_export.py │ ├── urdf_export.py │ └── mesh_export.py └── requirements.txt依赖方面,核心是cadquery和numpy。如果要用大语言模型做参数提取,还需要openai或者requests来调接口。requirements.txt大概长这样:
cadquery>=2.4 numpy>=1.24 requests>=2.31 lxml>=4.9安装的时候我强烈建议用 conda 创建独立环境,因为 CadQuery 的 OCCT 依赖和系统库容易冲突:
conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge cadquery pip install numpy requests lxml4.2 几何生成模块的抽象设计
不同零件类型的生成逻辑差异很大,但有一些共同的操作:创建基础形体、打孔、倒角、阵列、布尔运算。我设计了一个基类来封装这些通用操作,子类只需要实现具体的建模流程。
import cadquery as cq class PartGenerator: def __init__(self, params): self.params = params self.model = None def validate(self): """参数校验,子类可重写""" raise NotImplementedError def build(self): """几何构建,子类必须实现""" raise NotImplementedError def export_step(self, filepath): cq.exporters.export(self.model, filepath) def export_stl(self, filepath, tolerance=0.01): cq.exporters.export(self.model, filepath, tolerance=tolerance) def generate(self): self.validate() self.build() return self.model法兰盘生成器的实现:
class FlangeGenerator(PartGenerator): def validate(self): p = self.params if p["outer_diameter"] <= 0: raise ValueError("外径必须大于0") if p["thickness"] <= 0: raise ValueError("厚度必须大于0") if p["center_hole_diameter"] >= p["outer_diameter"]: raise ValueError("中心孔直径不能大于等于外径") if p["bolt_hole_count"] and p["bolt_circle_radius"]: max_r = p["outer_diameter"] / 2 - p["bolt_hole_diameter"] / 2 if p["bolt_circle_radius"] > max_r: raise ValueError("螺栓孔分布圆半径超出外径范围") def build(self): p = self.params model = cq.Workplane("XY").circle(p["outer_diameter"] / 2).extrude(p["thickness"]) if p["center_hole_diameter"]: model = model.faces(">Z").workplane().hole(p["center_hole_diameter"]) if p["bolt_hole_count"] and p["bolt_hole_diameter"]: model = ( model.faces(">Z").workplane() .polarArray(p["bolt_circle_radius"], 0, 360, p["bolt_hole_count"]) .hole(p["bolt_hole_diameter"]) ) self.model = model这个设计的好处是,新增零件类型只需要继承PartGenerator并实现validate和build两个方法,导出逻辑完全复用。我后来加了一个支架生成器,只写了不到 50 行代码就搞定了。
4.3 主流程串联与异常处理
主入口的逻辑很直接:接收文本、提取参数、选择生成器、生成模型、导出文件。但异常处理要做足,因为用户输入千奇百怪,什么情况都可能出现。
def text_to_cad(text, output_format="step", output_dir="./output"): # 第一步:参数提取 try: params = extract_params(text) except Exception as e: return {"success": False, "error": f"参数提取失败: {str(e)}"} # 第二步:选择生成器 part_type = params.get("part_type", "").lower() generators = { "flange": FlangeGenerator, "法兰": FlangeGenerator, "bracket": BracketGenerator, "支架": BracketGenerator, } generator_cls = generators.get(part_type) if not generator_cls: return {"success": False, "error": f"暂不支持的零件类型: {part_type}"} # 第三步:生成几何 try: generator = generator_cls(params) model = generator.generate() except ValueError as e: return {"success": False, "error": f"参数校验失败: {str(e)}"} except Exception as e: return {"success": False, "error": f"几何生成失败: {str(e)}"} # 第四步:导出文件 import os os.makedirs(output_dir, exist_ok=True) filename = f"{part_type}_{params['outer_diameter']}x{params['thickness']}" filepath = os.path.join(output_dir, f"{filename}.{output_format}") try: if output_format == "step": generator.export_step(filepath) elif output_format == "stl": generator.export_stl(filepath) else: return {"success": False, "error": f"不支持的输出格式: {output_format}"} except Exception as e: return {"success": False, "error": f"文件导出失败: {str(e)}"} return {"success": True, "filepath": filepath, "params": params}这个流程跑通之后,从输入一段文字到拿到 STEP 文件,整个过程在 3 秒以内。我实测了 20 个不同参数的法兰盘,全部一次通过,没有出现几何错误。
5. 实测中暴露的问题与我的处理方式
5.1 参数提取的稳定性问题
用大语言模型做参数提取,最大的问题是输出不稳定。同样的输入,今天提取出来是 6 个孔,明天可能变成 5 个。我分析了一下,主要是提示词里的示例不够明确,模型在边界情况上容易摇摆。解决办法是在提示词里加入 few-shot 示例,把常见的输入输出对放进去,让模型有明确的参照。
另一个问题是模型会"脑补"参数。用户没说厚度,模型可能根据"法兰盘"这个类型自动填一个 10mm。这在某些场景下是好事,但在需要精确控制的场景下就是灾难。我的做法是在提示词里明确要求"未提及的参数必须输出 null",然后在代码层面做默认值填充,这样至少默认值是我控制的,不是模型随机生成的。
5.2 几何生成的边界情况
几何内核虽然精确,但遇到极端参数也会出问题。比如螺栓孔直径大于分布圆周长除以孔数量,孔与孔之间就会重叠,布尔运算可能失败。还有中心孔直径接近外径时,壁厚太薄,倒角操作会报错。这些边界情况需要在validate方法里提前拦截,给出明确的错误提示,而不是让几何内核抛出一堆看不懂的异常。
我整理了一份常见边界情况检查清单,每次新增零件类型都会对照检查:
| 检查项 | 判断条件 | 处理方式 |
|---|---|---|
| 尺寸为正 | 所有尺寸参数 > 0 | 否则报错 |
| 孔径小于外径 | 中心孔直径 < 外径 | 否则报错 |
| 孔不重叠 | 分布圆周长 / 孔数 > 孔直径 × 1.5 | 否则警告 |
| 壁厚足够 | (外径 - 中心孔直径) / 2 > 2mm | 否则警告 |
| 阵列角度合理 | 360 / 孔数 > 孔直径对应角度 | 否则警告 |
5.3 文件导出的兼容性验证
生成的 STEP 文件我一般会用至少两个软件验证:FreeCAD 和在线 STEP 查看器。FreeCAD 验证几何完整性,在线查看器验证格式兼容性。有一次我生成的 STEP 文件在 FreeCAD 里打开正常,但导入某个商业 CAD 软件时提示"无效的 B-rep 数据",后来发现是 CadQuery 导出时的精度设置问题,调整tolerance参数后解决。
STL 文件的验证更简单,用 MeshLab 或者任何三维查看器打开,检查有没有破面、法向量反转。如果发现破面,通常是tolerance设得太大,减小到 0.005mm 一般能解决。
提示:批量生成模型时,建议加一个自动验证步骤,用
cadquery重新读取生成的 STEP 文件,检查体积、包围盒尺寸是否和预期一致。这一步能拦截大部分导出异常。
6. 这套方案还能怎么扩展
text-to-cad目前的实现覆盖了规则几何体的生成,但实际工程需求远不止于此。我后续打算从几个方向继续扩展。第一个方向是装配体生成,支持多个零件的相对位置描述,比如"在底板上方 50mm 处安装一个法兰盘,用 4 个 M6 螺栓固定"。这需要引入装配约束的解析和求解,复杂度比单零件高一个量级。
第二个方向是参数化模板库。与其每次都让模型从零生成几何,不如预先做好一批参数化模板,文本解析只负责填充参数。这样生成速度更快,几何质量也更可控。模板库可以用 YAML 定义,每个模板描述参数槽位和对应的 CadQuery 代码片段。
第三个方向是和仿真流程的深度集成。生成 URDF 之后,自动调用仿真环境做一次简单的运动学验证,确认关节运动范围、碰撞检测没有问题,再把结果反馈给用户。这样用户拿到的不仅是一个模型文件,而是一个经过验证的可仿真模型。
Python 生态在这方面有天然优势,CadQuery、Trimesh、PyBullet 这些库可以无缝衔接。我试过用 PyBullet 加载生成的 URDF 做碰撞检测,整个流程跑下来不到 10 秒,对于快速迭代设计来说完全够用。
最后分享一个我在实际项目中总结的小技巧:文本描述里加上尺寸公差和材料信息,虽然不影响几何生成,但可以在导出时写入 STEP 文件的元数据里,后续做工艺规划时直接读取,省去手工录入的麻烦。CadQuery 支持在导出时附加自定义属性,具体做法是给model对象设置val().wrapped的属性,不过这个操作比较底层,需要参考 OCCT 的文档来写。