☰
text-to-cad 实战:从自然语言到 STEP/GLB/STL 的工程化落地
2026/10/8 20:37:34 网站建设 项目流程

1. 从一段文字到三维模型:text-to-cad 到底在解决什么问题

第一次听到 text-to-cad 这个词,很多人会下意识觉得它离自己很远,像是实验室里的概念演示。但如果你真正在制造业、工业设计、3D 打印或者机械加工一线待过,就会明白它瞄准的是一个极其现实的痛点:从需求描述到可制造模型之间,横着一条又长又陡的鸿沟。

传统流程是什么样的?客户或者产品经理给你一段文字需求,比如“一个 80mm × 60mm × 40mm 的安装盒,壁厚 2mm,底部开两个 M4 沉头孔,侧面留一个 20mm 的线缆出口”。你得打开 CAD 软件,手动建草图、拉伸、打孔、倒角,一套操作下来,熟练工也得十几分钟到半小时。如果需求改了,比如壁厚从 2mm 变成 2.5mm,或者孔位挪了 5mm,很多参数化没做好的模型就得推倒重来。

text-to-cad 要做的,就是把这段自然语言直接翻译成 CAD 模型文件。输出格式通常落在STEP、GLB、STL这几个上。STEP 是工业界通用的边界表示格式,带完整的几何拓扑信息,能导入 SolidWorks、中望 CAD、Fusion 360 继续编辑;GLB 是 glTF 的二进制版本,适合在网页和渲染引擎里做可视化预览;STL 则是 3D 打印和网格处理的老朋友,虽然只有三角面片、没有特征信息,但胜在通用性极强。

这个方向适合谁来关注?我梳理了一下,大致是三类人。第一类是做参数化设计工具的产品和技术团队,想给自己的 CAD 软件或者在线建模平台加一个自然语言入口;第二类是3D 打印服务商和创客,经常需要根据客户口述快速出一个可打印的模型;第三类是自动化产线和工装夹具工程师,需要批量生成结构相似、尺寸不同的零件模型。哪怕你只是刚学 CAD 制图入门,理解这套逻辑也能帮你更清楚地知道“参数”和“特征”在建模里到底意味着什么。

我在这篇文章里会把这套东西拆开讲:整体架构怎么设计、核心的几何生成环节怎么落地、STEP/GLB/STL 三种格式各自怎么处理、实际跑起来会遇到哪些坑。内容会偏工程实践,代码和参数都会给到,能直接抄作业的部分我会标清楚。

2. 整体架构设计:为什么不能直接让大模型吐出一个 STEP 文件

2.1 核心矛盾:语言是离散的,几何是连续的

很多人第一反应是:现在大模型这么强,直接让它输出一个 STEP 文件不就行了?我一开始也这么想过,实测下来完全走不通。原因很根本——语言模型擅长的是符号序列预测,而 CAD 模型是连续空间里的精确几何。STEP 文件里一个圆柱面,背后是轴线位置、半径、方向向量、参数范围这一整套数学定义,大模型没法保证这些数值在几何上自洽。你让它生成一个“半径 5mm 的孔”,它可能给你写出半径 5.3 的坐标,或者孔的中心线跟面不垂直,模型直接就是坏的。

所以正确的思路是分层解耦:让语言模型负责它擅长的部分——理解意图、抽取参数、规划建模步骤;把真正的几何计算交给确定性的几何内核去做。这也是目前这个领域里比较主流的做法。

2.2 三层架构拆解

我把整个系统拆成三层,从下往上说。

第一层是几何内核层。这是地基,负责所有精确的几何运算:布尔运算、倒角、抽壳、求交。可选的开源方案有 OpenCASCADE(简称 OCCT),这是工业级的老牌内核,STEP 读写支持非常完整;还有 CGAL,偏计算几何,做网格和布尔也行,但 CAD 特征建模不如 OCCT 顺手。如果做网页端轻量预览,Three.js 自带的几何体加上 CSG 库也能凑合,但精度和鲁棒性差一个量级。我的建议是,只要涉及 STEP 输出,老老实实上 OCCT。

