Skip to content

第18章 STEP 导入与几何分割


18.1 学习目标

  • 理解 STEP 格式在 CAD/CAM 工业中的核心地位(ISO 10303 标准),以及 Gmsh 通过 OpenCASCADE 内核导入 STEP 的完整流程
  • 掌握 occ::importShapes()gmsh::merge() 的本质区别:前者可返回最高维实体标签,便于后续程序化操作
  • 学会使用 Geometry.OCCTargetUnit 选项在导入时自动转换几何单位(毫米/米/英寸等)
  • 掌握几何分割(Geometry Partitioning)的完整流程:包围盒计算 -> 创建切割面 -> 空间变换(旋转+平移) -> copy + translate 批量生成 -> fragment 共形布尔运算 -> remove 清理
  • 理解 removeEntities() 在模型层面的实体删除以及 surf=true 仅保留剖面的应用场景
  • 了解几何分割在 CAE 前处理中的实际价值:大模型分区域并行网格划分、CAD 特征的区域提取

18.2 核心概念说明

18.2.1 STEP 格式与 ISO 10303

STEP(Standard for the Exchange of Product model data,产品模型数据交换标准)是 ISO 10303 系列国际标准定义的中性文件格式,是 CAD/CAM/CAE 领域最重要、最通用的几何数据交换格式。几乎所有主流 CAD 软件(CATIA、SolidWorks、NX、Creo、FreeCAD)都原生支持 STEP 导入/导出。

特性 说明
标准编号 ISO 10303(常称为 STEP)
常见扩展名 .step, .stp, .stpnc
几何内核 基于边界表示法(B-rep, Boundary Representation)
数据类型 AP203(机械零件)、AP214(汽车工业)、AP242(统一标准)
内容层级 实体(Solid)、壳体(Shell)、面(Face)、边(Edge)、顶点(Vertex)

在 CAE 前处理中的角色:CAD 工程师在 CAD 系统中完成几何建模后,导出为 STEP 文件;CAE 工程师在 Gmsh 中导入 STEP 进行网格生成。STEP 是连接 CAD 和 CAE 的桥梁。

18.2.2 importShapes() vs merge() 的核心区别

Gmsh 提供了两种导入外部几何的方式:

occ::importShapes("file.step", v)      → 返回 (dim, tag) 列表,可直接操作
gmsh::merge("file.step")               → 仅合并数据,不返回实体标签
特性 occ::importShapes() gmsh::merge()
所属模块 gmsh::model::occ:: gmsh::(全局)
返回值 vector<pair<int,int>> 最高维实体标签 无返回值
后续操作 可直接用返回标签做切割/变换/分片 需通过 getEntities() 反查
适用场景 需要程序化修改几何 仅浏览或直接划分网格

为什么要用 importShapes 本章的核心任务是几何分割——需要对导入的体做包围盒查询、切割面和体进行 fragment 布尔运算。这些操作必须知道体和面的标签。

实际代码对比:

// 方案A: importShapes —— 推荐用于几何操作场景
std::vector<std::pair<int, int>> v;
gmsh::model::occ::importShapes("part.step", v);
// v[0].first = 3, v[0].second = 1  → 导入的体(dim=3, tag=1)
// 现在可以直接用 v 做后续操作

// 方案B: merge —— 适合仅查看/直接网格化
gmsh::merge("part.step");
// 没有返回标签,需要用 getEntities() 反查所有3D实体
std::vector<std::pair<int, int>> entities;
gmsh::model::getEntities(entities, 3);  // 多一步

18.2.3 Geometry.OCCTargetUnit 单位转换

CAD 系统使用不同的长度单位:机械行业多用毫米(mm),建筑行业多用米(m),芯片设计用微米(um)。OpenCASCADE 默认假设 STEP 数据的单位是毫米,但可以通过 Geometry.OCCTargetUnit 选项指定目标单位:

// 在 importShapes 之前设置目标单位
gmsh::option::setString("Geometry.OCCTargetUnit", "M");   // 转换为米
gmsh::option::setString("Geometry.OCCTargetUnit", "MM");  // 保持毫米(默认)
gmsh::option::setString("Geometry.OCCTargetUnit", "CM");  // 转换为厘米
gmsh::option::setString("Geometry.OCCTargetUnit", "UM");  // 转换为微米

