AMCAX Kernel
Geometry kernel for CAD/CAE/CAM
九韶内核 1.0.0.0
载入中...
搜索中...
未找到
拓扑结构的 B-rep 表示

B-rep 结构

边界表达(Boundary Representation,简称 B-rep)是当前最流行的数据结构。B-rep 可以在减轻存储压力的同时,保持完备性和无歧义。B-rep 使用实体边界上的点、线、面来表示实体,实体的体信息由包含在边界内的三维空间导出。其两个基本元素为:几何信息(Point、Curve、Surface);拓扑信息(Vertex、Edge、Face连接关系)。

拓扑结构的定义

定义

我们先给出一个 B-rep 结构的各级拓扑结构的定义:

  • Vertex 是拓扑意义上的点,对应几何上的 Point
  • Edge 是拓扑意义上的边,对应几何上的 Curve 以及两个约束 Curve 边界的 Vertex
  • Wire 是一些首尾相连的 Edge,相邻 Edge 共用 Vertex
  • Face 是拓扑意义上的面,对应几何上的 Surface 以及约束 Surface 边界的若干 Closed Wire
  • Shell 是一些相连的 Face,相邻 Face 要共用 1 个或多个 Edge
  • Solid 是被 Closed Shell 围成的空间
  • CompSolid 是一些 Solid
  • Compound 是若干拓扑结构的集合

常见例子

接下来我们将举一些例子,方便您更好的理解。

一个曲面构成的Face

不难看出,这个例子有 4 个 Vertex,4 个 Edge,1 个 Wire,1 个 Face。

两个相邻的曲面构成的Shell

因为中间重合部分的 Vertex 和 Edge 是共用的,所以这个例子有 6 个 Vertex,7 个 Edge,2 个 Wire,2 个 Face,1 个 Shell。

正方体

这个例子有 8 个 Vertex,12 个 Edge,6 个 Wire,6 个 Face,1 个 Shell。

圆柱

这个例子有 2 个 Vertex,3 个 Edge,3 个 Wire,3 个 Face,1 个 Shell。而 0 号 Edge 在 Wire 中被复用了,所以这是一条经典的 Seam Edge。

这个例子有 2 个 Vertex,3 个 Edge,1 个 Wire,1 个 Face,1 个 Shell。在这个例子中不仅有 Seam Edge,还有两条没有长度的Edge。事实上,这两条 Edge 尽管在 3D 中没有长度,但是在参数域上是有长度的,所以这是两条经典的 Degenerated Edge(退化边)。

Edge

接下来,我们将介绍比较常见的几种 Edge。

普通Edge

在一个封闭的 TopoShape(这里指 Shell、Solid)内,通常来说最常见的 Edge 是两个不同 Face 的邻接 Edge。普通 Edge 属于两个不同的 Face,且长度不为 0。

Seam Edge

一个常见的特例是圆柱的侧边,这条 Edge 被一个 Face 所有,且在 Face 的一个 Wire 中出现两次(一次正,一次反)。Seam Edge 是合法的。

Degenerated Edge(退化边)

一个常见的特例是球的顶边,这条 Edge 长度为 0,退化成了一个点。退化 Edge 是合法的。

自由 Edge

只属于 1 个 Face,且在 Face 的 1 个 Wire 中只出现一次的 Edge 称为自由 Edge。自由 Edge 在封闭 TopoShape 中是非法的。

非流形 Edge

在 1 个 Shape 或 Solid 中,属于超过 2 个 Face 的 Edge 称为非流形 Edge。非流形 Edge 是非法的。

拓扑遍历

TopoShape 的树状结构

首先我们先来介绍一下 TopoShape 的树状结构,如下图所示:

遍历

对一个拓扑结构进行遍历是十分常见的操作,我们内核提供两种方式: AMCAX::TopoExplorerAMCAX::WireExplorer ,接下来我们将对这两种方式进行逐一介绍。

TopoExplorer

AMCAX::TopoExplorer 可以实现对 TopoShape 中的指定类型( Shape , Compound , CompSolid , Solid , Shell , Face , Wire , Edge , Vertex)按照树状结构进行先序遍历,找到指定类型的子 shape。比如,遍历一个 TopoShape 的面可以用如下代码:

for (AMCAX::TopoExplorer ex(shape, AMCAX::ShapeType::Face); ex.More(); ex.Next())
{
const AMCAX::TopoShape& crshape = ex.Current();
const AMCAX::TopoFace& crface = AMCAX::TopoCast::Face(crshape);
}
static AMCAX_API const TopoFace & Face(const TopoShape &s)
将形状转换为面
用于遍历 B-Rep 结构的工具类
定义 TopoExplorer.hpp:14
AMCAX_API bool More() const
判断遍历器中是否还有形状未遍历
面类
定义 TopoFace.hpp:12
形状的基类,包含具有位置和方向信息的基础形状
定义 TopoShape.hpp:15