第二层是建模操作层。这一层把几何内核的能力封装成一个个“建模动作”,比如create_box(length, width, height)、create_cylinder(radius, height)、boolean_cut(target, tool)、fillet(edge, radius)。每个动作对应内核里的一次调用。这一层的关键是动作要原子化、可组合,这样上层规划出来的步骤序列才能稳定执行。

第三层是语言理解与规划层。这一层接自然语言,输出一个结构化的建模步骤列表(通常叫建模脚本或者操作序列)。比如输入“一个带盖的盒子,底部四个角各有一个安装孔”,这一层要输出类似这样的东西:

[ {"op": "create_box", "params": {"length": 80, "width": 60, "height": 40}}, {"op": "shell", "params": {"thickness": 2, "open_face": "top"}}, {"op": "create_cylinder", "params": {"radius": 2.2, "height": 10}, "position": [10, 10, 0]}, {"op": "boolean_cut", "params": {"target": "box", "tool": "cylinder_1"}}, ... ]

2.3 为什么选“脚本中间层”而不是端到端

这里有个关键的设计取舍值得说清楚。有人会想,能不能训练一个模型直接从文本映射到几何参数,跳过中间脚本?理论上可以,但工程上不划算。原因有三点。

第一,可调试性。中间脚本是人能读懂的,模型生成错了,你能一眼看出是哪一步参数不对,是孔位算错了还是壁厚理解反了。端到端的话,你面对的就是一个坏掉的模型,无从下手。

第二,可复用性。同一个建模脚本,改几个参数就能生成一系列零件。比如你有一批安装盒,只是长宽不同,脚本模板固定,参数一换就批量出图。这在实际生产里价值极大。

第三,几何正确性有保障。脚本里的每一步都经过几何内核校验,布尔运算失败会报错,你能捕获并处理。端到端生成的话,错误是隐式的,可能生成一个看起来像但实际有自交面的模型,导入下游软件才炸。

提示:如果你只是想做个 demo 快速验证,用大模型直接生成 OpenSCAD 代码也是个取巧办法,OpenSCAD 本身就是脚本化建模,语法简单,模型能导出 STL。但它的 STEP 输出能力弱,做工业级应用还是得回到 OCCT 这条路。

2.4 技术栈选型参考

我把常见的技术栈组合列一下,方便你按自己的场景选。

场景语言理解几何内核输出格式适合人群
快速原型验证大模型 APIOpenSCADSTL个人开发者、创客
网页端轻量建模大模型 APIThree.js + CSGGLB前端团队
工业级参数化本地部署模型OpenCASCADESTEP/STL制造业软件团队
批量零件生成规则 + 模板OpenCASCADESTEP产线自动化工程师

选型的时候有个经验:先确定输出格式,再倒推内核。如果下游只要 STL 做打印,OpenSCAD 甚至纯网格方案都够;如果要 STEP 进 CAD 继续编辑,那 OCCT 基本是绕不开的。

3. 核心细节解析:从一句话到建模脚本的关键环节

3.1 意图识别与参数抽取

自然语言进来,第一件事是搞清楚用户到底要什么形状,以及各个尺寸是多少。这一步的难点不在“识别”,而在“补全”。用户说“一个盒子”,他不会告诉你壁厚多少、有没有圆角、孔位在哪。这些默认值需要系统根据常识和上下文补上。

我的做法是维护一套默认参数表,按零件类型分类。比如“盒子”类默认壁厚 2mm、外圆角 R2、无盖;“法兰”类默认厚度 10mm、螺栓孔按标准分布。当用户没提某个参数时,用默认值填充,同时在返回结果里标注“以下参数为默认值,可调整”。这样既保证了模型能生成,又给了用户修改的入口。