重要:此选项必须在 importShapes()merge() 调用之前设置,OpenCASCADE 会在导入时执行单位缩放。

18.2.4 几何分割的整体流程

本章演示的几何分割(Geometry Partitioning)是将一个完整的 CAD 体沿某个方向切成 N 片的过程,其核心流程如下:

                          ┌──────────────┐
                          │ 1. 导入 STEP │  importShapes()
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 2. 获取包围盒│  getBoundingBox()
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 3. 创建切割面│  addRectangle() → rotate() → translate()
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 4. 批量生成  │  copy() + translate() (N-2次)
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 5. 共形相交  │  fragment(体, 切割面列表)
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 6. 清理残余  │  remove(多余的2D实体)
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │7. 可选:仅剖面│  getEntitiesInBoundingBox → removeEntities
                          └──────┬───────┘
                                 │
                          ┌──────▼───────┐
                          │ 8. 划分网格  │  mesh::generate()
                          └──────────────┘

18.3 C++ 代码逐段讲解

18.3.1 初始化与 STEP 导入

#include <set>
#include <cmath>
#include <cstdlib>
#include <algorithm>
#include <gmsh.h>

int main(int argc, char **argv)
{
  gmsh::initialize(argc, argv);
  gmsh::model::add("ch18_step_partition");

  // ---- 导入 STEP 文件 ----
  // importShapes 返回最高维实体的 (dim, tag) 列表
  std::vector<std::pair<int, int>> v;
  try {
    gmsh::model::occ::importShapes("../t20_data.step", v);
  } catch(...) {
    gmsh::logger::write("无法加载 STEP 文件,程序退出。");
    gmsh::finalize();
    return 0;
  }

解释occ::importShapes() 是 OpenCASCADE 模块提供的 STEP 导入函数。第一个参数是文件路径;第二个参数 v 是输出——导入后几何中最高维实体的 (dim, tag) 列表。对于含有一个完整实体的 STEP 文件,v 通常只有一项,如 {(3, 1)},表示维度为 3、标签为 1 的体。

使用 try-catch 包裹导入操作是一个好习惯:STEP 文件可能损坏、不存在,或包含 OpenCASCADE 无法识别的几何特征(如某些样条曲面类型),捕获异常后优雅退出。

18.3.2 设置单位转换(可选)

  // ---- 可选:设置目标单位 ----
  // 如果 STEP 文件包含英制尺寸,可用此行转换为公制毫米:
  // gmsh::option::setString("Geometry.OCCTargetUnit", "MM");
  //
  // 如果希望输出为米(建筑/土木常用),可设置为 "M":
  // gmsh::option::setString("Geometry.OCCTargetUnit", "M");
  //
  // 注意:此选项必须在 importShapes() 之前设置才能生效。
  // 支持的字符串值: "M"(米), "MM"(毫米,默认), "CM"(厘米), "UM"(微米), "IN"(英寸)

解释Geometry.OCCTargetUnit 控制 OpenCASCADE 导入时的单位缩放。默认值是 "MM"(毫米)。如果你的 STEP 文件是其他单位(例如英制零件以英寸为基准),设置此项后 OpenCASCADE 会自动将数值乘以换算系数。此选项对后续所有几何操作透明——设置后即可按照统一的单位体系进行包围盒计算、切割面定位等。

18.3.3 获取包围盒

  // ---- 获取体的包围盒(Bounding Box) ----
  double xmin, ymin, zmin, xmax, ymax, zmax;
  gmsh::model::occ::getBoundingBox(v[0].first, v[0].second,
                                    xmin, ymin, zmin,
                                    xmax, ymax, zmax);

解释occ::getBoundingBox(dim, tag, xmin, ymin, zmin, xmax, ymax, zmax) 返回指定实体的轴对齐包围盒(Axis-Aligned Bounding Box, AABB)。v[0] 是导入体的 (dim=3, tag=1);六个 double 引用参数用于输出包围盒的极小点和极大点坐标。

包围盒是后续所有操作的基线数据——切割面的大小、偏移量都需要基于这些坐标计算。AABB 是沿全局坐标系各轴的最小/最大值,因此对于摆放方向不规则(旋转放置)的零件,包围盒会偏大。

18.3.4 设定切割参数

  // ---- 切割参数设定 ----
  int N = 5;                // 切割片数(切成N片,需要N-1个切割面)
  std::string dir = "X";    // 切割方向:"X"、"Y" 或 "Z"
  bool surf = false;        // false=保留体积切片, true=仅保留剖面

  double dx = (xmax - xmin);
  double dy = (ymax - ymin);
  double dz = (zmax - zmin);

  // 切割面的尺寸:矩形在切割方向上的法向面内定义
  double L = (dir == "X") ? dz : dx;   // 切割面的第一轴向尺寸
  double H = (dir == "Y") ? dz : dy;   // 切割面的第二轴向尺寸

解释N=5 表示将体切割成 5 片,需要 4 个切割面(分别位于总长度的 1/5、2/5、3/5、4/5 处)。

dir 指定切割方向。若选择 "X",即沿 X 轴方向切,则切割面应平行于 YZ 平面。为此,我们在 XY 平面创建一个矩形的切割面,然后将其旋转到与切割方向垂直的姿态: - "X" 方向切:切割面初始在 XY 平面,沿 Y 轴旋转 -90 度使其平行于 YZ 平面 - "Y" 方向切:切割面初始在 XY 平面,沿 X 轴旋转 +90 度使其平行于 XZ 平面 - "Z" 方向切:切割面无需旋转,保持在 XY 平面

LH 是切割面在自身平面内的两个尺寸,分别取包围盒在非切割方向上的跨度,确保切割面能完全覆盖体的截面。

surf 标志控制最终输出:false 时保留切割产生的所有体积片段(如共轭换热分析中需要分别对流体域和固体域划分网格);true 时仅保留各个切割面位置上的剖面(如用于展示内部截面或渐进式切片可视化)。

18.3.5 创建第一个切割面

  // ---- 创建第一个切割面 ----
  // addRectangle 在 XY 平面创建矩形面(z=zmin处)
  std::vector<std::pair<int, int>> s;
  s.push_back({2, gmsh::model::occ::addRectangle(xmin, ymin, zmin, L, H)});

  // 将切割面旋转到与切割方向垂直的平面
  if(dir == "X") {
    // 绕 Y 轴旋转 -90 度:XY平面 → YZ平面
    gmsh::model::occ::rotate({s[0]}, xmin, ymin, zmin, 0, 1, 0, -M_PI / 2);
  }
  else if(dir == "Y") {
    // 绕 X 轴旋转 +90 度:XY平面 → XZ平面
    gmsh::model::occ::rotate({s[0]}, xmin, ymin, zmin, 1, 0, 0, M_PI / 2);
  }
  // dir=="Z" 时无需旋转,保持 XY 平面即可

  // 将切割面平移到第一个切割位置
  double tx = (dir == "X") ? dx / N : 0;
  double ty = (dir == "Y") ? dy / N : 0;
  double tz = (dir == "Z") ? dz / N : 0;
  gmsh::model::occ::translate({s[0]}, tx, ty, tz);

解释:这段代码展示了 OpenCASCADE 几何建模中"创建 -> 变换(旋转+平移)"的标准模式。

  1. occ::addRectangle(x, y, z, dx, dy) 在 XY 平面创建一个矩形面,返回其标签。注意这个矩形在其局部坐标中是一个平面面(Planar Face)。
  2. occ::rotate({entities}, ax, ay, az, angle) 将指定实体绕通过点 (xmin, ymin, zmin)、方向为 (0, 1, 0) 的轴旋转 -PI/2 弧度。第一个参数是实体标签列表。旋转轴的方向向量 (ax, ay, az) 自动归一化。
  3. occ::translate({entities}, dx, dy, dz) 将实体平移。这里将切割面移到体内部的第一个切割位置(对于 N=5 且方向为 X,tx = dx/5)。

关键设计思想:先创建一个足够大的面,再通过旋转+平移将其放到正确的位置。这比手工计算三维空间中的精确坐标更简洁可靠。

18.3.6 批量生成切割面

  // ---- 批量生成其余切割面(N-2个) ----
  // 第0个切割面已是第一个切割位置,再创建第2到第N-1个(共N-1个切割面)
  std::vector<std::pair<int, int>> tmp;
  for(int i = 1; i < N - 1; i++) {
    gmsh::model::occ::copy({s[0]}, tmp);       // 复制第一个切割面
    s.push_back(tmp[0]);                         // 加入切割面列表
    gmsh::model::occ::translate({s.back()},     // 平移到第i个切割位置
                                i * tx, i * ty, i * tz);
  }

解释:循环从 i=1i=N-2(共 N-2 次迭代),与已有的第一个切割面一起组成 N-1 个切割面。

  1. occ::copy({src}, dst) 深拷贝源实体。dst 输出新实体的标签列表。复制产生的实体与源实体几何完全相同,但拥有独立的标签。
  2. 将新标签追加到 s 列表,便于后续的 fragment 操作。
  3. translate({s.back()}, i*tx, i*ty, i*tz) 将新产生的切割面平移到对应的切割位置。偏移量随 i 递增:tx*(1), tx*(2), ... 对应于总长的 2/N, 3/N, ..., (N-1)/N。

为什么用 copy + translate 而不是 addRectangle + rotate + translate? 因为第一个切割面已经完成了 addRectangle 和 rotate 操作,copy 产生一个完全相同的几何面,省去了重复的矩形构建和角度旋转。

18.3.7 Fragment:共形布尔分片运算

  // ---- Fragment 分片运算 ----
  // fragment 将体与所有切割面共形相交
  std::vector<std::pair<int, int>> ov;            // 输出: 分片后的所有体
  std::vector<std::vector<std::pair<int, int>>> ovv;  // 输出: 每个工具体产生的碎片
  gmsh::model::occ::fragment(v, s, ov, ovv);

解释occ::fragment(objects, tools, ov, ovv) 是 OpenCASCADE 中最强大的布尔运算之一——共形分片(Conformal Fragmentation)。它的行为是:

  • objects 列表中的每个实体,用 tools 列表中的所有实体进行切割
  • 结果是 objects 中的每个体被切割面切成了多个子体积
  • 切割面的实体也会被保留在结果中(作为面嵌入到各子体的边界上)

与普通布尔运算(cut/fuse/intersect)不同,fragment 保证了共形性(Conformality):切割面与体相交后,切割面上的交线在相邻子体的边界上是完全一致的,不存在几何间隙或重叠。这是因为 fragment 将所有参与实体作为整体进行拓扑运算,确保交界面被共享。

结果参数: - ov:分片后生成的所有最高维实体(体或面) - ovv:嵌套列表,对应每个 object 被分割后产生的碎片——ovv[i]v[i] 被切割后的子实体列表

以上面 5 片分割为例,ov 将包含 5 个体积碎片。(实际数量取决于 STEP 文件中体的数量——如果文件包含多个体,每个体都要被切割。)

18.3.8 清理多余的切割面残余

  // ---- 清理切割面在体外部"伸出"的部分 ----
  // fragment 之后,切割面上只有与体相交的部分被保留为体的边界
  // 但 OpenCASCADE 可能残留一些面实体需要清理
  gmsh::model::occ::getEntities(tmp, 2);     // 获取所有2D实体(面)
  gmsh::model::occ::remove(tmp, true);       // 递归删除不在体边界上的面

解释:fragment 运算后,切割面被分割成两类: 1. 与体内部相交的部分——变成了相邻子体积的共享边界面 2. 在体外部"伸出"的部分——这些面对体的拓扑不再有意义,需要清理

occ::remove({entities}, true) 中的 true 表示递归(recursive)删除。递归删除意味着:如果一个面被删除,那么只有该面独享的边和顶点也会被一并删除(体边界上与其他面共享的边和点则保留)。

occ::synchronize() 将 OpenCASCADE 内核中的拓扑变化同步到 Gmsh 的模型数据结构中,后续的网格划分和模型查询操作才能正确反映这些变化。

18.3.9 surf=true:仅保留剖面

  if(surf) {
    // ---- 模式2: 仅保留剖面(丢弃体) ----
    double eps = 1e-4;
    std::vector<std::pair<int, int>> s_surf;    // 收集剖面标签

    // 在各切割位置处用包围盒搜索剖面
    for(int i = 1; i < N; i++) {
      // 构造搜索包围盒:每个切割位置处一个薄片状的包围盒
      double xx = (dir == "X") ? xmin : xmax;
      double yy = (dir == "Y") ? ymin : ymax;
      double zz = (dir == "Z") ? zmin : zmax;

      std::vector<std::pair<int, int>> e;
      gmsh::model::getEntitiesInBoundingBox(
        xmin - eps + i * tx, ymin - eps + i * ty, zmin - eps + i * tz,
        xx + eps + i * tx,   yy + eps + i * ty,   zz + eps + i * tz,
        e, 2);  // 搜索维度2(面)
      s_surf.insert(s_surf.end(), e.begin(), e.end());
    }

解释:此段代码进入"仅保留剖面"模式——丢掉所有体积实体,只保留切割位置处体的内部截面。

核心技巧是用 getEntitiesInBoundingBox() 在各切割位置做一个空间查询: - 对于 dir="X",搜索范围为 [xmin + i*tx, xmax + i*tx] (X 方向从最小到最大跨度,但位于切割平面位置附近),Y 和 Z 方向只取 ±eps 的薄片 - eps 设为 1e-4,保证能捕获到所有剖面上的面实体,同时避免误捕其他位置的面 - 最后一个参数 2 表示只搜索维度为 2 的实体(面)

对于 N=5 的情况,循环 i=1i=4,在 4 个切割位置各搜一次,收集到所有剖面上的面。

    // ---- 删除其余所有实体(保留剖面) ----
    std::vector<std::pair<int, int>> dels;
    gmsh::model::getEntities(dels, 2);          // 所有面

    // 从删除列表中移除要保留的剖面(集合差运算)
    for(auto it = s_surf.begin(); it != s_surf.end(); ++it) {
      auto it2 = std::find(dels.begin(), dels.end(), *it);
      if(it2 != dels.end()) dels.erase(it2);
    }

    // 按维度从高到低依次删除:先删体,再删面,再删边,最后删点
    gmsh::model::getEntities(tmp, 3);
    gmsh::model::removeEntities(tmp);    // 删除所有3D体
    gmsh::model::removeEntities(dels);   // 删除不留的面
    gmsh::model::getEntities(tmp, 1);
    gmsh::model::removeEntities(tmp);    // 删除所有1D边
    gmsh::model::getEntities(tmp, 0);
    gmsh::model::removeEntities(tmp);    // 删除所有0D点
  }

解释:删除流程分为两个阶段:

阶段1 — 构造"白名单":先从所有2D实体中找出要保留的剖面,通过 std::find + erase 构造差集 dels = {所有面} \ {剖面}

阶段2 — 逐维度删除:删除顺序必须从高维到低维(3D -> 2D -> 1D -> 0D)。因为低维实体可能作为高维实体的边界存在——如果先删除低维实体,高维实体的拓扑会变得不一致。removeEntities() 在模型层面(而非 OpenCASCADE 内核层面)删除实体,适用于不再需要对几何进行进一步 CAD 操作(变换、布尔等)的场景。

此时模型只剩下 N-1 个切割位置的剖面面。这些面可以直接用于二维网格划分,也可以作为可视化截面或导出为 IGES/STEP 供其他工具使用。

18.3.10 网格划分与输出

  // ---- 网格划分 ----
  gmsh::option::setNumber("Mesh.MeshSizeMin", 3);
  gmsh::option::setNumber("Mesh.MeshSizeMax", 3);
  gmsh::model::mesh::generate(3);           // 生成三维网格
  gmsh::write("ch18_step_partition.msh");   // 输出为 .msh 文件

  // ---- 启动 GUI 查看结果 ----
  std::set<std::string> args(argv, argv + argc);
  if(!args.count("-nopopup")) gmsh::fltk::run();

  gmsh::finalize();
  return 0;
}

