1. 从一段文字到三维模型:text-to-cad 到底在解决什么问题
第一次听到 text-to-cad 这个词,很多人脑子里冒出来的画面可能是:对着电脑敲一句“给我画一个法兰盘”,然后屏幕上就自动出现一个带螺栓孔的 STEP 模型。这个画面放在三五年前还属于科幻范畴,但放到今天,它已经是一条可以跑通的技术链路了。text-to-cad 本质上做的事情,是把自然语言描述翻译成参数化几何数据,再输出成 CAD 领域通用的文件格式,比如 STEP、GLB、STL 这些。它解决的核心痛点很直接:传统 CAD 建模需要人手动拉伸、旋转、倒角、打孔,一个中等复杂度的零件,熟手也要画上半小时到几小时;而 text-to-cad 想做的,是让“描述需求”这件事本身变成建模动作。
我接触这个方向是从一个很具体的场景开始的。当时手上有一批非标件需要快速出概念模型,用来做装配干涉检查,精度要求不高,但数量多、迭代快。用传统方式一个个画,时间成本完全扛不住。于是我开始研究能不能用代码和语言模型把这条路自动化。实测下来,text-to-cad 目前最适合的定位是概念设计阶段的快速原型生成,而不是替代精密工程出图。它能帮你把想法在几十秒内变成一个可视化的三维实体,导出 STL 直接丢进切片软件,或者导出 STEP 进入后续的 CAD 流程做二次编辑。
这篇文章适合几类人看:一是做机械设计、工业设计,想了解 AI 辅助建模边界在哪的工程师;二是做 3D 打印、创客项目,需要快速把文字想法变成可打印模型的玩家;三是做软件开发,想在自己的工具链里集成 text-to-cad 能力的程序员。不管你用的是中望 CAD、SolidWorks 还是 FreeCAD,底层的几何逻辑是相通的。我会把整条链路的思路、关键参数、踩过的坑都摊开讲,尽量让你看完就能自己搭一套跑起来。
2. 整体架构设计:为什么是“语言模型 + 几何内核”这套组合
2.1 核心链路的四个阶段拆解
text-to-cad 的完整链路,我把它拆成四个阶段:语义解析、参数提取、几何构建、格式导出。这四个阶段环环相扣,任何一个环节出问题,最终模型都会跑偏。
语义解析阶段,输入是一句人话,比如“一个外径 80mm、内径 40mm、厚度 10mm 的圆环,边缘倒角 2mm”。这一步要做的不是简单关键词匹配,而是理解尺寸之间的从属关系——外径和内径是同心圆,厚度是拉伸方向,倒角作用在边缘。早期我用正则表达式硬匹配,遇到稍微绕一点的描述就崩,后来换成语言模型做结构化抽取,稳定性提升非常明显。
参数提取阶段,是把解析结果映射成一组数值和约束。这里有个关键决策:用参数化建模还是用网格建模。参数化建模输出的是带特征的实体,可以后续修改尺寸;网格建模输出的是三角面片,改起来就麻烦了。text-to-cad 如果目标是工程可用,必须走参数化路线,这也是为什么 STEP 格式比 STL 更有价值——STEP 保留了几何拓扑关系,STL 只是一堆三角形。
几何构建阶段,是真正调用几何内核的地方。常见的选择有 OpenCASCADE、CGAL、Manifold 这几套。OpenCASCADE 功能最全,STEP 支持最好,但学习曲线陡;Manifold 轻量、速度快,适合做布尔运算密集的场景,但格式支持少一些。我最终选的是 OpenCASCADE,原因后面细说。
格式导出阶段,就是把内核里的实体序列化成文件。STEP 用于工程交换,GLB 用于网页预览和渲染,STL 用于 3D 打印。三种格式面向三种下游,缺一不可。
2.2 为什么选 OpenCASCADE 而不是自己造轮子
有人可能会想,不就是画几个圆柱、方块、圆环吗,自己用 numpy 算顶点坐标不就行了。我一开始也这么天真过。手写一个立方体确实不难,八个顶点十二条边,闭着眼睛都能写。但一旦涉及布尔运算——比如从一个大圆柱里挖掉一个小圆柱——事情就复杂了。两个实体相交的边界需要重新计算,产生新的边和面,还要保证法线方向一致、没有自相交。这套东西自己实现,没有几个月根本做不稳。
OpenCASCADE 是工业级的几何内核,布尔运算、倒角、圆角、抽壳这些操作都是现成的,而且经过了几十年的工程验证。它的 Python 绑定 pythonocc 用起来也还算顺手。代价是包比较大,安装稍微折腾,但在概念设计这个场景下,稳定性比轻量更重要。我试过用 Manifold 做快速原型,简单形状确实快,但一到复杂倒角就出各种退化面,最后还是回到 OpenCASCADE。
提示:如果你只是想做网页端轻量预览,Manifold 加 three.js 的组合足够;但如果要导出 STEP 进入正式 CAD 流程,OpenCASCADE 是绕不开的选择。
2.3 语言模型在链路中的角色定位
语言模型在这条链路里只做一件事:把非结构化文本变成结构化 JSON。它不负责几何计算,也不负责文件读写。这个边界一定要划清楚,否则很容易陷入“让模型直接输出坐标点”的误区。我试过让模型直接生成顶点列表,结果它编造出来的坐标经常自相矛盾,围不成一个封闭实体。
正确的做法是定义一套中间表示,比如:
{ "type": "ring", "outer_diameter": 80, "inner_diameter": 40, "thickness": 10, "chamfer": 2, "unit": "mm" }模型只负责把“外径八十内径四十厚十毫米倒角两毫米的圆环”翻译成上面这段 JSON。几何构建由确定性代码完成,这样每次同样的输入,输出完全一致,可复现、可测试。这个设计决策是我踩了无数次坑之后才定下来的,早期让模型自由发挥,结果同一个描述跑两次出来两个不同的模型,根本没法用。
3. 核心细节解析:从文字到实体的关键环节
3.1 语义解析的粒度控制与容错设计
语义解析最难的地方不是理解标准描述,而是处理模糊、口语化、有歧义的输入。用户不会像写工程图纸那样严谨,他可能说“来个粗一点的管子”,也可能说“直径大概五厘米左右”。这时候如果解析器太死板,直接报错,体验就很差。
我的做法是引入默认值兜底和置信度标注。比如“粗一点”映射到默认壁厚 3mm,同时在输出 JSON 里加一个confidence字段,标记哪些参数是明确给出的,哪些是猜的。后续几何构建时,低置信度的参数可以用不同颜色高亮显示,提醒用户确认。这个设计在实际使用中非常有用,用户一眼就能看出哪些地方需要自己核对。
另一个关键是单位处理。CAD 领域毫米和米混用是常态,有人习惯说“10 个厚”,有人习惯说“0.01 米”。解析器必须统一到毫米,并且在 JSON 里显式记录单位。我遇到过因为单位没统一,一个本该 80mm 的圆环变成了 80m,导出 STL 后切片软件直接崩溃的情况。
3.2 几何构建中的参数校验与自动修正
拿到结构化参数后,不能直接丢给几何内核,必须先做一轮参数校验。常见的非法情况包括:内径大于外径、厚度为零或负数、倒角半径超过最小边长的二分之一。这些如果不在构建前拦住,内核会抛出各种难以理解的异常,排查起来很痛苦。
我整理了一份校验规则表,放在几何构建之前执行:
| 校验项 | 规则 | 处理方式 |
|---|---|---|
| 直径关系 | 内径 < 外径 | 自动交换或报错 |
| 厚度 | > 0 且 < 外径 | 超限时截断并警告 |
| 倒角 | < 最小壁厚 / 2 | 自动缩小到安全值 |
| 单位 | 必须为 mm/m | 统一转换为 mm |
| 孔位 | 不超出实体边界 | 裁剪或报错 |
这张表看起来简单,但每一条都是实际踩坑总结出来的。特别是倒角那条,倒角半径设得太大,OpenCASCADE 会直接生成失败,而且报错信息非常隐晦,新手根本看不懂。提前校验能省掉大量调试时间。
3.3 三种导出格式的适用场景与参数选择
STEP、GLB、STL 这三种格式,很多人搞不清楚什么时候该用哪个。我用一张表说清楚:
| 格式 | 本质 | 适用场景 | 关键参数 |
|---|---|---|---|
| STEP | 边界表示实体 | 工程交换、二次编辑 | 精度模式、单位 |
| GLB | 三角网格 + 材质 | 网页预览、渲染 | 网格细分精度 |
| STL | 纯三角网格 | 3D 打印、切片 | 弦高、角度公差 |
STEP 导出时,精度模式选“精确”还是“近似”影响很大。精确模式保留解析曲面,文件小、质量高,但某些下游软件兼容性差;近似模式把所有曲面三角化,兼容性好但文件大。我的经验是,如果下游是主流 CAD 软件,用精确模式;如果是自研工具或网页端,用近似模式。
STL 导出的弦高参数直接决定打印质量。弦高设得小,曲面更光滑,但三角形数量暴增,文件可能从几 MB 涨到几百 MB。我一般根据模型尺寸动态设置:小零件用 0.01mm,大零件用 0.1mm。角度公差同理,默认 0.5 弧度对大多数场景够用,追求极致光滑可以调到 0.1。
注意:GLB 导出时如果模型有内部空腔,记得开启双面渲染,否则从某些角度看会“透”过去,看起来像破面。
4. 实操过程:从零搭一套可跑的 text-to-cad 流程
4.1 环境准备与依赖安装
先把环境搭起来。我用的技术栈是 Python + pythonocc + 一个语言模型 API。pythonocc 的安装是第一个坎,不同操作系统差异很大。
在 Linux 上相对简单:
conda install -c conda-forge pythonocc-coreWindows 上建议用 conda 而不是 pip,因为 pip 版本的 pythonocc 经常缺 DLL。macOS 上 conda 同样是最省心的路径。装完之后跑一句from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder验证一下,不报错就说明环境 OK。
语言模型这块,我用的是通用的文本生成接口,输入提示词,输出 JSON。提示词的设计很关键,我后面单独讲。文件读写用标准库就够,STEP 和 STL 的导出 pythonocc 都有现成接口,GLB 需要额外装一个 trimesh 做转换。
4.2 提示词工程:让模型稳定输出结构化 JSON
提示词写得好不好,直接决定解析成功率。我试过很多版本,最终稳定下来的结构是这样的:
你是一个 CAD 参数解析器。用户会用自然语言描述一个三维零件。 请把描述转换成如下 JSON 格式,只输出 JSON,不要任何解释: { "type": "零件类型", "params": { ... }, "unit": "mm", "confidence": { ... } } 支持的零件类型:cylinder, box, ring, sphere, plate_with_holes。 如果某个参数未给出,使用合理默认值,并在 confidence 中标记为 low。这个提示词有几个要点:一是限定输出格式,明确说只输出 JSON;二是给出类型白名单,防止模型自由发挥造出无法处理的类型;三是要求标注置信度,方便后续校验。实测下来,加了这几条之后,解析成功率从六成提升到九成以上。
对于复杂零件,比如“一个 100x60x10 的板,四角各有一个直径 6 的通孔,孔中心距边缘 10mm”,模型也能正确解析成 plate_with_holes 类型,参数完整。这种描述如果靠正则,基本没戏。
4.3 几何构建代码的核心逻辑
几何构建这块,我以圆环为例展示核心逻辑。圆环的本质是两个同心圆柱做布尔差集:
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 def make_ring(outer_d, inner_d, thickness): axis = gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)) outer = BRepPrimAPI_MakeCylinder(axis, outer_d/2, thickness).Shape() inner = BRepPrimAPI_MakeCylinder(axis, inner_d/2, thickness).Shape() ring = BRepAlgoAPI_Cut(outer, inner).Shape() return ring这段代码看起来简单,但有几个细节要注意。坐标轴的方向决定拉伸方向,如果设成 (1,0,0),圆环就横过来了。布尔运算的顺序不能反,大减小才是挖孔,小减大得到的是空集。还有,布尔运算之后最好做一次ShapeUpgrade_UnifySameDomain,把共面的碎片合并,否则导出的 STEP 里会有大量冗余面,下游软件打开很卡。
倒角操作在布尔运算之后做:
from OCC.Core.BRepFilletAPI import BRepFilletAPI_MakeChamfer from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_EDGE chamfer = BRepFilletAPI_MakeChamfer(ring) explorer = TopExp_Explorer(ring, TopAbs_EDGE) while explorer.More(): chamfer.Add(2.0, explorer.Current()) explorer.Next() result = chamfer.Shape()这里遍历所有边加倒角,实际项目中通常只对特定边操作,需要根据边的几何特征筛选。全部加倒角在圆环上效果还行,但在复杂零件上会把不该倒的边也倒了,看起来很奇怪。
4.4 导出环节的参数配置与实测数据
导出 STEP 的代码:
from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs writer = STEPControl_Writer() writer.Transfer(shape, STEPControl_AsIs) writer.Write("output.step")STEPControl_AsIs表示按原样导出,保留解析曲面。如果换成STEPControl_ManifoldSolidBrep,会做一些简化。实测一个外径 80 内径 40 厚 10 的圆环,精确模式导出文件约 12KB,近似模式约 45KB。文件大小差异在简单零件上不明显,但复杂装配体上能差十倍。
STL 导出:
from OCC.Core.StlAPI import StlAPI_Writer from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh BRepMesh_IncrementalMesh(shape, 0.01, False, 0.5, True) writer = StlAPI_Writer() writer.Write(shape, "output.stl")BRepMesh_IncrementalMesh的第一个参数是弦高,第二个是角度公差。弦高 0.01mm 对 80mm 的圆环来说,圆周被分成约 250 段,表面非常光滑。如果调到 0.1mm,段数降到 80 左右,肉眼在屏幕上几乎看不出差别,但文件小很多。这个权衡要根据实际用途来定。
GLB 导出需要先把 OCC 的实体转成 trimesh 的网格,再导出。这一步稍微绕,但网上有现成的转换脚本可以参考。转换过程中要注意坐标系,OCC 默认 Z 轴向上,GLB 常用 Y 轴向上,需要做一次旋转。
5. 常见问题与排查技巧实录
5.1 解析失败与模型跑偏的典型场景
实际用下来,问题主要集中在几个地方。第一类是描述过于抽象,比如“画个好看的东西”,这种模型也无能为力,只能返回默认的方块。第二类是尺寸矛盾,比如“直径 10mm 的管子,壁厚 8mm”,内径就变成负数了,校验层会拦截并提示。第三类是类型识别错误,把“圆盘”识别成“圆柱”,虽然都是圆形,但一个扁一个高,差别很大。
针对类型识别,我在提示词里加了几个易混淆类型的区分说明,比如“圆盘是厚度远小于直径的圆柱,圆柱是厚度和直径相当的圆形实体”。加了这段之后,误判率明显下降。
5.2 几何内核报错的排查思路
OpenCASCADE 的报错信息出了名的难懂,经常是一串十六进制地址加一句“Standard_Failure”。我的排查套路是:先看是不是参数非法,用校验层拦住;再看是不是布尔运算失败,尝试调整运算顺序或加一个微小的偏移;最后看是不是内存问题,大模型有时候会爆内存。
布尔运算失败最常见的原因是两个实体恰好相切,比如内径刚好等于外径,或者孔刚好在边缘上。这种临界情况内核处理不好,解决办法是加一个 0.001mm 的容差,让它们要么完全相交,要么完全分离。
5.3 导出文件在下游软件打不开的原因
STEP 文件在 SolidWorks 里打不开,十有八九是单位设置不一致。OCC 默认单位是毫米,但有些导出设置会写成米,下游软件按米解析,模型就变得巨大或极小。检查方法很简单,用文本编辑器打开 STEP 文件,看头部有没有SI_UNIT和MILLI字样。
STL 文件在切片软件里显示破面,通常是法线方向不一致。OCC 导出的 STL 默认法线朝外,但如果模型有内部空腔,内壁的法线可能朝内,切片软件就分不清哪里是实体哪里是空气。解决办法是在导出前统一法线方向,或者用切片软件的修复功能自动处理。
GLB 在网页端加载后颜色不对,多半是材质丢失。GLB 支持 PBR 材质,但 OCC 转出来的网格默认没有材质信息,需要在 trimesh 里手动指定一个基础材质。
5.4 性能优化:从几十秒到几秒的提速经验
早期我的流程跑一个中等零件要三十多秒,主要时间花在语言模型调用和几何构建上。优化之后降到五秒以内,几个关键改动:
- 缓存解析结果:相同的描述直接返回缓存的 JSON,省掉模型调用。
- 并行构建:多个零件同时构建时用多进程,OCC 的 GIL 限制用多进程绕开。
- 降低网格精度:预览用低精度,导出用高精度,分两档。
- 延迟倒角:倒角操作最耗时,放到最后做,且只对必要边做。
这几条改下来,体验提升非常明显。特别是缓存那条,实际使用中大量描述是重复或相似的,缓存命中率能到四成以上。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 模型尺寸离谱 | 单位不一致 | 检查 JSON 的 unit 字段 | 统一转 mm |
| 布尔运算失败 | 实体相切 | 打印两个实体的包围盒 | 加 0.001mm 容差 |
| STEP 打不开 | 精度模式不兼容 | 换近似模式重导 | 用 STEPControl_ManifoldSolidBrep |
| STL 破面 | 法线不一致 | 在切片软件里看 | 导出前统一法线 |
| 倒角生成失败 | 半径过大 | 检查最小边长 | 自动缩小到边长一半 |
| 解析结果为空 | 描述太抽象 | 看模型原始输出 | 补充默认值兜底 |
| 导出文件巨大 | 网格精度过高 | 看三角形数量 | 调大弦高参数 |
| GLB 颜色丢失 | 无材质信息 | 检查 trimesh 材质 | 手动指定基础材质 |
这张表我贴在工位上,遇到问题先查表,八成能直接定位。剩下两成再深入调试。
6. 这套流程还能怎么扩展
跑通基础链路之后,我陆续加了一些扩展能力,实用性提升不少。一个是批量生成,读一个 CSV,每行一个描述,批量出模型,适合做系列化零件的概念设计。另一个是参数化模板,把常用零件做成模板,用户只填关键尺寸,减少模型解析的不确定性。还有一个是版本对比,同一个描述生成多个版本,并排显示,方便挑选。
再往深了走,可以接约束求解器,让用户用“A 面平行于 B 面”这种关系式描述,而不是死板的尺寸。这块我还在摸索,目前用语言模型做关系抽取,再映射到 OCC 的约束接口,效果还不稳定,但方向是对的。
最后分享一个我踩过的小坑:导出 STL 时如果模型有薄壁结构,弦高设得太大会导致薄壁消失,打印出来是个破洞。解决办法是对薄壁区域单独加密网格,或者干脆把弦高设小一档。这个坑我是在打印一个壁厚 1mm 的盒子时发现的,切片预览里看着好好的,打出来侧面全是缝,排查了半天才定位到弦高参数上。