Gmsh 源码深度解析-第 2 章 几何内核

Gmsh 源码深度解析-第 2 章 几何内核

本章目标:讲清 Gmsh 的几何层------实体继承谱系、双几何内核(自研 GEO 与 OpenCASCADE)的 数据结构与接口、解析曲面/曲线家族、布尔与局部修饰操作、拉伸与变换、嵌入/周期/复合、 从网格反推拓扑,以及几何文件读写与脚本桥。

第 1 章建立了 GModel/GEntity 的数据模型;本章深入"这些实体究竟怎么描述几何、怎么被 创建、怎么被修改"。

2.1 双内核架构

2.1.1 两个内核一句话对比

Gmsh 最独特的设计之一是同时内置两套几何内核:

  • GEO 内核(自研,"Built-in") :只支持解析几何------点、直线、圆弧、椭圆弧、样条、Bezier、 BSpline、平面、球面、圆柱面、圆锥、圆环、参数曲面。无需任何外部依赖 ,源码在 src/geo/gmsh*.cpp 与 src/geo/Geo.cpp。
  • OCC 内核:基于 OpenCASCADE 的完整 B-Rep 能力,支持任意曲面、布尔运算、倒角/圆角、 抽壳、缝合、STEP/IGES 导入。

第 1 章看到 GModel 用四个 *_Internals 句柄挂载内核(GEO_Internals/OCC_Internals/ ACIS_Internals/Parasolid_Internals),上层只面对 GEntity 抽象。脚本用 SetFactory 命令(或 Geometry 相关命令)切换当前内核,第 3 章会看到这个选择记录在全局变量 gmsh_yyfactory(src/parser/Parser.h 第 444 行),并在语法动作里分派。

2.1.2 为什么需要两个内核

一个自然的疑问:既然有了 OpenCASCADE,为什么还要维护自研 GEO 内核?反过来,既然自研内核 够用,为什么还要 OCC?答案在两者的能力/依赖权衡:

维度 GEO 内核 OCC 内核
外部依赖 无 OpenCASCADE(体积大)
曲面类型 解析曲面(球/柱/锥/环/平面/参数曲面) 任意 B-Rep(含 NURBS、修剪面)
布尔运算 不支持 完整(并/交/差/分片)
局部修饰 不支持 倒角/圆角/抽壳/去特征
STEP/IGES 不支持 支持
网格化支持 完整(Gmsh 原生参数化) 完整(投影器适配)
语义特点 简单、可预测 工业级但有时"黑箱"

结论:GEO 内核保证 Gmsh 在任何环境下都能独立跑 (这是它被大量分发的前提);OCC 内核 在有依赖时提供工业级能力。两者共享同一套网格/后处理逻辑,这就是双内核的价值。

2.1.3 双内核的语义差异

双内核带来一个必须向用户明示的问题:同一门脚本语言,两个内核可能给出不同的几何结果 。 最典型的例子是 Surface 命令。t5.geo 的注释说得很清楚:

With the built-in kernel, if all the curves are circle arcs with the same center, a spherical patch is created; otherwise transfinite interpolation is used. With the OpenCASCADE kernel, Surface can be used with an arbitrary number of boundary curves, and will fit a BSpline patch through them.

也就是说:

  • 自研内核下,Surface 由边界曲线类型决定生成"球面片"或"transfinite 插值面";
  • OCC 内核下,Surface 对任意条数的边界曲线拟合一个 BSpline 面。

另一个例子是第 3 章会看到的 Extrude:OCC 内核不支持 twist(扭转拉伸) ,语法动作里 直接报错"Twisting extrude not available with OpenCASCADE geometry kernel" (src/parser/Gmsh.y 第 3644 行)。

这类差异是双内核架构的固有代价,也是用户选择内核时必须知道的。

2.2 实体继承谱系

2.2.1 四个维度基类

text 复制代码
GEntity
├── GVertex   (0 维:点)
├── GEdge     (1 维:曲线)
├── GFace     (2 维:曲面)
└── GRegion   (3 维:体)

每个基类之下,子类按"内核来源"与"工程状态"两条正交轴划分(第 1 章 1.17 节已列清单):

基类 自研内核 CAD 内核 离散 分区/影子/重叠
GVertex gmshVertex OCCVertex discreteVertex partitionVertex
GEdge gmshEdge OCCEdge discreteEdge partitionEdge/ghostEdge
GFace gmshFace OCCFace discreteFace partitionFace/ghostFace/overlapFace
GRegion gmshRegion OCCRegion discreteRegion partitionRegion/ghostRegion

命名规律非常清晰:前缀 = 内核或状态。

2.2.2 一个子类必须实现的"作业清单"

第 1 章 1.17.4 节列出六项。这里用 gmshVertex(src/geo/gmshVertex.h) 验证:

cpp 复制代码
class gmshVertex : public GVertex {
private:
  Vertex *_v;                    // 自研内核的原生顶点对象
public:
  gmshVertex(GModel *m, Vertex *v);
  virtual GPoint point() const;              // 几何求值
  virtual double x() const; virtual double y() const; virtual double z() const;
  virtual void setPosition(GPoint &p);
  virtual GeomType geomType() const;         // 类型声明
  virtual ModelType getNativeType() const { return GmshModel; }   // 内核身份
  virtual void *getNativePtr() const { return _v; }               // 原生指针
  virtual void setPrescribedMeshSizeAtVertex(double l);
  virtual SPoint2 reparamOnFace(const GFace *gf, int) const;      // 面间参数转换
  virtual void writeGEO(FILE *fp, const std::string &meshSizeParameter = "");
  virtual void writePY(FILE *fp, const std::string &meshSizeParameter = "");
  void resetNativePtr(Vertex *v);
};

六项一一对应:point/x/y/z(几何求值)、geomType(类型)、getNativeType(内核身份)、 getNativePtr(原生指针)、reparamOnFace(参数转换)、writeGEO/writePY(回写脚本)。 另有 resetNativePtr(当内核对象被重建时重新绑定)与 setPrescribedMeshSizeAtVertex (设置尺寸约束)。

再看 gmshFace(src/geo/gmshFace.h):

cpp 复制代码
class gmshFace : public GFace {
private:
  Surface *_s;                       // 自研内核曲面
  Range<double> _parBounds[2];       // 缓存参数域(避免每次查询)
public:
  gmshFace(GModel *m, Surface *s);
  virtual Range<double> parBounds(int i) const { return _parBounds[i]; }
  using GFace::point;
  virtual GPoint point(double par1, double par2) const;             // (u,v) → 3D
  virtual GPoint closestPoint(const SPoint3 &queryPoint, const double initialGuess[2]) const;
  virtual bool containsPoint(const SPoint3 &pt) const;
  virtual double getMetricEigenvalue(const SPoint2 &);              // 度量特征值(各向异性)
  virtual SVector3 normal(const SPoint2 &param) const;
  virtual std::pair<SVector3, SVector3> firstDer(const SPoint2 &param) const;
  virtual void secondDer(const SPoint2 &, SVector3 &, SVector3 &, SVector3 &) const;
  virtual GeomType geomType() const;
  virtual void geomProperties(std::vector<int> &integers, std::vector<double> &reals) const;
  virtual bool haveParametrization();
  virtual ModelType getNativeType() const { return GmshModel; }
  virtual void *getNativePtr() const { return _s; }
  virtual SPoint2 parFromPoint(const SPoint3 &, bool onSurface = true, bool convTestXYZ = false) const;
  virtual void resetMeshAttributes();
  void resetNativePtr(Surface *s);
  bool degenerate(int dim) const;
};

比 gmshVertex 多出的是曲面的参数化契约 :parBounds、point(u,v)、normal、 firstDer、secondDer、parFromPoint、closestPoint、getMetricEigenvalue。这些正是 第 5 章参数化网格算法依赖的接口。

注意 _parBounds[2] 是缓存 :参数域在构造时算一次存起来,之后 parBounds(i) 直接返回。 这是一个小但常见的优化------参数域查询在网格化中调用极频繁。

2.2.3 曲线基类的接口

GEdge(第 1 章 1.7.2 节)提供 point(p)、position(p)、firstDer、secondDer、 curvature、isSeam。注意曲线只有一个 参数(i=0),曲面有两个(i=0,1)。这个 "参数个数 = 维度"的一致性贯穿全代码库。

2.3 自研 GEO 内核:GEO_Internals

2.3.1 容器:脚本语法的内存镜像

第 1 章提到 GEO_Internals(src/geo/GModelIO_GEO.h)的容器 与脚本语法层级一一对应。完整列出:

cpp 复制代码
class GEO_Internals {
public:
  // this will become private
  Tree_T *Points, *Curves, *EdgeLoops, *Surfaces, *SurfaceLoops, *Volumes,
         *PhysicalGroups;
  Tree_T *DelPoints, *DelCurves, *DelSurfaces, *DelVolumes;   // 待删除的
  List_T *DelPhysicalGroups;

private:
  std::multimap<int, std::vector<int>> _meshCompounds;
  int _maxPointNum, _maxLineNum, _maxLineLoopNum, _maxSurfaceNum;
  int _maxSurfaceLoopNum, _maxVolumeNum, _maxPhysicalNum;
  void _allocateAll();
  void _freeAll();
  bool _changed;                                        // 脏标记
  bool _transform(int mode, const std::vector<std::pair<int, int>> &dimTags,
                  double x, double y, double z, double dx, double dy, double dz,
                  double a, double b, double c, double d);
  bool _extrude(int mode, const std::vector<std::pair<int, int>> &inDimTags,
                double x, double y, double z, double dx, double dy, double dz,
                double ax, double ay, double az, double angle,
                std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e = 0);
  bool _addCompoundSpline(int &tag, const std::vector<int> &curveTags,
                          int numPoints, bool bspline);
public:
  GEO_Internals() { _allocateAll(); }
  ~GEO_Internals() { _freeAll(); }
  void destroy() { _freeAll(); _allocateAll(); }
  bool getChanged() const { return _changed; }
  ...
};

三个要点:

  1. 容器与语法层级一一对应 :Points ↔ Point()、Curves ↔ Line/Circle/Spline、 EdgeLoops ↔ Curve Loop、Surfaces ↔ Plane Surface/Surface、SurfaceLoops ↔ Surface Loop、 Volumes ↔ Volume、PhysicalGroups ↔ Physical。GEO 内核就是这门 DSL 语义的内存镜像, 这是理解自研内核的关键。
  2. 删除容器独立 :DelPoints/DelCurves/... 存放"待删除"的实体,而不是立即从主容器删除。 这是一种延迟删除 策略,让"删除 + 重建"这类脚本序列(如布尔替换、Delete 命令)可以 批量处理,避免边遍历边修改容器。
    • _changed 脏标记:脚本每改一次几何就置位,上层同步时据此决定是否重建 GEntity 集合。
    • _maxXxxNum 是一组按类别维护的最大编号(对应 newp/newc/... 的自动编号,第 3 章)。

2.3.2 几何构造 API(按维度)

GEO_Internals 提供了与 DSL 命令同构的方法。点与曲线(第 59--75 行):

cpp 复制代码
bool addVertex(int &tag, double x, double y, double z, double lc);
bool addVertex(int &tag, double x, double y, gmshSurface *s, double lc);   // 面上点
bool addLine(int &tag, int startTag, int endTag);
bool addLine(int &tag, const std::vector<int> &pointTags);
bool addCircleArc(int &tag, int startTag, int centerTag, int endTag,
                  double nx = 0., double ny = 0., double nz = 0.);
bool addEllipseArc(int &tag, int startTag, int centerTag, int majorTag, int endTag, ...);
bool addSpline(int &tag, const std::vector<int> &pointTags);
bool addBezier(int &tag, const std::vector<int> &pointTags);
bool addBSpline(int &tag, const std::vector<int> &pointTags,
                const std::vector<double> &seqknots = std::vector<double>());
bool addCompoundSpline(int &tag, const std::vector<int> &curveTags, int numPoints);
bool addCompoundBSpline(int &tag, const std::vector<int> &curveTags, int numPoints);

tag 是引用传递 ,这是关键约定:传入负 tag 时内核自动分配一个新 tag 并写回。它正是脚本 p = newp; 的底层实现(第 3 章会看到 newp 在词法层就索取 tag)。

环与曲面/体(第 76--85 行):

cpp 复制代码
bool addCurveLoop(int &tag, const std::vector<int> &curveTags, bool reorient = false);
bool addCurveLoops(const std::vector<int> &curveTags, std::vector<int> &curveLoopTags);
bool addPlaneSurface(int &tag, const std::vector<int> &wireTags);
bool addDiscreteSurface(int &tag);
bool addSurfaceFilling(int &tag, const std::vector<int> &wireTags, int sphereCenterTag = -1);
bool addSurfaceLoop(int &tag, const std::vector<int> &surfaceTags);
bool addVolume(int &tag, const std::vector<int> &shellTags);

注意 addCircleArc(start, center, end) 与 OCC 的 addCircleArc(start, middle, end, center=true) 参数语义不同 :自研内核用"圆心",OCC 用"弧上中点"(center=false)或"圆心" (center=true)。这是双内核又一处理语义差异。

addSurfaceFilling(..., sphereCenterTag = -1) 是 2.1.3 节"球面片"的实现:当所有边界曲线 是同心圆弧时,用 sphereCenterTag 指定球心,退化成球面片。

addDiscreteSurface 用于创建"无解析表示"的离散面(对应 STL/网格导入)。

2.3.3 变换与挤出

cpp 复制代码
bool extrude(const std::vector<std::pair<int, int>> &inDimTags, double dx, double dy, double dz,
             std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e = 0);
bool revolve(const std::vector<std::pair<int, int>> &inDimTags, double x, double y, double z,
             double ax, double ay, double az, double angle,
             std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e = 0);
bool twist(const std::vector<std::pair<int, int>> &inDimTags, double x, double y, double z,
           double dx, double dy, double dz, double ax, double ay, double az, double angle,
           std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e = 0);