参数抽取这块,大模型的 function calling 能力很好用。你定义好一个 schema,把每个建模操作需要的参数列清楚,让模型按 schema 输出。比让它自由发挥写 JSON 稳定得多。实测下来,带 schema 约束的抽取准确率能到 90% 以上,剩下的靠后处理规则兜底。

3.2 建模步骤的规划逻辑

参数有了,接下来要决定“先做什么、后做什么”。这个顺序不能乱,因为几何操作有依赖关系。比如你要在一个盒子上打孔,必须先有盒子实体,才能做布尔减。你要给边倒角,必须先有那条边。

规划逻辑我总结成一条原则:从整体到局部,从加法到减法。先建主体形状(拉伸、旋转),再做整体修饰(抽壳、倒角),最后做局部特征(打孔、开槽)。这样每一步的操作对象都是明确的,不会出现“要倒角的边还不存在”这种问题。

举个具体例子。输入是“一个 L 形支架,两臂各长 50mm,宽 30mm,厚 5mm,每个臂上两个 M5 孔”。规划出来的步骤应该是:

  1. 在草图平面画 L 形轮廓
  2. 拉伸 5mm 成实体
  3. 在第一个臂的上表面定位两个孔中心
  4. 打两个直径 5.5mm 的通孔
  5. 在第二个臂上重复定位和打孔

注意这里没有先打孔再拉伸,因为拉伸是基于草图的,孔是三维特征,顺序反了就没法做。

3.3 几何内核的调用封装

这一层是把规划好的步骤翻译成 OCCT 的实际调用。以 Python 为例,常用的绑定是pythonocc-core。我贴一段创建盒子并打孔的代码,你能看到实际长什么样:

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder from OCC.Core.gp import gp_Pnt, gp_Ax2, gp_Dir # 创建 80x60x40 的盒子 box = BRepPrimAPI_MakeBox(80, 60, 40).Shape() # 在 (10,10,0) 位置创建一个半径 2.2、高 10 的圆柱作为刀具 axis = gp_Ax2(gp_Pnt(10, 10, -1), gp_Dir(0, 0, 1)) cylinder = BRepPrimAPI_MakeCylinder(axis, 2.2, 12).Shape() # 布尔减,得到带孔的盒子 result = BRepAlgoAPI_Cut(box, cylinder).Shape()

这段代码里有个细节值得说:圆柱的起始位置我放在了 z = -1,高度给了 12,而不是从 z = 0 开始、高度 10。这是为了保证布尔运算的刀具完全穿透实体。如果刀具和实体表面刚好共面,OCCT 的布尔运算有时会出问题,产生退化边或者运算失败。让刀具两端都超出实体一点,是最稳妥的做法。这个坑我踩过不止一次,后来养成了习惯,所有做减法的刀具都往外延伸 1mm。

3.4 三种输出格式的处理差异

STEP、GLB、STL 的导出逻辑差别很大,不能一套代码走天下。

STEP 导出用 OCCT 的STEPControl_Writer,它写的是精确的边界表示,文件里保留完整的曲面和拓扑信息。导出时要注意单位,OCCT 默认是毫米,如果你的模型是按米建的,导出前要缩放。另外 STEP 有 AP203 和 AP214 等不同协议,AP214 对颜色和层的信息支持更好,一般选它。

STL 导出需要先做网格化,用BRepMesh_IncrementalMesh把曲面离散成三角面片。这里的关键参数是线性偏差(linear deflection)和角度偏差(angular deflection)。线性偏差控制弦高误差,值越小网格越密、文件越大。做 3D 打印一般取 0.1mm 到 0.05mm 就够,做精细外观件可以到 0.01mm。角度偏差控制曲面转折处的细分,默认 0.5 弧度通常没问题。

