九韶内核 1.0.0.0
载入中...
搜索中...
未找到
拓扑结构的 B-rep 表示

B-rep 结构

边界表达(Boundary Representation,简称 BRep)是目前应用最广泛的数据结构,它能够在减轻存储压力的同时,确保数据的完备性与无歧义性。 BRep 仅是一个笼统概念,具体的拓扑结构如何构建仍是一个开放性问题。本文仅讨论九韶内核所采用的 BRep 结构,即 Vertex-Edge-Wire-Face-Shell-Solid-CompSolid-Compound。

拓扑结构的定义

定义

我们先对 B-rep 结构中各级拓扑结构的定义进行说明:

  • Vertex :拓扑意义上的点,对应几何层面的 Point
  • Edge :拓扑意义上的边,对应几何层面的 Curve,以及约束该 Curve 边界的两个 Vertex
  • Wire :由若干首尾相连的 Edge 组成,相邻 Edge 共用一个 Vertex
  • Face :拓扑意义上的面,对应几何层面的 Surface,以及约束该 Surface 边界的若干 Closed Wire
  • Shell :由若干相连的 Face 组成,相邻 Face 需共用一条或多条 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 虽在三维空间中没有长度,但在参数域上具备长度,因此属于典型的 Degenerated Edge(退化边)。

Edge

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

普通 Edge

在封闭的 TopoShape(此处指 Shell、Solid)中,最常见的 Edge 通常是两个不同 Face 的邻接边。普通 Edge 分属两个不同的 Face,且长度不为 0。

Seam Edge

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

Degenerated Edge(退化边)

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

自由 Edge

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

非流形 Edge

在一个 Shape 或 Solid 中,若某条 Edge 属于两个以上的 Face,则该 Edge 称为非流形 Edge。非流形 Edge 是非法的。

拓扑遍历

TopoShape 的树状结构

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

遍历

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

TopoExplorer

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())
{
// Get the current face
const AMCAX::TopoShape& crshape = ex.Current();
// Cast shape to face
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 noexcept
判断遍历器中是否还有形状未遍历
面类
定义 TopoFace.hpp:12
形状的基类,包含具有位置和方向信息的基础形状
定义 TopoShape.hpp:15

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

std::unordered_set<AMCAX::TopoShape> faceSet;
for (AMCAX::TopoExplorer ex(shape, AMCAX::ShapeType::Face); ex.More(); ex.Next())
{
// Get the current face
const AMCAX::TopoShape& crshape = ex.Current();
if (faceSet.find(crshape) != faceSet.end())
{
continue;
}
// Cast shape to face
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())
{
// Get the current edge
const AMCAX::TopoEdge& edge = ex.Current();
}
边的类
定义 TopoEdge.hpp:12
环类
定义 TopoWire.hpp:12
遍历环的工具类
定义 WireExplorer.hpp:20
AMCAX_API bool More() const noexcept
判断遍历器中是否还有边未遍历

获取拓扑信息

序号

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

// Construct a box from two diagonal corner points
AMCAX::Point3 p1(-5.0, -5.0, 0.0);
AMCAX::Point3 p2(5.0, 5.0, 3.0);
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Edge, edgeIndexSet);
// Cast shape to edge
AMCAX::TopoEdge edge1 = AMCAX::TopoCast::Edge(edgeIndexSet[3]);
// Get id
int edgeid = edgeIndexSet.index(edge1);
std::cout << edgeid << std::endl;// 3
索引集的模板类
定义 IndexSet.hpp:20
int index(const key_type &key) const
获取给定键的索引
定义 IndexSet.hpp:213
创建立方体的类
定义 MakeBox.hpp:20
static AMCAX_API const TopoEdge & Edge(const TopoShape &s)
将形状转换为边
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 可以这样获得:

// Construct a cylinder from a local coordinate system, a radius and a height
double radius = 3.;
double height = 5.;
AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(frame, radius, height);
// MapShapesAndAncestors
AMCAX::TopoExplorerTool::MapShapesAndAncestors(cylinder , AMCAX::ShapeType::Edge, AMCAX::ShapeType::Face, edgeFaceMap1);
// MapShapesAndUniqueAncestors
AMCAX::TopoExplorerTool::MapShapesAndUniqueAncestors(cylinder, AMCAX::ShapeType::Edge, AMCAX::ShapeType::Face, edgeFaceMap2);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet);
// Cast shape to edge
std::list<AMCAX::TopoShape>& adjacentFaces1 = edgeFaceMap1[edge2];
std::list<AMCAX::TopoShape>& adjacentFaces2 = edgeFaceMap2[edge2];
std::cout << "MapShapesAndAncestors:" << adjacentFaces1.size() << std::endl;// 2
std::cout << "MapShapesAndUniqueAncestors:" << adjacentFaces2.size() << std::endl;// 1
索引映射的模板类
定义 IndexMap.hpp:21
创建圆柱体的类
定义 MakeCylinder.hpp:18
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)
构造给定类型的子形状到所有给定类型的祖先形状的映射关系
FrameT< double, 3 > Frame3
三维标架
定义 FrameT.hpp:887

退化边

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

// Construct a sphere from a center point and a radius
AMCAX::Point3 p(-3.0, 0.0, 0.0);
double radius = 1.;
AMCAX::TopoShape sphere = AMCAX::MakeSphere(p, radius);
AMCAX::TopoExplorerTool::MapShapes(sphere, AMCAX::ShapeType::Edge, edgeSet);
// Cast shape to edge
std::cout << AMCAX::TopoTool::Degenerated(de) << std::endl;// 1
创建球体的类
定义 MakeSphere.hpp:18
static AMCAX_API bool Degenerated(const TopoEdge &e)
获取边的退化边标志

