1. 从一段文字到三维实体:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多人会下意识觉得它是个噱头——输入一句话就能生成 CAD 模型?这听起来像是把设计师十几年的经验压缩成一次回车键。但真正在机械设计、工业建模或者 3D 打印这条线上摸爬滚打过的人会明白,这个方向要解决的根本不是"替代设计师",而是把重复性建模劳动从人手里拿走。
传统 CAD 工作流的痛点非常具体。假设你要建一个法兰盘,参数是外径 120mm、内径 60mm、厚度 15mm、均布 6 个直径 10mm 的螺栓孔。在 SolidWorks 或者中望 CAD 里,你需要:新建草图、画圆、标注尺寸、拉伸、再建草图、画孔、阵列、切除。这一套动作熟练工也要两三分钟,而且一旦参数改了,你得重新走一遍。如果一天要出几十个类似的变体件,这种重复劳动会把人逼疯。
text-to-cad 的核心思路是:用自然语言描述几何意图,由程序解析成参数化的建模指令,最终输出标准格式的三维文件。这里的"标准格式"就是关键词里反复出现的 STEP、GLB、STL。它们各自承担不同角色——STEP 是精确的 B-rep 边界表示,适合后续在 CAD 软件里继续编辑;STL 是三角网格,适合 3D 打印和切片;GLB 是带材质的轻量级场景格式,适合网页预览和可视化展示。
所以这个项目的本质,是一条从语义到几何的转换管线。它适合谁?三类人最该关注:一是需要批量生成参数化零件的工程师,二是做 3D 打印前处理、经常要临时改模型的创客,三是想把建模能力集成到自己产品里的开发者。哪怕你只是刚学 CAD 制图入门,理解这条管线的逻辑,也能帮你搞清楚"参数"和"几何"之间到底是怎么映射的。
我下面要拆的,不是某个具体开源库的 API 文档,而是这条管线在真实落地时会遇到的技术选型、几何内核、格式转换、精度控制这几道坎。这些内容在官方文档里往往一笔带过,但真正动手做的时候,每一道都能卡你半天。
2. 语义解析层:把"人话"翻译成建模指令的三种路线
2.1 规则模板匹配:最笨但最稳的第一版方案
如果你现在就要动手做一个 text-to-cad 的原型,我强烈建议第一版不要碰大模型,先用正则加模板匹配把流程跑通。原因很简单:你需要先验证"几何生成"这一段是通的,而不是一上来就被语义理解的不可控性拖死。
规则模板的思路是预先定义一批句式模式。比如用户输入"创建一个直径 80 毫米、高 50 毫米的圆柱",你用正则提取出直径=80、高=50、形状=圆柱,然后调用几何内核的make_cylinder(radius=40, height=50)。这套东西听起来原始,但它的确定性是巨大优势——同样的输入永远得到同样的输出,调试起来一目了然。
我实际做的时候会把模板设计成"槽位填充"结构。先定义形状关键词表:圆柱、立方体、球、法兰、支架、齿轮毛坯。再定义参数关键词表:直径、半径、长、宽、高、厚、孔数、孔径。然后用一个简单的状态机扫描句子,遇到形状词就锁定几何类型,遇到参数词就往后抓数字和单位。单位处理是个坑,毫米和米混用会导致模型差一千倍,所以我在解析层强制做单位归一化,默认毫米,遇到"米""cm"立刻换算。
提示:规则模板阶段一定要把"解析结果"打印出来给人看,不要直接进几何生成。我踩过的坑是解析错了但几何照样生成,结果出来一个尺寸离谱的模型,排查了半天才发现是正则贪婪匹配把两个数字吃成了一个。
这套方案的边界也很清楚:它只能处理你预定义过的句式。用户说"来个胖一点的圆柱",它就懵了。但对于内部工具、批量出图这种场景,输入往往是结构化的,规则模板的覆盖率能到 80% 以上,性价比极高。
2.2 大模型抽取结构化参数:能力上限高但需要约束
当你需要处理更自由的自然语言时,大模型就派上用场了。但注意,不是让大模型直接生成几何代码,而是让它做"信息抽取"——把一段话变成结构化的 JSON。这个定位非常关键,因为让模型直接写几何代码,出错率极高且难以验证;而让它填 JSON 字段,你可以对每个字段做类型和范围校验。
具体做法是设计一个严格的输出 schema,比如:
{ "shape": "cylinder", "params": { "radius": 40, "height": 50, "unit": "mm" }, "features": [ {"type": "hole", "diameter": 10, "count": 6, "pattern": "circular"} ] }然后通过提示词约束模型只输出这个结构。实测下来,模型对"直径""半径"这类词的区分偶尔会出错,所以我在 schema 里加了一层校验:如果shape是圆柱但只给了diameter,程序自动除以 2 转成radius。这种后处理纠错比指望模型一次做对要可靠得多。
这里有个经验:不要让模型输出数值计算的结果。比如用户说"直径是高的两倍",你别让模型算出具体数字,而是让它输出{"radius": "height * 1"}这样的表达式,由你的程序去求值。模型做算术的可靠性远低于做抽取,把计算交给确定性代码,是保证精度的关键。
2.3 混合架构:规则兜底加模型增强
真正上生产的时候,我采用的是混合架构。先用规则模板快速匹配,命中就直接走;没命中的再丢给大模型抽取。这样既保证了常见输入的稳定性和速度,又用模型覆盖了长尾表达。
这个架构还有一个隐藏好处:规则命中的样本可以反哺模型。你可以把规则解析成功的句子和对应 JSON 存下来,作为微调数据或者少样本示例,让模型在你这个垂直领域的表现越来越好。我做过统计,混合架构下,规则能覆盖约 65% 的请求,模型处理剩下的 35%,整体准确率比纯模型方案高出不少,而且响应延迟因为大部分请求走了规则而明显下降。
3. 几何内核选型:为什么 OpenCASCADE 是绕不开的那一个
3.1 B-rep 与网格表示的本质区别
在讲内核之前,必须先搞清楚一个概念:B-rep 和网格是两种完全不同的几何表示。这直接决定了你的 text-to-cad 输出 STEP 还是 STL。
B-rep(Boundary Representation)用数学曲面(平面、圆柱面、NURBS 曲面)精确描述物体的边界。一个圆柱面在 B-rep 里就是一个方程,无论你放大多少倍都是光滑的。STEP 格式就是基于 B-rep 的,所以它适合后续在 CAD 软件里继续做布尔运算、倒角、抽壳这些操作。
网格表示则用一堆三角形去逼近曲面。一个圆柱面会被切成几十上百个三角形,放大看就是多边形。STL 就是纯三角网格,它没有"圆柱"这个概念,只有一堆顶点和面片。好处是简单、通用,3D 打印切片软件只认这个;坏处是精度受三角面片数量限制,而且没法直接做精确的布尔运算。
理解这个区别后,你就明白为什么 text-to-cad 的管线通常是:语义 → B-rep 建模 → 导出 STEP(保留精度)→ 网格化 → 导出 STL(用于打印)。GLB 则是把网格加上材质和场景信息,用于可视化。
3.2 OpenCASCADE 的实际使用体验
开源世界里做 B-rep 建模,OpenCASCADE(简称 OCCT)基本是唯一成熟的选择。它提供了完整的几何建模能力:基本体素、布尔运算、倒角、抽壳、曲面构造。Python 侧可以通过pythonocc-core调用,C++ 侧直接用原生库。
我实际用下来的感受是:功能强大但学习曲线陡峭,文档质量参差不齐。它的 API 命名偏学术,比如创建一个圆柱要用BRepPrimAPI_MakeCylinder,布尔运算要用BRepAlgoAPI_Cut。第一次看这些名字会有点懵,但用熟了会发现它的抽象其实很合理。
一个典型的圆柱建模代码大概是这样:
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder from OCC.Core.gp import gp_Ax2, gp_Pnt, gp_Dir from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs # 定义圆柱的轴:位置在原点,方向沿 Z 轴 axis = gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)) # 半径 40,高 50 cylinder = BRepPrimAPI_MakeCylinder(axis, 40, 50).Shape() # 导出 STEP writer = STEPControl_Writer() writer.Transfer(cylinder, STEPControl_AsIs) writer.Write("cylinder.step")这段代码看起来简单,但有几个坑我踩过。第一,gp_Ax2的方向参数必须是单位向量,传了非单位向量不会报错但结果诡异。第二,OCCT 的坐标系默认右手系,Z 轴朝上,如果你习惯了某些软件的 Y 轴朝上,导入后模型方向会不对。第三,STEP 导出时的STEPControl_AsIs模式保留原始几何,但如果你的模型有单位设置,导出时要注意单位一致性。
3.3 网格化与 STL 导出的精度控制
从 B-rep 到 STL 需要做网格化(meshing),这一步的精度控制直接决定打印质量。OCCT 提供了BRepMesh_IncrementalMesh来做这件事,核心参数是线性偏差(linear deflection)和角度偏差(angular deflection)。
线性偏差控制三角形边离真实曲面的最大距离。设成 0.1mm,意味着网格和真实曲面的误差不超过 0.1mm。角度偏差控制相邻三角形法向的最大夹角。这两个值越小,网格越密,文件越大。
我的经验值是:普通 3D 打印件,线性偏差设 0.05~0.1mm 足够;精细件或者小尺寸零件,设 0.01~0.02mm。角度偏差一般设 0.5 弧度左右。设太小会导致三角形数量爆炸,一个简单圆柱能生成几十万个面片,切片软件直接卡死。
from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh from OCC.Core.StlAPI import StlAPI_Writer # 网格化,线性偏差 0.05,角度偏差 0.5 BRepMesh_IncrementalMesh(cylinder, 0.05, False, 0.5, True) stl_writer = StlAPI_Writer() stl_writer.SetASCIIMode(False) # 二进制模式,文件更小 stl_writer.Write(cylinder, "cylinder.stl")注意:STL 有 ASCII 和二进制两种格式。ASCII 可读但文件体积是二进制的 5 倍以上。生产环境一律用二进制,除非你需要人工检查顶点数据。
4. 格式转换链路:STEP、STL、GLB 各自的坑
4.1 STEP 转 STL 时最容易丢的东西
很多人以为 STEP 转 STL 就是"网格化一下",但实际操作中经常发现转出来的模型缺面、破面、法向翻转。这些问题的根源通常有三个。
第一是公差设置不当。STEP 里的曲面拼接处有缝合公差,如果网格化时的线性偏差比缝合公差还大,接缝处就会裂开。解决办法是网格化前先做一次ShapeFix_Shape修复,把缝合公差统一。
第二是法向不一致。STL 本身不强制法向朝外,但切片软件依赖法向判断内外。OCCT 网格化后有时会出现部分面片法向翻转,导致切片时把实体当成空腔。我一般会在导出前用ShapeAnalysis_Shell检查一下 shell 的闭合性和法向一致性。
第三是微小特征丢失。如果模型上有 0.01mm 的倒角,而你的线性偏差设了 0.1mm,这个倒角在网格化时会被直接抹平。这不是 bug,是精度不够。要么调小偏差,要么在建模阶段就把这种微小特征处理掉。
4.2 GLB 导出:可视化场景的特殊要求
GLB 是 glTF 的二进制版本,主要用于网页和移动端的 3D 预览。它和 STL 最大的区别是带材质、带层级、带场景信息。如果你只是把 STL 换个后缀名成 GLB,那是没用的,因为格式结构完全不同。
从 OCCT 的 B-rep 导出 GLB,通常要经过"网格化 → 转成三角网格数据结构 → 写入 glTF"这几步。Python 里可以用trimesh库来承接:先把 OCCT 网格导出成 STL 或 OBJ,再用 trimesh 读进来,加上材质,导出 GLB。
import trimesh mesh = trimesh.load("cylinder.stl") # 给模型一个基础材质 mesh.visual = trimesh.visual.ColorVisuals(mesh, face_colors=[180, 180, 180, 255]) mesh.export("cylinder.glb")这里有个细节:GLB 的坐标系和 CAD 不同。glTF 规范里 Y 轴朝上,而 CAD 通常 Z 轴朝上。直接导出会导致模型在网页里"躺着"。我一般会在导出前做一次旋转,把 Z-up 转成 Y-up。这个转换矩阵是绕 X 轴旋转 -90 度。
4.3 三种格式的选型对照
| 格式 | 几何表示 | 精度 | 可编辑性 | 典型用途 | 文件体积 |
|---|---|---|---|---|---|
| STEP | B-rep | 精确 | 高,可继续布尔运算 | CAD 交换、后续加工 | 中等 |
| STL | 三角网格 | 受网格密度限制 | 低,只能整体缩放 | 3D 打印、切片 | 较大 |
| GLB | 三角网格+材质 | 受网格密度限制 | 低 | 网页预览、AR/VR | 中等,可压缩 |
选型逻辑很直接:要后续编辑就出 STEP,要打印就出 STL,要展示就出 GLB。很多 text-to-cad 项目会同时输出三种,让用户按需下载。我建议默认给 STEP,因为它是信息最完整的,其他两种都可以从 STEP 再转出来,反过来则不行。
5. 参数化建模的实战细节:从描述到实体的完整链路
5.1 特征分解:把复杂零件拆成基本操作的序列
一个真实的零件描述往往不是单一形状。比如"一个 100x60x10 的底板,四角各有一个直径 8 的通孔,中心有一个直径 30、深 5 的沉孔"。这句话里包含了三个特征:长方体基体、四个角孔、一个中心沉孔。
我的处理方式是把描述解析成特征树,每个特征对应一次建模操作。基体是make_box,角孔是make_cylinder加cut布尔减,沉孔是make_cylinder加cut。特征之间有顺序依赖——必须先有基体才能切孔。
from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut # 基体 base = BRepPrimAPI_MakeBox(100, 60, 10).Shape() # 四角孔,假设孔中心距边 10mm hole_positions = [(10, 10), (90, 10), (10, 50), (90, 50)] for x, y in hole_positions: axis = gp_Ax2(gp_Pnt(x, y, -1), gp_Dir(0, 0, 1)) hole = BRepPrimAPI_MakeCylinder(axis, 4, 12).Shape() base = BRepAlgoAPI_Cut(base, hole).Shape()这段代码里有个容易忽略的点:孔的圆柱高度要比板厚大。板厚 10mm,我建了 12mm 高的孔,并且起点在 z=-1,这样保证布尔减能完全穿透,不会在底面留一层薄膜。这是布尔运算的经典技巧,新手经常因为圆柱和板面刚好齐平而切不穿。
5.2 布尔运算的稳定性问题与规避
OCCT 的布尔运算在大多数情况下是可靠的,但遇到共面、相切、微小间隙这些情况时,偶尔会失败或者产生退化面。我遇到过最典型的问题是:两个圆柱做布尔并集,如果它们的轴线刚好相交于一点,结果可能是一个空 shape。
规避方法有几个。第一,尽量避免精确相切,让相交部分有明确的重叠量。比如两个零件要贴合,让它们重叠 0.01mm,比刚好接触要稳。第二,布尔运算前对输入 shape 做ShapeFix修复。第三,如果一次布尔失败,尝试调整运算顺序,先做简单的再做复杂的。
提示:布尔运算失败时不要反复重试同样的参数,那只会浪费时间。先检查两个 shape 是否有有效体积(用
GProp_GProps算体积,接近零说明 shape 有问题),再检查是否有共面接触。
5.3 参数校验:在建模之前拦住错误输入
text-to-cad 的一个隐藏风险是用户输入了物理上不合理的参数。比如"直径 10 的圆柱上开一个直径 20 的孔",这在几何上无法实现。如果不做校验,布尔运算会失败或者产生诡异结果。
我在解析层和建模层之间加了一道参数校验。校验规则包括:孔径必须小于基体尺寸、孔深不能超过板厚(除非是通孔)、阵列孔不能超出边界、壁厚不能小于某个最小值。这些规则用简单的数值比较就能实现,但能拦掉大量无效请求。
校验失败时,返回的不是一个报错,而是带建议的提示。比如"孔径 20 超过了基体宽度 10,建议将孔径改为小于 10"。这种反馈对用户友好得多,也减少了来回试错。
6. 精度、性能与批量生成的工程化考量
6.1 浮点精度与单位一致性
CAD 建模对精度的要求比一般图形应用高得多。OCCT 内部用双精度浮点,但即便如此,单位不一致仍然是头号杀手。我见过最离谱的 bug 是:解析层默认毫米,但某个模板里写死了米,结果生成的模型小了 1000 倍,在视图里看就是一个点。
我的做法是在系统入口处强制单位归一化,所有内部计算统一用毫米,只在最终导出时按需转换。同时,在解析层对每个数值都记录单位,如果用户没写单位,默认毫米但给出提示。
另一个精度问题是数值比较。判断两个点是否重合,不能用==,要用距离小于某个容差。OCCT 默认容差是 1e-7 米,也就是 0.0001mm。在毫米单位下,这个容差要相应调整。我一般用 1e-4mm 作为几何容差,比这个小的差异视为零。
6.2 批量生成的并发与缓存策略
当 text-to-cad 用于批量出图时,性能就成了问题。OCCT 的建模是 CPU 密集型的,单个复杂零件可能要几百毫秒。如果一次要生成几百个,串行跑会让人等到怀疑人生。
我的方案是多进程并发。注意不是多线程,因为 OCCT 的很多操作不是线程安全的,多线程会出随机崩溃。用 Python 的multiprocessing起多个进程,每个进程独立处理一个建模任务,互不干扰。进程数设成 CPU 核心数就行,再多反而因为上下文切换变慢。
缓存也很重要。如果同样的参数被请求多次,没必要重复建模。我用参数的哈希值作为 key,把生成的 STEP 文件路径缓存起来。命中缓存直接返回文件,省掉整个建模流程。实测在批量场景下,缓存命中率能到 40% 以上,整体吞吐提升明显。
6.3 常见报错与排查对照
| 报错现象 | 可能原因 | 排查方向 |
|---|---|---|
| 布尔运算返回空 shape | 输入 shape 无效或共面接触 | 检查体积、增加重叠量 |
| STL 缺面 | 网格化偏差过大或曲面未缝合 | 调小线性偏差、先做 ShapeFix |
| 模型尺寸差 1000 倍 | 单位不一致 | 检查解析层单位归一化 |
| 导出 STEP 后打不开 | 写入未完成或路径含中文 | 检查文件流关闭、用英文路径 |
| 网格面片数爆炸 | 偏差设太小 | 调大线性偏差、检查是否有微小特征 |
| 模型方向不对 | 坐标系约定不同 | 检查 Z-up 与 Y-up 转换 |
这张表是我在实际项目中一点点攒出来的,每一条背后都是至少半小时的排查。尤其是"单位不一致"和"坐标系约定"这两个,新手几乎必踩。
7. 我在实际搭建这条管线时踩过的几个坑
第一个坑是过早引入大模型。我一开始就想用大模型做端到端的语义到代码生成,结果发现模型生成的几何代码经常有语法错误,而且同样的输入两次结果不一样,根本没法调试。后来退回到规则模板加结构化抽取,流程才稳定下来。这个教训是:先把确定性部分做扎实,再引入不确定性。
第二个坑是忽视 STL 的二进制模式。早期我用 ASCII 模式导出,一个中等复杂度的零件 STL 有几十兆,传输和加载都慢。改成二进制后体积降到几兆,切片软件打开速度明显变快。这个改动只需要一行代码,但收益巨大。
第三个坑是没有做参数校验。有一次用户输入了一个孔径大于基体的请求,布尔运算没报错但生成了一个破碎的 shape,导出的 STL 在切片软件里显示成一堆散片。从那以后我加了参数校验层,宁可提前拒绝,也不生成垃圾数据。
第四个坑是并发用了多线程。OCCT 在多线程下会偶发崩溃,而且崩溃位置随机,极难复现。换成多进程后彻底解决。这个坑让我明白,对于非线程安全的库,多进程是更稳妥的选择,代价只是进程间通信的开销。
第五个坑是GLB 的坐标系。第一次导出 GLB 在网页里预览,模型是躺着的。查了 glTF 规范才发现 Y-up 的约定。加了一个旋转矩阵后正常。这种规范层面的差异,不看文档根本想不到。
8. 这条管线还能往哪些方向延伸
把基础的 text-to-cad 管线跑通之后,能扩展的方向其实不少。我目前在做的一个方向是模板库加参数替换。把常见的标准件(法兰、轴承座、齿轮毛坯)做成参数化模板,用户只需要描述关键参数,程序自动套模板生成。这比从零解析要快得多,也更稳定。
另一个方向是反向能力:从已有的 STEP 文件里提取参数,生成文字描述。这在零件复用场景下很有用——你拿到一个别人的模型,想知道它的关键尺寸,程序帮你读出来。这个方向技术上就是特征识别加参数提取,OCCT 提供了BRepAdaptor系列工具可以做曲面类型识别。
还有一个方向是和切片流程打通。生成 STL 后直接调用切片引擎,输出打印路径预览。这样用户从一句话到看到打印预览,全程不用打开任何 CAD 软件。对于创客场景,这个闭环体验很有吸引力。
不过我得说,无论往哪个方向走,几何内核的稳定性始终是地基。语义解析可以换模型,格式转换可以换库,但 B-rep 建模这一层如果选错了或者用不好,上面盖什么都是空中楼阁。所以如果你打算认真做这个方向,花时间把 OCCT 的布尔运算、网格化、修复工具吃透,比追任何新模型都值。