解释Mesh.MeshSizeMinMesh.MeshSizeMax 设为相同的值 3,表示均匀网格尺寸(各单元边长约 3),适用于演示目的。实际生产使用中,通常用第8章介绍的尺寸场(Field)来指定空间变化的网格尺寸。

mesh::generate(3) 生成三维网格。对于 surf=false 模式(保留体积切片),生成的是四面体网格;对于 surf=true 模式(仅保留剖面),虽然调用的是 generate(3),但由于模型中没有三维实体了,Gmsh 会自动退化为二维三角形网格生成。

对称切割面的网格一致性:fragment 保证了相邻子体积共享切割面,但不能保证这些共享面上的网格一致。如果需要在切割面上有节点一一对应的共形网格(例如用于域分解方法 /DDM),参见第15章的 setPeriodic 或第21章(t21.cpp/t21.geo)的 PartitionMesh


18.4 面向 CAE 的实际应用场景

18.4.1 大模型分区域并行网格划分

对于超大 CAD 模型(如上万零部件的航空发动机、整车白车身),单次全局网格划分在内存和时间上都不可行。可采用以下策略:

  1. 将 STEP 模型导入 Gmsh
  2. 沿多个方向切割成 M x N x P 个子区域
  3. 对每个子区域独立(在分布式节点上)生成网格
  4. 利用 fragment 的共形性保证相邻子区域交界面的拓扑一致性

