1. 从一句话到三维模型:text-to-cad 到底在解决什么问题
第一次听到 text-to-cad 这个词,我的反应是:这不就是把自然语言变成 CAD 模型吗?听起来像是给工程师省事的工具,但真正动手做过之后才发现,它背后牵扯的东西远比想象中复杂。简单来说,text-to-cad 是一类将文本描述直接转换为计算机辅助设计(CAD)文件的技术方案,输出格式通常包括 STEP、URDF、G-code 等。它的核心价值在于:让不会画图的人也能通过文字描述生成可用的三维模型,让会画图的人从重复性的建模劳动中解放出来。
我最初接触这个方向是因为一个实际需求——手头有一批参数化的零件需要反复调整尺寸,每次改动都要在 CAD 软件里重新拉伸、打孔、倒角,效率极低。后来我想,能不能用代码或者文字描述来驱动这些操作?顺着这个思路摸索下去,发现 text-to-cad 这个领域已经有不少成熟的方案和工具链了。
这篇文章适合几类人看:一是对 CAD 建模有基础但想探索自动化路径的工程师;二是做机器人仿真、需要批量生成 URDF 模型的开发者;三是做增材制造、需要从文本直接生成 G-code 的从业者;四是对 AI 辅助设计感兴趣、想了解当前技术边界的产品经理和研究者。不管你是哪种,我都会从实际操作的角度的出发,把踩过的坑和验证过的方案讲清楚。
2. 核心技术路线拆解:从文本到 CAD 文件的完整链路
2.1 文本解析与意图理解层
text-to-cad 的第一步永远是对输入文本的解析。这里的文本可能是“一个长 50mm、宽 30mm、高 20mm 的长方体,中心有一个直径 10mm 的通孔”,也可能是“一个法兰盘,外径 100mm,内径 60mm,厚度 15mm,均匀分布 6 个直径 8mm 的螺栓孔”。这两种描述的复杂度不同,解析难度也完全不同。
目前主流的做法有两种:一种是基于规则模板的解析,另一种是基于大语言模型的语义理解。规则模板的好处是稳定、可控,适合参数化程度高、描述格式固定的场景。比如你可以定义一套模板:“创建一个[形状],尺寸为[长]x[宽]x[高],在[位置]添加[特征]”。用户输入只要匹配这个模板,就能被准确解析成结构化的参数。
但规则模板的局限性也很明显——用户不会总是按照你预设的格式说话。这时候就需要大语言模型来兜底。我实测下来,用 LLM 做意图理解和参数抽取,准确率在简单几何体上能到 90% 以上,但一旦涉及复杂装配体或者工程语义(比如“配合公差”、“表面粗糙度”),准确率就会明显下降。所以我的建议是:规则模板处理高频简单场景,LLM 处理长尾复杂场景,两者结合使用。
注意:LLM 输出的参数一定要做合法性校验。我遇到过模型把“直径 10mm”理解成“半径 10mm”的情况,如果不校验直接建模,出来的零件直接报废。
2.2 几何建模与特征生成层
文本解析完之后,下一步就是把结构化参数变成实际的几何体。这一步的核心工具是几何内核,常见的有 OpenCASCADE、CGAL、OpenSCAD 等。OpenCASCADE 是工业级的内核,功能强大但学习曲线陡峭;OpenSCAD 更轻量,适合快速原型,但复杂曲面能力有限。
我个人的选择是:如果目标是输出 STEP 文件用于后续工程加工,优先用 OpenCASCADE;如果只是做概念验证或者简单可视化,OpenSCAD 足够用。以 OpenCASCADE 为例,你可以用 Python 的pythonocc-core库来调用它,代码大概长这样:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox from OCC.Core.gp import gp_Pnt # 创建一个 50x30x20 的长方体 box = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 50, 30, 20).Shape()这段代码创建了一个长方体,但还没有打孔。打孔需要用到布尔运算:
from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder # 创建一个直径 10mm 的圆柱体作为孔 cylinder = BRepPrimAPI_MakeCylinder(5, 20).Shape() # 布尔减运算 result = BRepAlgoAPI_Cut(box, cylinder).Shape()这里有个细节:圆柱体的位置默认在原点,如果孔不在原点,你需要先做平移变换。我见过不少人在这里翻车——孔的位置偏了,但视觉上不明显,直到导出 STEP 文件去加工才发现问题。
2.3 文件导出与格式转换层
生成几何体之后,最后一步是导出成目标格式。不同的应用场景需要不同的格式:
| 格式 | 适用场景 | 特点 |
|---|---|---|
| STEP | 工程加工、跨软件交换 | 保留精确几何信息,通用性强 |
| URDF | 机器人仿真 | 描述连杆和关节,配合 Coppeliasim 等仿真环境 |
| G-code | 增材制造、CNC 加工 | 直接驱动机床,与具体设备强相关 |
| STL | 3D 打印预览 | 三角网格,丢失精确几何信息 |
导出 STEP 用 OpenCASCADE 的STEPControl_Writer就行:
from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs writer = STEPControl_Writer() writer.Transfer(result, STEPControl_AsIs) writer.Write("output.step")导出 URDF 就复杂一些,因为 URDF 本质上是 XML 格式的机器人描述文件,你需要定义 link 和 joint 的层级关系。如果你的模型是单个零件,导出 URDF 意义不大;但如果是多关节的机械臂,就需要把每个连杆的几何体和关节的旋转轴都描述清楚。我一般会先用 CAD 生成各个连杆的 STEP 文件,再用脚本把它们组装成 URDF。
G-code 的生成又是另一条路。你需要先切片(slicing),再根据切片结果生成路径。常用的工具是 Cura 的引擎或者 PrusaSlicer 的命令行接口。这里的关键参数是层高、填充率、打印速度,这些参数直接决定最终成品的质量和强度。
3. 实操环境搭建:从零开始跑通一条 text-to-cad 流水线
3.1 工具选型与依赖安装
我推荐的技术栈是:Python 3.10+、pythonocc-core、trimesh、numpy。如果你需要 LLM 做文本解析,再加一个 openai 或者本地模型的 SDK。安装 pythonocc-core 是最容易出问题的一步,因为它依赖 OpenCASCADE 的底层库。
在 Ubuntu 上,你可以用 conda 安装:
conda install -c conda-forge pythonocc-core在 Windows 上,我建议直接用 conda 而不是 pip,因为 pip 版本的 pythonocc-core 经常缺少预编译的二进制文件。如果你坚持用 pip,可能需要自己编译 OpenCASCADE,那个过程相当痛苦。
实操心得:我试过在 Windows 上用 pip 装 pythonocc-core,折腾了两个小时都没成功,最后换 conda 五分钟搞定。所以如果你在 Windows 上,别跟自己过不去,直接用 conda。
3.2 文本解析模块的实现
假设我们要处理这样一段输入:“创建一个法兰盘,外径 120mm,内径 80mm,厚度 20mm,均匀分布 8 个直径 10mm 的螺栓孔,螺栓孔中心距外径边缘 15mm。”
首先定义一个解析函数,把这段文字变成结构化参数:
import re def parse_flange(text): params = {} params['outer_diameter'] = float(re.search(r'外径\s*(\d+)', text).group(1)) params['inner_diameter'] = float(re.search(r'内径\s*(\d+)', text).group(1)) params['thickness'] = float(re.search(r'厚度\s*(\d+)', text).group(1)) params['bolt_count'] = int(re.search(r'(\d+)\s*个', text).group(1)) params['bolt_diameter'] = float(re.search(r'直径\s*(\d+)mm\s*的螺栓孔', text).group(1)) params['edge_distance'] = float(re.search(r'边缘\s*(\d+)mm', text).group(1)) return params这个正则表达式版本只适合格式固定的输入。如果用户说“做一个圆盘,外面直径 12 厘米,中间挖个 8 厘米的洞”,正则就匹配不上了。这时候就需要 LLM 出场:
def parse_with_llm(text): prompt = f"""从以下描述中提取法兰盘参数,以 JSON 格式返回: 描述:{text} 需要的字段:outer_diameter, inner_diameter, thickness, bolt_count, bolt_diameter, edge_distance 单位统一为毫米。""" # 调用 LLM API 并解析返回的 JSON response = call_llm(prompt) return json.loads(response)3.3 几何建模的完整实现
拿到参数之后,开始建模。法兰盘的本质是一个大圆柱减去一个小圆柱,再减去若干个螺栓孔圆柱。用 pythonocc-core 实现:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut from OCC.Core.gp import gp_Ax2, gp_Pnt, gp_Dir import math def build_flange(params): # 外圆柱 outer_radius = params['outer_diameter'] / 2 thickness = params['thickness'] outer = BRepPrimAPI_MakeCylinder(outer_radius, thickness).Shape() # 内圆柱(中心孔) inner_radius = params['inner_diameter'] / 2 inner = BRepPrimAPI_MakeCylinder(inner_radius, thickness).Shape() result = BRepAlgoAPI_Cut(outer, inner).Shape() # 螺栓孔 bolt_radius = params['bolt_diameter'] / 2 bolt_circle_radius = outer_radius - params['edge_distance'] for i in range(params['bolt_count']): angle = 2 * math.pi * i / params['bolt_count'] x = bolt_circle_radius * math.cos(angle) y = bolt_circle_radius * math.sin(angle) axis = gp_Ax2(gp_Pnt(x, y, 0), gp_Dir(0, 0, 1)) bolt_hole = BRepPrimAPI_MakeCylinder(axis, bolt_radius, thickness).Shape() result = BRepAlgoAPI_Cut(result, bolt_hole).Shape() return result这段代码里有个关键点:螺栓孔的定位。bolt_circle_radius是螺栓孔中心所在的圆半径,它等于外半径减去边缘距离。角度用2 * pi * i / n计算,保证均匀分布。我见过有人用360 / n算角度然后传给cos和sin,结果全错了——因为 Python 的三角函数用的是弧度不是角度。
3.4 导出与验证
建模完成后,导出 STEP 文件:
from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs def export_step(shape, filename): writer = STEPControl_Writer() writer.Transfer(shape, STEPControl_AsIs) writer.Write(filename)导出之后,我强烈建议用 FreeCAD 或者 CAD Assistant 打开检查一下。我遇到过布尔运算失败导致模型有破面的情况,在代码里看不出来,但打开文件一看就发现孔没打穿或者多了一块。
注意事项:布尔运算的顺序会影响结果。先减中心孔再减螺栓孔,和反过来,在某些边界情况下结果不同。我的经验是先从大的特征开始减,小的特征后减,这样稳定性更好。
4. 不同输出格式的实战要点与避坑指南
4.1 STEP 文件导出的精度控制
STEP 文件有两种模式:精确几何(exact geometry)和近似几何(approximate geometry)。默认情况下 OpenCASCADE 导出的是精确几何,文件体积小、精度高。但如果你在建模过程中用了网格化操作(比如从 STL 导入的模型),导出的 STEP 可能就变成了近似几何。
我踩过的一个坑是:从 STL 文件导入一个模型,做了布尔运算之后导出 STEP,结果文件体积暴涨到几百 MB。原因是 STL 的三角网格在布尔运算后产生了大量碎片面。解决办法是先用ShapeFix_Shape做修复和简化:
from OCC.Core.ShapeFix import ShapeFix_Shape fixer = ShapeFix_Shape(shape) fixer.Perform() fixed_shape = fixer.Shape()4.2 URDF 导出的关节定义
URDF 的核心是 link 和 joint。link 是刚体,joint 是连接两个 link 的关节。如果你要把一个多关节机械臂的 CAD 模型转成 URDF,需要做几件事:
- 把每个连杆单独导出成 STL 或 STEP
- 定义每个关节的类型(旋转、平移、固定)
- 定义关节的旋转轴和限位
- 定义每个 link 的惯性矩阵
惯性矩阵是最容易被忽略的部分。很多人只填了几何形状就导出 URDF,结果在 Coppeliasim 里仿真时物理行为完全不对——因为仿真引擎需要惯性信息来计算动力学。你可以用 trimesh 来计算惯性矩:
import trimesh mesh = trimesh.load('link.stl') mesh.density = 1000 # 设置材料密度 kg/m^3 inertia = mesh.moment_inertia然后在 URDF 里填入对应的值。这里要注意单位:URDF 默认用米和千克,如果你的 CAD 模型是毫米,需要先缩放。
4.3 G-code 生成的关键参数
从 CAD 到 G-code 的链路通常是:CAD → STL → 切片 → G-code。切片参数直接决定打印结果:
| 参数 | 推荐值 | 影响 |
|---|---|---|
| 层高 | 0.1-0.3mm | 越小精度越高,但打印时间越长 |
| 填充率 | 15%-30% | 影响强度和材料消耗 |
| 打印速度 | 40-80mm/s | 太快会导致层间粘合不良 |
| 喷嘴温度 | 根据材料定 | PLA 约 200°C,ABS 约 240°C |
我一般用 PrusaSlicer 的命令行模式来批量生成 G-code:
prusa-slicer --export-gcode --layer-height 0.2 --fill-density 20% model.stl实操心得:批量生成 G-code 时,一定要给每个文件加上参数标识,比如
model_layer02_fill20.gcode。否则几十个文件混在一起,你根本分不清哪个是哪个参数生成的。
5. 常见问题排查与性能优化实录
5.1 布尔运算失败的排查思路
布尔运算失败是 text-to-cad 里最常见的问题。表现是:代码不报错,但导出的模型有破面或者缺失特征。排查步骤:
- 检查两个几何体是否有重叠区域。如果没有重叠,布尔减运算会返回空或者原形状。
- 检查几何体是否有效。用
BRepCheck_Analyzer验证:
from OCC.Core.BRepCheck import BRepCheck_Analyzer analyzer = BRepCheck_Analyzer(shape) print(analyzer.IsValid())- 如果几何体无效,用
ShapeFix_Shape修复后再运算。 - 如果还是失败,尝试调整运算顺序,或者把复杂布尔运算拆成多步。
5.2 大模型解析的稳定性优化
用 LLM 做文本解析时,输出格式不稳定是最大的痛点。同一个描述,模型可能这次返回 JSON,下次返回 Markdown 表格。我的解决方案是:
- 在 prompt 里明确要求“只返回 JSON,不要任何其他文字”
- 用
temperature=0降低随机性 - 加一层后处理,用正则从返回文本里提取 JSON 部分
- 对关键参数做范围校验,超出合理范围就报错
def safe_parse(response_text): # 尝试直接解析 try: return json.loads(response_text) except: pass # 尝试从文本中提取 JSON match = re.search(r'\{.*\}', response_text, re.DOTALL) if match: return json.loads(match.group()) raise ValueError("无法解析 LLM 返回内容")5.3 批量生成的性能瓶颈
如果你需要生成几百个模型,性能就成了问题。我实测下来,单个法兰盘建模加导出 STEP 大约需要 0.5-1 秒,其中大部分时间花在布尔运算上。优化方向:
- 用多进程并行处理,Python 的
multiprocessing模块就能搞定 - 如果几何体之间没有依赖关系,并行效率接近线性
- 导出 STEP 时关闭不必要的元数据写入,能省 10%-20% 的时间
from multiprocessing import Pool def process_one(params): shape = build_flange(params) export_step(shape, f"flange_{params['id']}.step") with Pool(8) as p: p.map(process_one, all_params)注意:OpenCASCADE 不是线程安全的,所以用多进程而不是多线程。多线程会导致随机崩溃,这个坑我踩过。
6. 从原型到落地:我的实际项目经验
6.1 一个真实的参数化零件生成案例
去年我接了一个小项目,需要为一批非标设备生成安装支架的 CAD 模型。支架的基本形状固定,但每个设备的安装孔位和尺寸都不同。手动建模的话,每个支架大概需要 15 分钟,200 个支架就是 50 个小时。
我用 text-to-cad 的思路做了一套自动化流程:把每个支架的参数写在 Excel 里,用脚本读取参数、生成模型、导出 STEP。整个流程跑完 200 个支架只用了 8 分钟。当然,前期调试花了大概两天时间,主要是处理各种边界情况——比如孔位太靠近边缘导致布尔运算失败、支架厚度太薄导致导出后破面等等。
这个项目的经验告诉我:text-to-cad 的价值不在于完全替代人工,而在于处理那些重复性高、参数化程度高的任务。对于真正需要创造力的设计工作,它目前还远远不够。
6.2 与 Coppeliasim 仿真的衔接
另一个让我印象深刻的场景是 URDF 导入 Coppeliasim。我帮一个做机器人研究的朋友把他们的机械臂 CAD 模型转成 URDF,然后在 Coppeliasim 里做运动仿真。过程中最大的问题是坐标系对齐——CAD 软件里的装配坐标系和 URDF 里的 link 坐标系往往不一致,需要手动调整每个 joint 的原点和轴向。
我的做法是:先在 CAD 里把每个连杆的坐标系调整到关节旋转中心,再导出 STL。然后在 URDF 里用<origin>标签微调位置。这个过程需要反复试错,但一旦调好,后续的仿真就很顺畅了。
6.3 当前方案的局限与改进方向
说实话,目前的 text-to-cad 方案还远没有到“输入一句话就出图”的程度。它的局限主要在三个方面:
第一,复杂语义理解不够。用户说“做一个好看的支架”,模型完全不知道“好看”是什么意思。工程语义里的“配合”、“公差”、“表面处理”更是难以用文本准确描述。
第二,几何特征的组合爆炸。简单几何体(圆柱、长方体、孔)的生成已经很成熟,但自由曲面、复杂扫掠、放样这些高级特征,目前还没有可靠的文本驱动方案。
第三,验证环节缺失。生成的模型是否满足工程要求,目前只能靠人工检查。未来如果能结合仿真验证(比如自动做应力分析),整个链路的可靠性会大幅提升。
我个人的判断是:text-to-cad 在参数化标准件、简单结构件领域会很快落地,但在复杂产品设计领域,它更多是辅助工具而不是替代方案。如果你现在想入局,建议从自己熟悉的细分场景切入,把一个小闭环跑通,比追求大而全的方案更有价值。
最后分享一个小技巧:如果你在 Windows 上做开发,建议用 WSL2 而不是原生 Windows。OpenCASCADE 在 Linux 下的编译和运行都稳定得多,而且 conda 的包管理也更顺畅。我切换过去之后,环境问题至少减少了 70%。