OCCT OCAF 实战:从"看不见的数据"到数据驱动的 3D 显示
前段时间在学 OpenCASCADE(OCCA Technology,下称 OCCT)的 OCAF 模块,踩了不少坑,也终于把"数据驱动 3D 显示"这条链路跑通了。这篇文章记录完整过程:从 OCAF 是什么、怎么搭,到自定义属性、撤销重做、持久化,最后用 TPrsStd 把数据接到 3D 视图,并解决"改了参数 viewer 没反应"的经典问题。
环境:OCCT 7.9.3(Homebrew 安装)/ macOS + Apple M2 / CMake + GLFW / C++17

最终效果:两个零件来自同一个 OCAF 文档,各自带着不同的变换属性------一个绕斜轴旋转 45°,一个平移到远处
一、OCAF 到底是干什么的?
先说清一个容易混淆的点:几何内核和 OCAF 是两层东西。
- 几何内核(TKBRep/TKPrim...)管的是"形状是什么 "------
TopoDS_Shape就是一个纯几何对象,程序一关啥都不剩 - OCAF(Open CASCADE Application Framework)管的是 CAD 应用的"其他一切":
| CAD 软件需要 | OCAF 提供 |
|---|---|
| 模型层次结构(装配→零件→特征) | 标签树(TDF_Label) |
| 参数、名字、注释、引用 | 标准属性(TDataStd_*) |
| 自定义业务数据 | 自定义属性(继承 TDF_Attribute) |
| Ctrl+Z / Ctrl+Y | 事务 + TDF_Delta 自动生成 |
| 形状的参数化演化历史 | TNaming(拓扑命名) |
| 文档保存/打开 | 持久化驱动框架 |
一句话:几何内核是"画图的",OCAF 是让你做出"CAD 软件"的。
二、工程搭建:两个链接坑
用 CMake 链 OCAF 库时踩了两个坑(7.9.3):
cmake
target_link_libraries(ocaf_tutorial PRIVATE
TKernel TKMath # 基础 / gp 数学
TKCDF TKLCAF # 文档基座(CDM,CDF,TDF) / 标准属性+文档
TKCAF # TNaming 拓扑命名 ← 坑2
TKBin # 完整 OCAF 二进制持久化驱动 ← 坑1相关
TKBRep TKPrim TKTopAlgo
)
- 坑 1 :网上老教程让链接
TKTDF------7.9 里这个库不存在,TDF 已并入 TKLCAF - 坑 2 :
TNaming_NamedShape符号在 TKCAF 里,不在 TKLCAF,漏链就是一堆 undefined symbol
三、第 1-2 课:应用、文档、标签树
OCAF 的入口是"应用"和"文档"。老教程会教你手写 Plugin/Resources 文件 + 设环境变量------7.9 已经不用了 ,一行 DefineFormat 搞定:
cpp
Handle(TDocStd_Application) anApp = new TDocStd_Application();
anApp->DefineFormat ("OCAF-myApplication", // 格式名
"MyApplication Document Version 1.0",
"sta", // 扩展名
new BinDrivers_DocumentRetrievalDriver(), // 读取驱动
new BinDrivers_DocumentStorageDriver()); // 存储驱动
Handle(TDocStd_Document) aDoc;
anApp->NewDocument ("OCAF-myApplication", aDoc);
文档的本质是一棵标签树 ,每个标签(TDF_Label)可以挂任意多个属性(靠 GUID 区分):
cpp
TDF_Label aRoot = aDoc->Main(); // 根标签 0
TDataStd_Name::Set (aRoot, "Assembly"); // 挂名字
TDF_Label aBoxLabel = aRoot.FindChild (1); // 子标签 0:1
TDataStd_Name::Set (aBoxLabel, "Box");
TDataStd_Integer::Set (aBoxLabel, 1); // 挂整数
TDataStd_Comment::Set (aBoxLabel, "示例零件");
TDF_Label aDimLabel = aBoxLabel.FindChild (1); // 孙标签 0:1:1
TDataStd_Real::Set (aDimLabel, 50.0); // 挂实数(尺寸参数)
TDF_Label aRefLabel = aRoot.FindChild (3);
TDF_Reference::Set (aRefLabel, aBoxLabel); // 跨分支引用(持久化)
记住这个心智模型:OCAF ≈ CAD 版的数据库------Label 是表,Attribute 是字段,TDF_Delta 是事务日志,StorageDriver 是存储引擎。
四、第 3 课:自定义属性(官方文档有 3 个 bug)
想要自己的业务数据(比如"零件的位姿变换"),继承 TDF_Attribute。核心四件套:
GetID()------ 属性唯一 GUIDSet()------ Find-or-Create 惯用法- 修改前
Backup()------ 撤销的根基 Restore/NewEmpty/Paste三个虚方法 ------ 撤销/重做/复制粘贴所需
cpp
class MyPackage_Transformation : public TDF_Attribute
{
public:
static const Standard_GUID& GetID();
static Handle(MyPackage_Transformation) Set (const TDF_Label& theLabel);
gp_Trsf Get() const; // 按记录的类型组装 gp_Trsf
void SetRotation (const gp_Ax1&, Standard_Real angle);
void SetTranslation (const gp_Vec&);
// ... 镜像/缩放/复合
const Standard_GUID& ID() const override;
void Restore (const Handle(TDF_Attribute)&) override;
Handle(TDF_Attribute) NewEmpty() const override;
void Paste (const Handle(TDF_Attribute)&,
const Handle(TDF_RelocationTable)&) const override; // ← 注意 const
Standard_OStream& Dump (Standard_OStream&) const override;
private:
gp_TrsfForm myType;
gp_Ax1 myAx1; gp_Ax2 myAx2; gp_Ax3 myFirstAx3, mySecondAx3;
Standard_Real myAngle, myScale;
gp_Pnt myFirstPoint, mySecondPoint;
};
官方文档(ocaf.md)的代码在 7.9 上有 3 处编译错误,踩坑实录:
| 文档 bug | 修正 |
|---|---|
Dump() 实现里用了未声明的 anOS(参数是 theOS) |
改用 theOS |
Dump 声明非 const、实现 const,签名打架 |
统一 const(基类虚函数就是 const) |
Paste 实现缺 const(7.9 基类签名是 const = 0) |
补 const |
Set 声明按值传 TDF_Label、实现按引用 |
统一 const TDF_Label& |
撤销/重做有三个前置条件,缺一个都不生效(我第一次跑 Undo 纹丝不动,查了半天):
cpp
aDoc->SetUndoLimit (10); // ① 默认撤销是【禁用】的,必须设上限
aDoc->NewCommand(); // ② 每组修改必须包在事务里
aTrsf->SetRotation (gp_Ax1 (gp_Pnt(0,0,0), gp_Dir(0,0,1)), M_PI / 4.0);
aDoc->CommitCommand(); // ③ Commit 时才生成 TDF_Delta
aDoc->Undo(); // 回滚到上一个 delta
aDoc->Redo();
五、第 4-5 课:TNaming 与持久化
把形状挂到标签上,附带"演化历史"(PRIMITIVE=新图元/MODIFY=修改/...):
cpp
TDF_Label aBoxLabel = aRoot.FindChild (1);
BRepPrimAPI_MakeBox aMakeBox (50, 50, 50);
TNaming_Builder aBuilder (aBoxLabel);
aBuilder.Generated (aMakeBox.Shape()); // PRIMITIVE 演化
保存与打开:
cpp
anApp->SaveAs (aDoc, "ocaf_tutorial.sta");
anApp->Close (aDoc);
anApp->Open ("ocaf_tutorial.sta", aDoc2); // 标准属性、NamedShape 全部还原
六、解剖 .sta 文件:里面到底存了什么?
二进制文件直接 strings + xxd 就能看出结构(源码在 BinLDrivers_DocumentStorageDriver.cxx):
go
"BINFILE" ← 魔数 7 字节
01 02 03 04 ← 字节序标记(小端)
FSD_FileHeader(12×int32) ← 各 section 偏移表(未用=-1)
── ASCII 信息区(明文!)──
"10/22/2026"
"FILE_FORMAT: OCAF-myApplication"
"REFERENCE_COUNTER: 0"
"MODIFICATION_COUNTER: 1"
"START_TYPES" ← 类型注册表(类型名↔整数ID)
TDataStd_Name / TDataStd_Integer / TDataStd_Comment
/ TDataStd_RealArray / TNaming_NamedShape / ...
"END_TYPES"
── 二进制体 ──
递归标签树: [int32 tag] + 属性流[类型ID, 对象ID, 载荷...]
形状数据: TNaming 的 BRep 二进制(v12+ 内联在属性流)
心智模型:OCAF ≈ CAD 版数据库 ------Label 是表,Attribute 是字段,.sta 是存储引擎的落盘格式。
七、第 6 课:TPrsStd 三步法,数据接上 3D 视图
ocaf_tutorial 是纯控制台的(数据层本来就没 GUI)。要让数据"被看见",官方 TPrsStd_Sample 给了三步法:
cpp
// ① 把 "AIS 视图器" 作为【属性】挂到根标签 ------ 数据框架从此知道自己连着哪个视图
TPrsStd_AISViewer::New (aDoc->Main(), myV3dViewer);
// ② 从数据框架取回该属性
Handle(TPrsStd_AISViewer) anAisViewer;
TPrsStd_AISViewer::Find (aDoc->Main(), anAisViewer);
// ③ 交互上下文不自己 new, 而是从数据框架里拿!
myContext = anAisViewer->GetInteractiveContext();
// 把形状标签变成可显示对象 (关联它的 TNaming_NamedShape)
Handle(TPrsStd_AISPresentation) aPrs =
TPrsStd_AISPresentation::Set (shapeLabel, TNaming_NamedShape::GetID());
aPrs->SetColor (Quantity_NOC_GREEN);
aPrs->Display (true);
第 ③ 步是 OCAF 的灵魂:AIS_InteractiveContext 不在 main 里创建,而是作为属性存在文档里------"哪些对象显示、什么颜色"成了文档数据的一部分,换台电脑打开,显示状态原样还原。
我把 glfw 窗口与这套数据接起来,3D 窗口里出现了绿色线框立方体 ------它就是从 .sta 里读出来的那个方块。