这比直接对整机划分能成倍降低单节点内存需求,且天然支持并行化。

18.4.2 CAD 特征的区域提取

有时只需要对零件的某个局部区域生成网格(例如只关心应力集中区域的孔洞周围区域)。通过以下流程可提取感兴趣的子区域:

  1. 导入 STEP
  2. 用包围盒定位感兴趣区域
  3. 创建多个切割面围成"截取窗口"
  4. fragment + remove 得到目标子区域
  5. 仅对该子区域划分精细网格

18.4.3 多材料/多物理场区域划分

在共轭换热(CHT, Conjugate Heat Transfer)分析中,固体域和流体域的界面需要共形网格。可以:

  1. 先后导入固体和流体的 STEP 文件到同一模型
  2. 用 fragment 确保界面处的拓扑一致性
  3. 对不同域设置不同的网格尺寸
  4. 导出时通过 Physical Group 区分材料区域

18.5 关键 API 速查表

API 说明
occ::importShapes(filename, out) 导入 STEP/IGES/BREP 文件,out 返回最高维实体 (dim,tag) 列表
option::setString("Geometry.OCCTargetUnit", unit) 设置导入单位:"M"/"MM"/"CM"/"UM"/"IN",须在 importShapes 前调用
occ::getBoundingBox(dim, tag, xmin, ymin, zmin, xmax, ymax, zmax) 获取实体的轴对齐包围盒(AABB)
occ::addRectangle(x, y, z, dx, dy) 在 XY 平面创建矩形面,返回标签
occ::rotate({entities}, cx, cy, cz, ax, ay, az, angle) 绕通过点 (cx,cy,cz)、方向为 (ax,ay,az) 的轴旋转实体
occ::translate({entities}, dx, dy, dz) 平移实体
occ::copy({source}, dest) 深拷贝实体,dest 输出新标签列表
occ::fragment(objects, tools, out, outMap) 共形分片:用 tools 切割 objects,保证共享边界的拓扑一致性
occ::remove({entities}, recursive) 删除 OpenCASCADE 实体,recursive=true 时级联删除其独享的低维边界
occ::synchronize() 将 OpenCASCADE 内核变更同步到 Gmsh 模型
model::getEntitiesInBoundingBox(xmin,ymin,zmin,xmax,ymax,zmax, out, dim) 空间查询:获取包围盒内的指定维度实体
model::removeEntities({entities}) 在模型层面递归删除实体(不再需要 CAD 操作时使用)