bool boundaryLayer(const std::vector<std::pair<int, int>> &inDimTags,
                   std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e = 0);
bool translate/rotate/dilate/symmetry(...);
bool splitCurve(int tag, const std::vector<int> &pointTags, std::vector<int> &curveTags);
bool intersectCurvesWithSurface(const std::vector<int> &curveTags, int surfaceTag,
                                std::vector<int> &pointTags);

注意所有操作都接受 dimTags 并返回 outDimTags ------这是贯穿全代码库的 (维度, 标签) 寻址(第 1 章)。twist 正是 OCC 不支持的那个(第 2.1.3 节)。

splitCurve/intersectCurvesWithSurface 是自研内核的"几何操作":把曲线在给定点处分段、 求曲线与曲面的交点。这些是"自研内核能做的事"的代表------虽不如 OCC 全面,但覆盖常见需求。

2.3.4 GEO_Internals 的辅助方法

其余接口覆盖:setMaxTag/getMaxTag(按维度,-2 壳 -1 线框)、addCompoundSpline、 removeAllDuplicates、healGeometry、setTolerance、getBoundingBox、setCenter 等 (第 116--160 行声明)。其中 _addCompoundSpline 演示了"复合样条"的实现路径。

2.4 自研内核的 legacy 数据结构:Geo.h

2.4.1 一句重要的警告

src/geo/Geo.h 开头的注释非常重要:

cpp 复制代码
// Internal data structures and functions for the built-in CAD kernel. This is
// legacy code and should never be used directly: use GEO_Internals instead, or
// the public api.

这是遗留(legacy)代码,不应直接使用 。作者明确引导后来者走 GEO_Internals 或公开 API。 这解释了为什么自研内核有两层:底层 Geo.h 的裸数据结构(Vertex/Curve/Surface...), 上层 GEO_Internals 的封装接口。

2.4.2 Vertex 与 Curve

Vertex(第 29--65 行):

cpp 复制代码
class Vertex {
public:
  int Num;             // 编号
  int Typ;             // 类型(如 MSH_POINT / MSH_SEGM_LINE)
  double lc, u, w;     // 目标尺寸、参数、权重
  Coord Pos;           // 笛卡尔坐标
  // a model vertex is usually defined in the euclidian coordinates (Pos).
  // Yet, it can also be defined in the parametric coordinates of a surface:
  // pntOnGeometry stores the local coordinates of the vertex in the gmshSurface
  gmshSurface *geometry;
  SPoint2 pntOnGeometry;      // 若在曲面上,存其局部参数 (u,v)
  int boundaryLayerIndex;
  ...
  Vertex operator%(Vertex &autre) { /* 叉积 */ }
};

geometry + pntOnGeometry 这对字段揭示了一个重要能力:顶点可以定义在曲面的参数坐标上 ("面上点"),这样做出来的点天然落在曲面上,网格化时无需再投影。这对应脚本里的 Point { u, v } In Surface ... 写法。

Curve(第 74--103 行):

cpp 复制代码
class Curve {
public:
  int Num, Typ;
  bool degenerated;
  int Method;                 // 网格方法(如 transfinite)
  int nbPointsTransfinite;    // transfinite 点数
  int typeTransfinite;
  double coeffTransfinite;
  double l;                   // 长度
  double mat[4][4];
  Vertex *beg, *end;
  int begByTag, endByTag;
  double ubeg, uend;
  List_T *Control_Points;     // 控制点(样条)
  ExtrudeParams *Extrude;
  float *k;                   // 节点向量(BSpline 用 float 存!)
  int degre;                  // 样条阶
  CircParam Circle;           // 圆的参数(见下)
  gmshSurface *geometry;
  int ReverseMesh;
  int master;
  std::list<double> affineTransformation;
  bool degenerate() const { ... }
};

要点:

  • mat[4][4] :4×4 变换矩阵,用于参数化曲线的变换(如 Affine 命令)。

  • k 是 float* :BSpline 节点向量用 float 而非 double------一处遗留的性能/空间取舍 (现代代码多用 double)。

  • CircParam Circle :圆的参数结构(第 67--72 行):

    cpp 复制代码
    class CircParam {
    public:
      double t1, t2, f1, f2, incl;
      double invmat[3][3];
      double n[3];     // 法向
    };

    t1/t2 是角度范围、f1/f2 过渡、invmat 逆变换、n 平面法向。圆/弧被表示为"单位圆 经变换"------这是解析曲线的高效表示。

  • degenerate():把"起止点相同且是直线且无控制点"的特殊情形识别为退化(零长线段), 第 4 章的一维网格会遇到它(避免积分死循环)。

2.4.3 Surface / EdgeLoop / SurfaceLoop / Volume / PhysicalGroup

cpp 复制代码
class EdgeLoop  { public: int Num; List_T *Curves; };
class SurfaceLoop { public: int Num; List_T *Surfaces; };

class Surface {
public:
  int Num, Typ;
  int Method;
  int Recombine;
  int Recombine_Dir;          // -1 左, +1 右, ±2 交替左右
  double RecombineAngle;
  int TransfiniteSmoothing;
  List_T *Generatrices;       // 边界曲线(拓扑)
  List_T *GeneratricesByTag;
  List_T *TrsfPoints;         // 变换点
  Vertex *InSphereCenter;     // 球心(球面片)
  ExtrudeParams *Extrude;
  // A surface is defined topologically by its Generatrices i.e. curves that
  // are the closure of it. The geometry of the surface is defined hereafter.
  // Note that this representation should be the only one in gmsh, so parameter
  // "Type" should disappear from the class Surface.
  gmshSurface *geometry;
  int ReverseMesh, MeshAlgorithm, MeshSizeFromBoundary;
  bool degenerate() const;
};

class Volume {
public:
  int Num, Typ, Method, Recombine3D, QuadTri;
  ExtrudeParams *Extrude;
  List_T *TrsfPoints;
  List_T *Surfaces;
  List_T *SurfacesOrientations;
  List_T *SurfacesByTag;
};

class PhysicalGroup { public: int Num, Typ; List_T *Entities; };

Surface 的注释道出了一个架构洞见:

A surface is defined topologically by its Generatrices ... The geometry of the surface is defined hereafter. Note that this representation should be the only one in gmsh, so parameter "Type" should disappear from the class Surface.

即"面 = 拓扑边界(Generatrices)+ 几何表示(geometry) "应当是唯一表示,Typ 字段本应 消失。这是作者对自己遗留设计的反思------Surface 里同时有 Typ(老的类型字段)和 geometry(新的几何指针),是历史包袱。

Recombine_Dir 的注释"-1 左, +1 右, ±2 交替左右"说明重组合(生成四边形)是有方向策略的 ------这正是第 5 章四边形网格的可控性来源。

PhysicalGroup 只有 Num/Typ/Entities------物理组就是一个"编号 + 类型 + 实体列表"的简单 容器(第 1 章 GModel::_physicalNames 存它的名字)。

2.4.4 比较器与释放函数

Geo.h 末尾声明了一批 CompareVertex/CompareCurve/... 与 FreeVertex/FreeCurve/...:

cpp 复制代码
int CompareVertex(const void *a, const void *b);
...
void FreeVertex(void *a, void *b);
...

Compare* 用于 Tree_T/List_T 的排序(自研容器需要比较函数),Free* 用于析构。这种 "C 风格的比较/释放回调"是 Geo.h legacy 属性的又一体现。

2.5 解析曲面:gmshSurface 家族

2.5.1 极简抽象

自研内核的曲面抽象是 src/geo/gmshSurface.h 的 gmshSurface:

cpp 复制代码
class gmshSurface {
protected:
  static std::map<int, gmshSurface *> allGmshSurfaces;   // 全局曲面表
public:
  bool vertex_defined_on_surface;
  virtual ~gmshSurface() {}
  static void reset() { /* 删除没有顶点的曲面 */ }
  static int maxTag();
  static gmshSurface *getSurface(int tag);
  virtual Range<double> parBounds(int i) const = 0;
  enum gmshSurfaceType {
    Plane, Nurbs, Cylinder, Sphere, Cone, Torus, ParametricSurface, PolarSphere
  };
  virtual gmshSurfaceType geomType() const = 0;
  virtual SPoint3 point(double par1, double par2) const = 0;
  virtual SPoint3 point(const SPoint2 &p) const { return point(p.x(), p.y()); }
  virtual SPoint2 parFromPoint(double x, double y, double z);
  virtual SVector3 normal(const SPoint2 &param) const;
  virtual std::pair<SVector3, SVector3> firstDer(const SPoint2 &param);
  virtual double getMetricEigenvalue(const SPoint2 &);
};

它只有一个纯虚几何方法:point(u,v)(参数 → 三维点) 。其余(normal、firstDer、 parFromPoint)基类都能用通用方式(有限差分/数值求逆)提供。parBounds 也必须由子类给出 (参数域)。

2.5.2 全局曲面表与生命周期

allGmshSurfaces 是静态全局表 。为什么要它?因为曲面可能被顶点引用(Vertex::geometry), 需要按 tag 反查。reset() 的注释逻辑说明它只删除"没有任何顶点在其上的曲面"( vertex_defined_on_surface == false 的),避免删掉仍被引用的。

getMetricEigenvalue(u,v)(第 64 行)返回曲面在参数点处度量的特征值 。这是自研内核为 各向异性尺寸场提供的接口:它把参数空间的尺寸转换成物理空间的尺寸(第 4 章 CurvatureField 与各向异性度量用它)。

2.5.3 具体曲面类

gmshSphere(第 67--90 行):

cpp 复制代码
class gmshSphere : public gmshSurface {
private:
  double xc, yc, zc, r;
  gmshSphere(double _x, double _y, double _z, double _r) : xc(_x),yc(_y),zc(_z),r(_r) {}
public:
  static gmshSurface *NewSphere(int _iSphere, double _x, double _y, double _z, double _r);
  virtual Range<double> parBounds(int i) const
  {
    return i == 0 ? Range<double>(0., 2*M_PI) : Range<double>(0., M_PI);
  }
  virtual gmshSurface::gmshSurfaceType geomType() const { return gmshSurface::Sphere; }
  using gmshSurface::point;
  virtual SPoint3 point(double par1, double par2) const;
};

球面的参数域 (0,2π)×(0,π) 天然对应经纬度,point(u,v) 是标准球坐标变换。 构造函数是 private、工厂方法是 static (NewSphere)------这是"受控创建 + 全局登记"模式: 确保所有球面都被登记进 allGmshSurfaces。

同族还有 gmshPlane、gmshCylinder、gmshCone、gmshTorus、gmshNurbs、 gmshParametricSurface(由用户函数定义),以及 PolarSphere(极坐标球,极点处理不同, 避免参数奇异)。

2.5.4 二次曲面识别:gmshQuadrics

src/geo/gmshQuadrics.h 是另一组类,用于从一堆点识别二次 曲面:

cpp 复制代码
class gmshQuadric {
  std::vector<SPoint3> points;
public:
  virtual double distance(const SPoint3 &p) = 0;
  virtual std::size_t minNumPoints() = 0;
  virtual void compute(std::vector<SPoint3> &p) = 0;
  virtual std::string name() = 0;
  bool addPointIfOk(const SPoint3 &p, double d)
  {
    if(points.size() < minNumPoints()) {
      for(auto pp : points) if(pp.distance(p) < 1.e-12) return true;
      points.push_back(p);
      if(points.size() == minNumPoints()) compute(points);   // 点数够就拟合
      return true;
    }
    double dist = distance(p);
    if(dist < d) return true;
    return false;
  }
};

它的工作方式是增量拟合 :每加入一个点,若达到最小点数就 compute(拟合);之后再用 distance(p) < d 判断新点是否属于这个二次曲面。子类:

cpp 复制代码
class gmshQuadricPlane    : ... { std::size_t minNumPoints() { return 3; } ... };  // 平面:3 点
class gmshQuadricSphere   : ... { std::size_t minNumPoints() { return 4; } ... };  // 球:4 点
class gmshQuadricCylinder : ... { std::size_t minNumPoints() { return 6; } ... };  // 柱:6 点

这个能力的用途 :当 Gmsh 读入一组没有解析信息的点/网格(如 STL),它能"猜出"这些点构成 一个平面/球/柱,从而给它们赋予解析几何。这对把 STL 转换为可参数化几何(进而高质量网格化) 非常有用。compute 的具体拟合算法(平面用 mean_plane、球/柱用最小二乘)在各子类的 .cpp 里。

2.6 曲线适配与参数化

2.6.1 gmshEdge 的适配

gmshEdge(src/geo/gmshEdge.h)持有 Curve *_c,实现:

cpp 复制代码
virtual Range<double> parBounds(int i) const;
virtual GeomType geomType() const;
virtual bool haveParametrization();
virtual GPoint point(double p) const;
virtual SVector3 firstDer(double par) const;
virtual SVector3 secondDer(double par) const;
virtual ModelType getNativeType() const { return GmshModel; }
virtual void *getNativePtr() const { return _c; }
virtual std::string getAdditionalInfoString(bool multline = false);
virtual int minimumMeshSegments() const;
virtual int minimumDrawSegments() const;
virtual void resetMeshAttributes();
virtual SPoint2 reparamOnFace(const GFace *face, double epar, int dir) const;
virtual void writeGEO(FILE *fp);
virtual void discretize(double tol, std::vector<SPoint3> &dpts, std::vector<double> &ts);
virtual bool degenerate(int dim) const;
void resetNativePtr(Curve *edge, GVertex *v1, GVertex *v2);

两个值得注意的方法:

  • minimumMeshSegments() :该曲线至少 要分多少段。基类默认 1,gmshEdge 覆盖它以 处理退化/特殊情形(第 4 章 meshGEdgeProcessing 的 filterMinimumN 用它)。
  • discretize(tol, dpts, ts) :按容差 tol 离散成点列 dpts 及对应参数 ts。用于 STL 导出、显示、几何计算。不同曲线类型(直线/圆弧/样条)用不同策略。

2.6.2 参数化与方向

