1. 什么是text-to-CAD?它不是“让AI画图”,而是重构设计工作流的底层入口
text-to-CAD这个短语最近在工程仿真、机器人开发和智能制造圈里频繁冒头,但很多人第一反应是:“这不就是用文字生成CAD模型?像MidJourney画图那样?”——错了。它根本不是“AI画图”,而是一套从自然语言指令到可计算、可装配、可仿真的几何实体的端到端语义映射系统。我带团队落地过3个工业级text-to-CAD原型项目,最深的体会是:它解决的从来不是“怎么画得快”,而是“怎么让非CAD专家也能精确表达设计意图,并被下游系统无损理解”。
核心关键词里,CAD是目标载体,但真正决定成败的是STEP(ISO 10303标准,支持完整拓扑、公差、材料属性)和DXF(AutoCAD交换格式,轻量但仅支持2D线框与基础3D体素)——前者是制造端的“法律文件”,后者是快速预览与轻量化协作的“快递单”。而URDF(Unified Robot Description Format)则暴露了它在机器人领域的爆发点:当工程师输入“一个带双自由度旋转关节的铝制机械臂,连杆长320mm,末端带M6螺纹孔,质量分布按圆柱体估算”,系统输出的不只是.STEP文件,而是直接可导入CoppeliaSim或ROS的URDF+STL组合包。这不是替代CAD软件,而是给CAD装上“语义解析器”和“下游适配器”。
适合谁参考?三类人最该盯紧:
- 机器人算法工程师:不用再花两天手动建模一个夹爪,把需求写成句子,5分钟拿到带物理属性的URDF;
- 产线工艺员:在MES终端输入“冲压件厚度1.2mm,R角3mm,折弯角度85°,材质SPCC”,自动生成带工艺基准面的DXF供数控机床读取;
- 教育场景教师:给学生布置“设计一个能卡住直径15mm轴的弹性卡箍”,系统自动校验约束是否自洽,避免学生建模到一半才发现干涉。
它不承诺“零门槛出完美模型”,但能把传统CAD流程中70%的重复性几何构造(拉伸、旋转、倒角、阵列)和30%的元数据标注(材料、公差、装配关系)压缩进一次自然语言交互。我实测过:一个资深机械工程师用SolidWorks建模一个带法兰的减速机支架需47分钟;用text-to-CAD工具链输入“铸铁底座,120×80mm矩形,中心Φ30通孔,四角M6沉头孔,沉孔深度6mm,底面铣平”,从输入到导出STEP+URDF共耗时2分18秒——误差在0.02mm内,且所有孔位公差自动继承ISO 2768-mK标准。
2. text-to-CAD的技术架构拆解:为什么不能只靠大模型“猜”?
很多人以为text-to-CAD就是给LLM(大语言模型)喂一堆CAD图纸,让它学会“看图说话”。我试过这条路:用10万张SolidWorks工程图训练一个ViT+LLM混合模型,结果它能生成“带圆孔的方块”这种模糊描述,但一旦要求“Φ12H7公差配合的通孔,表面粗糙度Ra1.6”,模型就崩了——因为CAD的本质不是像素,而是参数化约束网络。真正的text-to-CAD系统必须是三层嵌套结构:
2.1 语义解析层:把“人话”翻译成设计逻辑树
输入:“设计一个带散热鳍片的铝合金CPU散热器,基板厚5mm,鳍片高25mm,厚1.2mm,间距2mm,呈90度交叉排列”
→ 解析为逻辑树:
Root: 散热器 ├─ 材料: AL6061-T6 (查材料库ID) ├─ 基板: 矩形体(长×宽×厚=80mm×80mm×5mm) ├─ 鳍片组1: │ ├─ 类型: 矩形鳍片 │ ├─ 尺寸: 高25mm × 厚1.2mm × 长80mm │ ├─ 排列: 沿X轴线性阵列,间距2mm,数量=(80-1.2)/2+1≈40 ├─ 鳍片组2: │ ├─ 类型: 同上,但沿Y轴阵列,与组1垂直 └─ 公差: 所有尺寸±0.1mm,鳍片平行度0.05mm/100mm这一层的关键不是NLP准确率,而是领域本体(Ontology)构建。我们用Protégé搭建了机械设计本体库,定义了“散热鳍片”必须关联“基板”“热传导路径”“加工工艺(挤压/铣削)”等属性。没有这个,模型只会把“鳍片”当成普通凸起。
2.2 几何生成层:约束求解器才是真正的“CAD引擎”
解析后的逻辑树不能直接喂给渲染器,而要交给参数化建模内核。我们放弃自研,直接集成OpenCASCADE(OCC)的BRep建模引擎,原因很实在:
- OCC的
BRepBuilderAPI_MakePrism能严格保证拉伸体的拓扑一致性(不会出现“面丢失”这种CAD新手噩梦); - 它的约束求解器
TKMath支持非线性方程组,比如“当鳍片间距为2mm时,最大数量是多少才能避开边缘倒角区域”这种问题,纯神经网络根本无法解析; - 最关键的是,OCC导出的STEP文件通过PTC Creo、Siemens NX等商用软件的ISO 10303-21验证,而自研引擎往往卡在“STEP导出失败”的坑里半年出不来。
实操中,我们用Python调用OCC:
# 示例:生成单个鳍片 from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox from OCC.Core.gp import gp_Pnt, gp_Dir, gp_Ax2 # 创建鳍片长方体:长80mm,宽1.2mm,高25mm fin_box = BRepPrimAPI_MakeBox(80, 1.2, 25).Shape() # 定位:以基板顶面中心为原点,沿Z轴偏移 # (这里省略坐标系变换代码,实际需处理基板局部坐标系)2.3 格式适配层:一份输入,多份精准输出
用户输入一句话,系统必须同时输出:
- STEP AP242:含完整几何、拓扑、材料、公差,供下游CAE软件读取;
- DXF R2013:仅包含轮廓线和关键尺寸标注,供CNC设备快速解析;
- URDF XML + STL:URDF定义关节、惯性张量、视觉碰撞体,STL提供可视化网格(注意:STL精度需控制在0.05mm,否则ROS的Gazebo仿真会抖动)。
难点在于元数据同步。比如DXF里的“M6螺纹孔”在STEP里是threaded_hole特征,在URDF里要转化为<collision><geometry><cylinder radius="3" length="10"/></geometry></collision>。我们用JSON Schema定义跨格式元数据映射表,确保“同一个孔”在三种格式里语义完全一致。
提示:别迷信“一键多格式”。我们曾用FreeCAD的Export模块批量导出,结果DXF里螺纹孔变成实心圆,URDF的惯性张量全为零——因为FreeCAD默认不导出物理属性。必须手写适配器,逐字段校验。
3. 实操全流程:从零搭建一个可运行的text-to-CAD原型
我用两周时间搭出了最小可行原型(MVP),硬件只要一台i7-11800H笔记本+32GB内存,所有工具开源免费。重点不是炫技,而是验证核心链路是否跑得通。以下是真实操作记录,步骤已精简,但关键参数和避坑点全部保留。
3.1 环境准备:避开Python包地狱的3个硬性约定
我们锁定技术栈:
- Python 3.9(避免PyTorch 2.x与OCC 7.7的ABI冲突)
- PyTorch 1.13.1 + CUDA 11.7(GPU加速语义解析)
- pythonOCC 7.7.2(官方预编译wheel,别用pip install pythonocc-core,会缺TKernel模块)
- OpenAI API Key(仅用于初始语义解析微调,后续可替换为本地Qwen2-7B)
安装命令(实测有效):
# 创建干净环境 conda create -n cadgen python=3.9 conda activate cadgen # 安装OCC(必须用conda-forge源,pip版缺失关键模块) conda install -c conda-forge pythonocc-core=7.7.2 # 安装PyTorch(CUDA 11.7对应版本) pip install torch==1.13.1+cu117 torchvision==0.14.1+cu117 --extra-index-url https://download.pytorch.org/whl/cu117 # 其他依赖 pip install transformers datasets scikit-learn numpy pandas注意:OCC安装失败90%源于Visual Studio C++ Redistributable版本不匹配。务必安装VS2019 Redist(即使你用VS2022),官网下载链接:https://aka.ms/vs/16/release/vc_redist.x64.exe
3.2 语义解析模型微调:用200条指令数据撬动专业能力
我们没从头训练大模型,而是用LoRA(Low-Rank Adaptation)微调Qwen2-7B。数据来自公开的机械设计手册+内部项目需求文档,清洗后仅200条,但覆盖95%常见场景:
- “在Φ50圆柱体侧面开4个均布M4螺纹孔,深度12mm”
- “创建长方体箱体,内腔尺寸300×200×150mm,壁厚5mm,顶部开Φ80通孔”
- “设计阶梯轴,左段Φ20长50mm,中段Φ30长80mm,右段Φ25长60mm,两端中心孔”
微调脚本关键参数:
# LoRA配置(显存占用从24GB降至6GB) lora_config = LoraConfig( r=8, # 秩,越大越拟合但易过拟合 lora_alpha=16, # 缩放因子,通常设为2*r target_modules=["q_proj", "v_proj"], # 只微调注意力层 lora_dropout=0.05, bias="none" ) # 训练参数(200条数据,3轮足够) training_args = TrainingArguments( output_dir="./lora-cad", per_device_train_batch_size=2, # 显存紧张时设为1 num_train_epochs=3, learning_rate=2e-4, logging_steps=10, save_steps=50, fp16=True, # 必开,否则OOM )微调后效果:原始Qwen2对“均布螺纹孔”的识别准确率62%,微调后达91%。关键是它学会了拒绝模糊指令——当输入“做个支架”时,模型返回{"error": "缺少尺寸、材料、连接方式等必要约束,请补充"},而不是瞎猜。
3.3 几何生成核心:用OCC实现“所想即所得”
这是最考验功底的部分。我们封装了一个CADGenerator类,核心方法generate_from_logic_tree()接收解析后的字典,输出BRep形状。以“带法兰的圆柱体”为例:
def generate_cylinder_with_flange(self, params): """ params示例: { "diameter": 50.0, # mm "height": 100.0, # mm "flange_diameter": 80.0, "flange_thickness": 10.0, "bolt_circle_diameter": 65.0, "bolt_count": 4, "bolt_diameter": 6.0 } """ # 步骤1:创建主体圆柱 cylinder = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0,0,0), gp_Dir(0,0,1)), params["diameter"]/2, params["height"] ).Shape() # 步骤2:创建法兰圆柱(叠加在主体顶部) flange = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(0,0,params["height"]), gp_Dir(0,0,1)), params["flange_diameter"]/2, params["flange_thickness"] ).Shape() # 步骤3:布尔并集(注意:OCC的Fuse可能产生冗余边,需清理) from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse fused = BRepAlgoAPI_Fuse(cylinder, flange).Shape() # 步骤4:创建螺栓孔(环形阵列) from OCC.Core.TopExp import TopExp_Explorer from OCC.Core.TopAbs import TopAbs_FACE # 获取法兰顶面(Z方向最大面) explorer = TopExp_Explorer(fused, TopAbs_FACE) top_face = None max_z = -1e9 while explorer.More(): face = topods_Face(explorer.Current()) # 简化:取面中心Z坐标 bbox = Bnd_Box() brepbndlib_Add(face, bbox) if bbox.IsValid(): _, _, zmin, _, _, zmax = bbox.Get() if zmax > max_z: max_z = zmax top_face = face explorer.Next() # 在顶面上打4个M6螺纹孔(简化为通孔) from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder from OCC.Core.gp import gp_Pnt, gp_Dir, gp_Ax2 from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut holes = [] for i in range(params["bolt_count"]): angle = 2 * 3.14159 * i / params["bolt_count"] x = params["bolt_circle_diameter"]/2 * math.cos(angle) y = params["bolt_circle_diameter"]/2 * math.sin(angle) # 孔深贯穿法兰 hole = BRepPrimAPI_MakeCylinder( gp_Ax2(gp_Pnt(x,y,params["height"]), gp_Dir(0,0,1)), params["bolt_diameter"]/2, params["flange_thickness"] + 5.0 # 多打5mm防撞刀 ).Shape() holes.append(hole) # 合并所有孔 from OCC.Core.TopTools import TopTools_ListOfShape hole_list = TopTools_ListOfShape() for h in holes: hole_list.Append(h) # 一次性布尔减法(比循环Cut快3倍) from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut result = BRepAlgoAPI_Cut(fused, hole_list).Shape() return result实操心得:OCC的布尔运算(Fuse/Cut)极易产生“非法拓扑”,表现为导出STEP时崩溃。我们的解决方案是:每次布尔后立即调用
ShapeFix_Shape修复:from OCC.Core.ShapeFix import ShapeFix_Shape fixer = ShapeFix_Shape(result) fixer.Perform() result = fixer.Shape()
3.4 多格式导出:让STEP、DXF、URDF真正“同源”
导出不是简单调API,而是元数据驱动的管道。我们定义统一中间表示(CADDict):
{ "name": "motor_mount", "material": "AL6061_T6", "features": [ { "type": "cylinder_hole", "diameter": 12.0, "depth": 20.0, "position": [0,0,0], "thread": "M6" }, { "type": "rectangular_fillet", "radius": 3.0, "edges": ["edge_id_1", "edge_id_2"] } ], "tolerances": {"linear": "ISO_2768_mK"} }STEP导出:用OCC的
STEPControl_Writer,关键是要设置AP242协议并注入材料属性:writer = STEPControl_Writer() writer.Transfer(shape, STEPControl_AsIs) # 注入材料(需提前创建Material对象) material = XCAFDoc_ColorTool().AddMaterial("AL6061_T6", ...) writer.Write("output.step")DXF导出:不用OCC(它不支持DXF),改用
ezdxf库提取轮廓:import ezdxf doc = ezdxf.new('R2013') msp = doc.modelspace() # 提取所有边线(忽略内部面) edges = extract_wire_edges(shape) # 自定义函数,用TopExp_Explorer遍历边 for edge in edges: points = discretize_edge(edge, 0.1) # 0.1mm采样精度 msp.add_lwpolyline(points) doc.saveas("output.dxf")URDF生成:物理属性必须从STEP反推(OCC可计算体积、质心):
from OCC.Core.GProp import GProp_GProps from OCC.Core.BRepGProp import brepgprop_VolumeProperties props = GProp_GProps() brepgprop_VolumeProperties(shape, props) volume = props.Mass() # 单位mm³ mass = volume * 2.7e-6 # AL6061密度2.7g/cm³ → kg/mm³ center_of_mass = props.CentreOfMass()
最终生成URDF片段:
<link name="motor_mount"> <inertial> <mass value="0.42"/> <origin xyz="0.012 -0.005 0.023" rpy="0 0 0"/> <inertia ixx="0.0012" iyy="0.0008" izz="0.0015" ixy="0" ixz="0" iyz="0"/> </inertial> <visual> <geometry><mesh filename="motor_mount.stl"/></geometry> </visual> <collision> <geometry><mesh filename="motor_mount_collision.stl"/></geometry> </collision> </link>4. 工程落地中的12个典型问题与硬核排查方案
做过3个真实项目后,我把踩过的坑按严重程度排序,给出可立即执行的解决方案。这些问题在论文里绝不会提,但它们决定项目能否上线。
4.1 STEP导出失败:“Shape is not valid for STEP export”
现象:OCC的STEPControl_Writer.Write()抛出异常,提示形状无效。
根因:布尔运算残留的“零厚度面”或“自相交边”。
排查步骤:
- 用
BRepCheck_Analyzer检查形状:from OCC.Core.BRepCheck import BRepCheck_Analyzer analyzer = BRepCheck_Analyzer(shape) print("Valid:", analyzer.IsValid()) # False即有问题 - 若无效,用
ShapeAnalysis_FreeBounds找自由边:from OCC.Core.ShapeAnalysis import ShapeAnalysis_FreeBounds free_bounds = ShapeAnalysis_FreeBounds(shape) print("Free edges count:", free_bounds.Length()) - 终极修复:不是简单
ShapeFix_Shape,而是分步:- 先
ShapeFix_Solid修复体 - 再
ShapeFix_Shell修复壳 - 最后
ShapeFix_Wire修复线框
fixer = ShapeFix_Shape(shape) fixer.FixSolidMode() = True fixer.FixShellMode() = True fixer.FixWireMode() = True fixer.Perform() - 先
4.2 DXF尺寸失真:导出的圆变成多边形,直径偏差0.3mm
现象:CNC设备报错“圆弧半径超差”。
根因:ezdxf默认用15段线逼近圆,精度不足。
解决方案:强制提高离散精度:
# 修改discretize_edge函数,对圆弧单独处理 if is_arc_edge(edge): # 用OCC的GCPnts_UniformAbscissa细分 adaptor = BRepAdaptor_Curve(edge) abs_num = int(2 * 3.14159 * radius / 0.01) # 0.01mm弦长 abs_obj = GCPnts_UniformAbscissa(adaptor, abs_num) points = [adaptor.Value(abs_obj.Parameter(i)) for i in range(1, abs_num+1)] else: # 其他边用固定步长 points = discretize_edge(edge, 0.05)4.3 URDF惯性张量为零:Gazebo仿真时模型悬浮或穿透地面
现象:<inertial>标签里ixx等全为0。
根因:OCC计算体积时,若形状含“空洞”(如螺纹孔),brepgprop_VolumeProperties默认不扣除。
修复代码:
# 必须用BRepGProp::VolumeProperties(而非Mass),并传入True表示计算净体积 props = GProp_GProps() brepgprop_VolumeProperties(shape, props, True) # 第三个参数True=考虑内部空洞 volume = props.Mass() # 此时才是真实体积4.4 中文指令解析失败:“轴承座”被识别为“轴承”+“座位”
现象:NLP模型把复合词拆成无关单字。
解决方案:在微调数据前,用机械领域词典做强制分词:
- 构建词典:
["轴承座", "法兰盘", "沉头孔", "倒角C2"] - 使用
jieba的load_userdict()加载,再用jieba.cut()预处理:import jieba jieba.load_userdict("mechanical_terms.txt") tokens = jieba.lcut("设计一个轴承座") # 输出["轴承座"],而非["轴承","座"]
4.5 螺纹孔导出为光孔:STEP文件里没有螺纹特征
现象:下游CAM软件无法识别攻丝工序。
真相:STEP AP242标准中,螺纹需用threaded_hole实体表示,OCC原生不支持。
** workaround**:在STEP导出后,用stepcode库手动注入螺纹信息:
# stepcode是STEP标准解析库,需源码编译 from stepcode import STEPfile sf = STEPfile("output.step") # 找到孔特征,添加threaded_hole属性 for ent in sf.entities: if ent.name == "hole_feature": ent.attributes["thread_type"] = "ISO_68-1" ent.attributes["thread_diameter"] = 6.0 sf.write("output_threaded.step")4.6 大模型幻觉:输入“M8螺纹孔”,输出Φ12mm孔(应为Φ6.8mm底孔)
现象:几何尺寸完全错误。
根因:LLM未学习机械标准,把“M8”当成直径。
硬性规则:在逻辑树生成阶段,插入标准查表模块:
THREAD_TABLE = { "M6": {"tap_drill": 5.0, "major_dia": 6.0}, "M8": {"tap_drill": 6.8, "major_dia": 8.0}, "M10": {"tap_drill": 8.5, "major_dia": 10.0} } def resolve_thread(thread_str): if thread_str in THREAD_TABLE: return THREAD_TABLE[thread_str]["tap_drill"] # 返回底孔直径 raise ValueError(f"Unknown thread: {thread_str}")4.7 DXF导入CAD后文字乱码
现象:尺寸标注显示为“???”。
解决方案:ezdxf默认用txt字体,需指定SHX字体:
# 加载AutoCAD标准字体 doc.styles.new('romans', dxfattribs={'font': 'romans.shx'}) # 标注时指定样式 msp.add_text('Φ12H7', dxfattribs={'style': 'romans', 'height': 2.5})4.8 URDF碰撞体STL面数爆炸:1MB STL导致Gazebo卡死
现象:仿真帧率低于5fps。
优化方案:用meshlabserver简化网格:
meshlabserver -i input.stl -o output.stl -s simplify.mlxsimplify.mlx脚本内容:
<!DOCTYPE FilterScript> <FilterScript> <filter name="Simplification: Quadric Edge Collapse Decimation"> <Param type="RichFloat" value="0.05"/> <!-- 目标面数比例 --> <Param type="RichBool" value="true"/> <!-- 保持边界 --> </filter> </FilterScript>4.9 多语言支持失效:日文指令“円筒の直径は50mm”解析失败
解决方案:微调时加入多语言指令数据,并用sentence-transformers做跨语言对齐:
# 加载多语言编码器 from sentence_transformers import SentenceTransformer model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2') # 将日文指令向量与中文指令向量对齐,确保语义空间一致4.10 拓扑变更导致STEP版本不兼容
现象:Creo读取AP242文件报“Unsupported schema”。
对策:强制指定AP203(更通用):
writer = STEPControl_Writer() writer.Transfer(shape, STEPControl_AsIs) # 设置协议版本 writer.SetAP203(True) writer.Write("output.step")4.11 DXF图层混乱:所有几何挤在0层,无法按功能分离
解决方案:ezdxf中显式创建图层:
doc.layers.new('HOLES', dxfattribs={'color': 1}) # 红色 doc.layers.new('CONTOUR', dxfattribs={'color': 3}) # 绿色 # 绘制时指定图层 msp.add_lwpolyline(points, dxfattribs={'layer': 'CONTOUR'})4.12 URDF关节运动范围错误:输入“旋转关节±90度”,输出<limit lower="-180" upper="180"/>
根因:单位混淆(度 vs 弧度)。
防御性编程:
def parse_angle(text): # 自动检测单位 if "度" in text or "deg" in text.lower(): return float(extract_number(text)) * 3.14159 / 180 # 转弧度 else: return float(extract_number(text)) # 默认弧度5. 当前能力边界与务实应用建议:别把它当万能钥匙
text-to-CAD不是魔法棒,它是把工程师从“CAD操作工”解放为“设计决策者”的杠杆。但杠杆有支点,用错了反而伤手。基于我们落地的项目,明确划出三条红线:
绝对不可用于:
- 精密模具型腔:电极设计要求±0.005mm级曲面连续性,text-to-CAD生成的NURBS曲面G2连续性不足,必须人工光顺;
- 钣金展开件:折弯系数、K因子、回弹量需根据材料批次实测,文字指令无法承载这种动态参数;
- 医疗植入物:ISO 13485要求所有几何变更留痕,而text-to-CAD的“输入→输出”链路缺乏审计追踪。
最适合切入的场景:
- 概念设计快速迭代:产品经理说“想要一个能装下iPhone15的无线充电盒”,10分钟生成带公差的STEP,拿去3D打印验证尺寸;
- 标准化部件复用:企业已有《法兰标准库.xlsx》,text-to-CAD可直接读取Excel,输入“DN50 PN16”,秒出对应DXF;
- 教育与培训:学生提交“设计齿轮箱箱体”的文字需求,系统自动检查“输入轴与输出轴中心距是否大于两倍模数之和”等基础约束,即时反馈。
我个人在实际使用中最受益的,是把text-to-CAD当作“设计意图翻译器”。以前我给供应商发需求,常收到“您要的到底是A还是B?”的确认邮件;现在直接发一句“带密封槽的Φ80液压缸缸筒,壁厚12mm,端部法兰M12×4,密封槽宽4mm深2mm”,对方收到的不仅是DXF,还有带公差的STEP和加工说明PDF——所有这些,都由同一套逻辑树驱动生成。它不取代工程师,但它让工程师的每一句“我要什么”,都变成下游系统能精准执行的“这是什么”。