需要注意的是, AMCAX::TopoExplorer 不会考虑重复性(IsSame级别的“相等”),也就是说,如果一条 Edge 被两个 Face 共用,那 AMCAX::TopoExplorer 仍然会遍历这条 Edge 两次。如果不希望遍历两次,则可以使用一个 std::unordered_set 来去重,代码如下:

std::unordered_set<AMCAX::TopoShape> faceSet;
for (AMCAX::TopoExplorer ex(shape, AMCAX::ShapeType::Face); ex.More(); ex.Next())
{
const AMCAX::TopoShape& crshape = ex.Current();
if (faceSet.find(crshape) != faceSet.end())
{
continue;
}
const AMCAX::TopoFace& crface = AMCAX::TopoCast::Face(crshape);
}

在上文中,我们提到了重复性,重复性其实隐含了“相等”的定义。接下来我们将介绍关于 TopoShape 三种层次的“相等”,严格程度逐渐增加:

  • IsPartner:TopoTshape 一致
  • IsSame:TopoTshape 和 TopoLocation 一致
  • IsEqual:TopoTshape 和 TopoLocation,Orientation 一致
  • IndexSet:默认是 IsSame 级别

WireExplorer

当需要按照拓扑连通顺序遍历 Wire 中的 Edge 时(前一条边的终点 = 下一条边的起点), AMCAX::TopoExplorer 可能无法满足需求。例如,使用三个 Point3 构造三条 Edge 并组合成一个 Wire 时,若添加 Edge 的顺序与它们的实际连接顺序不一致,此时通过 TopoExplorer 遍历得到的 Edge 顺序将与实际拓扑连接顺序不符,而是按构造次序返回的。此时应使用 AMCAX::WireExplorer ,它能确保按照 Edge 的实际连接顺序进行遍历,不过遍历速度会变慢。

for (AMCAX::WireExplorer ex(wire); ex.More(); ex.Next())
{
const AMCAX::TopoEdge& edge = ex.Current();
}
边的类
定义 TopoEdge.hpp:12
环类
定义 TopoWire.hpp:12
遍历环的工具类
定义 WireExplorer.hpp:20
AMCAX_API bool More() const
判断遍历器中是否还有边未遍历

获取拓扑信息

序号

由于树状结构和先序遍历的存在,事实上每条 Edge(其他的 Vertex、Wire、Face 也是类似的)都有自己的序号。而 AMCAX::TopoExplorerTool 类提供了一些快捷的遍历操作, AMCAX::TopoExplorerTool::MapShapes 可以获得某类拓扑结构的有序号集合(如 Edge),并且得到的集合是去重的。

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Edge, edgeIndexSet);
AMCAX::TopoEdge edge1 = static_cast<const AMCAX::TopoEdge&>(edgeIndexSet[3]);
//get id
int edgeid = edgeIndexSet.index(edge1);
std::cout << edgeid << std::endl;
索引集的模板类
定义 IndexSet.hpp:20
int index(const key_type &key) const
获取给定键的索引
定义 IndexSet.hpp:198
创建立方体的类
定义 MakeBox.hpp:18
static AMCAX_API void MapShapes(const TopoShape &s, ShapeType t, IndexSet< TopoShape > &shapeSet)
构造给定类型的子形状集合
PointT< double, 3 > Point3
三维点
定义 PointT.hpp:459

事实上,这个序号和在 FreeCAD 中左下角显示的序号是一致的(FreeCAD 从 1 开始,而我们内核从 0 开始)。

所属Face

AMCAX::TopoExplorerTool::MapShapesAndAncestors 可以获取子 shape 到所属的父 shape 的 Map,而 AMCAX::TopoExplorerTool::MapShapesAndUniqueAncestors 同样可以获取 Map,两者的区别在于当输入 seam 边时,MapShapesAndUniqueAncestors 会返回 1 个所属 face,而 MapShapesAndAncestors 会返回 2 个所属 face。比如 Edge 到所属 Face 的 Map 可以这样获得:

AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(AMCAX::Frame3(AMCAX::Point3(0.0, 0.0, 0.0), AMCAX::Direction3(0.0, 0.0, 1.0)), 3.0, 5.0);
//MapShapesAndAncestors
AMCAX::TopoExplorerTool::MapShapesAndAncestors(cylinder, AMCAX::ShapeType::Edge, AMCAX::ShapeType::Face, edgeFaceMap);
//MapShapesAndUniqueAncestors
AMCAX::TopoExplorerTool::MapShapesAndUniqueAncestors(cylinder, AMCAX::ShapeType::Edge, AMCAX::ShapeType::Face, edgeFaceMap2);
索引映射的模板类
定义 IndexMap.hpp:21
创建圆柱体的类
定义 MakeCylinder.hpp:16
static AMCAX_API void MapShapesAndUniqueAncestors(const TopoShape &s, ShapeType ts, ShapeType ta, IndexMap< TopoShape, std::list< TopoShape > > &shapeMap, bool useOrientation=false)
构造给定类型的子形状到所有给定类型的唯一祖先形状的映射关系
static AMCAX_API void MapShapesAndAncestors(const TopoShape &s, ShapeType ts, ShapeType ta, IndexMap< TopoShape, std::list< TopoShape > > &shapeMap)
构造给定类型的子形状到所有给定类型的祖先形状的映射关系
DirectionT< double, 3 > Direction3
三维方向
定义 DirectionT.hpp:587
FrameT< double, 3 > Frame3
三维标架
定义 FrameT.hpp:885

退化边

判断一条 Edge 是否是退化的是非常重要的,比如我们不应当在一条退化边上进行采样。我们可以通过 AMCAX::TopoTool::Degenerated 来判断。

AMCAX::TopoShape sphere = AMCAX::MakeSphere(AMCAX::Point3(-3.0, 0.0, 0.0), 1.0);
AMCAX::TopoExplorerTool::MapShapes(sphere, AMCAX::ShapeType::Edge, edgeSet);
std::cout << AMCAX::TopoTool::Degenerated(de)<<std::endl;
创建球体的类
定义 MakeSphere.hpp:16
static AMCAX_API const TopoEdge & Edge(const TopoShape &s)
将形状转换为边
static AMCAX_API bool Degenerated(const TopoEdge &e)
获取边的退化边标志

Seam 边

可以通过 AMCAX::TopoTool::IsClosed 来判断一条 Edge 是否为 Seam 边。

AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(AMCAX::Frame3(AMCAX::Point3(0.0, 0.0, 0.0), AMCAX::Direction3(0.0, 0.0, 1.0)), 3.0, 5.0);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet2);
AMCAX::TopoEdge edge22 = AMCAX::TopoCast::Edge(edgeSet2[1]);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet2);
AMCAX::TopoFace face11 = AMCAX::TopoCast::Face(faceSet2[0]);
std::cout << AMCAX::TopoTool::IsClosed(edge22, face11) << std::endl;
static AMCAX_API bool IsClosed(const TopoShape &s)
判断形状是否封闭

几何信息提取

Point

Vertex 中只有一个 Point,表示三维空间中的点。而从 Vertex 中获取 Point 的方法为 AMCAX::TopoTool::Point

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Vertex, vertexSet);
static AMCAX_API const TopoVertex & Vertex(const TopoShape &s)
将形状转换为顶点
static AMCAX_API Point3 Point(const TopoVertex &v)
获取顶点的几何点

Curve 与 PCurve

curve3d 是 3 维参数曲线。pcurve是 2 维参数曲线。pcurve 是与 <Edge, Surface, Location> 绑定的,即 Edge, Surface, Location 是一条 pcurve 的索引。

Edge 与曲线

一个 Edge 中通常有一条 curve3d,两条 pcurve。事实上,通过 pcurve 和 surface 还可以构建一条 3 维参数曲线,但由于 Tolerance 的存在,所以创建出来的两条 3 维参数曲线与 curve3d 基本不会重合。在几何建模的过程中,Edge 中的 Curve 数量没有要求,没有 Curve 也没有问题。但除特殊情况外,最终的几何模型应当包含 1 条 curve3d 和 2 条 pcurve。

接下来我们将介绍几种特殊情况:

Geom3Plane 上的 Edge

如果 Edge 位于一个平面(Geom3Plane)上,则不需要额外的 pcurve。因为平面上的 Edge 可以直接通过 Curve3d 来表示。

Seam Edge

Seam Edge 有 1 条 curve3d 和两条 pcurve,这两条 pcurve 都在 1 个 Face 上,两条 pcurve 的区别在于 Edge 的 Orientation:一条 pcurve 对应 Edge 的正向,另一条 pcurve 对应 Edge 的反向。

Degenerated Edge

Degenerated Edge 只有 1 条 pcurve,没有 curve3d。

自由 Edge(不属于任何面的边)

自由 Edge 有 1 条 curve3d,1 条 pcurve。