reparamOnFace(face, epar, dir)(曲线版)与 reparamOnFace(face, par)(点版)是相邻面之间 的参数转换 :当一条边同时属于两个面 A 和 B 时,它在 A 上的参数 u 要能转成在 B 上的参数。 这对共形网格(第 5 章)是必需的,否则公共边两侧的网格点对不上。

dir 参数表示方向(±1),因为同一条边在两个面上的走向可能相反。

2.6.3 变换与 Affine

Curve::mat[4][4] 与 GEntity::affineTransform(第 1 章)支撑 Affine 命令(第 3 章 token tAffine)。周期的仿射变换也存这里(GEntity::affineTransform 是 std::vector<double>, 长度 16 = 4×4)。

2.7 OCC 内核:OCC_Internals

2.7.1 状态与"形状↔tag"双向绑定

OCC_Internals(src/geo/GModelIO_OCC.h)与 GEO_Internals 结构对称,但内部是 OpenCASCADE 的 TopoDS_Shape。它的核心难点是:OCC 用 TopoDS_Shape 标识几何,而 Gmsh 用整数 tag 标识实体,两者必须双向映射。

它用多组映射表实现(第 61--64 行附近):

cpp 复制代码
NCollection_DataMap<TopoDS_Shape, int, TopTools_ShapeMapHasher> _wireTag, _shellTag;
NCollection_DataMap<int, TopoDS_Shape> _tagWire, _tagShell;

以及按维度分组的 _vmap(顶点)、_emap(边)、_fmap(面)、_somap(体)等 (由 _addShapeToMaps 填充,第 131 行声明)。_wireTag/_tagWire 专门处理 Wire (线框,OCC 特有、Gmsh 无对应维度)与 Shell(壳)。注释(第 201 行)区分了三种特殊维度:

cpp 复制代码
// set/get max tag of entity for each dimension (0, 1, 2, 3), as well as
// -2 for shells and -1 for wires

即 tag 空间的维度约定被扩展:-1=wire、-2=shell。

2.7.2 绑定、解绑与"模糊 tag"

绑定/解绑是 OCC 适配的核心操作,接口成组出现(第 84--100 行):

cpp 复制代码
void _bind(const TopoDS_Vertex &vertex, int tag, bool recursive = false);
void _bind(const TopoDS_Edge   &edge,   int tag, bool recursive = false);
void _bind(const TopoDS_Wire   &wire,   int tag, bool recursive = false);
void _bind(const TopoDS_Face   &face,   int tag, bool recursive = false);
void _bind(const TopoDS_Shell  &shell,  int tag, bool recursive = false);
void _bind(const TopoDS_Solid  &solid,  int tag, bool recursive = false);
void _bind(TopoDS_Shape shape, int dim, int tag, bool recursive = false);
... _unbind / _unbindWithoutChecks / _unbind() ...

recursive 表示"同时绑定/解绑其子形状"(如绑一个体时把面、边、点一起绑)。这在同步时很 关键:OCC 的一个 Solid 天然包含 Faces/Edges/Vertices,但 Gmsh 需要它们各自是独立实体。

_getFuzzyTag(dim, s)(第 79 行)是"模糊查找":

cpp 复制代码
// get tag of shape, but search for other candidates at the same location if
// the actual shape is not found
int _getFuzzyTag(int dim, const TopoDS_Shape &s);

为什么需要模糊查找? 因为 OCC 在布尔/变换后可能产生"几何上相同但拓扑对象不同"的形状 (浮点误差导致 TopoDS_Shape 的 IsSame 为假)。模糊查找用位置容差判断"是不是同一条边/ 面",从而复用已有 tag、避免重复实体。这是 OCC 适配中最"脏"但最必要的部分。

_multiBind(第 107 行)处理"一个形状可能对应多个实体"的情形,返回 outDimTags, 并用 returnHighestDimOnly/returnNewOnly 控制返回范围------它服务于"读入 STEP 后把整个模型 绑成一批实体"。

2.7.3 "待删除"与"待保留"

cpp 复制代码
// cache of <dim,tag> pairs corresponding to entities that will need to be
// removed from the model at the next synchronization
std::set<std::pair<int, int>> _toRemove;
// cache of <dim,tag> pairs corresponding to entities that should not be
// unbound during boolean operations
std::set<std::pair<int, int>> _toPreserve;

_toRemove 与 GEO_Internals 的 DelXxx 容器同义(延迟删除)。_toPreserve 是 OCC 特有的: 布尔运算中某些实体不能被解绑(例如被"保留"作为结果的某个面)。第 2.8 节会看到布尔 运算如何用它。

2.7.4 几何构造 API(OCC 版)

OCC 的构造 API 远比自研内核丰富。基础图元(第 206--281 行):

cpp 复制代码
bool addVertex(int &tag, double x, double y, double z, double meshSize = MAX_LC);
bool addLine(int &tag, int startTag, int endTag);
bool addLine(int &tag, const std::vector<int> &pointTags);
bool addCircleArc(int &tag, int startTag, int middleTag, int endTag, bool center = true);
bool addCircle(int &tag, double x, double y, double z, double r,
               double angle1, double angle2, const std::vector<double> &N = {}, const std::vector<double> &V = {});
bool addEllipseArc(int &tag, int startTag, int centerTag, int majorTag, int endTag);
bool addEllipse(int &tag, double x, double y, double z, double r1, double r2,
                double angle1, double angle2, const std::vector<double> &N = {}, const std::vector<double> &V = {});
bool addSpline(int &tag, const std::vector<int> &pointTags,
               const std::vector<SVector3> &tangents = {});
bool addBezier(int &tag, const std::vector<int> &pointTags);
bool addBSpline(int &tag, const std::vector<int> &pointTags, const int degree = -1,
                const std::vector<double> &weights = {}, const std::vector<double> &knots = {},
                const std::vector<int> &multiplicities = {});
bool addWire(int &tag, const std::vector<int> &curveTags, bool checkClosed);
bool addCurveLoop(int &tag, const std::vector<int> &curveTags);
bool addRectangle(int &tag, double x, double y, double z, double dx, double dy,
                  double roundedRadius = 0.);
bool addDisk(int &tag, double xc, double yc, double zc, double rx, double ry,
             const std::vector<double> &N = {}, const std::vector<double> &V = {});

与自研内核的差异:

  • addCircleArc(start, middle, end, center=true) :参数是"弧上中点"(center=false)或 "圆心"(center=true)------与自研内核(圆心)语义不同(2.3.2 节)。
  • addCircle/addEllipse 带 N(法向)与 V(轴):可以指定圆/椭圆所在平面的朝向。
  • addBSpline 带 degree/weights/knots/multiplicities:完整的 NURBS 控制能力 ------这是自研内核做不到的。
  • addRectangle 带 roundedRadius:直接生成带圆角的矩形(自研内核需手工搭)。
  • addWire:OCC 特有的线框概念。

曲面与体(第 237--281 行):

cpp 复制代码
bool addPlaneSurface(int &tag, const std::vector<int> &wireTags);
bool addSurfaceFilling(int &tag, int wireTag,
                       const std::vector<int> &pointTags = {}, const std::vector<int> &surfaceTags = {},
                       const std::vector<int> &surfaceContinuity = {}, const int degree = 2,
                       const int numPointsOnCurves = 15, const int numIter = 2, const bool anisotropic = false,
                       const double tol2d = 0.00001, const double tol3d = 0.0001,
                       const double tolAng = 0.01, const double tolCurv = 0.1,
                       const int maxDegree = 8, const int maxSegments = 9);
bool addBSplineFilling(int &tag, int wireTag, const std::string &type = "");
bool addBezierFilling(int &tag, int wireTag, const std::string &type = "");
bool addBSplineSurface(int &tag, const std::vector<int> &pointTags, const int numPointsU,
                       const int degreeU, const int degreeV, const std::vector<double> &weights,
                       const std::vector<double> &knotsU, const std::vector<double> &knotsV,
                       const std::vector<int> &multiplicitiesU, const std::vector<int> &multiplicitiesV,
                       const std::vector<int> &wireTags = {}, bool wire3D = true);
bool addTrimmedSurface(int &tag, int surfaceTag, const std::vector<int> &wireTags, bool wire3D);
bool addSurfaceLoop(int &tag, const std::vector<int> &surfaceTags, bool sewing);
bool addVolume(int &tag, const std::vector<int> &shellTags);

addSurfaceFilling 是 OCC 曲面构造的"瑞士军刀" :它接受任意边界线框、可选参考点/ 参考曲面及其连续性,用一串容差(tol2d/tol3d/tolAng/tolCurv)、最大阶数(maxDegree)、 最大段数(maxSegments)拟合出曲面。这正是 2.1.3 节"Surface 在 OCC 下拟合 BSpline 面"的 实现。

addThruSections (放样,第 284 行)与 addThickSolid(抽壳,第 290 行):

cpp 复制代码
bool addThruSections(int tag, const std::vector<int> &wireTags, bool makeSolid, bool makeRuled,
                     std::vector<std::pair<int, int>> &outDimTags,
                     int maxDegree = -1, const std::string &continuity = "",
                     const std::string &parametrization = "", bool smoothing = false);
bool addThickSolid(int tag, int solidTag, const std::vector<int> &excludeFaceTags,
                   double offset, std::vector<std::pair<int, int>> &outDimTags);

addThruSections 用一族线框放样成面或体(对应脚本 ThruSections);addThickSolid 把实体 抽壳(移去某些面并偏移,对应脚本 ThickSolid)。注意它们返回 outDimTags 而非单个 tag ------因为它们可能产生多个实体。

基本体(第 268--281 行):

cpp 复制代码
bool addSphere(int &tag, double xc, double yc, double zc, double radius,
               double angle1, double angle2, double angle3);
bool addBox(int &tag, double x, double y, double z, double dx, double dy, double dz);
bool addCylinder(int &tag, double x, double y, double z, double dx, double dy, double dz, double r, double angle);
bool addCone(int &tag, double x, double y, double z, double dx, double dy, double dz, double r1, double r2, double angle);
bool addWedge(int &tag, double x, double y, double z, double dx, double dy, double dz, double ltx, ...);
bool addTorus(int &tag, double x, double y, double z, double r1, double r2, double angle, ...);

对应脚本命令 Sphere/Box/Cylinder/Cone/Wedge/Torus(第 3 章 token 表 tSphere/ tBox/tCylinder/tCone/tWedge/tTorus)。这些是 OCC 独有的"参数化基本体" ------自研 内核没有 Box 这类高层命令(虽然 tBox token 存在,但在自研内核里通常由多条命令拼出)。

2.7.5 局部修饰:倒角、圆角、抽壳、去特征

第 307--339 行是一组 OCC 独有的"局部修饰"操作:

cpp 复制代码
bool fillet(const std::vector<int> &volumeTags, const std::vector<int> &curveTags,
            const std::vector<double> &radii, std::vector<std::pair<int, int>> &outDimTags,
            bool removeVolume);
bool chamfer(const std::vector<int> &volumeTags, const std::vector<int> &curveTags,
             const std::vector<int> &surfaceTags, const std::vector<double> &distances,
             std::vector<std::pair<int, int>> &outDimTags, bool removeVolume);
bool defeature(const std::vector<int> &volumeTags, const std::vector<int> &surfaceTags,
               std::vector<std::pair<int, int>> &outDimTags, bool removeVolume);
bool fillet2D(int &tag, const int edgeTag1, const int edgeTag2, double radius,
              int pointTag = -1, bool reverse = false);
bool chamfer2D(int &tag, const int edgeTag1, const int edgeTag2, double distance1, double distance2);
bool offsetCurve(const int curveLoopTag, double offset,
                 std::vector<std::pair<int, int>> &outDimTags);
  • fillet(圆角):在指定曲线上以给定半径倒圆角。
  • chamfer(倒角):在指定曲线/面上倒平角。
  • defeature(去特征):移除被指定面覆盖的小特征(几何简化)。
  • fillet2D/chamfer2D:二维(平面内)的圆角/倒角。
  • offsetCurve(偏移曲线):把一个曲线环向外/向内偏移,生成新的曲线环。

removeVolume 参数表示"是否删除输入体"(对应于脚本里 {...; Delete;} 子句,第 3 章)。

2.7.6 修复(Healing)

cpp 复制代码
// apply various healing algorithms to try to fix the shape
void _healShape(TopoDS_Shape &myshape, double tolerance, bool fixDegenerated,
                bool fixSmallEdges, bool fixSmallFaces, bool sewFaces,
                bool makeSolids, double scaling = 0.0);

_healShape 是 OCC 修复的入口,fixDegenerated(退化边)、fixSmallEdges(小边)、 fixSmallFaces(小面)、sewFaces(缝合)、makeSolids(把壳拼成体)是一组修复开关, 对应脚本命令 HealShapes(第 3 章 token tHealShapes)。这是处理"不完美 CAD 导入"的 关键工具,与第 1 章"not yet non-manifold B-Rep"的哲学契合。

2.7.7 变换与拉伸的实现

cpp 复制代码
bool _transform(const std::vector<std::pair<int, int>> &inDimTags,
                BRepBuilderAPI_Transform *tfo, BRepBuilderAPI_GTransform *gtfo);

两个 OCC 变换器:BRepBuilderAPI_Transform(刚体 + 均匀缩放)与 BRepBuilderAPI_GTransform(一般仿射,含非均匀缩放)。Gmsh 同时准备两者,因为不均匀缩放 (如椭圆化)必须用 GTransform。

拉伸与扫掠(第 152--162 行):

cpp 复制代码
bool _extrudePerDim(int mode, int inDim, const std::vector<int> &inTags,
                    double x, double y, double z, double dx, double dy, double dz,
                    double ax, double ay, double az, double angle, int wireTag,
                    std::vector<std::pair<int, int>> &outDimTags, ExtrudeParams *e,
                    const std::string &trihedron);
bool _extrude(int mode, const std::vector<std::pair<int, int>> &inDimTags, ..., const std::string &trihedron = "");

