出图这件事,重复一千遍也不会变成手艺,只会变成损耗。所以我把它写成了代码。

一、开篇:为什么一个结构工程师要写出图插件

我的日常里有一类工作:量大、重复、还容易出错——新零件进来要建图、放视图、标注尺寸;中心线要一条条加;图层要按规范分;图纸编号后缀一张都不能错。

一天下来,时间全耗在「点鼠标」上。更糟的是人会累:漏一根中心线、标错一个后缀,返工比新画还费劲。

这些步骤每一步都有规则。有规则的东西,为什么不能交给代码?

于是我动手了。插件最终长这样:

指标数值
代码规模公开仓库 9333 行 C++(内部版 6622 行 + 25 次 Git 提交,脱敏后开源)
模块7 步出图向导 + 打标图、爆炸参数、爆炸图、球标,共 6 个模块
共享库1 个(tag 遍历、几何查询、图层规范等公共能力)
覆盖建图、视图、参数、坐标/线性标注、中心线、图层切换、后缀编号全流程

源码在 GitHub 开源:tan-wen-peng/UG_Draw_NXopen_Dill。本文所有代码都出自这个仓库,可直接对照。

这篇文章不讲 API 手册(文档里有),只讲真正折磨我的三个坑,和我最后学到的底层心法。

二、先定边界:把「人的流程」翻译成「函数的流程」

动手前最重要的一件事,是把自己出图的过程拆成一张表:

步骤人工操作插件模块
1 建图新建图纸页、选模板Step1 SheetAndViews
2 参数图纸页首选项设置Step2 SheetPreferences
3 标注逐个点选边/孔,放坐标与线性尺寸Step3/4 Dimensions
4 中心线对孔、圆柱面补中心线Step5 Centerlines
5 图层按规范移动对象到图层Step6 LayerSwitch
6 编号按规则生成图纸编号后缀Step7 AppendSuffix

每个模块一个 DLL,共用底层一个静态库。这个拆法让我能小步提交、单模块调试——下面三个坑,都是这样一个个被逼出来的。

三、坑一:跨图纸误收——插件把别的图纸的东西收走了

现象

批量出图时,脚本一次处理多张图纸页。跑完一检查:A 图纸里出现了 B 图纸的标注,中心线跑到了错误的视图上。最离谱的一次,插件把另一个打开着的部件的对象「收」了进来。

排查

一开始我用的是 NXOpen 的「优雅」写法:打开部件、拿 collection、foreach 循环。逻辑看着没问题,批量跑就串。

打日志看对象的 tag 和所属部件,才发现根源:

  1. NXOpen 对象是「包装层视图」。底层每个对象只有一个全局唯一的 tag,NXOpen 对象只是这个 tag 的轻量外壳。某些 collection 在遍历时依赖的是「会话当前上下文」,而不是「我手上的这个部件」。
  2. 部件切换时上下文漂移。批量处理频繁切换工作部件/显示部件,遍历发生在切换之后、上下文还没稳定时,拿到的就是隔壁部件的东西。
  3. 我把对象收集进一个 list,之后没有校验每个对象的归属,默认「我收集到的就是当前图纸的」。

解法:绕开包装层,走 UF 原始 tag 通道

两条原则。

第一,遍历只在「这个部件内部」进行,用 UF 的部件级循环:

// 错误示范:会话级 collection,跨图纸时对象归属不可控
for (auto *obj : part->Annotations()) { /* ... */ }

// 正确示范:用 tag 在指定部件内部循环
tag_t partTag = part->Tag();
tag_t objTag  = NULL_TAG;
while (UF_OBJ_cycle_objs_in_part(partTag, UF_dimension_type, &objTag) == 0
       && objTag != NULL_TAG) {
    // 只处理确认属于当前部件的对象
}

第二,每个对象动手前先验归属。凡是要移动、删除、修改的对象,先问一句「你是这个部件的吗」:

bool belongsToCurrentPart(NXOpen::NXObject *obj, tag_t expectPartTag) {
    if (obj == nullptr) return false;
    // tag 才是对象的唯一身份:一个对象在整个会话里只有这一个身份
    return obj->OwningPart()->Tag() == expectPartTag;
}

改完之后,「跨图纸误收」再没出现过。这个坑教我的不是某个 API 的用法,而是 NX 对象模型的一个基本事实:tag 才是对象的唯一身份,包装层只是视图。凡是批量操作,永远在 tag 层面确认归属。

四、坑二:垂直标注漏判——明明垂直,插件就是不标

现象

尺寸标注模块上线后,测试发现有的孔位垂直尺寸插件就是不生成。图纸看着明明是一根垂直线,插件却「看不见」。

排查

我的垂直判断写得非常「数学」:取线段两个端点,如果 x1 == x2 就是垂直线。单步调试一看——不是不执行,是条件永远不成立。

问题出在双精度浮点的直接相等比较。建模核心里一根「垂直」线的两个端点,X 坐标可能是 100.000000 和 100.000001——差了 1e-6,== 判下来就是「不垂直」。图纸上画得再正,数据层面它就是这么「不干净」。

解法:带容差的几何判断

把「相等」改成「足够接近」,容差要跟部件单位绑定,不能写死:

double eps = 1.0e-4;  // 与部件单位/精度相关的相对容差,按需调整
if (fabs(p1[0] - p2[0]) < eps) {
    // 判为垂直线:两 X 坐标在容差内一致
}

更稳的写法是直接问几何本身,而不是自己猜坐标:

UF_CURVE_line_t lineData;
if (UF_CURVE_ask_line_data(lineTag, &lineData) == 0) {
    double dx = lineData.end_point[0] - lineData.start_point[0];
    double dy = lineData.end_point[1] - lineData.start_point[1];
    // 用方向向量判断:水平 |dy|≈0,垂直 |dx|≈0
    bool isVertical = fabs(dx) < eps && fabs(dy) > eps;
}

心法

CAD 里的「垂直」「平行」「共线」都是容差概念,不是数学概念。任何几何判断,只要还是「浮点坐标 + 相等比较」,就迟早漏判。写完几何判断的第一件事是问自己:如果两个点差了 1e-6,我的代码还对不对?

五、坑三:视图相关几何拾取——点选投影边,就是点不到

前两个坑来自标注与批量处理,第三个坑出在中心线模块——它是整个插件里最「玄学」的一个。

现象

中心线要在剖视图里拾取两条投影边。我一开始用常规思路:在当前视图下选边。结果要么选不到,要么选到了但中心线跟视图投影边失去关联——视图一更新,中心线就悬空。

排查与解法

关键点在于:制图成员视图里的投影边是「视图相关几何」,不是模型空间的实体边。常规选择默认落在工作视图/模型空间,拾取到的根本不是视图里的那条投影边。

仓库里 Step5 的代码注释把解法写得很直白——把光标视图切到「任意视图」:

// 关联点修正要点:不再在模型空间/绝对坐标直接创建点对象,
// 而是把光标视图切到「任意视图」(UF_UI_set_cursor_view(0)),
// 在制图成员视图内拾取投影边(视图相关几何),
// 再用 Centerline2dBuilder 的 Side1/Side2 把拾取到的投影边
// (连同所在视图与拾取坐标)写入,生成的中心线与视图投影边保持关联。
int oldCursorView = 1;
UF_UI_ask_cursor_view(&oldCursorView);
UF_UI_set_cursor_view(0);

NXOpen::Selection::Response r1 = ui->SelectionManager()->SelectObject(
    "拾取视图内第一条投影边", "中心线-边1",
    NXOpen::Selection::SelectionScopeWorkPart, false, true, &edge1, &cur1);

// ... 拾取第二条边后,恢复光标视图
UF_UI_set_cursor_view(oldCursorView);

Centerline2dBuilder* clBuilder =
    part->Annotations()->Centerlines()->CreateCenterline2dBuilder(NULL);
// SetValue(对象, 所在视图, 拾取点) —— 保证关联建立在视图投影边上
clBuilder->Side1()->SetValue(edge1, sectionView, cur1);
clBuilder->Side2()->SetValue(edge2, sectionView, cur2);
clBuilder->Commit();

完整源码可下载:nx-centerline-example.cpp

心法

NXOpen 里凡是「看起来像几何的东西」,动手前先问一句:它是模型空间的实体,还是某个视图的投影? 前者在任何视图下都能选,后者只有在正确的视图上下文里才「存在」。视图相关的坑,绝大多数都出在这个判断上。

六、为什么最终全面转向 UF 原始 tag 通道

三个坑修完后我做了个复盘,发现它们有同一个根源:我在用 NXOpen 包装层做「跨对象批量操作」,而包装层对这类场景的边界行为我并不完全掌握。

UF(User Function)函数是 NX 的老接口,参数是裸 tag,看起来丑、写起来啰嗦,但有一个不可替代的优点:行为可预测。哪个对象、属于哪个部件、什么类型,全都要你自己显式处理,没有「优雅的隐式上下文」。

我的最终原则:

  • 单对象操作、快速原型:用 NXOpen,代码短、好读;
  • 批量遍历、跨部件操作、要稳的:走 UF + tag,一切显式;
  • 两边互转:NXObjectManager::Get(tag) 从 tag 拿回 NXOpen 对象,取 tag 用 obj->Tag()。

这条原则,比任何单个 API 都值钱。

七、落地效果与下一步

插件上线后,出图流程从「人做六步」变成「插件跑六步、人做检查」:

环节之前之后
建图 + 视图手动逐张建批量自动建
标注 + 中心线逐边点选规则自动生成
图层规范手工移动按规范自动归档
图纸编号后缀人工核对按规则自动生成

单张壳体图的处理时间从〔人工耗时:待补〕降到〔插件耗时:待补〕;编号规则、图层命名全部统一——从源头消灭了「这张图是谁画的」这种玄学问题。

下一步两件事:插件对接 ERP 图号体系(出图即合规),以及把爆炸图/球标模块整理成独立文章。

八、后记:给想搞二次开发的机械工程师

  1. 从 Journal(录制宏)起步。录一段操作,回放生成的代码,改几行就是你的第一个小工具。不要一上来读 6000 页 API 文档。
  2. 先搞懂 tag 和对象模型。NX 里对象身份是 tag,NXOpen 是包装层。这个概念通了,90% 的诡异问题都能自己解释。
  3. 小步提交,坑要留痕。这个仓库的每一次提交,最值钱的都是修坑的那几次。坑不写下来,下次还会踩。

出图插件三查清单(动手前念一遍):

  • 批量操作 → tag 层面验归属,别信 collection;
  • 几何判断 → 永远带容差,别用 == 比浮点;
  • 视图内拾取 → 先问「模型实体还是视图投影」,再切光标视图。

机械工程师写代码,不是为了转行,是为了把「重复一千遍」的活压缩成「检查一遍」的活。省下来的时间,才够用来做真正需要人的判断的事——比如,这张图到底该不该这样设计。


相关阅读:螺纹不是乱线,是三角形——记我画对第一根粗牙螺纹 配套资源:下载页 · 爆炸参数样例