需要注意的是,内核不提供为 curve3d 计算 pcurve 的功能。正确的建模方式应当是先构建 pcurve,然后用 pcurve 和 surface 计算 curve3d:

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
static AMCAX_API bool BuildCurves3d(const TopoShape &s)
为形状中的所有边构造三维曲线

获取 Edge 的 Curve

Curve3d

获取 Edge 的 Curve3d 的方法是 AMCAX::TopoTool::Curve

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Edge, edgeSet);
double fp, lp;
const std::shared_ptr<AMCAX::Geom3Curve>& curve3d = AMCAX::TopoTool::Curve(AMCAX::TopoCast::Edge(edgeSet[0]), loc3, fp, lp);
表示实体位置局部变换的类
定义 TopoLocation.hpp:14
static AMCAX_API const std::shared_ptr< Geom3Curve > & Curve(const TopoEdge &e, TopoLocation &l, double &first, double &last)
获取边的曲线及其位置和参数边界

有几点需要注意:
1.接口返回的是原始的曲线,或者说,如果一条 Edge 是一个 Geom3Line 和两个 Vertex 构成的线段,那么这个接口获取到的是一条无限长的直线(Geom3Line),fp 和 lp 是两个 Vertex 在曲线上的参数。

2.AMCAX::TopoTool::Curve 有重载函数,一个输入 loc 而另一个没有。输入 loc 的接口得到的一定是指向 edge 中曲线内存的一个指针,而没输入 loc 的接口,在 edge 的 location 为空时,返回指向内存的指针,非空时返回指向一条新构建曲线的指针。

3.如果输入的是一个退化边,会返回空指针。

4.输入 loc 的接口获得的 Curve 和实际的位置差了一个 location,而对 Curve 直接应用 location.Transformation() 是禁止的,这会改变 edge 的内容。正确的做法是,复制一份 Curve 再对 CopyCurve 进行变换:

std::shared_ptr<AMCAX::Geom3Curve> CopyCurve = std::dynamic_pointer_cast<AMCAX::Geom3Curve>(curve3d->Copy());
CopyCurve->Transform(loc3.Transformation());// apply the location to the copy curve
AMCAX_API const Transformation3 & Transformation() const
获取复合变换

pcurve

获取 pcurve 的方法是 AMCAX::TopoTool::CurveOnSurface

AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(AMCAX::Frame3(AMCAX::Point3(0.0, 0.0, 0.0), AMCAX::Direction3(0.0, 0.0, 1.0)), 3.0, 5.0);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet2);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet2);
double fp2=0., lp2=0.;
std::shared_ptr<AMCAX::Geom2Curve> pcurve = AMCAX::TopoTool::CurveOnSurface(AMCAX::TopoCast::Edge(edgeSet2[0]), AMCAX::TopoCast::Face(faceSet2[0]), fp2, lp2);
创建二维边的类
定义 MakeEdge2d.hpp:22
static AMCAX_API bool Write(const TopoShape &s, std::ostream &os, int format=3)
将形状写入输出流
static AMCAX_API std::shared_ptr< Geom2Curve > CurveOnSurface(const TopoEdge &e, const TopoFace &f, double &first, double &last, const std::shared_ptr< bool > &isStored=nullptr)
获取指定面上某条边的参数曲线及其参数边界

需要注意的是,如果输入的是 seam 边,需要特殊处理,根据 Edge 的 Orientation 会返回不同的 pcurve:

AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(AMCAX::Frame3(AMCAX::Point3(0.0, 0.0, 0.0), AMCAX::Direction3(0.0, 0.0, 1.0)), 3.0, 5.0);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet2);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet2);
double fp3=0., lp3=0.;
std::shared_ptr<AMCAX::Geom2Curve> pcurve2 = AMCAX::TopoTool::CurveOnSurface(AMCAX::TopoCast::Edge(edgeSet2[1]), AMCAX::TopoCast::Face(faceSet2[0]), fp3, lp3);
std::cout << pcurve2->Value(fp3) << ";" << pcurve2->Value(lp3) << std::endl;
double fp4 = 0., lp4 = 0.;
AMCAX::OrientationType reverseOrientation = AMCAX::TopoTool::Complement(AMCAX::TopoCast::Edge(edgeSet2[1]).Orientation());
AMCAX::TopoEdge reversedEdge = AMCAX::TopoCast::Edge(edgeSet2[1].Oriented(reverseOrientation));
std::shared_ptr<AMCAX::Geom2Curve> pcurve3 = AMCAX::TopoTool::CurveOnSurface(reversedEdge, AMCAX::TopoCast::Face(faceSet2[0]), fp4, lp4);
std::cout << pcurve3->Value(fp4) << ";" << pcurve3->Value(lp4) << std::endl;
static AMCAX_API OrientationType Complement(const OrientationType &o)
获取方向的补方向
OrientationType
方向类型
定义 TopologyMacros.hpp:11