trihedron(伴随坐标系)参数支持沿路径扫掠 (addPipe,第 303 行):把一个截面沿一条 导引线扫成体,同时用 trihedron 控制截面的朝向旋转(Frenet/DiscreteTrihedron 等)。 这是 OCC 的高级拉伸能力。

_setExtrudedAttributes/_copyExtrudedAttributes(第 172--180 行)把 OCC 的拉伸结果与 Gmsh 的 ExtrudeParams 绑定,从而让第 4 章的"拉伸体结构化网格"能识别它们。

2.7.8 包围盒与 STL

cpp 复制代码
bool _getBoundingBox(const TopoDS_Shape &s, double &xmin, double &ymin, double &zmin,
                     double &xmax, double &ymax, double &zmax);
bool _makeSTL(const TopoDS_Shape &s, std::vector<SPoint3> &vertices,
              std::vector<SVector3> &normals, std::vector<int> &triangles);

_makeSTL 用 OCC 的三角化器把形状转成 STL(顶点 + 法向 + 三角形)。这是"OCC 形状 → Gmsh 网格/离散面"的桥梁,也是 STL 导出的 OCC 路径。

2.8 布尔运算

2.8.1 统一入口与操作枚举

布尔运算在 OCC_Internals 里有一个统一入口 booleanOperator(第 342 行):

cpp 复制代码
bool booleanOperator(int tag, BooleanOperator op,
                     const std::vector<std::pair<int, int>> &objectDimTags,
                     const std::vector<std::pair<int, int>> &toolDimTags,
                     std::vector<std::pair<int, int>> &outDimTags,
                     std::vector<std::vector<std::pair<int, int>>> &outDimTagsMap,
                     bool removeObject, bool removeTool);

BooleanOperator 是枚举(定义在 GModelIO_OCC.h 或相关头),包含 union/intersection/ difference/fragments/section。所有布尔操作都走同一个入口,靠 op 区分 ------这样结果处理、 tag 分配、_toPreserve 逻辑只写一份。

便捷封装(第 349--360 行起):

cpp 复制代码
bool booleanUnion(int tag, const std::vector<std::pair<int, int>> &objectDimTags,
                  const std::vector<std::pair<int, int>> &toolDimTags,
                  std::vector<std::pair<int, int>> &outDimTags,
                  std::vector<std::vector<std::pair<int, int>>> &outDimTagsMap,
                  bool removeObject, bool removeTool);
bool booleanIntersection(...);   // 交
// difference / fragments / section 同理

2.8.2 outDimTagsMap 的语义

注意 outDimTagsMap 是 vector<vector<(dim,tag)>>------二维结构。它的语义是:

  • 外层:每个输入实体产生的结果组;
  • 内层:该输入实体对应到的一批输出实体。

为什么需要它? 因为布尔运算会把输入实体"切割"成多个碎片,且不同输入碎片的归属需要追溯。 例如 BooleanFragments 把 A 和 B 交叠区域切成若干块,outDimTagsMap 告诉调用者"A 的哪部分 变成了哪些输出实体"。这是 Gmsh 能"保留布尔前后的实体对应关系"的关键,也是共形网格化 (第 6 章)所需要的。

2.8.3 对象的删除与保留

removeObject/removeTool(对应脚本 {...; Delete;} 的两种位置)决定是否删除输入。 配合第 2.7.3 节的 _toPreserve,OCC 能精确控制"哪些输入实体在布尔后被删、哪些保留"。

2.8.4 布尔在脚本中的暴露

第 3 章会看到脚本命令 BooleanUnion/BooleanIntersection/BooleanDifference/BooleanSection/ BooleanFragments/ThickSolid(token tBooleanUnion 等)。词法层还有别名(BooleanFuse → tBooleanUnion、BooleanCommon → tBooleanIntersection、BooleanCut → tBooleanDifference), 方便熟悉 OCC 术语的用户。

BooleanFragments 的工程意义:它不删除任何输入,只把重叠区域"碎片化"为互不重叠的子实体 并保留全部碎片。这是把 CAD 装配转成"共形网格"的关键步骤------两个零件交界处必须有共享面才能 生成共形网格。

2.9 OCC 实体的适配

OCC 形状被适配成 OCCVertex/OCCEdge/OCCFace/OCCRegion,实现第 2.2.2 节的"作业清单"。 第 1 章 2.3 节已讲 OCCFace 的关键细节,这里补充完整。

OCCFace(src/geo/OCCFace.h)持有三种表示:

cpp 复制代码
TopoDS_Face _s;                     // 拓扑面
Handle(Geom_Surface) _occface;      // 几何曲面
const BRepAdaptor_Surface _sf;      // 面 → 参数化适配器(可求值)
double _umin, _umax, _vmin, _vmax;  // 参数域
bool _periodic[2]; double _period[2];
double _radius; SPoint3 _center;    // 若为球/柱,缓存半径与中心
mutable std::vector<GeomAPI_ProjectPointOnSurf *> _projectors;     // 按线程的投影器
mutable std::vector<Handle(ShapeAnalysis_Surface)> _localProjectors;
double _projectorBounds[4];
double _tolerance;

三个要点:

  1. _setup()(第 31 行)在构造时提取参数域并探测周期性------这是"OCC 圆柱面被当作周期 曲面参数化网格"的实现。

  2. 投影器按线程惰性创建 (第 34--38 行注释):

    sql 复制代码
    // both projectors keep state from one projection to the next, so they cannot
    // be shared between threads: keep one of each per thread, created on demand

    OCC 的 GeomAPI_ProjectPointOnSurf 会保留状态、非线程安全,因此 _projectors[threadId] 每线程一份,用 mutable 允许在 const 方法里创建。

  3. 区分全局/局部投影器 :ShapeAnalysis_Surface 局部投影器在有初值猜测时"much faster than the global search ... especially on B-splines"(第 37--38 行注释)。曲面网格生成时每个新点 都有相邻点的参数作为初值(第 5 章),因此局部投影器是热路径。

OCCFace 的几何契约方法(第 53--70 行)返回 OCCRegion 的 getNativePtr() 为 &_s (TopoDS_Face*),getNativeType() 返回 OpenCascadeModel。

2.10 拉伸与变换:ExtrudeParams

第 1 章提到 ExtrudeParams 同时描述几何变换与网格分层。完整结构 (src/geo/ExtrudeParams.h):

cpp 复制代码
// geo.Mode
#define EXTRUDED_ENTITY 1
#define COPIED_ENTITY   2
// geo.Type
#define TRANSLATE        1
#define ROTATE           2
#define TRANSLATE_ROTATE 3
#define BOUNDARY_LAYER   4

class ExtrudeParams {
public:
  ExtrudeParams(int Mode = EXTRUDED_ENTITY);
  void fill(int type, double T0, double T1, double T2, double A0, double A1, double A2,
            double X0, double X1, double X2, double angle);
  double u(int iLayer, int iElemLayer);                     // 第 iLayer 层的归一化位置
  void Extrude(int iLayer, int iElemLayer, double &dx, double &dy, double &dz);
  void Extrude(double t, double &x, double &y, double &z);   // 按参数求点
  void GetAffineTransform(std::vector<double> &tfo);
  struct {
    bool ExtrudeMesh;         // ★ 是否同时挤出网格
    bool Recombine;
    int QuadToTri;
    int NbLayer;                        // 层数
    std::vector<int> NbElmLayer;        // 每层单元数
    std::vector<double> hLayer;         // 每层厚度(归一化)
    bool ScaleLast;                     // 最后一层是否按源网格尺寸缩放
    std::map<int, std::pair<double, std::vector<int>>> Holes;   // 孔洞
    int ViewIndex, BoundaryLayerIndex;
  } mesh;
  struct {
    int Mode;                 // EXTRUDED_ENTITY / COPIED_ENTITY
    int Type;                 // TRANSLATE / ROTATE / TRANSLATE_ROTATE / BOUNDARY_LAYER
    int Source;               // 源实体
    double trans[3];
    double axe[3], pt[3], angle;
  } geo;
  static bool calcLayerScaleFactor[2];
  static smooth_data *normals[2];            // 边界层用光滑法向场
  static std::vector<SPoint3> normalsCoherence;
};

关键点:

  1. mesh/geo 两个子结构 :geo 描述"几何怎么变",mesh 描述"网格怎么长"。 挤出同时是几何操作和网格策略 ------这就是 Extrude {0,0,1} { Surface{1}; Layers{5}; Recombine; } 一次生成几何 + 结构化网格的原因。
  2. geo.Mode :EXTRUDED_ENTITY(真拉伸,生成新几何)与 COPIED_ENTITY(复制,生成 周期对应实体,第 7 章边界层与周期网格用它)。
  3. geo.Type 四种:平移、旋转、平移+旋转(螺旋/twist)、边界层。
  4. mesh.NbElmLayer/hLayer:逐层控制每层的单元数与厚度------这是结构化拉伸网格的可控性 来源,也是边界层控制的基础。
  5. ScaleLast(第 1 章已提到):最后一层是否按源网格尺寸缩放,且"按 section 局部"生效 (第 41--46 行注释解释了缩放段与非缩放段相邻时的法向平均行为)。
  6. 静态 normals/calcLayerScaleFactor:边界层挤出所需的全局光滑法向场与缩放标志。

2.11 嵌入、周期与复合

除常规几何构造外,Gmsh 支持三种特殊关系(第 3 章 token 均有对应):

2.11.1 嵌入(Embed)

Embedded 命令把低维实体"钉"到高维实体上。实现入口在语法层的 addEmbedded(dim, tags, dim2, tag2)(Gmsh.y 第 128 行声明)。落到实体上:

  • GFace::addEmbeddedVertex/addEmbeddedEdge(第 1 章 1.7.3 节);
  • GRegion::addEmbeddedVertex/Edge/Face(第 1 章 1.7.4 节)。

第 6 章会看到 边界恢复必须同时恢复"曲面 + 嵌入曲线 + 嵌入点" ------嵌入实体在三维网格层 的落地就在这里。GRegion 还提供 getEmbeddedMeshVertices()(第 86 行)取嵌入实体的网格点。

2.11.2 周期(Periodic)

Periodic 命令声明两个实体在仿射变换下周期对应。实现入口:addPeriodicEdge/ addPeriodicFace(Gmsh.y 第 123--125 行声明)。落到实体上:

  • GEdge::setMeshMaster(master, eps)(第 1 章 1.7.2 节)------边级周期;
  • GFace::setMeshMaster(master, ...) 与 edgeCounterparts(第 1 章 1.7.3 节)------面级周期;
  • GEntity::affineTransform + correspondingVertices(第 1 章)。

第 7 章 FixPeriodicMesh 会做最终校正,保证从实体网格真的与主实体周期对应。

2.11.3 复合(Compound)

Compound 把一组面/体声明为一个逻辑单元,网格化时作为一个整体(GModel::_fields 之外的 _meshCompounds 在手 GEO_Internals)。第 5 章的 Mesh2D 里有一条专门逻辑处理复合面成员 的串行网格化(第 4 章已提)。

GFace 的复合相关接口:compound(第 1 章 GEntity::compound)、getEmbeddedEdges(force) (force=false 时若面是复合成员则不返回嵌入实体,第 141--145 行注释)。

2.12 从网格反推拓扑

Gmsh 能读入纯网格文件(STL/MSH/INP...),从中重建几何拓扑 ------没有 CAD 表示,只有离散 三角形/四面体,却要恢复"点-边-面-体"的层次结构。这在 src/geo/GModelCreateTopologyFromMesh.cpp 实现。

2.12.1 拓扑完整性检查

cpp 复制代码
bool topoExists(GModel *gm)
{
  for(auto it = gm->firstEdge(); it != gm->lastEdge(); it++) {
    GEdge *ge = *it;
    if(!ge->getBeginVertex() || !ge->getEndVertex()) return false;   // 边缺端点
  }
  for(auto it = gm->firstFace(); it != gm->lastFace(); it++) {
    GFace *gf = *it;
    if(gf->edges().empty()) return false;                            // 面缺边
  }
  for(auto it = gm->firstRegion(); it != gm->lastRegion(); it++) {
    GRegion *gr = *it;
    if(gr->faces().empty()) return false;                            // 体缺面
  }
  return true;
}

topoExists 用"每个实体是否有更低一维的边界实体"判定拓扑完整性。注意它是逐维降级检查: 边要有端点、面要有边、体要有面。

2.12.2 简单连通边修复

cpp 复制代码
// FIXME: Two times the same MLine in each connected part if periodic
std::vector<GEdge *> ensureSimplyConnectedEdge(GEdge *ge)
{
  std::vector<GEdge *> _all;
  std::set<MLine *> _lines;
  std::map<MVertex *, std::pair<MLine *, MLine *>> _conn;
  _all.push_back(ge);
  // create vertex to edge connectivity: only two neighbors are considered...
  for(std::size_t i = 0; i < ge->lines.size(); i++) { ... }
  ...
}

ensureSimplyConnectedEdge 把手性/周期性导致的"同一条逻辑边被拆成多条 MLine"重新串成单条 简单连通边。它构建顶点 → 边的连接表(每个顶点最多两个邻居,即流形假设),再沿连通性 把线段串起来。

2.12.3 工程意义

这一步为什么重要? 因为大量工程数据只有网格没有 CAD:

  • 自适应(第 4 章 PostViewField)需要在几何上重新网格化------没有拓扑就没法做;
  • 分区(第 7 章)需要实体边界------没有拓扑就无法标记分区面;
  • 后处理切割、物理组标注也需要实体。

因此,"从网格倒推回拓扑"是 Gmsh 处理纯网格数据的前提能力。

2.13 几何文件读写

2.13.1 GEO 与 OCC 的原生格式

2.13.2 CAD 格式

OCC 内核支持:

  • STEP (FORMAT_STEP = 36):工业标准交换格式,带属性(作者、组织、单位------见 src/common/Options.h 里一连串 opt_geometry_occ_step_*)。
  • IGES (FORMAT_IGES = 37)。
  • BREP (FORMAT_BREP = 35):OCC 原生格式。

