ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

text-to-cad 实战:自然语言生成 CAD 模型与 STEP/DXF/URDF 导出

text-to-cad 实战:自然语言生成 CAD 模型与 STEP/DXF/URDF 导出 1. 从一段文字到一张图纸text-to-cad 到底在解决什么问题如果你在机械设计、建筑结构或者工业制造行业待过一段时间就一定经历过这样的场景客户发来一段文字描述——“一个长宽高分别是 120mm、80mm、50mm 的铝合金外壳四角做 R5 圆角顶面开两个直径 20mm 的散热孔孔间距 60mm”——然后你需要打开 CAD 软件手动拉伸、倒角、打孔一步步把这段文字变成三维模型。整个过程熟练工也得花十几分钟复杂一点的零件半小时起步。text-to-cad 这个方向要干的事情就是把这十几分钟甚至半小时的重复劳动压缩到几秒钟。它的核心逻辑并不神秘把自然语言描述解析成结构化的几何参数再通过程序化建模接口生成 CAD 实体最后导出成 STEP、DXF、URDF 这些下游工具能直接读取的格式。关键词里出现的 STEP 是三维实体交换格式DXF 是二维图纸交换格式URDF 是机器人仿真里描述连杆和关节的 XML 格式——这三个格式基本覆盖了从设计到仿真到出图的全链路。这篇文章适合谁看如果你是会写 Python 但不太懂 CAD 二次开发的工程师或者你是懂设计但想用脚本批量处理图纸的技术人员再或者你是做机器人仿真需要批量生成 URDF 模型的研究者这篇内容都能给你一套可以直接上手跑的思路。我不会只讲概念而是把参数解析、几何生成、格式导出、踩坑经验全部拆开讲清楚。2. 自然语言到几何参数解析层怎么设计才靠谱2.1 为什么不能直接让大模型输出 CAD 命令很多人第一反应是既然有大语言模型直接让它输出 AutoCAD 的 LISP 脚本或者 FreeCAD 的 Python 命令不就行了我一开始也是这么想的实测下来问题很大。大模型输出的代码看起来像那么回事但经常出现坐标计算错误、单位混乱、实体布尔运算顺序不对等问题。更致命的是它生成的代码不可复现——同样的输入换个时间跑出来的结果可能不一样。正确的做法是把任务拆成两步第一步让模型只负责把自然语言转成结构化的 JSON 参数第二步用确定性的代码根据 JSON 参数生成几何。这样模型只做它擅长的事情——理解语义和提取参数几何计算交给精确的数学库来完成。举个例子输入“一个 120x80x50 的盒子四角 R5 圆角顶面两个直径 20mm 的孔孔间距 60mm”解析层应该输出这样的结构{ type: box, dimensions: {length: 120, width: 80, height: 50}, unit: mm, fillets: {edges: vertical, radius: 5}, features: [ { type: hole, face: top, diameter: 20, count: 2, spacing: 60, position: centered } ] }这个 JSON 就是解析层和几何层之间的契约。只要这个结构定义得足够清晰后面生成几何的代码就是纯数学问题不涉及任何模糊判断。2.2 参数提取中最容易翻车的三个地方第一个坑是单位推断。用户说“长 120”到底是毫米还是厘米还是英寸我的做法是在解析层强制要求单位如果用户没写默认按毫米处理但在返回结果里加一个unit_assumed: true的标记提醒用户确认。这个细节看起来小但实际项目中因为单位搞错导致整个零件报废的情况我见过不止一次。第二个坑是相对位置描述。“孔在顶面中间偏左 20mm”这种描述不同的人理解不一样。偏左是从哪个方向看是沿着长度方向还是宽度方向我的处理方式是建立一个标准坐标系以零件底面中心为原点长度方向为 X 轴宽度方向为 Y 轴高度方向为 Z 轴。所有位置描述都转换到这个坐标系下的绝对坐标。如果用户的描述有歧义解析层会返回一个ambiguity字段列出可能的解释让用户选择。第三个坑是特征顺序。先倒角再打孔和先打孔再倒角结果可能完全不同。比如孔的位置如果刚好在圆角边上顺序不同会导致孔被切掉一部分或者圆角失败。我的做法是在 JSON 里用数组顺序明确表示特征的应用顺序并且在几何生成层严格按照这个顺序执行。2.3 用 Pydantic 做参数校验的实操细节解析层输出的 JSON 不能直接信任必须做严格的校验。我用 Pydantic 定义了一套数据模型每个字段都有类型约束和范围检查。比如直径必须是正数圆角半径不能大于最小边长的一半孔间距不能超过零件尺寸等。from pydantic import BaseModel, Field, validator class Hole(BaseModel): diameter: float Field(gt0, le500) count: int Field(ge1, le100) spacing: float Field(gt0) validator(spacing) def spacing_must_fit(cls, v, values): # 这里可以加更复杂的校验逻辑 return v这样做的好处是如果模型输出的参数不合理会在解析阶段就报错而不是等到几何生成时才发现问题。报错信息也能直接告诉用户哪个参数有问题方便修正。3. 几何生成引擎选型FreeCAD、OpenCASCADE 还是 CadQuery3.1 三个候选方案的实测对比几何生成是整个流程的核心选错工具后面会非常痛苦。我实际用过的三个方案是 FreeCAD 的 Python API、直接调用 OpenCASCADE 的 Python 绑定pythonocc、以及 CadQuery。下面这张表是我自己的实测对比维度FreeCAD APIpythonoccCadQuery安装难度中等需要装完整 FreeCAD较高依赖多低pip 直接装建模代码简洁度一般API 比较啰嗦低需要手动管理拓扑高链式调用很直观STEP 导出支持支持支持DXF 导出支持需要额外处理支持布尔运算稳定性偶尔出问题稳定稳定文档和社区丰富但分散一般活跃且集中适合场景需要 GUI 交互底层定制纯脚本批量生成我最终选了 CadQuery 作为主力方案原因是它的代码写起来最像“描述几何”而不是“操作 CAD 软件”。比如生成一个带圆角和孔的盒子CadQuery 的代码是这样的import cadquery as cq result (cq.Workplane(XY) .box(120, 80, 50) .edges(|Z).fillet(5) .faces(Z).workplane() .hole(20) .pushPoints([(-30, 0), (30, 0)]) .hole(20) )这段代码的可读性非常好基本上看一遍就知道在做什么。而且 CadQuery 底层用的就是 OpenCASCADE几何内核的稳定性有保障。3.2 圆角失败最常见的几何生成报错圆角操作是几何生成里最容易出问题的地方。当你对一个边做圆角时如果相邻边的长度小于圆角半径或者圆角面和其他特征发生干涉操作就会失败。CadQuery 会抛出一个ValueError但错误信息往往不够具体。我的处理策略是分三步第一在参数校验阶段就检查圆角半径是否合理比如半径不能超过最短边长的 40%第二在几何生成时用 try-except 捕获异常如果圆角失败就自动降低半径重试每次降低 20%最多重试三次第三如果自动降级后仍然失败就跳过圆角并在返回结果里加一个警告告诉用户哪个边没能成功倒角。def safe_fillet(workplane, edges, radius, max_retries3): for i in range(max_retries): try: return workplane.edges(edges).fillet(radius) except ValueError: radius * 0.8 return workplane # 放弃圆角返回原始形状这个策略在实际项目中救了我很多次。用户不需要知道底层发生了什么他们只需要拿到一个能用的模型哪怕圆角稍微小一点也比整个生成失败要好。3.3 孔特征定位的坐标系陷阱打孔的时候最容易搞混的是坐标系。CadQuery 的workplane()方法会创建一个新的局部坐标系后续的hole()和pushPoints()都是在这个局部坐标系下操作的。如果你在顶面创建了工作平面然后想在一个相对于零件中心的位置打孔需要先想清楚局部坐标系的原点在哪里。我的经验是每次创建 workplane 之后先用center()方法把原点移到面的中心然后再用绝对坐标计算孔的位置。这样逻辑最清晰不容易出错。比如顶面两个孔间距 60mm那就是在 X 轴上 -30 和 30 的位置各打一个孔。还有一个细节hole()方法默认是贯穿整个零件的。如果你只想打一个盲孔需要指定深度参数。这个在文档里写得不明显我一开始就踩过这个坑打出来的孔直接把零件穿透了。4. 导出格式的门道STEP、DXF、URDF 各自怎么处理4.1 STEP 导出注意单位和工作坐标系STEP 是三维实体交换的标准格式几乎所有 CAD 软件都能读。CadQuery 导出 STEP 很简单cq.exporters.export(result, output.step)但有两个细节需要注意。第一CadQuery 默认导出的单位是毫米这符合大多数机械设计场景但如果你需要英寸需要在导出前对模型做缩放。第二导出的坐标系是建模时的坐标系如果下游软件期望不同的原点位置需要在导出前做平移变换。我遇到过一个实际问题生成的模型导入到某个仿真软件后位置总是偏的。排查了半天才发现那个仿真软件默认把 STEP 文件的几何中心放在场景原点而我的模型原点在底面中心。解决办法是在导出前把模型整体平移让几何中心和原点重合。4.2 DXF 导出二维投影的坑比想象中多DXF 主要用于二维图纸交换从三维模型生成 DXF 需要先做投影。CadQuery 可以通过section()方法获取截面然后导出为 DXF。但这里有个问题截面得到的是一个面而 DXF 需要的是轮廓线。我的做法是先获取截面的外轮廓线再导出section result.section(0) # 在 Z0 处截断 cq.exporters.export(section, output.dxf)实测下来简单的形状没问题但如果有内部孔洞导出的 DXF 可能会丢失孔的信息。这时候需要手动提取所有轮廓线包括外轮廓和内孔轮廓然后分别导出。这个处理比较繁琐但如果你的下游流程需要 DXF 来做激光切割或者线切割这一步绕不开。另外提醒一点DXF 的版本很多不同软件对版本的兼容性不一样。我一般导出为 R2010 版本兼容性最好。CadQuery 的导出器默认版本可能不同需要查一下文档确认。4.3 URDF 生成从几何模型到机器人描述文件URDF 是机器人仿真里描述连杆和关节的格式它本身不包含几何信息而是引用外部的网格文件通常是 STL 或 DAE。所以从 text-to-cad 生成 URDF 的流程是先生成三维几何导出为 STL然后生成引用这些 STL 的 URDF XML 文件。URDF 的核心结构包括 link连杆和 joint关节。每个 link 包含视觉几何、碰撞几何和惯性参数。视觉几何和碰撞几何可以直接引用 STL 文件惯性参数需要根据几何形状和材料密度计算。robot namegenerated_part link namebase_link visual geometry mesh filenamebase.stl/ /geometry /visual collision geometry mesh filenamebase.stl/ /geometry /collision inertial mass value0.5/ inertia ixx0.001 ixy0 ixz0 iyy0.001 iyz0 izz0.001/ /inertial /link /robot惯性参数的计算是个容易忽略的点。如果只是做视觉演示随便填一个值也能跑但如果要做动力学仿真惯性矩阵不对会导致结果完全错误。我的做法是用 CadQuery 获取模型的体积乘以材料密度得到质量然后根据几何形状用标准公式计算惯性矩。对于复杂形状可以用网格划分工具做数值积分。5. 批量生成与自动化把单次生成变成流水线5.1 从 Excel 参数表到批量 STEP 文件实际工作中更多时候不是处理单条文字描述而是有一张 Excel 表格每一行是一个零件的参数需要批量生成所有模型。这个场景下text-to-cad 的解析层可以简化——直接读 Excel 的列作为参数跳过自然语言解析这一步。我的做法是用 pandas 读 Excel每一行转成一个 JSON 对象然后循环调用几何生成函数。关键是要做好错误隔离某一个零件生成失败不能影响其他零件。用 try-except 包住每次生成失败的记录写到日志文件里最后统一报告。import pandas as pd df pd.read_excel(parts.xlsx) results [] for idx, row in df.iterrows(): try: params row.to_dict() model generate_model(params) cq.exporters.export(model, foutput/part_{idx}.step) results.append({idx: idx, status: success}) except Exception as e: results.append({idx: idx, status: failed, error: str(e)})这个流程跑一百个零件大概两三分钟比手动建模快太多了。而且参数表改一下重新跑就行完全可复现。5.2 文件命名和目录结构的规范批量生成最容易乱的是文件管理。我的规范是输出目录按日期分文件夹文件名包含零件编号和关键参数。比如20250115/part_001_120x80x50.step。这样即使过了几个月回头看也能一眼知道这个文件是什么。另外建议在输出目录里放一个manifest.json记录每个文件的生成时间、输入参数、使用的代码版本。这个习惯在出问题回溯时非常有用。5.3 性能优化什么时候该用并行单次生成一个简单零件大概 0.5 到 2 秒一百个零件串行跑也就两三分钟没必要上并行。但如果零件复杂比如有几百个特征或者数量上千串行就太慢了。这时候可以用 Python 的multiprocessing做并行。需要注意的是CadQuery 和 OpenCASCADE 不是线程安全的所以不能用多线程必须用多进程。每个进程独立初始化 CadQuery 环境处理一部分零件。进程数一般设为 CPU 核心数太多反而会因为上下文切换降低效率。from multiprocessing import Pool def process_part(args): idx, params args model generate_model(params) cq.exporters.export(model, foutput/part_{idx}.step) return idx with Pool(processes4) as pool: results pool.map(process_part, enumerate(parts))实测下来4 个进程处理 200 个中等复杂度零件总时间从 6 分钟降到不到 2 分钟提升还是很明显的。6. 实际项目中踩过的坑和应对策略6.1 模型输出不稳定同样的输入为什么结果不一样这个问题困扰了我很久。同样的文字描述有时候生成的模型是对的有时候圆角位置偏了有时候孔打穿了。排查后发现根源在自然语言解析层——大模型对同一句话的理解每次可能有细微差异导致输出的 JSON 参数不一致。解决办法是在解析层加缓存。把输入文本做哈希如果之前处理过相同的文本直接返回缓存的 JSON 结果。这样既保证了结果一致性又减少了模型调用次数。缓存可以用简单的 JSON 文件实现也可以用 SQLite。另一个办法是在解析层的 prompt 里加 few-shot 示例让模型输出格式更稳定。我一般会放三到五个示例覆盖常见的零件类型和特征组合。这个投入很值得能显著降低解析出错的概率。6.2 复杂特征的组合爆炸当零件同时包含拉伸、旋转、圆角、倒角、孔、槽等多种特征时参数空间会急剧膨胀。我一开始试图用一个通用的 JSON schema 覆盖所有情况结果 schema 越来越复杂维护成本极高。后来我换了个思路把特征做成可组合的模块每个模块有自己的参数定义和生成函数。JSON 里用一个数组表示特征序列每个特征有自己的类型和参数。这样新增特征类型只需要加一个模块不影响已有的代码。{ base: {type: box, dimensions: [120, 80, 50]}, features: [ {type: fillet, edges: vertical, radius: 5}, {type: hole, face: top, diameter: 20, positions: [[-30, 0], [30, 0]]}, {type: chamfer, edges: bottom, distance: 2} ] }这个设计的好处是扩展性强而且每个特征的生成逻辑可以独立测试出了问题容易定位。6.3 下游软件兼容性STEP 文件打不开怎么办生成的 STEP 文件在某些 CAD 软件里打不开这是很常见的问题。原因通常有两个一是 STEP 文件的版本不对二是几何本身有缺陷比如面没有闭合。版本问题好解决CadQuery 支持指定 STEP 的协议版本一般用 AP214 兼容性最好。几何缺陷比较麻烦需要用 OpenCASCADE 的修复工具做一遍检查。CadQuery 提供了Shape.fix()方法可以修复一些常见的几何问题。model model.fix() # 修复几何缺陷 cq.exporters.export(model, output.step, STEP, opt{write_pcurves: False})如果修复后仍然打不开那可能是下游软件本身的问题。我遇到过某个国产 CAD 软件对 STEP 的支持不完整同样的文件在 FreeCAD 里能打开在那个软件里就报错。这种情况只能导出为其他格式比如 IGES 或 STL来绕过。7. 关于 text-to-cad 后续扩展的一些个人想法这个方向目前能做到的是“文字描述到参数化模型”的自动化但离“任意文字描述到任意复杂模型”还有距离。我个人的判断是短期内最现实的落地场景是标准化零件的批量生成——比如法兰、支架、外壳这类形状规整、参数有限的零件。这些场景下解析层的准确率可以做到很高几何生成也稳定。再往远一点看如果能结合草图识别和三维重建让用户上传一张手绘草图或者参考照片自动提取几何参数并生成模型那适用场景会宽很多。但这涉及到计算机视觉和几何推理的深度结合技术难度不小。我在实际使用中最大的体会是不要追求一步到位。先把一个细分场景做透比如只处理“带孔和圆角的盒子类零件”把解析准确率和生成稳定性做到 95% 以上再逐步扩展特征类型。这样每一步都有可用的产出而不是做一个大而全但什么都不精的系统。另外一个小技巧在解析层的 prompt 里明确告诉模型“如果描述中有不确定的地方返回一个 clarification 字段而不是猜测”。这个改动让我的解析准确率提升了不少因为模型不再强行猜测模糊描述而是把问题抛回给用户确认。虽然多了一步交互但避免了生成错误模型后返工的麻烦。
返回列表