在上面的示例中,获取了圆柱 seam 边的两条 pcurve ,这两条 pcurve 的朝向始终一致,与 curve3d 的朝向一致,这不需要考虑 edge 的朝向。

Surface

一个 Face 中只有一个 Surface。获取 Surface 的方式是 AMCAX::TopoTool::Surface

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Face, faceSet);
std::shared_ptr< AMCAX::Geom3Surface > surface1 = AMCAX::TopoTool::Surface(AMCAX::TopoCast::Face(faceSet[0]), loc);
std::shared_ptr< AMCAX::Geom3Surface > surface2 = AMCAX::TopoTool::Surface(AMCAX::TopoCast::Face(faceSet[0]));
static AMCAX_API const std::shared_ptr< Geom3Surface > & Surface(const TopoFace &f, TopoLocation &l)
获取面的曲面及曲面的位置

输入 loc 的接口获得的 Surface 和实际的位置差了一个 location,而对 Surface 直接应用 location.Transformation() 是禁止的,这会改变 face 的内容。正确的做法是,复制一份 Surface 再对 CopySurface 进行变换:

std::shared_ptr<AMCAX::Geom3Surface> CopySurface = std::dynamic_pointer_cast<AMCAX::Geom3Surface>(surface1->Copy());
CopySurface->Transform(loc.Transformation());// apply the location to the copy surface
virtual AMCAX_API std::shared_ptr< Geom3Geometry > Copy() const =0
获取拷贝的几何对象

Mesh

Mesh 指的是三角网格。Mesh 在 CAD 中的存在意义是渲染。由于 OpenGL 的绘制基本单元是点、线段、三角形,因此曲线必须离散成多边形进行绘制,曲面必须离散成 Mesh 进行绘制,因此在包含图形用户界面(GUI)的应用程序中,看到的 BRep 模型通常是通过三角网格来可视化的。如下图所示。

显然,离散会带来误差,这意味着在包含 GUI 的应用程序中显示的 BRep 模型并不是模型本身,而是模型的 Mesh,二者之间的误差由离散精度,即网格的疏密程度决定,考虑到 CAD 模型的 Mesh 生成必须非常快,Mesh 的精度可想而知并不高。

由离散带来的问题是非常多的,如通过视觉判断两个事实上相同的圆是否重叠,或两个相同的球是否重叠,就会出现下图所示的情况。或者将光滑曲面渲染成出现褶皱的情况。另一类问题是网格生成失败,这会导致存在破洞。因此看到模型上有洞,需要综合考量 Mesh 的原因和模型本身的原因。

接下来我们将介绍 Mesh 的生成、获取、读写方法。

AMCAX::TopoShape box = AMCAX::MakeBox(AMCAX::Point3(-5.0, -5.0, 0.0), AMCAX::Point3(5.0, 5.0, 3.0));
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Face, faceSet);
AMCAX::BRepMeshIncrementalMesh mesher(box, 0.005, true);
const auto& mesh = AMCAX::TopoTool::Triangulation(AMCAX::TopoCast::Face(faceSet[0]), loc2);
std::cout<<AMCAX::OBJTool::WriteShape(box, "./mesh.obj")<<std::endl;
auto mesh2 = AMCAX::OBJTool::ReadFile("./mesh.obj");
网格化的类
定义 BRepMeshIncrementalMesh.hpp:16
static AMCAX_API std::shared_ptr< TriangularMesh > ReadFile(const std::string &file)
从文件读取网格
static AMCAX_API bool WriteShape(const TopoShape &shape, const std::string &file, bool divideGroup=true)
将形状中的网格写入文件
static AMCAX_API const std::shared_ptr< TriangularMesh > & Triangulation(const TopoFace &f, TopoLocation &l, unsigned char purpose=0)
在指定网格用途的情况下,获取面的三角网格及面的位置

虽然 Mesh 对于渲染而言是必要的,但不一定要在模型构建后使用内核为模型生成 Mesh ,因为包含 GUI 的软件会自行生成 Mesh,进而用于渲染。渲染中的法向是一个关键信息,如果 Mesh 不提供法向,渲染时会根据 Mesh 自动计算一个近似的法向。如果需要确保三角网格的法向和 CAD 模型的法向是一致的,则可以使用 AMCAX::MakeShapeTool::EnsureNormalConsistency()