2.13.3 网格/离散格式

src/geo/ 下有大量 GModelIO_<FMT>.cpp:MSH/MSH2/MSH3/MSH4(原生网格)、 STL(三角面)、PLY、OBJ、OFF、VRML、X3D、UNV、INP(Abaqus)、BDF (Nastran)、MED、CGNS、MATLAB、MESH、MAIL、KEY、RAD、DIFF、IR3、 TOCHNOG、SU2、P3D、SAMCEF、CELUM、ACTRAN、NIfTI(体素图像)等。

这些文件共同说明:Gmsh 的几何层同时承担"格式枢纽"职责 ------它把各种几何/网格格式统一 成 GModel,从而能被同一套算法处理(第 1 章 FORMAT_* 清单)。

2.14 几何脚本桥

2.14.1 scriptStringInterface:字符串 → 几何

src/geo/scriptStringInterface.cpp 提供 scriptAddCommand:把外部传入的命令字符串写入临时文件、调用 gmsh_yyparse() 解析、再同步 内核状态。它让 C++ 代码(GUI、插件、API)能用最少胶水复用整门脚本语言。这是"少量命令 走 API、复杂命令走脚本字符串"两条互补路径中的后者。

2.14.2 Geo.cpp:脚本 → 内核

src/geo/Geo.cpp 的 CreateVertex/CreatePhysicalGroup(第 106--155 行) 把脚本语义直接写入 GModel::current() 的自研内核容器。它是"脚本命令 → 内核数据结构"的 落地点 ,与 GEO_Internals::addXxx 一起构成自研内核的写入路径。

2.14.3 拓扑与插值工具

2.15 GVertex 的完整实现

src/geo/GVertex.h 是最简单的实体基类,却包含几条值得记住的约定:

cpp 复制代码
class GVertex : public GEntity {
protected:
  std::vector<GEdge *> l_edges;    // 该点所属的边
  double meshSize;                 // 该点上的目标尺寸
public:
  virtual GPoint point() const { return GPoint(); }
  virtual double x() const { return 0.; }
  virtual double y() const { return 0.; }
  virtual double z() const { return 0.; }
  virtual SPoint3 xyz() const { return SPoint3(x(), y(), z()); }
  virtual void setPosition(GPoint &p);
  void addEdge(GEdge *e);
  void delEdge(GEdge *e);
  virtual std::vector<GEntity *> boundaryEntities() const { std::vector<GEntity *> e; return e; }  // 空
  virtual int dim() const { return 0; }
  virtual GeomType geomType() const { return Point; }
  virtual inline double prescribedMeshSizeAtVertex() const { return meshSize; }
  virtual void setPrescribedMeshSizeAtVertex(double l) { meshSize = l; }
  virtual SBoundingBox3d bounds(bool fast = false) { return SBoundingBox3d(SPoint3(x(), y(), z())); }
  ...
  std::vector<MPoint *> points;    // ★ 网格(点元)
};

三个要点:

  1. boundaryEntities() 返回空(第 50--54 行):点是 0 维,没有更低维边界。这是第 1 章 "逐维降级"的终点。
  2. bounds() 直接构造单点包围盒 (第 85--88 行):点的包围盒就是一个退化点。SBoundingBox3d 的单点构造。
  3. points(std::vector<MPoint*>)是网格容器 :0 维网格就是这一列 MPoint。第 4 章 Mesh0D 往这里加。

meshSize 是"该点上的目标尺寸",对应脚本 Point{i} = {..., lc} 的最后一个分量 lc,也对应 命令 Characteristic Length{ PointsOf{...} } = ...。第 4 章 Mesh0D/尺寸场会读它。

isOnSeam(gf) (第 121 行)判断"该点是否落在给定面的缝合线上"------与 GEdge::isSeam 配合, 用于周期曲面参数化。

2.16 GEdge 的完整实现

GEdge 的后半部分(src/geo/GEdge.h 第 149--279 行)包含大量网格与 参数化接口。

2.16.1 meshAttributes:边的网格属性

cpp 复制代码
struct {
  char method;                      // 网格方法(MESH_TRANSFINITE / MESH_NONE / ...)
  double coeffTransfinite;          // transfinite 系数
  double meshSize, meshSizeFactor;  // 尺寸与缩放因子
  int nbPointsTransfinite;          // transfinite 点数
  int typeTransfinite;              // transfinite 类型
  int minimumMeshSegments;          // 最少段数
  ExtrudeParams *extrude;           // 拉伸参数(若有)
  bool reverseMesh;                 // 反转网格方向
} meshAttributes;

virtual double getMeshSize() const { return meshAttributes.meshSize; }
virtual double getMeshSizeFactor() const { return meshAttributes.meshSizeFactor; }

struct { mutable GEntity::MeshGenerationStatus status; } meshStatistics;
  • method :取值如 MESH_TRANSFINITE(结构化)、MESH_NONE(不网格化)。第 4 章 meshGEdgeProcessing 里分支判断 ge->meshAttributes.method == MESH_TRANSFINITE 正是读它。
  • mutable status :meshStatistics.status 是 mutable,因为"读取状态"是 const 操作, 而状态会被流水线更新(第 4 章重试机制)。
  • reverseMesh :反转网格方向(第 4 章 orientMeshGEdge 用它)。

2.16.2 长度与参数化

cpp 复制代码
double length() const { return _length; }
void setLength(const double l) { _length = l; }
double length(const double &u0, const double &u1, const int nbQuadPoints = 4);  // 数值积分求长度
double prescribedMeshSizeAtParam(double u);                    // 参数 u 处的目标尺寸
void setMeshSizeParametric(const std::vector<double> u, const std::vector<double> lc);  // 设参数-尺寸表

virtual Range<double> parBounds(int i) const { return Range<double>(); }
virtual Range<double> parBoundsOnFace(GFace *face = nullptr) const { return parBounds(0); }
double getLowerBound() const { return parBounds(0).low(); };
double getUpperBound() const { return parBounds(0).high(); };

virtual double parFromPoint(const SPoint3 &P) const;             // 空间点 → 参数
virtual bool refineProjection(const SVector3 &Q, double &u, int MaxIter, double relax,
                              double tol, double &err) const;    // 投影细化
virtual bool XYZToU(const double X, const double Y, const double Z, double &U,
                    const double relax = 1, bool first = true) const;   // 牛顿法反求参数
virtual GPoint closestPoint(const SPoint3 &queryPoint, double &param) const;

length(u0,u1,nbQuadPoints=4) 用 4 点 Gauss 求积计算曲线弧长 ------这是第 4 章 Integration 的曲线版。XYZToU 用牛顿迭代反求参数 (从空间点求曲线参数),refineProjection 则用 带松弛的迭代细化投影。这两个函数是"把空间点投影到曲线"的核心,被网格优化、STL 生成、后处理 探针大量使用。

2.16.3 退化判定与"太小"

cpp 复制代码
void setTooSmall(bool const b) { _tooSmall = b; }
virtual bool isMeshDegenerated() const
{
  if(_tooSmall) Msg::Debug("Degenerated mesh on curve %d: too small", tag());
  if(_v0 && _v0 == _v1 && mesh_vertices.size() < 2)
    Msg::Debug("Degenerated mesh on curve %d: %d mesh nodes", tag(), (int)mesh_vertices.size());
  return _tooSmall || (_v0 && _v0 == _v1 && mesh_vertices.size() < 2);
}
virtual bool periodic(int dim) const { return _v0 == _v1; }   // 闭曲线 = 周期

两条判定:

  • 退化 :要么被标记"太小"(_tooSmall),要么是闭曲线 (_v0==_v1)但网格节点少于 2 ------后者意味着"网格化失败/退化"。第 4 章 Mesh1D 后的 CheckEmptyMesh 会报这类问题。
  • 周期 :periodic(dim) 直接用"起止点相同"判定------这是闭曲线(如整圆)的周期性质。

2.16.4 类型与导出

cpp 复制代码
virtual void getElementTypes(std::vector<int> &types) const
{ types.clear(); types.push_back(TYPE_LIN); }     // 只有线族
std::size_t getNumMeshElements() const { return lines.size(); }   // 单元数 = 线段数
virtual bool is3D() const { return true; }         // 是否三维边(非曲面上的修剪曲线)
bool storeSTLAsMesh();                             // 把 STL 三角化存为网格
virtual void writeGEO(FILE *fp);
virtual void writePY(FILE *fp);

is3D() 区分"真正的三维曲线"与"曲面上的修剪曲线(trimmed curve)"------后者其实是曲面的 一部分,参数化方式不同。storeSTLAsMesh 把离散化结果直接当网格用(离散实体专用)。

2.17 GFace 的完整实现

GFace 的后半部分(src/geo/GFace.h 第 159--308 行)是全书最"厚"的 接口之一。

2.17.1 参数化与度量

cpp 复制代码
void XYZtoUV(double X, double Y, double Z, double &U, double &V, double relax,
             bool onSurface = true, bool convTestXYZ = false) const;   // 空间 → 参数
virtual GPoint point(double par1, double par2) const;
virtual SPoint2 parFromPoint(const SPoint3 &, bool onSurface = true, bool convTestXYZ = false) const;
virtual bool containsParam(const SPoint2 &pt);
virtual GPoint closestPoint(const SPoint3 &queryPoint, const double initialGuess[2]) const;
virtual SVector3 normal(const SPoint2 &param) const;
virtual std::pair<SVector3, SVector3> firstDer(const SPoint2 &param) const;
virtual void secondDer(const SPoint2 &param, SVector3 &dudu, SVector3 &dvdv, SVector3 &dudv) const;

XYZtoUV 与曲线版的 XYZToU 对应,是"空间点 → 曲面参数"的核心(relax 是牛顿法松弛因子, convTestXYZ 控制是否用坐标收敛判据)。第 5 章参数化网格生成的一切都建立在 point/parFromPoint 之上。

度量特征值接口(第 183--193 行的注释非常关键):

cpp 复制代码
// if the mapping is a conforming mapping, i.e. a mapping that
// conserves angles, this function returns the eigenvalue of the
// metric at a given point this is a special feature for
// stereographic mappings of the sphere that is used in 2D mesh
// generation !
virtual double getMetricEigenvalue(const SPoint2 &);

// eigen values are absolute values and sorted from min to max of
// absolute values eigen vectors are the COLUMNS of eigVec
virtual void getMetricEigenVectors(const SPoint2 &param, double eigVal[2], double eigVec[4]) const;

注释点明了为什么需要度量 :球面等需要保角映射(conforming mapping) ,参数化后参数 空间的面积/长度与物理空间不同。getMetricEigenvalue 返回这个"拉伸比",让二维网格算法 在参数空间用正确的度量。getMetricEigenVectors 更给出特征向量(各向异性方向)------"eigen vectors are the COLUMNS of eigVec"是调用约定。

2.17.2 曲率

cpp 复制代码
inline double curvature(const SPoint2 &param) const { return curvatureMax(param); }
virtual double curvatureDiv(const SPoint2 &param) const;    // 法向散度
virtual double curvatureMax(const SPoint2 &param) const;    // 最大曲率
virtual double curvatures(const SPoint2 &param, SVector3 &dirMax, SVector3 &dirMin,
                          double &curvMax, double &curvMin) const;   // 主曲率与方向
  • curvatureDiv("divergence of the normal"):法向散度意义下的曲率。
  • curvatures(...):给出主曲率 curvMax/curvMin 与方向 dirMax/dirMin。第 4 章的 CurvatureField(按曲率加密)与各向异性尺寸场用它 ------大曲率方向应加密,且加密方向沿 dirMax。

2.17.3 拓扑与 STL

cpp 复制代码
virtual int genusGeom() const;                     // 曲面的亏格 G(带孔数)
virtual bool checkTopology() const { return true; }
virtual bool buildRepresentationCross(bool force = false);
virtual bool buildSTLTriangulation(bool force = false);   // 建 STL 三角化(幂等)
bool fillVertexArray(bool force = false);
bool storeSTLAsMesh();
void computeMeanPlane(const std::vector<MVertex *> &points);
void computeMeanPlane(const std::vector<SPoint3> &points);
void computeMeanPlane();                            // 用包围顶点
void getMeanPlaneData(double VX[3], double VY[3], double &x, double &y, double &z) const;
void getMeanPlaneData(double plan[3][3]) const;
  • genusGeom :曲面的亏格(环柄数)。这是拓扑信息,第 8 章的同调计算会用到。
  • buildSTLTriangulation(force) :惰性构建 STL 三角化(force=false 时已存在则不重建)。 这是"曲面 → 三角形"的通用离散,用于显示、导出、以及离散实体的网格。
  • computeMeanPlane :从点集求平均平面 (mean_plane)。第 5 章参数化、第 7 章解缠 都要先求曲面的平均平面(解缠在"近似平面的片"里做)。

GPoint(src/geo/GPoint.h)是"带参数的三维点":既有 (x,y,z) 又有 参数。point(u,v) 返回 GPoint 而不是裸 SPoint3,正是为了同时给出参数------这是 Gmsh 参数化 设计的细节。

2.17.4 网格属性与统计

cpp 复制代码
virtual void getElementTypes(std::vector<int> &types) const
{ types.clear(); types.push_back(TYPE_TRI); types.push_back(TYPE_QUA); types.push_back(TYPE_POLYG); }
std::size_t getNumMeshElements() const;              // 三角形 + 四边形 + 多边形
virtual void resetMeshAttributes();
void moveToValidRange(SPoint2 &pt) const;            // 周期面:把参数移进有效区间
void computeMeshSizeFieldAccuracy(double &avg, double &max_e, double &min_e, int &nE, int &GS);
bool fillPointCloud(double maxDist, std::vector<SPoint3> *points,
                    std::vector<SPoint2> *uvpoints = nullptr,
                    std::vector<SVector3> *normals = nullptr);
  • getElementTypes 明确列出面的三种单元族:三角形、四边形、多边形。
  • moveToValidRange (第 296--298 行注释:"for periodic faces, move parameters into the range chosen for that face"):周期面的参数可能落在 [0,2π) 之外,需要搬回有效区间。
  • computeMeshSizeFieldAccuracy :评估"实际网格尺寸"相对"目标尺寸场"的偏差------这是 网格质量诊断 的一部分,第 4 章 PrintMesh2dStatistics 报的效率指标相关。
  • fillPointCloud :把面上的点按最大间隔 maxDist 采样成点云(可选带参数与法向)------用于 距离场、背景网格采样。