18.6 完整可运行代码

// -----------------------------------------------------------------------------
//  Chapter 18: STEP Import and Geometry Partitioning
//
//  功能: 导入 STEP 文件,沿指定方向将几何体切割为 N 片,
//        支持两种模式:(1) 保留体积切片 (2) 仅保留剖面
//
//  编译 (需链接 Gmsh SDK):
//    g++ -o ch18_step_partition ch18_step_partition.cpp -lgmsh
//
//  运行:
//    ./ch18_step_partition              (带 GUI 查看)
//    ./ch18_step_partition -nopopup     (仅生成 .msh 文件)
//
//  依赖文件:
//    t20_data.step  (Gmsh 教程提供的 STEP 示例文件)
// -----------------------------------------------------------------------------

#include <set>
#include <cmath>
#include <cstdlib>
#include <algorithm>
#include <gmsh.h>

int main(int argc, char **argv)
{
  gmsh::initialize(argc, argv);
  gmsh::model::add("ch18_step_partition");

  // ===== 1. 导入 STEP 文件 =====
  std::vector<std::pair<int, int>> v;
  try {
    gmsh::model::occ::importShapes("../t20_data.step", v);
  } catch(...) {
    gmsh::logger::write("无法加载 STEP 文件,程序退出。");
    gmsh::finalize();
    return 0;
  }

  // ===== 2. 获取包围盒 =====
  double xmin, ymin, zmin, xmax, ymax, zmax;
  gmsh::model::occ::getBoundingBox(v[0].first, v[0].second,
                                    xmin, ymin, zmin,
                                    xmax, ymax, zmax);

  // ===== 3. 切割参数 =====
  int         N    = 5;     // 切割片数
  std::string dir  = "X";   // 切割方向 "X" / "Y" / "Z"
  bool        surf = false; // false=保留体积切片, true=仅保留剖面

  double dx = xmax - xmin, dy = ymax - ymin, dz = zmax - zmin;
  double L  = (dir == "X") ? dz : dx;  // 切割面第一轴向尺寸
  double H  = (dir == "Y") ? dz : dy;  // 切割面第二轴向尺寸

  // ===== 4. 创建第一个切割面 =====
  std::vector<std::pair<int, int>> s;
  s.push_back({2, gmsh::model::occ::addRectangle(xmin, ymin, zmin, L, H)});

  if(dir == "X")
    gmsh::model::occ::rotate({s[0]}, xmin, ymin, zmin, 0, 1, 0, -M_PI / 2);
  else if(dir == "Y")
    gmsh::model::occ::rotate({s[0]}, xmin, ymin, zmin, 1, 0, 0,  M_PI / 2);

  double tx = (dir == "X") ? dx / N : 0;
  double ty = (dir == "Y") ? dy / N : 0;
  double tz = (dir == "Z") ? dz / N : 0;
  gmsh::model::occ::translate({s[0]}, tx, ty, tz);

  // ===== 5. 批量生成其余切割面 (N-2个) =====
  std::vector<std::pair<int, int>> tmp;
  for(int i = 1; i < N - 1; i++) {
    gmsh::model::occ::copy({s[0]}, tmp);
    s.push_back(tmp[0]);
    gmsh::model::occ::translate({s.back()}, i * tx, i * ty, i * tz);
  }

  // ===== 6. Fragment 共形分片 =====
  std::vector<std::pair<int, int>> ov;
  std::vector<std::vector<std::pair<int, int>>> ovv;
  gmsh::model::occ::fragment(v, s, ov, ovv);

  // ===== 7. 清理不在体边界上的残余面 =====
  gmsh::model::occ::getEntities(tmp, 2);
  gmsh::model::occ::remove(tmp, true);
  gmsh::model::occ::synchronize();

  // ===== 8. 可选:仅保留剖面 =====
  if(surf) {
    double eps = 1e-4;
    std::vector<std::pair<int, int>> s_surf;

    for(int i = 1; i < N; i++) {
      double xx = (dir == "X") ? xmin : xmax;
      double yy = (dir == "Y") ? ymin : ymax;
      double zz = (dir == "Z") ? zmin : zmax;

      std::vector<std::pair<int, int>> e;
      gmsh::model::getEntitiesInBoundingBox(
        xmin - eps + i * tx, ymin - eps + i * ty, zmin - eps + i * tz,
        xx   + eps + i * tx, yy   + eps + i * ty, zz   + eps + i * tz,
        e, 2);
      s_surf.insert(s_surf.end(), e.begin(), e.end());
    }

    // 从所有面中排除要保留的剖面
    std::vector<std::pair<int, int>> dels;
    gmsh::model::getEntities(dels, 2);
    for(auto it = s_surf.begin(); it != s_surf.end(); ++it) {
      auto it2 = std::find(dels.begin(), dels.end(), *it);
      if(it2 != dels.end()) dels.erase(it2);
    }

    // 按维度从高到低删除
    gmsh::model::getEntities(tmp, 3);
    gmsh::model::removeEntities(tmp);     // 删除所有体
    gmsh::model::removeEntities(dels);    // 删除不留的面
    gmsh::model::getEntities(tmp, 1);
    gmsh::model::removeEntities(tmp);     // 删除所有边
    gmsh::model::getEntities(tmp, 0);
    gmsh::model::removeEntities(tmp);     // 删除所有点
  }

  // ===== 9. 网格划分 =====
  gmsh::option::setNumber("Mesh.MeshSizeMin", 3);
  gmsh::option::setNumber("Mesh.MeshSizeMax", 3);
  gmsh::model::mesh::generate(3);
  gmsh::write("ch18_step_partition.msh");

  // ===== 10. 可选 GUI =====
  std::set<std::string> args(argv, argv + argc);
  if(!args.count("-nopopup")) gmsh::fltk::run();

  gmsh::finalize();
  return 0;
}