Seam 边

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

// Construct a cylinder from a local coordinate system, a radius and a height
double radius = 3.;
double height = 5.;
AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(frame, radius, height);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet);
// Cast shape to edge
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet);
// Cast shape to face
std::cout << AMCAX::TopoTool::IsClosed(edge2, face1) << std::endl; //1
static AMCAX_API bool IsClosed(const TopoShape &s)
判断形状是否封闭

几何信息提取

Point

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

// Construct a box from two diagonal corner points
AMCAX::Point3 p1(-5.0, -5.0, 0.0);
AMCAX::Point3 p2(5.0, 5.0, 3.0);
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Vertex, vertexSet);
// Get the point of a vertex
static AMCAX_API const TopoVertex & Vertex(const TopoShape &s)
将形状转换为顶点
static AMCAX_API Point3 Point(const TopoVertex &v)
获取顶点的几何点

Curve 与 PCurve

curve3d 是三维参数曲线,pcurve 是二维参数曲线。pcurve 与 <Edge, Surface, Location> 三者绑定,即 Edge、Surface、Location共同构成一条 pcurve 的索引标识。

Edge 与曲线

一条 Edge 中通常包含一条 curve3d 与两条 pcurve 。从原理上,通过 pcurve 与对应的 Surface 可反向构建出一条三维参数曲线,但由于存在 Tolerance ,这种方式构建的三维参数曲线,与 Edge 中原有的 curve3d 基本不会完全重合。 在几何建模过程中,对 Edge 内包含的 Curve 数量并无强制要求,即使不含任何 Curve 也可正常操作;但除特殊场景(如退化 Edge)外,一条 Edge 应包含 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。

获取 Edge 的 Curve

Curve3d

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

// Construct a box from two diagonal corner points
AMCAX::Point3 p1(-5.0, -5.0, 0.0);
AMCAX::Point3 p2(5.0, 5.0, 3.0);
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Edge, edgeSet);
// Cast shape to edge
// Get the curve of an edge with its location and bounds
double fp, lp;
std::shared_ptr<AMCAX::Geom3Curve> curve3d = AMCAX::TopoTool::Curve(edge1, location, 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 进行变换:

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

pcurve

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

// Construct a cylinder from a local coordinate system, a radius and a height
double radius = 3.;
double height = 5.;
AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(frame, radius, height);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet);
// Cast shape to edge
// Cast shape to face
// Get the pcurve of an edge on a given face
double fp = 0., lp = 0.;
std::shared_ptr<AMCAX::Geom2Curve> pcurve = AMCAX::TopoTool::CurveOnSurface(edge1, face1, fp, lp);
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:

// Construct a cylinder from a local coordinate system, a radius and a height
double radius = 3.;
double height = 5.;
AMCAX::TopoShape cylinder = AMCAX::MakeCylinder(frame, radius, height);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Edge, edgeSet);
AMCAX::TopoExplorerTool::MapShapes(cylinder, AMCAX::ShapeType::Face, faceSet);
// Cast shape to edge
// Cast shape to face
// Get the pcurve of an edge on a given face
double fp = 0., lp = 0.;
std::shared_ptr<AMCAX::Geom2Curve> pcurve1 = AMCAX::TopoTool::CurveOnSurface(edge2, face1, fp, lp);
// Get the complement of an orientation
// Get the reversed edge
AMCAX::TopoEdge reversedEdge = AMCAX::TopoCast::Edge(edge2.Oriented(reverseOrientation));
// Get the pcurve of the reversed edge on a given face
std::shared_ptr<AMCAX::Geom2Curve> pcurve2 = AMCAX::TopoTool::CurveOnSurface(reversedEdge, face1, fp, lp);
AMCAX_API TopoShape Oriented(OrientationType orient) const noexcept
获取指定方向后的形状
AMCAX_API OrientationType Orientation() const noexcept
获取形状的方向
static AMCAX_API OrientationType Complement(const OrientationType &o) noexcept
获取方向的补方向
OrientationType
方向类型
定义 TopologyMacros.hpp:11

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

Surface

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

// Construct a box from two diagonal corner points
AMCAX::Point3 p1(-5.0, -5.0, 0.0);
AMCAX::Point3 p2(5.0, 5.0, 3.0);
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Face, faceSet);
// Cast shape to face
// Get the surface and the location of the surface from a face
std::shared_ptr< AMCAX::Geom3Surface > surface1 = AMCAX::TopoTool::Surface(face1, loc);
// Get the surface of a face
std::shared_ptr< AMCAX::Geom3Surface > surface2 = AMCAX::TopoTool::Surface(face1);
static AMCAX_API const std::shared_ptr< Geom3Surface > & Surface(const TopoFace &f, TopoLocation &l)
获取面的曲面及曲面的位置

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

// Get the copied surface
std::shared_ptr<AMCAX::Geom3Surface> CopySurface = std::dynamic_pointer_cast<AMCAX::Geom3Surface>(surface1->Copy());
// Transform the surface
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 的生成、获取、读写方法。

// Construct a box from two diagonal corner points
AMCAX::Point3 p1(-5.0, -5.0, 0.0);
AMCAX::Point3 p2(5.0, 5.0, 3.0);
AMCAX::TopoExplorerTool::MapShapes(box, AMCAX::ShapeType::Face, faceSet);
// Cast shape to face
// Construct a mesh
double linDeflection = 0.005;
AMCAX::BRepMeshIncrementalMesh mesher(box, linDeflection);
// Get the triangular mesh
auto mesh = AMCAX::TopoTool::Triangulation(face1, loc);
// Write the mesh of a shape to a file
AMCAX::OBJTool::WriteShape(box, "./mesh.obj");
// Read a mesh from a file
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()