1. 从一段文字到三维实体:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多人脑子里浮现的画面大概是:对着电脑敲一句"给我画一个法兰盘",然后屏幕上就自动长出一个三维模型。这个想象不算离谱,但真正落地的时候,它解决的问题比"自动画图"要具体得多,也琐碎得多。
我接触这个方向是从一个很实际的需求开始的:手头有一批标准件,规格参数都躺在 Excel 表格里,但每次要出模型都得手动在 CAD 里拉伸、打孔、倒角,一个两个还好,上百个就是纯体力活。后来我想,既然参数是结构化的文本,模型是结构化的几何,那中间这段"翻译"工作为什么不能自动化?这就是 text-to-cad 的核心命题——把自然语言或结构化文本描述,转换成可用的 CAD 几何文件。
它输出的东西通常不是某个私有格式,而是几种通用交换格式:STEP(保留精确边界表示 B-rep,适合后续在专业 CAD 里继续编辑)、STL(三角网格,3D 打印和快速预览的通用语言)、GLB(带材质和层级信息的二进制 glTF,适合网页端和可视化场景)。这三种格式基本覆盖了从"要精度"到"要好看"到"要能打印"的全部需求。
适合看这篇内容的人,我大致分三类。第一类是做参数化建模的工程师,手里有大量重复性建模任务,想用脚本或 AI 提效;第二类是做 3D 打印和创客的玩家,想用一句话生成一个能打印的模型,省去学建模软件的时间;第三类是做工具链集成的开发者,想把 text-to-cad 能力嵌进自己的产品里,比如在线定制、电商预览、教育演示。这三类人的关注点不一样,但底层要打通的链路是同一套。
需要先泼一盆冷水:text-to-cad 目前不是"许愿机"。你输入"一个好看的杯子",它给不了你惊喜;但你输入"外径 80mm、内径 70mm、高 100mm、底部封闭的圆柱壳",它能给你一个精确的实体。所以理解它的能力边界,比盲目期待它"懂设计"要重要得多。下面我会把这条链路从文本解析、几何生成、格式导出到实际踩坑,一层层拆开讲。
2. 文本描述怎么变成几何参数:解析层的关键设计
2.1 为什么不能直接让大模型"画"出几何
很多人第一反应是:现在大模型这么强,直接让它输出 CAD 文件不就行了?我试过,结论是能出,但不能用。原因在于大模型生成的是 token 序列,它对"这个面是不是真的闭合""这两个实体有没有干涉""这个圆角半径是不是超过了壁厚"没有几何层面的校验能力。它可能给你一段看起来很像 STEP 的文本,但里面实体拓扑是错的,导入 CAD 直接报错。
所以正确的架构是两段式:第一段用语言模型做"语义到参数"的抽取,第二段用确定性的几何内核做"参数到实体"的构建。语言模型负责理解"外径 80、壁厚 5"这种表达,几何内核负责保证结果在数学上成立。这个分工是整个 text-to-cad 系统能不能稳定跑起来的分水岭。
2.2 参数抽取的三种输入形态
实际项目里,输入文本的规整程度差别很大,我一般按三种形态分别处理:
- 完全结构化:像
{"type":"cylinder","od":80,"id":70,"h":100}这种 JSON 或键值对。这种最好办,直接映射到几何内核的 API,几乎不需要语言模型介入,稳定性最高。 - 半结构化:像"圆柱壳,外径80,内径70,高100,单位毫米"。这种需要做实体识别和单位归一,用规则加小模型就能搞定,准确率能到 95% 以上。
- 自由自然语言:像"帮我做一个大概巴掌大的圆筒,壁别太厚"。这种最考验解析层,需要模型推断尺寸范围、默认壁厚、甚至补全缺失的约束。
我的经验是,不要指望解析层一步到位。更稳的做法是解析出一个"参数草案",然后回显给用户确认或微调。比如系统解析出"外径 80mm、壁厚 5mm",先展示出来让用户改,改完再生成。这一步交互能挡掉大量因为歧义导致的错误模型。
2.3 单位与坐标系:最容易被忽略的隐形坑
单位问题我在项目里栽过不止一次。文本里写"直径 80",到底是毫米还是厘米?如果解析层默认毫米,用户其实想的是厘米,出来的模型就小了十倍,3D 打印出来是个指甲盖大小的东西。强制单位归一是必须的:所有输入统一转成毫米,内部计算全程毫米,导出时再按目标格式决定是否转换。
坐标系同样关键。STEP 和 STL 对原点的约定不同,STL 很多切片软件默认模型要落在正 Z 半轴且底面贴 Z=0,而 STEP 里原点可以随便放。如果生成时不管坐标系,导出的 STL 可能在切片软件里"飘"在平台下方,用户还得手动挪。我的做法是在几何构建阶段就把模型底面归到 Z=0,几何中心对齐 X/Y 原点,这样导出的 STL 直接能打印,STEP 导入后位置也符合直觉。
2.4 约束求解:让参数"自洽"的那一步
参数抽出来不代表能直接建模。举个真实例子:用户说"外径 80、内径 85",这明显矛盾,内径比外径还大。如果直接丢给几何内核,要么报错,要么生成一个负壁厚的诡异实体。所以在参数和几何之间,需要一层约束校验:
| 校验项 | 规则 | 处理方式 |
|---|---|---|
| 尺寸正负 | 所有长度参数 > 0 | 报错并要求修正 |
| 内外关系 | 内径 < 外径 | 自动交换或提示 |
| 壁厚下限 | 壁厚 ≥ 最小可制造厚度 | 警告并建议值 |
| 圆角半径 | 半径 < 相邻壁厚 | 自动裁剪到安全值 |
| 孔位干涉 | 孔不超出实体边界 | 提示并高亮冲突 |
这层校验看起来不起眼,但它是"生成能用模型"和"生成报错模型"之间的那道闸门。我建议把校验规则做成可配置的,因为不同工艺(3D 打印、CNC、钣金)对最小壁厚、最小孔径的要求完全不同。
3. 几何内核选型:OpenCASCADE、CGAL 还是自研
3.1 为什么几何内核是 text-to-cad 的心脏
解析层把文本变成了参数,接下来要把参数变成真正的几何体。这一步靠的是几何建模内核,它负责布尔运算、倒角、抽壳、曲面求交这些底层操作。内核选得好,后面省一半力气;选得不好,光是布尔运算的稳定性问题就能拖垮整个项目。
目前开源圈子里主流的选择是OpenCASCADE(OCCT),它支持精确 B-rep 表示,能直接导出 STEP,布尔运算和倒角能力在开源里算最成熟的。另一个是CGAL,它在计算几何、网格处理上很强,但做实体建模和 STEP 导出不如 OCCT 顺手。还有一类是纯网格路线,用trimesh或Open3D直接操作三角网格,优点是轻量、快,缺点是精度和 STEP 导出能力弱。
3.2 三种路线的实测对比
我把三条路线都跑过一遍,结论如下表:
| 路线 | 代表库 | STEP 导出 | 布尔稳定性 | 上手难度 | 适用场景 |
|---|---|---|---|---|---|
| 精确 B-rep | OpenCASCADE | 原生支持 | 高 | 中高 | 工程件、需后续编辑 |
| 计算几何 | CGAL | 需转换 | 中 | 高 | 复杂曲面、网格优化 |
| 纯网格 | trimesh/Open3D | 不支持 | 低 | 低 | 3D 打印、可视化预览 |
如果你的目标是"生成能进专业 CAD 继续改的模型",OCCT 基本是唯一解。如果只是"生成一个能打印的网格",trimesh 足够,而且开发速度快得多。我自己的项目是双轨:简单件走 trimesh 快速出 STL,复杂件走 OCCT 出 STEP。
3.3 OCCT 的 Python 绑定:pythonocc 的坑与技巧
OCCT 原生是 C++,Python 侧一般用pythonocc-core。装它是个坎,conda 装最省事:
conda install -c conda-forge pythonocc-corepip 装经常因为编译依赖失败,我踩过好几次,最后老老实实回到 conda。装好之后,构建一个圆柱壳的代码大概长这样:
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 # 外圆柱 outer = BRepPrimAPI_MakeCylinder(40, 100).Shape() # 半径40,高100 # 内圆柱,用于挖空 inner = BRepPrimAPI_MakeCylinder(35, 100).Shape() # 布尔差 shell = BRepAlgoAPI_Cut(outer, inner).Shape()这段代码看着简单,但有几个坑:布尔运算前两个实体必须有明确的位置关系,否则可能得到空结果;半径参数是半径不是直径,从文本解析来的"外径80"要除以2;布尔运算后要检查IsDone(),失败时得回退或调整容差。
3.4 容差设置:布尔运算失败的元凶
OCCT 的布尔运算偶尔会失败,尤其是两个面几乎相切或者间隙极小时。默认容差是 1e-7 米级别,对于毫米单位的模型,这个容差有时候太严。我的做法是在布尔运算前统一把模型缩放到米制(OCCT 内部习惯用米),运算完再缩回来,这样容差和几何尺度匹配,成功率明显提升。这个技巧在官方文档里不显眼,但实战里非常管用。
4. 三种导出格式的取舍:STEP、STL、GLB 各管什么
4.1 STEP:要精度、要可编辑就选它
STEP(.step/.stp)是工程界的通用交换格式,它保存的是精确的边界表示,圆弧就是圆弧,平面就是平面,不是用三角面片逼近的。这意味着导入 SolidWorks、中望 CAD、Fusion 360 之后,你还能继续拉伸、打孔、改尺寸。如果你的 text-to-cad 产物要进正规设计流程,STEP 是首选。
导出 STEP 用 OCCT 的STEPControl_Writer:
from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs writer = STEPControl_Writer() writer.Transfer(shell, STEPControl_AsIs) writer.Write("output.step")注意STEPControl_AsIs这个模式,它保留原始几何不做额外处理,适合精确件。如果模型有大量小面片,导出文件会很大,这时候可以考虑先做形状简化再导出。
4.2 STL:3D 打印和快速预览的通用语
STL 只存三角网格,没有单位、没有材质、没有层级,就是一堆三角形。它的好处是几乎所有切片软件、3D 打印工具、网格处理软件都认。缺点是精度靠网格密度控制,密度低了圆变多边形,密度高了文件巨大。
导出 STL 的关键参数是线性偏差(linear deflection)和角度偏差(angular deflection)。线性偏差控制弦高误差,角度偏差控制相邻面片夹角。我的经验值:一般件线性偏差 0.1mm、角度偏差 0.5 弧度,出来的网格既光滑又不至于太大。精密件可以调到 0.01mm,但文件可能翻好几倍。
from OCC.Core.StlAPI import StlAPI_Writer from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh # 先网格化 BRepMesh_IncrementalMesh(shell, 0.1, False, 0.5, True) writer = StlAPI_Writer() writer.Write(shell, "output.stl")4.3 GLB:网页展示和带材质场景的答案
GLB 是 glTF 的二进制版,天生为实时渲染设计。它支持材质、颜色、层级、动画,文件小、加载快,网页端 three.js、Babylon.js 直接能读。如果你做的是在线定制、产品预览、教育演示,GLB 是最合适的输出。
从 OCCT 的 B-rep 转 GLB 一般要经过网格化,再写 glTF。Python 侧可以用trimesh中转:
import trimesh mesh = trimesh.load("output.stl") mesh.export("output.glb")这条路简单,但会丢掉精确几何,只剩网格。如果对材质有要求,可以在 trimesh 里给 mesh 赋颜色和材质再导出。
4.4 格式选择的决策表
| 需求 | 首选格式 | 理由 |
|---|---|---|
| 后续在 CAD 里继续编辑 | STEP | 保留精确 B-rep |
| 3D 打印 | STL | 切片软件通用 |
| 网页/APP 实时预览 | GLB | 轻量、带材质 |
| 跨软件交换且要精度 | STEP | 工程标准 |
| 快速看形状 | STL/GLB | 生成快 |
我一般让系统同时导出三种格式,用户按需下载。存储成本不高,但省去了"我要的格式没有"的尴尬。
5. 从零跑通一个最小可用版本:完整实操链路
5.1 环境准备与依赖清单
先把环境搭起来。我推荐 Python 3.10 加 conda,依赖如下:
conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge pythonocc-core trimesh numpy pip install openai # 如果要用语言模型做解析pythonocc-core负责几何,trimesh负责网格和 GLB 中转,numpy做数值处理。语言模型部分按你的方案选,本地模型或 API 都行。
5.2 解析层:把一句话拆成参数字典
假设输入是"外径80毫米、内径70毫米、高100毫米的圆柱壳",解析层要输出:
{ "type": "cylinder_shell", "outer_diameter": 80.0, "inner_diameter": 70.0, "height": 100.0, "unit": "mm" }如果用规则解析,正则匹配"外径(\d+)"这类模式就够。如果用语言模型,给它一个明确的 JSON schema,让它按格式输出,再校验字段完整性。校验这一步不能省,模型偶尔会漏字段或给字符串,直接进几何层就崩。
5.3 几何层:参数到实体的构建函数
def build_cylinder_shell(od, id_, h): outer_r = od / 2 / 1000.0 # 转米 inner_r = id_ / 2 / 1000.0 h_m = h / 1000.0 outer = BRepPrimAPI_MakeCylinder(outer_r, h_m).Shape() inner = BRepPrimAPI_MakeCylinder(inner_r, h_m).Shape() result = BRepAlgoAPI_Cut(outer, inner).Shape() if result.IsNull(): raise ValueError("布尔运算失败,请检查参数") return result注意这里先转米再建模,就是为了配合 OCCT 的容差习惯。导出前再转回毫米或按格式要求处理。
5.4 导出层:一次生成三种格式
def export_all(shape, basename): # STEP from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs w = STEPControl_Writer() w.Transfer(shape, STEPControl_AsIs) w.Write(f"{basename}.step") # STL from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh from OCC.Core.StlAPI import StlAPI_Writer BRepMesh_IncrementalMesh(shape, 0.1, False, 0.5, True) StlAPI_Writer().Write(shape, f"{basename}.stl") # GLB import trimesh mesh = trimesh.load(f"{basename}.stl") mesh.export(f"{basename}.glb")跑完这段,你会得到三个文件。STEP 拿去 CAD 改,STL 拿去打印,GLB 拿去网页展示。
5.5 实测中的意外情况
第一次跑通不代表稳定。我实测遇到过的意外包括:布尔运算偶发失败(缩放法解决)、STL 网格出现破面(调小线性偏差)、GLB 材质丢失(trimesh 导出时手动赋材质)、STEP 导入某些软件后单位错乱(导出时显式写单位)。这些问题在 demo 里不出现,一上量就冒出来,所以批量生成前一定要做小样本验证。
6. 踩坑实录:那些让模型"看起来对但用不了"的细节
6.1 壁厚为零的"纸片实体"
有次用户输入"外径80、内径80",解析层没拦住,几何层布尔差出来一个空壳,导出的 STL 是一层零厚度的面片,切片软件直接报错。根因是约束校验缺失。修复方案是在参数层加"内径必须小于外径至少 0.5mm"的硬规则,不满足就拒绝生成并提示。
6.2 STL 在切片软件里"飘"在平台下方
前面提过坐标系问题。OCCT 默认以原点为中心建圆柱,底面在 Z=-h/2。切片软件期望底面在 Z=0,结果模型一半在平台下。修复是建模后整体平移,把最小 Z 归零:
from OCC.Core.Bnd import Bnd_Box from OCC.Core.BRepBndLib import brepbndlib box = Bnd_Box() brepbndlib.Add(shape, box) xmin, ymin, zmin, xmax, ymax, zmax = box.Get() # 平移使 zmin=0,xy 居中这个平移步骤我建议做成导出前的标准动作,别指望用户手动挪。
6.3 圆角半径大于壁厚导致的失败
用户说"边缘倒个 R5 的圆角",但壁厚只有 3mm。OCCT 倒角会失败或产生自交。修复是在参数层做圆角半径与相邻壁厚的比较,超过就自动裁剪到壁厚的 80% 并提示用户。这个"自动裁剪加提示"的策略比直接报错体验好得多。
6.4 批量生成时的内存泄漏
用 OCCT 批量生成几百个模型时,内存会持续上涨。原因是 OCCT 的 C++ 对象需要显式释放,Python 的 GC 管不到。解决是每生成一批后手动清理,或者用子进程隔离,跑完就退出。这个坑很隐蔽,单次生成看不出来,一上量就 OOM。
6.5 中文参数描述的解析歧义
"直径80"和"半径80"差一倍,"高100"和"厚度100"在不同语境下含义不同。我的处理是在解析层维护一个同义词表,把"直径/外径/OD"归一到 outer_diameter,把"半径/R"归一到 radius,并且优先要求用户用明确词汇。自由文本解析的准确率永远做不到 100%,所以回显确认这一步是刚需。
7. 把 text-to-cad 接进真实工作流的几种姿势
7.1 批量标准件生成
这是最直接的落地场景。把 Excel 里的规格表读进来,每行生成一个模型,批量导出 STEP 和 STL。我做过一个项目,300 多个法兰、垫片、套筒,原来手动建模要两天,脚本跑完不到十分钟。关键是参数表要规范,列名和解析层的字段对齐,否则还得写映射。
7.2 在线定制与即时预览
电商或定制类产品,用户填几个尺寸,后台生成 GLB 推到前端 three.js 预览,确认后生成 STEP 下单生产。这条链路里GLB 的生成速度是关键,因为用户等不了太久。我的优化是 GLB 走轻量网格路线,STEP 异步生成,用户确认后再出。
7.3 教育演示与参数化教学
教 CAD 的时候,让学生用一句话描述模型,系统实时生成,直观展示"参数怎么影响形状"。这种场景对精度要求不高,但对响应速度和可视化要求高,GLB 加网页渲染是最佳组合。
7.4 与现有 CAD 工具的衔接
生成的 STEP 可以直接拖进 SolidWorks、中望 CAD、Fusion 360。如果要做二次开发,很多 CAD 提供 API,可以把 text-to-cad 的解析层嵌进去,做成插件。这块要注意不同 CAD 对 STEP 版本的支持差异,导出时选兼容性好的版本(如 AP214)。
8. 关于精度、性能与扩展性的一些个人经验
8.1 精度不是越高越好
新手容易陷入"精度越高越好"的误区,把 STL 的线性偏差调到 0.001mm,结果文件几百 MB,切片软件打开就卡死。精度要匹配用途:3D 打印 FDM 层高 0.2mm,网格精度 0.1mm 足够;光固化可以到 0.05mm;网页预览 0.5mm 都行。按需设置,别一刀切。
8.2 生成速度的瓶颈在哪
实测下来,瓶颈通常在布尔运算和网格化,不在解析。一个中等复杂度的件,解析几十毫秒,布尔运算可能几百毫秒,网格化又是几百毫秒。优化方向是缓存常用几何、并行生成、按需网格化(预览用粗网格,导出用细网格)。
8.3 扩展性:从圆柱壳到任意形状
最小版本只支持圆柱壳,但架构要留扩展口。我的做法是把几何构建做成注册表,每种 type 对应一个构建函数,新增形状只需注册新函数,解析层和导出层不用动。这样从圆柱壳扩展到法兰、齿轮、支架,改动量很小。
8.4 一个容易被忽视的点:错误信息的可读性
用户输入错了,系统报"BRepAlgoAPI_Cut failed"没人看得懂。要把错误翻译成人话,比如"内径不能大于外径,请检查尺寸"。错误信息的质量直接决定用户能不能自己修好问题,这块投入产出比很高。
8.5 版本与依赖管理
OCCT、pythonocc、trimesh 的版本兼容性偶尔会出问题,尤其是跨大版本升级时 API 会变。我的经验是锁定版本,用 conda 环境文件固定,别用 latest。生产环境里,一次意外的依赖升级可能让整套流程崩掉。
最后分享一个我常用的调试技巧:把中间产物都落盘。解析出的参数字典存 JSON,布尔运算后的实体存 BREP,网格化后的存 STL,每一步都能单独检查。出问题时能快速定位是哪一层的事,比在黑盒里猜高效得多。这套链路我从最初的手动建模,到脚本化,再到文本驱动,前后迭代了挺多版,最大的体会是:text-to-cad 的价值不在于"AI 帮你画图"这个噱头,而在于把重复的、参数化的建模工作变成可批量、可复现、可集成的流程。把解析、几何、导出这三层解耦好,剩下的就是按需扩展形状库和优化性能了。