绿色方块 = 文档 0:1 标签上的 TNaming 形状;金色圆锥 = 0:2 标签(顺带也放进文档了,但还没给它接显示)
八、数据驱动显示:同一形状,不同姿态
真正有意思的来了。给每个零件标签挂一个独立的变换属性:
css
0 "Assembly"
├─ 0:1 "Box" NamedShape(方块@原点) + Transformation(绕Z旋转45°)
├─ 0:2 "Cone" NamedShape(圆锥@(25,125,0)) + Transformation(平移100,0,0)
└─ 0:3 Reference → 0:1
viewer 端遍历零件标签,把变换属性读出来作用到显示对象:
cpp
for (TDF_ChildIterator it (myDoc->Main()); it.More(); it.Next())
{
TDF_Label aPart = it.Value();
Handle(TNaming_NamedShape) aNS;
if (!aPart.FindAttribute (TNaming_NamedShape::GetID(), aNS)) continue;
Handle(TPrsStd_AISPresentation) aPrs =
TPrsStd_AISPresentation::Set (aPart, TNaming_NamedShape::GetID());
// ★ 数据驱动显示: 读变换属性 → 作用到 AIS 对象
Handle(MyPackage_Transformation) aTrsf;
if (aPart.FindAttribute (MyPackage_Transformation::GetID(), aTrsf))
{
aPrs->GetAIS()->SetLocalTransformation (aTrsf->Get());
}
aPrs->Display (true);
}
结果:形状几何完全相同,变换属性不同 → 显示姿态完全不同。我给 Box 设的是绕 (10,0,1) 斜轴旋转 45°,Cone 平移 (100,0,0) ------这就是参数化 CAD 的底层逻辑------改数据,不改几何。