GFace 还有 edgeOrientations()/setBoundEdges(第 1 章 1.7.3 节)、normalToPlanarMesh、 buildRepresentationCross(构建"十字"表示,GUI 显示用)等。

2.18 GRegion 的完整实现

GRegion(src/geo/GRegion.h)的补充接口(第 88--140 行):

cpp 复制代码
virtual std::vector<GEdge *> const &edges() const;    // 由边界面推出所有边
virtual std::vector<GVertex *> vertices() const;      // 由边推出所有点
virtual SBoundingBox3d bounds(bool fast = false);
virtual SOrientedBoundingBox getOBB();
bool edgeConnected(GRegion *r) const;                 // 是否与另一体通过边相连
double computeSolidProperties(std::vector<double> cg, std::vector<double> inertia);
virtual void writeGEO(FILE *fp);
virtual void writePY(FILE *fp);
  • edges()/vertices() 是"推导"出来的 :体不像面那样直接存边/点,而是遍历 l_faces 的 边/点去重得到。这是"只在必要的维度存直接引用"的取舍(面存边、边存点,体只存面)。
  • edgeConnected(r) :判断"两个体是否只通过一条边相连"(非通过面)。这在分区/连通性分析 里有用(第 6 章 FindConnectedRegions 用面连接,edgeConnected 是更细的关系)。
  • computeSolidProperties:计算体积、质心、惯性张量。

体的网格容器(第 1 章 1.7.4 节)是 tetrahedra/hexahedra/prisms/pyramids/polyhedra; getElementTypes 列出 TYPE_TET/PYR/PRI/HEX/TRIH/POLYH。

2.19 几何容差与一致性维护

2.19.1 容差参数

Gmsh 用几个容差控制"几何一致性":

  • GEntity::tolerance() (第 1 章 1.8.2 节)默认 1.e-14------单个实体的建模容差。
  • CTX::instance()->geom.tolerance------全局几何容差。
  • CTX::instance()->geom.matchMeshTolerance ------周期/共形网格匹配的容差(第 4 章 MatchPeriodicNodes 用它,乘包围盒对角线)。

2.19.2 顶点吸附与去重

cpp 复制代码
// GModel
void snapVertices();                                  // 按几何容差把点吸附到边/面上
void remove(GRegion *r);  // 等

snapVertices 把"离曲线/曲面很近但不精确在之上"的点吸附上去。这对 CAD 导入的"近似共面" 数据很重要。

OCC 侧的 removeAllDuplicates(在 OCC_Internals,第 2.7.6 节的 healing 系列)删除重合 形状。

2.19.3 有向包围盒

SOrientedBoundingBox(src/geo/SOrientedBoundingBox.h) 是 GEntity::getOBB() 返回的类型。与轴对齐包围盒(SBoundingBox3d)不同,OBB 是有向的 ------它按实体的主轴对齐,更紧。用途:快速碰撞/包含测试、GUI 拾取。

2.20 MSH 格式的版本演进与几何层 I/O

第 1 章 1.20.5 节给出了 MSH 读写器的参数签名。这里补充版本差异------为什么需要 MSH1/2/3/4 四套实现:

  • MSH1:最老格式。
  • MSH2:长期事实标准,DST/文本/二进制。节点与单元分开列,物理组用"单元块"表示。 局限:不结构化表达分区、高阶单元信息冗长。
  • MSH3:引入"实体块"的层次化表示,性能与可扩展性更好。
  • MSH4 :面向分区/大规模 的现代格式,有明确的实体块、节点块、分区表示。第 1 章看到 _writeMSH4 带 partitionsToSave 与 entityBounds------正是为分区导出设计的。

src/geo/GModelIO_MSH.cpp、_MSH2.cpp、_MSH3.cpp、_MSH4.cpp 分别处理它们,GModelIO_MSH.cpp 是格式探测与分派。读入时按文件头版本分派,写出时按扩展名/选项选择版本。

MSH2/MSH3 的 _ghostCells(第 1 章 1.5.3 节)注释"Use only for compatibility with mesh format msh2 and msh3"正说明:MSH4 用更结构化的方式表示影子单元。

2.21 GEO 脚本几何命令 ↔ 内核 API 对照表

把第 3 章会详细解析的脚本命令与本章内核 API 对照,能加深对"脚本 → 内核"路径的理解:

脚本命令 自研内核 API OCC 内核 API
Point(1) = {x,y,z,lc} GEO_Internals::addVertex OCC_Internals::addVertex
Line(1) = {1,2} addLine addLine
Circle(1) = {1,2,3} addCircleArc(start,center,end) addCircleArc(start,middle,end,center)
Ellipse addEllipseArc addEllipseArc
Spline / BSpline addSpline/addBSpline addSpline/addBSpline(degree,knots,...)
Bezier addBezier addBezier
Curve Loop addCurveLoop addCurveLoop
Plane Surface addPlaneSurface addPlaneSurface
Surface addSurfaceFilling(sphereCenterTag) addSurfaceFilling(容差族)
Sphere/Cylinder/Cone/Box/Torus (需多条命令拼) addSphere/addCylinder/addCone/addBox/addTorus
Volume addVolume addVolume
Extrude extrude/revolve/twist extrude/revolve(无 twist)
Boolean* 不支持 booleanUnion/Intersection/Difference/Fragments/Section
Fillet/Chamfer 不支持 fillet/chamfer
ThickSolid 不支持 addThickSolid
ThruSections 不支持 addThruSections
Transform(Translate/Rotate/Dilate/Symmetry) translate/rotate/dilate/symmetry _transform
Physical CreatePhysicalGroup(Geo.cpp) 绑定 + _multiBind
Embedded 实体 embedded* 实体 embedded*
Periodic addPeriodicEdge/Face addPeriodicEdge/Face
Coherence/HealShapes healGeometry _healShape

这张表清楚地说明:自研内核能覆盖"解析几何 + 基础拓扑 + 拉伸",OCC 内核额外提供"基本体 + 布尔 + 局部修饰 + CAD I/O"。选择内核本质上是选择这张表的右半部分要不要。

2.22 常见陷阱与实操建议

  1. 内核语义差异 :Circle 的第二个参数(圆心 vs 弧上中点)、Surface(球面片 vs BSpline)、 twist(仅自研内核)。跨内核复用脚本时要小心。
  2. tag 在变换/布尔后会变 :布尔产生新实体、删除输入(取决于 removeXxx),因此不要 在脚本里对布尔后的实体硬编码 tag,应使用返回的 outDimTags(脚本里 out[] = Boolean...;)。
  3. _getFuzzyTag 的容差 :OCC 实体的"同一位置"判定依赖容差,太大或太小都会导致"实体重复" 或"实体丢失"。调 Geometry.Tolerance 相关选项需谨慎。
  4. 周期网格的三个层次 :GEntity::affineTransform(变换)+ correspondingVertices(顶点 对应)+ edge/faceCounterparts(边/面对应)。三者要一致,否则 FixPeriodicMesh 会报 "does not match"。
  5. 嵌入实体的可见性 :getEmbeddedEdges(force=false) 在面属于复合时不返回嵌入实体 (第 2.11.3 节)------判断"是否有嵌入"要用 force=true。

2.23 离散实体:为什么"没有参数化"很重要

discreteVertex/discreteEdge/discreteFace/discreteRegion(src/geo/discreteFace.cpp 等)代表"没有任何解析表示、只有网格"的实体。它们的存在是 Gmsh "格式枢纽"定位的直接后果: 读入 STL/网格文件后,几何信息是缺失的,只能创建离散实体。

2.23.1 关键契约:haveParametrization = false

GEntity::haveParametrization() 与 isFullyDiscrete()(第 1 章 1.8.2 节)在这里第一次被"真正 返回假":

cpp 复制代码
// is the entity and its bounding entities fully discrete (i.e. without
// parametrization for curves and surfaces), and should thus not be (re)meshed?
virtual bool isFullyDiscrete() { return !haveParametrization(); }

discrete* 覆盖 haveParametrization() 返回 false。于是:

  • 第 4 章的流水线在 meshGRegion::operator() 里 if(gr->isFullyDiscrete()) return; 提前返回 ------不再重新网格化;
  • 第 5/6 章的算法不会试图对离散面/体做参数化三角化。

这就是"读入的网格不该被覆盖"这一语义的实现。用户的意图通常是"我只有网格,别动它"。

2.23.2 离散面的参数化:临时平均平面

即便没有解析表示,某些操作仍需要 (u,v)(如显示、后处理)。discreteFace 的做法是用 平均平面(mean plane)临时投影:

  • GFace::computeMeanPlane()(第 2.17.3 节)从顶点/点集求一个平均平面;
  • discreteFace 的 point(u,v)/parFromPoint 用这个平面做二维投影/反投影。

这是"没有解析曲面时用近似平面充当参数域"的实用方案。注意它是近似的 ------对弯曲离散面, (u,v) 只在一定范围内有意义。

2.23.3 STL 直接当网格

GFace::buildSTLTriangulation(force)(第 2.17.3 节)与 storeSTLAsMesh() 让"三角化结果直接 成为网格"。对离散面,这几乎是唯一合理的网格来源。

2.24 分区/影子/重叠实体

2.24.1 分区实体

partitionVertex/partitionEdge/partitionFace/partitionRegion(src/geo/partition*.h) 是第 7 章 METIS 分区的产物。它们的核心契约是 getParentEntity():

cpp 复制代码
// returns the parent entity for partitioned entities
virtual GEntity *getParentEntity() { return nullptr; }

基类返回 nullptr,分区实体返回原始(未分区)实体。于是"分区面属于哪个原始面"能 O(1) 回答。 分区实体的 physicals 也继承自父实体。

2.24.2 影子实体(ghost)

ghostEdge/ghostFace/ghostRegion 是分区交界处为邻居保存的单元副本。第 1 章提到 GModel::_ghostCells(multimap<MElement*, short>)------它把"哪些单元是影子、属于哪个分区" 记下来,仅用于 MSH2/MSH3 兼容。并行有限元通过影子单元交换边界数据。

2.24.3 重叠实体(overlap)

overlapFace/overlapRegion 对应第 1 章 GModel::_overlapManagers (std::vector<OverlapManager>)。重叠网格(overset/chimera)允许两组网格空间重叠,用于相对 运动(旋转机械的动静网格)。OverlapManager 管理一组重叠及其边界数据,多个 manager 支持 不同重叠配置共存(第 1 章 1.5.3 节注释)。

2.25 各文件格式的几何层细节

第 2.13 节列了格式清单,这里补充几种关键的"几何语义":

2.25.1 STL:只有三角面、没有拓扑

STL 是最贫信息的几何格式:只有一堆三角形,没有点/边/面的层次,也没有共享节点(虽然通常 顶点会重复出现)。因此 Gmsh 读 STL 后必须:

  1. 合并重复顶点(removeAllDuplicates/去重);
  2. 通过第 2.12 节的拓扑重建(topoExists 失败 → 重建)恢复边/面;
  3. 通常会创建 discreteFace(无解析)。

这就是第 2.12 节"从网格反推拓扑"的主要用途之一。

2.25.2 PLY / OBJ / OFF

  • PLY:可带顶点颜色/法向的三角网格。
  • OBJ:Wavefront,支持顶点/纹理/法向索引。
  • OFF:Object File Format,简单的多边形网格。

三者都比 STL 信息多(有显式索引),但同样无拓扑层次。

2.25.3 INP / BDF / UNV / MED / CGNS

这些是有限元/工程格式 ,通常既有网格又有物理组/材料信息:

  • INP(Abaqus):*NODE/*ELEMENT/*ELSET 段。
  • BDF(Nastran):GRID/CTETRA 等卡片。
  • UNV(I-deas):2411/2412 段(这也是第 1 章 MElement::getVertexUNV 的来源)。
  • MED:法国 EDF 的格式,带场数据。
  • CGNS:CFD 通用格式,带结构化/非结构网格与解。

读入时,Gmsh 会把"单元集"映射成物理组 ------这解释了为什么 Gmsh 能"读 Abaqus 的 INP 后 直接看到 ELSET 分组"。这也是 GModelIO_INP.cpp/GModelIO_BDF.cpp 里大量 switch 的原因。

2.25.4 NIfTI:体素图像 → 网格

GModelIO_NIfTI.cpp(ENABLE_NII2MESH)把三维体素图像转成三角网格。这是医学/图像分割场景 的入口------Gmsh 能直接把一张 3D 影像变成可仿真网格。

2.25.5 为什么 MSH 成为事实标准

MSH 是 Gmsh 原生格式,四套版本(MSH1/2/3/4,第 2.20 节)让它能表达:节点/单元、物理组、 分区、影子单元、高阶单元、参数坐标、额外属性。它的表达力 + 开源 + 通用性,使 MSH 成为被 众多工具读写的交换格式(第 1 章生态定位)。

2.26 几何层的性能与内存考量

几何层是"数据密集"的,其性能设计有几条可循:

2.26.1 自研容器 Tree_T / List_T vs STL 容器

Geo.h 与 GEO_Internals 用 List_T/Tree_T(src/common/ListUtils.h、 TreeUtils.h),而不是 std::vector/std::map。原因:

  • 历史:自研容器早于 C++ STL 普及;
  • C 风格接口 :List_T 用 void* 存任意类型,配 Compare*/Free* 回调------这在 legacy C 代码里方便;
  • 代价:类型不安全、无 RAII、性能未必优于现代 STL。

新代码(如 OCC_Internals、GModel)则多用 std::vector/std::set/std::unordered_map。 "新代码用 STL、旧代码用自研容器"是读 Gmsh 时要注意的分界线。

