前言:最近在使用 OCCT 开发三维算法时,我发现它在底层的模型设计上非常有层次性,于是写下这篇博客,从源码细节的角度梳理边、线、面之间的映射关系。
一、TopoDS_Shape
OCCT 中每个拓扑对象(TopoDS_Edge、TopoDS_Face等)与它的底层实现(TopoDS_TEdge、TopoDS_TFace等)遵循的是句柄(Handle)与实现分离的设计模式:TopoDS_Shape是轻量的句柄,真正承载拓扑数据的是TopoDS_TShape。
1.1 TopoDS_Shape的定义
TopoDS_Shape ├── myTShape : Handle(TopoDS_TShape) —— 指向实际的拓扑数据 ├── myLocation : TopLoc_Location —— 局部坐标系变换 └── myOrientation: TopAbs_Orientation —— 方向(FORWARD / REVERSED / INTERNAL / EXTERNAL)| 成员 | 作用 |
|---|---|
myTShape | 真正存储拓扑树结构(子形状列表)和几何引用的对象 |
myLocation | 该子形状相对于父形状的空间变换(平移、旋转等) |
myOrientation | 形状的有向性,用于边和面的边界方向判断 |
1.2 TopoDS_Shape的设计理念
这三个成员各司其职:myTShape是被共享的拓扑数据本体;myLocation让同一个TopoDS_TShape能以不同的空间位置被复用;myOrientation让同一个TopoDS_TShape能以不同的方向被引用。因此多个TopoDS_Shape可以共享同一个TopoDS_TShape,仅 Location 与 Orientation 不同。
例如,一条 Wire 的边列表存储在TopoDS_TWire中。当它被平移到 Z=10 的位置时,不需要复制整个边列表,只需新建一个 Wire 句柄指向同一个 TopoDS_TShape,并把 Location 设为对应变换:
TopoDS_Wire #1 (Location: Identity) └── myTShape ──→ TopoDS_TWire └── [Edge A, Edge B, Edge C, ...] TopoDS_Wire #2 (Location: 沿 Z 平移 10) └── myTShape ──→ TopoDS_TWire ← 同一个 TopoDS_TShape!这种设计带来两个直接好处:
- 内存高效:装配(Assembly)结构中,同一零件的不同实例共享底层几何和拓扑数据,只记录不同的位置变换。
- 方向灵活:同一个 TopoDS_TShape 可以被以
TopAbs_FORWARD和TopAbs_REVERSED两种方向引用——面的外环和内环就是通过方向来区分的。
提示:在 OCCT 8.0 中,
TopoDS_TShape进一步优化为用一个uint16_t的 bit-packed 字段存储形状类型和 8 个布尔标志位,ShapeType()变成了非虚函数,查询效率更高。[“https://occt3d.com/occt3d-open-source-8-0-is-released/”]
1.3 TopoDS_Shape之间的比较
因为两个TopoDS_Shape可能指向同一个TopoDS_TShape但 Location 不同,所以判断"两个形状是不是同一个"时要特别注意:
IsSame():比较myTShape指针是否相同(忽略 Location 和 Orientation)IsEqual():比较myTShape+myLocation+myOrientation全部相同TopTools_ShapeMapHasher:默认基于 TopoDS_TShape 指针做哈希,所以 Map 中"去重"的单位是 TopoDS_TShape
1.4 八种形状类型与层级关系
OCCT 的 B-Rep 拓扑体系定义了 8 种形状类型,按"从复杂到简单"的层级排列:
层级(复杂度从高到低) 0 COMPOUND 复合体:任意形状的集合(无拓扑连接要求) 1 COMPSOLID 复合实体:多个通过面连接的 Solid 2 SOLID 实体:由 Shell 围成的三维封闭空间 3 SHELL 壳:由边连接的 Face 集合 4 FACE 面:有界的曲面 5 WIRE 线框:由顶点连接的 Edge 序列 6 EDGE 边:有界的曲线段 7 VERTEX 顶点:空间中的一个点每一层都由下一层的形状组合而成:
Compound └── Solid └── Shell └── Face └── Wire └── Edge └── Vertex注意:这个层级关系不是严格的"父→子"包含关系树,而是有向无环图(DAG):一条 Edge 可以被多个 Wire 引用,而这些 Wire 分别属于相邻的不同 Face。理解这一点对后面讲映射至关重要。
二、TopoDS_TShape
2.1 TopoDS_TShape 的定义
TopoDS_TShape是 OCCT 拓扑体系中真正承载数据的抽象基类(继承自Standard_Transient),它只保存形状类型与状态标志这类轻量元数据,并定义两个核心接口:
TopoDS_TShape(抽象基类,继承自 Standard_Transient) │ ├── ShapeType() : TopAbs_ShapeEnum │ —— 查询该形状的类型(对应 1.4 中的 8 种类型之一) │ ├── Shapes() : TopoDS_Iterator │ —— 返回遍历直接子形状的迭代器 │ └── 8 个状态标志 —— locked / free / modified / checked / orientable / closed / infinite / convex| 接口 / 标志 | 说明 |
|---|---|
ShapeType() | 返回形状类型(TopAbs_ShapeEnum),与 1.4 中的 8 种类型一一对应 |
Shapes() | 返回直接子形状的迭代器(TopoDS_Iterator) |
| 8 个状态标志 | 记录形状状态:是否锁定(Locked)、是否可自由修改(Free)、是否被修改过(Modified)、是否已检查(Checked)、是否有方向(Orientable)、是否封闭(Closed)、是否无限(Infinite)、是否凸(Convex) |
注意,TopoDS_TShape本身不携带几何数据——点、曲线、曲面以及子形状的引用都由它的派生类承载,这正是第一节"句柄与实现分离"设计模式的落点。
2.2 TopoDS_TShape 的类体系
从TopoDS_TShape向下派生出与 8 种形状类型一一对应的 8 个具体类:
TopoDS_TShape(抽象基类) ├── TopoDS_TVertex —— 顶点 ├── TopoDS_TEdge —— 边 ├── TopoDS_TWire —— 线框 ├── TopoDS_TFace —— 面 ├── TopoDS_TShell —— 壳 ├── TopoDS_TSolid —— 实体 ├── TopoDS_TCompSolid —— 复合实体 └── TopoDS_TCompound —— 复合体这些具体类只承载拓扑骨架,真正的几何数据由 BRep 包中对应的实现类在继承后叠加:
| 纯拓扑类 | BRep 实现类 | 叠加的几何数据 |
|---|---|---|
TopoDS_TVertex | BRep_TVertex | 三维点坐标gp_Pnt |
TopoDS_TEdge | BRep_TEdge | 3D 曲线、2D pcurve、容差 |
TopoDS_TWire | BRep_TWire | 边引用列表 |
TopoDS_TFace | BRep_TFace | 曲面Geom_Surface、线引用列表 |
TopoDS_TShell | BRep_TShell | 面引用列表 |
TopoDS_TSolid | BRep_TSolid | 壳引用列表 |
"句柄与实现分离"在这里体现得最彻底:程序里持有的TopoDS_Shape句柄只是指向这些TopoDS_TShape对象的轻量引用,真正存储数据的是TopoDS_TShape及其 BRep 包中的实现类。
三、遍历:等级过滤与深度优先
3.1 TopoDS_Iterator:不带过滤的直接迭代
TopoDS_Iterator是最底层的迭代器,它直接遍历当前形状的直接子形状,不做任何类型过滤:
TopoDS_Shape shape=...;for(TopoDS_Iteratorit(shape);it.More();it.Next()){constTopoDS_Shape&sub=it.Value();// sub 是 shape 的直接子形状,类型可能是任意更低层级}它只看一层,不会递归深入。如果你传入一个 Solid,它返回的是 Shell,而不是 Face 或 Edge。
3.2 TopExp_Explorer:带类型过滤的深度优先遍历
TopExp_Explorer是日常开发中最常用的遍历工具,它会深度优先地遍历整棵子树,并只返回指定类型的形状:
#include<TopExp_Explorer.hxx>#include<TopAbs_ShapeEnum.hxx>// 遍历 solid 中所有的 FaceTopExp_Explorerexp(shape,TopAbs_FACE);for(;exp.More();exp.Next()){constTopoDS_Face&face=TopoDS::Face(exp.Current());// 处理 face}等级过滤的原理:Explorer 内部维护两个关键参数——
| 参数 | 含义 |
|---|---|
ToFind | 要查找的目标类型(如TopAbs_FACE) |
ToAvoid | 遍历中跳过的类型(默认为TopAbs_SHAPE,即不跳过任何层) |
Explorer 的核心规则是:它假定形状树中的子形状只能与父形状同类型或更简单(即层级数字更大)。当你搜索 Face 时,它不会去 Wire、Edge 或 Vertex 里面找 Face——因为按拓扑层级,下层不可能包含上层的东西。
这个"等级过滤"规则让遍历效率大大提高:如果你只想要所有 Edge,Explorer 不会在遇到 Vertex 之后继续往下走(因为 Vertex 已经是最底层了),也不会在遇到 Compound 之后又跳回去检查更高层级。
3.3 遍历中的去重问题
由于拓扑树是 DAG 而不是严格的树,一条共享边会被多个 Wire 引用,在遍历中会被多次访问。这就是为什么 OCCT 提供了TopExp::MapShapes:
#include<TopExp.hxx>#include<TopTools_IndexedMapOfShape.hxx>TopTools_IndexedMapOfShape edgeMap;TopExp::MapShapes(shape,TopAbs_EDGE,edgeMap);// edgeMap 中每条边只出现一次MapShapes内部还是用TopExp_Explorer遍历,但把结果放入一个Map中,自动基于 TopoDS_TShape 去重。
四、Edge、Wire、Face 的对应关系与相邻面查找
4.1 三种形状的基数关系
| 关系 | 基数 | 说明 |
|---|---|---|
| Face → Wire | 1 : N | 一个面可以有外环 + 多个内环(孔) |
| Wire → Edge | 1 : N | 一个线框由多条边首尾连接组成 |
| Edge → Face | N : N | 一条边可以属于多个面(共享边) |
一个 Face └── 多个 Wire(第一个是外环,其余是内环/孔洞) └── 多个 Edge(按顺序首尾相连) ↑ └── 同一条 Edge 可以被多个 Wire 引用 (这些 Wire 分属相邻的不同 Face)4.2 相邻面查找:走 Edge→Face,而不是 Wire→Face
两个面"相邻"的本质是共享一条边。立方体正面与侧面共享一条棱边:正、反两个面各有自己独享的 Wire,两条 Wire 都引用了这条公共边(方向一正一反),但彼此独立、没有交集。这是因为Wire 是每个 Face 独有的边界结构:
- 一个 Wire 只属于一个 Face(作为它的外环或内环),相邻面之间不存在共享的 Wire;
- Wire 本身不携带任何几何数据,只持有 Edge 的引用列表(
BRep_TWire中只有myEdges),面的边界形状完全由这些边的顺序与方向决定。
因此面与面之间的连接点只能是 Edge,而不是 Wire。