两个零件各自的 Transformation 属性被 viewer 读取并作用到显示对象上:绿色方块绕斜轴倾斜 45°,金色圆锥平移到了远处(对比第 6 课配图中二者的建模位置)
一个调试经验:验证变换是否生效,选让姿态变化最明显的参数------绕 Z 轴转 45° 的方块在透视相机下几乎看不出区别,我一开始还以为代码没生效,后来把轴改成斜轴才确认。
九、翻车实录:改了参数,viewer 纹丝不动
得意没多久就翻车了:我改了 tutorial 里 SetTranslation/SetRotation 的参数,重新生成 .sta,viewer 显示毫无变化。
排查后发现是两个原因叠加:
原因 1:撤销/重做演示的事务覆盖。 我的演示是"事务1 平移 → 事务2 旋转 → Undo → Redo",最终状态=事务2 的旋转。改事务1 的平移参数,永远不会体现在最终结果(纯演示用)。
原因 2(致命):自定义属性根本不持久化。 回顾第六节:.sta 的类型表里没有 MyPackage_Transformation------自定义属性没写持久化驱动,保存时被直接跳过 。于是 viewer 打开文件发现"没有变换属性"→ 触发我写的"旧数据重建"逻辑 → 用它自己硬编码的默认参数(45°/100)新建了文档。改什么参数都白改,viewer 永远显示默认值。
修复 :用 OCAF 的正统方案------镜像到标准属性 (ocaf.md 开篇原话:"能用标准属性就用标准属性")。给属性类加两个静态方法,把 3×4 变换矩阵写进 TDataStd_RealArray(1,12)(标准属性自动存盘):
cpp
void MyPackage_Transformation::StoreMatrix (const TDF_Label& theLabel, const gp_Trsf& theTrsf)
{
Handle(TDataStd_RealArray) anArr = TDataStd_RealArray::Set (theLabel, 1, 12);
int k = 1;
for (int i = 1; i <= 3; ++i)
for (int j = 1; j <= 4; ++j, ++k)
anArr->SetValue (k, theTrsf.Value (i, j));
}
Standard_Boolean MyPackage_Transformation::ReadMatrix (const TDF_Label& theLabel, gp_Trsf& theTrsf)
{
Handle(TDataStd_RealArray) anArr;
if (!theLabel.FindAttribute (TDataStd_RealArray::GetID(), anArr) || anArr->Length() < 12)
return false;
theTrsf.SetValues (anArr->Value(1), anArr->Value(2), anArr->Value(3), anArr->Value(4),
anArr->Value(5), anArr->Value(6), anArr->Value(7), anArr->Value(8),
anArr->Value(9), anArr->Value(10), anArr->Value(11), anArr->Value(12));
return true;
}
数据流变成:
makefile
tutorial: SetRotation/SetTranslation → StoreMatrix(RealArray) → .sta 含 RealArray ✓
viewer: 打开 .sta → ReadMatrix → gp_Trsf → SetLocalTransformation → 显示 ✓
第5课读回验证:变换矩阵(RealArray 镜像)读回: 成功。再次修改参数 → viewer 姿态跟着变 ✓。
教训:自定义属性是"内存里的富 API",持久化要么写 BinM 驱动(进阶),要么镜像到标准属性(实用)。
验证对比(下图):修复后重跑 tutorial + viewer,方块的斜轴旋转终于生效了(对比第 8 节配图前的状态------修改参数前 viewer 永远显示默认姿态)。

