几何内核数据结构设计及原理

文章目录

  • [BRep 拓扑结构设计说明](#BRep 拓扑结构设计说明)
    • [1. 第一条:建模就是修改,所以 BRep 推荐使用链表结构](#1. 第一条:建模就是修改,所以 BRep 推荐使用链表结构)
    • [2. 第二条:同样是链表,关系怎么挂也会影响效率](#2. 第二条:同样是链表,关系怎么挂也会影响效率)
      • [2.1 直观但不推荐的设计](#2.1 直观但不推荐的设计)
      • [2.2 更推荐的设计:Loop 只保存第一条 Coedge](#2.2 更推荐的设计:Loop 只保存第一条 Coedge)
      • [2.3 为什么这对欧拉操作特别重要?](#2.3 为什么这对欧拉操作特别重要?)
    • [3. 奇技淫巧:新建实体直接插到链表头部](#3. 奇技淫巧:新建实体直接插到链表头部)
    • [4. 最终数据结构设计:Face / Loop / Coedge / Edge](#4. 最终数据结构设计:Face / Loop / Coedge / Edge)
      • [4.1 Face 的数据结构](#4.1 Face 的数据结构)
      • [4.2 Loop 的数据结构](#4.2 Loop 的数据结构)
      • [4.3 Coedge 的数据结构](#4.3 Coedge 的数据结构)
        • [4.3.1 在 Loop 内表达顺序](#4.3.1 在 Loop 内表达顺序)
        • [4.3.2 在 Face 之间表达邻接](#4.3.2 在 Face 之间表达邻接)
        • [4.3.3 表达 Edge 的使用方向](#4.3.3 表达 Edge 的使用方向)
      • [4.4 Edge 的数据结构](#4.4 Edge 的数据结构)
    • [5. 拓扑访问方法](#5. 拓扑访问方法)
      • [5.1 Loop 访问 Coedge](#5.1 Loop 访问 Coedge)
      • [5.2 Face 访问 Edge](#5.2 Face 访问 Edge)
      • [5.3 Edge 访问 Coedge](#5.3 Edge 访问 Coedge)
      • [5.4 Vertex 访问 Edge](#5.4 Vertex 访问 Edge)
    • [6. 拓扑设计的整体讲述](#6. 拓扑设计的整体讲述)
      • [6.1 总体层级关系](#6.1 总体层级关系)
      • [6.2 Body / Lump / Shell 是模型的大层级](#6.2 Body / Lump / Shell 是模型的大层级)
      • [6.3 Face 是曲面上的一块区域](#6.3 Face 是曲面上的一块区域)
      • [6.4 Loop 是 Face 的边界](#6.4 Loop 是 Face 的边界)
      • [6.5 Coedge 是"有方向的 Edge 使用"](#6.5 Coedge 是“有方向的 Edge 使用”)
      • [6.6 Edge 是真实拓扑边](#6.6 Edge 是真实拓扑边)
      • [6.7 Vertex 是拓扑点](#6.7 Vertex 是拓扑点)
    • [7. 拓扑和几何要分离](#7. 拓扑和几何要分离)
    • [8. 支持不同模型类型](#8. 支持不同模型类型)
      • [8.1 Wire Body](#8.1 Wire Body)
      • [8.2 Sheet Body](#8.2 Sheet Body)
      • [8.3 Solid Body](#8.3 Solid Body)
      • [8.4 Non-manifold Body](#8.4 Non-manifold Body)
    • [10. 设计经验总结](#10. 设计经验总结)
      • [10.1 上级只存入口,下级自己串链](#10.1 上级只存入口,下级自己串链)
      • [10.2 Coedge 是拓扑关系核心](#10.2 Coedge 是拓扑关系核心)
      • [10.3 Edge / Vertex 保存入口,不保存完整缓存](#10.3 Edge / Vertex 保存入口,不保存完整缓存)
      • [10.4 新实体优先头插](#10.4 新实体优先头插)
      • [10.5 遍历顺序不是语义](#10.5 遍历顺序不是语义)
    • [11. 一个简单例子:四边形 Face](#11. 一个简单例子:四边形 Face)
    • [12. 最后总结](#12. 最后总结)
    • 附录------代码实现

BRep 拓扑结构设计说明

本文不展开讲半边结构本身,默认读者已经了解 half-edge / coedge 的基本的了解(如果后续有人感兴趣,我会专门写一篇讲解半边结构设计)。这里重点讲述在几何内核里,BRep 拓扑数据应该怎么组织,才能让建模操作更简单、更稳定、更高效。

首先说一下,数据结构的设计原理很简单,定义数据结构,就是为了操作简单便捷高效。

BRep 拓扑结构的设计核心不是"把数据摆得整齐",而是为了让建模修改方便。

建模内核里最常见的事情不是静态展示模型,而是不断修改模型:

  • 新增一个点, 新增一条边;
  • 把一个 loop 拆开;
  • 把两个 face 合并;
  • 在 shell 上新增一个 face;
  • 布尔运算后重建局部拓扑;
  • 倒角、圆角、拉伸、切割后更新边界关系。

因此,一个好的 BRep 数据结构,应该满足:

text 复制代码
局部修改简单;
指针关系清晰;
遍历方式统一;
能自然支持 solid / sheet / wire / non-manifold 等复杂情况。

简单说就是:

设计要点:所谓建模,就是建造模型。注重建造,也就是修改。BRep 拓扑结构是为了"建造模型"服务的,而建造模型最重要的是修改方便。而大家都知道,数据结构中,便于修改的数据结构是链表,所以,brep的数据结构肯定得推荐用链表的形式。


1. 第一条:建模就是修改,所以 BRep 推荐使用链表结构

所谓建模,就是建造模型。

建造模型的过程,本质上就是不断修改拓扑结构。例如一个很简单的操作:在一个已有面上加一条边,或者从一个顶点拉出一条新边,这都会改变原有的 vertex、edge、coedge、loop、face 之间的关系。

在数据结构里,最适合频繁插入、删除、拆分、合并的结构是什么?

答案通常是链表。

如果使用数组保存拓扑对象,比如:

text 复制代码
Shell 里面放 vector<Face*>
Face  里面放 vector<Loop*>
Loop  里面放 vector<Coedge*>

看起来很直观,但是一旦涉及修改,就会遇到问题:

  1. 插入一个元素时,可能要移动后面的元素;
  2. 删除一个元素时,可能要维护下标变化;
  3. 拆分和合并时,数组内容要重新整理;
  4. 如果外部还依赖数组下标,修改会更加麻烦;
  5. 建模操作越复杂,维护成本越高。

而链表结构更适合这种场景。

例如 Shell 下有很多 Face,用链表可以表示为:

text 复制代码
Shell : Face3 -> Face2 -> Face1 -> null

Face 下有多个 Loop:

text 复制代码
Face : Loop2 -> Loop1 -> null

Loop 里面有多个 Coedge:

text 复制代码
Loop : Coedge1 <-> Coedge2 <-> Coedge3 <-> Coedge1

这种结构的好处是:

  • 新增实体时,只需要改少量指针;
  • 删除实体时,只需要断开局部关系;
  • 插入到某个位置时,不需要移动一整段数据;
  • 欧拉操作这种局部拓扑修改会非常自然。

所以,BRep 拓扑结构推荐用"指针 + 链表"的形式来组织。

这不是因为链表高级,而是因为:

BRep 的核心工作是修改拓扑,而链表天然适合局部修改。


2. 第二条:同样是链表,关系怎么挂也会影响效率

只说"用链表"还不够。

同样是链表,不同的挂法,效率和可维护性差别会非常大。

这里用 Loop 和 Coedge 的关系举例。

2.1 直观但不推荐的设计

一种很直观的设计是:Loop 里面直接保存一个 coedge 链表或数组。

text 复制代码
Loop
 └─ coedgeList
     ├─ Coedge1
     ├─ Coedge2
     ├─ Coedge3
     └─ Coedge4

这个设计很容易理解:一个 loop 由多条 coedge 组成,所以 loop 保存所有 coedge。

但是在建模操作中,它并不一定高效。

比如欧拉操作里非常常见的 mev

make edge and vertex,创建一个新的 vertex 和 edge,同时把新的 coedge 插入到原有 loop 中。

假设我们要把一条新的 coedge 插到 Coedge2 和 Coedge3 中间。如果 Loop 自己维护完整 coedge list,就经常需要:

  1. 先从 Loop 的列表里找到 Coedge2 或 Coedge3;
  2. 确定插入位置;
  3. 调整列表;
  4. 更新新增 coedge 的 owner;
  5. 更新相关 edge / vertex / twin 等关系。

如果 coedge 数量很多,查找和维护成本就会上升。

2.2 更推荐的设计:Loop 只保存第一条 Coedge

更推荐的设计是:

Loop 只保存第一条 coedge 的指针;

Coedge 自己保存 previous 和 next 指针;

多条 Coedge 自己组成一个环形双向链表。

也就是说:

text 复制代码
Loop 只知道入口:firstCoedge
Coedge 自己知道:previous / next

这样一来,如果我们已经知道要在 Coedge2 和 Coedge3 中间插入新的 CoedgeNew,只需要改四个指针:

text 复制代码
CoedgeNew.previous = Coedge2
CoedgeNew.next     = Coedge3
Coedge2.next       = CoedgeNew
Coedge3.previous   = CoedgeNew

插入前:

text 复制代码
Coedge2 <-> Coedge3

插入后:

text 复制代码
Coedge2 <-> CoedgeNew <-> Coedge3

Loop 本身甚至不需要知道这次局部插入的细节。

这就是这个设计的关键:

Loop 只提供入口,真正的顺序关系由 Coedge 自己维护。

2.3 为什么这对欧拉操作特别重要?

欧拉操作的特点是局部修改。

比如:

  • mev:新增 vertex 和 edge;
  • mef:新增 edge 和 face;
  • kemr:删除 edge 并生成 inner loop;
  • kfmrh:合并 face 并形成孔环。

这些操作通常并不是重建整个 body,而是在已有拓扑上改一小块关系。

如果拓扑结构设计得好,欧拉操作就会变成"改几个指针"。

如果拓扑结构设计得不好,每次操作都要扫描列表、移动元素、维护数组下标,代码会复杂很多,也更容易出错。

所以,Loop / Coedge 这种设计的本质目的是:

text 复制代码
让局部拓扑修改变成局部指针修改。

这也是 BRep 数据结构设计的核心思想。


3. 奇技淫巧:新建实体直接插到链表头部

这里讲一个非常简单但很有用的技巧:

上级实体只保存下级链表的头指针。

新建下级实体时,直接插到链表头部。

以 Shell 和 Face 为例。

Shell 下面会挂一个 Face 指针,Face 自己再保存 next 指针,于是 Shell 下的 Face 形成一个链表:

text 复制代码
Shell -> Face99 -> Face98 -> ... -> Face1 -> null

现在新建了一个 Face100,要挂到 Shell 下面。

一种做法是:从 Shell 的第一个 Face 开始,一直找到最后一个 Face,然后挂到尾部。

text 复制代码
Shell -> Face1 -> Face2 -> ... -> Face99 -> Face100

这个做法的问题是:如果 Shell 下面已经有 99 个 Face,就要遍历 99 次才能找到尾部。

更简单的做法是:直接把新 Face 插到头部。

只需要两步:

text 复制代码
Face100.next = Shell.firstFace
Shell.firstFace = Face100

插入后变成:

text 复制代码
Shell -> Face100 -> Face99 -> Face98 -> ... -> Face1 -> null

是不是很简单?

这就是为什么在 Parasolid、ACIS 这类内核里,有时候你遍历一个 Shell 的所有 Face,会发现"最后创建的 Face 最先被访问到"。

原因很简单:

新建 Face 被插到了 Face 链表的头部。

这个技巧不只适用于 Shell -> Face,也适用于很多上级/下级关系:

text 复制代码
Body  -> Lump
Lump  -> Shell
Shell -> Face
Face  -> Loop
Shell -> Wire

头插法的优点是:

  1. 不需要找链表尾部;
  2. 新增操作是 O(1);
  3. 实现非常简单;
  4. 非常适合频繁创建拓扑实体的建模场景。

当然,它也带来一个注意点:

遍历顺序不应该被当成稳定语义。

也就是说,不能认为第一个遍历到的 Face 就一定是"最早创建的 Face"或"最重要的 Face"。链表顺序只是内部组织方式,不应该作为业务逻辑依赖。

如果需要稳定识别某个 Face,应该使用 Tag、Persistent ID、Attribute 或历史特征系统,而不是依赖链表顺序。


4. 最终数据结构设计:Face / Loop / Coedge / Edge

下面把几个最核心的拓扑数据结构列出来。

这里不是完整代码,而是按照设计意图列出关键成员,方便理解它们之间的关系。

4.1 Face 的数据结构

Face 表示曲面上的一个有界区域。

它不直接保存 Edge,而是通过 Loop 访问边界。

text 复制代码
Face
 ├─ next          指向同一个 Shell / SubShell 下的下一个 Face
 ├─ loop          指向 Face 的第一个 Loop
 ├─ shell         所属 Shell
 ├─ surface       Face 对应的几何曲面
 └─ sense         Face 方向是否与曲面自然方向一致

可以理解为:

text 复制代码
Face 负责表达"曲面上的一块区域"。
Loop 负责表达"这块区域的边界"。
Surface 负责表达"这块区域所在的几何曲面"。

Face 的关键访问入口是:

text 复制代码
Face -> Loop -> Coedge -> Edge

所以 Face 不需要直接保存所有 Edge。

这样设计的好处是:

  • 一个 Face 可以有多个 Loop,例如一个外环和多个内孔;
  • 每个 Loop 自己维护一圈 Coedge;
  • Edge 可以被多个 Face 共享;
  • Face 的边界方向由 Coedge 的顺序和 sense 表达。

4.2 Loop 的数据结构

Loop 表示 Face 上的一条边界环。

一个 Face 通常至少有一个外环,也可能有多个内环。

text 复制代码
Loop
 ├─ next          指向同一个 Face 下的下一个 Loop
 ├─ coedge        指向该 Loop 的第一条 Coedge
 ├─ face          所属 Face
 └─ loopType      Loop 类型,例如 outer / inner 等

Loop 的关键设计是:

text 复制代码
Loop 不保存所有 Coedge,只保存第一条 Coedge。

Coedge 自己通过 previous / next 组成环:

text 复制代码
Loop -> Coedge1 <-> Coedge2 <-> Coedge3 <-> Coedge1

这样做特别适合插入、删除、反转、拆分 loop。

例如一个外环:

text 复制代码
Face
 └─ Outer Loop
     └─ Coedge1 -> Coedge2 -> Coedge3 -> Coedge4 -> Coedge1

一个带孔的 Face:

text 复制代码
Face
 ├─ Outer Loop
 └─ Inner Loop

Loop 的类型也很重要,例如:

  • Outer:外边界;
  • Inner:孔边界;
  • Vertex:孤立顶点 loop;
  • Wire:wire 类型 loop;
  • Winding:周期曲面上的缠绕 loop。

4.3 Coedge 的数据结构

Coedge 是 BRep 里最关键的关系节点。

它表示:

一条 Edge 在某个 Loop / Face / Wire 中的一次有方向使用。

text 复制代码
Coedge
 ├─ next          同一个 Loop 中的下一条 Coedge
 ├─ previous      同一个 Loop 中的上一条 Coedge
 ├─ twin          同一条 Edge 在相邻 Face 中的另一条 Coedge
 ├─ edge          被使用的真实 Edge
 ├─ sense         使用方向是否与 Edge 本身方向一致
 └─ owner         所属 Loop / Wire

Coedge 同时承担三件事:

4.3.1 在 Loop 内表达顺序
text 复制代码
coedge.next
coedge.previous

通过 next / previous,多个 Coedge 构成一个边界环。

4.3.2 在 Face 之间表达邻接
text 复制代码
coedge.twin

一条 Edge 通常被两个 Face 共享。两个 Face 各有一条 Coedge 使用这条 Edge,这两条 Coedge 互为 twin。

text 复制代码
FaceA 的 CoedgeA ---- twin ---- FaceB 的 CoedgeB
            │                         │
            └──────── Edge ───────────┘

通过 twin,可以从一个 Face 跨过 Edge 找到相邻 Face。

4.3.3 表达 Edge 的使用方向

Edge 自己有方向,比如从 start vertex 到 end vertex。

但是同一条 Edge 在不同 Face 的边界里,使用方向可能相同,也可能相反。

所以 Coedge 需要 sense:

text 复制代码
sense = true   表示 Coedge 方向与 Edge 方向一致
sense = false  表示 Coedge 方向与 Edge 方向相反

这样 Edge 可以保持唯一,而不同 Face 可以通过 Coedge 表达各自的边界方向。

4.4 Edge 的数据结构

Edge 表示真实拓扑边。

它连接两个 Vertex,并引用一条三维几何曲线。

text 复制代码
Edge
 ├─ startVertex   起点 Vertex
 ├─ endVertex     终点 Vertex
 ├─ coedge        指向使用这条 Edge 的某一条 Coedge
 ├─ curve         三维几何曲线
 ├─ sense         Edge 方向与几何曲线方向是否一致
 ├─ interval      Edge 在曲线上的参数范围(可选)
 └─ tolerance     边容差

注意:

text 复制代码
Edge 也不保存所有 Coedge,只保存其中一条 Coedge 作为入口。

普通流形模型里,一条 Edge 通常被两个 Face 使用,对应两条 Coedge:

text 复制代码
Edge -> CoedgeA
         │
         twin
         ▼
       CoedgeB
         │
         twin
         ▼
       CoedgeA

非流形情况下,一条 Edge 可能被多个 Face 共用,对应多条 Coedge:

text 复制代码
Edge -> Coedge1 -> twin -> Coedge2 -> twin -> Coedge3 -> twin -> Coedge1

所以 Edge 只需要保存一条 Coedge 作为入口,完整关系可以通过 twin 链走出来。

这也是一个很重要的思想:

Edge 保存访问入口,不保存完整缓存。


5. 拓扑访问方法

上面讲的是数据怎么存,下面讲数据怎么访问。

BRep 的访问通常不是"某个对象直接保存所有下级对象",而是通过入口指针和链表关系逐步走出来。

5.1 Loop 访问 Coedge

Loop 只保存第一条 Coedge。

遍历一个闭合 Loop 的 Coedge,可以这样理解:

text 复制代码
first = loop.coedge
coedge = first

do {
    visit(coedge)
    coedge = coedge.next
} while (coedge != first)

访问路径是:

text 复制代码
Loop -> first Coedge -> next -> next -> ... -> first Coedge

这就是 loop 边界遍历的基础。

5.2 Face 访问 Edge

Face 不直接保存 Edge。

Face 访问 Edge 的路径是:

text 复制代码
Face -> Loop -> Coedge -> Edge

如果 Face 有多个 Loop,就先遍历 Loop 链表,再遍历每个 Loop 里的 Coedge 环。

text 复制代码
Face
 ├─ Loop1 -> Coedge -> Edge
 └─ Loop2 -> Coedge -> Edge

注意:如果只是访问当前 Face 的边界,通常按 Coedge 顺序访问即可。

如果要收集唯一 Edge,则需要根据 Edge 指针或 Tag 去重。

5.3 Edge 访问 Coedge

Edge 保存一条 Coedge 入口。

访问使用该 Edge 的所有 Coedge,可以从这条入口 Coedge 出发,沿 twin 链遍历。

普通流形情况:

text 复制代码
Edge -> CoedgeA -> twin -> CoedgeB -> twin -> CoedgeA

非流形情况:

text 复制代码
Edge -> Coedge1 -> twin -> Coedge2 -> twin -> Coedge3 -> twin -> Coedge1

这样就可以通过 Edge 找到所有使用它的 Face。

访问路径是:

text 复制代码
Edge -> Coedge -> Owner Loop -> Face

5.4 Vertex 访问 Edge

Vertex 通常保存一个相邻 Edge 的入口。

text 复制代码
Vertex -> one Edge

通过这条 Edge,再结合 Coedge 的 previous / next / twin 关系,可以找到围绕该 Vertex 的其它 Edge。

也就是说,Vertex 不一定要维护完整的相邻边数组。

这样设计是为了降低修改成本:

  • 如果每个 Vertex 都维护完整 edge list,拓扑修改时要同步更新很多缓存;
  • 如果 Vertex 只维护入口,局部拓扑改动更简单;
  • 需要完整邻接时,可以沿拓扑关系临时遍历出来。

对于非流形顶点,Vertex 可以保留多个入口,分别对应不同的面组或 wire 分支。


6. 拓扑设计的整体讲述

前面已经把核心结构列出来了,这里再从整体上串起来讲一遍。

6.1 总体层级关系

一个常见的 BRep 拓扑层级可以理解为:

text 复制代码
Body
 ├─ Lump 链表
 │   └─ Shell 链表
 │       ├─ Face 链表
 │       │   └─ Loop 链表
 │       │       └─ Coedge 环形双向链表
 │       │           └─ Edge
 │       │               ├─ Start Vertex
 │       │               ├─ End Vertex
 │       │               └─ 3D Curve Geometry
 │       │
 │       └─ Wire 链表
 │
 └─ Wire 链表(wire body 时使用)

其中几个关键点要特别注意:

  • Body 是模型入口;
  • Lump 表示一个连通体区域;
  • Shell 表示一组连通的面壳;
  • Face 是曲面上的一个有界区域;
  • Loop 是 Face 的边界环,可以是外环、内环、孔环、退化环等;
  • Coedge 是 Edge 在某个 Face / Loop 中的一次"有方向使用";
  • Edge 是真实拓扑边,可以被多个 Coedge 使用;
  • Vertex 是边的端点;
  • 几何对象 和拓扑对象分离:Face 引用 Surface,Edge 引用 Curve,Vertex 引用 Point。

简单说:

Face 不是直接拥有 Edge,而是通过 Loop 里的 Coedge 使用 Edge。

Edge 也不是只属于一个 Face,它可以被两个甚至多个 Face 通过 Coedge 共享。

这正是 BRep 能表达实体边界、相邻面、非流形结构的关键。

6.2 Body / Lump / Shell 是模型的大层级

一个 Body 是模型入口。

它下面可以有 Lump,也可以直接有 Wire。

text 复制代码
Body
 ├─ Lump 链表
 └─ Wire 链表

Lump 表示一个连通体区域,Shell 表示一组连通的边界壳。

text 复制代码
Body -> Lump -> Shell

Shell 下面挂 Face:

text 复制代码
Shell -> Face -> Face -> Face

这些上级/下级关系大多采用链表头指针的方式组织。

6.3 Face 是曲面上的一块区域

Face 本身不是几何曲面。

Face 是拓扑实体,它引用一个 Surface。

text 复制代码
Face -> Surface

Surface 负责回答:这个面所在的几何曲面是什么?是平面、圆柱面、球面、NURBS 曲面,还是其它曲面。

Face 负责回答:这张曲面上哪一块区域属于模型边界?

这个区域由 Loop 限定。

text 复制代码
Face -> Loop

6.4 Loop 是 Face 的边界

Loop 是一圈边界。

一个 Face 可以有多个 Loop:

text 复制代码
Face
 ├─ Outer Loop
 ├─ Inner Loop
 └─ Inner Loop

外环表示 Face 的外边界,内环表示孔。

Loop 不直接保存所有 Edge,而是保存第一条 Coedge。

text 复制代码
Loop -> Coedge

6.5 Coedge 是"有方向的 Edge 使用"

Coedge 是整个结构里最重要的连接点。

Edge 是真实边;Coedge 是 Edge 在某个 Face 边界中的一次使用。

为什么要多一层 Coedge?

因为同一条 Edge 可能被两个 Face 共用,并且在两个 Face 中方向不同。

如果 Face 直接挂 Edge,就很难表达:

  • 这条 Edge 在当前 Face 里的方向;
  • 当前 Face 过这条 Edge 后的相邻 Face;
  • 这条 Edge 在当前 Face 参数域里的 pcurve;
  • 非流形情况下多个 Face 共用一条 Edge。

Coedge 正好解决这些问题。

它一边通过 next / previous 参与 Loop;一边通过 twin 连接相邻 Face;一边通过 edge 引用真实 Edge。

text 复制代码
Loop 顺序:Coedge.next / Coedge.previous
Face 邻接:Coedge.twin
真实边:  Coedge.edge
方向:    Coedge.sense
参数曲线:Coedge.pcurve

6.6 Edge 是真实拓扑边

Edge 连接两个 Vertex,并引用一条三维曲线。

text 复制代码
Edge
 ├─ Start Vertex
 ├─ End Vertex
 └─ 3D Curve

Edge 不属于某一个 Face。

它可以被多个 Face 通过 Coedge 使用。

所以 Edge 保存一条 Coedge 入口,通过 twin 链可以找到所有使用它的 Coedge。

这让普通流形和非流形结构都能被统一表达。

6.7 Vertex 是拓扑点

Vertex 引用一个几何 Point。

text 复制代码
Vertex -> Point

Edge 用 Vertex 作为边界端点。

普通情况下,Vertex 保存一个相邻 Edge 的入口即可;复杂非流形情况下,可以保存多个入口。

这依然遵循同一个思想:

保存访问入口,而不是保存所有关系的完整缓存。


7. 拓扑和几何要分离

BRep 结构里还有一个非常重要的原则:

拓扑回答"谁和谁相连";几何回答"它在空间中长什么样"。

所以:

text 复制代码
Face   -> Surface
Edge   -> Curve
Vertex -> Point
Coedge -> PCurve

Face 不直接等于 Surface。

Edge 不直接等于 Curve。

Vertex 不直接等于 Point。

它们是拓扑实体,只是引用几何实体。

这样设计的好处是:

  1. 多个拓扑实体可以共享几何;
  2. 拓扑修改和几何修改可以相对解耦;
  3. 同一条 3D Edge 可以在不同 Face 上有不同的 2D pcurve;
  4. 建模算法可以先处理拓扑关系,再更新几何关系。

例如两个 Face 共享一条 Edge:

text 复制代码
FaceA 的 CoedgeA -> Edge -> 3D Curve
FaceB 的 CoedgeB -> Edge -> 3D Curve

但它们各自在曲面参数域里的 pcurve 可以不同:

text 复制代码
CoedgeA -> PCurve on SurfaceA
CoedgeB -> PCurve on SurfaceB

这就是拓扑-几何分离带来的表达能力。


8. 支持不同模型类型

一个好的 BRep 结构不能只支持封闭实体,还要能支持 wire body、sheet body、solid body,以及非流形结构。

8.1 Wire Body

Wire Body 没有 Face,主要由 Wire、Coedge、Edge、Vertex 组成。

text 复制代码
Body -> Wire -> Coedge -> Edge -> Vertex

这种情况下,Coedge 的 owner 可能是 Wire,而不是 Loop。

8.2 Sheet Body

Sheet Body 有 Face,但不是封闭体。

它的边界上可能存在 free edge。

free edge 的特点是:

text 复制代码
只有一侧 Face 使用它;
Coedge 的 twin 可能为空。

8.3 Solid Body

Solid Body 通常由封闭 Shell 构成。

普通流形 solid 中,一条 Edge 一般被两个 Face 使用,对应两条互为 twin 的 Coedge。

8.4 Non-manifold Body

非流形情况下,一条 Edge 可以被多个 Face 共用。

这时 twin 不再只是两条 Coedge 互指,而可以形成一个 Coedge 环。

text 复制代码
Coedge1 -> twin -> Coedge2 -> twin -> Coedge3 -> twin -> Coedge1

这也是 Edge 只保存一条 Coedge 入口的原因之一。


10. 设计经验总结

整个 BRep 拓扑设计可以总结成几句话。

10.1 上级只存入口,下级自己串链

text 复制代码
Body  只存第一个 Lump
Lump  自己存 next

Shell 只存第一个 Face
Face  自己存 next

Face  只存第一个 Loop
Loop  自己存 next

Loop  只存第一条 Coedge
Coedge 自己存 next / previous

这样新增和删除都很轻量。

10.2 Coedge 是拓扑关系核心

Face 不直接挂 Edge,而是通过 Coedge 使用 Edge。

text 复制代码
Face -> Loop -> Coedge -> Edge

Coedge 同时表达:

  • loop 内顺序;
  • edge 使用方向;
  • 相邻 face;
  • pcurve;
  • owner 关系。

10.3 Edge / Vertex 保存入口,不保存完整缓存

text 复制代码
Edge   保存一条 Coedge 入口
Vertex 保存一条或少量 Edge 入口

需要完整邻接关系时,通过拓扑链遍历出来。

这样可以避免修改时维护大量缓存。

10.4 新实体优先头插

text 复制代码
newItem.next = oldHead
head = newItem

这是一个非常简单但很实用的技巧。

它让新增操作变成 O(1),非常适合建模内核。

10.5 遍历顺序不是语义

由于大量使用头插法,遍历顺序可能和创建顺序相反。

因此不要依赖遍历顺序表达业务含义。

如果需要稳定识别拓扑实体,应使用:

  • Tag;
  • Persistent ID;
  • Attribute;
  • History / Feature 记录。

11. 一个简单例子:四边形 Face

假设有一个四边形 Face。

它有:

  • 1 个 Face;
  • 1 个 Loop;
  • 4 条 Coedge;
  • 4 条 Edge;
  • 4 个 Vertex。

结构如下:

text 复制代码
Face
 └─ Loop
     └─ Coedge1 -> Coedge2 -> Coedge3 -> Coedge4
          ▲                              │
          └──────────────────────────────┘

Coedge1.edge = Edge1
Coedge2.edge = Edge2
Coedge3.edge = Edge3
Coedge4.edge = Edge4

Edge1.start = Vertex1
Edge1.end   = Vertex2
Edge2.start = Vertex2
Edge2.end   = Vertex3
Edge3.start = Vertex3
Edge3.end   = Vertex4
Edge4.start = Vertex4
Edge4.end   = Vertex1

如果旁边还有另一个 Face 共享 Edge2,那么另一个 Face 也会有一条 Coedge 使用 Edge2。

text 复制代码
FaceA 的 Coedge2 ---- twin ---- FaceB 的某条 Coedge
          │                         │
          └──────── Edge2 ──────────┘

这样就能从 FaceA 通过 Coedge2 找到 Edge2,也能通过 twin 找到 FaceB。


12. 最后总结

BRep 拓扑结构的设计不是为了静态看起来直观,而是为了建模操作简单、稳定、高效。

推荐的核心设计是:

text 复制代码
用链表组织拓扑实体;
上级实体只保存下级入口;
同级实体通过 next 串起来;
Loop 内部用 Coedge 的 next / previous 组成环;
Edge 通过 Coedge 被 Face 使用;
Coedge 通过 twin 连接相邻 Face;
Edge / Vertex 保存访问入口,不维护复杂缓存;
新建实体优先头插,局部操作只改少量指针。

这套设计非常适合欧拉操作,也非常适合 wire body、sheet body、solid body、open shell、free edge、non-manifold edge、isolated vertex loop 等复杂建模场景。

一句话概括:

好的 BRep 拓扑结构,就是让复杂建模操作最终变成简单的局部指针修改。

简单说就是:

BRep 拓扑结构的好坏,不在于静态看起来是否直观,而在于建模时改起来是否简单、稳定、高效。

附录------代码实现

最后,附上工程上实际的拓扑结构的代码实现:见资源 拓扑数据结构设计与定义,C++实现