2.26.2 惰性缓存

第 1 章 1.5.3 节列了三类惰性缓存:按 tag 的顶点/单元缓存、_elementOctree、_curvatures。 它们共同的思路是"用到才算"------因为建索引/八叉树代价高,而很多操作不需要。

2.26.3 GEntity 的"职责膨胀"

GEntity 同时存几何、网格、物理组、周期对应、渲染顶点数组(va_lines/va_triangles)、 边界层单元(curvedBLElements)。这带来:

  • 优点:一处访问全部信息,无需多表查询;
  • 缺点:每个实体对象都大,即使大多数字段为空;维护复杂。

第 1 章的注释"these will become protected at some point"正是对此的自觉。

2.26.4 参数顶点的开销

MEdgeVertex/MFaceVertex 比 MVertex 多存参数(1 或 2 个 double)。对百万级网格, 这会带来可观的内存增长。但它是参数化网格、优化、投影的前提,无法省略。这是"功能换内存" 的典型取舍。

2.27 一个完整的几何建模实例

把本章串起来,追踪一个包含"基本体 + 布尔 + 拉伸"的脚本片段在各层的调用:

geo 复制代码
SetFactory("OpenCASCADE");
Box(1) = {0, 0, 0, 1, 1, 1};
Sphere(2) = {0.5, 0.5, 0.5, 0.55};
BooleanDifference(3) = { Volume{1}; Delete; }{ Volume{2}; Delete; };

2.27.1 使用 OCC 内核

SetFactory("OpenCASCADE") 把 gmsh_yyfactory 置为 "OpenCASCADE"(第 3 章)。此后所有 几何命令都走 OCC_Internals。

2.27.2 创建基本体

  • Box(1) = {...} → 语法动作调用 GModel::current()->getOCCInternals()->addBox(1, 0,0,0, 1,1,1) (第 2.7.4 节)。OCC 生成一个 TopoDS_Solid,并 _bind 到 tag 1,同时递归绑定其面/边/点。
  • Sphere(2) = {...} → addSphere(2, 0.5,0.5,0.5, 0.55, angle1, angle2, angle3)。

此时 GModel 的 regions 里出现两个 OCCRegion。

2.27.3 布尔差

BooleanDifference(3) = { Volume{1}; Delete; }{ Volume{2}; Delete; } → 语法动作调用 OCC_Internals::booleanDifference(3, {objectDimTags}, {toolDimTags}, outDimTags, outDimTagsMap, removeObject=true, removeTool=true)(第 2.8 节)。

  • OCC 执行布尔差,得到新的 TopoDS_Solid;
  • _multiBind 把结果绑到 tag 3;
  • 因为 Delete,输入体 1、2 被加入 _toRemove(延迟删除);
  • outDimTagsMap 记录"体 1/2 的各部分如何映射到输出实体"。

2.27.4 同步

脚本执行后,OCC_Internals::getChanged() 为真,Gmsh 把 OCC 形状同步成 GEntity: 新体 3 成为 OCCRegion,其子面/边/点成为 OCCFace/OCCEdge/OCCVertex;被删的体 1、2 从 GModel::regions 移除。

2.27.5 涉及的分层

text 复制代码
parser 层 : Gmsh.y 的 Boolean 语义动作、gmsh_yyfactory
geo  层 : OCC_Internals(addBox/addSphere/booleanDifference/_multiBind/_toRemove)、
          GModel(regions 集合增删)、OCCRegion/OCCFace/OCCEdge/OCCVertex
common层 : Msg(错误/信息)、CTX(geomChanged)

这条链揭示了 OCC 内核的完整生命周期命令:创建 → 绑定 → 布尔 → 延迟删除 → 同步。

2.28 实体子类职责矩阵

把本章涉及的所有子类汇总成一张职责矩阵,便于查阅"哪个类负责什么":

子类 前缀含义 getNativeType() 是否可参数化 网格容器 特殊职责
gmshVertex 自研点 GmshModel 是(点) points 存 Vertex*、reparamOnFace
gmshEdge 自研曲线 GmshModel 是 lines 存 Curve*、discretize、minimumMeshSegments
gmshFace 自研曲面 GmshModel 是 triangles/quadrangles/polygons 存 Surface*、getMetricEigenvalue
gmshRegion 自研体 GmshModel 是 tetrahedra/... computeSolidProperties
OCCVertex OCC 点 OpenCascadeModel 是 points 存 TopoDS_Vertex
OCCEdge OCC 边 OpenCascadeModel 是 lines 存 TopoDS_Edge+BRepAdaptor_Curve
OCCFace OCC 面 OpenCascadeModel 是 triangles/... 存 TopoDS_Face+BRepAdaptor_Surface、按线程投影器
OCCRegion OCC 体 OpenCascadeModel 是 tetrahedra/... 存 TopoDS_Solid
discreteVertex 离散点 GmshModel(或 Unknown) 否 points 读入网格后创建
discreteEdge 离散边 否 lines 无解析,只有线段
discreteFace 离散面 否 triangles/... 用平均平面临时参数化
discreteRegion 离散体 否 tetrahedra/... 只有体网格
partitionVertex/Edge/Face/Region 分区 同父实体 同父实体 同父实体 getParentEntity() 指向原始实体
ghostEdge/Face/Region 影子 --- --- --- 并行边界交换副本
overlapFace/Region 重叠 --- --- --- 对应 OverlapManager

这张表的核心信息是两列:"是否可参数化"与"内核来源"。前者决定"能否被重新网格化" (第 4 章),后者决定"用哪个内核的几何求值"。

2.29 GModel 与几何内核的同步机制

2.29.1 内核生命周期

GModel 提供创建/销毁各内核的接口(第 1 章 1.5.3 节):

cpp 复制代码
void createGEOInternals();
void createOCCInternals();
void createACISInternals();
void createParasolidInternals();
OCC_Internals *getOCCInternals() { return _occ_internals; }
GEO_Internals *getGEOInternals() { return _geo_internals; }
...
void deleteGEOInternals();
void deleteOCCInternals();
void resetOCCInternals();
void deleteACISInternals();
void deleteParasolidInternals();

resetOCCInternals() 是"重建一个空的 OCC 内核"------用于清空几何。这些方法成对出现,说明 内核对象是"可插拔、可替换"的:切换内核时创建新的、销毁旧的,或直接换句柄。

2.29.2 脚本→实体的同步

脚本只写内核容器(第 2.3.1 节的 Tree_T),需要一次"同步"把内核数据提升为 GEntity。 同步的核心逻辑(Gmsh.y 里调用 createGEOInternals 等 + 脚本执行后的 pending 动作)会: 遍历内核容器,为每个内核对象创建对应的 GEntity 子类,设置 tag、边界关系(l_edges/ l_dirs/l_faces)、物理组。

GModel 的保护方法(第 1 章 1.5.3 节 第 172--193 行声明)就是这套同步的工具:

cpp 复制代码
void _storeElementsInEntities(std::map<int, std::vector<MElement *>> &map);
void _storeParentsInSubElements(std::map<int, std::vector<MElement *>> &map);
void _associateEntityWithMeshVertices();
void _storeVerticesInEntities(std::map<std::size_t, MVertex *> &vertices);
void _storeVerticesInEntities(std::vector<MVertex *> &vertices);
void _storePhysicalTagsInEntities(int dim, std::map<int, std::map<int, std::string>> &map);
  • _storeElementsInEntities :把"按实体编号分组的单元"写入对应实体,需要时为纯网格数据 创建 discrete 实体(注释:"creating discrete geometrical entities on the fly if needed")。 这正是第 2.23 节离散实体的创建点。
  • _storeParentsInSubElements :把单元的子元(MSubElement)的父指针从数字 id 换成指针。
  • _associateEntityWithMeshVertices/_storeVerticesInEntities :建立"节点属于哪个实体"。 这两个与第 1 章 pruneMeshVertexAssociations 配合,用于网格拓扑大改后的重新归类。
  • _storePhysicalTagsInEntities:把物理组标签写进实体。

这套"先读当前模型、再写实体"的流程,就是"读入一个网格文件后 Gmsh 内部发生的事"。

2.29.3 实体增删与 tag 变更

cpp 复制代码
bool add(GRegion *r); bool add(GFace *f); bool add(GEdge *e); bool add(GVertex *v);
bool remove(GRegion *r); ... 
void remove(int dim, int tag, std::vector<GEntity *> &removed, bool recursive = false);
void remove(const std::vector<std::pair<int, int>> &dimTags, std::vector<GEntity *> &removed, bool recursive = false);
bool changeEntityTag(int dim, int tag, int newTag);

注释(第 478 行)指出:每次 add/remove 都会调用 CTX::geomChanged() ,从而广播 "几何变了"。changeEntityTag 因为要维护 std::set 的排序,实现上是"取出、改 tag、插回"。 remove(dimTags, removed, recursive=true) 会递归删除子实体------这正是脚本 Delete 命令的 底层。

2.29.4 实体查询

cpp 复制代码
GRegion *getRegionByTag(int n) const;
GFace *getFaceByTag(int n) const;
GEdge *getEdgeByTag(int n) const;
GVertex *getVertexByTag(int n) const;
GEntity *getEntityByTag(int dim, int n) const;
void getEntities(std::vector<GEntity *> &entities, int dim = -1) const;
void getEntitiesInBox(std::vector<GEntity *> &entities, const SBoundingBox3d &box, int dim = -1) const;
bool getBoundaryTags(const std::vector<std::pair<int, int>> &inDimTags,
                     std::vector<std::pair<int, int>> &outDimTags,
                     bool combined, bool oriented = true, bool recursive = false);

getEntityByTag(dim, n) 是 (dim,tag) 寻址的标准入口(第 1 章)。getBoundaryTags 对应 脚本 Boundary 命令(第 1 章 1.20.3 节)。

2.30 几何选项

几何行为由 CTX::instance()->geom.* 控制。几个关键的:

  • geom.tolerance:全局几何容差(点/边/面判定的基础)。
  • geom.matchMeshTolerance :周期/共形网格匹配容差(第 4 章 MatchPeriodicNodes 用 tol * bounds().diag())。
  • geom.occFastProjection :OCC 快速(局部)投影开关。默认关闭 ,但高阶节点放置时会 临时打开(第 7 章 SetOrderN 第 1874--1875 行),因为那时总有初值猜测。
  • geom.occTargetUnit :STEP 导入的目标单位(对应选项 Geometry.OCC.TargetUnit)。
  • 一连串 geom.occStep*(作者、组织、时间戳、schema 等):STEP 导出的元数据(第 1 章 Options.h 里的 opt_geometry_occ_step_*)。

这些选项让"几何层的行为"可调,而不必改代码。

2.31 与其他几何库/内核的对比

Gmsh 的几何层可以与同类方案对比,理解其定位:

方案 几何表示 布尔 依赖 Gmsh 的关系
Gmsh GEO 内核 解析曲面 无 无 自研,保证独立运行
OpenCASCADE 完整 B-Rep 完整 大 可选后端(ENABLE_OCC)
ACIS 完整 B-Rep 完整 商业 可选(需商业库)
Parasolid 完整 B-Rep 完整 商业 可选
CGAL 精确算术 有限 模板库 未直接集成
STL(作为"几何") 三角面 无 无 作为输入格式读入

Gmsh 的策略是"自研保底 + 商业/开源 CAD 增强":GEO 内核是自研的、无依赖的"保底方案"; OCC/ACIS/Parasolid 是可插拔的"增强方案"。这既保证 Gmsh 能被无条件分发,又能在有依赖时获得 工业能力。

2.32 几何数据流全景

把本章的几何层数据流画成一张图,标出三个入口如何汇聚:

text 复制代码
  .geo 脚本            API 调用             文件导入
  (Point/Line/...)     (gmsh::model::geo::) (STEP/STL/MSH/INP...)
        │                    │                     │
        │  parser(第3章)      │                     │ GModelIO_*(本章)
        ▼                    ▼                     ▼
  ┌──────────────────────────────────────────────────────────┐
  │             几何内核(二选一或并列)                        │
  │  GEO_Internals           OCC_Internals                    │
  │  Tree_T: Points/Curves/  TopoDS_Shape + _vmap/_emap/...   │
  │  EdgeLoops/Surfaces/...  + _getFuzzyTag + _toPreserve     │
  └───────────────────────────┬──────────────────────────────┘
                              │ 同步(创建 GEntity)
                              ▼
  ┌──────────────────────────────────────────────────────────┐
  │  GModel:vertices / edges / faces / regions              │
  │  每个 GEntity 子类:gmsh*/OCC*/discrete*/partition*/...    │
  │  持有 mesh_vertices + MElement 容器                       │
  └───────────────────────────┬──────────────────────────────┘
                              │ 供 mesh 层(第4-7章)消费
                              ▼
                     网格生成 / 后处理 / 求解器

读图要点 :无论几何从哪来,最终都要变成 GModel 里的 GEntity;而 GEntity 同时是 "几何"和"网格容器"。这条数据流是理解本章与后续章节的总纲。

2.33 常见问答

Q1:为什么点、线、面的 tag 可以相同? 因为它们只在各自维度内唯一 (第 1 章 1.21.1 节)。GModel 用四个独立的 std::set 按维度 存储(第 1 章 1.5.2 节),真正跨维度的标识是 (dim, tag) 对。

Q2:改了几何,为什么已有网格还在? 网格不会自动删除------它是实体属性(第 1 章)。必须显式调用 deleteMesh(),或重新网格化时 由第 4 章的"降维删除"策略清理。

Q3:OCC 布尔运算后,原来的 tag 还能用吗? 不一定。若用了 Delete 子句,输入实体会被删除;新实体获得新 tag(脚本里的 BooleanXxx(3) 指定输出 tag)。应始终用返回的 outDimTags,不要硬编码(第 2.22 节陷阱 2)。

