Gmsh 源码深度解析-第 1 章 架构总览与数据模型
本章目标:建立对 Gmsh 代码库的完整心智模型------它的定位与历史、目录与模块划分、构建系统、 分层依赖、核心数据模型(
GModel/GEntity/MElement/MVertex)、单元类型编号体系、 全局上下文与选项系统、API 层的生成机制,以及消息/Onelab 通信设施。本章是全系列的坐标系。后续每一章的类、函数、文件,都能在本章建立的框架里找到位置。
1.1 Gmsh 是什么
1.1.1 一句话定义与三个事实
Gmsh 的自我描述写在仓库根目录的 README.txt 开头:
This is Gmsh, an automatic three-dimensional finite element mesh generator with built-in pre- and post-processing facilities.
这句话看似平常,却精确定义了项目的边界,也决定了整个代码库的形态。把它拆成三个事实:
- automatic(自动) :用户输入几何(点、曲线、曲面、体),Gmsh 输出网格。从几何到网格 的"翻译"是自动的------这意味着代码库里必须存在一套完整的几何建模能力 (第 2 章)和一套 完整的网格生成算法(第 4--7 章)。用户不需要手工指定每个节点的位置。
- three-dimensional(三维):网格生成从一维(边)到二维(面)到三维(体)逐维推进。 虽然 Gmsh 支持只做到一维或二维,但其算法核心是为三维体网格服务的------很多二维的行为 受三维需求反向约束(例如第 4 章会看到"一维边段数受二维四边形算法约束"、第 6 章"HXT 只吃三角形面")。
- built-in pre- and post-processing(内建前后处理) :
- 前处理:几何建模 + 脚本化描述 + 网格生成。
- 后处理 :读入有限元结果(位移、应力、温度、磁场......)、组织成"视图"、可视化、导出。 这两块能力让 Gmsh 不只是"一个网格器",而是完整的仿真前后处理工具链。这也是为什么 它会被大量 CAE 软件当作"网格 + 可视化"组件集成。
1.1.2 历史、版权与许可证
版权行出现在几乎每个源文件的首行,把年份信息保留了下来。以 5.0 版源码为例:
cpp
// Gmsh - Copyright (C) 1997-2026 C. Geuzaine and J.-F. Remacle
//
// See the LICENSE.txt file in the Gmsh root directory for license information.
// Please report all issues on https://gitlab.onelab.info/gmsh/gmsh/issues.
这行注释记录了三个稳定事实:
- 时间跨度:1997 年起至 2026 年(年份随版本滚动更新),说明这是一个持续维护近三十年的 大型 C++ 项目。
- 维护者:C. Geuzaine(Christophe Geuzaine)与 J.-F. Remacle(Jean-François Remacle), 分别来自列日大学与鲁汶大学(UCLouvain),长期主导几何内核(Geuzaine)与网格算法 (Remacle)两条主线。
- 官方问题入口 :
gitlab.onelab.info,属于 Onelab 生态(第 10 章)。
许可证在 LICENSE.txt 与 README.txt 中都有说明:
Gmsh is copyright (C) 1997-2026 C. Geuzaine and J.-F. Remacle, and is distributed under the terms of the GNU General Public License, Version 2 or later, with an exception to allow for easier linking with external libraries.
这里的关键是 GPLv2+ 加上"链接例外" 。纯 GPL 会让"把 Gmsh 作为库链接进商业软件"在法律上 传染整个商业软件;链接例外则允许这样做而不必开源调用方。正是这条例外,使 api/ 下 的多语言绑定(C++/C/Python/Julia/Fortran/JS)能被大量商业与工业软件采用,也解释了为什么 Gmsh 要花大力气维护一个稳定的对外 API(第 10 章)。
1.1.3 版本号与版本注入
版本号定义在顶层 CMakeLists.txt 中(第 107--109 行):
cmake
set(GMSH_MAJOR_VERSION 5)
set(GMSH_MINOR_VERSION 0)
set(GMSH_PATCH_VERSION 0)
随后是一段很能说明工程成熟度的逻辑(第 110--132 行):如果构建时没有显式传入 GMSH_RELEASE,就用 git 取当前提交哈希,拼出一个开发版本号:
cmake
if(NOT GMSH_RELEASE)
find_package(Git)
if(GIT_FOUND)
execute_process(COMMAND ${GIT_EXECUTABLE} log -1 --format=%h
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} ERROR_QUIET
OUTPUT_VARIABLE GIT_COMMIT_HASH
OUTPUT_STRIP_TRAILING_WHITESPACE)
endif()
if(GIT_COMMIT_HASH)
message(STATUS "Found git hash ${GIT_COMMIT_HASH}")
set(GMSH_EXTRA_VERSION "${GMSH_EXTRA_VERSION}-git-${GIT_COMMIT_HASH}")
else()
set(GMSH_EXTRA_VERSION "${GMSH_EXTRA_VERSION}-git")
endif()
set(GMSH_EXTRA_VERSION_TEXI "${GMSH_EXTRA_VERSION_TEXI} (development version)")
endif()
最终版本字符串(第 134--136 行):
cmake
set(GMSH_SHORT_VERSION "${GMSH_MAJOR_VERSION}.${GMSH_MINOR_VERSION}.${GMSH_PATCH_VERSION}")
set(GMSH_VERSION "${GMSH_SHORT_VERSION}${GMSH_EXTRA_VERSION}")
于是运行中的 Gmsh 会报告形如 5.0.0-git-a1b2c3d 的版本。这套机制让任何一份二进制都能 回答"我是哪个提交构建的" ,对排查"用户报告的问题到底对应哪版代码"至关重要。版本字符串 最终注入到生成的头文件 GmshVersion.h,并通过选项 General.Version 暴露给脚本与 GUI。
1.1.4 生态定位
把 Gmsh 放回它能被理解的位置:
- 它是开源 CAE 工具链的最常用"网格入口":Gmsh 生成的网格可以被 CalculiX、Code_Aster、 GetDP、FEniCS、deal.II、OpenFOAM(经转换)等大量求解器读取。
- 它有一种事实上的网格交换格式
.msh(MSH 2/3/4,见第 2 章的GModelIO_MSH*),被众多 工具读写。 - 它是 Onelab 平台的前端:通过 socket 与外部求解器交互(第 10 章)。
- 它是可以被嵌入的库 :
api/提供五种语言绑定(第 10 章)。
理解这四点,就能理解为什么 Gmsh 的代码里同时存在"GUI、脚本语言、文件格式、API、求解器 接口"这些看似庞杂的东西------它们服务于"成为 CAE 工具链枢纽"这一目标。
1.2 顶层目录结构
text
gmsh-git-source/
├── api/ # 对外的稳定 API:gmsh.h(C++)、gmshc.h(C)、gmsh.py、gmsh.jl、gmsh.f90
├── contrib/ # 第三方/内嵌库:HighOrderMeshOptimizer、MeshOptimizer、MeshQualityOptimizer、
│ # voro++、Blossom、Metis、mmg、netgen、hxt、onelab、picojson、robin_hood ...
├── doc/ # 文档(含 texinfo/ 下的参考手册源)
├── examples/ # 示例文件(几何、网格、后处理、API 调用)
├── src/ # 全部 C++ 实现(下面逐目录展开)
├── tutorials/ # 21 个教程脚本(t1.geo ... t21.geo,含 .py 版本)
├── utils/ # 打包、WASM 构建、辅助脚本、测试
├── CMakeLists.txt # 顶层构建(97 KB,是理解构建选项的核心文件)
├── CHANGELOG.txt # 变更日志(69 KB)
├── CREDITS.txt # 贡献者与第三方许可
├── README.txt # 构建说明
├── LICENSE.txt # 许可证
├── .clang-format # 代码风格(3035 字节)
└── .clang-tidy # 静态检查配置
1.2.1 src/ 的模块划分
src/ 下十个子目录就是 Gmsh 的模块边界,它们不是随意命名,而是对应一条清晰的依赖链:
| 目录 | 职责 | 代表类/文件 |
|---|---|---|
common/ |
全局状态、选项、消息、API 实现、I/O、命令行 | Context、Options、GmshMessage、gmsh.cpp、Main.cpp |
geo/ |
几何内核 + 网格数据模型 + 文件格式读写 | GModel、GEntity、MElement、GEO_Internals、OCC_Internals |
mesh/ |
网格生成算法与尺寸场 | Generator、Field、meshGEdge/Face/Region* |
numeric/ |
数值基础设施 | ElementType、GaussIntegration、BasisFactory、robustPredicates |
parser/ |
GEO 脚本语言的词法与语法分析 | Gmsh.l、Gmsh.y、FunctionManager |
solver/ |
内置有限元求解器 | functionSpace、dofManager、linearSystem、elasticitySolver |
post/ |
后处理视图与文件读写 | PView、PViewData、PViewDataList、PViewOptions |
plugin/ |
后处理/网格插件 | PluginManager、CutPlane、Isosurface、HomologyComputation |
graphics/ |
OpenGL 渲染 | drawContext、View 相关 |
fltk/ |
FLTK 图形界面 + Onelab 面板 | FlGui、openglWindow |
这十个目录的依赖方向是严格单向 的(见 1.4 节):越底层越通用,越上层越贴近用户。 一个新人最有效的读码路径通常是:common → geo → mesh → numeric,而 post/solver/ graphics/fltk 按需深入。
1.2.2 contrib/ 的意义
contrib/ 封装了大量第三方库,它们是 Gmsh 能力的来源,但在源码层面被"隔离"在 contrib 中, 通过接口调用(如第 6 章的 HXT 头、第 9 章的 voro++)。这带来两个工程好处:
- 可选性 :第三方库通过
ENABLE_<LIB>开关(见 1.3)可关,于是 Gmsh 能不依赖它们独立 构建,也能在有它们时获得额外能力。 - 稳定性:第三方库升级/替换只影响 contrib 适配层,不波及核心算法。
1.3 构建系统:以选项为骨架
1.3.1 opt() 宏:一箭双雕
CMakeLists.txt 最先定义了一个 opt() 宏(第 29--32 行):
cmake
macro(opt OPTION HELP VALUE)
option(ENABLE_${OPTION} ${HELP} ${VALUE})
set(OPT_TEXI "${OPT_TEXI}\n@item ENABLE_${OPTION}\n${HELP} (default: ${VALUE})")
endmacro()
它一次做两件事:生成一个 ENABLE_xxx 的 CMake 开关,同时把这句说明追加到 OPT_TEXI (最终进入参考手册)。于是**"构建选项文档"永远与构建系统保持同步**------想让文档过期都难。 这是一个低成本、高回报的元编程设计。
1.3.2 完整的能力清单
紧随其后的是一长串 opt(...),按字母序排列(第 34--105 行)。这份清单本身就是 Gmsh 的 "功能目录",值得完整列出并解读:
核心模块开关:
cmake
opt(MESH "Enable mesh module" ${DEFAULT})
opt(SOLVER "Enable built-in finite element solvers (required for reparametrization)" ${DEFAULT})
opt(POST "Enable post-processing module (required by GUI)" ${DEFAULT})
opt(PARSER "Enable GEO file parser (required for .geo/.pos scripts)" ${DEFAULT})
opt(PLUGINS "Enable post-processing plugins" ${DEFAULT})
opt(FLTK "Enable FLTK graphical user interface (requires mesh/post)" ${DEFAULT})
注意依赖描述的措辞:SOLVER 被标注为"required for reparametrization"(重参数化依赖它), POST"required by GUI"、PARSER"required for .geo/.pos scripts"。这些括号不是装饰------它们 是构建期真实存在的依赖(1.3.3)。
几何内核与网格算法:
cmake
opt(OCC "Enable OpenCASCADE CAD kernel" ${DEFAULT})
opt(BAMG "Enable Bamg 2D anisotropic mesh generator" ${DEFAULT})
opt(HXT "Enable HXT library (for reparametrization and meshing)" ${DEFAULT})
opt(MMG "Enable Mmg mesh adaptation interface" ${DEFAULT})
opt(NETGEN "Enable Netgen 3D frontal mesh generator" ${DEFAULT})
opt(METIS "Enable Metis mesh partitioner" ${DEFAULT})
opt(TETGEN?) # 见后续;tet 相关能力
opt(CGNS "Enable CGNS import/export (experimental)" ${DEFAULT})
opt(MED "Enable MED mesh and post file formats" ${DEFAULT})
opt(MESQUITE "Enable mesquite" ${DEFAULT})
opt(VOROPP "Enable voro++ (for hex meshing, experimental)" ${DEFAULT})
opt(BLOSSOM "Enable Blossom algorithm (needed for full quad meshing)" ${DEFAULT})
opt(OPTHOM "Enable high-order mesh optimization tools" ${DEFAULT})
opt(DOMHEX "Enable experimental DOMHEX code" ${DEFAULT})
opt(QUADMESHINGTOOLS "Enable QuadMeshingTools extensions" ${DEFAULT})
opt(QUADTRI "Enable QuadTri structured meshing extensions" ${DEFAULT})
opt(BOUNDARY_LAYERS "Enable boundary layer plugin" ${DEFAULT})
opt(OCTREE_SIZE_FIELD "Enable the octree-based automatic mesh size field" ${DEFAULT})
线性代数与并行:
cmake
opt(EIGEN "Enable Eigen for linear algebra (instead of Blas/Lapack)" ON)
opt(BLAS_LAPACK "Enable BLAS/Lapack for linear algebra (if Eigen if disabled)" OFF)
opt(PETSC "Enable PETSc linear solvers (required for SLEPc)" OFF)
opt(SLEPC "Enable SLEPc eigensolvers" OFF)
opt(MUMPS "Enable MUMPS sparse direct linear solver" OFF)
opt(GMM "Enable GMM linear solvers (simple alternative to PETSc)" ${DEFAULT})
opt(OPENMP "Enable OpenMP" ${DEFAULT})
opt(OPENACC "Enable OpenACC" OFF)
opt(MPI "Enable MPI (experimental, not used for meshing)" OFF)
构建产物:
cmake
opt(BUILD_LIB "Enable 'lib' target for building static Gmsh library" OFF)
opt(BUILD_SHARED "Enable 'shared' target for building shared Gmsh library" OFF)
opt(BUILD_DYNAMIC "Enable dynamic Gmsh executable (linked with shared library)" OFF)
opt(BUILD_ANDROID "Enable Android NDK library target (experimental)" OFF)
opt(BUILD_IOS "Enable iOS library target (experimental)" OFF)
opt(RPATH "Use RPATH in dynamically linked targets" ON)
opt(PACKAGE_STRIP "Strip symbols in install packages to reduce install size" ON)
其余开关覆盖:3M(专有扩展)、CGNS_CPEX0045、FFMPEG、FLTK、GEOMETRYCENTRAL、 GETDP、GRAPHICS、MATHEX、MPEG_ENCODE、MSVC_STATIC_RUNTIME、NII2MESH、 NUMPY、PETSC4PY、OCC_CAF、OCC_STATIC、OCC_TBB、ONELAB、ONELAB_METAMODEL、 OS_SPECIFIC_INSTALL、POPPLER、PRIVATE_API、PRO、PROFILE、SYSTEM_CONTRIB、 TCMALLOC、TESTS、TINYOBJLOADER、TINYXML2、TOUCHBAR、VISUDEV、WRAP_JAVA、 WRAP_PYTHON、ZIPPER 等。
这份清单里藏着几条重要结论:
- 默认全开 :几乎所有功能能力默认
${DEFAULT}(通常为 ON),说明 Gmsh 追求"开箱即用" 的完整功能,而非极简内核。 - 重依赖默认关 :
PETSC、SLEPC、MUMPS、MPI、BLAS_LAPACK默认 OFF,因为它们需要 外部环境;默认线性代数后端是EIGEN(ON)。 - 实验性功能隔离 :
DOMHEX、VOROPP、GEOMETRYCENTRAL、CGNS、ONELAB_METAMODEL等 被标注 statistical/experimental。
1.3.3 依赖与条件编译的钥匙
这些开关决定了源码里 #if defined(HAVE_*) 分支。举几个真实例子:
-
src/mesh/meshGRegionHxt.cpp 第 25 行:
cpp#if defined(HAVE_HXT) #include "hxt_omp.h" extern "C" { #include "hxt_tetMesh.h" #include "hxt_tetDelaunay.h" #include "hxt_boundary_recovery.h" ...即 HXT 三维算法(第 6 章的默认算法)只有在
ENABLE_HXT时编译。 -
src/mesh/HighOrder.cpp 的高阶优化分支:
cpp#if defined(HAVE_OPTHOM) HighOrderMeshFastCurving(m, p, false); #else Msg::Error("High-order mesh optimization requires the OPTHOM module"); #endif即高阶曲边优化在
ENABLE_OPTHOM关闭时会给出明确错误而不是静默失败。 -
src/post/PViewData.h 里对后处理字段的条件引用:
cpp#if defined(HAVE_POST) class PView; #endif即 src/mesh/Field.h 只有在后处理模块存在时才能引用
PView------ 尺寸场可以从后处理视图采样(PostViewField,第 4 章)。这正是一条从 mesh 指向 post 的 反向依赖,通过条件编译"软化"了编译期的硬依赖。
1.3.4 三种构建形态
README.txt 明确列出了构建方式:
-
单体可执行文件(monolithic):
bashmkdir build && cd build && cmake .. && make产出
gmsh可执行文件,所有模块静态编入。 -
动态链接库(dynamic):
bashcmake -DENABLE_BUILD_DYNAMIC=1 .. make && make install额外产出共享库
libgmsh,并安装 C++/C 头文件与 Python/Julia/Fortran 模块。这是外部程序 链接 Gmsh 的标准方式(对应ENABLE_BUILD_DYNAMIC与ENABLE_BUILD_SHARED、ENABLE_BUILD_LIB)。 -
WebAssembly(WASM):
The JavaScript Gmsh API needs Gmsh compiled to WebAssembly with Emscripten: see utils/wasm/README.txt.
即
api/里的 JavaScript 绑定来自 Emscripten 构建。这使 Gmsh 能在浏览器里运行------一个 几何/网格库能跑在前端,是很罕见的工程成就。
此外,ccmake(CMake 的字符界面)与命令行 -D 传参都受支持,CMAKE_PREFIX_PATH 用于 指定外部依赖位置,-DENABLE_FLTK=0 可构造无 GUI 版本。
1.3.5 私有 API 与生成文件的登记
顶层 CMakeLists 第 147 行登记了对外 API 的产物:
cmake
set(GMSH_API api/gmsh.h api/gmshc.h api/gmsh.h_cwrap api/gmsh.f90)
第 149--169 行处理 ENABLE_PRIVATE_API:当开启时,把 src/ 下各模块的头文件(common、numeric、 geo、mesh、solver、post、plugin、graphics)加上 contrib 里优化器的头一起暴露,并给出明确 警告:
cmake
message(WARNING "The private API is unsupported and undocumented. It is meant "
"for expert Gmsh developers, not for regular Gmsh users, who should rely "
"on the stable public API (gmsh/api) instead.")
这条警告区分了公开 API (稳定、生成、文档化)与私有 API (泄露内部 C++ 类型、仅供 插件作者使用)。第 9 章的共享库插件正是基于私有 API 构建的------它们需要 PView、PViewDataList 等内部类型。
1.4 分层架构与依赖方向
1.4.1 依赖图
Gmsh 的模块依赖可以用下图概括(箭头表示"依赖/调用"):
text
┌───────────────────────────────────────────────┐
│ 用户层 fltk(GUI) / graphics(渲染) / api(对外API) │
└───────────────┬───────────────────────────────┘
│
┌───────────────▼───────────────────────────────┐
│ 应用层 post(后处理) / plugin(插件) / solver(求解器)│
└───────────────┬───────────────────────────────┘
│
┌───────────────▼───────────────────────────────┐
│ 网格层 mesh(网格生成) ── 依赖 numeric + geo │
└───────────────┬───────────────────────────────┘
│
┌───────────────▼───────────────────────────────┐
│ 几何层 geo(GModel/GEntity/MElement,双几何内核) │
└───────────────┬───────────────────────────────┘
│
┌───────────────▼───────────────────────────────┐
│ 基础层 common(Context/Options/Msg/IO) + numeric │
└───────────────────────────────────────────────┘
1.4.2 为什么几何层是核心枢纽
与许多"几何归几何、网格归网格"的架构不同,Gmsh 的 geo 目录同时 是几何(B-Rep)的容器 和网格(MElement)的容器。第 1.6/1.8 节会看到:GEntity 直接持有 mesh_vertices,GFace 直接持有 triangles/quadrangles,GRegion 直接持有 tetrahedra/hexahedra/......
这种"几何与网格不分离"的选择有几个深远后果:
- 网格是几何实体的"属性" :既然是属性,改变几何就必须让网格失效------这解释了第 4 章
GenerateMesh里"要做二维先删体的网格、要做一维先删面体的网格"的降维删除策略。 - 归属关系清晰:曲线上的节点归曲线,面上的单元归面,避免了"节点到底属于谁"的歧义。 这对并行网格生成的正确性很关键(第 4 章)。
- 代价 :
GEntity承担了过多职责(既描述几何,又存网格,还存物理组、周期对应、渲染 顶点数组)。源码里GEntity的注释自嘲"these will become protected at some point" (第 1.6 节),正是对这种"职责膨胀"的自觉。
1.4.3 numeric 为什么是独立一层
第 8 章会展开,但这里先给结论:numeric 提供的单元类型枚举 、Gauss 积分 、有限元 基函数 、鲁棒几何谓词 ,被 geo(求交、离散)、mesh(雅可比、形函数)、solver(积 分、基函数)、post(插值)同时 使用。若把它塞进 mesh,则 geo 与 post 都要反向 依赖 mesh,破坏分层。因此它必须独立成层。
1.4.4 common 提供全局单例与 I/O
Context(CTX::instance())、Options、GmshMessage 是几乎每个模块都要用到的全局设施。 把"全局可变状态"集中在 common,至少让它的影响面可控。第 1.11/1.12 节详述。
1.4.5 api 是薄壳
src/common/gmsh.cpp(约 9000 行)逐个把公开 API 函数转调到底层 GmshXxx()。它本身不含算法,只含"参数搬运 + 错误处理 + 生命周期管理"。第 1.13 节详述。
1.5 核心数据模型(一):GModel
1.5.1 类注释:一句自我定位
src/geo/GModel.h 的类注释只有一句话:
cpp
// A geometric model. The model is a "not yet" non-manifold B-Rep.
class GModel {
"not yet non-manifold B-Rep"是一句极精炼的定位。含义是:Gmsh 的模型允许暂时处于非流形、 非一致的状态,例如:
- 两个曲面共享一条边但并未真正"缝合";
- 两个体共享一个面但面在两边各自有一份;
- 一条曲线与一个曲面相交但没有交点实体。
为什么允许这种"不干净"的表示?因为 CAD 导出的文件(STEP/IGES/STL)经常不完美。若要求模型 一进来就必须是完美流形的,Gmsh 会拒绝大量真实数据。**"宽容表示 + 逐步修复"**是这里的核心 哲学,也是 Gmsh 能处理工业数据的前提。修复手段包括 HealShapes、Coherence(几何缝合)、 以及网格层的边界恢复(第 6 章)。
1.5.2 四类实体的存储
cpp
// the sets of geometrical regions, faces, edges and vertices in the model
std::set<GRegion *, GEntityPtrLessThan> regions;
std::set<GFace *, GEntityPtrLessThan> faces;
std::set<GEdge *, GEntityPtrLessThan> edges;
std::set<GVertex *, GEntityPtrLessThan> vertices;
这段代码有三个要点:
- 四种维度各一个集合,而不是一个"所有实体"的大集合。原因是不同维度的接口差异很大 (面的参数化、体的边界),分开存储便于按维度遍历与查找。
- 用
std::set而非std::vector:实体指针在算法过程中频繁增删(布尔运算、删除、 合并、分区),std::set的增删复杂度 O(logn)、且迭代稳定(不会因插入导致已有迭代器 失效)。std::vector在删除时会移动大量元素,代价高。 - 比较器
GEntityPtrLessThan按 tag 排序 (见 1.6.6),保证遍历顺序确定------这对网格生成 顺序的可复现性(第 4 章的renumberMeshCanonically)是基础。
1.5.3 GModel 的私有成员逐条解析
GModel 的私有区(第 53--171 行)信息量巨大,逐条来看:
同调相关的数据(第 54--60 行):
cpp
std::multimap<std::pair<const std::vector<int>, const std::vector<int>>,
std::pair<const std::string, const std::vector<int>>>
_homologyRequests;
std::set<GRegion *, GEntityPtrLessThan> _chainRegions;
std::set<GFace *, GEntityPtrLessThan> _chainFaces;
std::set<GEdge *, GEntityPtrLessThan> _chainEdges;
std::set<GVertex *, GEntityPtrLessThan> _chainVertices;
这四组 _chainXxx 是"链(chain)"实体------第 8 章会讲 Gmsh 的计算拓扑能力(同调/上同调)。 把链作为一等实体存进模型,意味着同调计算的结果(如"环绕某个孔的环链")可以像普通几何一样 被引用、显示与导出。
网格边/面全局表与编号(第 61--65 行):
cpp
hashmapMEdge _mapEdgeNum;
hashmapMFace _mapFaceNum;
// the maximum vertex and element id number in the mesh
std::size_t _maxVertexNum, _maxElementNum;
std::size_t _checkPointedMaxVertexNum, _checkPointedMaxElementNum;
_mapEdgeNum/_mapFaceNum 是全局的"网格边 / 网格面"哈希表(MEdge/MFace 是低维拓扑 元)。它们让"两个单元是否共享同一条边/面"可以 O(1) 查询------这是边界恢复、共形性检查、 分区(createMEdges/createMFaces)的核心。_maxVertexNum/_maxElementNum 是全局编号 计数器(1.5.4 详述)。_checkPointed... 用于"检查点"(快照当前编号水位)。
这两个哈希表用第 1.5.4 的原子编号策略配合 OpenMP,见下。
重叠网格管理(第 67--70 行):
cpp
// Overlaps data: each OverlapManager holds a group of overlaps with their
// boundary data. Multiple managers allow different overlap configurations.
std::vector<OverlapManager> _overlapManagers;
int _nextOverlapTag = 0;
OverlapManager 支持 Gmsh 的**重叠网格(overset/chimera)**能力:允许两组网格在空间上重叠, 用于相对运动(如旋转机械的动/静网格)。多个 manager 支持不同重叠配置共存。
模型元信息(第 101--109 行):
cpp
std::string _name; // 模型名
std::string _fileName; // 读入的文件名
std::set<std::string> _fileNames; // 关联过的所有文件名
char _visible; // 可见性
_fileNames 是集合,因为一个模型可以由多个文件 merge(File->Merge)而来。
按 tag 直接访问的缓存(第 111--117 行):
cpp
// vertex and element caches to speed-up direct access by tag (mostly used
// for post-processing I/O)
std::vector<MVertex *> _vertexVectorCache;
std::map<std::size_t, MVertex *> _vertexMapCache;
std::vector<std::pair<MElement *, int>> _elementVectorCache;
std::map<std::size_t, std::pair<MElement *, int>> _elementMapCache;
std::map<int, int> _elementIndexCache;
这是"向量 + 映射表双结构":向量用于顺序遍历(快),映射表用于按编号随机访问(O(1))。 注释点明用途是"mostly used for post-processing I/O"------后处理写文件时经常需要"按编号找节点/ 单元",这是高频操作,必须加速。
影子单元(第 119--122 行):
cpp
// ghost cell information (stores partitions for each element acting as a ghost cell)
// /!\ Use only for compatibility with mesh format msh2 and msh3
std::multimap<MElement *, short> _ghostCells;
影子单元(ghost cell)是分区交界处为邻居分区保存的单元副本(第 7 章)。注释警告它"仅用于 与 MSH2/MSH3 兼容"------MSH4 对影子单元有更结构化的表示(第 2 章的 GModelIO_MSH4)。
空间索引(第 124--127 行):
cpp
// an octree for fast mesh element lookup
MElementOctree *_elementOctree;
// the octree of the mesh elements, built on first use
MElementOctree *_getElementOctree();
八叉树惰性构建(首次使用时才建),用于"给定空间点找单元"这类查询(后处理探针、剖切)。 惰性是必要的:对百万单元建八叉树代价很高,而很多任务从不查询空间。
曲率缓存(第 129--130 行):
cpp
// global cache storage of discrete curvatures
std::map<MVertex *, std::pair<SVector3, SVector3>> _curvatures;
每个网格顶点缓存一对主曲率方向 SVector3(第 8 章的 curvature.h 计算)。这是各向异性 尺寸场 的输入之一(曲率大处沿曲率方向加密,第 4 章的 CurvatureField)。
四个几何内核句柄(第 132--139 行):
cpp
// GEO (Gmsh native) model internal data
GEO_Internals *_geo_internals;
// OpenCASCADE model internal data
OCC_Internals *_occ_internals;
// ACIS model internal data
ACIS_Internals *_acis_internals;
// Parasolid model internal data
_Parasolid_Internals *_parasolid_internals;
这就是 Gmsh 的多内核架构 :同一个 GModel 可以同时挂着自研 GEO 内核与 CAD 内核 (OCC/ACIS/Parasolid)。上层代码只面对统一的 GEntity 抽象;内核切换由脚本的 SetFactory 命令与 gmsh_yyfactory(第 3 章)控制。实体创建、布尔、拉伸则由对应的 *_Internals 完成 (第 2 章)。
尺寸场管理器(第 141--142 行):
cpp
// characteristic length (mesh size) fields
FieldManager *_fields;
这就是第 4 章尺寸场系统的挂载点:m->getFields() 返回它。
当前网格化实体与错误记录(第 144--149 行):
cpp
// entity that is currently being meshed (used for error reporting)
GEntity *_currentMeshEntity;
// last entities/vertices where a meshing error has been reported
std::vector<GEntity *> _lastMeshEntityError;
std::vector<MVertex *> _lastMeshVertexError;
_currentMeshEntity 让任何深层错误都知道"是哪个实体在网格化时出问题";后两个向量在失败时 记录出问题的实体/节点,供 GUI 高亮(第 4 章的 clearLastMeshEntityError)。
当前模型索引(第 151--153 行):
cpp
// index of the current model (in the static list of all loaded models)
static int _current;
GModel 支持多个模型同时加载 (static std::vector<GModel *> list,第 215 行)。_current 是"当前活动模型"的下标。任何时刻只有一个"当前模型",这解释了为什么大量 API 都隐含"操作 当前模型"(GModel::current())。
物理组与原始实体名(第 162--164 行):
cpp
// map between the pair <dimension, elementary or physical number> and an optional name
std::map<std::pair<int, int>, std::string> _physicalNames, _elementaryNames;
键是 (维度, 编号) 对,值是名字。物理组名(如 "inlet"/"wall")与原始实体名都存这里。
分区数(第 166--167 行):
cpp
// the set of all used mesh partition numbers
std::size_t _numPartitions;
文件附加属性(第 169--170 行):
cpp
// additional attributes (e.g. stored in extra sections of MSH files)
std::map<std::string, std::vector<std::string>> _attributes;
MSH 文件的额外段(自定义属性)也挂在模型上。
1.5.4 全局编号与原子计数
GModel 用一组原子操作管理全局节点/单元编号(第 238--293 行)。核心三个:
cpp
std::size_t incrementAndGetMaxVertexNumber()
{
std::size_t _myVertexNum;
#pragma omp atomic capture
{
++_maxVertexNum;
_myVertexNum = _maxVertexNum;
}
return _myVertexNum;
}
#pragma omp atomic capture 保证"自增并取回"是一个原子操作。这让多个线程可以并发地创建 节点而不用锁------每个线程拿到唯一编号。配套还有:
cpp
void setMaxVertexNumber(std::size_t num) {
std::size_t max;
#pragma omp atomic read
max = _maxVertexNum;
if(num > max) {
#pragma omp atomic write
_maxVertexNum = num;
}
}
"只增不减"的语义(取 max 后写入)保证编号不会被回退到已用过的值。
1.5.5 规范重编号:可复现性的基石
并行网格生成带来的问题是:不同线程创建节点的顺序不确定,导致编号不确定 。为了让"同一 输入产生同一网格(逐字节一致)",Gmsh 在阶段末尾调用 renumberMeshCanonically(第 294--304 行 注释):
cpp
// renumber canonically the nodes and elements created since the maximum
// numbers were maxVertexBefore and maxElementBefore: entity by entity, the
// elements in their order, the nodes in the order of their first appearance
// in the elements, after the maximum numbers; the others keep their numbers.
// The meshing of a dimension depends on the node numbers of the lower
// dimensions (containers ordered or hashed by number, point ids), and a
// meshing with several threads numbers nodes and elements in the order the
// threads happen to create them: this makes the mesh independent of the
// threads
这段注释极有价值,它解释了为什么重编号是必要的:
- 跨维度依赖编号:低维网格的编号会影响高维网格的生成("containers ordered or hashed by number, point ids")。若节点编号随线程而变,则高维网格也会随之不同。
- 确定性目标:"this makes the mesh independent of the threads"------重编号让网格与线程数/ 调度无关。
重编号规则:"实体逐个(按 tag 顺序)→ 单元按自身顺序 → 节点按其在单元中的首次出现顺序", 全部排在阶段开始时的最大编号之后;其余保持原编号。
1.5.6 网格边/面的全局表
第 320--355 行是网格低维拓扑的管理:
cpp
// create mesh edges and faces
void createMEdges(const std::vector<std::pair<int, int>> &dimTags = ...);
void createMFaces(const std::vector<std::pair<int, int>> &dimTags = ...);
// add a mesh edge or face in the global edge or face map with number "num",
// or number it (starting at 1) if num == 0
std::size_t addMEdge(MEdge &&edge, std::size_t num = 0);
std::size_t addMFace(MFace &&face, std::size_t num = 0);
void reserveMEdges(std::size_t n) { _mapEdgeNum.reserve(_mapEdgeNum.size() + n); }
void reserveMFaces(std::size_t n) { _mapFaceNum.reserve(_mapFaceNum.size() + n); }
std::size_t getNumMEdges() const { return _mapEdgeNum.size(); };
std::size_t getNumMFaces() const { return _mapFaceNum.size(); };
std::size_t getMEdge(MVertex *v0, MVertex *v1, MEdge &edge);
std::size_t getMFace(MVertex *v0, MVertex *v1, MVertex *v2, MVertex *v3, MFace &face);
MEdge 与 MFace 是由节点构成的低维拓扑元 (不是单元)。把它们放进全局哈希表,就能 回答"这条边被哪些单元共享"。reserveMEdges/reserveMFaces 是性能优化:网格化前预留容量, 避免多次 rehash。
getMEdge(v0,v1,edge) 返回该边在表中的编号(不存在返回 0)------这正是 script 命令 CreateTopology、CreateMeshEdges/CreateMeshFaces(第 3 章 token tCreateMeshEdges)等 功能的底层。
1.5.7 实体增删与编号变更
第 477--491 行:
cpp
bool add(GRegion *r);
bool add(GFace *f);
bool add(GEdge *e);
bool add(GVertex *v);
bool remove(GRegion *r);
...
// (each add or remove calls CTX::geomChanged())
注释点明每次增删都会调用 CTX::geomChanged()------这是第 1.11 节"变更时间戳"机制的用法: 任何几何变更都会通知全局上下文,从而让缓存失效、GUI 重绘。
changeEntityTag(int dim, int tag, int newTag)(第 475 行)演示了 std::set 的一个麻烦: 修改实体的 tag 会改变它在集合中的排序位置,因此必须"从集合中取出、改 tag、再插回"。
1.5.8 物理组 API
第 519--595 行是一整块物理组(physical group)API。物理组是 Gmsh 的语义标注机制 :把 几何实体分组并命名,导出时只写属于某个物理组的元素(t1.geo 注释确认了这一点)。
cpp
void getPhysicalGroups(std::map<int, std::vector<GEntity *>> groups[4]) const;
void addPhysicalGroup(int dim, int tag, const std::vector<int> &tags);
int setPhysicalName(const std::string &name, int dim, int num = 0);
std::string getPhysicalName(int dim, int num) const;
int getPhysicalNumber(const int &dim, const std::string &name);
std::vector<int> getTagsForPhysicalName(int dim, const std::string &name);
void setPhysicalNumToEntitiesInBox(int EntityDimension, int PhysicalNumber,
std::vector<double> p1, std::vector<double> p2);
getPhysicalGroups(groups[4]) 用一个长度为 4 的数组(对应 0--3 维)分维度返回物理组。 setPhysicalNumToEntitiesInBox 能"把落在盒里的实体都设为某个物理组"------对应脚本命令 Physical ... In BoundingBox(第 3 章 token tBoundingBox)。
1.6 核心数据模型(二):GEntity
1.6.1 字段全览
GEntity 的私有字段(src/geo/GEntity.h 第 32--49 行):
cpp
GModel *_model; // all entities are owned by a GModel
int _tag; // the tag (the number) of this entity
GEntity *_meshMaster; // points to the master entity in periodic mesh, or 'this' if non-periodic
char _visible, _selection;
char _onlySomeElementsVisible;
unsigned int _color;
SOrientedBoundingBox *_obb; // 有向包围盒(protected)
要点:
- 归属 :
_model表明实体归模型所有,GEntity不自己管理生命周期。 - tag 唯一性 :
_tag在每个维度内唯一(第 1.8 节会看到 tag 是std::set的排序键)。 _meshMaster的语义 :指向周期网格中的主实体,非周期时指向自己 。这个"self-pointer" 约定很聪明:gf->getMeshMaster() != gf这一判断同时是"是否周期"的判断(第 4 章Mesh2D用它决定是否退化为单线程)。
1.6.2 public 区:实体与网格/物理组的绑定
第 55--77 行(注释注明"these will become protected at some point"):
cpp
// the mesh vertices uniquely owned by the entity
std::vector<MVertex *> mesh_vertices;
// a list of geometrical entities that form a compound mesh
std::vector<GEntity *> compound;
// the corresponding principal points of the entity in case of periodic mesh
std::map<GVertex *, GVertex *> vertexCounterparts;
// the physical groups that contain this entity
std::vector<int> physicals;
// the vertex arrays to draw the mesh of the entity efficiently
VertexArray *va_lines, *va_triangles;
VertexArray *va_clip_lines, *va_clip_triangles;
// the set of high-order elements fixed by the "fast curving" boundary layer optimization
std::set<MElement *> curvedBLElements;
逐条解读:
-
mesh_vertices:实体"独有"的网格顶点。注意"uniquely owned"------共享顶点不在这里, 而由更低维实体拥有。这个归属规则是网格生成正确性的基础。 -
compound:一组实体组成"复合网格",网格化时作为一个整体(第 5 章MeshSetTransfiniteFacesAutomatic与复合面处理)。 -
vertexCounterparts:周期网格的顶点级主从对应(GVertex* → GVertex*)。 -
physicals:所属物理组编号列表。 -
va_lines/va_triangles:渲染用的顶点数组(第 9/图形章)。va_clip_*是切割面 (clipping plane)追加的部分,与主体分开存放,这样移动切面不必重建主体数组------注释 (第 70--73 行)写得很清楚:what the clipping planes add, kept apart so that moving a plane does not rebuild the arrays above: the section they cut (capping), or the cut elements drawn whole (whole element mode)
-
curvedBLElements:被"快速曲边"边界层优化固定住的高阶单元(第 7 章HighOrderFastCurving的curveOuterBL)。
1.6.3 枚举(一):ModelType
第 84--91 行:
cpp
// all known native model types
enum ModelType {
UnknownModel,
GmshModel, // 自研 GEO 内核
OpenCascadeModel, // OpenCASCADE
AcisModel, // ACIS
ParasolidModel // Parasolid
};
getNativeType() 返回它,用于判断"这个实体来自哪个内核"。第 2 章的 gmshEdge 返回 GmshModel、OCCFace 返回 OpenCascadeModel。
1.6.4 枚举(二):GeomType 是一份功能地图
第 94--136 行的 GeomType 枚举内容丰富(40 个取值),它把内核几何类型 与工程态类型 混在一起,本身就构成一份功能地图:
cpp
enum GeomType {
Unknown, Point, BoundaryLayerPoint, Line, Circle, Ellipse, Conic, Parabola,
Hyperbola, TrimmedCurve, OffsetCurve, BSpline, Bezier, ParametricCurve,
BoundaryLayerCurve, DiscreteCurve, Plane, Nurb, Cylinder, Sphere, Cone, Torus,
RuledSurface, ParametricSurface, BSplineSurface, BezierSurface,
SurfaceOfRevolution, BoundaryLayerSurface, DiscreteSurface, DiscreteDiskSurface,
Volume, DiscreteVolume, PartitionPoint, PartitionCurve, PartitionSurface,
PartitionVolume, GhostCurve, GhostSurface, GhostVolume, OverlapSurface,
OverlapVolume,
};
按语义分组:
- 解析基元 :
Point、Line、Circle、Ellipse、Conic、Parabola、Hyperbola、BSpline、Bezier、TrimmedCurve、OffsetCurve、ParametricCurve(曲线侧);Plane、Nurb、Cylinder、Sphere、Cone、Torus、RuledSurface、SurfaceOfRevolution、BSplineSurface、BezierSurface(曲面侧)。 - 离散实体 :
DiscreteCurve、DiscreteSurface、DiscreteDiskSurface(可参数化、同构于 圆盘)、DiscreteVolume------表示"没有解析表达式,只有网格",来自 STL/网格文件导入。 - 工程态 :
BoundaryLayerPoint/Curve/Surface(边界层)、PartitionPoint/Curve/Surface/ Volume(分区)、GhostCurve/Surface/Volume(分区影子)、OverlapSurface/Volume(重叠 网格)。
getTypeString()(第 142--191 行)把它转成人类可读字符串,实现用一个 const char *name[] 数组按枚举值索引:
cpp
virtual std::string getTypeString()
{
const char *name[] = { "Unknown", "Point", "Boundary layer point", ... };
unsigned int type = (unsigned int)geomType();
if(type >= sizeof(name) / sizeof(name[0])) return "Undefined";
else return name[type];
}
注意这里枚举值与字符串数组下标必须严格对应,是一种"用数组当 switch"的紧凑写法,代价是 加枚举值时要同步维护数组长度。
1.6.5 枚举(三):网格生成状态
第 139 行:
cpp
// the mesh generation status
enum MeshGenerationStatus { PENDING, DONE, FAILED };
这个三态枚举是第 4 章流水线多轮重试机制的核心:流水线反复扫描 PENDING 的实体并调用 mesh(),直到没有 PENDING 为止。
1.6.6 比较器:GEntityPtrLessThan
第 475--486 行:
cpp
struct GEntityPtrLessThan {
// beware that this comparison function does *not* compare the entity
// dimension; this is on purpose
bool operator()(const GEntity *ent1, const GEntity *ent2) const
{
return ent1->tag() < ent2->tag();
}
// lookup by tag, without creating a temporary entity
using is_transparent = void;
bool operator()(const GEntity *ent, int tag) const { return ent->tag() < tag; }
bool operator()(int tag, const GEntity *ent) const { return tag < ent->tag(); }
};
两个要点:
- 不比较维度 (注释强调"on purpose"):因为
GModel的四个std::set已经按维度分开 存储,同一集合内维度相同,无需再比。这使排序更快。 is_transparent:这是 C++14 的"透明比较器"特性,允许std::set<GEntity*>用整数 tag 直接find(tag)而不必构造临时GEntity*。这是找不到实体时避免创建临时对象的关键 优化 ------getFaceByTag(n)等之所以高效,就靠它。
另有 GEntityPtrFullLessThan/GEntityPtrFullEqual/GEntityPtrFullHash(第 488--508 行): 按 (dim, tag) 排序/相等/哈希的版本,用于需要同时区分维度的哈希容器。
1.7 四个实体基类与它们的网格容器
GEntity 只提供"实体都是什么"的通用部分;每个维度的差异由四个基类补充。关键区别之一 是它们各自持有不同种类的网格单元容器------这正是"几何与网格不分离"的具体体现。
1.7.1 GVertex(0 维)
GVertex 描述一个几何点:坐标 (x,y,z)、一个"目标网格尺寸" lc、一份 points (std::vector<MPoint*>)。第 4 章的 Mesh0D 会给每个几何点建一个 MVertex 和一个 MPoint。 GVertex 还维护"相邻的边"列表,用于从点出发遍历拓扑。
1.7.2 GEdge(1 维)
GEdge(src/geo/GEdge.h)的字段揭示了它的双重身份:
cpp
// prescribed mesh size (_lc) at parametric points (_u_lc) (sorted by _u_lc)
std::vector<double> _u_lc, _lc;
protected:
GVertex *_v0, *_v1; // 起止顶点
std::vector<GFace *> _faces; // 相邻面
public:
int masterOrientation; // 相对主边的方向
std::vector<MLine *> lines; // ★ 该边的网格(线段集合)
GEdge *compoundCurve; // 复合曲线(保留原始曲线的网格)
std::vector<SPoint3> stl_vertices_xyz; // STL 离散
要点:
lines:边的网格是std::vector<MLine*>------一串线段。这就是第 4 章一维网格的产物。_u_lc/_lc:一串"参数位置 → 目标尺寸"的采样点,是尺寸场在边上离散化的结果。它们 按参数排序,供meshGEdgeProcessing(第 4 章)插值求局部尺寸。compoundCurve注释解释了它的存在:"when a compound of curves is created, both meshes should be kept alive (because the 2D meshing procedure will need to access the mesh of each of the original curves, in addition to the mesh of the compound curve)"------复合曲线既要保留 复合后的网格,又要保留原始子曲线的网格,因为二维网格化两者都要用。stl_vertices_xyz:为 STL 可视化单独保存的离散点列。
GEdge 的几何契约方法(第 128--149 行)是曲线的标准接口:
cpp
virtual GPoint point(double p) const; // 参数 → 点
virtual SVector3 position(double p) const; // 参数 → 位置向量
virtual SVector3 firstDer(double par) const; // 一阶导(切向)
virtual SVector3 secondDer(double par) const; // 二阶导(默认用中心差分)
virtual double curvature(double par) const; // 曲率
virtual bool isSeam(const GFace *face) const; // 是否为某面的缝合线
注意 secondDer 的注释:"default implementation using central differences"------基类提供一个 用中心差分的默认实现,具体内核(gmshEdge/OCCEdge)若有解析二阶导则覆盖它。这是"默认 实现 + 可选覆盖"的实用模式。
GEdge 还有 setBeginVertex/setEndVertex(第 61--62 行),注意它们会反向注册到自己 的顶点:
cpp
void setBeginVertex(GVertex *gv) { _v0 = gv; if(gv) gv->addEdge(this); }
void setEndVertex(GVertex *gv) { _v1 = gv; if(gv) gv->addEdge(this); }
这就是"双向拓扑链接":边记得顶点,顶点也记得边。整个 Gmsh 的拓扑都靠这种双向/多向指针 连接起来(第 5 章的 BDS 是同一思想在二维算法内部的翻版)。
isSeam 值得单独说:**缝合线(seam)**是圆柱/球面这类周期曲面在参数化时"切开"的那条线 (参数 u=0 与 u=2π 重合处)。它属于两个参数位置但几何上是同一条线。Gmsh 必须识别 seam 才能正确参数化周期曲面网格。
1.7.3 GFace(2 维)
GFace(src/geo/GFace.h)的字段:
cpp
std::set<GVertex *, GEntityPtrLessThan> embedded_vertices;
BoundaryLayerColumns _columns;
public: // this will become protected or private
std::vector<GEdgeLoop> edgeLoops; // ★ 边界环
std::map<GEdge *, std::pair<GEdge *, int>> edgeCounterparts; // 周期边对应
private:
GRegion *r1, *r2; // 相邻的两个体
std::vector<GEdge *> l_edges; // 边界边
std::vector<int> l_dirs; // 边界边方向
std::vector<GEdge *> embedded_edges;
要点:
-
r1/r2:一个面最多被两个体共享(流形假设)。addRegion的实现很直接:cppvoid addRegion(GRegion *r) { if(r == r1 || r == r2) return; r1 ? r2 = r : r1 = r; } GRegion *getRegion(int const num) const { return num == 0 ? r1 : r2; }第 6 章
FindConnectedRegions用gf->getRegion(0)/getRegion(1)找"面的另一个体", 正是靠这个。 -
l_edges/l_dirs:边界边 + 每条边的方向(±1)。set(const vector<GEdge*>&)一次性 设边,setOrientations设方向。方向用于把边串成一致的环(第 2 章GEdgeLoop)。 -
edgeLoops:std::vector<GEdgeLoop>------面被哪些环围成(一个外环 + 若干内环)。带孔 的面就是"一个外环 + 多个内环"。 -
edgeCounterparts:周期面的边级对应(用于周期网格复制)。 -
_columns:BoundaryLayerColumns,边界层挤出所需的"列"结构(第 7 章)。 -
embedded_edges/embedded_vertices:嵌入到该面的低维实体。
GFace 的网格容器是 triangles/quadrangles/polygons(继承自更低层的实现或自身成员), 第 5 章 BDS2GMSH 就是往 gf->triangles/gf->quadrangles 里 push 单元。
GFace 的几何契约方法包括由 1.8 节统一列举的 point(u,v)、firstDer、normal、 parFromPoint、reparamOnFace 等。还提供:
cpp
bool normalToPlanarMesh(SVector3 &normal, bool orient = true) const;
用于判断"该面当前网格是否共面",并给出法向------某些算法(如 PolyMeshDelaunayize,第 7 章) 只对平面网格有效。
1.7.4 GRegion(3 维)
GRegion(src/geo/GRegion.h)的字段:
cpp
std::vector<GVertex *> embedded_vertices;
std::vector<GFace *> embedded_faces;
std::vector<GEdge *> embedded_edges;
std::vector<int> l_dirs;
BoundaryLayerColumns _columns;
private:
std::vector<GFace *> l_faces; // 边界面
它对外暴露的网格容器(getNumMeshElementsByType 等,第 129--140 行)覆盖全部三维单元: tetrahedra、hexahedra、prisms、pyramids、polyhedra。getElementTypes(第 117 行) 明确列出该维度的单元族:
cpp
virtual void getElementTypes(std::vector<int> &types) const
{
types.clear();
types.push_back(TYPE_TET); types.push_back(TYPE_PYR);
types.push_back(TYPE_PRI); types.push_back(TYPE_HEX);
types.push_back(TYPE_TRIH); types.push_back(TYPE_POLYH);
}
这里出现了 TYPE_TRIH(三面体,MTrihedron)------第 6 章"六面体/四面体混合网格的共面过渡" 用的过渡单元。
GRegion 还提供体积/惯性/质心计算:
cpp
double computeSolidProperties(std::vector<double> cg, std::vector<double> inertia);
对应脚本命令 Mass/CenterOfMass/MatrixOfInertia(第 3 章 token tMass/tCenterOfMass/ tMatrixOfInertia),以及 OCC 内核的质量属性查询。
GRegion 支持 writeGEO/writePY(第 111--114 行)------把体几何回写成 .geo/.py 脚本。 这是 Gmsh 能"读入任何格式再导出为脚本"的关键 :每个实体子类都实现自己的 writeGEO, 于是导出脚本就是遍历所有实体调用它们的 writeGEO。
1.8 GEntity 的虚接口契约
GEntity 的虚方法非常多(约 60 个),它们定义了"任何几何实体都必须能回答的问题"。按用途 归类理解,比逐条记忆更有效。
1.8.1 维度与边界关系
cpp
virtual int dim() const { return -1; } // 空间维度
virtual GEntity *getParentEntity() { return nullptr; } // 分区实体的父实体
virtual std::list<GRegion *> regions() const; // 相邻/被包围的体
virtual std::vector<GFace *> faces() const; // 相邻/被包围的面
virtual std::vector<GEdge *> const &edges() const; // 相邻/被包围的边
virtual std::vector<GVertex *> vertices() const; // 被包围的顶点
virtual std::vector<GEntity *> boundaryEntities() const = 0; // 边界的实体
boundaryEntities() 是纯虚 (第 233 行)------每个实体必须实现"我的低维边界是谁"。 GEdge 返回起止顶点、GFace 返回 l_edges、GRegion 返回 l_faces。注意方向相反: 低维实体返回的 faces()/regions() 是"我不被谁包围 (即我包围谁)",而高维实体的 boundaryEntities() 是"我的边界是谁"。这一对反问关系贯穿全代码库。
1.8.2 几何契约(最核心的一组)
cpp
virtual GeomType geomType() const { return Unknown; } // 类型
virtual void geomProperties(std::vector<int>&, std::vector<double>&) const; // 内核几何属性
virtual Range<double> parBounds(int i) const; // 参数域
virtual bool continuous(int dim) const; // 参数方向是否连续
virtual bool periodic(int dim) const; // 是否周期
virtual double period(int dim) const; // 周期长度
virtual bool degenerate(int dim) const; // 参数退化
virtual bool haveParametrization(); // 是否有参数化
virtual bool isFullyDiscrete() { return !haveParametrization(); } // 是否纯离散
virtual double tolerance() const { return 1.e-14; } // 建模容差
virtual bool containsPoint(const SPoint3 &pt) const; // 是否含某点
virtual ModelType getNativeType() const { return UnknownModel; }
virtual void *getNativePtr() const { return nullptr; } // 内核原生指针
virtual int getNativeInt() const { return 0; } // 内核原生整数 id
virtual SBoundingBox3d bounds(bool fast = false); // 包围盒
virtual SOrientedBoundingBox getOBB(); // 有向包围盒
这组接口解决的是"实体在几何上是什么":
parBounds/point/firstDer/normal是参数化曲线/曲面的标准接口(第 2 章)。continuous/periodic/period/degenerate描述参数域的特殊性:周期(圆柱 u 方向)、 退化(球面极点、圆锥顶点)。第 5 章参数化网格必须处理这些。haveParametrization/isFullyDiscrete区分"有解析表示"与"只有网格"。纯离散实体不 需要(也不应该)被重新网格化 ------第 4、6 章的isFullyDiscrete()提前返回就是用它。getNativePtr/getNativeType/getNativeInt是通往具体内核的"逃生舱口"(第 2 章)。
1.8.3 网格契约
cpp
virtual void mesh(bool verbose) {} // ★ 生成本实体的网格(多态分派点)
virtual void deleteMesh() {} // 删网格
virtual std::size_t getNumMeshElements() const; // 单元总数
virtual std::size_t getNumMeshElementsByType(int familyType) const;
virtual MElement *getMeshElement(std::size_t index) const;
virtual MElement *getMeshElementByType(int familyType, std::size_t index) const;
virtual void addElement(MElement *e); // 加单元
virtual void removeElement(MElement *e, bool del = false);
virtual void removeElements(bool del = false);
virtual void relocateMeshVertices(const std::vector<double>& pmin, const std::vector<double>& pmax);
virtual bool reorder(int elementType, const std::vector<std::size_t>& ordering);
mesh(bool) 是 Gmsh 依赖倒置的核心 :第 4 章的流水线遍历实体、调用 mesh(true),完全 不关心实体是平面、球面、离散面还是边界层面。每个子类(GEdge/GFace/GRegion)实现自己 的网格生成。getNumMeshElementsByType/getMeshElementByType 按"单元族"(TYPE_TRI 等, 第 1.10 节)索引,让算法可以"只遍历三角形"而不扫全部单元。
1.8.4 周期网格契约
cpp
GEntity *getMeshMaster() const { return _meshMaster; }
void setMeshMaster(GEntity *);
void setMeshMaster(GEntity *, const std::vector<double>&, bool updateCorrespondingVertices = true);
bool updateCorrespondingVertices(double tolerance = -1.);
void copyMasterCoordinates();
virtual void alignElementsWithMaster() {}
周期网格让从实体直接复用主实体的网格。updateCorrespondingVertices(第 319--323 行 注释):"match the nodes with those of the master through the affine transform, and move them onto the images of their master nodes"------按仿射变换把从实体节点移到主实体节点的像上。 setMeshMaster 有两个重载:一个给定变换、一个给定对应关系(互相推导),覆盖了"已知变换" 与"已知对应"两种情形。
1.8.5 可见性 / 选择 / 颜色 / 物理组
cpp
virtual char getVisibility();
virtual void setVisibility(char val, bool recursive = false); // 递归设置子实体
enum SelectionState { SelectNone = 0, SelectOn = 1, SelectShow = 2, SelectHover = 3 };
virtual char getSelection();
static int numSelected;
static std::set<GEntity *> selected; // 全局选中集合
virtual unsigned int getColor();
virtual bool useColor();
virtual void addPhysicalEntity(int physicalTag) { physicals.push_back(physicalTag); }
SelectionState(第 342--347 行)的注释很细,区分了"已选中""选中并显示标记与标签""鼠标 悬停高亮"三种状态(对应 General.MouseHoverHighlight)。numSelected/selected 是静态 全局------绘制代码可以只看"当前选中集合"而不用遍历所有实体。这是"用少量全局状态换渲染性能" 的取舍。
1.8.6 网格尺寸契约
cpp
virtual double getMeshSize() const { return MAX_LC; } // 实体级目标尺寸(MAX_LC = 1e22)
virtual double getMeshSizeFactor() const { return 1.; }
实体本身可以带一个全局尺寸约束(脚本里 Characteristic Length{...} = ...,第 3 章 token tCharacteristic/tLength)。MAX_LC = 1e22(src/geo/GEntity.h 第 19 行)是一个"无穷大"哨兵值,表示"未设约束"。
1.9 MElement:网格单元契约
1.9.1 不可变的编号与分区
MElement(src/geo/MElement.h)的私有字段(第 31--41 行):
cpp
// the id number of the element (this number is unique and is guaranteed never
// to change once a mesh has been generated, unless the mesh is explicitly renumbered)
std::size_t _num;
// the number of the mesh partition the element belongs to (this used to be a
// short, which silently overflowed above 32767 partitions; widening it is
// free, as the bytes were padding)
int _partition;
// a visibility flag
char _visible;
第二条注释是一则真实的 bug 修复记录 :分区号原来用 short,在超过 32767 个分区时静默 溢出;改成 int 是"免费的",因为原来的字节是填充(padding)。这类注释对理解代码演进很有 价值。_num 的"不可变"语义与 MVertex::_num 一致,并配有一个"不该用"的 forceNum:
cpp
// force the immutable number (this should never be used, except when
// explicitly renumbering the mesh)
void forceNum(std::size_t num);
1.9.2 单元类型与阶数
cpp
virtual int getDim() const = 0; // 维度(纯虚)
virtual int getPolynomialOrder() const { return 1; } // 多项式阶
virtual bool getIsAssimilatedSerendipity() const
{ return ElementType::getSerendipity(getTypeForMSH()) > 0; }
virtual bool getIsOnlySerendipity() const
{ return ElementType::getSerendipity(getTypeForMSH()) > 1; }
这里第一次出现了 ElementType::getSerendipity 的实际使用(第 1.10 节详解)。两个判断 区分"可视为 Serendipity""只能是 Serendipity",对应 Serendipity 返回值 1 与 2 的语义。
1.9.3 顶点访问与多语言节点排序
cpp
virtual std::size_t getNumVertices() const = 0;
virtual const MVertex *getVertex(int num) const = 0;
virtual MVertex *getVertex(int num) = 0;
virtual void setVertex(int num, MVertex *v) { Msg::Error("Vertex set not supported..."); }
最值得注意的是一长串"按某格式排序取节点"的接口(第 120--153 行):
cpp
virtual MVertex *getVertexUNV(int num); // I-deas UNV 顺序
virtual MVertex *getVertexMATLAB(int num);
virtual MVertex *getVertexTOCHNOG(int num);
virtual MVertex *getVertexBDF(int num); // Nastran BDF
virtual MVertex *getVertexDIFF(int num);
virtual MVertex *getVertexINP(int num); // Abaqus INP
virtual MVertex *getVertexKEY(int num);
virtual MVertex *getVertexRAD(int num);
virtual MVertex *getVertexNEU(int num);
int getTypeForVTK() const; // VTK cell 类型
std::size_t getNumVerticesVTK() const;
MVertex *getVertexVTK(int num);
这揭示了网格导出的本质困难:不同求解器对同一单元(如 10 节点四面体)的节点排列顺序不同 。 Gmsh 的解法是让每个单元子类提供"按各格式排序"的取节点接口,写出时调用对应格式的版本。 getTypeForVTK 更进一步,直接给出 VTK 的 cell 类型编号(注释说这套接口被 .vtk、.vtu、 .su2 三个写出器共享)。
1.9.4 高阶节点的分类
cpp
virtual int getNumEdgeVertices() const { return 0; } // 边内节点
virtual int getNumFaceVertices() const { return 0; } // 面内节点
virtual int getNumVolumeVertices() const { return 0; } // 体内节点
std::size_t getNumPrimaryVertices() const
{
return getNumVertices() - getNumEdgeVertices() - getNumFaceVertices() - getNumVolumeVertices();
}
注释坦承命名会让人困惑:
These functions should be renamed to getNumInternal*Vertices(), to avoid confusion with getEdgeVertices() and getFaceVertices()
即它们是"边/面/体内部的 高阶节点数",而非"这个单元有几条边"。getNumPrimaryVertices 由此推出"一阶角节点数"------这是区分"角点"与"高阶点"的关键,很多算法只处理角点(第 9 章 getNumCornerNodes)。
1.9.5 边与高阶边
cpp
virtual int getNumEdges() const = 0;
virtual MEdge getEdge(int num) const = 0;
virtual MEdgeN getHighOrderEdge(int num, int sign); // 高阶边(MEdgeN)
MEdgeN getHighOrderEdge(const MEdge &edge);
MEdge 是低维边(第 1.5.6 节全局边表的元素),MEdgeN 是高阶边 (沿边的全部节点)。 getHighOrderEdge 让"从一条低维边取出它在某单元里的高阶表示"成为可能------这是高阶网格与 边界恢复都要用的。
1.10 单元类型编号体系:GmshDefines.h
1.10.1 两套编号:TYPE_* 与 MSH_*
src/common/GmshDefines.h 定义了两套编号体系,它们的分工 是理解数值内核的钥匙。
第一套:TYPE_*(父类型 / family type)(第 68--82 行):
cpp
#define TYPE_PNT 1
#define TYPE_LIN 2
#define TYPE_TRI 3
#define TYPE_QUA 4
#define TYPE_TET 5
#define TYPE_PYR 6
#define TYPE_PRI 7
#define TYPE_HEX 8
#define TYPE_POLYG 9
#define TYPE_POLYH 10
#define TYPE_XFEM 11
#define TYPE_MINI 12
#define TYPE_TRIH 13
#define TYPE_MAX_NUM 13 // keep this up-to-date when adding new type
只有 13 个值,对应"几何形状的族":点、线、三角形、四边形、四面体、金字塔、棱柱、六面体、 多边形、多面体、XFEM(扩展有限元)、迷你元、三面体。算法层(雅可比、形函数、积分)通常 只关心族,不关心阶。
第二套:MSH_*(MSH 文件格式类型) (第 84--228 行):这是 MSH 文件格式的官方编号 , // Element types in .msh file format (numbers should not be changed)。它覆盖从 1 阶到 10 阶 的巨量组合,按"几何形状 × 阶数 × 完备/不完备族"展开。分类阅读:
基础一阶(第 85--99 行):
cpp
#define MSH_LIN_2 1
#define MSH_TRI_3 2
#define MSH_QUA_4 3
#define MSH_TET_4 4
#define MSH_HEX_8 5
#define MSH_PRI_6 6
#define MSH_PYR_5 7
#define MSH_PNT 15
二阶完备与 Serendipity(第 92--103 行):
cpp
#define MSH_LIN_3 8 // 3 节点线
#define MSH_TRI_6 9
#define MSH_QUA_9 10 // 完备四边形(含中心点)
#define MSH_TET_10 11
#define MSH_HEX_27 12 // 完备六面体
#define MSH_PRI_18 13
#define MSH_PYR_14 14
#define MSH_QUA_8 16 // Serendipity 四边形(无中心点)
#define MSH_HEX_20 17 // Serendipity 六面体
#define MSH_PRI_15 18
#define MSH_PYR_13 19
三角形高阶(第 104--109、126--140 行)覆盖到 66 节点(10 阶):
cpp
#define MSH_TRI_9 20
#define MSH_TRI_10 21
...
#define MSH_TRI_66 46
并且有带 I 后缀的 Serendipity 变体:
cpp
#define MSH_TRI_15I 24
#define MSH_TRI_21I 53
#define MSH_QUA_16I 40
#define MSH_QUA_36I 60
四面体/六面体/棱柱/金字塔的高阶(第 151--217 行)用注释明确标注族别:
cpp
// TETS COMPLETE (6->10)
#define MSH_TET_84 71
#define MSH_TET_120 72
...
#define MSH_TET_286 75
// TETS INCOMPLETE (6->10)
#define MSH_TET_34 79
...
// HEXES COMPLETE (3->9)
#define MSH_HEX_64 92
...
// PRISMS COMPLETE (5->9)
#define MSH_PRI_126 106
...
// PYRAMIDS COMPLETE (3->9)
#define MSH_PYR_30 118
...
特殊类型(第 218--228 行):
cpp
// Additional types
#define MSH_PYR_1 132
#define MSH_PNT_SUB 133 // 子/从属元(sub-entity)
#define MSH_LIN_SUB 134
#define MSH_TRI_SUB 135
#define MSH_TET_SUB 136
#define MSH_TET_16 137
#define MSH_TRI_MINI 138
#define MSH_TET_MINI 139
#define MSH_TRIH_4 140 // Tri hedron -- One quad face and two triangular faces
// -- made for connecting hexes and tets
#define MSH_MAX_NUM 140 // keep this up-to-date when adding new type
MSH_TRIH_4 的注释再次确认了三面体的用途:连接六面体与四面体(混合网格过渡,第 6 章)。
1.10.2 父子查询:ElementType 命名空间
查询接口在 src/numeric/ElementType.h:
cpp
int getParentType(int type); // MSH 类型 → TYPE_*
int getOrder(int type); // 阶数
int getDimension(int type); // 维度
int getNumVertices(int type); // 节点数
int getSerendipity(int type); // >0:Serendipity 族;<2:Normal 族;1:两者皆可
int getType(int parentType, int order, bool serendip = false); // 反查 MSH 类型
int getPrimaryType(int type); // 一阶类型
std::string nameOfParentType(int type, bool plural = false);
getParentType(src/numeric/ElementType.cpp)是一张覆盖 全部 MSH_* 的巨型 switch。例如线(第 14--24 行)把 MSH_LIN_2 ... MSH_LIN_11 全归 TYPE_LIN, 三角形(第 25--43 行)把 MSH_TRI_3 ... MSH_TRI_66 全归 TYPE_TRI,四边形(第 44--63 行) 覆盖到 MSH_QUA_121。
getSerendipity 的返回值语义(注释"Gives > 0 if element MSH type is in Serendipity Family. Gives < 2 if element MSH type is in 'Normal' Family. 1 is for element MSH type that is either Serendipity or not !")对应第 1.9.2 节 getIsAssimilatedSerendipity(>0)与 getIsOnlySerendipity(>1)的判断。
"父类型 + 阶数 + 族别"是数值内核的寻址三元组 (第 8 章 FuncSpaceData):给定它,就能 确定形函数、积分规则、雅可比多项式。MSH 编号只是"磁盘上的表示",算法内部几乎总是先转成 这个三元组。
1.10.3 单元族与导出格式
getTypeForVTK(第 1.9.3 节)把单元映射到 VTK cell 类型;MSH_* 则是 MSH 格式的类型。 同一单元在不同场合用不同编号 :磁盘用 MSH_*,算法内部用 TYPE_*+阶,导出 VTK 用 VTK 类型。这三套编号的转换散落在 ElementType、各单元子类、各 I/O 写出器里。理解这一点,就能 明白为什么 Gmsh 的 I/O 代码里到处是 switch。
1.11 MVertex:网格节点
1.11.1 字段:不可变编号与可变索引
MVertex(src/geo/MVertex.h)的字段(第 25--43 行):
cpp
// the id of the vertex: this number is unique and is guaranteed never to
// change once a vertex has been created, unless the mesh is explicitly renumbered
std::size_t _num;
// a vertex index, used during mesh generation or for some IO operations ...
// By convention, vertices with negative indices are not saved.
long int _index;
// a visibility and polynomial order flags
char _visible, _order;
// the cartesian coordinates of the vertex
double _x, _y, _z;
// the geometrical entity the vertex is associated with
GEntity *_ge;
这里有两个编号,区别很关键,注释也写得很细:
_num(不可变):全局唯一、创建后不变(除非显式重编号)。它是节点在文件里的编号、 也是跨实体的唯一标识。_index(可变) :网格生成期或某些 I/O 用的"索引",不保证唯一 、重编号后会变。 注释给出三条规则:- "vertices with negative indices are not saved"------负索引表示"不保存",是一个哨兵约定;
- "On some architectures _index will be smaller than _num: this is OK, as _index is only used for partial indexing in 2D and 3D meshing, or for IO formats that don't support 64 bit indexing"------
_index是long int,在 32 位平台上可能小于 64 位的_num, 但因为它只用于局部索引或 32 位 I/O,所以没问题; - "_index is destined to be eventually removed anyway"------作者已计划最终删掉它。
这种"作者明确标注将来要删的字段"在成熟项目里很常见,它避免了后来者误以为该字段是必需的。
_ge 表示节点所属的几何实体。结合第 1.7 节的"实体持有 mesh_vertices",Gmsh 是双向 维护这个关系:实体持有节点列表,节点也知道自己在哪个实体上。
1.11.2 参数化访问
cpp
// get/set ith parameter
virtual bool getParameter(int i, double &par) const { par = 0.; return false; }
virtual bool setParameter(int i, double par) { return false; }
基类默认"无参数",派生类覆盖。两条约定:
- 返回值表示是否成功 :普通三维节点没有参数 → 返回
false; i是参数方向下标 :曲线只有i=0(单参数 u),曲面有i=0,1( u,v)。
1.11.3 多格式写出
MVertex 的 I/O 方法列表(第 114--134 行)长得惊人:
cpp
void writeMSH(FILE *fp, ...); void writeMSH2(...);
void writePLY2(FILE *fp); void writeVRML(FILE *fp, ...);
void writeUNV(...); void writeVTK(...); void writeMATLAB(...);
void writeTOCHNOG(...); void writeMESH(...); void writeOFF(...);
void writeNEU(...); void writeBDF(...); void writeINP(...); void writeKEY(...);
void writeRAD(...); void writeDIFF(...); void writeSU2(...);
这是"一个节点、十几种文件格式"的现实。每种格式的坐标精度、二进制/文本、缩放、参数 保存方式都不同 ,因此不能统一,只能各写一份。第 9 章会看到后处理也有类似的多格式 I/O。 Gmsh 之所以"什么都能读、什么都能写",代价就是这一大批看似冗余的 writeXxx。
1.11.4 参数顶点:MEdgeVertex 与 MFaceVertex
关键派生类:
MEdgeVertex(第 137--160 行):
cpp
class MEdgeVertex : public MVertex {
protected:
double _u, _lc; // 参数 u 与目标尺寸 lc
public:
MVertexBoundaryLayerData *bl_data;
MEdgeVertex(double x, double y, double z, GEntity *ge, double u,
std::size_t num = 0, double lc = -1.0);
virtual bool getParameter(int i, double &par) const { if(i != 0) return false; par = _u; return true; }
virtual bool setParameter(int i, double par) { ... }
};
曲线上的节点带参数 _u(在曲线上的位置)、目标尺寸 _lc,以及边界层数据 bl_data。
MFaceVertex (在 src/geo/MVertex.h 后续)------曲面上的节点带 (u,v) 两个参数。第 5 章 BDS2GMSH 里就写成:
cpp
v[i] = new MFaceVertex(n[i]->X, n[i]->Y, n[i]->Z, gf, n[i]->u, n[i]->v);
为什么需要参数顶点? 因为曲面网格算法在参数平面里工作(第 5 章)。算法只知道 (u,v); 要转回三维就用 gf->point(u,v)。把 (u,v) 存在节点上,就能:
- 在优化时把节点"投影回曲面"(第 7 章解缠);
- 在导出时保存参数坐标(
saveParametric选项); - 在周期/相邻面之间转换(
reparamOnFace)。
MVertexBoundaryLayerData 与 MVertexRTree(src/geo/MVertexRTree.h) 是配套设施:前者存边界层节点的"列"信息,后者是节点的 R 树空间索引(用于快速"找最近的 节点")。
1.12 全局上下文 Context
1.12.1 单例与规模
Gmsh 大量使用全局单例 而非依赖注入。核心是 src/common/Context.h 的 Context 类,以 CTX::instance() 访问。它保存了数百个运行期参数,按类别分:
text
general.* 通用(终端、批处理、线程数、版本...)
geom.* 几何(容差、匹配容差、OCC 快速投影...)
mesh.* 网格(各维算法、尺寸因子、优化、线程数、重试...)
solver.* 求解器
post.* 后处理
view[n].* 每个视图的显示选项
这种"一个大对象装下所有可调参数"的设计好处是:任何模块都能就地读取参数,无需传递配置 对象。代价是全局可变状态------线程安全要额外小心。
1.12.2 变更时间戳:让缓存失效
Context 维护一张"变更计数"表(第 178--220 行附近),用于判断"几何/网格/实体是否变了, 由此派生的缓存是否要重建"。典型计数器:
CTX::geomChanged():几何变更(第 1.5.7 节GModel::add/remove调它);CTX::meshChanged():网格变更;CTX::entityColorsChanged():实体颜色变更(GEntity::setColor调它);CTX::entityVisibilityChanged():可见性变更(GEntity::setVisibility调它)。
为什么需要这个? 因为 Gmsh 里"由数据派生的缓存"无处不在:渲染的顶点数组、后处理的 八叉树、尺寸场、包围盒......如果每次访问都重算,性能会崩溃;如果从不重算,就会显示陈旧数据。 变更计数器提供了一个廉价的"全局性质版本号":缓存记下自己构建时的计数,发现变了就重建。 这是"观察者模式"的极简替代。
1.12.3 串行锁
CTX::instance()->lock 是一个全局锁。第 4 章 GenerateMesh 用它保证网格生成不可重入:
cpp
if(CTX::instance()->lock) { Msg::Info("I'm busy! Ask me that later..."); return; }
CTX::instance()->lock = 1;
它也是并行失败后"解冻"的关键------第 4 章异常处理里会 CTX::instance()->lock = 0; 再抛异常。
1.13 选项系统 Options
1.13.1 面向用户的选项 = 字符串键 + 存取函数
src/common/Options.h 把每个面向用户的选项声明成一个函数。字符串 选项签名如下(第 25--27 行):
cpp
#define OPT_ARGS_STR int num, int action, const std::string &val
#define OPT_ARGS_NUM int num, int action, double val
#define OPT_ARGS_COL int num, int action, unsigned int val
std::string opt_general_axes_label0(OPT_ARGS_STR);
std::string opt_general_version(OPT_ARGS_STR);
std::string opt_geometry_occ_target_unit(OPT_ARGS_STR);
std::string opt_solver_name(OPT_ARGS_STR);
std::string opt_view_name(OPT_ARGS_STR);
每个选项对应三个参数:num(数组下标,用于 View[0].xxx 这类带索引的选项)、action (做什么)、val(值)。返回 std::string 是因为 get 需要返回字符串。
1.13.2 action 是位掩码
第 12--27 行:
cpp
#define GMSH_SET (1 << 0)
#define GMSH_GET (1 << 1)
#define GMSH_GUI (1 << 2)
#define GMSH_SET_DEFAULT (1 << 3)
#define GMSH_GET_DEFAULT (1 << 4)
#define GMSH_SESSIONRC (1 << 0)
#define GMSH_OPTIONSRC (1 << 1)
#define GMSH_FULLRC (1 << 2)
#define GMSH_DEPRECATED (1 << 3)
// action is a combination of GMSH_SET, GMSH_GET, GMSH_GUI
同一个函数既负责 set、get、gui,又负责 set/get default ,靠 action 的位组合 区分行为。于是字符串选项和数值选项共用同一套读写协议。这是用位掩码把"多个操作"塞进一个 入口的经典手法。
第二组(SESSIONRC/OPTIONSRC/FULLRC/DEPRECATED)是选项来源:
GMSH_SESSIONRC:会话级(本次运行临时改的);GMSH_OPTIONSRC:选项文件(持久化);GMSH_FULLRC:全部(会话 + 文件);GMSH_DEPRECATED:已废弃选项。
这让 Gmsh 能区分"临时改的"与"写进配置的",在退出/重载时正确复原------也解释了为什么有些 选项改了不会持久化。
1.13.3 从 API 到选项的路径
API 的 gmsh::option::setNumber("Mesh.Algorithm", 6) 最终经过:
SplitOptionName("Mesh.Algorithm", c, n, i)把"Mesh.Algorithm"拆成category="Mesh"、name="Algorithm"、索引i;GmshSetOption(c, n, value, i)找到对应opt_*函数并调用。
实现见 src/common/gmsh.cpp(第 227--236 行):
cpp
GMSH_API void gmsh::option::setNumber(const std::string &name, const double value)
{
if(!_checkInit()) return;
std::string c, n; int i;
SplitOptionName(name, c, n, i);
if(!GmshSetOption(c, n, value, i))
Msg::Error("Could not set option '%s'", name.c_str());
}
DefaultOptions.h 则保存每个选项的默认值,General.RestoreDefaults 会据此复位。
1.14 API 层与代码生成
1.14.1 薄壳与生命周期
src/common/gmsh.cpp(约 9000 行)是公开 API 的实现,但它是薄壳 : 每个函数只做"参数搬运 + 生命周期检查 + 转调底层 GmshXxx()"。入口:
cpp
static int _initialized = 0;
static int _argc = 0;
static char **_argv = nullptr;
static bool _checkInit()
{
if(!_initialized) { CTX::instance()->terminal = 1; Msg::Error("Gmsh has not been initialized"); return false; }
if(!GModel::current()) { Msg::Error("Gmsh has no current model"); return false; }
return true;
}
GMSH_API void gmsh::initialize(int argc, char **argv,
const bool readConfigFiles, const bool run)
{
if(_initialized) { Msg::Warning("Gmsh has aleady been initialized"); return; }
if(run) new GModel(); // 运行 app 时先建模型
if(GmshInitialize(argc, argv, readConfigFiles, false)) {
_initialized = 1; _argc = argc; _argv = new char *[_argc + 1];
for(int i = 0; i < argc; i++) _argv[i] = argv[i];
#if defined(HAVE_FLTK)
if(FlGui::available()) return; // GUI 已在运行
#endif
if(run) {
if(CTX::instance()->batch) { if(!Msg::GetGmshClient()) CTX::instance()->terminal = 1; GmshBatch(); }
else GmshFLTK(argc, argv);
} else {
CTX::instance()->abortOnError = 2; // 库模式:出错即抛异常
CTX::instance()->terminal = 1; // 库模式:消息打到终端
}
return;
}
Msg::Error("Something went wrong when initializing Gmsh");
}
这段代码把 API 的两条运行路径讲清楚了:
run=true(应用模式) :与直接运行gmsh可执行文件一致------批处理走GmshBatch(), 否则进 GUI(GmshFLTK)。run=false(库模式) :作为被链接的库使用------abortOnError = 2(出错抛异常)、terminal = 1(消息打到终端)。第 10 章会看到,这正是 Python/Julia 里gmsh.initialize()的默认行为。
_checkInit 在每个 API 入口做"双检查"(已初始化 + 当前模型存在),是 API 的防御性风格。
1.14.2 一份规格,五种语言
C++ API 的声明在 api/gmsh.h,文件头注明它是由 api/gen.py 生成 的。 生成器与规格在 api/GenApi.py、api/gen.py。GenApi.py 的 arg 类(第 16--49 行)为每个 参数同时维护五种表示:
python
class arg:
def __init__(self, name, value, python_value, julia_value, cpp_type, c_type, out):
self.cpp = cpp_type + " " + name + ((" = " + value) if value else "")
self.c = c_type + " " + name
self.python_arg = ""; self.python_return = ""; ...
self.julia_ctype = ""
self.fortran_args = [name]; self.fortran_c_args = [name]
...
输入类型工厂定义每种语言的映射。以 ibool 为例(第 54--64 行):
python
a.python_arg = "c_int(bool(" + name + "))"
a.cwrap_arg = "(int)" + name
a.julia_ctype = "Cint"
a.fortran_types = ["logical, intent(in)"]
a.fortran_c_api = ["integer(c_int), value, intent(in)"]
生成产物:
text
api/gmsh.h C++ 头(namespace gmsh)
api/gmsh.h_cwrap 用于生成 C 包装的中间头
api/gmshc.h C 头
api/gmshc.cpp C 实现(extern "C" 包装 C++)
api/gmsh.py Python(基于 ctypes)
api/gmsh.jl Julia(ccall)
api/gmsh.f90 Fortran(iso_c_binding)
(JS) 由 WASM 构建产出
1.14.3 各语言绑定的实现机理
- C++ :直接调用
src/common/gmsh.cpp的实现。 - C (api/gmshc.cpp):为每个 C++ 函数写一个
extern "C"包装, 把std::string转const char*、把异常转错误码、std::vector转数组。生成器用arg.c_pre/c_post/cwrap_pre/cwrap_post插入这段胶水。 - Python (api/gmsh.py):用
ctypes加载libgmsh,为每个函数构造argtypes/restype。 - Julia (api/gmsh.jl):用
ccall调 C 接口。 - Fortran (api/gmsh.f90):用
iso_c_binding调 C 接口。 - JavaScript :Emscripten/WASM 构建(
utils/wasm/)。
"一份规格、多语言发射"是 Gmsh 能以极小边际成本支持五种语言的根本原因,也是它能成为 "可嵌入库"的关键。对任何计划长期维护多语言 SDK 的项目,这套做法都值得借鉴。
1.14.4 公开 API 与私有 API
第 1.3.5 节提到 ENABLE_PRIVATE_API。两者的区别是:
- 公开 API (
api/):稳定、生成、文档化、跨语言一致。只暴露"用户级"概念 (模型、网格、选项、后处理)。 - 私有 API (
src/头文件):泄露内部 C++ 类型(PView、PViewDataList、MElement......), 仅供专家与插件作者使用。第 9 章的共享库插件正基于它构建。
CMake 第 150--152 行的警告明确劝退普通用户使用它。这条界线保持了"对外契约稳定"与"内部 自由演进"的平衡。
1.15 消息、进度与 Onelab
1.15.1 Msg:日志与通信中枢
src/common/GmshMessage.h 的 Msg 类是所有模块的日志、进度、 错误计数与外部通信中枢。它的静态状态(第 30--69 行)包括:
cpp
// MPI rank/size、进度条、计时器、错误计数、命令行参数、
// 远程 Gmsh 客户端、Onelab 客户端 ...
对外接口(第 76--181 行)覆盖:
- 消息 :
Msg::Error、Msg::Warning、Msg::Info、Msg::Debug; - 进度 :
Msg::StartProgressMeter、Msg::ProgressMeter、Msg::StopProgressMeter(第 4 章流水线里到处可见Msg::ProgressMeter(...)); - 错误计数 :
Msg::ResetErrorCounter、Msg::GetErrorCount、Msg::PrintErrorCounter(abortOnError就是据此判断是否中止); - 计时 :
Msg::Cpu()、Msg::TimeOfDay(); - Onelab:参数读写、客户端运行、物理组导入;
- MPI :
Msg::GetCommRank/Size(虽ENABLE_MPI默认关,但仍提供)。
Msg::Info("%d nodes %d elements", ...)(第 4 章 GenerateMesh 末尾)这类调用遍布全代码库。 把所有"与外界沟通"的出口集中在一个类,至少让日志/进度/错误的处理策略(重定向到文件、 GUI 状态栏、终端)只在一处实现。
1.15.2 Onelab 集成
src/common/onelab.h 是 Gmsh 与 Onelab 参数系统的集成层。核心数据 是参数基类 parameter(第 47--89 行),携带:
text
name, label, help 标识与说明
changed flag 是否被改动
visibility, read-only 可见性与只读属性
client change tracking 被哪个客户端改动
配套接口(第 90--136 行):setChanged、getChanged、addClient、getNumClients 实现 参数变更通知与依赖追踪:一个参数变了,依赖它的客户端会被标记,从而触发重算。
相关文件:onelabUtils.h/.cpp(工具)、GmshSocket.h(socket 协议)、GmshRemote.cpp、 gmshLocalNetworkClient.cpp(本地网络客户端)。第 10 章会讲这套机制如何让 Gmsh 启动外部 求解器、传网格/参数、收结果。
1.16 命令行、程序入口与文件格式
1.16.1 程序入口
src/common/Main.cpp 的 main() 负责:
- 初始化(
GmshInitialize); - 解析命令行(src/common/CommandLine.cpp, 如
-clscale、-3、-o等,t5.geo注释里就出现了-clscale); - 根据批处理/交互模式分别进入
GmshBatch()或GmshFLTK()。
API 的 gmsh::initialize(..., run=true) 正是复用了这条路径(第 1.14.1 节)。
1.16.2 平台抽象
src/common/OS.cpp(src/common/OS.h)、CreateFile.cpp、 OpenFile.cpp、StringUtils.cpp、ListUtils.cpp、MallocUtils.cpp、Hash.h 提供跨平台 基础设施:文件创建/打开、字符串工具、自研容器(List_T、Tree_T)、内存分配封装。它们 是几何/网格层大量使用的底层工具(List_T/Tree_T 在第 2 章的 GEO_Internals 里出现)。
1.16.3 文件格式:一份长清单
src/common/GmshDefines.h 开头是一长串文件格式编号 (第 9--66 行):
cpp
// IO file formats (numbers should not be changed)
#define FORMAT_MSH 1
#define FORMAT_UNV 2
#define FORMAT_VTU 3
#define FORMAT_GEO 8
#define FORMAT_STL 27
#define FORMAT_BDF 31
#define FORMAT_CGNS 32
#define FORMAT_MED 33
#define FORMAT_STEP 36
#define FORMAT_IGES 37
#define FORMAT_INP 39
#define FORMAT_OBJ 57
#define FORMAT_MP4 58
...
这份清单本身就是 Gmsh "格式枢纽"定位的证据:网格(MSH/UNV/STL/INP/BDF/MED/CGNS)、几何 (GEO/STEP/IGES/BREP)、后处理(POS/VTK/VTU/X3D)、图像视频(BMP/GIF/JPEG/PNG/MPEG/MP4)、 矢量图(PS/EPS/SVG/TIKZ)......全都在列。每种格式在 src/geo/GModelIO_<FMT>.cpp 或 src/post/PViewIO_<FMT>.cpp 里有对应的读写实现(第 2、9 章)。
GModelIO_MSH*.cpp 单独有四个文件(MSH/2/3/4),说明 MSH 格式本身有多个不兼容版本, Gmsh 必须同时支持读写与互操作(第 2 章详述)。
1.17 实体子类谱系:按"来源/状态"正交划分
理解了 GEntity 的契约后,一个自然的疑问是:四个维度基类之下,究竟有多少子类?答案揭示 了 Gmsh 用两条正交的轴线给实体分类。
1.17.1 轴线一:几何内核来源
gmsh*(自研 GEO 内核) :gmshVertex、gmshEdge、gmshFace、gmshRegion,以及 特殊曲面gmshSurface/gmshQuadrics。getNativeType()返回GmshModel。 第 2 章会看到gmshEdge内部持有Curve *_c、gmshFace内部持有解析曲面。OCC*(OpenCASCADE) :OCCVertex、OCCEdge、OCCFace、OCCRegion。getNativeType()返回OpenCascadeModel,getNativePtr()返回TopoDS_*/Geom_*。 第 2 章会看到OCCFace如何包装TopoDS_Face+BRepAdaptor_Surface。
此外还有 ACIS/Parasolid 内核(ACIS_Internals/Parasolid_Internals,需对应商业库), 它们复用同样的 *Vertex/*Edge/*Face/*Region 命名约定。
1.17.2 轴线二:工程状态
discrete*(纯离散) :discreteVertex、discreteEdge、discreteFace、discreteRegion。 这些实体没有解析参数化 ,只有网格------来自 STL、网格文件的导入,或网格生成后自动创建的 几何。它们的geomType()返回DiscreteCurve/DiscreteSurface/DiscreteVolume,isFullyDiscrete()为真,因此不会被重新网格化(第 1.8.2 节)。partition*(分区实体) :partitionVertex、partitionEdge、partitionFace、partitionRegion。第 7 章 METIS 分区的产物。它们通过getParentEntity()指向原始实体。ghost*(影子实体) :ghostEdge、ghostFace、ghostRegion。分区交界处为邻居保存的 单元副本。与partition*一样服务于并行有限元的边界交换。overlap*(重叠实体) :overlapFace、overlapRegion。对应第 1.5.3 节的OverlapManager,用于重叠网格(overset)。
1.17.3 两条轴线的组合意义
于是"一个实体"由"哪个内核 + 什么状态"共同描述。例如:
gmshFace:自研内核 + 正常几何(可参数化、可网格化);OCCFace:CAD 内核 + 正常几何;discreteFace:读完网格文件后自动创建的"无解析"面;partitionFace:自研/离散面在分区后的"切片";ghostFace:分区面在邻居进程里的影子。
这套划分的好处 :网格算法只需面对"正常几何实体"(gmsh*/OCC*),分区/影子/重叠 由专门代码处理,互不干扰。第 4 章的 GFace::mesh、第 6 章的 meshGRegion::operator() 都会 对 isFullyDiscrete()/getNativeType() 做快速判断,正是依赖这条轴线。
1.17.4 子类需要覆盖的关键虚函数
每个子类至少要覆盖这几类:
getNativeType()/getNativePtr()------声明内核身份、交出原生指针;geomType()------声明几何类型(决定getTypeString的显示);- 几何求值 ------
point/firstDer/normal/secondDer/curvature(曲线/曲面); reparamOnFace------相邻面之间的参数转换;writeGEO/writePY------回写成脚本;discretize------按容差离散(STL 导出、显示、几何计算都要用)。
以 gmshEdge(src/geo/gmshEdge.h)为例,它把 3--6 项全部实现, 并在 getNativePtr() 里 return _c;。这六项就是"一个新内核要接入 Gmsh"必须完成的作业 清单------如果你想为 Gmsh 加一个几何内核,实现这六项是关键路径。
1.18 一次完整调用链:从脚本 Point 到网格节点
为把本章的抽象串起来,我们追踪一个最小脚本片段的全生命周期:
geo
lc = 1e-2;
Point(1) = {0, 0, 0, lc};
Line(1) = {1, 2};
Curve Loop(1) = {4, 1, -2, 3};
Plane Surface(1) = {1};
1.18.1 解析阶段(第 3 章)
gmsh_yylex(词法)把 Point 识别为 tPoint,Line 识别为 tCurve......(第 3 章的 token 表)。gmsh_yyparse(语法)在语义动作里:
- 对
Point(1) = {0,0,0,lc}:取出 tag、坐标、lc,调用GModel::current()->getGEOInternals()->addVertex(tag, x,y,z, lc)(第 2 章GEO_Internals); - 对
Line(1) = {1,2}:addLine(tag, 1, 2); - 对
Curve Loop(1) = {...}:addCurveLoop(tag, curveTags, reorient); - 对
Plane Surface(1) = {1}:addPlaneSurface(tag, wireTags)。
注意:这一步只写进内核容器(Tree_T),还没有创建 GEntity 对象。
1.18.2 同步阶段(第 2 章)
脚本被执行后(或用户显式同步时),GEO_Internals 的 _changed 标志(第 2 章第 29/51 行) 为真,于是 Gmsh 把内核数据"提升"为 GModel 的 GEntity:
- 点容器 → 创建
gmshVertex,加入GModel::vertices集合; - 曲线容器 → 创建
gmshEdge,解析起止点、设置_v0/_v1,加入edges; - 曲面容器 → 创建
gmshFace,解析边界环、设置l_edges/l_dirs,加入faces。
同时 GModel 维护 tag→实体 的查找(getFaceByTag 等,靠第 1.6.6 节的透明比较器)。
1.18.3 网格阶段(第 4 章)
模拟 Mesh 1(只做一维)后调用 GenerateMesh(m, 1):
CTX::instance()->lock加锁、srand(randomSeed)、SetOrder1;Mesh0D:给每个gmshVertex建MVertex+MPoint;Mesh1D:遍历GModel::edges,对每条gmshEdge调ed->mesh(true)(第 1.8.3 节的 多态分派点);meshGEdge::operator()→meshGEdgeProcessing(第 4 章):积分求长度与段数N,生成N-1个MEdgeVertex,写入ge->lines与ge->mesh_vertices;renumberMeshCanonically:把并行创建的节点重编号为确定顺序。
1.18.4 全程涉及的类
把这条链上出现的类按层排列:
text
parser 层 : gmsh_yylex / gmsh_yyparse(Gmsh.l / Gmsh.y)
common 层 : CTX(参数、锁、随机种子)、Msg(进度)
geo 层 : GModel(模型容器)、GEO_Internals(脚本内存镜像)、
gmshVertex/gmshEdge/gmshFace(实体)、MVertex/MEdgeVertex/MPoint/MLine(网格数据)
mesh 层 : GenerateMesh、Mesh0D/Mesh1D、meshGEdge、meshGEdgeProcessing
numeric层: Integration(积分求长度)、GaussIntegration(若涉及数值积分)
这条链是理解 Gmsh 的"最小闭环":脚本 → 内核 → 实体 → 网格。第 2--4 章会分别把每一段 放大细讲。
1.19 架构取舍与横向对比
最后,把 Gmsh 的关键架构决策与它们的代价/收益列成一张表,并与其他网格工具做简要对比。
1.19.1 关键取舍
| 决策 | 收益 | 代价 |
|---|---|---|
几何与网格不分离(GEntity 直接持网格) |
归属清晰、跨维依赖简单、无需映射表 | GEntity 职责膨胀、改几何要删网格 |
全局单例 CTX |
任何模块就地读参数、无需传配置 | 全局可变状态、线程安全需额外小心 |
| 多几何内核(GEO/OCC/ACIS/Parasolid) | 无依赖可跑、有依赖获工业 B-Rep | 语义差异(同一命令结果不同)、适配代码量大 |
| 平面数组网格内核(flat)取代对象式 | 缓存友好、易并行 | 迁移期两套内核并存、维护成本 |
| 生成式多语言 API | 五种绑定零边际成本 | 生成器本身要维护、调试生成代码较难 |
| 自研脚本 DSL + flex/bison | 无运行时依赖、语法可控 | 循环靠"回读"、无 AST 优化空间 |
大量 #if defined(HAVE_*) 条件编译 |
功能可选、依赖可关 | 组合爆炸、某些路径缺测 |
1.19.2 与其他工具的概念对比
- TetGen :专注三维 Delaunay,功能窄而精。Gmsh 反而把 TetGen 的边界恢复算法内嵌使用 (第 6 章),自己负责"几何 + 一到二维 + 后处理"。这解释了为什么同一功能可能有两套实现 (自带 del3d 与 TetGen 恢复)。
- Netgen :自有几何内核 + 前沿推进,与 Gmsh 是"同代产品"。Gmsh 把 Netgen 作为三维前沿 算法的一种可选后端 (
ALGO_3D_FRONTAL)。 - OpenFOAM:以求解器为中心,网格工具(blockMesh/snappyHexMesh)服务于 CFD。Gmsh 与它 的定位差异在于"Gmsh 是前处理枢纽,不绑定任何求解器"。
- MMG :专注各向异性网格适应(remeshing)。Gmsh 把 MMG3D 作为各向异性体网格后端 (
ALGO_3D_MMG3D)。
从这张对比可以看出 Gmsh 的独特策略:它不追求每个算法都是世界第一,而是追求"把同类工具 的优点都集成进来,并提供一个统一的几何/脚本/API 外壳"。这既是它能成为"网格入口"的原因, 也解释了其代码库为何如此庞杂------庞杂是"集成策略"的必然代价。
1.20 GModel 的实现细节与常用操作
前面从"数据结构"角度看了 GModel;这里从"用法"角度补全它的实现细节,这些是读后续章节 时最常遇到的 API。
1.20.1 当前模型机制
cpp
// the static list of all loaded models
static std::vector<GModel *> list;
static GModel *current(int index = -1); // 返回当前模型;index>=0 时同时设为当前
static int setCurrent(GModel *m);
static int getCurrentIndex() { return _current; }
static void setCurrentIndex(int index) { _current = index; }
int setAsCurrent() { return setCurrent(this); }
static GModel *findByName(const std::string &name, const std::string &fileName = "");
要点:
- 多模型同时存在 (
list),但只有一个是"当前"。current()无参时返回list[_current]。 getCurrentIndex()/setCurrentIndex()成对出现,用于"临时切到别的模型处理完再切回来"。 注释(第 224--227 行)明确了这个用途:"the index of the current model as set (-1 for the last one), to restore it after using a model temporarily"。findByName的第二个参数语义很绕(注释:"return model only if it does not have a link to the fileName"),用于避免"同一文件被当成两个模型"。
1.20.2 销毁与清理
cpp
void destroy(bool keepName = false); // 清空模型(可选保留名字)
void deleteMesh(); // 删掉实体里的网格
void deleteMesh(const std::vector<GEntity *> &entities);
void destroyMeshCaches(bool keepVertexCaches = false); // 删缓存(改网格后必须调)
void pruneMeshVertexAssociations(); // 重建节点-实体关联
void deleteVertexArrays(); // 删渲染顶点数组
void deleteGeometryVertexArrays();
几个易混点的区别:
destroyMeshCaches注释(第 364--365 行):"this must be called when the mesh is changed"------ 改网格后必须调用 ,否则按 tag 的缓存会指向陈旧数据。keepVertexCaches允许"只加了单元、 没动节点"时保留节点缓存。deleteMesh是"删掉实体里的网格并清缓存",destroyMeshCaches只清缓存。pruneMeshVertexAssociations会"先解除所有节点-实体关联,再重建" (_associateEntityWithMeshVertices+_storeVerticesInEntities,第 176--188 行声明的 两个 protected 方法)------它用于网格拓扑被大幅改动后的重新归类。
1.20.3 批量查询实体
cpp
void getEntities(std::vector<GEntity *> &entities, int dim = -1) const;
void getEntities(std::vector<GEntity *> &entities,
const std::vector<std::pair<int, int>> &dimTags) 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);
这组方法对应脚本/API 里最常见的"按维度/按 (dim,tag)/按包围盒取实体"。 getBoundaryTags 尤其重要------它对应脚本命令 Boundary { ... }(第 3 章 token tRecursive/tCombined),combined/oriented/recursive 三个开关直接来自命令参数。 它是"从面找到它的边、从边找到它的点"的标准入口。
1.20.4 网格边/面表的实际用法
第 1.5.6 节列了 createMEdges/addMEdge 等。实际用法是:
cpp
// createMEdges / createMFaces 遍历网格,把所有出现的边/面加入全局表
m->createMEdges(); // 对应脚本 CreateTopology / CreateMeshEdges
std::size_t n = m->getNumMEdges();
m->reserveMEdges(1000); // 预计再加 1000 条
std::size_t id = m->addMEdge(MEdge(v0, v1)); // num==0 → 自动编号
std::size_t found = m->getMEdge(v0, v1, e); // 查现有边(0 = 不存在)
这套表让"两个单元共享哪条边"可 O(1) 回答------第 6 章边界恢复、第 7 章分区都依赖它。
1.20.5 MSH 文件读写的实现签名
GModel 私有区声明了所有 MSH 读写器(第 72--98 行),签名揭示了许多可选项:
cpp
int _readMSH2(const std::string &name);
int _writeMSH2(const std::string &name, double version, bool binary,
bool saveAll, bool saveParametric, double scalingFactor,
int elementStartNum, int saveSinglePartition, bool append,
bool renumberVertices);
int _writePartitionedMSH2(const std::string &baseName, bool binary, ...);
int _readMSH3(const std::string &name);
int _writeMSH3(...);
int _writePartitionedMSH3(...);
int _readMSH4(const std::string &name);
int _writeMSH4(const std::string &name, double version, bool binary,
bool saveAll, bool saveParametric, double scalingFactor,
bool append, const std::vector<int> &partitionsToSave = {},
std::map<GEntity *, SBoundingBox3d> *entityBounds = nullptr);
int _writePartitionedMSH4(...);
这些参数逐一对应 MSH 文件的选项:
binary:文本还是二进制;saveAll:是否保存不属于任何物理组的元素;saveParametric:是否保存参数坐标(u,v)(配合第 1.11.4 节的参数顶点);scalingFactor:坐标缩放;elementStartNum:起始单元编号;saveSinglePartition:只保存某个分区;append:追加而非覆盖;renumberVertices:写出前重编号;partitionsToSave、entityBounds(MSH4 专属):保存哪些分区、每个实体的包围盒。
这份参数表本身就是 MSH 导出功能的完整清单。第 2 章会把它们与格式细节对应起来。
1.20.6 物理组相关
cpp
bool noPhysicalGroups();
void getPhysicalGroups(std::map<int, std::vector<GEntity *>> groups[4]) const;
void getEntitiesForPhysicalName(const std::string &name, std::vector<GEntity *> &entities) const;
int getMaxPhysicalNumber(int dim);
int getMaxElementaryNumber(int dim); // dim<0 表示所有维度
noPhysicalGroups() 与导出行为直接相关:若没有物理组,Gmsh 默认导出全部 元素;若有, 则只导出属于物理组的(t1.geo 注释确认)。
1.21 读码建议与常见误区
给第一次读 Gmsh 源码的人几条能省时间的经验:
1.21.1 常见误区
- 误以为 tag 全局唯一 。tag 只在维度内唯一 :点的 tag 1 与面的 tag 1 是两个不同实体。 因此
(dim, tag)才是真正的标识(第 2、3 章的dimTags、GeoEntity语法都基于此)。 - 误以为只有一个编号 。
MVertex/MElement都有"不可变_num+ 可变_index"两个 编号(第 1.9.1、1.11.1 节)。混用会出错:重编号后_index可能变,_num不变。 - 误以为
_map缓存可直接改 。_vertexMapCache/_elementMapCache是缓存,改网格后必须destroyMeshCaches(),否则读到陈旧指针(第 1.20.2 节)。 - 误以为几何与网格独立 。
GEntity直接持有网格;改几何必须让网格失效(第 4 章降维删除)。 - 忽略
HAVE_*条件编译 。同一个函数名可能有两套实现(如三维 Delaunay 的 flat 与对象式 内核,第 6 章)。搜索时要注意#if defined(HAVE_XXX)。
1.21.2 高效定位代码
- 从脚本命令反查 :任何脚本命令都能在 src/parser/Gmsh.l 找到 token,再到 src/parser/Gmsh.y 找语义动作,最后跟到内核/实体。
- 从 API 反查 :在 api/gmsh.h 找函数,再到 src/common/gmsh.cpp 找实现,最后到
GmshXxx。 - 从选项反查 :在 src/common/Options.h 搜
opt_<category>_<name>, 再到Options.cpp看它读写哪个CTX字段。 - 从实体反查网格 :
GEntity::mesh→gmshFace::mesh/OCCFace→meshGFace/meshGRegion。 - 从算法反查入口 :
algo2d/algo3d的枚举(src/common/GmshDefines.h) → 第 5/6 章列出的入口函数表。
1.21.3 全系列术语约定
本章定义、后续沿用的术语:
- 维度(dim):0=点、1=边、2=面、3=体;
- tag:实体在其维度内的编号;
(dim, tag)寻址:跨维度的唯一标识;- 族别(family) :
TYPE_*,几何形状类别(三角形/四面体......); - 阶(order):多项式阶;
- Serendipity:不完备高阶族(无面/体内部节点);
- 参数化契约 :
parBounds/point/firstDer/normal/reparamOnFace; - 参数顶点 :携带
(u,v)的MEdgeVertex/MFaceVertex。
1.22 关键类型速查表
把本章出现的核心类型做成一张速查表,便于回头查阅:
| 类型 | 文件 | 一句话职责 |
|---|---|---|
GModel |
src/geo/GModel.h |
模型的容器:四维实体集合、内核句柄、缓存、编号 |
GEntity |
src/geo/GEntity.h |
几何实体基类:几何契约 + 网格容器 + 周期/物理组 |
GVertex/GEdge/GFace/GRegion |
src/geo/G*.h |
四个维度的实体基类 |
gmshVertex/gmshEdge/gmshFace/gmshRegion |
src/geo/gmsh*.cpp |
自研 GEO 内核实体的适配 |
OCCVertex/OCCEdge/OCCFace/OCCRegion |
src/geo/OCC*.cpp |
OpenCASCADE 实体的适配 |
discrete*/partition*/ghost*/overlap* |
src/geo/*.cpp |
离散/分区/影子/重叠实体 |
MElement |
src/geo/MElement.h |
网格单元基类(多语言节点排序、高阶节点分类) |
MVertex/MEdgeVertex/MFaceVertex |
src/geo/MVertex.h |
网格节点(可选参数 (u,v)) |
ElementType |
src/numeric/ElementType.h |
MSH_* ↔ TYPE_*+阶+族 的父子查询 |
GEOModel 内核 |
src/geo/GModelIO_GEO.h |
GEO_Internals:自研内核状态 |
OCC_Internals |
src/geo/GModelIO_OCC.h |
OpenCASCADE 内核状态 |
Context |
src/common/Context.h |
全局单例 CTX::instance():参数 + 变更时间戳 |
Options |
src/common/Options.h |
opt_* 存取函数 + 位掩码 action |
Msg |
src/common/GmshMessage.h |
日志/进度/错误计数/Onelab 通信中枢 |
FieldManager |
src/mesh/Field.h |
尺寸场系统(第 4 章) |
PView |
src/post/PView.h |
后处理视图(第 9 章) |
GMSH_Plugin |
src/plugin/Plugin.h |
插件基类(第 9 章) |
FunctionSpace<T> |
src/solver/functionSpace.h |
有限元空间(第 10 章) |
1.22.1 线程安全小结
本章多处涉及并发,这里做个小结,为后续章节铺路:
- 编号是线程安全的 :
incrementAndGetMaxVertexNumber用#pragma omp atomic capture(第 1.5.4 节); - 顺序不确定由重编号修正 :
renumberMeshCanonically(第 1.5.5 节); - 全局状态不是线程安全的 :
CTX的参数、Msg的错误计数都是共享可变的,因此网格生成 用CTX::instance()->lock串行化整段过程(第 1.12.3 节); - 缓存不是线程安全的 :改网格后必须
destroyMeshCaches(第 1.20.2 节); - 具体算法有各自的并行约束:边界层、拉伸、周期面会退化单线程(第 4 章),MMG3D 完全 不可并发(第 6 章)。
理解了这五点,就能解释后续章节里所有"为什么这里要加锁 / 为什么这里退化为单线程"的疑问。
1.22.2 本章涉及的关键常量
| 常量 | 值 | 定义处 | 含义 |
|---|---|---|---|
MAX_LC |
1.e22 |
GEntity.h |
尺寸"无穷大"哨兵(未设约束) |
TYPE_MAX_NUM |
13 | GmshDefines.h |
单元族最大值 |
MSH_MAX_NUM |
140 | GmshDefines.h |
MSH 单元类型最大值 |
GMSH_PLUGIN_API_VERSION |
2 | Plugin.h |
插件二进制 ABI 版本 |
MAX_RECUR_TESTS/MAX_RECUR_LOOPS |
100 | Gmsh.y |
脚本嵌套分支/循环上限(第 3 章) |
GMSH_MAJOR/MINOR/PATCH |
5/0/0 | CMakeLists.txt |
版本号 |
VAL_INF |
1.e200 |
PViewData.h |
后处理值的"无穷"哨兵(第 9 章) |
INV_24/INV_60/INV_90 |
倒数 | qualityMeasures.cpp |
质量度量中的归一化因子(第 7 章) |
这些"哨兵值 / 上限 / 版本"看似零碎,却常是理解边界行为的钥匙:例如看到 1.e22 就知道 "这是未设尺寸",看到 1.e200 就知道"这是后处理里未定义的值"。
同样地,GMSH_PLUGIN_API_VERSION = 2 也是一个关键线索:它意味着共享库插件的 ABI 在版本 2 发生过一次不兼容变更,任何用旧版本头文件编译的插件在新版 Gmsh 里都会被拒绝加载(第 9 章)。 而 MAX_RECUR_TESTS/MAX_RECUR_LOOPS = 100 则提示:脚本里的嵌套 If/For 深度是有硬上限 的,超过就会报错------这是第 3 章"循环靠回读实现"这一朴素设计的直接后果。
把这些常量记住,读后续章节时遇到它们即可立刻定位语义,不必每次都回到定义处确认。
1.23 本章小结与后续地图
把本章建立的坐标系浓缩成七点:
- 模块边界 = 目录边界 ,依赖从
numeric/common单向流向geo → mesh → post/solver → graphics/fltk → api;geo是核心枢纽(同时持有几何与网格),numeric是横切层。 - 构建以选项为骨架 :
opt()宏一箭双雕(开关 + 文档),ENABLE_*决定HAVE_*条件 编译;三种构建形态(单体/动态库/WASM)。 GModel是"尚未非流形的 B-Rep"容器 :四个维度的GEntity集合、四内核句柄、 惰性缓存(顶点/单元、八叉树、曲率)、原子编号 + 规范重编号保证并行可复现性。GEntity是双层契约 :几何契约(参数域/求点/切向/法向/周期/退化)+ 网格契约 (mesh()多态分派、节点/单元访问);派生类按"内核或工程态"划分。MElement/MVertex只关心拓扑与几何 :不可变_num+ 可变_index、多语言节点排序、 高阶节点分类;参数顶点(MEdgeVertex/MFaceVertex)携带(u,v)以支持参数化网格。- 两套单元编号 + 父子查询 :
MSH_*(磁盘/API 协议)与TYPE_*(算法族),ElementType提供父子双向映射;"父类型 + 阶数 + 族别"是数值内核寻址三元组。 - API 是生成的薄壳 :一份规格发射五语言;全局单例
CTX提供参数与变更时间戳,Options用位掩码统一读写,Msg集中日志/进度,Onelab负责外部协作。
接下来的路线:
- 第 2 章 下钻几何层:
GVertex/GEdge/GFace/GRegion的继承谱系、GEO 与 OCC 双内核细节、 布尔/拉伸/变换、拓扑构建(从网格反推拓扑)。 - 第 3 章 讲脚本层:
Gmsh.l/Gmsh.y如何定义 GEO 语言,表达式、变量、结构体、控制流、 宏如何工作。 - 第 4 章 进入网格流水线:
GenerateMesh如何分阶段调度,尺寸场系统如何组织。 - 第 5--10 章依次覆盖二维算法、三维算法、质量与高级技术、数值内核、后处理/插件、求解器/API。
每一章都会沿用本章建立的术语(维度、tag、(dim,tag) 寻址、族别、参数化契约),不再重复 定义。
最后给出一张"贯穿全系列的数据流"图作为本章的收束,后续九章都在这条链上工作:
text
.geo / .py 脚本 API 调用 文件导入(STEP/STL/MSH...)
│ │ │
└───────────┬───────────┴───────────┬────────────┘
▼
┌───────────────────┐ parser(第 3 章)
│ Gmsh.y 语义动作 │──────────────┐
└───────────────────┘ │
│ ▼
│ ┌───────────────────────┐
│ │ 内核 GEO_Internals / │ 第 2 章
│ │ OCC_Internals(Tree_T)│
│ └───────────┬───────────┘
│ │ 同步
▼ ▼
┌───────────────────────────────────────────────┐
│ GModel:vertices/edges/faces/regions 的 GEntity │ 第 1 章
│ 每个 GEntity 持有 MVertex / MElement │
└───────────────────────┬───────────────────────┘
│ GenerateMesh(第 4 章)
▼
┌───────────────────────────────────────────────┐
│ 网格:1D 边(第 4--5 章)/ 2D 面(第 5 章)/ │
│ 3D 体(第 6 章)/ 质量优化(第 7 章) │
└───────────────────────┬───────────────────────┘
│
┌─────────────┬───────────┴───────────┬──────────────┐
▼ ▼ ▼ ▼
后处理视图 内置求解器 导出文件 图形/GUI
(第 9 章) (第 10 章) (MSH/VTK/...) (graphics/fltk)
▼
对外 API 与多语言绑定(第 10 章)
掌握了这张图,你就能在任意时刻回答"我现在在哪一层、下一步会调用谁"。这是本系列最重要的 收获。