GLB 导出相对特殊,因为 glTF 是面向渲染的格式,它不关心精确几何,只关心三角网格和材质。所以流程是先把模型网格化,再把网格顶点和法线写进 glTF 结构。Python 里可以用trimesh库,它能把 OCCT 网格或者 STL 直接转成 GLB,还支持加材质和颜色。如果要做网页预览,GLB 是最省事的,Three.js 加载起来一行代码的事。

格式精度文件大小可编辑性典型用途
STEP精确中等高,保留特征CAD 编辑、加工
STL网格近似大低,只有面片3D 打印、仿真
GLB网格近似小低,面向渲染网页预览、展示

4. 实操过程:搭一个能跑通的 text-to-cad 最小系统

4.1 环境准备与依赖安装

先把环境搭起来。我用的组合是 Python 3.10 + pythonocc-core + trimesh + 一个大模型 API。pythonocc-core 用 conda 装最省事,pip 装经常编译报错。

conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge pythonocc-core pip install trimesh openai

装完先验证一下 OCCT 能不能正常导入:

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox box = BRepPrimAPI_MakeBox(10, 10, 10).Shape() print("OCCT 正常,盒子创建成功")

如果这一步报错,多半是 conda 环境没激活或者 OCCT 版本冲突,重装一遍通常能解决。

4.2 定义建模操作的 schema

接下来定义大模型要输出的操作 schema。我用 JSON Schema 的形式,把支持的操作和参数列清楚。这里只列几个核心操作,实际项目可以扩展。

OPERATION_SCHEMA = { "type": "object", "properties": { "operations": { "type": "array", "items": { "type": "object", "properties": { "op": {"type": "string", "enum": ["create_box", "create_cylinder", "boolean_cut", "shell", "fillet"]}, "params": {"type": "object"}, "position": {"type": "array", "items": {"type": "number"}} }, "required": ["op", "params"] } } } }

schema 定义得越细,模型输出越稳定。但也不能太细,否则模型理解成本高,容易漏参数。我的经验是每个操作最多 5 个参数,超出的拆成多个操作。

4.3 调用大模型生成建模脚本

把用户输入和 schema 一起发给模型。系统提示词里要写清楚:你是一个 CAD 建模助手,根据用户描述生成建模操作序列,所有尺寸单位是毫米,未指定的参数用合理默认值。

import openai import json def text_to_script(user_input): response = openai.ChatCompletion.create( model="gpt-4", messages=[ {"role": "system", "content": "你是 CAD 建模助手,输出 JSON 格式的建模操作序列,单位毫米。"}, {"role": "user", "content": user_input} ], functions=[{"name": "build_model", "parameters": OPERATION_SCHEMA}], function_call={"name": "build_model"} ) args = response.choices[0].message.function_call.arguments return json.loads(args)

实测下来,用 function calling 比让模型直接输出 JSON 字符串稳定很多,基本不会出现格式错误。如果用的是本地模型,可以用 grammar 约束或者 few-shot 示例来达到类似效果。

4.4 执行建模脚本并导出

拿到操作序列后,逐个执行。我写一个简单的执行器,把每个操作映射到 OCCT 调用。

from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut from OCC.Core.gp import gp_Pnt, gp_Ax2, gp_Dir def execute_operations(ops): shapes = {} for i, op in enumerate(ops): if op["op"] == "create_box": p = op["params"] shapes[f"shape_{i}"] = BRepPrimAPI_MakeBox(p["length"], p["width"], p["height"]).Shape() elif op["op"] == "create_cylinder": p = op["params"] pos = op.get("position", [0, 0, 0]) axis = gp_Ax2(gp_Pnt(pos[0], pos[1], pos[2] - 1), gp_Dir(0, 0, 1)) shapes[f"shape_{i}"] = BRepPrimAPI_MakeCylinder(axis, p["radius"], p["height"] + 2).Shape() elif op["op"] == "boolean_cut": target = shapes[op["params"]["target"]] tool = shapes[op["params"]["tool"]] shapes[f"shape_{i}"] = BRepAlgoAPI_Cut(target, tool).Shape() return shapes[f"shape_{len(ops)-1}"]