Q4:怎么判断一个面是平面 / 球面 / 圆柱? gf->geomType()(第 1 章 GeomType 枚举)会告诉你是 Plane/Sphere/Cylinder/... gf->getNativeType() 告诉你内核对。对纯离散数据,可用 gmshQuadric 系列"从点猜"(第 2.5.4 节)。

Q5:自研 GEO 内核能做布尔吗? 不能。布尔、倒角、抽壳、STEP/IGES 都需要 OCC 内核(第 2.8 节、第 2.21 节对照表)。

Q6:什么是 seam(缝合线)? 圆柱/球面这类周期曲面参数化时需要"切开"的那条线(GEdge::isSeam,第 2.6 节)。它对应两个 参数位置但几何上是同一条线;Gmsh 必须识别它才能正确参数化周期曲面。

Q7:为什么 OCC 相关代码里有那么多 _bind/_unbind? 因为 OCC 用 TopoDS_Shape、Gmsh 用整数 tag,两者必须维护双向映射(第 2.7.2 节)。创建、 布尔、变换、删除都会改变映射,因此必须成对更新。

Q8:GEO_Internals 和 Geo.h 是什么关系? Geo.h 是 legacy 裸数据结构(Vertex/Curve/Surface...),GEO_Internals 是封装接口。 头文件注释明确"This is legacy code and should never be used directly"(第 2.4.1 节)。

2.34 几何层的容错哲学

第 1 章说 GModel 是"not yet non-manifold B-Rep"------允许暂时不干净的表示,逐步修复。 本章可以看到这套哲学的具体落地手段,汇总如下:

不干净的情形 容错/修复手段 位置
两曲面共享边但未缝合 Coherence / _healShape(sewFaces) 第 2.7.6 节
小边/小面/退化边 _healShape(fixSmallEdges/fixSmallFaces/fixDegenerated) 第 2.7.6 节
顶点不在曲线上 snapVertices() 第 2.19.2 节
重复形状 removeAllDuplicates 第 2.19.2 节
OCC 浮点导致的"同一位置不同形状" _getFuzzyTag 模糊匹配 第 2.7.2 节
壳未拼成体 _healShape(makeSolids) 第 2.7.6 节
纯网格无拓扑 topoExists + 拓扑重建 第 2.12 节
手性/周期性导致的断裂边 ensureSimplyConnectedEdge 第 2.12.2 节
曲面参数奇异(极点) getMetricEigenvalue 处理度量;退化三角过滤 第 2.17.1 节、第 5 章

核心思想:几何层不追求"一进来就完美",而是提供一组可组合的修复工具,让用户/算法按需 清理。这既让 Gmsh 能吞下各种真实数据,也把"修复到什么程度"的决定权留给用户。

2.34.1 错误处理

几何层的错误通过 Msg::Error/Msg::Warning 报告(第 1 章 1.15 节)。典型错误:

  • Msg::Error("Unknown source curve %d for boundary layer", ...)(边界层源缺失);
  • Msg::Error("Surface %d contains %zu elements which are not triangles...")(第 6 章 HXT)。

GModel 的 _lastMeshEntityError/_lastMeshVertexError(第 1 章 1.5.3 节)在网格化失败时 记录"出问题的实体/节点",供 GUI 高亮。链路是:几何层的实体信息 → 网格层的错误记录 → GUI 显示。

2.35 术语中英对照速查

英文/代码 中文 说明
elementary entity 原始实体 脚本里 Point/Line/Surface/Volume 定义的基本实体
physical group 物理组 语义分组(第 1 章)
(dim, tag) 维度+标签 跨维度标识
family type (TYPE_*) 单元族 几何形状类别
Serendipity 不完备族 无面/体内部节点
seam 缝合线 周期曲面的切开线
degenerate 退化 零长边 / 参数极点
periodic 周期 从实体复用主实体网格
compound 复合 多实体作为整体网格化
embedded 嵌入 低维实体钉在高维实体上
partition / ghost 分区 / 影子 并行分块的产物
overlap 重叠 重叠网格(chimera)
parBounds 参数域 曲线的 [u0,u1]、曲面的 [u0,u1]×[v0,v1]
reparamOnFace 面间参数转换 公共边两侧参数对应
mean plane 平均平面 近似平面,用于解缠/离散面参数化
OBB 有向包围盒 按主轴对齐的包围盒
healing 修复 OCC 的形状修复
fuzzy tag 模糊标签 按位置容差匹配形状

2.36 源码索引与延伸阅读

把本章涉及的几何层文件按功能分组,作为可快速定位的索引:

实体基类与子类:

  • GEntity.h/.cpp:实体基类(契约)
  • GVertex.h/.cpp、GEdge.h/.cpp、GFace.h/.cpp、GRegion.h/.cpp:四维基类
  • gmshVertex.h/.cpp、gmshEdge.h/.cpp、gmshFace.h/.cpp、gmshRegion.h/.cpp:自研内核适配
  • OCCVertex.h/.cpp、OCCEdge.h/.cpp、OCCFace.h/.cpp、OCCRegion.h/.cpp:OCC 适配
  • discreteVertex/Edge/Face/Region.h/.cpp:离散实体
  • partition*.h、ghost*.h、Overlap*.h:分区/影子/重叠

内核状态:

  • GModelIO_GEO.h/.cpp:GEO_Internals
  • GModelIO_OCC.h/.cpp:OCC_Internals
  • Geo.h、Geo.cpp:legacy 自研内核数据结构与脚本落地

解析几何:

  • gmshSurface.h/.cpp、gmshQuadrics.h/.cpp:自研解析曲面与二次曲面识别
  • GPoint.h、SBoundingBox3d.h、SOrientedBoundingBox.h/.cpp:几何点/包围盒
  • affineTransformation.h/.cpp:仿射变换
  • ExtrudeParams.h/.cpp:挤出参数

拓扑与插值:

  • GEdgeLoop.h/.cpp、findLinks.h/.cpp、GeoInterpolation.h/.cpp
  • intersectCurveSurface.h/.cpp、GModelCreateTopologyFromMesh.h/.cpp
  • GModelAutoExtrude.cpp、GModelParametrize.h/.cpp(重参数化,第 10 章)

网格元素(与几何层一起):

  • MElement.h/.cpp、MVertex.h/.cpp、MElementOctree.h/.cpp
  • MFace.h、MEdge.h、MFaceHash.h、MEdgeHash.h、MVertexRTree.h
  • MSubElement.h/.cpp、各 M*.h/.cpp(MTriangle/MTetrahedron/...)

文件 I/O:

  • GModelIO_MSH*.cpp(MSH/2/3/4)、GModelIO_GEO.cpp、GModelIO_OCC.cpp
  • GModelIO_STL/PLY/OBJ/OFF/VRML/X3D/UNV/INP/BDF/MED/CGNS/NIfTI/...cpp

数据流:本章第 2.32 节的全景图。

2.37 本章小结

几何层的核心:

  1. 双内核 + 统一抽象 :GEntity 定义几何契约(参数域/求点/切向/法向/投影/离散), 自研 GEO 与 OCC 是两种实现;脚本用 gmsh_yyfactory 分派,存在必须向用户明示的语义差异 (Surface、Circle 参数、twist)。
  2. GEO 内核 = 解析几何 + DSL 内存镜像 :GEO_Internals 的容器与脚本层级一一对应; gmshSurface 用极简 point(u,v) 抽象覆盖所有解析曲面;Geo.h 是明确标注的 legacy 数据结构;gmshQuadrics 能"从点猜解析曲面"。
  3. OCC 内核 = 工业 B-Rep + 适配复杂度 :核心难点是 TopoDS_Shape ↔ tag 双向绑定与 _getFuzzyTag 模糊匹配;提供布尔、倒角/圆角/抽壳/去特征、STEP/IGES、局部修饰、healing; 投影器按线程惰性创建以解决线程安全。
  4. 布尔与拉伸是几何-网格的桥梁 :booleanOperator 统一入口 + outDimTagsMap 追溯对应 关系;ExtrudeParams 的 mesh/geo 双结构让挤出同时是几何操作与网格策略。
  5. 嵌入/周期/复合 三种特殊关系,靠实体的 embedded*/*Counterparts/compound 落地, 在第 6、7 章的网格层被消费。
  6. 从网格反推拓扑 (topoExists/ensureSimplyConnectedEdge)让 Gmsh 能处理纯网格数据。
  7. 几何层同时是格式枢纽 :GModelIO_* 覆盖几十种几何/网格格式。

第 3 章转向脚本层,看这些几何命令是如何被解析器解析、求值并调用到 GEO_Internals/OCC_Internals 的。

全章回顾

如果把本章压缩成一句话:Gmsh 的几何层用一套统一的 GEntity 契约,包裹了两个能力互补的 内核(自研解析内核与 OpenCASCADE),并在两者之上叠加了布尔、拉伸、嵌入/周期/复合、拓扑重建 与几十种文件格式的读写,从而既能独立运行又能对接工业 CAD。

读本章时应记住四组"对照":

  1. 两个内核的对照(自研 vs OCC,第 2.1、2.21 节);
  2. 两种解析表示的对照 (GEO_Internals 的容器 vs TopoDS_Shape,第 2.3、2.7 节);
  3. 两种实体状态的对照(可参数化 vs 离散,第 2.23 节);
  4. 两种几何来源的对照(CAD 建模 vs 网格反推,第 2.12 节)。

这四组对照,正是理解后续"网格层如何在不同几何上生成网格"的前提:第 4 章会根据 isFullyDiscrete()/geomType()/getMeshingAlgo() 走不同路径,而这些判断全部来自本章建立的 实体契约。

关键设计决策回顾

决策 为什么这么做 代价
实体统一契约(GEntity) 网格/后处理只面对一种抽象 实体类臃肿
双内核 无依赖可跑 + 有依赖获工业能力 语义差异、适配代码多
容器镜像脚本层级(GEO_Internals) 脚本↔内核映射直接 与实体集合两份表示,需同步
延迟删除(DelXxx/_toRemove) 批量处理、避免遍历中修改 状态需显式同步
OCC 模糊 tag(_getFuzzyTag) 容忍浮点误差导致的"形状不等" 容差敏感
(dim,tag) 寻址 跨维度唯一标识 每次都要带维度
离散实体(无参数化) "读入的网格别动" 部分几何操作不可用
投影器按线程惰性创建 解决 OCC 线程安全问题 内存与实现复杂度

把这八个决策记住,当你在后续章节看到任何"奇怪"的代码时,多半能在这里找到动机。

给实现者的检查清单

如果你要为 Gmsh 增加一个新的几何内核适配,或排查几何相关问题,可按下面的顺序检查:

  1. 实体是否实现了六项作业 (第 2.2.2 节):getNativeType/getNativePtr/geomType/ point/reparamOnFace/writeGEO。
  2. tag 映射是否双向一致 (第 2.29.2 节):创建/布尔/变换/删除后,GModel 的实体集合 与内核容器是否同步。
  3. 是否处理了周期性/退化 (第 2.6/2.17 节):periodic/degenerate 是否覆盖了你的 parBounds 的特殊情形。
  4. 投影是否有初值路径 (第 2.9 节):closestPoint 在有初值时是否走快速路径。
  5. 容差是否合理 (第 2.19 节):tolerance/matchMeshTolerance 与数据类型匹配。
  6. 是否声明了离散性 (第 2.23 节):没有解析表示时应让 haveParametrization() 返回假, 否则第 4 章会尝试重新网格化它。

这份清单总结的,正是本章反复强调的"几何实体必须回答的问题"。

至此,几何层的全貌已经建立:统一的实体契约、两个内核、以及覆盖建模/布尔/拉伸/拓扑重建/I/O 的完整能力。带着这套坐标进入第 3 章------看这门脚本语言如何把用户意图翻译成对内核的调用。

几何层的验证手段

Gmsh 仓库自带的 tutorials/(t1.geo ... t21.geo)实际上是几何与网格功能的回归测试集 。 其中前几个教程(t1--t6)专门覆盖几何:t1 讲基本实体与物理组,t4 讲带孔的面,t5 讲 宏与"在立方体内挖球形孔",t6 讲后处理。对比不同内核运行同一脚本(SetFactory 切换), 就能直观看到第 2.1.3 节的语义差异。

对开发者而言,验证几何改动的标准做法是:跑一遍相关教程脚本,检查实体数量/tag/包围盒是否 符合预期;对 OCC 相关改动,还要额外验证布尔结果的 outDimTagsMap 与实体归属。这与本章 "实体契约 + tag 映射 + 容错"三大主题一一对应。

本章小结:几何层是 Gmsh 的"输入侧基础设施"。它决定了 Gmsh 能吃下什么数据、能表达什么 形状、以及这些形状以何种精度被网格化。下一章进入"输入如何被表达"------脚本语言与解析器。

本章的关键词清单,供检索时使用:GModel、GEntity、GeomType、ModelType、GEO_Internals、 OCC_Internals、Tree_T、gmshSurface、gmshQuadrics、Curve、Vertex、Surface、 ExtrudeParams、booleanOperator、outDimTagsMap、_getFuzzyTag、_toRemove、_toPreserve、 _healShape、topoExists、ensureSimplyConnectedEdge、isFullyDiscrete、parBounds、 reparamOnFace、computeMeanPlane、getMetricEigenvalue、SOrientedBoundingBox。

相关推荐
0+1112 小时前
算法 --栈
算法·leetcode
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 9 章 后处理与插件体系
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 7 章 网格质量、优化与高级技术
算法·开源
鶴哥只手遮天2 小时前
OpenFOAM 源码深度剖析 · 第二季-15 实战:手写一个求解器、库与边界条件
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 8 章 数值内核与高阶有限元
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 4 章 网格生成总控与尺寸场
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 10 章 求解器框架、API 与语言绑定
算法·开源
鶴哥只手遮天2 小时前
OpenFOAM 源码深度剖析 · 第二季-02 燃烧与化学反应:`combustionModel`、化学求解器与 Arrhenius 反应率
算法·开源
鶴哥只手遮天2 小时前
Gmsh 源码深度解析-第 6 章 三维体网格算法
算法·开源