十、总结
| 收获 | 说明 |
|---|---|
| OCAF 分层观 | 几何内核管形状,OCAF 管模型的一切"其他数据" |
| 标签树 + 属性 | 万物皆 Label + GUID 属性,Find-or-Create 惯用法 |
| 撤销三件套 | SetUndoLimit + NewCommand/CommitCommand + 修改前 Backup(),缺一不可 |
| 持久化真相 | 标准属性自动存盘;自定义属性不存(需 BinM 驱动或标准属性镜像) |
| TPrsStd 三步法 | AISViewer 挂根标签 → Find → GetInteractiveContext(),显示状态即数据 |
| 7.9 vs 老文档 | DefineFormat 替代资源文件;TKTDF 不存在;TNaming 在 TKCAF |
完整代码在 GitHub 仓库 learning/ocaf/ 目录:ocaf_tutorial.cpp(5 课数据层)、ocaf_viewer.cpp(第 6 课显示)、MyPackage_Transformation.hxx/.cpp(自定义属性)。
发掘金时:文中配图在
blog/images/目录,上传到掘金图床后替换相对路径即可。
下一步计划:OCAF Function Mechanism(ocaf_func.md)------参数驱动重算,改一个圆锥参数自动重建整根"钉子",彻底实现参数化建模。
若有帮助,点个赞再走;有错误欢迎评论区指正。