注意这里刀具圆柱我统一加了 2mm 高度、起点下移 1mm,就是前面说的穿透处理。执行完拿到最终 shape,就可以导出了。

4.5 导出 STEP 和 STL

导出 STEP:

from OCC.Core.STEPControl import STEPControl_Writer, STEPControl_AsIs from OCC.Core.Interface import Interface_Static_SetCVal def export_step(shape, filepath): writer = STEPControl_Writer() Interface_Static_SetCVal("write.step.unit", "MM") writer.Transfer(shape, STEPControl_AsIs) writer.Write(filepath)

导出 STL:

from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh from OCC.Core.StlAPI import StlAPI_Writer def export_stl(shape, filepath, deflection=0.1): BRepMesh_IncrementalMesh(shape, deflection, False, 0.5, True) writer = StlAPI_Writer() writer.Write(shape, filepath)

deflection 取 0.1 是打印场景的常用值,模型表面看起来足够光滑,文件也不会太大。如果做外观展示,可以调到 0.02。

4.6 导出 GLB 做网页预览

GLB 走 trimesh 这条路:

import trimesh def export_glb(stl_path, glb_path): mesh = trimesh.load(stl_path) mesh.export(glb_path)

trimesh 会自动处理顶点合并和法线计算。如果模型有多个部件,可以分别加载再合并成一个 scene 导出,这样 GLB 里会保留部件层级。

5. 常见问题与排查技巧实录

5.1 布尔运算失败怎么办

这是最高频的问题。表现是BRepAlgoAPI_Cut返回空 shape,或者结果 shape 的IsDone()返回 False。原因通常有三种:刀具和实体表面共面、刀具没有完全穿透、实体本身有自交或退化面。

排查顺序我一般是这样的。先检查刀具是否完全穿透,把刀具两端各延伸 1mm 再试。如果还不行,检查实体是不是有微小面或者重复顶点,可以用ShapeFix_Shape修一遍。最后考虑是不是 OCCT 版本对某些布尔场景支持不好,换个运算顺序或者拆成多次布尔。

提示:OCCT 的布尔运算有个SetFuzzyValue方法,设置一个容差值,能容忍微小的几何误差。默认是 0,遇到共面问题时设成 1e-5 有时能救回来。但这是权宜之计,根本解法还是把几何做干净。

5.2 模型尺寸不对怎么定位

有时候模型能生成,但尺寸跟预期差很多。这种情况八成是单位问题或者参数传递错了。我的排查方法是在每一步操作后打印包围盒,看尺寸是不是符合预期。

from OCC.Core.Bnd import Bnd_Box from OCC.Core.BRepBndLib import brepbndlib def get_bbox(shape): bbox = Bnd_Box() brepbndlib.Add(shape, bbox) return bbox.Get()

包围盒返回 (xmin, ymin, zmin, xmax, ymax, zmax)。如果某一步之后尺寸突然不对,问题就出在那一步。这个方法比盯着代码看快得多。

5.3 STL 文件太大或太粗糙

STL 大小和精度是一对矛盾。文件太大,打印软件加载慢;太粗糙,曲面看起来有棱角。我的经验值是:一般结构件用 0.1mm 线性偏差,外观件用 0.02mm,微型精密件用 0.005mm。角度偏差保持 0.5 弧度不动就行,它主要影响圆柱面的分段数。

如果文件还是太大,可以在导出前做一次网格简化,trimesh 有simplify_quadric_decimation方法,能减少三角面片数量同时保持形状。但注意简化会损失精度,打印前要确认关键尺寸没变。

5.4 大模型输出的参数不合理

模型有时候会给出明显不合理的参数,比如壁厚 0.1mm、孔径 0.5mm。这种情况要在执行前加一层参数校验。我维护一个合理范围表,每个参数有上下限,超出范围就报警或者用默认值替换。