18.7 本章小结

  • STEP(ISO 10303)是 CAD/CAM/CAE 领域最通用的几何交换格式,Gmsh 通过 OpenCASCADE 内核原生支持导入。
  • occ::importShapes() 返回导入实体的 (dim, tag) 列表,是后续程序化几何操作(切割、变换、布尔运算)的基础;gmsh::merge() 仅负责合并数据,不返回标签。
  • Geometry.OCCTargetUnit 在导入时执行单位缩放,应在 importShapes() 调用前设置。
  • 几何分割的核心流程:getBoundingBox 获取基线尺寸 -> addRectangle + rotate + translate 创建并定位切割面 -> copy + translate 批量生成 -> fragment 执行共形分片 -> remove 清理残余。
  • fragment 保证相邻子区域交界面的拓扑一致性(共形性),是其区别于普通布尔运算的关键。
  • getEntitiesInBoundingBox 空间查询 + removeEntities 逐维度删除可实现仅保留剖面的后处理。
  • 几何分割的实际价值在于大模型并行网格划分、感兴趣子区域提取和多物理场区域划分。
  • 本章技术与第21章(t21.geo / 网格分区 Mesh Partitioning)互补:本章在几何层面切割体,第21章在网格层面对已生成的网格进行分区。

18.8 注意事项

  1. STEP 文件质量:部分 CAD 系统导出的 STEP 文件可能含有 OpenCASCADE 无法识别的几何元素(如某些样条类型),导入时会抛出异常,应使用 try-catch 保护。
  2. 包围盒精度getBoundingBox 返回的是轴对齐包围盒(AABB),对于斜放的零件可能偏大。如需更紧致的包围盒,需自行计算 OBB(Oriented Bounding Box,有向包围盒)。
  3. fragment 的计算复杂度:参与 fragment 的实体越多,布尔运算越慢。对于非常复杂的几何体(上千个面),建议先用 cutintersect 做局部处理,减少一次 fragment 中的实体数量。
  4. remove 与 removeEntities 的区别occ::remove 操作 OpenCASCADE 内核数据,后续仍可进行 CAD 变换;model::removeEntities 操作 Gmsh 模型数据,删除后不能再执行 CAD 操作。在 surf=true 模式下因不再需要进一步的几何操作,使用后者更直接。
  5. 切割面的网格一致性:fragment 保证了几何层面的共形性,但不保证网格层面上相邻子区域在共享面上的节点对齐。如需节点一一对应的共形网格,参见第15章的 setPeriodic 或第21章的 PartitionMesh
  6. 切割方向为 "X" 或 "Y" 时:切割面的初始创建位置在 z=zmin 的 XY 平面上,旋转中心在 (xmin, ymin, zmin),确保旋转后切割面能够覆盖整个截面。