开篇先交代一个背景:最近在帮注塑模团队做一套下料统计工具,核心需求很简单——在NX里选中零件或打开一个prt,程序自动算出这块料的长、宽、高,再把结果填进BOM属性、导出下料单。听上去就是“测量体”三个字的事,但真正落地的时候发现,从“量一个数”到“算出一个车间能用的坯料尺寸”,中间隔着坐标系、余量规则、单位换算、异常体过滤一堆细节。我选择用Python而不是C++来做NX二次开发,原因也很实际:开发周期短、不用编译、NX从11.0版本开始就内置了Python解释器,录一段Journal再改一改就能用。这篇文章是“坯料尺寸”系列的第一篇,先把最核心的包络尺寸计算讲清楚,后面再陆续展开毛坯块自动生成、装配体整体尺寸、通孔盲孔识别这些延伸内容。
最近社区里聊NX MCP的朋友不少,AI辅助写NX脚本确实能提速,但如果连底层的包络计算逻辑都没掌握,AI生成一堆API调用也未必能过验证。我始终觉得,二次开发里最值钱的不是代码本身,而是你知道“为什么用这个接口、这个数算出来能不能用”。这篇就把这块地基夯实。
1. 先把“坯料尺寸”这个概念跟工程师对齐
很多新手一上来就写代码,结果算出来的数和车间老师傅说的“毛坯尺寸”对不上,问题往往出在概念没对齐。坯料尺寸,指的是零件在进入机加工之前,下料阶段需要准备的原材料尺寸,可能是一块板材的长宽厚,可能是一根棒料的直径和长度,也可能是一个锻件毛坯的外廓。它跟三维模型里实体的包围盒尺寸有强关联,但绝不是同一个东西。
1.1 为什么不能直接拿“测量体”的结果当坯料尺寸
NX的“测量体”功能或GetBoundingBox得到的,是模型几何在当前坐标系下的最大外接长方体范围,它只反映几何外形,完全不考虑加工工艺。车间下料不会按照“模型净尺寸”来切料,至少要预留加工余量:铣削要留夹持位和精加工余量,磨削要留磨量,线切割要留穿丝位置。另外,采购标准规格板材时,长宽必须按照供应商能提供的规格圆整,比如你算出212.6mm,车间大概率会直接按220mm甚至230mm下料,因为220mm正好是一张板的整数分割尺寸。
我一开始只导出了模型的长宽高,车间反馈过两次“没法用”,后来才把余量和圆整规则做进程序里。所以这套系列文章里说的“坯料尺寸”,准确讲是:模型包络尺寸 + 工艺余量 + 采购/下料圆整规则。第一篇先解决包络尺寸怎么算,余量和圆整在第五部分讲。
1.2 哪些项目场景真正需要程序化计算
手动测量一个零件的包络尺寸,一分钟内能搞定,为什么还要写程序?我实际遇到的需求大概有三类:
- 批量BOM刷新:设计变更后,几十个零件的外形尺寸全部变了,下料单要重新生成,手动一个个量根本不现实;
- 参数化驱动:通过表达式或电子表格驱动模型尺寸,每次变化都要自动更新毛坯,这必然要靠二次开发而不是人工操作;
- 跨系统数据交互:把NX里的外形尺寸写到属性、导出到Excel/CSV,再传给ERP或MES系统,手动量出来的数很难保证和模型一致。
如果你也是这类需求,这篇文章的脚本可以直接抄作业。如果你只是偶尔想量一个零件的尺寸,那确实没必要做二次开发,记住这个边界能省很多事。
2. NX Python开发环境:从录Journal到自己写脚本
入门的最大障碍不是API不熟,而是很多人不知道“NX里怎么写Python、怎么跑起来”。NX的Python支持不是外挂一个解释器,而是随软件一起发布的CPython环境,所以不需要你额外安装Python。所有NXOpen Python脚本,本质上和录制的Journal是同一种东西,只不过Journal是你手动操作时系统自动生成的代码,我们自己写就是结构化、可复用的版本。
2.1 版本要求与内置解释器
从NX 11.0开始,Siemens就提供了Python Journal功能,后续1880、1926、2206、2306、2412这些版本都在持续完善。我日常主力是NX 2206和NX 2306,下面的代码在这两个版本上都验证过。
打开NX之后,你可以在菜单栏找到Tools > Journal,这里面有Record、Run、Edit三个主要入口。Record是录制,Run是运行某个.py文件,Edit是打开最近录制的脚本。第一次接触的人我建议先做一步:手动创建一个方块,然后Tools > Journal > Record,随便做几个操作,停止录制,再用Edit打开生成的文件看一眼。你会瞬间理解NX Python脚本的骨架长什么样:开头import NXOpen和import NXOpen.UF,然后Session.GetSession()拿到会话对象,所有操作都从会话和Part对象出发。
2.2 三种运行脚本的方式
- 第一种:Tools > Journal > Run,选择.py文件。最常用,适合已经写好的脚本,运行后输出结果,不修改模型。
- 第二种:File > Execute > NX Open。效果和Run类似,我更习惯用Journal Run,因为入口短。
- 第三种:在Recording状态下操作,让NX自己生成代码,然后保存成自己的工具。这种方式适合查API,不适合正式工程,因为生成的代码噪音很大。
在NX 12之后的版本里,还有一个非常方便的入口:直接在Command Finder(命令查找器)里输入“Journal”,也能快速定位到录制和运行入口。
2.3 要不要装VS Code或PyCharm
很多朋友纠结开发环境,其实NX Python脚本没必要走复杂IDE。因为NX内置的解释器只存在于NX进程内部,VS Code和PyCharm都不能直接“运行”NX脚本。你可以用它们来写代码、做语法高亮,但执行和调试一定是在NX里完成。
我个人习惯是:用VS Code写代码,写好后到NX里Journal > Run。如果你追求更舒服的调试体验,可以后续研究NX的远程调试功能,但第一篇文章不建议在这里卡住。另有几个内置Python环境的特点需要提前知道:
- 不能用
pip install到NX内置Python里(至少在标准授权方式下很麻烦); numpy、pandas这些第三方库默认不可用,但csv、json、os、math这些标准库是可以import的;- 如果你需要做复杂数学计算,要么用NX自己的数学API,要么把数据导出到外部用你自己的Python环境处理。
这个限制在很多人第一次写NX脚本时会踩坑。比如想直接import numpy算矩阵,结果报ModuleNotFoundError。我后来的做法是:涉及复杂算法时,先在外部用普通Python写一个独立的小工具验证,再把结果通过文件传给NX脚本,两边解耦。
3. 计算包络尺寸的三条技术路线
现在进入正题:怎么用代码拿到一个体的长宽高。NX的API体系里,至少有三种拿法,各有优势和局限。我分别说清楚,你按自己的NX版本和精度要求选。
3.1 NXOpen对象方式:Body.GetBoundingBox()
NXOpen是NX二次开发的官方推荐接口体系,C++、C#、Python都有对应封装。对一个具体实体,可以直接调用Body.GetBoundingBox(),拿到一个包含最小角点和最大角点的对象或数组,不同NX版本对这个返回值类型的封装略有差异。
有的版本返回一个BoundingBox对象,里面有CenterPoint之类的属性;有的版本直接返回6个浮点数。最稳妥的办法是,在你的NX里按F11打开录制,然后手动选中一个体,用“分析→测量”或者“创建包容块”操作,录出来的代码里就会展示当前版本GetBoundingBox的真实用法。我的经验是,这个接口适合需要快速拿到某个体的局部包络,但想要遍历整个part所有体的总包络时,每次都去解析返回值反而麻烦。
3.2 UF函数方式:UF_MODL_ask_bounding_box
UF函数是NX的老牌C接口,虽然官方一直说推荐用NXOpen,但UF函数在Python里依然可以调用,而且稳定性极好。我这套工具的核心就是用UF_MODL_ask_bounding_box。它接收一个tag(实体的标识),返回一个长度为6的浮点数数组:前三个值是最小角点的X、Y、Z,后三个值是最大角点的X、Y、Z。
在Python里的调用方式是这样的:
import NXOpen import NXOpen.UF ufs = NXOpen.UF.UFSession.GetUFSession() box = ufs.Modl.AskBoundingBox(body.Tag)body.Tag在Python里可以直接作为整型传入,这一点对新手很友好。拿到box之后,box[0]到box[2]是min角点,box[3]到box[5]是max角点,长宽高就是对应坐标相减。
这个接口返回的单位是部件单位。如果你的prt单位是毫米,返回的就是毫米;如果是英寸,返回的就是英寸。做工具时一定要统一单位,我习惯在输出前做一次round四舍五入,保留三位小数。
3.3 遍历Face求极值:自定义方向的前置方案
当你遇到一个斜着放置的零件,或者希望按工作坐标系的某个方向而不是绝对坐标系方向来算尺寸时,UF和NXOpen的默认包围盒都不够用。这时候你需要自己遍历实体的所有面,再遍历每个面上的边或者顶点,把所有点投影到目标方向轴上求最小值、最大值。
这套逻辑本身不复杂,但计算量和代码量会明显上升。对于第一篇文章,我不展开写完整代码,只给一个思路:先收集体上所有面的顶点坐标,然后定义三个目标方向向量(比如工作坐标系的X轴、Y轴、Z轴),每个点分别和方向向量做点积,取每个方向上的最小值和最大值,差值就是该方向上的“投影尺寸”。这就是最小有向包围盒(OBB)的雏形。后续如果大家的零件普遍是斜的,我再专门写一篇。
三种方式对比如下:
| 方案 | 稳定性 | 代码量 | 灵活性 | 适用场景 |
|---|---|---|---|---|
| Body.GetBoundingBox() | 中等,版本差异大 | 少 | 低 | 快速看单个体 |
| UF_MODL_ask_bounding_box | 高,老接口稳定 | 中 | 中 | 批量统计、工具开发 |
| 遍历Face求极值 | 高,但代码复杂 | 多 | 高 | 需自定义方向或OBB |
我目前的生产工具默认用UF函数,这样不管在哪个NX版本上跑,行为都一致。如果遇到方向不对的零件,再进入自定义方向分支处理。
4. 完整脚本:求当前工作part中所有体的最大包络尺寸
下面这段脚本是我工具里最基础的一个版本,用途是:打开任意一个prt,运行它,自动计算当前工作部件下所有实体体的整体包络尺寸,并输出最小角点、最大角点以及X/Y/Z方向尺寸。
import NXOpen import NXOpen.UF def listing_write(the_session, text): """把文本输出到NX的信息窗口""" listing_window = the_session.ListingWindow() listing_window.Open() listing_window.WriteLine(text) def main(): the_session = NXOpen.Session.GetSession() work_part = the_session.Parts.Work if work_part is None: print("没有打开的工作部件,请先打开一个prt再运行") return ufs = NXOpen.UF.UFSession.GetUFSession() min_pt = [float("inf"), float("inf"), float("inf")] max_pt = [float("-inf"), float("-inf"), float("-inf")] body_count = 0 for body in work_part.Bodies: tag = body.Tag try: box = ufs.Modl.AskBoundingBox(tag) except Exception as ex: listing_write(the_session, "体 %s 计算失败: %s" % (body.JournalIdentifier, str(ex))) continue body_count += 1 min_pt[0] = min(min_pt[0], box[0]) min_pt[1] = min(min_pt[1], box[1]) min_pt[2] = min(min_pt[2], box[2]) max_pt[0] = max(max_pt[0], box[3]) max_pt[1] = max(max_pt[1], box[4]) max_pt[2] = max(max_pt[2], box[5]) if max_pt[0] < min_pt[0]: listing_write(the_session, "当前part没有找到任何可计算的实体体") return dx = round(max_pt[0] - min_pt[0], 3) dy = round(max_pt[1] - min_pt[1], 3) dz = round(max_pt[2] - min_pt[2], 3) listing_write(the_session, "共统计实体体数量: %d" % body_count) listing_write(the_session, "最小角点: (%s, %s, %s)" % (min_pt[0], min_pt[1], min_pt[2])) listing_write(the_session, "最大角点: (%s, %s, %s)" % (max_pt[0], max_pt[1], max_pt[2])) listing_write(the_session, "包络尺寸: 长X宽X高 = %s X %s X %s" % (dx, dy, dz)) listing_write(the_session, "对应方向: X = %s, Y = %s, Z = %s" % (dx, dy, dz)) if __name__ == "__main__": main()这段代码有几点值得说明。
第一,work_part.Bodies遍历的是当前部件下所有实体body(包括实体和片体,具体和版本有关)。如果你的prt里有大量片体,不想统计它们,可以在遍历时加一个body.IsSolidBody判断,但这个属性在不同NX版本里命名略有差异,建议用录制Journal的方式确认一下准确写法。
第二,listing_write这个函数封装了信息窗口输出。NX的print在Journal运行时输出位置不稳定,有时候能看到,有时候被吞掉。信息窗口输出是NX Open Python最可靠的显示方式,尤其适合工具类脚本。你打开信息窗口的快捷键是Ctrl+Shift+L,或者通过Window > Output。
第三,异常处理很关键。一个prt里可能有损坏的几何体、轻量级体或者比较特殊的构造体,直接调用BoundingBox偶尔会报错。我的处理方式是单个体失败不中断整个程序,打印出JournalIdentifier,方便你回到模型里定位问题体。
要验证脚本对不对,最快的方法是新建一个prt,插入一个100x50x20的方块体,然后运行脚本。正常输出应该类似:
共统计实体体数量: 1 最小角点: (0.0, 0.0, 0.0) 最大角点: (100.0, 50.0, 20.0) 包络尺寸: 长X宽X高 = 100.0 X 50.0 X 20.0 对应方向: X = 100.0, Y = 50.0, Z = 20.0如果你把方块旋转45度再运行,会发现长宽高变成一组和原来完全不同的数值。这不是代码出错,而是UF函数的包围盒始终基于绝对坐标系的X/Y/Z方向,模型一旋转,外接长方体的范围就变了。这就引出下一部分最重要的话题:怎样保证你算出来的尺寸是“车间能用”的尺寸。
5. 让结果可用:从包络尺寸到真正的坯料尺寸
很多教程写到第四部分就结束了,但现实中拿到一个“绝对坐标系下的长宽高”往往还不够。真正要让结果可用,必须处理三个问题:方向对齐、余量规则、结果输出。
5.1 方向问题:WCS与绝对坐标系
先说坐标系。UF_MODL_ask_bounding_box和大部分NX内置测量体功能一样,返回的是绝对坐标系下的包络范围。在模具设计、机械加工场景里,零件在建模时一般会尽量把主方向摆正到绝对坐标方向,这时候直接用没问题。但总会有一些零件是斜着建模的,或者为了装配方便,主方向和工作坐标系的某个轴对齐而不是和绝对坐标对齐。
遇到这种情况,最简单粗暴的修正方案有两个:
- 方案A:在NX里手动把WCS旋转到目标方向,然后旋转体或移动体,把模型摆正后再算。这个方案适合单个零件,不适合批量。
- 方案B:程序里读取当前工作坐标系(WCS)的三个轴方向,把实体的所有关键点投影到WCS轴上,得到按WCS方向的外接尺寸。这个方案可封装成通用函数,但代码量不小。
对于第一篇文章,我的建议是:先确认你的模型有没有摆正。90%的规则零件其实都是好的,只有少数斜装零件需要特殊处理。等这套基础脚本跑通之后,再针对斜装零件写一个“按WCS方向求投影尺寸”的增强版函数。
5.2 余量和圆整规则怎么加
拿到了包络尺寸,不等于拿到了坯料尺寸。实际项目中,长宽高三个方向都要根据不同加工工艺加放余量。我在这套工具里把余量配置做成一个独立的JSON或Python字典,由工艺工程师维护,而不是写死在代码里。
一个简单示例:
MARGIN_RULES = { "cnc": {"x": 2.0, "y": 2.0, "z": 3.0}, "grind": {"x": 0.5, "y": 0.5, "z": 0.5}, "wire_edm": {"x": 1.0, "y": 1.0, "z": 0.2}, } def apply_margin(dx, dy, dz, rule_name="cnc"): rule = MARGIN_RULES[rule_name] final_dx = dx + rule["x"] final_dy = dy + rule["y"] final_dz = dz + rule["z"] return final_dx, final_dy, final_dz圆整规则就更有行业味道了。板料下料通常按“逢5进1”或“逢10进1”,棒料按标准直径靠档。举例:你算出某块板的长度为122.4mm,按10mm圆整规格,应当取130mm;宽度81.2mm,取90mm。这个逻辑可以用一段很短的代码实现:
import math def round_up_to_step(size, step=10): return math.ceil(size / step) * step但注意,这种“逢10进1”不是所有车间都适用,有的精加工车间可能要求“逢5进1”。所以最好把圆整步长也做成配置项,不要让算法替你拍板。
5.3 把结果写进属性或导出CSV
打印到信息窗口只是调试阶段的做法,生产环境要把结果沉淀下来。最常见的两种做法是写属性和导CSV。
写属性的话,可以用NXOpen的属性API把长宽高、坯料编码写到部件属性或体属性。具体接口在不同版本有差异,我建议你先用录制Journal的方式走一遍“属性→编辑属性”,看看录制代码里的真实API,再改成自己的逻辑。关键点是,属性名要提前和BOM、ERP团队约定好,避免大小写和命名不一致。
导CSV反而简单,因为NX内置Python的标准库是完整的,直接用csv模块写文件即可:
import csv import os def export_dimension_to_csv(file_path, dx, dy, dz): with open(file_path, "w", newline="") as f: writer = csv.writer(f) writer.writerow(["名称", "X/mm", "Y/mm", "Z/mm"]) writer.writerow(["当前部件", dx, dy, dz])我实际项目里的做法是选一个局域网共享目录,程序跑完后把整个文件夹下所有prt的尺寸统一汇总成一个CSV,然后下游的ERP定时任务去读这个文件。这样设计和生产之间就有了一个“脏活累活”的自动通道,不需要人为抄数。
5.4 后面几篇我会写什么
坯料尺寸这个方向可以一直往下挖。第二篇我打算讲如何根据包络尺寸自动生成一个毛坯块体(Block),并把毛坯块放到指定图层;第三篇讲装配环境下统计所有组件整体包络尺寸,这里会涉及轻量级组件、Reference Set、隐藏组件等一堆边界情况;再往后可以聊特征识别,比如区分通孔和盲孔,通过面的拓扑关系判断孔类型,用于估算加工工时和线切割路径。这些都是实际车间项目里会连续遇到的问题,一篇篇拆开写,每一篇都可以直接落地。
最后分享一个我在实际使用中发现的细节:UF_MODL_ask_bounding_box返回的是当前几何的真实包络,但如果模型里有被抑制的特征、轻量级显示状态或者小平面体,统计结果可能和设计人员肉眼看到的不一样。所以自动计算做完之后,建议在交付前人工抽查两三个模型,尤其是带WAVE链接或者大量特征的复杂part。这个经验是常规教程里不会写的,但搞二次开发的人早晚会遇到。