参数合理下限合理上限默认值
壁厚0.5mm20mm2mm
孔径1mm50mm5mm
圆角半径0.2mm10mm2mm
零件尺寸1mm1000mm100mm

校验不通过时,不要直接报错终止,而是用默认值替换并记录警告,让流程能继续跑完。用户看到模型后再决定要不要调。

5.5 常见问题速查表

现象可能原因排查方法解决
布尔运算返回空刀具共面/未穿透检查刀具位置和高度刀具两端延伸 1mm
模型尺寸偏差大单位错误打印包围盒统一用毫米
STL 加载慢网格太密看文件大小调大 deflection
曲面有棱角网格太粗看圆柱面分段调小 deflection
参数离谱模型幻觉看脚本 JSON加参数校验
STEP 导入报错几何有退化用 ShapeFix 修复修完再导出

6. 几个我踩过的坑和实操心得

6.1 别迷信大模型的几何直觉

我一开始试过让模型直接算孔位坐标,比如“四个角各留 10mm 边距”,它有时候算对,有时候算错,尤其是零件尺寸变化的时候。后来我改成让模型只输出“边距 10mm”这个语义参数,坐标计算交给代码。代码里根据零件包围盒和边距算出实际坐标,稳定得多。这个思路可以推广到所有涉及数值计算的地方:模型负责理解意图,代码负责精确计算。

6.2 建模脚本要留“可编辑锚点”

生成的模型如果用户想改,最好能回到脚本层面改,而不是在 CAD 里手动改。所以我在脚本里给每个关键特征加了 id 和注释,比如{"op": "create_cylinder", "id": "mounting_hole_1", "params": {...}}。用户说“把安装孔改成 M5”,系统能定位到对应操作,改半径参数重新生成。这个设计在实际使用中很受欢迎,比重新描述一遍需求快得多。

6.3 批量生成时注意内存

如果你要批量生成几百个零件,每个都调 OCCT 建实体、导 STL,内存会涨得很快。OCCT 的 shape 对象不会自动释放,需要手动管理。我的做法是每生成一个零件就导出、记录结果,然后显式删除 shape 引用,必要时手动触发垃圾回收。跑批量任务时,内存能稳定在一个可控范围。

6.4 关于 STEP 和 STL 的转换

热词里有个“sw 中 stl 转 stp”,这其实是个反向需求——从网格回到精确几何。这件事本质上是有信息损失的,STL 只有三角面片,转成 STEP 只能得到一堆平面,曲面信息没了。如果非要转,可以用逆向工程软件拟合曲面,但精度和效率都不理想。所以我的建议是:能在源头生成 STEP 就别走 STL 中转。text-to-cad 系统直接输出 STEP,下游要用 STL 再转,这样信息是完整的。

6.5 网页预览的坐标系问题

GLB 导出后在网页里看,有时候模型是躺着的或者朝向不对。这是因为 CAD 的坐标系(Z 轴向上)和 glTF 的坐标系(Y 轴向上)不一致。trimesh 导出时不会自动转换,需要手动绕 X 轴旋转 -90 度。这个坑很隐蔽,模型在 CAD 里看是对的,到网页里就歪了。加一行旋转变换就能解决,但不知道的话能查半天。

import trimesh import numpy as np mesh = trimesh.load(stl_path) rotation = trimesh.transformations.rotation_matrix(-np.pi/2, [1, 0, 0]) mesh.apply_transform(rotation) mesh.export(glb_path)

这套东西我从最初的概念验证做到能稳定出图,前后调了大概两周。核心的体会就是:语言模型和几何内核各司其职,中间用结构化的建模脚本连接,这个架构一旦搭好,后面扩展新的建模操作、支持新的输出格式都是顺水推舟的事。真正花时间的不是写代码,而是处理各种几何边界情况和参数校验。如果你也在做类似的东西,建议先把布尔运算的稳定性问题解决掉,这是整个流程里最容易出岔子的环节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询